news 2026/10/22 19:44:39

2017 Transformer原文【公式附代码手写】:Attention is All you need Reading Note【Transformer阅读笔记】

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
2017 Transformer原文【公式附代码手写】:Attention is All you need Reading Note【Transformer阅读笔记】

1. 从论文公式到可运行代码:Transformer 自注意力机制到底怎么落地

2017 年那篇《Attention Is All You Need》我第一次读的时候,卡在 3.2.1 节整整一个下午。公式Attention(Q,K,V) = softmax(QK^T/√d_k)V看着就一行,但真动手写的时候,Q、K、V 从哪来、维度怎么对齐、mask 加在哪一步、为什么除以 √d_k 而不是 d_k,全是坑。这篇笔记就按论文逐节走一遍,把每个公式手写推导,再配一份能直接跑的 PyTorch 代码,帮你从公式到代码完整走通。

Transformer 是什么?一句话:它是第一个完全靠注意力机制做序列转换的模型,把 RNN 的循环和 CNN 的卷积全扔了,编码器和解码器各堆 6 层,每层就是多头自注意力加逐位置前馈网络。它能做什么?机器翻译、文本摘要、语言建模,后来所有大模型的底座都是它。适合谁?刚读完论文想动手复现的、面试被问自注意力细节答不利索的、想搞懂多头注意力到底在并行什么的同学。

我试过直接抄 GitHub 上的实现,结果维度对不上、mask 广播报错、softmax 梯度消失,折腾两天才跑通。所以这篇不堆概念,每个公式后面都跟一段可复制代码,你跟着敲一遍,比看十遍论文都管用。下面从论文的问题背景讲起,再到 TaoToken 的前置准备,然后是完整配置、验证请求、报错排查,最后给一个语义一致的收尾。

2. 论文问题背景与 TaoToken 前置准备:为什么自注意力能替代循环

论文第 1、2 节讲得很清楚:RNN 类模型沿序列位置逐步计算,隐藏状态 h_t 依赖 h_{t-1},这种固有的顺序性阻断了训练样本内的并行化。序列一长,内存约束就限制 batch 大小,训练慢得离谱。ConvS2S 和 ByteNet 用卷积减少顺序计算,但关联两个任意位置信号所需的操作数随距离增长——ConvS2S 是线性增长,ByteNet 是对数增长,学远距离依赖依然困难。

Transformer 的做法是把任意两个位置之间的操作数降到常数级。代价是平均注意力加权位置降低了有效分辨率,论文用多头注意力来抵消这个影响。这就是 3.2.2 节多头注意力的动机:不是花哨,是刚需。

在动手写代码之前,我建议先把环境和一个稳定的模型调用入口准备好,方便你边写边验证。我平时用 TaoToken 做模型对话和 API 调试,它的接入文档写得很清楚,Base URL 和 Key 的配置方式对新手友好。你可以先到官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 看一下整体介绍,然后到 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 生成一个 Key。如果你只是想先跑通对话验证公式理解,可以直接用模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 试一下。

这里要强调一点:TaoToken 是合规的模型调用入口,不是让你去搞什么灰色中转。它的 API 地址是 https://taotoken.net/api,配置的时候 Base URL 填这个就行。我实测下来,把注意力模块的维度打印出来对照论文,再用模型对话问几个关于 QK^T 维度的问题,理解会快很多。

前置准备清单:Python 3.9+、PyTorch 2.0+、numpy。装好之后先跑一个最小验证,确认 torch 能正常 import,然后我们进入 3.1 节的编码器解码器结构。

3. 可复制配置:编码器解码器栈与多头注意力模块

论文 3.1 节说编码器由 N=6 个相同层堆叠,每层两个子层:多头自注意力、逐位置全连接前馈网络。每个子层周围用残差连接再做层归一化,输出是LayerNorm(x + Sublayer(x))。为了残差能相加,所有子层和嵌入层输出维度都是 d_model=512。解码器也是 6 层,但每层多一个第三子层,对编码器栈输出做多头注意力,并且自注意力子层要加因果 mask,防止位置 i 关注到 i 之后的位置。

先写一个可复制的配置片段,把超参数固定下来,后面所有代码都引用它。我用 JSON 格式,路径和字段名保持和论文一致:

{ "d_model": 512, "num_heads": 8, "d_k": 64, "d_v": 64, "d_ff": 2048, "num_layers": 6, "dropout": 0.1, "max_seq_length": 5000, "warmup_steps": 4000, "label_smoothing": 0.1 }

