注意力機制即 Attention mechanism在序列學習任務上具有巨大的提升作用,在編解碼器框架內,通過在編碼段加入Attention模型,對源數據序列進行數據加權變換,或者在解碼端引入Attention 模型,對目標數據進行加權變化,可以有效提高序列對序列的自然方式下的系統表現。

什麼是Attention:

Attention模型的基本表述可以這樣理解成:

當我們人在看一樣東西的時候,我們當前時刻關注的一定是我們當前正在看的這樣東西的某一地方,換句話說,當我們目光移到別處時,注意力隨著目光的移動也在轉移。

這意味著,當人們注意到某個目標或某個場景時,該目標內部以及該場景內每一處空間位置上的注意力分布是不一樣的。

這一點在如下情形下同樣成立:當我們試圖描述一件事情,我們當前時刻說到的單詞和句子和正在描述的該事情的對應某個片段最先關,而其他部分隨著描述的進行,相關性也在不斷地改變。

從上面兩種情形來看,對於 Attention的作用角度出發,我們就可以從兩個角度來分類 Attention種類:

  • 空間注意力 Spatial Attention
  • 時間注意力 Temporal Attention

這樣的分類更多的是從應用層面上,而從 Attention的作用方法上,可以將其分為 Soft Attention 和 Hard Attention,這既我們所說的, Attention輸出的向量分布是一種one-hot的獨熱分布還是soft的軟分布,這直接影響對於上下文信息的選擇作用。

為什麼要加入Attention:

當輸入序列非常長時,模型難以學到合理的向量表示

序列輸入時,隨著序列的不斷增長,原始根據時間步的方式的表現越來越差,這是由於原始的這種時間步模型設計的結構有缺陷,即所有的上下文輸入信息都被限制到固定長度,整個模型的能力都同樣收到限制,我們暫且把這種原始的模型稱為簡單的編解碼器模型。

編解碼器的結構無法解釋,也就導致了其無法設計。

長輸入序列帶來的問題:

使用傳統編碼器-解碼器的RNN模型先用一些LSTM單元來對輸入序列進行學習,編碼為固定長度的向量表示;然後再用一些LSTM單元來讀取這種向量表示並解碼為輸出序列。

採用這種結構的模型在許多比較難的序列預測問題(如文本翻譯)上都取得了最好的結果,因此迅速成為了目前的主流方法。

這種結構在很多其他的領域上也取得了不錯的結果。然而,它存在一個問題在於:輸入序列不論長短都會被編碼成一個固定長度的向量表示,而解碼則受限於該固定長度的向量表示。

這個問題限制了模型的性能,尤其是當輸入序列比較長時,模型的性能會變得很差(在文本翻譯任務上表現為待翻譯的原始文本長度過長時翻譯質量較差)。

「一個潛在的問題是,採用編碼器-解碼器結構的神經網路模型需要將輸入序列中的必要信息表示為一個固定長度的向量,而當輸入序列很長時則難以保留全部的必要信息(因為太多),尤其是當輸入序列的長度比訓練數據集中的更長時。」

— Dzmitry Bahdanau, et al., Neural machine translation by jointly learning to align and translate, 2015

如何使用Attention機制:

Attention機制的基本思想是:打破了傳統編碼器-解碼器結構在編解碼時都依賴於內部一個固定長度向量的限制。

Attention機制的實現是 通過保留LSTM編碼器對輸入序列的中間輸出結果,然後訓練一個模型來對這些輸入進行選擇性的學習並且在模型輸出時將輸出序列與之進行關聯。

換一個角度而言,輸出序列中的每一項的生成概率取決於在輸入序列中選擇了哪些項。

Attention-based Model 其實就是一個相似性的度量,當前的輸入與目標狀態約相似,那麼在當前的輸入的權重就會越大。就是在原有的model上加入了Attention的思想。

沒有attention機制的encoder-decoder結構通常把encoder的最後一個狀態作為decoder的輸入(可能作為初始化,也可能作為每一時刻的輸入),但是encoder的state畢竟是有限的,存儲不了太多的信息,對於decoder過程,每一個步驟都和之前的輸入都沒有關係了,只與這個傳入的state有關。attention機制的引入之後,decoder根據時刻的不同,讓每一時刻的輸入都有所不同。

---------------------

以上原理內容引自CSDN ,作者:GerHard_Z ,深度學習中 的 Attention機制

理論描述的差不多了,開始代碼部分:

先編寫Position_Embedding層,代碼如下:

from keras import backend as K
from keras.engine.topology import Layer
from keras.models import Model
from keras.layers import *

class Position_Embedding(Layer):

def __init__(self, size=None, mode=sum, **kwargs):
self.size = size # 必須為偶數
self.mode = mode
super(Position_Embedding, self).__init__(**kwargs)

def call(self, x):
if (self.size == None) or (self.mode == sum):
self.size = int(x.shape[-1])
batch_size, seq_len = K.shape(x)[0], K.shape(x)[1]
position_j = 1. / K.pow(10000.,
2 * K.arange(self.size / 2, dtype=float32
) / self.size)
position_j = K.expand_dims(position_j, 0)
position_i = K.cumsum(K.ones_like(x[:, :, 0]), 1) - 1 # K.arange不支持變長,只好用這種方法生成
position_i = K.expand_dims(position_i, 2)
position_ij = K.dot(position_i, position_j)
position_ij = K.concatenate([K.cos(position_ij), K.sin(position_ij)], 2)
if self.mode == sum:
return position_ij + x
elif self.mode == concat:
return K.concatenate([position_ij, x], 2)

