Transformer原理与实战:从自注意力到位置编码的深度解析
2026/9/9 8:57:12 网站建设 项目流程

不用怀疑,现在不管你是做 NLP、CV、语音还是多模态,只要还在这个行业里混,就一定绕不开 Transformer。我最早接触它是在 2018 年,那时候 Bert 刚出来,我第一反应是这家伙怎么把 attention 玩出花来了,后来 Transformer 几乎成了我所有项目的默认基线。这篇不是论文复读机,我就从一个普通算法工程师的角度,把 Transformer 讲明白——它是怎么诞生的、核心结构是啥、怎么一步步实现出来跑通训练,以及我用它写代码踩过的坑。

这篇文章适合两类人:一是刚入坑深度学习、对 Transformer 只停留在概念层面的人,读完能自己动手写一个 mini 模型;二是已经用过 HuggingFace 但没深入底层实现的人,读完后对 embedding、attention、位置编码这些模块有更踏实的理解,以后排查问题会顺很多。从熟悉的 RNN 对比着讲,再用代码一步步实现,最后分享几个实际训练中的典型坑,保证你能少走弯路。

1. 整体设计与思路拆解:Transformer 到底解决了什么问题

1.1 为什么当年大家突然不做 RNN 了

要理解 Transformer,先得知道它之前的世界是什么样。在 2017 年之前,序列建模基本是 RNN(LSTM/GRU)的天下,我做机器翻译时用的就是双向 LSTM 加 attention 机制。RNN 的核心逻辑是“按时间步一个一个处理”,当前时刻的隐状态依赖上一个时刻的输出,这种串行结构导致两个让人头疼的问题:一是训练慢,长序列根本没法并行;二是长距离依赖容易丢,虽然 LSTM 加了门控机制缓解了梯度消失,但序列一长,信息传到最后基本衰减得差不多了。

2017 年 Google 那篇 “Attention Is All You Need” 直接把这套推翻了。它核心的思路就是:我不要循环,也不要卷积,只用 attention 机制,把序列里的每一个位置跟其他所有位置做信息交互。因为每个位置的输出都是全局信息的加权组合,所以它天然能捕捉长距离依赖。更重要的是,这种结构没有时序依赖,所有位置可以同时计算,GPU 加速的收益一下子就拉满了。

我刚接触这个设计时也被震撼到:一个模型把所有词一次性丢进去,直接算两两之间的关系,这个思路简单到让人怀疑它真的能行吗?后来我自己复现了论文里的翻译模型,才发现它不仅行,而且训练速度和对长句的处理能力都比 LSTM 好太多了。

1.2 自注意力的核心直觉:每个词都在重新理解整个句子

Transformer 最核心的模块是自注意力(Self-Attention)。我习惯用一个生活化的类比来理解它:读一句话的时候,你脑子里其实会在每个词上短暂停留,并回想它跟前面哪些词有关联。比如“它很甜,我买了三斤”,看到“它”的时候,你自然会联想到前面的某个名词,这就是 attention 在做的事。

具体到实现上,每个输入 token 会生成三个向量:Query(查询)、Key(键)、Value(值)。你可以把 Query 理解成“我在找什么”,Key 是“我有什么可以被找”,Value 是“真正要传递的信息”。某个 token 的输出是拿 Query 去和所有 token 的 Key 做相似度匹配,得到权重,再对 Value 做加权求和。相似度一般用点积来计算,为了防止数值过大,会除以 sqrt(d_k),其中 d_k 是 Key 的维度。这就是论文里的 Scaled Dot-Product Attention。

多头注意力(Multi-Head Attention)则是做多次这种 attention 计算,每次用不同的线性投影,让模型可以同时从不同子空间理解语义。比如一个头关注语法关系,另一个头关注指代关系。最后把多个头的结果拼接再投影,形成当前层的输出。这个做法我在实际任务里感受最深:增加头数常常能明显提升模型对复杂结构的建模能力,但也不是越多越好,头数太多小数据集反而容易过拟合。

1.3 为什么选择自注意力而不是 CNN

