在 Transformer 中使用的自注意力(self-attention)機制,本質上是一種點積(dot-product)注意力的特例,即兩個輸入是同一向量。

在涉及到雙方互動的 NLP 應用,比如 QA 中,其實通用形式反而更普遍一點。如果寫成簡化公式,就是

softmax(QK)V/\sqrt{d}

。我關注的第一個問題是:

在 Q 和 KV 來源於不同向量的時候,最終的輸出向量的維度是什麼,它又是一種什麼樣的語義概念呢?

除了點積注意力,Transormer 的另一項首創是多頭(MultiHead)注意力,這是另一種形式的特例。我關注的第二個問題是:

如何在一次運算中,“分頭”操作是否帶來了額外的計算開銷,它又是如何增加模型表現力的?

今天扒的程式碼來自於 Bert,函式 attention_layer():

https://

github。com/google-resea

rch/bert/blob/master/modeling。py

1。 基本符號

假設進行 attention 計算的雙方,分別是

from_tensor

to_tensor

,前者構成 Query,後者構成 Key 和 Value。使用

B

代表 batch_size,

F

代表 from_tensor 的序列長度,

T

代表to_tensor 的序列長度,

N

代表多頭注意力的頭數,

H

代表每個頭的維度。

扒原始碼:跳出self-attention看多頭點積注意力

dot-attention 全流程

2. 尺度變換

對應原始碼中的 reshape_to_matrix() 函式。這一步處理又兩層一次,首先由於 from_tensor 和 to_tensor 本身的向量維度(即最後一維的尺寸),是不強制相同的,可以是 64,可以是 128,也可以是 256。後續的 attention 計算,涉及到矩陣點積,必須要把兩邊的尺度拉平。

另外,透過三個不用的尺度變換矩陣, from_tensor 對映成了 Query,to_tensor 則對映成了 Key 和 Value。Q,K,V,才是 attention 的真正輸入,它們的尺寸很重要:

Q - [B, F, N × H]

K - [B, T, N × H]

V - [B, T, N × H]

劃重點,這一步沒有非線性啟用函式,僅僅是完成了矩陣的尺度變換。

對於自注意力機制,我們常說 Q、K、V 在初始化階段是未分化的,是可以互換的,就是這一層意思。

3。 分頭

對應原始碼中的 transpose_for_scores() 函式。其實“分頭”並沒有多複雜,就是直白地按照順序,把向量依次切成多份。

扒原始碼:跳出self-attention看多頭點積注意力

512 的向量,分成 4 個頭

分頭完成之後,各個向量的尺度發生了改變,由 3 維變成 4 維。並且,為了後續的點積做準備,進行了一次維度的重排:

Q - [B, F, N, H] -> 重排 -> [B, N, F, H]

K - [B, T, N, H] -> 重排 -> [B, N, T, H]

V - [B, T, N, H] -> 重排 -> [B, N, T, H]

顯然,“多頭”本身並沒有進行向量的擴容,即沒有引入額外的資訊量,只是拆開了而已。

4。 多頭

QK^{T}

計算 attention 分數,原始碼只有兩行:

attention_scores

=

tf

matmul

query_layer

key_layer

transpose_b

=

True

attention_scores

=

tf

multiply

attention_scores

1。0

/

math

sqrt

float

size_per_head

)))

線性代數里學的矩陣點積,都是二維矩陣和二維矩陣。這裡的 Q 和 K,在分頭之後已經是四維矩陣了,這要怎麼點積?其實 tf。matmul() 這個 api,對於高於二維的矩陣,使用前面的維度進行切片,得到二維的 slice,然後在進行真正的點積。因此,Q 和 K 在計算 attenion 分數時,實際上進行了 B × N 次的二維矩陣點積,最終的輸出矩陣是:

score - [B, N, F, T]

請注意,關於多頭,實際上就是在 attention 之前,拆分出一個 N 的維度,把原本的 B 個 batch 的 [F, N × H] 和 [N × H, T] 點積,變成了 B × N 個 batch 的 [F, H] 和 [H, T] 點積。完成之後,再將維度恢復回去。在這種實現方式下,

拋開維度處理的部分(其實可以忽略不計),多頭機制並沒有帶來額外的計算開銷。

所以說,多頭 attention 比 單頭 attention 更慢嗎?並不。

5。 attention-mask

每個樣本的序列長度是不一致的,在拼 batch 時候,被 pad 成了 T。在計算 attention 的時候,這些被 pad 的位置是不能參與貢獻的。解決方法是,給這些位置 ,設定一個負極大值(-10000),在計算

e^{x}

之後變成無限趨近於 0,最終分配到的 softmax 權重小得可以忽略:

if attention_mask is not None:

attention_mask = tf。expand_dims(attention_mask, axis=[1])

adder = (1。0 - tf。cast(attention_mask, tf。float32)) * -10000。0

attention_scores += adder

劃重點,attention-maski 是針對 K!不是 Q 更不是 V!

6。 加權求和

即使用 attention 分數,對 V 進行加權求和。輸入矩陣的維度為:

score - [B, N, F, T] × V - [B, N, T, H]

output - [B, N, F, H] -> 重排 -> [B, F, N, H] -> 合維 -> [B, F, N × H]

來看看最終輸出的維度,和 from_tensor 一致(不是 to_tensor)。從這個層面來理解,點積注意力的本質是使用 to_tensor 來獲得 from_tensor

在每個序列位置

的另一種表示。也就是說,這一過程並不改變 from_tensor 本身的序列長度。