transformer(attention is all you need)文章阅读+重点代码理解
2025年开始,准备学习大模型相关的东西,所以从基础知识transformer开始补起。文章思路如下:以论文顺序讲解参考我学习的视频的部分简写了,详情见参考文献(链接)在必要的部分穿插了源码讲解目的:transformer是2017年由Google提出的,用于序列转导任务的模型(例如文本生成,翻译),以解决之前的基于RNN模型做此任务的问题(输入输出层必须等长、遗忘、只能串行运行)借鉴的研究FFN
·
〇、笔者前言
- 2025年开始,准备学习大模型相关的东西,所以从基础知识transformer开始补起。文章思路如下:
- 以论文顺序讲解
- 参考我学习的视频的部分简写了,详情见参考文献(链接)
- 在必要的部分穿插了源码讲解
一、介绍 & 背景
- 目的:transformer是2017年由Google提出的,用于序列转导任务的模型(例如文本生成,翻译),以解决之前的基于RNN模型做此任务的问题(输入输出层必须等长、遗忘、只能串行运行)
- 借鉴的研究:
- FFN(前馈神经网络 Feedforward Neural Network)
- 优点:可学习参数多
- 缺点:① 无法建模序列顺序;② 处理不同长度的输入输出效果差(因为输入输出层维度固定)
- RNN(循环神经网络 Recurrent Neural Network)
- 原始RNN
- 优点:① 能建模序列顺序(因为逐个输入);② 支持不定长输入
- 缺点:① 遗忘;② 输入输出必须等长
- 编码器-解码器结构
- 结构:把输出中间结果(编码器)、最终结果(解码器)的模型拆开
- 注意力机制
- 结构:给解码器赋权值
- 优点:解决遗忘问题
- 原始RNN
- FFN(前馈神经网络 Feedforward Neural Network)
- 核心创新点:
- 并行计算
- 只用自注意力机制、FFN、其他小组件
- 推荐一起学:【文献1】的视频前半部分 + PPT + 笔记(有丰富的用于解释的原理图)
二、模型结构
1.结构
1.1 论文部分
-
整体结构:编码器(左半部分),解码器(右半部分)
-
小部件:
- Input Embedding 词嵌入:自然语言根据词表,分词并投影成向量
- Positional Encoding 位置编码:正余弦函数
- Attention 注意力(上下文编码)
- 自注意力:Q、K、V来源于同一向量,仅投影矩阵W不同
- 交叉注意力:Q来自编码器,K、V来自解码器
- 掩码注意力:用于解码器,因为要隐藏此处及此处之后的词(即预测未来)
- 多头注意力:每个头关注的信息不一样,学完后拼起来再过线性层(~8个低清晰度结果,映射而非仅拼接,才能得1个高精度结果)
- Add & Norm 残差连接和归一化
- Linear:将向量映射到词表中,获取每个词的得分
- Softmax:将得分映射成概率
- Feed forward:FNN


-
注意力公式
- d k \sqrt {d_k} dk的原因:缩放。分析某行和某列相乘得到矩阵中的某个元素, q ⋅ k q · k q⋅k = ( q 1 ⋅ k 1 ) (q_{1} · k_{1}) (q1⋅k1) + ( q 2 ⋅ k 2 ) (q_{2} · k_{2}) (q2⋅k2) + … + ( q d ⋅ k d ) (q_{d} · k_{d}) (qd⋅kd) ,共 d k \sqrt {d_k} dk项加和,其中 V a r ( q i ⋅ k i ) = 1 Var(q_{i} · k_{i})=1 Var(qi⋅ki)=1,因此加和的方差为 d k {d_k} dk,标准差为 d k \sqrt {d_k} dk

