Transformer代码随记

本文基于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层之前将scoreattn_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).

声明:本内容为作者独立观点,不代表电子星球立场。未经允许不得转载。授权事宜与稿件投诉,请联系:editor@netbroad.com
觉得内容不错的朋友,别忘了一键三连哦!
赞 2
收藏 3
关注 49
成为作者 赚取收益
全部留言
0/200
成为第一个和作者交流的人吧