Attention
一、基本的Attention原理
1.1 什麼是Attention?
Attention模型的基本表述可以這樣理解成(個人理解):當我們人在看一樣東西的時候,我們時刻關注的一定是正在看的東西的某一個地方,換句話說,當我們目光移到別處時,注意力隨着目光的移動野在轉移,這意味着,當人們注意到某個目標或某個場景時,該目標內部以及該場景內每一處空間位置上的注意力分佈是不一樣的。上圖形象化展示了人類在看到一副圖像時是如何高效分配有限的注意力資源的,其中紅色區域表明視覺系統更關注的目標,很明顯對於上圖所示的場景,人們會把注意力更多投入到人的臉部,文本的標題以及文章首句等位置。
這一點在如下情形下同樣成立:當我們試圖描述一件事情,我們當前時刻說到的單詞和句子和正在描述的該事情的對應某個片段最先關,而其他部分隨着描述的進行,相關性也在不斷地改變。
從上述兩種情形,可以看出,對於Attention的作用角度出發,我們就可以從兩個角度來分類Attention種類:空間注意力和時間注意力,即Spatial Attention 和Temporal Attention。這種分類更多的是從應用層面上,而從Attention的作用方法上,可以將其分爲Soft Attention和Hard Attention,這既我們所說的,Attention輸出的向量分佈是一種one-hot的獨熱分佈還是soft的軟分佈,這直接影響對於上下文信息的選擇作用。
1.2 爲什麼要加入Attention
- 計算能力的限制:當要記住很多“信息“,模型就要變得更復雜,然而目前計算能力依然是限制神經網絡發展的瓶頸。
-優化算法的限制:雖然局部連接、權重共享以及pooling等優化操作可以讓神經網絡變得簡單一些,有效緩解模型複雜度和表達能力之間的矛盾;但是,如循環神經網絡中的長距離以來問題,信息“記憶”能力並不高。
1.3 Attention的原理
論文:Neural machine translation by jointly learning to align and translate
其網絡結構如下:
自我感覺如下理解更容易一些:
將Source中的構成元素想象成是由一系列的<Key,Value>數據對構成,此時給定Target中的某個元素Query,通過計算Query和各個Key的相似性或者相關性,得到每個Key對應Value的權重係數,然後對Value進行加權求和,即得到了最終的Attention數值。所以本質上Attention機制是對Source中元素的Value值進行加權求和,而Query和Key用來計算對應Value的權重係數。
至於Attention機制的具體計算過程,如果對目前大多數方法進行抽象的話,可以將其歸納爲兩個過程:第一個過程是根據Query和Key計算權重係數,第二個過程根據權重係數對Value進行加權求和。而第一個過程又可以細分爲兩個階段:第一個階段根據Query和Key計算兩者的相似性或者相關性;第二個階段對第一階段的原始分值進行歸一化處理;這樣,可以將Attention的計算過程抽象爲如下圖展示的三個階段。
二、HAN(Hierarchical Attention Networks)的原理
論文:Hierarchical Attention Networks for Document Classification
網絡結構:
HAN模型就是分層次的利用注意力機制來構建文本向量表示的方法。
文本由句子構成,句子由詞構成,HAN模型對應這個結構分層的來構建文本向量表達;
文本中不同句子對文本的主旨影響程度不同,一個句子中不同的詞語對句子主旨的影響程度也不同,因此HAN在詞語層面和句子層面分別添加了注意力機制;
分層的注意力機制還有一個好處,可以直觀的看出用這個模型構建文本表示時各個句子和單詞的重要程度,增強了可解釋性。
這篇論文裏面使用雙向GRU來構建句子表示和文本表示,以句子爲例,得到循環神經網絡中每個單元的輸出後利用注意力機制整合得到句子向量表示(不使用attention時,一般會使用MAX或AVE),過程如下:
按照文中說法,先經過一層MLP得到隱層表示;然後與word level context vector (詞語級別的context vector)做點積,各詞語得到的結果再經過softmax函數後的結果就是各自的重要程度,即;最後加權和得到句子表示。文本向量的構建與此一致,之後經過全連接層和softmax分類。
三、Attention實現文本分類
#Attention模塊代碼
#! -*- coding: utf-8 -*-
from __future__ import print_function
from keras.datasets import imdb
from keras.preprocessing import sequence
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)
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)
#帶有Attention的文本分類網絡
# from attention import Position_Embedding, Attention
max_features = 20000
maxlen = 80
batch_size = 32
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')
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)
S_inputs = Input(shape=(None,), dtype='int32')
embeddings = Embedding(max_features, 128)(S_inputs)
embeddings = Position_Embedding()(embeddings) # 增加Position_Embedding能輕微提高準確率
O_seq = Attention(8, 16)([embeddings, embeddings, embeddings])
O_seq = GlobalAveragePooling1D()(O_seq)
O_seq = Dropout(0.5)(O_seq)
outputs = Dense(1, activation='sigmoid')(O_seq)
model = Model(inputs=S_inputs, outputs=outputs)
print(model.summary())
# try using different optimizers and different optimizer configs
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
print('Train...')
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=5,
validation_data=(x_test, y_test))
score, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
print('Test score:', score)
print('Test accuracy:', acc)
#輸出結果