
Transformer这个词这两年只要碰过AI、NLP、大模型的人耳朵都听出茧子了。但你真让我一句话说清楚它是什么很多朋友反而卡住了。更别说网上那些教程一上来就是Q、K、V、多头注意力、位置编码公式铺满一屏看完除了头大没有任何收获。这篇我打算换个讲法。不堆公式不贴论文就用大白话把Transformer到底干了什么、每个部件为什么存在、手写一个最小的实现需要多少代码以及我实际训练中踩过的坑一次性讲透。无论是刚入门深度学习的学生还是工作中要用到Transformer做项目的工程师这篇文章都值得你花二十分钟读完。1. Transformer到底解决了什么问题先搞懂它为什么出现1.1 从RNN和LSTM的“记性差”说起在Transformer出现之前处理序列数据文本、语音、时间序列的主力是RNN循环神经网络和它的升级版LSTM。这两兄弟的思路是“按顺序读”就像一个人逐字逐句读书每读到一个新词都要结合脑子里记住的“上文”来理解当前这个词。这个思路有问题吗有而且问题不小。第一个问题是“记性差”。你让RNN去翻译一个长句子比如100个词等到读到第80个词的时候第3个词的信息早就被“冲淡”了。LSTM用门控机制改善了这一点但依然是按顺序处理长距离依赖仍然是硬伤。这就像让你记一串电话号码读到后面忘了前面。第二个问题是“不能并行”。RNN必须一个词一个词地算第t个词的计算依赖第t-1个词的结果所以GPU再强也帮不上忙训练速度被卡死在串行逻辑上。2017年那会儿训练一个像样的翻译模型要好几周其中很大一部分时间都耗在了这个顺序依赖上。这两个问题叠加就导致了一个尴尬局面模型不大但训练极慢效果还不理想。行业急需一种能“一眼看完整句话”的架构。1.2 Transformer的破局思路让每个词同时看完整句话Transformer给出的答案是不要按顺序读了一次性把整句话丢给模型让每个词跟句子里的所有其他词同时“对话”看看谁跟谁的关系更紧密。打个比方。你在一个派对上满屋子都是人词你要判断“苹果”这个词在当前句子里的意思到底是“手机品牌”还是“一种水果”。你不需要按什么顺序去听每个人说话你只需要扫视全场关注那些跟你关系最紧密的人——“iPhone”这个词会告诉你“苹果”是品牌“好吃”这个词会告诉你“苹果”是水果。这种“全员同时互相对话按关系紧密程度聚合信息”的机制就是自注意力Self-Attention。它有两个直接好处任意两个词之间的距离都是1不管它们在句子里隔了多远都能直接建立联系长距离依赖问题迎刃而解。词与词之间的计算是独立的可以完全并行GPU终于能满负荷跑起来了。这就是Transformer名字的由来它不是某种玄学架构而是一个“全员直接对话”的信息处理框架。理解了这一点后面所有细节都是在回答一个问题怎么把这个“全员对话”做得又快又好。2. 核心组件逐一拆解自注意力、多头、位置编码到底在干嘛2.1 Self-Attention的完整工作流程Q、K、V是怎么算出来的自注意力是Transformer的心脏。网上充斥着各种公式其实它的本质就是三步提问、匹配、取内容。假设我们有一个句子“猫坐在垫子上”经过词嵌入Embedding之后每个词变成一个向量比如512维。接下来每个词的向量会乘以三个不同的权重矩阵得到三个新向量Query查询向量Q代表“我想找什么”。Key键向量K代表“我能提供什么线索”。Value值向量V代表“我真正的内容是什么”。用刚才派对的例子来说Q是你在派对上问“谁跟苹果有关”K是每个人的自我介绍“我是iPhone”“我是香蕉”V则是每个人真正想传递的信息。具体计算过程是这样的对于句子里的每个词 i用它的Q向量去跟所有词包括它自己的K向量做点积得到一个相似度分数。分数越高说明这个词跟当前词的关系越紧密。然后把这个分数除以 d_k 的平方根d_k是K向量的维度除以它是为了不让点积结果太大导致后续的softmax进入饱和区梯度变小再做softmax归一化得到一组和为1的权重。最后用这组权重对所有词的V向量做加权求和得到词i的新表示。这一步做完每个词就“吸收”了整个句子的信息而且知道了信息该从哪里重点获取。一次计算全局视野这就是自注意力的精髓。我在实际写代码时发现一个特别容易忽略的点Q、K、V虽然是同一个输入句子生成的但经过三个不同的线性变换后它们已经代表了三种不同角色。很多初学者误以为QKV那注意力就退化成单纯的相似度比较了效果会大打折扣。2.2 Multi-Head Attention一个注意力不够就分多头来凑单个自注意力机制有一个问题它只能学习一种“关系模式”。但句子里的关系是多样的比如语法关系主谓宾、指代关系“它”指代“猫”、语义关联“吃”和“食物”等等。如果只用一组Q、K、V就相当于只用一个视角看问题太片面了。多头注意力Multi-Head Attention的做法很简单把Q、K、V的维度切成h份每份用一组独立的权重矩阵做自注意力得到h个不同的表示最后把它们拼接起来再过一次线性变换。用生活化的说法单头注意力是一个人在看全场的对话多头注意力是请了好几个专家每个专家看不同的侧面。有的专家专门看语法搭配有的专家专门看语义相近有的专家专看看代词指代。最后把专家的意见汇总起来得到更立体、更全面的理解。h头数在经典Transformer里取8每个头的维度是 d_model / h 512 / 8 64。这里有个实操注意点头数并不是越多越好头数太多会导致每个头的维度太小表达能力不足反而掉点。BART-large、GPT-3这些大模型把头数加到12到96但那是配合超大模型规模的普通任务8个头够用。2.3 位置编码没有顺序信息的Transformer就是词袋模型自注意力有一个“致命”特点它是置换不变的。什么意思就是你把“猫坐在垫子上”改成“垫子坐在猫上”Transformer计算出来的表示一模一样。因为注意力机制只关心词与词之间的相似度完全不关心它们在句子里的先后位置。但语言的顺序是极其重要的“我爱你”和“你爱我”意思完全不同。所以Transformer必须人为地注入位置信息这就是位置编码Positional Encoding存在的意义。原始Transformer用的是正弦余弦函数编码公式不太需要硬记关键是它的设计理念每个位置得到一个独一无二的编码向量。不同维度的编码频率不同所以相邻位置的编码既有相似性也有差异性模型能学到相对位置关系。实操中很多新项目直接改用可学习的位置嵌入Learned Positional Embedding比如BERT就是这么做的。它在训练时把每个位置当作一个“词”来学习嵌入向量实现更简单效果也不差。我在自己的项目里两种都用过如果序列长度固定比如分类任务可学习位置嵌入方便省事如果序列长度变化大比如长文本生成用正弦余弦编码更稳因为它在推理时能外推到更长的位置。2.4 残差连接、层归一化和前馈网络稳定训练的“三件套”每次多头注意力计算完之后并不是直接往下传而是要经过一套“稳定三件套”残差连接Residual Connection、层归一化Layer Normalization和前馈网络Feed-Forward Network, FFN。先说残差连接。它的思想是把输入和输出直接相加让梯度有一条“高速公路”可以畅通无阻地回传。为什么要这样因为Transformer层数一多BERT-base有12层梯度在深层网络中很容易消失残差连接能保证每层至少能学到一个“不劣化”的变换。我在研究生阶段自己搭过没有残差的Transformer训练6层就已经很难收敛了加了残差之后12层稳稳地跑。再说层归一化。它跟批归一化Batch Normalization不一样不是跨样本做归一化而是对同一个样本的所有特征维度做归一化。这样做的好处是每个样本的数值分布都是稳定的不受batch大小影响也不受序列长度影响特别适合变长序列的场景。最后是前馈网络。它是一个两层的全连接网络中间用ReLU激活函数维度先扩大再缩小经典配置是 512 → 2048 → 512。这个网络的作用是对每个位置的信息做一次“深度学习”因为注意力机制本质上是“加权求和”是个线性操作表达能力有限必须靠FFN引入非线性变换模型才能学到复杂的模式。3. 从零手写一个迷你Transformer代码说人话3.1 环境准备与整体结构规划理论聊完必须上手写代码。我接下来用PyTorch实现一个最小可用的Transformer目标是翻译一个非常简单的任务把英文单词翻译成中文单词让你直观感受整个流程。环境很简单pip install torch numpy整体结构我按5个模块来组织词嵌入层把词索引映射成向量。位置编码层注入顺序信息。多头注意力层核心的信息交换模块。前馈网络层非线性变换模块。最终输出层预测下一个词的概率分布。我没有刻意写得很精简而是按工程习惯分文件、分函数这样你能直接照着改造成自己项目的骨架。3.2 实现多头注意力核心代码逐行注释先来看多头注意力的实现这是整个Transformer最核心的部分。我在代码里加了详细注释每个张量的形状变化都标注了。import torch import torch.nn as nn import math 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.d_k d_model // n_heads # 每个头的维度 # Q、K、V各自独立的线性变换 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(dropout) def forward(self, x, maskNone): batch_size, seq_len, _ x.size() # 1. 生成Q、K、V并拆分成多头 # 形状: [batch, seq_len, d_model] - [batch, seq_len, n_heads, d_k] Q self.w_q(x).view(batch_size, seq_len, self.n_heads, self.d_k) K self.w_k(x).view(batch_size, seq_len, self.n_heads, self.d_k) V self.w_v(x).view(batch_size, seq_len, self.n_heads, self.d_k) # 2. 交换维度方便做批量矩阵乘法 # [batch, n_heads, seq_len, d_k] Q Q.transpose(1, 2) K K.transpose(1, 2) V V.transpose(1, 2) # 3. 计算注意力分数: Q K^T / sqrt(d_k) # [batch, n_heads, seq_len, seq_len] scores torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) # 4. 应用maskpadding mask或因果mask if mask is not None: scores scores.masked_fill(mask 0, float(-inf)) # 5. softmax归一化得到注意力权重 attn_weights torch.softmax(scores, dim-1) attn_weights self.dropout(attn_weights) # 6. 加权求和得到输出 # [batch, n_heads, seq_len, d_k] context torch.matmul(attn_weights, V) # 7. 合并多头 # [batch, seq_len, n_heads, d_k] - [batch, seq_len, d_model] context context.transpose(1, 2).contiguous().view( batch_size, seq_len, self.d_model ) return self.w_o(context)这里我踩过一个实实在在的坑拆多头时用view还是reshape以及transpose之后要记得contiguous。在你代码里如果你忘记加.contiguous()直接.view()会报错因为transpose之后张量的内存布局不是连续的。排查这类报错花了我不少时间所以你直接照着我这个写法来能少走弯路。另一个注意点mask的作用是屏蔽padding位置和未来位置。padding mask用于防止模型把注意力放在无意义的填充词上因果maskcausal mask用于自回归生成任务保证模型生成第t个词时看不到t之后的词。这是做GPT类模型的核心后面讲推理时会提到。3.3 完整TransformerBlock与位置编码实现有了多头注意力接下来构建一个完整的Transformer Block。结构是多头注意力 → 残差连接 → 层归一化 → 前馈网络 → 残差连接 → 层归一化。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(dropout) def forward(self, x): # 中间层用ReLU激活扩展维度再压缩回来 x self.linear1(x) x torch.relu(x) x self.dropout(x) x self.linear2(x) return x class TransformerBlock(nn.Module): def __init__(self, d_model, n_heads, d_ff, dropout0.1): super().__init__() self.attention MultiHeadAttention(d_model, n_heads, dropout) self.ffn FeedForward(d_model, d_ff, dropout) self.norm1 nn.LayerNorm(d_model) self.norm2 nn.LayerNorm(d_model) self.dropout nn.Dropout(dropout) def forward(self, x, maskNone): # 子层1多头注意力 残差连接 层归一化 attn_output self.attention(x, mask) x x self.dropout(attn_output) # 残差连接 x self.norm1(x) # 层归一化 # 子层2前馈网络 残差连接 层归一化 ffn_output self.ffn(x) x x self.dropout(ffn_output) # 残差连接 x self.norm2(x) # 层归一化 return x注意我这里的顺序先残差连接再层归一化。这是Post-LN的写法也是原始Transformer论文里的顺序。但实际工程里很多新模型采用Pre-LN也就是先归一化再进子层顺序反过来。两者的区别在于Pre-LN训练更稳定对学习率不那么敏感适合深层模型Post-LN理论上表达力更强但训练时需要更精细的学习率调节。我在自己的项目里深度超过12层就优先选Pre-LN稳定得多。位置编码的实现比较简单class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len5000): super().__init__() # 预计算位置编码矩阵 pe torch.zeros(max_len, d_model) position torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) # 用指数衰减的规律设置不同维度的频率 div_term torch.exp( torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model) ) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) pe pe.unsqueeze(0) # [1, max_len, d_model] self.register_buffer(pe, pe) # 不参与训练随模型移动设备 def forward(self, x): # x: [batch, seq_len, d_model] return x self.pe[:, :x.size(1)]位置编码我特意用register_buffer而不是直接存为普通张量这样编码矩阵会跟着模型一起移动到GPU但不会参与参数更新。这是PyTorch里一个非常实用的细节。3.4 完整训练流程从数据准备到损失计算有了以上组件可以拼装完整的Transformer编码器并跑一个简单任务。我做一个经典的复制任务copy task输入一串数字序列模型学习把输入序列原样输出。class TransformerEncoder(nn.Module): def __init__(self, vocab_size, d_model128, n_heads8, d_ff512, num_layers3, max_len50, dropout0.1): super().__init__() self.embedding nn.Embedding(vocab_size, d_model) self.pos_encoding PositionalEncoding(d_model, max_len) self.layers nn.ModuleList([ TransformerBlock(d_model, n_heads, d_ff, dropout) for _ in range(num_layers) ]) self.norm nn.LayerNorm(d_model) self.fc_out nn.Linear(d_model, vocab_size) def forward(self, x, maskNone): x self.embedding(x) x self.pos_encoding(x) for layer in self.layers: x layer(x, mask) x self.norm(x) return self.fc_out(x)训练时的几个关键设置import torch.optim as optim # 参数配置 vocab_size 32 # 词表大小用0-30的数字作为token31作为结束符 max_len 32 # 最大序列长度 batch_size 64 n_epochs 50 learning_rate 1e-3 model TransformerEncoder(vocab_sizevocab_size) criterion nn.CrossEntropyLoss(ignore_index0) # 忽略padding位置 optimizer optim.Adam(model.parameters(), lrlearning_rate, betas(0.9, 0.98), eps1e-9) # 生成简单的训练数据 def generate_data(batch_size, max_len): # 随机生成序列长度3~15每个位置取值1~30 lengths torch.randint(3, 16, (batch_size,)) data torch.zeros(batch_size, max_len, dtypetorch.long) for i, l in enumerate(lengths): data[i, :l] torch.randint(1, 31, (l,)) return data # 训练循环 for epoch in range(n_epochs): total_loss 0 for step in range(200): x generate_data(batch_size, max_len) # 输入和标签错开一位输入第t个位置预测第t1个位置 input_seq x[:, :-1] target_seq x[:, 1:] output model(input_seq) # [batch, seq_len-1, vocab_size] loss criterion(output.reshape(-1, vocab_size), target_seq.reshape(-1)) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) # 梯度裁剪 optimizer.step() total_loss loss.item() if (epoch 1) % 10 0: print(fEpoch {epoch1}/{n_epochs}, Loss: {total_loss/200:.4f})这里有几个训练细节值得特别注意梯度裁剪是必须的。Transformer的训练经常会出现梯度爆炸特别是训练的早期阶段。clip_grad_norm_相当于给梯度设了一个速度上限防止参数一步更新太多导致损失飞掉。我在不裁剪的情况下跑这个实验大概20轮左右就会遇到NaN。输入和标签错开一位是因果语言模型的标准做法。输入第1到第t-1个词模型预测第2到第t个词。这样模型每个位置都在“预测下一个词”符合自回归的逻辑。CrossEntropyLoss的ignore_index0是为了让模型忽略padding位置的损失。如果不加这个模型会花大量精力去“学习”预测padding标记而这部分知识毫无意义。4. 训练Transformer的实操心得学习率、优化器与稳定性4.1 学习率调度warmup为什么不是玄学训练Transformer的学习率设置跟传统神经网络很不一样。如果你直接用一个固定的学习率比如1e-3前期很可能会遇到训练不稳定的问题甚至直接发散。业界标准的做法是warmup 衰减。具体来说前N步比如4000步学习率从0线性增长到一个峰值然后按照逆平方根规律衰减。这样做有它的道理训练初期模型的权重是随机初始化的梯度方向噪声很大如果学习率太高参数会被推到很差的区域一旦进入这个区域就很难出来。先用小学习率“热身”让模型对数据分布有一个初步的感知再加大学习率加速收敛效果会好很多。我在实际项目里更实用的做法是用一个简单好调的调度器前5%的训练步数做warmup之后用余弦衰减Cosine Annealing逐渐把学习率降到一个极小值。这个方案在大多数任务上表现稳定不需要频繁调参。def get_learning_rate(step, d_model, warmup_steps4000): Transformer原论文中的学习率公式 step max(step, 1) # 防止0 return d_model ** (-0.5) * min(step ** (-0.5), step * warmup_steps ** (-1.5))4.2 优化器的选择Adam还是AdamWTransformer标配优化器是Adam但我会建议直接用AdamW。AdamW是Adam的改进版本把权重衰减weight decay从loss里解耦出来单独作用在参数上。这样做的好处是正则化效果更干净不会跟Adam的动量项互相干扰。具体配置上我推荐学习率峰值1e-4 到 1e-3之间小模型取高值大模型取低值。betas取(0.9, 0.98)第二个beta取0.98而不是默认的0.999因为Transformer训练中梯度的二阶矩变化比较剧烈0.999会显得“反应太慢”。weight decay1e-4左右。如果显存允许优先用混合精度训练AMP能显著加速且几乎不掉精度。optimizer optim.AdamW( model.parameters(), lr1e-4, betas(0.9, 0.98), eps1e-9, weight_decay1e-4 )注意eps取1e-9而不是PyTorch默认的1e-8别问我为什么这个问题在Transformer论文的footnote里专门提过Adam的eps选得过大会导致训练不稳定特别是在FP16混合精度场景下。我实测改为1e-9之后训练波动明显变小。5. 常见问题与排查实录我把踩过的坑都列在这5.1 损失不降或直接爆炸这是Transformer新手遇到最多的问题。我总结下来80%的情况是以下几种原因第一学习率太大。这是头号嫌疑。解决方式把学习率降到原来的十分之一或者加上warmup。如果你用1e-3起手就炸改成3e-4或者1e-4试试。我见过一个项目仅仅把学习率从1e-3降到5e-4就把训练从“完全发散”救回了“正常收敛”。第二没有梯度裁剪。在参数更新的最开始10步梯度爆炸是很常见的。加上clip_grad_norm_(max_norm1.0)能兜底。如果裁剪之后损失还是不稳定把阈值降到0.5。第三初始化问题。如果你的模型是自己搭的检查一下输出层和Embedding层是否共享权重以及Attention里的矩阵是否用了Xavier初始化。PyTorch默认的初始化其实还行但如果你手动改了初始化方式要确认方差是合理的。5.2 显存不够怎么办Transformer是显存大户尤其是长序列。我在处理长文本时经常遇到OOMOut of Memory报错。以下几个策略我屡试不爽梯度累积Gradient Accumulation不要一次塞进大batch而是用小batch算几步把梯度累加起来攒够了再更新一次参数。这相当于“用时间换显存”。混合精度训练AMPPyTorch自带torch.cuda.amp能把大部分计算降到FP16显存直接砍半速度还更快。注意在loss缩放时用GradScaler。减少序列长度如果任务允许把输入截断到模型实际需要的长度。很多场景下序列长度从512降到256效果几乎不掉但显存骤降。使用梯度检查点Gradient Checkpointing以增加约30%的计算时间为代价把显存占用降到原来的1/3左右HuggingFace的模型大多支持。# 梯度累积示例 accumulation_steps 4 optimizer.zero_grad() for i, batch in enumerate(dataloader): loss model(batch) loss loss / accumulation_steps # 均分保证总loss量级一致 loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()5.3 推理速度太慢Transformer推理是逐token生成的天生串行确实慢。有几种提速方案KV Cache键值缓存在自回归生成时每一步的Q、K、V其实大量重复计算了历史token的结果。把之前算过的K和V缓存起来每个新token只需要计算一次能带来数倍加速。HuggingFace的generate接口默认就做了这个优化。批处理Beam Search Batch一次性喂多个候选序列用batch推理代替单序列串行吞吐量能上来。量化Quantization把模型从FP16量化到INT8显存占用和速度都有改善但精度会有轻微损失。5.4 模型训练与验证指标不一致训练损失降得很好但验证集效果差。这个问题第一嫌疑是过拟合。Transformer参数量大小数据集上很容易“背答案”。解决办法加大Dropout尤其是注意力权重里的Dropout。加早停Early Stopping验证集指标变差就停止。做数据增强文本任务里常用的有回译、随机mask、同义词替换等。第二个嫌疑是训练和推理的mask不一致。训练时用了padding mask推理时忘了传入或者形状不匹配导致注意力分布完全变了。这个bug极其隐蔽我自己栽过跟头。排查方法很简单把训练和推理时同一个样本的中间注意力权重打印出来对比一眼就能看出区别。6. 从Transformer到生态主流的变体与选型建议6.1 BERT、GPT与三大派系Transformer原始论文提出的是编码器-解码器Encoder-Decoder结构但后来发展出了三个不同的派系选型时要区分清楚编码器派Encoder-only代表是BERT。它的思路是“双向理解”每个位置既能看左边的词也能看右边的词。适合做理解类任务文本分类、命名实体识别、语义相似度、信息抽取。我在做搜索场景的语义召回时用BERT类模型效果明显好于传统BM25。解码器派Decoder-only代表是GPT。它的思路是“单向左顾”只能看左边的词逐字预测下一个词。适合做生成类任务文本续写、对话、代码生成。现在的大语言模型几乎全是Decoder-only架构因为自回归训练方式简单且稳定还能通过大规模预训练“涌现”出理解能力。编码器-解码器派Encoder-Decoder代表是T5、BART。它既有编码器的双向理解又有解码器的自回归生成。适合需要“先理解再生成”的任务机器翻译、文本摘要、问答生成。我在做长文档摘要时BART的效果比直接用GPT生成要稳定得多。选型时我的经验是理解任务优先BERT系生成任务纯文本用GPT系理解生成混合或翻译摘要类任务用T5系。当然现在大模型时代很多任务可以直接用指令微调过的GPT模型来解决但如果你在做一个垂直领域的定制模型这个选型逻辑依然成立。6.2 Swin Transformer注意力机制进军视觉领域Transformer在NLP封神之后研究者开始思考能不能用到计算机视觉最初的ViTVision Transformer把图片切成一个个patch当作“词”来处理在ImageNet上能取得不错的效果但它有个致命弱点注意力是全局的patch数量随图像尺寸平方级增长计算量爆炸。一张512x512的图切成16x16的patch就有1024个token全局注意力的计算量是1024的平方约100万次这是不可接受的。Swin Transformer给出的方案非常巧妙层次化 窗口注意力。它先把图片分成小的窗口比如7x7在窗口内部做注意力大幅减少计算量然后通过“移位窗口”Shifted Window让不同窗口之间的信息可以交流。同时它构建了类似CNN的金字塔结构逐层下采样得到多尺度的特征。由于这些设计Swin Transformer在目标检测、语义分割等密集预测任务上输出效果超过了CNN时代的SOTA模型成为视觉Transformer的标杆架构之一。如果你要在视觉任务上尝试Transformer我建议直接从Swin或者基于它的改进版开始不要在朴素ViT上折腾计算效率和效果都差一截。6.3 什么时候不该用Transformer聊了这么多必须泼一盆冷水Transformer不是万能的。数据量极小比如几百条样本的任务传统方法或小模型可能更优。Transformer是数据饥渴型模型没有足够数据学不动。对延迟极度敏感的实时系统例如股票高频交易信号Transformer的推理开销可能不划算一个逻辑回归可能已经足够好。CPU部署、显存受限的边缘设备Transformer的参数量和内存带宽要求很高轻量级CNN或线性注意力模型可能更合适。可解释性要求极高的场景注意力权重不等于模型“理由”别把注意力可视化当作严谨的可解释性证据这一点我踩过坑注意力权重高不代表因果性强。说到底Transformer是一个强大的特征提取器但它不是银弹。选型的关键在于你对任务的理解、对数据规模的判断、对部署环境的预算这三者决定了最终要不要上Transformer。写在最后一些过来人的建议模型看再多遍不如动手写一遍。我建议你把我上面那套迷你Transformer代码从头到尾手动敲一遍不要复制粘贴。敲的过程中你会逼着自己去理解每一个张量形状的变化、每一个维度为什么要这么设置。等你把代码跑通的那一刻很多之前似懂非懂的概念会突然串起来。另外如果只记一句话我希望你记住Transformer的本质就是“让所有位置同时互相看按重要程度加权汇总信息”。自注意力负责看多头负责多看几个角度位置编码负责看的时候别忘了顺序残差和归一化负责看的稳。这套思想今天的GPT、BERT、Swin甚至扩散模型里的交叉注意力全都没有离开这个框架。我最初学Transformer时花了整整一周才把QKV的关系绕明白后来在一个实际推荐系统项目里用自注意力处理用户行为序列才真正理解了它的强大与局限。希望这篇文章能帮你把学习周期从一周缩短到一天。如果你照着代码跑出了自己的实验或者遇到了什么奇怪的问题欢迎在评论区交流我看到都会回。