​在Keras中可視化LSTM

作 者 | Praneet Bomma

編 譯 | VK

來 源 | Towards Data Science

你是否想知道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 "<text style=color:#000;padding-left:10px;background-color:{}> </text>".format(color, s)  	else:  		return "<text style=color:#000;background-color:{}>{} </text>".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