注意 d_k = d_v = d_model / num_heads = 512 / 8 = 64,这是论文 3.2.2 节的设置。多头注意力把 d_model 切成 8 个头,每个头独立算缩放点积注意力,最后拼接再过一个 W^O 投影。

下面是缩放点积注意力的完整实现,对应公式 (1):

import torch import torch.nn as nn import torch.nn.functional as F import math def scaled_dot_product_attention(Q, K, V, mask=None): """ 论文公式 (1): Attention(Q,K,V) = softmax(QK^T / sqrt(d_k)) V Q: (batch, seq_len, d_k) K: (batch, seq_len, d_k) V: (batch, seq_len, d_v) """ d_k = Q.size(-1) # 步骤1: QK^T,得到 (batch, seq_len, seq_len) 的相关性矩阵 scores = torch.matmul(Q, K.transpose(-2, -1)) # 步骤2: 除以 sqrt(d_k),防止 softmax 进入饱和区 scores = scores / math.sqrt(d_k) # 步骤3: 加 mask,被屏蔽位置设 -1e9 if mask is not None: scores = scores.masked_fill(mask == 0, -1e9) # 步骤4: softmax 归一化 attention_weights = F.softmax(scores, dim=-1) # 步骤5: 加权求和 output = torch.matmul(attention_weights, V) return output, attention_weights

为什么除以 √d_k?论文 3.2.1 节解释:当 d_k 较大时,点积在幅度上变大,softmax 会被推入梯度极小的区域。假设 Q、K 各分量独立、均值 0 方差 1,点积 QK^T 的方差就是 d_k,除以 √d_k 把方差拉回 1。你可以自己验证:把 d_k 设成 512 不缩放,softmax 输出会接近 one-hot,梯度几乎为 0。

接下来是多头注意力,对应公式MultiHead(Q,K,V) = Concat(head_1,...,head_h)W^O,其中head_i = Attention(QW_i^Q, KW_i^K, VW_i^V):

