编码器
注意力负责交流,前馈网络负责加工,二者组装成完整的编码器。
目标:编码器
先给这节课的目标命名: 编码器(Encoder) ,把一句话「读」成 上下文相关向量序列的那一半模型。输入是一串 token ID,输出是每个词的 维向量,且每个向量都已充分混合了整句的上下文。
此前学过的多头注意力、位置编码、Add & Norm, 本课都会组装进编码器。动手之前,还差最后一个模块。
最后一个模块:前馈网络
前馈网络(Feed-Forward Network,FFN) 的结构非常朴素:两层全连接夹一个激活函数, 正是你最熟悉的 MLP:
第一层把 升维到 (通常是 4 倍),激活后第二层再降回 。
它有一个关键性质: 逐位置独立(position-wise) ,同一组权重 分别施加到句子里每个词的向量上, 词与词之间不交换任何信息 。 于是编码器内部的分工非常清晰:
对应到代码,前馈网络的实现也很简短:
组装编码器层
模块都齐了。一个 编码器层(EncoderLayer) 由两个子层串联而成, 每个子层外面都套上「残差与层归一化」中的 Add & Norm 模式:
代码里的多头自注意力,就是前面亲手实现的那一个,只是它的 forward 多接了一个 mask 入口,用来屏蔽句中的填充位(作用见后文「padding 掩码」一节)。 跟着下面的代码,逐步看这两个子层怎么串起来、形状为什么进出不变:
堆叠 N 层,组装完整编码器
完整的编码器 = 词嵌入 + 位置编码 + N 个编码器层:
原论文取 N = 6。注意力的感受野是全局的,每一层里每个词都能关注到整句所有词, 并非靠堆叠才逐渐「看得更宽」。层数越深,是在上一轮已混合的结果上反复加工, 表示越来越抽象;加深的是加工的深度,而不是看到的范围。
这里也回应一个此前的疑问:辛苦学过的 word2vec 词向量去哪了?Transformer 的嵌入层(上面的 nn.Embedding )通常与模型其余部分 联合从头训练 , 也可以用 word2vec 等预训练向量来初始化。但要分清:嵌入层本身仍是 静态查表 , 上下文相关性不来自它,而来自它之上的注意力层层加工。
一个工程细节:padding 掩码
实际训练时一批句子长短不齐,要用 <pad> 补齐到同一长度(「残差与层归一化」和「分词器实战」中都提过它)。但 <pad> 是填充物、没有语义, 不该参与注意力混合 。
办法:在 softmax 之前,把所有指向 pad 位置的注意力分数设为 。softmax 会把 变成权重 0,pad 被干净地屏蔽。这就是上面代码里 mask 参数的用途:
这里 mask 用 0 标记要屏蔽的 pad 位、其余位置为 1;至于它的具体形状、以及怎么从一批长短不一的句子里构造出来并对齐到注意力分数上,是分词后的常规步骤,本课不展开。
记住这个「softmax 前置 −∞」的手法。 mask 参数马上会有第二个、更重要的用途。
阶段性回顾:一词多义解决了吗?
输入一句话,编码器输出每个词的上下文相关向量。「同一个词、不同语境、不同向量」, 这个目标在 机制上 已经达成:
会读,还不会写
编码器能把句子「读懂」成向量序列。但很多任务不止于读,翻译要「输入一句、 生成 一句」, 模型还得学会 逐词写出 新句子。
而「写」面临一个「读」没有的难题:写到第三个词时,后面的词还不存在, 模型必须做到 不偷看自己还没写出的内容 。怎么用注意力做到这一点? 之后的《掩码注意力》会给出答案。