你是否想知道LSTM層學到了什麼?有沒有想過是否有可能看到每個單元如何對最終輸出做出貢獻。我很好奇,試圖將其可視化。在滿足我好奇的神經元的同時,我偶然發現了Andrej Karpathy的博客,名為「循環神經網絡的不合理有效性」。如果你想獲得更深入的解釋,建議你瀏覽他的博客。
在本文中,我們不僅將在Keras中構建文本生成模型,還將可視化生成文本時某些單元格正在查看的內容。就像CNN一樣,它學習圖像的一般特徵,例如水平和垂直邊緣,線條,斑塊等。類似,在「文本生成」中,LSTM則學習特徵(例如空格,大寫字母,標點符號等)。 LSTM層學習每個單元中的特徵。
我們將使用Lewis Carroll的《愛麗絲夢遊仙境》一書作為訓練數據。該模型體系結構將是一個簡單的模型體系結構,在其末尾具有兩個LSTM和Dropout層以及一個Dense層。
你可以在此處下載訓練數據和訓練好的模型權重
https://github.com/Praneet9/Visualising-LSTM-Activations
這就是我們激活單個單元格的樣子。
讓我們深入研究代碼。
步驟1:導入所需的庫
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Dropout, CuDNNLSTM
from keras.callbacks import ModelCheckpoint
from keras.utils import np_utils
import re
# 可視化庫
from IPython.display import HTML as html_print
from IPython.display import display
import keras.backend as K
注意:我使用CuDNN-LSTM代替LSTM,因為它的訓練速度提高了15倍。CuDNN-LSTM由CuDNN支持,只能在GPU上運行。
步驟2:讀取訓練資料並進行預處理
使用正則表達式,我們將使用單個空格刪除多個空格。該char_to_int和int_to_char只是數字字符和字符數的映射。
# 讀取數據
filename = "wonderland.txt"
raw_text = open(filename, 'r', encoding='utf-8').read()
raw_text = re.sub(r'[ ]+', ' ', raw_text)
# 創建字符到整數的映射
chars = sorted(list(set(raw_text)))
char_to_int = dict((c, i) for i, c in enumerate(chars))
int_to_char = dict((i, c) for i, c in enumerate(chars))
n_chars = len(raw_text)
n_vocab = len(chars)
步驟3:準備訓練資料
準備我們的數據很重要,每個輸入都是一個字符序列,而輸出是後面的字符。
seq_length = 100
dataX = []
dataY = []
for i in range(0, n_chars - seq_length, 1):
seq_in = raw_text[i:i + seq_length]
seq_out = raw_text[i + seq_length]
dataX.append([char_to_int[char] for char in seq_in])
dataY.append(char_to_int[seq_out])
n_patterns = len(dataX)
print("Total Patterns: ", n_patterns)
X = np.reshape(dataX, (n_patterns, seq_length, 1))
# 標準化
X = X / float(n_vocab)
# one-hot編碼
y = np_utils.to_categorical(dataY)
filepath="weights-improvement-{epoch:02d}-{loss:.4f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor='loss', verbose=1, save_best_only=True, mode='min')
callbacks_list = [checkpoint]
步驟4:構建模型架構
# 定義 LSTM 模型
model = Sequential()
model.add(CuDNNLSTM(512, input_shape=(X.shape[1], X.shape[2]), return_sequences=True))
model.add(Dropout(0.5))
model.add(CuDNNLSTM(512))
model.add(Dropout(0.5))
model.add(Dense(y.shape[1], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.summary()
步驟5:訓練模型
model.fit(X, y, epochs=300, batch_size=2048, callbacks=callbacks_list)
使用Google Colab訓練模型時,我無法一口氣訓練模型300個epoch。我必須通過縮減權重數量並再次加載它們來進行3天的訓練,每天100個epoch
如果你擁有強大的GPU,則可以一次性訓練300個epoch的模型。如果你不這樣做,我建議你使用Colab,因為它是免費的。
你可以使用下面的代碼加載模型,並從最後一點開始訓練。
from keras.models import load_model
filename = "weights-improvement-303-0.2749_wonderland.hdf5"
model = load_model(filename)
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
# 用相同的數據訓練模型
model.fit(X, y, epochs=300, batch_size=2048, callbacks=callbacks_list)
現在到文章最重要的部分-可視化LSTM激活。我們將需要一些功能來實際使這些可視化變得可理解。
步驟6:後端功能以獲取中間層輸出
正如我們在上面的步驟4中看到的那樣,第一層和第三層是LSTM層。我們的目標是可視化第二LSTM層(即整個體系結構中的第三層)的輸出。
Keras Backend幫助我們創建一個函數,該函數接受輸入並為我們提供來自中間層的輸出。我們可以使用它來創建我們自己的管道功能。這裡attn_func將返回大小為512的隱藏狀態向量。這將是具有512個單位的LSTM層的激活。我們可以可視化這些單元激活中的每一個,以了解它們試圖解釋的內容。為此,我們必須將其轉換為可以表示其重要性的範圍的數值。
#第三層是輸出形狀為LSTM層(Batch_Size, 512)
lstm = model.layers[2]
#從中間層獲取輸出以可視化激活
attn_func = K.function(inputs = [model.get_input_at(0), K.learning_phase()],
outputs = [lstm.output]
)
步驟7:輔助功能
這些助手功能將幫助我們使用每個激活值來可視化字符序列。我們正在通過sigmoid功能傳遞激活,因為我們需要一個可以表示其對整個輸出重要性的規模值。get_clr功能有助於獲得給定值的適當顏色。
#獲取html元素
def cstr(s, color='black'):
if s == ' ':
return " ".format(color, s)
else:
return "{} ".format(color, s)
# 輸出html
def print_color(t):
display(html_print(''.join([cstr(ti, color=ci) for ti,ci in t])))
#選擇合適的顏色
def get_clr(value):
colors = ['#85c2e1', '#89c4e2', '#95cae5', '#99cce6', '#a1d0e8'
'#b2d9ec', '#baddee', '#c2e1f0', '#eff7fb', '#f9e8e8',
'#f9e8e8', '#f9d4d4', '#f9bdbd', '#f8a8a8', '#f68f8f',
'#f47676', '#f45f5f', '#f34343', '#f33b3b', '#f42e2e']
value = int((value * 100) / 5)
return colors[value]
# sigmoid函數
def sigmoid(x):
z = 1/(1 + np.exp(-x))
return z
下圖顯示了如何用各自的顏色表示每個值。
步驟8:獲取預測
get_predictions函數隨機選擇一個輸入種子序列,並獲得該種子序列的預測序列。visualize函數將預測序列,序列中每個字符的S形值以及要可視化的單元格編號作為輸入。根據輸出的值,將以適當的背景色列印字符。
將Sigmoid應用於圖層輸出後,值在0到1的範圍內。數字越接近1,它的重要性就越高。如果該數字接近於0,則意味著不會以任何主要方式對最終預測做出貢獻。這些單元格的重要性由顏色表示,其中藍色表示較低的重要性,紅色表示較高的重要性。
def visualize(output_values, result_list, cell_no):
print("\\nCell Number:", cell_no, "\\n")
text_colours = []
for i in range(len(output_values)):
text = (result_list[i], get_clr(output_values[i][cell_no]))
text_colours.append(text)
print_color(text_colours)
# 從隨機序列中獲得預測
def get_predictions(data):
start = np.random.randint(0, len(data)-1)
pattern = data[start]
result_list, output_values = [], []
print("Seed:")
print(""" + ''.join([int_to_char[value] for value in pattern]) + """)
print("\\nGenerated:")
for i in range(1000):
#為預測下一個字符而重塑輸入數組
x = np.reshape(pattern, (1, len(pattern), 1))
x = x / float(n_vocab)
# 預測
prediction = model.predict(x, verbose=0)
# LSTM激活函數
output = attn_func([x])[0][0]
output = sigmoid(output)
output_values.append(output)
# 預測字符
index = np.argmax(prediction)
result = int_to_char[index]
# 為下一個字符準備輸入
seq_in = [int_to_char[value] for value in pattern]
pattern.append(index)
pattern = pattern[1:len(pattern)]
# 保存生成的字符
result_list.append(result)
return output_values, result_list
步驟9:可視化激活
超過90%的單元未顯示任何可理解的模式。我手動可視化了所有512個單元,並注意到其中的三個(189、435、463)顯示了一些可以理解的模式。
output_values, result_list = get_predictions(dataX)
for cell_no in [189, 435, 463]:
visualize(output_values, result_list, cell_no)
單元格189將激活引號內的文本,如下所示。這表示單元格在預測時要查找的內容。如下所示,這個單元格對引號之間的文本貢獻很大。
引用句中的幾個單詞後激活了單元格435。
對於每個單詞中的第一個字符,將激活單元格463。
通過更多的訓練或更多的數據可以進一步改善結果。這恰恰證明了深度學習畢竟不是一個完整的黑匣子。
Github代碼:
https://github.com/Praneet9/Visualising-LSTM-Activations