class MultiHeadAttention(nn.Module): def __init__(self, d_model=512, num_heads=8, dropout=0.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.dropout = nn.Dropout(dropout) def forward(self, query, key, value, mask=None): batch_size, seq_len = query.size(0), query.size(1) # 线性投影 Q = self.W_q(query) K = self.W_k(key) V = self.W_v(value) # 拆成多头: (batch, seq_len, d_model) -> (batch, num_heads, seq_len, d_k) Q = Q.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) K = K.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) V = V.view(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) if mask is not None: mask = mask.unsqueeze(1).repeat(1, self.num_heads, 1, 1) # 每个头并行算缩放点积注意力 out, attn = scaled_dot_product_attention(Q, K, V, mask) # 拼接多头: (batch, num_heads, seq_len, d_k) -> (batch, seq_len, d_model) out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, self.d_model) # 输出投影 W^O out = self.W_o(out) return out, attn

这里有个容易踩的坑:view之后必须transpose,拼接回来的时候要先transpose再contiguous().view,否则维度会乱。我第一次写的时候忘了 contiguous,报错 "view size is not compatible",查了半天。

逐位置前馈网络对应公式 (2)FFN(x) = max(0, xW_1 + b_1)W_2 + b_2,就是两个线性层中间夹 ReLU,内层维度 d_ff=2048:

class PositionwiseFeedForward(nn.Module): def __init__(self, d_model=512, d_ff=2048, dropout=0.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): return self.linear2(self.dropout(F.relu(self.linear1(x))))

位置编码对应 3.5 节公式PE(pos,2i) = sin(pos/10000^(2i/d_model))和PE(pos,2i+1) = cos(pos/10000^(2i/d_model)):

class PositionalEncoding(nn.Module): def __init__(self, d_model=512, max_seq_length=5000, dropout=0.1): super().__init__() self.dropout = nn.Dropout(dropout) pe = torch.zeros(max_seq_length, d_model) position = torch.arange(0, max_seq_length, 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) self.register_buffer('pe', pe) def forward(self, x): return self.dropout(x + self.pe[:, :x.size(1), :])

把这些模块拼成编码器层和解码器层,就得到完整的 Transformer。解码器自注意力要加因果 mask,用torch.tril生成下三角矩阵:

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

到这里,论文 3.1 到 3.5 节的核心公式全部落地。你可以把上面的代码存成一个transformer_blocks.py,下一步我们跑验证。

4. 验证请求与成功结果:逐行打印维度确认公式正确

写完代码不验证等于没写。我习惯用固定随机种子,把每一步的维度打印出来,对照论文的 d_model=512、num_heads=8、d_k=64 逐个核对。

if __name__ == "__main__": torch.manual_seed(42) batch_size, seq_len, d_model = 2, 10, 512 num_heads = 8 # 构造输入 x = torch.randn(batch_size, seq_len, d_model) print(f"输入维度: {x.shape}") # (2, 10, 512) # 位置编码 pos_enc = PositionalEncoding(d_model) x = pos_enc(x) print(f"位置编码后: {x.shape}") # (2, 10, 512) # 多头自注意力 mha = MultiHeadAttention(d_model, num_heads) out, attn = mha(x, x, x) print(f"多头注意力输出: {out.shape}") # (2, 10, 512) print(f"注意力权重: {attn.shape}") # (2, 8, 10, 10) # 前馈网络 ffn = PositionwiseFeedForward(d_model) out = ffn(out) print(f"FFN输出: {out.shape}") # (2, 10, 512) # 因果 mask 验证 causal_mask = create_causal_mask(seq_len, x.device) out_masked, attn_masked = mha(x, x, x, mask=causal_mask) print(f"因果mask后注意力权重[0,0]:\n{attn_masked[0,0].round(decimals=3)}")

跑出来应该看到:输入 (2,10,512),位置编码后不变,多头注意力输出 (2,10,512),注意力权重 (2,8,10,10)——8 个头,每个头一个 10x10 的相关性矩阵。因果 mask 后,注意力权重矩阵应该是下三角,上三角全为 0。如果你看到上三角有非零值,说明 mask 没生效,检查masked_fill的条件是不是写反了。

再验证一下缩放的作用。把scores / math.sqrt(d_k)去掉,打印 softmax 后的最大值:

Q = torch.randn(1, 10, 512) K = torch.randn(1, 10, 512) scores_no_scale = torch.matmul(Q, K.transpose(-2, -1)) scores_scaled = scores_no_scale / math.sqrt(512) print(f"不缩放 softmax 最大值: {F.softmax(scores_no_scale, dim=-1).max().item():.4f}") print(f"缩放后 softmax 最大值: {F.softmax(scores_scaled, dim=-1).max().item():.4f}")

不缩放时 softmax 最大值会接近 1,分布极度尖锐;缩放后会平缓很多。这就是论文说的"推入梯度极小区域"的直观体现。

如果你想用模型对话快速验证对某个公式的理解,可以到 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 把 QK^T 的维度问题丢进去问,比翻论文快。API 调试的话,Base URL 填 https://taotoken.net/api,Key 用你在 API Keys 页面生成的,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 有完整示例。

5. 本篇常见错排查:401、维度不匹配、mask 广播失败

复现过程中我踩过的坑基本集中在这几类,对照着排查能省不少时间。

报错一:401 Unauthorized / invalid api key。这个一般出现在你调 TaoToken API 验证的时候。检查三件套:Base URL 是不是https://taotoken.net/api,Key 是不是从 API Keys 页面复制的完整字符串,Model ID 是不是填对了。三者缺一不可。如果还报 401,去 console 页面 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 看一下 Key 的状态是不是被禁用或过期。

报错二:RuntimeError: The size of tensor a (10) must match the size of tensor b (8)。这是多头注意力里 mask 广播失败。mask 原始形状是 (batch, seq_len, seq_len),但 QK^T 是 (batch, num_heads, seq_len, seq_len),需要在 num_heads 维度上扩展。正确写法是mask.unsqueeze(1).repeat(1, self.num_heads, 1, 1)。如果你忘了这一步,就会报维度不匹配。

报错三:view size is not compatible with input tensor's size and stride。这是拼接多头输出时忘了contiguous()。transpose之后张量在内存里不连续,直接view会报错。正确顺序是out.transpose(1, 2).contiguous().view(batch_size, seq_len, d_model)。

报错四:local proxy failed / connection refused。这个通常是网络配置问题,检查你的 API 地址有没有写错,或者本地有没有奇怪的代理设置干扰。TaoToken 的 API 地址就是https://taotoken.net/api,不要加多余路径。

报错五:reading choices 相关解析错误。如果你在解析模型返回的 JSON 时遇到KeyError: 'choices',先打印原始 response 看看结构。不同接口返回格式可能不同,别硬套一个模板。

报错六:OAuth 相关错误。如果你用的是 Claude Code 或 Codex 这类工具接入,OAuth 流程走不通时,检查回调地址和 Key 的权限范围。Claude Code 接入可以参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite 的配置说明,Base URL、Key、Model ID 三件套要写全。

报错七:softmax 输出全为 nan。检查 mask 里是不是用了-inf而不是-1e9。-inf在某些情况下会导致 nan,论文实现里用-1e9是更稳的选择。另外检查有没有除零,math.sqrt(d_k)里 d_k 不能为 0。

报错八:位置编码维度对不上。self.pe[:, :x.size(1), :]这里 x.size(1) 是 seq_len,如果你的输入是 (seq_len, batch, d_model) 格式,要改成 x.size(0)。建议统一用 batch_first 格式,少踩这个坑。

排查顺序建议:先看报错类型,维度问题查 shape,网络问题查 Base URL 和 Key,数值问题查 mask 和缩放。把每一步的 shape 打印出来,90% 的问题一眼就能定位。

6. 从公式到代码走通之后:长期编码与 Agent 场景的接入建议

把上面这套代码跑通,你对自注意力、多头、位置编码、mask 的理解就落地了。接下来如果要做长期编码或者 Agent 相关的项目,建议把模型调用也规范化。TaoToken 的 Coding Plan 页面 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 有适合长期编码场景的配置说明,Base URL、Key、Model ID 三件套配好之后,可以直接在编辑器或 Agent 框架里调用。

我自己的习惯是:公式推导用纸笔,代码实现用 PyTorch,模型验证用 TaoToken 的对话和 API。三者结合,理解速度比单看论文快很多。注意力权重矩阵打印出来之后,你可以直观看到每个头关注的位置,这比任何文字描述都清楚。

最后留一个实用技巧:把attn_masked[0,0]那个 10x10 矩阵存成 CSV,用 Excel 或 pandas 画个热力图,你会看到下三角的注意力分布。论文附录里展示的注意力可视化,你自己就能复现。这一步做完,Transformer 的阅读笔记才算真正闭环。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 21:10:33

Xcelium xrun 仿真回归实战:从编译到多核加速与覆盖率调优

简介:这份资源是面向硬件验证工程师、芯片设计师及半导体设计自动化从业者的 Cadence Xcelium(xrun)操作指南,兼顾初学者与有经验的技术人员。内容从 Linux 环境下的安装检查、单步与三阶段分离仿真讲起,系统梳理基础仿…

作者头像 李华
网站建设 2026/10/14 23:22:17

西门子S7-200 SMART ST20 PLC从入门到实战:硬件、编程与通讯全解析

我设备间的角落里,那台老旧的ST20 PLC在纸箱里躺了好几年。最近重新翻出来用,才意识到西门子S7-200 SMART这一代产品在中小型自动化项目中的地位有多稳。特别是ST20这个型号,它既不是最低端的入门款,也不是性能最强的旗舰款&#…

作者头像 李华
网站建设 2026/10/14 3:33:44

[Note-DBC]CANdb+下载安装教程

记录CANdb下载安装 一、安装包下载: 1、官方网页下载 下载中心 | Vector 填写个人信息 2、夸克网盘(网页总有点问题,一直跳转不到下载界面) 夸克网盘链接:(有需要的自取): 我用夸…

作者头像 李华
网站建设 2026/10/19 18:43:38

Cline 3.1 版本发布后,如何用 TaoToken 统一 Key 接入 VS Code 编程助手

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 8:23:40

pstack与Claude Code结合:从堆栈定位到AI辅助排障的完整实践

1. 项目思路拆解:当pstack遇上claude code先说清楚这个项目到底在干什么。pstack是Linux下最经典的进程堆栈分析工具,一条命令就能把进程当前所有线程的调用栈打出来,堪称排查卡死、死锁、高CPU问题时的第一把刀。claude code是Anthropic推出…

作者头像 李华
网站建设 2026/10/14 22:16:40

Tracy Profiler实战指南:从集成到分析,精准定位C++性能瓶颈

简介:Tracy 性能分析中文文档是一份面向 C/C 等语言开发者的跨平台性能剖析工具手册,覆盖 CPU、GPU 分析以及实时分析、帧分析、采样分析、远程或嵌入式遥测等核心机制,强调对被测程序影响最小化。文档在官方用户手册基础上清晰组织&#xff…

作者头像 李华