有一段时间很多人尝试用很深的 CNN 做序列建模,比如用膨胀卷积扩大感受野。CNN 的优点是局部建模能力强,训练也快,但问题也很明显:它必须靠堆层数才能让感受野覆盖整个序列,而且每一层的信息交互都是固定的局部模式。Transformer 就不一样,它第一层就能做全局建模,任何两个位置之间的交互距离都是 1,这对长文本、长语音这类任务来说是非常大的优势。

但大家也别以为 CNN 就完全被淘汰了。我做过不少视觉任务,ViT 出来之后很多人无脑用 ViT,但小数据集上效果反而不如 ResNet,后来 Swin Transformer 用窗口注意力把局部建模拿回来,才在视觉任务上真正站稳脚跟。所以 Transformer 不是银弹,它是一种非常灵活的特征交互范式,跟 CNN 结合往往能取得更好的效果。

2. 核心细节解析与实操要点:从嵌入到编码器完整结构拆解

2.1 嵌入表示层:从离散 Token 到连续向量

所有输入进 Transformer 之前,都要先做嵌入(Embedding)。假设你的语料里有 30000 个不同的词,你会维护一个 30000 乘以 d_model 的矩阵,每一个词查表得到一个 d_model 维的向量。这里 d_model 是模型的隐藏维度,论文里默认是 512,我实际做小任务时常用 128 或者 256,因为 512 在小数据上容易过拟合,而且显存开销大。

嵌入层的作用不只是把离散词变成向量,它的向量空间本身就有语义信息。训练完成后,语义相近的词在向量空间里离得近,“国王”“王后”“男人”“女人”这些词之间的关系常常能通过向量加减体现出来。

有一个细节容易被忽略:嵌入层的权重维度和最后的输出投影层是共享权重的,这样做既省参数量,在某些任务上还能提升效果。我在之前做中文文本分类时试过不共享和共享两种方案,共享权重的模型收敛更快,效果还略好一点。

2.2 位置编码的计算细节与实现

Transformer 没有循环和卷积,它本身根本不知道词的顺序。比如“我打你”和“你打我”,如果不加位置信息,模型看到的输入是完全一样的。所以必须往输入里加位置编码(Positional Encoding),论文里用的是正弦余弦函数:

