google 出品, 基于 encoder-decoder 架构的 seq2seq 自然语言翻译模型. paper 见参考[1].
创新之处是弃用 RNN,CNN 这样的结构, 采用 self-attention 与 positional-encoding 来加速训练并提升效果, 取得了 SOTA 成绩. 是后续 GPT 及 LLM 系列的基石之作!

1. BPE 分词

Byte Pair Encoding, 是一种子词(subword)分词算法. 特点是数据驱动, 先通过语料库构建 词汇表(vocabulary), 然后用它做输入文本的 贪心最长匹配.
优点是词典大小可控, 可有效处理未知词(OOV)和词形变化, 因此广泛用于 GPT, Bert 及 后续的 LLM 中.

1.1 训练

从字符级别开始,逐步合并高频的符号对,形成常见的子词单元。

  1. 所有单词从字符级别开始。
  2. 统计相邻符号(symbol)对的出现频率。
  3. 将最频繁的符号对合并为一个新的符号。
  4. 重复该过程,直到达到预定的合并次数或词汇表大小。

1.2 预测

贪心最长匹配(Greedy Longest Match).
从左到右, 找到存在于词汇表中的最长子串.

#例子: 
from transformers import GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
tokens = tokenizer.tokenize("drawing badminton")
print(tokens) # 输出: ['draw', 'ing', 'bad', 'm', 'inton']

2. encoder 结构

在这里插入图片描述
灰色的框叫 block, decoder block 比 encoder block 多了一次 masked-attn 处理.

2.1 inputs

word -> token_id -> lookup ->emb

2.2 Positional Encoding

将token所在seq的位置信息加以编码表示. 可以是 {sin/cos三角函数, embedding中学习} 两种方案, ablation study 显示差异不大.

2.3 self-attention

source 与 target 是两个 sequence, self-attention 是指 对同一 sequence 内, 各个 token 间的关联做计算.
论文中把提出的 attention 叫 Scaled Dot-Product Attention.
引入三个共享参数矩阵, Query,Key,Value, 对 embedding 后的 token 做表达的学习.
所谓 attention, 就是这么一个映射关系, o u t p u t = f ( q u e r y , { k e y , v a l u e } ) output=f(query, \{key , value\} ) output=f(query,{key,value}).
key 和 value 的长度必须一致, query 则不要要求.
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V (1) Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V \tag 1 Attention(Q,K,V)=softmax(dk​ ​QKT​)V(1)

图2. o u t p u t = f ( q u e r y , { k e y _ v a l u e } ) output=f(query, \{key\_value\} ) output=f(query,{key_value})的直观图示

M u l t i H e a d ( Q , K , V ) = C o n c a t ( h e a d 1 , . . . , h e a d h ) W O MultiHead(Q,K,V)=Concat(head_1,...,head_h)W^O MultiHead(Q,K,V)=Concat(head1​,...,headh​)WO
where h e a d i = A t t e n t i o n ( Q W i Q , K W i K , V W i V ) head_i=Attention(QW_i^Q,KW_i^K,VW_i^V) headi​=Attention(QWiQ​,KWiK​,VWiV​), W O ∈ R h d v × d m o d e l W^O\in \mathbb R^{hd_v\times d_{model}} WO∈Rhdv​×dmodel​

2.4 feed-forward (add&norm)

是图片中 feed-forward 的超集, 也包含了 add&norm.
其中 add 残差的操作与时机可以参考

# feed-forward, add & norm
    def call(self, inputs, **kwargs):
        # Inner layer, ReLU
        outputs = Dense(self.num_units[0], activation=tf.nn.leaky_relu)(inputs)
        # Outer layer, linear
        outputs = Dense(self.num_units[1])(outputs)
        # Residual connection
        outputs += inputs
        # Normalize
        outputs = utils.modules.layer_norm(outputs)

        return outputs

3. decoder 结构

3.1 cross attention

除了 source_seq 与 target_seq 各自的 self_attrntion 之外, 还有二者之间的 cross attention (旧称 vanilla_attention). 对应图1 transformer 架构图中, decoder内的第二个 sub-layer. 此时有 vanilla_attention(decoder_Q, encoder_memory_K, encoder_memory_V).

3.2 causal mask

旧称 future mask.
decoder中有个特别点就是masking, masking 的作用就是防止在训练的时候 使用未来时刻才输出的单词。比如训练时,第一个单词是不能参考第二个单词的生成结果的(但可以参考自己)。Masking就会把这个信息变成 0,用来保证预测位置 i 的信息只能基于比 i 小的输出。
下面章节的tf实现部分, 会有 下三角矩阵的api说明.

3.3 logits 计算

与 vocab_emb 计算内积. 这种做法有以下几个优点:

  1. 参数共享:共享这两个层之间的权重可以减少模型中的总参数数量,有助于缓解过拟合,并且在实践中有时能带来更好的性能。
  2. 对称性:输入和输出使用相同的表示空间,这在某种程度上保持了模型结构的对称性。
  3. 初始化优势:如果使用预训练的嵌入,则这些预训练的权重也可以用于初始化最后的线性层,从而可能加速收敛。