- d k \sqrt {d_k} dk的原因:缩放。分析某行和某列相乘得到矩阵中的某个元素, q ⋅ k q · k q⋅k = ( q 1 ⋅ k 1 ) (q_{1} · k_{1}) (q1⋅k1) + ( q 2 ⋅ k 2 ) (q_{2} · k_{2}) (q2⋅k2) + … + ( q d ⋅ k d ) (q_{d} · k_{d}) (qd⋅kd) ,共 d k \sqrt {d_k} dk项加和,其中 V a r ( q i ⋅ k i ) = 1 Var(q_{i} · k_{i})=1 Var(qi⋅ki)=1,因此加和的方差为 d k {d_k} dk,标准差为 d k \sqrt {d_k} dk
-
笔者私货:是之后很多transformer的改进版本的思路源头。比如FNN→MoE,MHA、Lora→MLA
1.2 代码部分
- 先看整体流程
def forward(self, src_seq, trg_seq):
# 1. 编码器处理输入序列
src_mask = get_pad_mask(src_seq, self.src_pad_idx)
enc_output, *_ = self.encoder(src_seq, src_mask)
# 2. 解码器生成输出序列
trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)
dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
# 3. 线性层得到最终输出
seq_logit = self.trg_word_prj(dec_output)
return seq_logit.view(-1, seq_logit.size(2))
- 再看每个组件(组件内的子函数,如需详细了解,再查找即可)
class Encoder(nn.Module):
def forward(self, src_seq, src_mask):
# 词嵌入 + 位置编码
enc_output = self.src_word_emb(src_seq) # 论文中的"Input Embedding"
enc_output = self.dropout(self.position_enc(enc_output)) # "Positional Encoding"
# 经过多个编码器层
for enc_layer in self.layer_stack: # 论文中的Nx重复
enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask)
return enc_output
class Decoder(nn.Module):
def forward(self, trg_seq, trg_mask, enc_output, src_mask):
# 词嵌入 + 位置编码
dec_output = self.trg_word_emb(trg_seq)
dec_output = self.dropout(self.position_enc(dec_output))
# 经过多个解码器层
for dec_layer in self.layer_stack: # 论文中的Nx重复
dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
return dec_output
- 举个例子:翻译"I love you" → “我爱你”
# 假设输入:src_seq = [I, love, you] → 编码为 [1, 2, 3]
# 目标:trg_seq = [<start>, 我, 爱, 你] → 编码为 [0, 4, 5, 6]
# 1. 编码器处理英文
enc_output = encoder([1, 2, 3], src_mask) # 得到英文的上下文表示
# 2. 解码器逐步生成中文
# 第一步:输入 [0] → 预测 "我"
# 第二步:输入 [0, 4] → 预测 "爱"
# 第三步:输入 [0, 4, 5] → 预测 "你"
# 第四步:输入 [0, 4, 5, 6] → 预测 <end>
# 掩码确保每一步只能看到之前的词
2. 为什么用 self-attention
- 笔者私货:
- 是之后很多transformer的改进版本(比如swin transformer)的思路源头
- CNN和transfomer结合的模型在训练/推理时,经常在attention处报显存不够
- 为啥用自注意力,而非RNN、CNN,3个标准:
- 每层的总计算复杂度(Computational Complexity per Layer)
- 可并行化的计算量(Amount of Computation that can be Parallelized)
- 网络中长距离依赖之间的路径长度(Path Length between Long-range Dependencies)
- 表格
-
参数解释
参数 含义 例子 备注 n 序列的长度 一句话有10个词,n=10 由输入数据决定,约10~5000 d 词向量的维度 每个词用一个512维的向量表示,d=512 由模型设计决定。模型越大则越大,因为会算的更细更精确(比如描述一个人的特征[身高, 体重, 年龄, 性格分数, …]) ;基础模型d = 512,GPT-3的d=12288 k 卷积核的宽度 卷积核每次看3个词,k=3 ~关注的区间。CNN在区间内为静态,训练好后(推理时)权重(卷积核矩阵)就无法改变 r 受限注意力的邻域大小 r=10,则序列中第 50 个词,就只关注第 45 到 55 个词 ~关注的区间。attention在区间内为动态,权重由自注意力矩阵实时计算得到。注:投影矩阵W是静态的,属于模型参数;自注意力权重是计算得出的中间结果,”动静结合“ -
公式推导方式
层类型 每层计算复杂度 串行操作数 最长路径长度 自注意力 某序列给其他序列打分,共需 n ⋅ d n·d n⋅d次;所有n个序列都给其他序列打分,故 n 2 ⋅ d n^{2}·d n2⋅d / / RNN 第1个块把自己的特征传给第2个块,第2个块输出为h=Wx+h1,即第二个块的计算量为 d 2 d^{2} d2,共需传 n n n次,即 n ⋅ d 2 n·d^{2} n⋅d2 / 最坏的情况:第一个词想影响最后一个词,需要传递的长度即为序列长度n CNN 取决于卷积类型 每个局部窗口的计算都是独立的,所以可并行 用队伍传令兵(即卷积核)解释(不许交头接耳),传令兵必须走n步(普通卷积时,或者说n-k步,但 O ( ⋅ ) O(·) O(⋅)时不计入 -k)。注:普通卷积 O ( n ) O(n) O(n),扩张卷积 O ( l o g k ( n ) ) O(log_{k}(n)) O(logk(n)) 受限自注意力 某序列给其他序列打分,共需 r ⋅ d r·d r⋅d次;所有n个序列都给其他序列打分,故 r ⋅ n ⋅ d r·n·d r⋅n⋅d 每个局部窗口的计算都是独立的,所以可并行 用队伍交头接耳解释,长度为n的队伍,每个子队伍(不重叠)长度为r,故有n/r个子队伍,从第一个子队伍传到最后一个子队伍需n/r此信息交流。若队伍重叠一半也可,则2n/r,但是算 O ( ⋅ ) O(·) O(⋅)时不计入2
-
三、训练 & 结论
本文重点关注模型设计和结构,故具体的训练、结果章节未分析
参考文献
更多推荐



所有评论(0)