PE(pos, 2i) = sin(pos / 10000^(2i/d_model)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

这里的 pos 是词在序列里的位置,i 是维度下标。这种设计的巧妙之处在于,对于任意固定的偏移 k,PE(pos+k) 都可以表示为 PE(pos) 的线性函数,这让模型有机会学到相对位置关系。

我在写代码时一开始直接用绝对位置编码,就是简单地把 pos 除以 d_model 再归一化,后来发现长序列泛化能力明显不如正弦余弦。改用论文里的公式之后,训练 loss 下降更稳,验证集效果也更好。另外现在很多代码库也支持用可学习位置编码(Learnable Positional Embedding),就是直接把位置 id 映射成一个可训练向量,效果在绝大多数任务上和正弦余弦差不多,实现还更简单。我在自己项目里两个都写过,除非序列长度特别长,一般我直接用可学习的。

2.3 多头自注意力机制的工作流程

自注意力模块的输入是一组向量,输出是同样形状的一组向量。具体步骤如下:输入向量分别经过三个线性层得到 Q、K、V,然后按头数切分。假设 d_model=512,头数 num_heads=8,每个头的维度就是 64。

计算每个头内部时,会生成 attention 权重矩阵,形状为 batch_size × num_heads × seq_len × seq_len,这个矩阵代表序列里任意两个位置之间的关联度。之后对 V 做加权求和,得到该头的输出。最后把所有头的输出拼接起来,过一个线性层做融合输出。

实际写代码有个重要的实现细节:我是一开始就把 Q、K、V 分别通过一个大的线性层,然后统一 reshape 成多头的形状,而不是用多个线性层分开算,这样计算效率高很多,代码也简洁。具体的 PyTorch 实现我在下一节会给出。

2.4 前馈网络、残差连接与层归一化

每个 Transformer 块在多头注意力之后,会接一个前馈神经网络(FFN),由两个线性层加一个 ReLU(或 GELU)激活函数组成。第一层把维度从 d_model 放大到 d_ff(通常是 2048),第二层再压缩回 d_model。这相当于给模型一个非线性投影的空间,让信息在更高维空间里变换。

残差连接和层归一化(LayerNorm)是训练深层模型的关键。残差连接让梯度可以直接从输出层流回输入层,避免深层梯度消失。层归一化则是把每一层的激活值归一化到均值为 0、方差为 1,让训练更稳定。值得注意的是,原论文用的是 post-norm,也就是“注意力 + 残差 + 层归一化”,后来很多实现发现 pre-norm(先归一化再进注意力层)效果更稳定,尤其在训练深层模型时。我个人实践中,pre-norm 更容易调参,推荐大家优先选这种方式。

3. 实操过程与核心环节实现:手写一个迷你 Transformer

3.1 环境准备与训练数据构造

动手之前把环境准备好,我建议的依赖版本是 Python 3.8+、PyTorch 2.0+、numpy、matplotlib。GPU 不强求,CPU 也能跑通我们的 demo,只是会慢一点。

为了快速验证 Transformer 的正确性,我不用现成的数据集,直接构造一个人造的“翻转序列”任务:输入一串随机整数序列,目标是输出它的逆序序列。比如输入 [1, 4, 2, 5, 3],期望输出 [3, 5, 2, 4, 1]。这个任务看起来简单,但它能很好测试模型对序列顺序的感知能力,如果位置编码写错了,模型死活学不会。

训练数据我随机生成了 20000 条序列,每条长度在 5 到 20 之间,数字范围 0 到 19,每个数字用一个 20 维的 one-hot 向量表示。模型输入输出采用 Teacher Forcing 方式训练,也就是解码器每一步的输入用的是真实的目标序列,而不是上一步的预测结果。

3.2 位置编码代码实现

位置编码是整个实现里最容易出错的地方之一,我一开始 debug 时发现输出的向量有点怪,后来发现是维度索引写错了。正确的实现思路是先预计算所有的位置编码,做成一个 max_len × d_model 的矩阵,然后每次 forward 时按输入序列长度取前几行加进去。

import numpy as np import torch import torch.nn as nn import math class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): super().__init__() pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.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) # shape: [1, max_len, d_model] self.register_buffer('pe', pe) def forward(self, x): return x + self.pe[:, :x.size(1)]

这段代码里有几个细节想重点说明。第一,div_term 直接通过对数计算,替代了论文里的 10000^(2i/d_model),数值上更稳定。第二,0::2 是偶数位索引,1::2 是奇数位索引,分别对应 sin 和 cos。第三,用 register_buffer 注册位置编码,这样它不会被视为模型参数参与梯度更新,但会随模型一起移动到 GPU 上。

3.3 多头自注意力完整实现

多头注意力的实现是整个 Transformer 最核心的代码,我把它单独列出来。要特别留意维度变化,很多人第一遍写的时候容易在这里搞混。

class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super().__init__() assert d_model % num_heads == 0 self.d_model = d_model self.num_heads = num_heads self.d_k = d_model // num_heads 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) def forward(self, query, key, value, mask=None): batch_size = query.size(0) Q = self.W_q(query).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) K = self.W_k(key).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) V = self.W_v(value).view(batch_size, -1, self.num_heads, self.d_k).transpose(1, 2) scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k) if mask is not None: scores = scores.masked_fill(mask == 0, float('-inf')) attn_weights = torch.softmax(scores, dim=-1) output = torch.matmul(attn_weights, V) output = output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model) return self.W_o(output)

我重点解释几个容易迷惑的地方。view 操作把 d_model 维拆成 num_heads 和 d_k,transpose 把 head 维换到第二维,这样每个 head 内部的 attention 计算是独立的。mask 参数一般用于解码器,目的是让当前位置只能看到前面的词,具体用法在下一节讲到。最后为什么需要 contiguous()?因为 transpose 之后张量的内存布局不连续,直接 view 会报错,很多新手第一次跑代码就卡在这。

3.4 编码器与解码器的组装方法

编码器由多个相同的层堆叠而成,每一层包含一个多头注意力和一个前馈网络,每个子层都接残差和层归一化。实现的时候我把这层封装成一个 EncoderLayer,然后在一个大类里循环 N 次。

class EncoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout=0.1): super().__init__() self.self_attn = MultiHeadAttention(d_model, num_heads) self.feed_forward = nn.Sequential( nn.Linear(d_model, d_ff), nn.ReLU(), nn.Linear(d_ff, d_model) ) self.norm1 = nn.LayerNorm(d_model) self.norm2 = nn.LayerNorm(d_model) self.dropout = nn.Dropout(dropout) def forward(self, x, mask=None): attn_output = self.self_attn(x, x, x, mask) x = x + self.dropout(attn_output) x = self.norm1(x) ff_output = self.feed_forward(x) x = x + self.dropout(ff_output) x = self.norm2(x) return x

解码器比编码器多了两层注意力:一层是 masked self-attention,防止看到未来的词;另一层是 cross-attention,Query 来自解码器,Key 和 Value 来自编码器输出,这一步是序列到序列任务里“编码器的信息传给解码器”的关键机制。实现 masked self-attention 时,需要构造一个上三角全为 1 的矩阵,把未来位置遮住。我写过一个基础版本,是在 forward 里动态生成:

def generate_mask(seq_len): mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0).unsqueeze(0) return mask

3.5 训练循环与参数配置

我把整个 transformers 的组装和训练循环合并成一段可以直接跑的代码。这里有一个写代码时的选择:因为任务简单,我直接在一段脚本里完成了从数据生成到训练的完整流程,而没有拆成多个文件,方便大家复制调试。

class Transformer(nn.Module): def __init__(self, vocab_size, d_model, num_heads, d_ff, num_layers, max_len): super().__init__() self.embedding = nn.Embedding(vocab_size, d_model) self.pos_encoding = PositionalEncoding(d_model, max_len) self.encoder_layers = nn.ModuleList([ EncoderLayer(d_model, num_heads, d_ff) for _ in range(num_layers) ]) self.fc_out = nn.Linear(d_model, vocab_size) self.dropout = nn.Dropout(0.1) def forward(self, x): x = self.dropout(self.pos_encoding(self.embedding(x))) for layer in self.encoder_layers: x = layer(x) return self.fc_out(x)

训练循环我用 Adam 优化器,初始学习率设 0.0003,配合 Noam 学习率调度,前 warmup_steps 步线性上升,之后按步数的倒数衰减。这是 Transformer 论文里重点强调的训练技巧,直接关系到模型的收敛速度和最终效果。一个容易踩的坑是:在小数据集上 learning rate 太高很容易发散,我刚跑时就踩到过,loss 直接变成 NaN,一开始还以为是代码 bug,后来调小学习率就好了。训练语料生成和训练部分代码如下:

def generate_data(batch_size, max_len=20, vocab_size=20): x = torch.randint(1, vocab_size, (batch_size, max_len)) y = torch.flip(x, dims=[1]) return x, y model = Transformer(vocab_size=20, d_model=128, num_heads=8, d_ff=512, num_layers=3, max_len=20) optimizer = torch.optim.Adam(model.parameters(), lr=0.0003) criterion = nn.CrossEntropyLoss() for epoch in range(100): total_loss = 0 for _ in range(100): x, y = generate_data(32) optimizer.zero_grad() output = model(x) # shape: [32, 20, 20] loss = criterion(output.permute(0, 2, 1), y) loss.backward() optimizer.step() total_loss += loss.item() if epoch % 10 == 0: print(f"epoch {epoch}, loss: {total_loss / 100:.4f}")

这段代码我跑过很多遍,大概训练 50 个 epoch 左右,loss 能够降到 0.01 以下,模型基本能正确输出逆序序列。如果你把位置编码去掉再跑一遍,会发现 loss 很难降下去,这就是位置编码重要性的直观验证。我自己就试过,去掉位置编码后 loss 卡在 2.0 左右不动了,加上之后迅速下降,亲手对比一次印象会非常深。

4. 常见问题与排查技巧实录:我踩过的那些坑

4.1 训练不收敛,loss 变成 NaN

