本文基于paddlepaddle的ernie中的Transformer代碼[5]進行分析。Transformer的Encoder部分主要由多頭注意力(Multi-Head Attention)和FFN組成,如Fig 1.1所示。
Fig 1.1 Transformer的encoder由多頭注意力模塊和FFN模塊組成。其中的輸入Inputs是文本字符串經過令牌化處理(tokenizing)之后的id,這個過程需要在詞表(vocabulary)中查找對應字符得到,詞表的節選如:
[PAD] 0
[CLS] 1
[SEP] 2
[MASK] 3
, 4
的 5
、 6
一 7
人 8
有 9
是 10
在 11
...
令牌化處理除了查表,之前還可能需要進行切詞,控制字符特殊處理等,不過我們暫時不考慮這些。通過查表后可以將字符轉換成為id。
最終得到的token id如同:
[1 1429 34 65 87 679 90 944 2 1429 134 74 262 82 54 542 698 14 65 90 34 504 67 2 ]
然后根據這個id逐個在embedding表中進行查表,這里的word embedding和word2vec之類的關系不太大(除了最后一步查表的步驟之外),比如一個詞表由10000個字符(包括了控制字符和特殊字符,以及模型相關的特殊字符比如[CLS],[SEP],[MASK]等),那么該embedding表 ,此處的
是embedding的維度,按照ID的數值從第i行表中抽出作為embedding。對于位置編碼(position embedding)和分段編碼(sentence embedding)而言,也是如此在對應的embedding表中查找,如以下代碼所示:
self.word_emb = nn.Embedding(d_vocab,d_emb,
weight_attr=P.ParamAttr(
name=append_name(name, 'word_embedding'),
initializer=initializer))
self.pos_emb = nn.Embedding(d_pos,d_emb,
weight_attr=P.ParamAttr(
name=append_name(name, 'pos_embedding'),
initializer=initializer))
self.sent_emb = nn.Embedding(d_sent,d_emb,
weight_attr=P.ParamAttr(
name=append_name(name, 'sent_embedding'),
initializer=initializer))
...
src_embedded = self.word_emb(src_ids)
pos_embedded = self.pos_emb(pos_ids)
sent_embedded = self.sent_emb(sent_ids)
embedded = src_embedded + pos_embedded + sent_embedded
如同論文所說的,后續將src_embedded,pos_embedded,sent_embedded相加得到最后的embedding。
Fig 1.2 將token,sentense,和position的embedding進行逐元素相加。
然而,在batch size大于1時,需要根據整個batch中長度最長的序列作為標注,記為max_length,然后對其他序列進行填充(padding),一般是填充[PAD]字符。為了讓自注意力忽略填充[PAD]的部分,在前向計算時候需要加上mask對填充部分進行屏蔽,一般將填充部分的mask設為0,而有效部分的mask作為1。記mask為,其中M為序列長度,那么有(1.1)式子,對應代碼如下所示。
attn_bias = (1. - attn_bias) * -10000.0
attn_bias = attn_bias.unsqueeze(1).tile([1, self.n_head, 1, 1]) # avoid broadcast =_=
此處將-10000視為是負無窮大,認為是序列的無效部分,那么可以看出當的元素為0時候,attn_bias=-10000表示該位置的字符無效,反之則attn_bias=0,該位的字符有效。Paddle的Transformer實現按照論文中說的,在多頭注意力層添加了這個attn_bias從而實現了避免填充部分干擾的問題。如以下代碼所示。
class AttentionLayer(nn.Layer):
def __init__(self, cfg, name=None):
super(AttentionLayer, self).__init__()
initializer = nn.initializer.TruncatedNormal(
std=cfg['initializer_range'])
d_model = cfg['hidden_size']
n_head = cfg['num_attention_heads']
assert d_model % n_head == 0
d_model_q = cfg.get('query_hidden_size_per_head',
d_model // n_head) * n_head
d_model_v = cfg.get('value_hidden_size_per_head',
d_model // n_head) * n_head
self.n_head = n_head
self.d_key = d_model_q // n_head
self.q = _build_linear(d_model, d_model_q,
append_name(name, 'query_fc'), initializer)
self.k = _build_linear(d_model, d_model_q,
append_name(name, 'key_fc'), initializer)
self.v = _build_linear(d_model, d_model_v,
append_name(name, 'value_fc'), initializer)
self.o = _build_linear(d_model_v, d_model,
append_name(name, 'output_fc'), initializer)
self.dropout = nn.Dropout(p=cfg['attention_probs_dropout_prob'])
def forward(self, queries, keys, values, attn_bias, past_cache):
assert len(queries.shape) == len(keys.shape) == len(values.shape) == 3
#bsz, q_len, q_dim = queries.shape
#bsz, k_len, k_dim = keys.shape
#bsz, v_len, v_dim = values.shape
#assert k_len == v_len
q = self.q(queries)
k = self.k(keys)
v = self.v(values)
cache = (k, v)
if past_cache is not None:
cached_k, cached_v = past_cache
k = P.concat([cached_k, k], 1)
v = P.concat([cached_v, v], 1)
q = q.reshape(
[0, 0, self.n_head, q.shape[-1] // self.n_head]).transpose(
[0, 2, 1, 3]) #[batch, head, seq, dim]
k = k.reshape(
[0, 0, self.n_head, k.shape[-1] // self.n_head]).transpose(
[0, 2, 1, 3]) #[batch, head, seq, dim]
v = v.reshape(
[0, 0, self.n_head, v.shape[-1] // self.n_head]).transpose(
[0, 2, 1, 3]) #[batch, head, seq, dim]
q = q.scale(self.d_key**-0.5)
score = q.matmul(k, transpose_y=True)
if attn_bias is not None:
score += attn_bias
score = F.softmax(score)
score = self.dropout(score)
out = score.matmul(v).transpose([0, 2, 1, 3])
out = out.reshape([0, 0, out.shape[2] * out.shape[3]])
out = self.o(out)
return out, cache
其中在過softmax
層之前將score
和attn_bias
相加,此時自注意力的公式變為:
其中的,
經過歸一化,因此范圍在[ 0 , 1 ],而attn_bias的范圍是{-10000,0},因此在attn_bias=-10000的時候,
的值很小,在經過了softmax之后將近為0,也意味著該token無效,一般用于屏蔽padding字符使用;而當attn_bias=0是,相當與該項不存在,不會造成屏蔽效果。從中我們看到,attn_bias的存在,或者進一步說mask的存在是對填充字符進行屏蔽用的,防止填充字符干擾到了自注意力結果。
無論是從原理上還是從FFN層的代碼來看,FFN的參數都和輸入序列長度無關。輸入長度在Transformer中會影響到的是位置編碼,因此在ViT [6]中會對位置編碼進行插值。
class PositionwiseFeedForwardLayer(nn.Layer):
def __init__(self, cfg, name=None):
super(PositionwiseFeedForwardLayer, self).__init__()
initializer = nn.initializer.TruncatedNormal(
std=cfg['initializer_range'])
d_model = cfg['hidden_size']
d_ffn = cfg.get('intermediate_size', 4 * d_model)
self.act = ACT_DICT[cfg['hidden_act']]()
self.i = _build_linear(
d_model,
d_ffn,
append_name(name, 'fc_0'),
initializer, )
self.o = _build_linear(d_ffn, d_model,
append_name(name, 'fc_1'), initializer)
prob = cfg.get('intermediate_dropout_prob', 0.)
self.dropout = nn.Dropout(p=prob)
def forward(self, inputs):
hidden = self.act(self.i(inputs))
hidden = self.dropout(hidden)
out = self.o(hidden)
return out
在代碼的多頭注意力這一塊,論文中采用的結構圖如Fig 1.3所示,如果不采用多頭注意力,只有一頭注意力的話,那么首先將Query,Key和Value從d_model映射到d_model_q,d_model_k,d_model_v,然后進行后續的自注意計算。但是如果采用了多頭注意力,那么理論上會將d_model_x平均劃分為d_model_x/n_head ,稱之為split_multi_head ,然后在每一頭上由d_model映射到d_model_x/n_head后,再在每一頭上進行自注意力計算,最后合并多頭注意力的計算結果(combine_multi_head)。
Fig 1.3 論文中對多頭注意力結構的示意圖。
然而在實際代碼中,通常不會采用這種直白(但是低效)的方式組織多頭注意力,如以上的代碼片段所示,在實現上還是直接將d_model維度直接映射到了d_model_x維度,然后通過reshape進行劃分,如以下代碼段所示。
q = q.reshape([0, 0, self.n_head, q.shape[-1] // self.n_head]).transpose(
[0, 2, 1, 3]) #[batch, head, seq, dim]
也就是說,如Fig 1.4所示,其中d代表映射前的維度,n代表的是序列的長度,而D代表映射后的維度,那么通過reshape可以劃分每個頭,如綠色和粉色所示,每段長度即是D/k,k是頭數。
Fig 1.4 在代碼實現上實際采取的方式。
Reference
[1]. Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, ?ukasz Kaiser, and Illia Polosukhin. Attention is all you need. In NIPS, 2017
[2]. Devlin, Jacob, Ming-Wei Chang, Kenton Lee, and Kristina Toutanova. “Bert: Pre-training of deep bidirectional transformers for language understanding.” arXiv preprint arXiv:1810.04805 (2018).
[3]. Sun, Yu, Shuohuan Wang, Yukun Li, Shikun Feng, Xuyi Chen, Han Zhang, Xin Tian, Danxiang Zhu, Hao Tian, and Hua Wu. “Ernie: Enhanced representation through knowledge integration.” arXiv preprint arXiv:1904.09223 (2019).
[4]. https://fesian.blog.csdn.net/article/details/116031656
[5]. https://github.com/PaddlePaddle/ERNIE
[6]. Dosovitskiy, Alexey, Lucas Beyer, Alexander Kolesnikov, Dirk Weissenborn, Xiaohua Zhai, Thomas Unterthiner, Mostafa Dehghani et al. “An image is worth 16x16 words: Transformers for image recognition at scale.” arXiv preprint arXiv:2010.11929 (2020).