def compute_output_shape(self, input_shape):
if self.mode == sum:
return input_shape
elif self.mode == concat:
return (input_shape[0], input_shape[1], input_shape[2] + self.size)

再來編寫Attention層,代碼如下:

class Attention(Layer):

def __init__(self, nb_head, size_per_head, **kwargs):
self.nb_head = nb_head
self.size_per_head = size_per_head
self.output_dim = nb_head * size_per_head
super(Attention, self).__init__(**kwargs)

def build(self, input_shape):
self.WQ = self.add_weight(name=WQ,
shape=(input_shape[0][-1], self.output_dim),
initializer=glorot_uniform,
trainable=True)
self.WK = self.add_weight(name=WK,
shape=(input_shape[1][-1], self.output_dim),
initializer=glorot_uniform,
trainable=True)
self.WV = self.add_weight(name=WV,
shape=(input_shape[2][-1], self.output_dim),
initializer=glorot_uniform,
trainable=True)
super(Attention, self).build(input_shape)

def Mask(self, inputs, seq_len, mode=mul):
if seq_len == None:
return inputs
else:
mask = K.one_hot(seq_len[:, 0], K.shape(inputs)[1])
mask = 1 - K.cumsum(mask, 1)
for _ in range(len(inputs.shape) - 2):
mask = K.expand_dims(mask, 2)
if mode == mul:
return inputs * mask
if mode == add:
return inputs - (1 - mask) * 1e12

def call(self, x):
# 如果只傳入Q_seq,K_seq,V_seq,那麼就不做Mask
# 如果同時傳入Q_seq,K_seq,V_seq,Q_len,V_len,那麼對多餘部分做Mask
if len(x) == 3:
Q_seq, K_seq, V_seq = x
Q_len, V_len = None, None
elif len(x) == 5:
Q_seq, K_seq, V_seq, Q_len, V_len = x
# 對Q、K、V做線性變換
Q_seq = K.dot(Q_seq, self.WQ)
Q_seq = K.reshape(Q_seq, (-1, K.shape(Q_seq)[1], self.nb_head, self.size_per_head))
Q_seq = K.permute_dimensions(Q_seq, (0, 2, 1, 3))
K_seq = K.dot(K_seq, self.WK)
K_seq = K.reshape(K_seq, (-1, K.shape(K_seq)[1], self.nb_head, self.size_per_head))
K_seq = K.permute_dimensions(K_seq, (0, 2, 1, 3))
V_seq = K.dot(V_seq, self.WV)
V_seq = K.reshape(V_seq, (-1, K.shape(V_seq)[1], self.nb_head, self.size_per_head))
V_seq = K.permute_dimensions(V_seq, (0, 2, 1, 3))
# 計算內積,然後mask,然後softmax
A = K.batch_dot(Q_seq, K_seq, axes=[3, 3]) / self.size_per_head ** 0.5
A = K.permute_dimensions(A, (0, 3, 2, 1))
A = self.Mask(A, V_len, add)
A = K.permute_dimensions(A, (0, 3, 2, 1))
A = K.softmax(A)
# 輸出並mask
O_seq = K.batch_dot(A, V_seq, axes=[3, 2])
O_seq = K.permute_dimensions(O_seq, (0, 2, 1, 3))
O_seq = K.reshape(O_seq, (-1, K.shape(O_seq)[1], self.output_dim))
O_seq = self.Mask(O_seq, Q_len, mul)
return O_seq

def compute_output_shape(self, input_shape):
return (input_shape[0][0], input_shape[0][1], self.output_dim)

對上述兩個層進行使用,代碼如下:

1)載入訓練集合測試集,這裡採用的是IMDB影評數據集:

from keras.preprocessing import sequence
from keras.datasets import imdb

print(Loading data...)
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
print(len(x_train), train sequences)
print(len(x_test), test sequences)

Loading data...

25000 train sequences25000 test sequences

2)對語句的向量化數據進行填充,統一設定為100個字元(英文就是字母):

max_features = 30000
maxlen = 100
batch_size = 64

print(Pad sequences (samples x time))
x_train = sequence.pad_sequences(x_train, maxlen=maxlen)
x_test = sequence.pad_sequences(x_test, maxlen=maxlen)
print(x_train shape:, x_train.shape)
print(x_test shape:, x_test.shape)

Pad sequences (samples x time)

x_train shape: (25000, 100)

x_test shape: (25000, 100)

3)設定模型,在其中同時使用Position_Embedding和Attention,提升效果:

S_inputs = Input(shape=(None,), dtype=int32)
embeddings = Embedding(max_features, 128)(S_inputs)
embeddings = Position_Embedding()(embeddings)
O_seq = Attention(8,16)([embeddings,embeddings,embeddings])
O_seq = GlobalMaxPooling1D()(O_seq)
O_seq = Dropout(0.5)(O_seq)
outputs = Dense(1, activation=sigmoid)(O_seq)
model = Model(inputs=S_inputs, outputs=outputs)

4) 模型編譯,可以使用不同的優化器(Optimizer)和度量指標(Metrics):

model.compile(loss=binary_crossentropy,optimizer=adam,metrics=[accuracy])

5)模型訓練

print(Train...)
model.fit(x_train, y_train, batch_size=batch_size, epochs=5, validation_data=(x_test, y_test))

訓練進程及結果即時反饋

以上只是拿一個常規的英文影評數據集進行測試,數據是比較乾淨的,感興趣的小夥伴,可以拿中文的數據試試,看看這兩個trick在中文上的效果到底咋樣。


推薦閱讀:
相关文章