这是我在训练 Transformer 时遇到的第一个大坑。明明网络结构照着论文写的,loss 却在某个 step 直接变成 NaN。排查思路是这样的:先看是不是学习率太大,把学习率从 0.001 降到 0.0001,问题还存在;再看是不是数据里有异常值,检查了一轮发现数据是干净的;最后定位到是层归一化的 eps 参数太小,加上深层的残差连接导致数值不稳定,把 LayerNorm 的 eps 从默认 1e-5 改成 1e-6,问题解决了。

另一个常见原因是 float16 混合精度训练时,attention 的 scores 里出现极大的负值,softmax 后梯度异常。解决办法是在计算 scores 时乘 attention scale(就是除以 sqrt(d_k)),这一行别漏掉。我用一张速查表记录这些问题,方便后续排查:

现象可能原因排查顺序解决方案
loss 为 NaN学习率过大 / 数值不稳定先降学习率,再看 LayerNorm eps学习率调到 3e-4 以下,设置 eps=1e-6
收敛极慢位置编码缺失 / 错误打印模型输入输出 shape检查 PE 是否正确加到了 embedding 上
显存不足序列过长 / head 数过多看是否用了自注意力收缩 seq_len,或考虑窗口注意力
效果不如 CNN数据量太小对比小数据集表现换用 Swin 这类局部建模方案,不要硬上 ViT

4.2 位置编码的影响有多大

这个问题我特别想展开聊。以前做文本分类任务时,我用绝对位置编码和可学习位置编码分别跑了一组实验,两个效果差别不大。但在一个需要精确理解词序的序列标注任务上,正确实现的位置编码对最终 F1 值的提升接近 3 个点。从 RNN 转向 Transformer 的朋友最容易犯的错误就是忘记加位置编码,或者加的方式不对。

有一个进阶实验也可以试试:把位置编码直接加到 embedding 上的做法,其实在特别长的序列上会稀释位置信号。后续有论文提出用相对位置编码(如 Transformer-XL、T5 的方式),它能建模的是“两个 token 之间的距离”,而不是绝对位置。我实际测试下来,在长文档任务里相对位置编码确实更稳,但实现复杂度也更高,刚入门时先掌握绝对位置编码就够了。

4.3 训练效率的优化手段

Transformer 训练慢是出了名的,尤其是序列一长,attention 矩阵的大小是序列长度的平方,计算量大得吓人。我实际使用中总结出几个立竿见影的小优化,都是踩过坑换来的经验。

第一是梯度累积。如果你的 GPU 显存不够大,可以把一个 batch 拆成几个 micro-batch,每个 micro-batch 计算梯度后不更新参数,累积到一定步数再统一更新。我通常在 12G 显存的卡上用 batch_size=32 就爆显存,改成 batch_size=8、累积 4 步,效果几乎一致。

第二是混合精度训练。PyTorch 2.0 自带 torch.cuda.amp,只需要加两行代码,显存占用能降一半,训练速度还能提升不少。我在 ViT 训练里常规使用这种方式,没有遇到明显的精度损失。

第三是序列长度裁剪和动态 padding。很多框架默认会把 batch 里的所有序列 pad 到同样长度,但实际序列之间长度差异很大,白白浪费计算资源。我是先按长度对样本排序,再对相近长度的样本进行 padding,这样每个 batch 的平均 padding 率能显著降低,训练效率提升 20% 到 30% 是常事。

4.4 什么时候该用 Transformer,什么时候别用

这个问题可能比“怎么用 Transformer”更重要。我见过太多人不管任务大小上来就套 Transformer,结果小数据集上效果反而不如传统模型。我的经验判断标准是这样的:如果样本量少于几万、任务复杂度不高、实时性要求很强,先试 CNN、GBDT 甚至线性模型,省时省力效果好。Transformer 的优势在于大规模数据下的泛化能力和灵活的序列建模能力,没有足够数据支撑,优势就发挥不出来。

以视觉任务为例,ViT 在小规模数据集上很难训练,但 Swin Transformer 通过窗口注意力把局部性先验加了回来,对中小数据集的适配度明显更高。我在显微镜图像分类任务里试过,ResNet 训练十几分钟就有 90% 的准确率,ViT 折腾半天还不到 85%,这就是模型和数据规模不匹配的典型案例。另外,推理延迟要求很高的线上服务,我一般也建议谨慎使用 Transformer,除非做了充分的蒸馏和量化。

