本文基于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).