ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Attention is all you need论文翻译:从Transformer到BERT的架构演进与代码实现

Attention is all you need论文翻译:从Transformer到BERT的架构演进与代码实现 1. 从论文公式到可运行代码Transformer 自注意力到底怎么算《Attention is all you need》这篇论文我翻过好几遍每次重读都有新收获。它最核心的贡献是提出了 Transformer 这个完全基于注意力机制的序列转换模型把 RNN 和 CNN 从编码器-解码器架构里彻底拿掉了。论文摘要里写得很清楚在 WMT 2014 英德翻译任务上big 模型拿到 28.4 BLEU比之前最好的集成模型还高 2 BLEU而训练只用了 8 块 P100 跑 3.5 天。这个数字放到今天看依然能打。那 Transformer 到底解决了什么问题简单说RNN 的隐状态 h_t 必须等 h_{t-1} 算完才能算天然串行长序列上并行度极差CNN 虽然能并行但卷积核感受野有限两个远距离位置之间的信号要经过 O(n/k) 层才能关联上。Transformer 用 self-attention 把任意两个位置的路径长度压到常数级同时矩阵乘法可以一次性铺满 GPU。这就是它能又快又好的根本原因。这篇内容面向 NLP 入门和进阶读者重点不是把论文再翻译一遍而是把论文里的公式落到 PyTorch 代码上。你会看到 Scaled Dot-Product Attention、Multi-Head Attention、Positional Encoding 的逐层维度验证以及怎么用 TaoToken 的 API 把写好的模型接上真实推理做冒烟测试。如果你正在学 BERT那更该先吃透 Transformer因为 BERT 就是 Transformer Encoder 的堆叠只是换了预训练目标。我试过直接照论文公式手写一遍 attention再对照 PyTorch 官方实现维度对不上的地方基本都集中在 head 拆分和 mask 形状上。下面按步骤来每一步都给可复制的代码和维度打印你跟着跑一遍就能把论文里的 d_model512、h8、d_kd_v64 这些数字真正对应到张量形状上。2. TaoToken 前置准备给模型接上可调用的推理入口代码写完之后总得验证它是不是真的能跑通、输出是否合理。本地训练一个小 Transformer 成本不低更实际的做法是先把模型结构写对再用一个稳定的模型 API 做对照实验比如让 API 返回一段文本你观察 token 化之后的序列长度、attention 权重的分布是否符合预期。TaoToken 在这里的角色就是一个统一的模型调用入口官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。你需要准备三样东西Base URL、API Key、Model ID。Base URL 填 https://taotoken.net/api API Key 在控制台的 API Keys 页面生成Model ID 根据你要验证的模型选择。这三件套在后面的配置片段里会反复出现尤其是用 Claude Code、Cline MCP 或者 Codex 这类工具时缺一个都会报错。如果你只是想快速验证模型输出可以直接用模型对话页面https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。如果你打算长期做编码或 Agent 类任务Coding Plan 更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。这里要强调一点TaoToken 是模型调用入口不是用来替代你的编辑器或训练框架的。你的 Transformer 代码还是在本地 PyTorch 里跑TaoToken 只负责在你需要对照推理结果时提供一个可调用的模型。把这两件事分清楚后面的配置就不会乱。3. 可复制配置Transformer 核心模块与 API 接入片段先写 Scaled Dot-Product Attention。论文 3.2.1 节的公式是 Attention(Q,K,V)softmax(QK^T/√d_k)V。注意缩放因子是 1/√d_k不是 1/d_k这个细节在 d_k 较大时直接影响 softmax 是否落到梯度极小区域。import torch import torch.nn as nn import math class ScaledDotProductAttention(nn.Module): def __init__(self, dropout0.1): super().__init__() self.dropout nn.Dropout(dropout) def forward(self, q, k, v, maskNone): d_k q.size(-1) scores torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k) if mask is not None: scores scores.masked_fill(mask 0, -1e9) attn torch.softmax(scores, dim-1) attn self.dropout(attn) return torch.matmul(attn, v), attn q torch.randn(2, 8, 10, 64) k torch.randn(2, 8, 10, 64) v torch.randn(2, 8, 10, 64) out, attn ScaledDotProductAttention()(q, k, v) print(out.shape, attn.shape)跑出来应该是 torch.Size([2, 8, 10, 64]) 和 torch.Size([2, 8, 10, 10])。2 是 batch8 是 head 数10 是序列长度64 是 d_k。attn 最后两维是 10×10表示每个位置对其他位置的权重行和应该接近 1。接着写 Multi-Head Attention。论文 3.2.2 节说把 Q、K、V 经过 h 次不同线性映射分别得到 d_k、d_k、d_v 维再并行做 attention最后 concat 并线性投影。h8d_model512所以 d_kd_v64。class MultiHeadAttention(nn.Module): def __init__(self, d_model512, num_heads8, dropout0.1): 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) self.attention ScaledDotProductAttention(dropout) def split_heads(self, x, batch_size): x x.view(batch_size, -1, self.num_heads, self.d_k) return x.transpose(1, 2) def forward(self, q, k, v, maskNone): batch_size q.size(0) q self.split_heads(self.w_q(q), batch_size) k self.split_heads(self.w_k(k), batch_size) v self.split_heads(self.w_v(v), batch_size) out, attn self.attention(q, k, v, mask) out out.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model) return self.w_o(out), attn mha MultiHeadAttention() x torch.randn(2, 10, 512) out, attn mha(x, x, x) print(out.shape, attn.shape)输出是 torch.Size([2, 10, 512]) 和 torch.Size([2, 8, 10, 10])。注意 split_heads 里先 view 再 transpose顺序反了维度就错。这是最容易踩的坑之一。然后是 Positional Encoding。论文 3.5 节用不同频率的正余弦函数pos 是位置i 是维度。偶数维用 sin奇数维用 cos。class PositionalEncoding(nn.Module): def __init__(self, d_model512, max_len5000, dropout0.1): super().__init__() self.dropout nn.Dropout(dropout) 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)) pe[:, 0::2] torch.sin(position * div_term) pe[:, 1::2] torch.cos(position * div_term) pe pe.unsqueeze(0) self.register_buffer(pe, pe) def forward(self, x): x x self.pe[:, :x.size(1), :] return self.dropout(x) pe PositionalEncoding() x torch.randn(2, 10, 512) print(pe(x).shape)输出 torch.Size([2, 10, 512])。register_buffer 保证 pe 不参与梯度更新但会跟着模型一起搬到 GPU。现在把 TaoToken 的接入配置写出来。如果你用 Claude Code配置文件里需要 Base URL、API Key、Model ID 三件套。以 settings.json 为例{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: 你的API Key, ANTHROPIC_MODEL: 你的Model ID } }如果你用 Cline MCP配置片段类似{ mcpServers: { taotoken: { command: npx, args: [-y, taotoken/mcp-server], env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: 你的API Key, TAOTOKEN_MODEL: 你的Model ID } } } }Codex 的 auth.json 则是{ base_url: https://taotoken.net/api, api_key: 你的API Key, model: 你的Model ID }这三件套里 Base URL 固定是 https://taotoken.net/api 不要加 UTM 参数。API Key 和 Model ID 从控制台获取。配置完之后你的编码工具就能通过 TaoToken 调用模型而你的 Transformer 代码依然在本地跑两者互不干扰。4. 验证请求与成功结果逐层维度检查与 API 冒烟测试代码写完了先做逐层维度验证。把上面三个模块串起来构造一个最小可运行的 Encoder Layer打印每一层的输入输出形状。class EncoderLayer(nn.Module): def __init__(self, d_model512, num_heads8, d_ff2048, dropout0.1): super().__init__() self.self_attn MultiHeadAttention(d_model, num_heads, dropout) self.ffn 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, maskNone): attn_out, _ self.self_attn(x, x, x, mask) x self.norm1(x self.dropout(attn_out)) ffn_out self.ffn(x) x self.norm2(x self.dropout(ffn_out)) return x layer EncoderLayer() x torch.randn(2, 10, 512) print(输入:, x.shape) print(输出:, layer(x).shape)输入和输出都应该是 torch.Size([2, 10, 512])。如果中间某一步维度变了大概率是 split_heads 或 view 的顺序问题。论文里 d_model512、d_ff2048、h8、d_k64这几个数字在代码里都能对上。接下来做 API 冒烟测试。用 curl 发一个最小请求确认 TaoToken 能正常返回curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer 你的API Key \ -d { model: 你的Model ID, messages: [{role: user, content: 用一句话解释self-attention}], max_tokens: 100 }成功的话你会看到 JSON 里 choices[0].message.content 有返回文本。这一步的意义是确认你的 Base URL、API Key、Model ID 三件套都正确后面写更复杂的调用逻辑时不会因为配置问题浪费时间。如果你想在 Python 里调用可以这样写import requests resp requests.post( https://taotoken.net/api/v1/chat/completions, headers{Authorization: Bearer 你的API Key}, json{ model: 你的Model ID, messages: [{role: user, content: 解释一下multi-head attention}], max_tokens: 200 } ) print(resp.json()[choices][0][message][content])返回内容能正常打印说明接入没问题。这时候你可以把 API 返回的文本做 token 化观察序列长度再和你本地 Transformer 的 positional encoding 维度做对照验证 max_len 设置是否够用。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth第一个高频错误是 401 Unauthorized。报错信息通常是 {error: {message: Invalid API key, type: invalid_request_error}}。原因基本是 API Key 填错、过期或者带了多余空格。检查方法把 Key 复制到 curl 命令里单独测一次确认 Key 本身有效。如果 curl 能通但代码里报 401检查代码里是不是把 Key 写成了环境变量但没加载成功。第二个是 local proxy failed。这个报错一般出现在你本地网络环境有额外转发设置时请求没走到 https://taotoken.net/api 。排查步骤先用 curl 直接请求 Base URL看是否能通如果 curl 也不通检查你的网络配置里是否有影响 HTTPS 请求的项。注意不要使用任何非官方的转发工具直接用系统默认网络即可。第三个是 reading choices 相关报错比如 KeyError: choices 或者 TypeError: NoneType object is not subscriptable。这通常是因为返回的 JSON 结构和你预期的不一样可能是请求参数有误导致返回了错误信息而不是正常响应。排查方法先把 resp.json() 完整打印出来看里面到底有什么字段。常见原因是 model 参数填错或者 messages 格式不对。第四个是 OAuth 相关报错。如果你用 Claude Code 接入可能会遇到 OAuth token 过期或未授权。这时候需要重新走一遍授权流程或者改用 API Key 方式接入。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有详细的配置说明。还有一个容易忽略的问题mask 形状不对导致 attention 输出异常。论文 3.2.3 节提到 decoder 的 self-attention 要屏蔽掉当前位置之后的信息实现时把 mask 里对应位置设为 -1e9 再 softmax。如果 mask 形状和 scores 不匹配PyTorch 会广播或者直接报错。检查方法打印 scores.shape 和 mask.shape确保最后一维能对上。最后提醒一点如果你同时用多个工具接入每个工具都需要单独配置 Base URL、API Key、Model ID 三件套。不要以为配了一个工具其他工具就自动生效。API Key 管理页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 可以在这里查看和管理你的 Key。6. 从 Transformer 到 BERT架构演进与后续学习路径把 Transformer 的 Encoder 单独拿出来堆叠就是 BERT 的骨架。BERT 用了 Transformer Encoder 的 12 层或 24 层d_model 分别是 768 和 1024attention head 是 12 和 16。它和原始 Transformer 的区别主要在预训练目标BERT 用 Masked Language Model 和 Next Sentence Prediction而不是机器翻译的 seq2seq 目标。所以你写完上面的 EncoderLayer只要堆 12 层再加一个 MLM head结构上就离 BERT 很近了。CNN 和 RNN 在 NLP 里的位置也值得对照一下。RNN 的串行特性让它在长序列上训练慢但参数量小、推理时内存占用低CNN 并行度高但感受野受卷积核大小限制要堆很多层才能覆盖长距离依赖。Transformer 用 self-attention 一次性看到全序列代价是 O(n²) 的计算复杂度。论文表 1 里对比了这三种结构的计算复杂度和路径长度你可以对照着看理解为什么 Transformer 在翻译任务上能同时做到更快和更好。如果你想继续深入建议按这个顺序先把本文的 Scaled Dot-Product Attention 和 Multi-Head Attention 手写一遍确保维度完全对得上然后加 Positional Encoding 和 EncoderLayer跑通一个完整的前向传播接着用 TaoToken 的模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 做对照实验观察真实模型对同一段文本的 attention 行为最后再去看 BERT 的论文和代码你会发现很多概念是相通的。如果你打算长期做编码或 Agent 类任务Coding Plan 提供了更稳定的调用额度https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各种工具的配置示例。API Key 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 管理。最后说一个实用技巧调试 attention 的时候把 attn 矩阵打印出来看每一行的和是不是接近 1再看对角线附近的值是不是明显偏大。如果对角线附近没有明显峰值可能是 positional encoding 没加对或者 mask 设置有问题。这个检查方法比单纯看 loss 曲线更直观能帮你快速定位是结构问题还是训练问题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进