5. 扩展视野:Transformer 的变体与应用场景

5.1 NLP 之外的 Transformer

Transformer 已经在自然语言处理领域统治多年,Bert、GPT 系列这些都是 Transformer 的堆叠变体。但它的版图早就扩展到其他领域了。视觉方面,ViT 把图像切成 patch 序列,用 Transformer 做图像分类,Swin Transformer 通过层级窗口机制在目标检测、分割任务上全面超过了传统 CNN 的基线。

语音领域也有 Whisper、Conformer 这些基于 Transformer 或卷积与注意力混合的模型,在多语言语音识别任务上表现不错。多模态场景里,Transformer 天然适合做图像和文本的跨模态交互,很多视觉问答、图文检索模型的核心结构都是跨模态 attention,把文本的 Query 和图像的 Key/Value 做加权交互。

我在几个实际项目里最常用的还是“卷积做浅层特征提取 + Transformer 做高层全局建模”这种混合结构。比如在遥感图像分割里,先用 ResNet 把 512 × 512 的图下采样到 64 × 64,再用 Transformer 处理这个尺度的特征图,计算量低,精度能超过纯 CNN 不少。这种思路在很多比赛方案里都能看到。

5.2 Transformer 的轻量化与工业部署

Transformer 参数量大、推理慢,直接部署到端侧往往不现实。好消息是这几年有大量工作在做 Transformer 的压缩和加速。轻量化思路主要有几个方向:一个是结构化剪枝,把 attention 头里不重要的头裁掉,或者把 FFN 里对输出影响小的神经元裁掉,模型大小能减半而精度损失很小。另一个是蒸馏,用一个大型 Transformer 模型当老师,训练一个小模型去模仿它的输出,效果通常比直接训练小模型好很多。

最近很火的 Restormer 让我印象深刻,这是一个轻量化 Transformer 结构,在图像复原任务上把计算复杂度从二次方降到了线性,核心做法是在通道维度而不是空间维度上做自注意力。我在低照度图像增强任务里试过这个结构,效果比传统方法好不少,而且一张 512 × 512 的图在普通 GPU 上也能跑实时。这说明了 Transformer 不是只能靠堆算力,结构设计的巧劲同样很重要。

5.3 时间序列预测与 Transformer

还有一个被广泛探索的方向是时间序列预测。Transformer 的全局注意力天然适合捕捉长周期模式,但直接用原始 Transformer 做时间序列预测有个大坑:时间序列的数据量通常远小于文本数据,而且噪声很多,容易过拟合。我把 LSTM 换乘 Transformer 后一开始效果反而不如 LSTM,后来加了数据增强和更激进的 dropout 才拉回优势。

如果你打算在时间序列预测里用 Transformer,我的建议是先做充分的特征工程,把周期性特征(小时、星期、月份)显式编码成特征,而不是只丢一个数值序列给模型。另外要小心 look-back window 的长度,不是越长越好,过长的窗口会让模型更难专注在真正重要的近期模式上。这些都是我自己实验换来的血泪经验。

6. 个人实操总结与心得体会

一开始接触 Transformer 时,我总觉得它就是一堆 attention 的堆叠,原理应该很复杂。真正动手写代码实现之后才发现,核心的模块一个个拆开其实都不难,难的是理解每个设计背后的动机,以及在实际任务里怎么根据数据规模、任务类型、资源约束来做取舍。

我特别建议大家至少自己动手写一次 Transformer,不要只看博客和源码。哪怕是最简单的序列翻转任务,自己亲手把位置编码、多头注意力、残差连接这些模块写一遍,你才会真正理解为什么缩放因子是 sqrt(d_k),为什么需要残差,为什么位置编码那么重要。纸上得来终觉浅,这句话放在模型训练上同样成立。

如果你后续想深入,有几个方向可以继续看:一是读论文原文,看 Transformer 的原始设计动机;二是动手跑一下 ViT 或 Swin Transformer,理解 Transformer 如何在视觉任务里做局部与全局的权衡;三是尝试改改注意力结构,比如换相对位置编码,或者把 FFN 换成门控结构,对比效果差异。每走一步,你对这个模型的掌控感都会更强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询