Transformer 是近几年深度学习领域绕不开的一个名字。很多同学从 CNN、RNN 一路学过来看到各类大模型、多模态模型都建立在 Transformer 之上难免会想这一套架构到底是怎么设计的QKV 是什么位置编码又是干什么的网上资料很多但要么偏数学推导要么只给调用代码很难有一条线把“从 Token 到训练”完整串起来。本文就用 PyTorch 从零实现一个可运行的 Transformer 编码器把词嵌入、位置编码、自注意力、多头注意力、残差连接、层归一化这些概念全部落实到代码里最后再用一个简单的文本分类训练任务验证效果。适合刚入门深度学习的同学也适合想要把理论真正“落地”的开发者。1. 凭什么 Transformer 能成为主流架构1.1 从序列建模的痛点说起在 Transformer 出现之前处理文本、语音等序列数据最常用的模型是 RNN 和 LSTM。它们的基本思路是“逐个读入”序列中的元素用一个隐藏状态把前面的信息记忆下来再传给下一个时刻。这个思路简单但也有两个很难绕开的毛病长距离依赖问题。信息要在时间方向上一步一步传递当序列很长时前面的内容很容易在传递过程中被“稀释”模型很难学到两个相隔很远词之间的关联。串行计算效率低。每个时刻的输出依赖前一个时刻的状态GPU 很难把整个序列的计算并行起来训练速度上不占优势。CNN 在处理图像时表现很好但卷积核的感受野是局部的想要覆盖长文本也需要堆很多层。于是一个既能直接建模全局关系、又能并行计算的架构就成了刚需。1.2 Transformer 的核心思想注意力取代循环Transformer 在 2017 年由 Google 团队在论文Attention Is All You Need中提出。它彻底抛弃了循环结构改用“自注意力机制”Self-Attention来刻画序列中任意两个位置之间的关系。一个词在编码时可以直接“看到”句子里的所有其他词并根据相关性分配不同的注意力权重。你可以这样通俗理解给模型一个句子“小明因为考试得了满分所以非常开心”当模型编码“开心”这个词时自注意力机制会帮它找到“满分”“考试”这些关键证据并让“开心”的向量表示中融入这些相关信息。这种并行地捕捉全局依赖的能力正是 Transformer 在长文本、大模型场景下表现优异的重要原因。基于 Transformer 延伸出的 BERT、GPT、ViT、Swin Transformer 等模型覆盖了自然语言处理、图像分类、目标检测、多模态理解等大量方向。因此把 Transformer 的核心组件亲手实现一遍理解它的前向传播过程对后续阅读源码、修改模型、甚至做模型部署都很有帮助。1.3 本文采用的实现路线Transformer 的完整结构包含 Encoder 和 Decoder 两大块。Encoder 负责把输入序列编码成上下文相关的向量表示Decoder 负责根据这些表示逐步生成输出序列。本文为了聚焦核心原理选择实现一个Encoder-only 的简化版 Transformer并把任务设计为文本分类。这样做的好处是不需要处理复杂的自回归生成逻辑我们可以把主要精力放在 Embedding、位置编码、多头自注意力、前馈网络、残差连接与层归一化这些通用组件上。掌握这些组件之后再去看 Encoder-Decoder 结构或者 GPT 的因果自注意力都会轻松很多。2. 环境准备与 PyTorch 安装2.1 本地环境建议本文代码基于 PyTorch 编写。不同操作系统、不同 GPU 环境下安装命令会有差异下面的步骤以最常见的 CPU/GPU 混合环境为例基本思路是通用的。# 使用 conda 创建独立环境避免依赖冲突 conda create -n transformer-demo python3.10 conda activate transformer-demoPyTorch 的安装建议直接参考 PyTorch 官网给出的命令。以 CUDA 11.8 为例可以使用类似下面的命令pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118如果你使用的是 CPU 环境可以把安装命令换成pip install torch torchvision torchaudio如果你的显卡是 AMD可以关注 PyTorch 官方针对 ROCm 发布的安装包如果只是做代码学习CPU 也完全足够跑通本文示例。需要注意的是PyTorch 版本迭代比较快不同版本的 API 有一些差异。本文代码使用的是nn.Module、nn.Linear、nn.LayerNorm等稳定 API在 PyTorch 1.13 到 2.x 的常见版本中都能正常运行。如果你在安装过程中遇到下载速度慢的问题可以考虑把 pip 源切换为国内镜像源例如pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple但要注意如果使用 NVIDIA GPU 且需要 CUDA 加速建议还是在官网选择正确的 CUDA 版本安装命令避免额外踩坑。2.2 验证安装结果安装完成后在终端里输入python进入交互式环境执行下面的代码import torch print(torch.__version__) print(torch.cuda.is_available())如果torch.cuda.is_available()输出True说明 GPU 可用输出False也不影响本次实验代码会在 CPU 上运行只是训练稍慢一些。3. Token 与 Embedding输入是如何变成向量的3.1 Token 到底是什么在深度学习模型里我们没法直接把“你好世界”这样的字符串喂给网络。第一步是把它切分成一个个最小单元这些单元就叫 Token。Token 可以是一个字、一个词也可能是通过 BPE 等算法切分出的子词片段。例如“我喜欢深度学习”可以拆成[我, 喜欢, 深度, 学习]每个 Token 在词表中都有一个唯一编号。我们可以给每个 Token 分配一个整数 ID比如{我: 2, 喜欢: 7, 深度: 11, 学习: 15}于是句子就变成了一个整数序列[2, 7, 11, 15]这个过程通常由分词器Tokenizer完成。在本文的简化实现里我们会跳过分词细节直接使用torch.randint生成模拟的 Token ID 序列来演示模型前向传播和训练流程。实际项目中你可以使用 HuggingFace 的BertTokenizer、GPT2Tokenizer等工具完成真实分词。3.2 词嵌入层的作用有了 Token ID 之后还需要把它们映射成稠密向量。这个映射层就是 Embedding 层。在 PyTorch 中nn.Embedding(vocab_size, d_model)可以理解为一个查找表给定 Token ID它返回对应的d_model维向量。import torch import torch.nn as nn vocab_size 1000 # 词表大小 d_model 128 # 隐藏维度 embedding nn.Embedding(vocab_size, d_model) tokens torch.randint(0, vocab_size, (4, 10)) # 模拟 batch4, seq_len10 的 Token ID embedded embedding(tokens) print(embedded.shape) # torch.Size([4, 10, 128])这里输出的张量形状是(batch_size, seq_len, d_model)含义是每个批次里有 4 个句子每个句子的长度是 10 个 Token每个 Token 被表示成一个 128 维向量。需要注意Embedding 层的参数是可学习的。在训练过程中网络会不断调整这些向量使语义相近的 Token 在向量空间中距离更近。这也是为什么经过预训练的 Embedding 往往比随机初始化的效果好很多。4. 位置编码给模型补上“顺序感”4.1 为什么必须加位置信息自注意力机制本身对输入顺序是不敏感的。它会把序列中所有 Token 当作一个集合来计算相关性无论“我打你”还是“你打我”如果把 Token 顺序打乱注意力计算的结果是完全一样的。可语言是讲究顺序的“我打你”和“你打我”语义完全不同。因此必须在输入中注入位置信息。解决方案是在 Embedding 向量上叠加一个“位置向量”。简单来说如果第 i 个 Token 的 Embedding 是e_i位置编码是p_i那么真正输入模型的就是x_i e_i p_i4.2 正余弦位置编码公式原版 Transformer 使用正余弦函数生成位置编码。对于位置pos的第i个维度编码公式如下PE(pos, 2i) sin(pos / 10000^(2i / d_model)) PE(pos, 2i 1) cos(pos / 10000^(2i / d_model))这个公式最巧妙的性质是不同位置产生的编码向量是周期性的、唯一的而且不需要额外学习参数。高频维度的变化较快负责区分相邻位置低频维度的变化较慢负责区分远距离位置。正余弦交替使用也让不同位置向量之间具备一定的线性关系便于模型捕捉相对位置信息。4.3 PyTorch 实现位置编码import math import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000, dropout0.1): super().__init__() self.dropout nn.Dropout(pdropout) # 预先计算好所有位置的编码 pe torch.zeros(max_len, d_model) position torch.arange(0, max_len).unsqueeze(1).float() div_term torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) # 偶数维度用 sin奇数维度用 cos pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) # 增加 batch 维度注册为 buffer不参与梯度更新 pe pe.unsqueeze(0) self.register_buffer(pe, pe) def forward(self, x): # x 形状: (batch, seq_len, d_model) seq_len x.size(1) x x self.pe[:, :seq_len, :] return self.dropout(x)这里有三个细节需要留意torch.arange(0, d_model, 2)取的是 0、2、4……这样的偶数索引对应公式中的2i。使用register_buffer注册位置编码它会被model.to(device)自动搬运到 GPU但不会参与梯度计算。forward中只取self.pe[:, :seq_len, :]是为了保证位置编码长度跟当前输入的序列长度一致避免形状不匹配。为什么选择正余弦而不是直接学一个位置向量一个原因是它可以外推到比训练时更长的序列长度另一个原因是正余弦编码包含大量的先验归纳偏置在序列长度不大时效果也不比可学习位置编码差。后续很多模型比如 BERT 使用的是可学习位置编码两种方案都可以没有绝对优劣。5. 自注意力机制让 Token 之间互相“关注”5.1 从相似度到注意力权重自注意力的核心是计算序列中任意两个 Token 之间的相关度。假设输入向量为x_1, x_2, ..., x_n我们希望为每个位置计算一个新的向量这个向量是其他所有位置向量的加权求和权重就由相关性决定。步骤可以拆解为计算查询向量q和键向量k的内积作为两两之间的相似度分数。将分数除以缩放因子sqrt(d_k)避免数值过大导致 Softmax 梯度消失。通过 Softmax 归一化成权重保证权重的和为 1。用权重对所有“值向量”v做加权求和。5.2 QKV 到底代表什么Q、K、V 是自注意力里最核心的三个概念很多人第一次接触时会觉得抽象。可以用一个检索场景来比喻Query查询表示你当前正在关注的信息内容。Key键表示序列中每个 Token 的“标签”用来和 Query 做匹配。Value值表示每个 Token 真正携带的信息内容。注意力分数Q * K^T就是 Query 和 Key 的匹配程度。匹配度越高对应的 Value 在最终输出中占的比重就越大。在实际代码里Q、K、V 都是通过线性变换从输入x得到的Q x W_Q K x W_K V x W_V这里的W_Q、W_K、W_V都是可学习的参数矩阵。它们给出的不是原始向量而是“经过投影后的表示”这种设计增加了模型的表达能力。5.3 多头注意力让模型从多个角度观察单头注意力相当于模型只用一组 QKV 投影观察一种“关联模式”。但序列中的关系往往是多层次的一个词可能既和句法主谓有关又和语义情感有关。多头注意力就是把d_model维空间切分成n_heads个子空间每个头独立做注意力计算最后拼接起来再做一次线性变换。例如d_model128、n_heads4每个头的维度就是128 / 4 32。这样既增加了模型的表达空间又不会显著增加计算量还让注意力权重更容易被理解和可视化。5.4 PyTorch 实现多头自注意力下面这段代码是本文的核心组件之一。我们先把代码完整贴出来再逐段解释。import math import torch import torch.nn as nn class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_heads, dropout0.1): super().__init__() assert d_model % n_heads 0, d_model 必须能被 n_heads 整除 self.d_model d_model self.n_heads n_heads self.head_dim d_model // n_heads self.scale math.sqrt(self.head_dim) self.w_q nn.Linear(d_model, d_model) self.w_k nn.Linear(d_model, d_model) self.w_v nn.Linear(d_model, d_model) self.w_o nn.Linear(d_model, d_model) self.dropout nn.Dropout(pdropout) def forward(self, x, maskNone): batch_size, seq_len, _ x.shape # 1. 生成 Q、K、V并拆分成多头 Q self.w_q(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) K self.w_k(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) V self.w_v(x).view(batch_size, seq_len, self.n_heads, self.head_dim).transpose(1, 2) # 2. 计算缩放点积注意力分数 scores torch.matmul(Q, K.transpose(-2, -1)) / self.scale # 3. 处理 mask将需要屏蔽的位置设为负无穷 if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) # 4. Softmax 归一化后加权求和 attn_weights torch.softmax(scores, dim-1) attn_weights self.dropout(attn_weights) context torch.matmul(attn_weights, V) # 5. 合并多头结果还原形状 context context.transpose(1, 2).contiguous().view( batch_size, seq_len, self.d_model ) output self.w_o(context) return output关键的形状变化如下x的形状是(batch, seq_len, d_model)。经过w_q(x)后仍然是(batch, seq_len, d_model)。view把它变成(batch, seq_len, n_heads, head_dim)transpose(1, 2)后变成(batch, n_heads, seq_len, head_dim)。这样每个头都能独立处理一整条序列。torch.matmul(Q, K.transpose(-2, -1))得到(batch, n_heads, seq_len, seq_len)其中每个元素表示对应位置上 Query 和 Key 的相似度。scale math.sqrt(self.head_dim)的作用非常关键。如果不缩放当head_dim很大时内积结果可能非常大Softmax 之后概率分布会变得很“尖锐”造成梯度消失影响训练稳定性。所以用head_dim的平方根做一次缩放。5.5 关于 mask 的说明在 Encoder 的文本分类场景中一般不需要 mask但在 Decoder 或 GPT 这类自回归模型中需要引入因果掩码causal mask确保当前位置只能看到自己及之前的位置看不到未来信息。这个 mask 通常是一个上三角矩阵对角线以上的位置设为False或 0对应位置在算注意力分数时被替换成负无穷。在本文的分类任务里我们会先不使用 mask。但代码中已经预留了mask参数后续你尝试实现 GPT 时可以直接复用。6. 层归一化、残差连接与前馈网络6.1 残差连接为什么不能省网络层数加深之后梯度在反向传播过程中容易消失或爆炸。残差连接的思路非常朴素把输入直接加到输出上。output x F(x)这样一来即使F(x)这一路的梯度很小输入那一路依然可以无损地把梯度回传模型训练起来会更稳定。Transformer 在自注意力和前馈网络之后都加了残差连接。6.2 LayerNorm 与 BatchNorm 的区别LayerNorm 是 Transformer 中使用的归一化方式。它与 BatchNorm 的区别在于归一化的维度不同BatchNorm 对每个特征维度跨 batch 做归一化因此依赖 batch 大小并且需要维护全局统计量。LayerNorm 对每个样本的单个层内所有神经元做归一化不依赖 batch 大小更容易适配变长序列。在(batch, seq_len, d_model)的张量上LayerNorm 通常对最后一维d_model做归一化也就是对每个 Token 的特征向量做标准化让均值为 0、方差为 1再进行可学习的缩放和平移。norm nn.LayerNorm(d_model)6.3 前馈网络与 Encoder 层组装Transformer 的 Encoder 层由两个子层组成多头自注意力子层配合残差连接和 LayerNorm。前馈网络子层配合残差连接和 LayerNorm。前馈网络就是两个线性层中间夹一个 ReLU 激活函数。第一个线性层会把维度从d_model放大到d_ff通常d_ff是d_model的四倍第二个线性层再把它压缩回去。先看前馈网络实现class FeedForward(nn.Module): def __init__(self, d_model, d_ff, dropout0.1): super().__init__() self.linear1 nn.Linear(d_model, d_ff) self.linear2 nn.Linear(d_ff, d_model) self.dropout nn.Dropout(pdropout) def forward(self, x): return self.linear2(self.dropout(torch.relu(self.linear1(x))))再组装完整 Encoder 层class EncoderLayer(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout0.1): super().__init__() self.self_attn MultiHeadAttention(d_model, n_heads, dropout) self.feed_forward FeedForward(d_model, d_ff, dropout) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout1 nn.Dropout(pdropout) self.dropout2 nn.Dropout(pdropout) def forward(self, x, maskNone): # 第一个子层多头自注意力 残差 LayerNorm attn_out self.self_attn(x, mask) x self.norm1(x self.dropout1(attn_out)) # 第二个子层前馈网络 残差 LayerNorm ff_out self.feed_forward(x) x self.norm2(x self.dropout2(ff_out)) return x很多实现会把dropout放在子层输出之后也有的放在残差之前效果差异不大关键是理解残差“把原输入绕过去”这一设计思想。实际中更常见的写法是x norm(x dropout(sublayer(x)))。之所以把 LayerNorm 放在残差之后而不是之前主要是为了保证每条残差路径上的梯度传播更顺畅。不同实现顺序各有理由但在实践中这两种排列都能工作读者不必过于纠结。7. 手撕一个可训练的简化版 Transformer 分类器7.1 总体模型结构我们现在把前面的组件全部拼起来得到一个完整的 Encoder-only Transformer 分类器。它的前向流程是Token ID - Embedding - 位置编码 - N 层 Encoder - LayerNorm - 取 [CLS] 向量 - 分类输出这里用了一个常见技巧在序列最前面插入一个特殊 Token[CLS]最后一层 Encoder 输出中对应[CLS]位置的向量就可以作为整条序列的语义聚合表示送入分类头。这个技巧在 BERT 中使用得非常多。在很多实际实现中[CLS]是一个专门学习的向量拼在输入序列的最前面。这里我们简化一点因为令牌序列本身就是随机数我们不需要单独构造特殊 Token直接约定“取第 0 个位置的向量做分类”即可。这样能让代码保持简洁同时不改变整体思路。完整模型代码如下import torch import torch.nn as nn class TransformerClassifier(nn.Module): def __init__(self, vocab_size, d_model, n_heads, d_ff, n_layers, max_len, num_classes, dropout0.1): super().__init__() self.embedding nn.Embedding(vocab_size, d_model) self.positional_encoding PositionalEncoding(d_model, max_len, dropout) self.layers nn.ModuleList([ EncoderLayer(d_model, n_heads, d_ff, dropout) for _ in range(n_layers) ]) self.norm nn.LayerNorm(d_model) self.classifier nn.Linear(d_model, num_classes) def forward(self, tokens, maskNone): x self.embedding(tokens) x self.positional_encoding(x) for layer in self.layers: x layer(x, mask) x self.norm(x) # 取第 0 个位置的向量作为整条序列的表示 cls_repr x[:, 0, :] logits self.classifier(cls_repr) return logits7.2 构造模拟训练数据因为本文重点是模型实现这里用随机生成的 Token 和标签模拟一个分类数据集。batch_size表示每个批次的样本数seq_len表示每条文本的长度。def make_batch(batch_size, seq_len, vocab_size, num_classes): tokens torch.randint(0, vocab_size, (batch_size, seq_len)) labels torch.randint(0, num_classes, (batch_size,)) return tokens, labels你需要理解的是真实场景中 tokens 应该来自 Tokenizer 对原始文本的编码labels 来自人工标注。上述代码只是为了快速验证模型前向和训练流程是否正常。7.3 初始化模型并设置训练参数我们选择一个偏小的模型配置这样在 CPU 上也能快速完成训练演示model TransformerClassifier( vocab_size1000, d_model128, n_heads4, d_ff512, n_layers2, max_len64, num_classes2, dropout0.1 ) # 用一个很小的输入测试前向 tokens, labels make_batch(batch_size4, seq_len32, vocab_size1000, num_classes2) logits model(tokens) print(logits.shape) # torch.Size([4, 2])如果输出形状为torch.Size([4, 2])说明模型前向流程基本正确。接下来就可以进入训练环节。7.4 训练循环训练过程采用标准的交叉熵损失和 Adam 优化器。这里我们只迭代 20 轮每轮内部随机生成 100 个批次用来观察 loss 是否在下降。import torch.optim as optim criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) model.train() for epoch in range(20): total_loss 0.0 for _ in range(100): tokens, labels make_batch( batch_size32, seq_len32, vocab_size1000, num_classes2 ) optimizer.zero_grad() logits model(tokens) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() avg_loss total_loss / 100 print(fEpoch {epoch 1:02d}, Loss: {avg_loss:.4f})由于标签是随机生成的模型不可能学到真正的规律loss 不会降到特别低但应该会从大约 0.7 附近开始下降。如果你把某一类样本的比例固定下来或者构造一个线性可分的规则数据集就能看到更明显的训练效果。7.5 增加一个可学习的简单分类任务为了让结果更具说服力我们可以构造一个非常简单的规则任务当每条 Token 序列中出现某个特殊 ID 时标签为 1否则为 0。这样模型确实能学到一些模式loss 会明显下降。def make_rule_batch(batch_size, seq_len, vocab_size): special_id 99 tokens torch.randint(0, vocab_size, (batch_size, seq_len)) # 如果序列中出现 special_id标签为 1 labels (tokens special_id).any(dim1).long() return tokens, labels替换训练循环里的make_batch为make_rule_batch再运行同样的训练代码loss 会稳定下降到较低水平。这说明 Transformer 确实从数据中捕捉到了“某个 Token 出现与否”这个特征并把它编码到了[CLS]位置的表示中。7.6 完整代码文件建议为了方便你保存和运行建议把代码按下面方式组织transformer-demo/ |-- model.py # 模型组件与 TransformerClassifier |-- train.py # 数据生成与训练循环model.py中存放PositionalEncoding、MultiHeadAttention、FeedForward、EncoderLayer、TransformerClassifier。train.py中存放make_batch、make_rule_batch和训练逻辑。8. 常见报错与排查思路手撕 Transformer 的过程中最常见的报错往往集中在形状不匹配上。下面整理了几个高频问题。问题现象常见原因解决思路注意力机制报错说维度无法相乘Q、K、V 的 shape 没有对齐检查view和transpose后的形状重点确认(batch, n_heads, seq_len, head_dim)是否正确训练时 loss 不下降学习率过高或过低数据无规律先在小数据上跑通尝试调整学习率观察梯度是否正常位置编码越界输入序列长度大于max_len把max_len设置得比训练/推理序列更长或者在 forward 中限制长度CUDA 内存不足batch 太大或序列太长减小 batch、减小d_model、使用混合精度训练mask 导致全为负无穷mask 形状或数值逻辑错误打印 mask 和 scores 的 shape确认需要屏蔽的位置是否被设置为 0d_model无法被n_heads整除多头注意力要求整除使用 assert 提前检查或选择合适配置排查这类问题有一个通用技巧不要在报错后盲目改代码先在关键张量之间插入print(shape)看每一层的输出形状是否符合预期。尤其是view、transpose、matmul这几个操作是最容易出现形状问题的地方。另一个经常被忽略的问题是 device 不匹配。如果你使用 GPU 训练记得在模型和数据上都调用.to(device)device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) tokens tokens.to(device) labels labels.to(device)如果模型参数在 GPU 上、数据在 CPU 上PyTorch 会抛出类似Expected all tensors to be on the same device的报错。9. 工程实践与后续学习建议9.1 让代码更接近真实项目的几个改进点本文为了把原理讲清楚做了大量简化。实际工程中你还需要关注以下几个方面词表与分词器。使用 BPE、WordPiece 或 SentencePiece 训练自己的分词器而不是按字随机切分。预训练好的 Tokenizer 通常更符合真实文本分布。Padding 与 Attention Mask。一个 batch 里文本长度不同需要把短序列填充到相同长度并用 mask 让模型忽略 padding 位置否则填充位会影响注意力计算。学习率调度。Transformer 训练通常配合 Warmup 和余弦退火常见做法是先用较小学习率热身再逐步衰减。这里推荐使用 PyTorch 的torch.optim.lr_scheduler实现。梯度裁剪。对于序列模型clip_grad_norm_能有效防止梯度爆炸是训练稳定性的重要保障。混合精度训练。用torch.cuda.amp可以显著减少显存占用并提升训练速度特别是模型较大时收益明显。实验记录。保存模型权重、记录每次实验的超参数和 loss 曲线。可以使用tensorboard或简单的 JSON 文件记录。9.2 从 Encoder 到 Encoder-Decoder 再到 GPT掌握本文的 Encoder 代码后你的下一步可以按下面顺序推进实现完整的Transformer Encoder-Decoder把 Decoder 里的 Cross-Attention 加上用于机器翻译任务。实现因果自注意力。只需要把 mask 换成上三角掩码把[CLS]分类头改成逐 Token 的预测头就是一个简化版 GPT。使用 HuggingFacetransformers库加载一个预训练模型对比自己的实现学习工业级代码的组织方式。尝试把 Transformer 用到图像上。ViT 把图片切成 Patch 后当作 Token 序列几乎是复用同一套代码。9.3 几个容易踩的坑不要一上来就追求大模型。先在小规模数据上跑通流程再逐步扩大。不要随意调整d_model和n_heads记得d_model必须能被n_heads整除。不要忽略数据顺序。位置编码是必须的不要因为测试时模型能跑就跳过去。不要把 dropout 设得过大。Transformer 对 dropout 比较敏感小模型尤其如此一般 0.1 左右比较稳妥。10. 总结本文从零开始实现了 Transformer 的核心组件Token Embedding、正余弦位置编码、多头自注意力、前馈网络、残差连接与层归一化并组装成了一个可用于文本分类的 Transformer 编码器。你可以跟着代码在本地跑通一次完整的训练流程观察 loss 的变化再逐步加入 mask、真实分词器、学习率调度等工程细节。手撕一遍之后再去看 BERT、GPT 或者 ViT 的源码你会发现自己对“自注意力如何并行建模全局依赖”已经有了直观认识。剩下的就是在小数据集上多做实验亲手调一调参数把每个张量的形状变化印在脑子里。代码写熟之后Transformer 就不再是纸面上的公式而是一个随时可以改动、可以调试、可以扩展的工具。