本次分享的論文是鼎鼎有名的,論文鏈接attention is all you need,其參考的 實現代碼tensorflow代碼實現。
自己水平有限,在讀這篇論文和實現代碼時,感覺比較喫力,花了兩三天才搞懂了一些,在此總結下。
廢話不多說,直接帶着代碼看論文介紹的網絡結構。
下面總結是以論文實驗 機器翻譯來說的。
我們分部分來看:
建議可以先看看 臺大教授李宏毅關於transformer的課程:https://www.bilibili.com/video/av56239558?from=search&seid=17256210640645614178
Stage1 Encode Input
和普遍的做法一樣,對文本輸入做 操作,
embedding_encoder = tf.get_variable("embedding_encoder", [Config.data.source_vocab_size, Config.model.model_dim], self.dtype)(注意這裏的model_dim)
embedding_inputs = embedding_encoder
上面其實就是做個輸入文本的矩陣而已。
模型裏已經剔除了,,如何體現輸入文本的先後關係呢?而這種序列的先後關係對模型有着至關重要的作用,於是論文中提出了 騷操作~,論文是這樣做的:
這裏的指的是上面 的維度,就是當前的詞在整個句子中的位置,例如第一個詞還是第二個詞等, 就是遍歷時的值,在代碼中是這樣做的:
def positional_encoding(dim, sentence_length, dtype=tf.float32):
encoded_vec = np.array([pos/np.power(10000, 2*i/dim) for pos in range(sentence_length) for i in range(dim)])#對每個位置處都產生一個維度爲dim的向量。
encoded_vec[::2] = np.sin(encoded_vec[::2])#偶數位置處
encoded_vec[1::2] = np.cos(encoded_vec[1::2])#奇數位置處
return tf.convert_to_tensor(encoded_vec.reshape([sentence_length, dim]), dtype=dtype)
#Positional Encoding
with tf.variable_scope("positional-encoding"):
positional_encoded = positional_encoding(Config.model.model_dim, Config.data.max_seq_length, dtype=self.dtype)
上面生成的其實就是位置信息的 矩陣。論文中提到這樣做的原因,就是希望模型能很容易的學到相對先後的位置信息。
# Add
position_inputs = tf.tile(tf.range(0, Config.data.max_seq_length), [self.batch_size])#將range(0, max_seq_length)列表複製batch_size次,生成shape爲[batch_size, max_seq_length]的張量。
position_inputs = tf.reshape(position_inputs,[self.batch_size, Config.data.max_seq_length]) # batch_size x [0, 1, 2, ..., n]#未經過embedding的位置輸入信息。
好了矩陣都做好了,該 了。
encoded_inputs = tf.add(tf.nn.embedding_lookup(embedding_inputs, inputs), tf.nn.embedding_lookup(positional_encoded, position_inputs))
這與輸入文本信息就結合的其位置信息了,作爲的整體輸入,這部分對應的上面那張圖的****部分,這部分的操作就是如下:
同理,就不再贅述了。
Stage2 Multi Head Attention
multi head attention 絕對是transform裏面的 一個重點和優點,正是有了這個機制,transform纔能有這麼好的效果。
當時論文讀到這裏有點懵逼,什麼叫?再仔細看看論文吧?
由上圖我們可以看出 有三個相同的輸入,不妨分別記爲,其實就是上面的其均爲。論文中提到對三個輸入做次不同的線性映射,即爲:
def _linear_projection(self, q, k, v):
q = tf.layers.dense(q, self.linear_key_dim, use_bias=False)
k = tf.layers.dense(k, self.linear_key_dim, use_bias=False)
v = tf.layers.dense(v, self.linear_value_dim, use_bias=False)
return q, k, v
上述代碼就是做線性映射,其中就是映射的個數。這裏面相當於把次的線性映射一起做了,後面需要把每一個映射結果分割開,故需要保證 和 能整除。 經過線性映射後生成的 的分別爲
然後按分割開來得:(這裏分割開,相當於初始個num_head 不同的權重,下面將會做num_head 次不同的attention操作。理解這非常重要)
def _split_heads(self, q, k, v):
def split_last_dimension_then_transpose(tensor, num_heads, dim):
┆ t_shape = tensor.get_shape().as_list()
┆ tensor = tf.reshape(tensor, [-1] + t_shape[1:-1] + [num_heads, dim // num_heads])
┆ return tf.transpose(tensor, [0, 2, 1, 3]) # [batch_size, num_heads, max_seq_len, dim]
qs = split_last_dimension_then_transpose(q, self.num_heads, self.linear_key_dim)
ks = split_last_dimension_then_transpose(k, self.num_heads, self.linear_key_dim)
vs = split_last_dimension_then_transpose(v, self.num_heads, self.linear_value_dim)
return qs, ks, vs
論文提到,這時生成的可以並行的放入到 中,那麼這個 是個什麼樣的結構呢?
上圖所示的結構在論文中被稱爲,其值的計算公式如下:
由上面可知,其公式中的 分別對應的是,其實都是,只是做了不同的線性映射,其中 的維度相同。我們可以這樣理解操作,假設爲爲的矩陣,爲相同,那麼經過操作以後,變成了爲的矩陣,怎樣理解這個生成的矩陣呢?其實就是做了個操作,即是當前句子中每個詞和其他詞做個乘積形成的矩陣,以得到每個詞的權重,以便學習當前應該到哪個詞。那麼爲什麼要除以呢?論文中說到,當兩個矩陣做時,可能會變得很大(試想一下,兩個矩陣相互獨立,且均值爲0,方差爲1,那麼經過矩陣相乘以後,均值還爲0,方差變成),經過後,梯度可能會變得很小,爲了抵消這種效果,再除以。其代碼如下:
def _scaled_dot_product(self, qs, ks, vs):
key_dim_per_head = self.linear_key_dim // self.num_heads
o1 = tf.matmul(qs, ks, transpose_b=True)
o2 = o1 / (key_dim_per_head**0.5)
if self.masked:
┆ diag_vals = tf.ones_like(o2[0, 0, :, :]) # (batch_size, num_heads, query_dim, key_dim)
┆ tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense() # (q_dim, k_dim)
┆ masks = tf.tile(tf.reshape(tril, [1, 1] + tril.get_shape().as_list()),
┆ ┆ ┆ ┆ ┆ [tf.shape(o2)[0], tf.shape(o2)[1], 1, 1])
┆ paddings = tf.ones_like(masks) * -1e9
┆ o2 = tf.where(tf.equal(masks, 0), paddings, o2)
o3 = tf.nn.softmax(o2)
return o3
好了,過了後:
由上圖可知,再過操作:(不同的head關注的點可能不一樣,這裏concat操作,相當於把num_head次不同的attention結果集成在一起,理解這非常重要)
def _concat_heads(self, outputs):
def transpose_then_concat_last_two_dimenstion(tensor):
┆ tensor = tf.transpose(tensor, [0, 2, 1, 3]) # [batch_size, max_seq_len, num_heads, dim]
┆ t_shape = tensor.get_shape().as_list()
┆ num_heads, dim = t_shape[-2:]
┆ return tf.reshape(tensor, [-1] + t_shape[1:-2] + [num_heads * dim])
return transpose_then_concat_last_two_dimenstion(outputs)
論文中提到,這樣做後,再過一層線性映射。
output = tf.layers.dense(output, self.model_dim)
故整個 操作如下:
def multi_head(self, q, k, v):
q, k, v = self._linear_projection(q, k, v)
qs, ks, vs = self._split_heads(q, k, v)
outputs = self._scaled_dot_product(qs, ks, vs)
output = self._concat_heads(outputs)
output = tf.layers.dense(output, self.model_dim)
return tf.nn.dropout(output, 1.0 - self.dropout)
然後在做個和:
def _add_and_norm(self, x, sub_layer_x, num=0):
with tf.variable_scope(f"add-and-norm-{num}"):
┆ return tf.contrib.layers.layer_norm(tf.add(x, sub_layer_x)) # with Residual connection
這裏面的 就是上面的輸出,就是。
矩陣並行化計算過程:
上面就是 的整個過程。
Stage3 Feed Forward
這一步就比較簡單了,就是做兩層的 而已,只不過內層的 會過 激活。
別問我爲啥
同理再過。
部分和上面差不多,只不過在 的部分,做時,我們不能使當前詞的後面的詞對當前詞產生影響,因爲在當前我們實際是不知道後面應該有哪些詞的,只不過在的時候可以批量的訓練,但是在的時候是不知道的。那麼該怎麼消除後面詞對當前詞的影響呢?
在時,會得到矩陣,我們只需要保留該矩陣的下三角部分即可,然後再做,既可消除後面詞對當前詞的影響。
def _scaled_dot_product(self, qs, ks, vs):
key_dim_per_head = self.linear_key_dim // self.num_heads
o1 = tf.matmul(qs, ks, transpose_b=True)
o2 = o1 / (key_dim_per_head**0.5)
if self.masked:
┆ diag_vals = tf.ones_like(o2[0, 0, :, :]) # (batch_size, num_heads, query_dim, key_dim)
┆ tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense() # (q_dim, k_dim)
┆ masks = tf.tile(tf.reshape(tril, [1, 1] + tril.get_shape().as_list()),
┆ ┆ ┆ ┆ ┆ [tf.shape(o2)[0], tf.shape(o2)[1], 1, 1])
┆ paddings = tf.ones_like(masks) * -1e9
┆ o2 = tf.where(tf.equal(masks, 0), paddings, o2)
o3 = tf.nn.softmax(o2)
return o3
剩餘部分的與上面類似,就不再贅述了。
整個論文的過程可以用如下動畫解釋:
個人看法
- 該論文擯棄了 等作爲基本的模型,而是單純的採用結構,使得計算並行性大大提高。
- 沒想到也可以單獨的作爲神經網絡的一層,甚至可以看作對的。
- Transformer的多頭注意力機制能從不同角度對每個對象的重要性進行評價,從而能更好的學習輸入對象中存在的各種關係並對其進行表徵。
不同的head, attention的注意力不一樣