四. train 与 predict 不同的 inference

4.1 train 与 loss

train 阶段, 直接输出整体的 logits #shape=(N, T2, vocab_size) , 与y_ = label_smoothing(tf.one_hot(y, depth=self.hp.vocab_size)) # (N, T2, vocab_size) 计算交叉熵.

4.2 predict 与 beam-search

在预测阶段, 根据 source sentence 的中间语义表示C和之前已经生成的历史信息, predicts y ^ \hat y y^​ auto-regressively.
y i = g ( C , y 1 , y 2 , . . . , y i − 1 ) y_i=g(C,y_1,y2_,...,y_{i−1}) yi​=g(C,y1​,y2,​...,yi−1​)
逐个位置选 token 时, 会选中 对数概率 最大的 那个 token, 属于 贪心 策略. 贪心能保证局部最优但可能错过全局最优. 可以类比 动态规划的背包问题.

所以引入 beam-search . 可以改善局部最优, 且增加翻译风格的多样性. 具体来说, 每个位置挑选token时, 选 top-n, 然后剪枝, 保留 n 个候选句子.
Q:那么最终如何从 n 个句子里挑最优呢?
A: 根据每个句子生成每个 token的 对数概率, 作连乘或平均, 当作句子整体的得分来比较.

五. tf 实现

没有纯净的官方 tf 实现. 因为官方基于 tf, 又搞了 tensor2tensor 库, 这里面才有 transformer 的实现.

不过, github 上有人基于 tf 给出了实现, 见参考 [3]. 但它是完全的面向过程风格, 没有 keras 面向对象的风格. 不好用.

  • 数据集
    Germany_2_English 翻译任务. 为了单机体验完整流程, 控制数据量. 训练集有 19万 样本, 验证集有 993 条样本. 英德的word加起来构成了 size=8000 的 vocabulary.
  • 超参
    v o c a b _ s i z e vocab\_size vocab_size=32000, d _ m o d e l d\_model d_model=512, m a x _ l e n _ s o u r c e = 100 max\_len\_source=100 max_len_source=100, m a x _ l e n _ t a r g e t = 100 max\_len\_target=100 max_len_target=100.
  • input/output
     	xs: tuple of
            x: int32 tensor. (N, T1) . T1为当前batch样本的最长原文长度.
            x_seqlens: int32 tensor. (N,)
            sents1: str tensor. (N,)
        ys: tuple of
            decoder_input: int32 tensor. (N, T2). T2为当前batch样本的最长译文长度.
            y: int32 tensor. (N, T2)
            y_seqlen: int32 tensor. (N, )
            sents2: str tensor. (N,)
    
    在这里插入图片描述
    在这里插入图片描述
  • 主干逻辑
def train(self, xs, ys):
    '''
    Returns
    loss: scalar.
    train_op: training operation
    global_step: scalar.
    summaries: training summary node
    '''
    # forward
    memory, sents1 = self.encode(xs)
    logits, preds, y, sents2 = self.decode(ys, memory)

    # train scheme
    y_ = label_smoothing(tf.one_hot(y, depth=self.hp.vocab_size))
    ce = tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=y_)
    nonpadding = tf.to_float(tf.not_equal(y, self.token2idx["<pad>"]))  # 0: <pad>
    loss = tf.reduce_sum(ce * nonpadding) / (tf.reduce_sum(nonpadding) + 1e-7)
  • future mask
diag_vals = tf.ones_like(inputs[0, :, :])  # (T_q, T_k)
# 详见, https://www.tensorflow.org/api_docs/python/tf/linalg/LinearOperatorLowerTriangular?version=stable, 对角线的元素被保留, 不置0.
# 但<=1.4 版本的tf, 是没有这个api的.  可以用 tf.matrix_band_part(input, -1, 0) 替代
tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense()  # (T_q, T_k)
masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(inputs)[0], 1, 1])  # (N, T_q, T_k)

paddings = tf.ones_like(masks) * padding_num
outputs = tf.where(tf.equal(masks, 0), paddings, inputs)

六. torch 实现

一个流行的实现见参考 [8].
logits 计算有两个选择: 1)与 vocab_emb 计算内积; 2)新开一个 linear. 可通过下面的配置选择.

class Transformer(nn.Module):
    def __init__():
        self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)

        if trg_emb_prj_weight_sharing:
            # Share the weight between target word embedding & last dense layer
            self.trg_word_prj.weight = self.decoder.trg_word_emb.weight

参考

1.transformer paper, Attention Is All You Need
2. blog, 机器翻译评价指标之BLEU详细计算过程
3. github 非官方实现, transformer
4. blog, 从Encoder-Decoder(Seq2Seq)理解Attention的本质
5. github, bert-use-demo
6. blog, Seq2Seq中的Attention和self-attention
7. my blog, residual connection, 残差链接
8. torch 实现
9. torch GPT 源码实现中的 Transformer 部分, myblog, GPT 系列笔记及简明源码

更多推荐