python神经网络编程入门(三十)——Transformer 从 RNN 到注意力:模型为什么需要“瞄一眼“
2026/8/9 21:41:01 网站建设 项目流程

📍路标:本篇位于《从零构建 Transformer》系列第 1/16 章 · 基石篇
系列主线:注意力思想 → 自注意力 → 多头 → 位置编码 → 编解码架构 → 手撕实现 → 预训练模型 → 实战微调 → 知识收官
进度:▸基石篇(1-5·本篇)▸ 架构篇(6-10) ▸ 预训练篇(11-13) ▸ 实战篇(14-16)

上一篇(二十九):模型优化、测试评估与知识体系收官


引言:读得越久,忘得越多

上一系列画上句号:我们用 GRU 把 IMDB 影评的情感分类做到了测试集 79.80%,还学会了 Dropout、混淆矩阵这一整套评估方法。循环网络的三大将——RNN、LSTM、GRU——被我们从头手撕了一遍,它们的记忆机制也讲透了。

但这一章,我们踩下一脚刹车,回头审视一个被一路"容忍"下来的问题:RNN 家族从根上就带着一个毛病——读得越久,越把前面的内容忘掉。这不是某个模型没调好,而是"串行读 + 单一记忆"这种设计天生如此。

这一章不做模型,只做一件事:先把这个病根彻底看清,再引出治它的思路——注意力(Attention)。它是整个 Transformer 的起点,也是第二、第三章要动手实现的 Q/K/V 三件套的引子。

🎯本章目标

  1. 说清 RNN 处理长句的两个独立病根:遗忘(长依赖是它的另一面)、串行;
  2. 用真实数值实验,亲眼看到 RNN 怎么把开头忘掉;
  3. 用一个生活类比 + 简易计算,理解"注意力"加权聚焦的思想;
  4. 铺垫清楚:为什么"回头看"能同时解决遗忘和串行两座大山。

一、RNN 的运转方式:串行读 + 单一记忆

先看清 RNN 是怎么"运转"的,病根才看得准。它从头到尾一个词一个词地读,手里捏着一个"隐藏状态"h t h_tht,每读一个词就更新一次记忆:

h t = tanh ⁡ ( W h h h t − 1 + W x h x t ) h_t = \tanh\big(W_{hh}\,h_{t-1} + W_{xh}\,x_t\big)ht=tanh(Whhht1+Wxhxt)

这句公式说了两件重要的事:

  1. 串行:算h 2 h_2h2必须等h 1 h_1h1先算出来,算h 3 h_3h3又得等h 2 h_2h2。整句话像一条单行的传送带,后面的工位必须等前面的工位干完,才能开工。
  2. 单一记忆:所有信息都被塞进同一个向量h t h_tht里。这个向量既是"读过的全部内容",又是"最近一个词的样子"——它要同时记住开头的"我"和结尾的"猫",容量有限,只能和稀泥。

把这两个运转特征画出来最直观:

RNN:一条单行传送带,记忆越传越稀love· · ·结尾h_t = tanh(W_hh · h_{t-1} + W_xh · x_t)所有信息都往这一个向量里塞传送带:第 2 个词必须等第 1 个词读完,第 3 个词再等第 2 个

左边开头的"我"要经过长途跋涉才能传到最后的记忆里,沿途每过一个词都要被"稀释"一次。传得越远,留下的越少。


二、病根一:遗忘(越传越稀)——真实数值实验

理论说得再好,不如动手跑一遍。这里用一个极简的 RNN 前向,模拟"开头有一个关键信息,后面全是无关内容"的场景。因为后面没有新信息输入,最后一步的隐藏状态,就纯粹反映"开头的关键信息传到这里还剩多少"。

先解释代码里那个W_hh=0.9是怎么来的——它不是随手拍的,而是 RNN 的必然。回顾 RNN 的更新公式,隐藏状态必须过tanh ⁡ \tanhtanh这道"压缩门":

h t = tanh ⁡ ( z t ) , ∂ h t ∂ z t = 1 − tanh ⁡ 2 ( z t ) h_t = \tanh(z_t), \qquad \frac{\partial h_t}{\partial z_t} = 1 - \tanh^2(z_t)ht=tanh(zt),ztht=1tanh2(zt)

tanh ⁡ \tanhtanh的导数恒满足1 − tanh ⁡ 2 ( z t ) ≤ 1 1-\tanh^2(z_t)\le 11tanh2(zt)1,而且除了z t = 0 z_t=0zt=0这个点,其余处处都严格小于 1。也就是说,信息每经过一个时间步,无论网络怎么调权重,都至少要被压缩一次。跑一下导数就能看到:

importnumpyasnp z=np.array([0.0,0.5,1.0,2.0,3.0])print("tanh 输入 z :",z)print("tanh 导数 1-tanh^2:",(1-np.tanh(z)**2).round(4))# 输出: [1.0, 0.786, 0.42, 0.071, 0.01]# 除了 z=0,处处 < 1 —— 这就是"越传越稀"的数学根源

所以W_hh=0.9只是给"每步至少压缩一次"一个具体数值,它代表的是那个恒小于 1 的压缩因子。真正要对比的,是"连乘压缩"(RNN)和"不做连乘"(注意力)的区别,下面这个对照实验会一并说明。

importnumpyasnpdefrnn_forward(seq,W_hh=0.9,W_xh=0.5):h=0.0forxinseq:# 一个词一个词地读h=np.tanh(W_hh*h+W_xh*x)returnhforLin[5,10,20,50]:seq=[1.0]+[0.0]*(L-1)# 只有开头是关键信息,其余都是 0h=rnn_forward(seq)print(f"序列长 L={L:3d}末尾隐藏状态 ={h:.4f}开头信息残留 ≈{h*100:.1f}%")

真实的运行结果:

序列长度 L末尾隐藏状态开头信息残留
50.261026.1%
100.145214.5%
200.04934.9%
500.00210.2%

一句话只有 20 个词,开头的关键信息传到结尾就只剩4.9%;到 50 个词,基本归零(0.2%)。把衰减画出来,是一条急转直下的曲线:

RNN 记忆衰减:开头的信息随序列变长急转直下0%10%20%30%40%50%01020304050序列长度 L26.1%14.5%4.9%0.5%信息越传越稀

这就是第 4 章讲过的梯度消失的"记忆版":信息每经过一个时间步都要乘一次小于 1 的系数,连乘几十次,就指数式地缩没了。LSTM 和 GRU 用门控造了条"近路",让梯度能传得更远,但本质仍是"串行传递 + 单一向量"——传得再远,也逃不过"越传越稀"的宿命。

关键对照:注意力为什么"不衰减"

光证明 RNN 会衰减还不够,严谨起见,把注意力放进同一个实验,用同样的"开头=关键信息"场景比一比。注意力的做法是"回头看":给开头位置一个固定权重α \alphaα,直接加权求和——注意,这里没有"传"的过程,也就没有连乘

defattention_end(seq,alpha=0.6):weights=np.array([alpha]+[0.0]*(len(seq)-1))# 只看开头,权重固定returnfloat(weights @ np.array(seq))# 线性加权,无连乘forLin[5,10,20,50]:seq=[1.0]+[0.0]*(L-1)print(f"L={L:4d}注意力残留 ={attention_end(seq)*100:.1f}%")

真实运行,把 RNN 和注意力放在同一张表里:

序列长度 LRNN 残留注意力残留
526.1%60.0%
1014.5%60.0%
204.9%60.0%
500.2%60.0%

RNN 的残留随序列长度指数式下跌ρ L \rho^LρL,连乘);注意力的残留纹丝不动α \alphaα,与长度无关)。根源就一句话:

R N N : 残留 ∝ ρ L ⏟ 连乘 v s 注意力 : 残留 = α ⏟ 不加不减 RNN:\ \text{残留} \propto \underbrace{\rho^{L}}_{\text{连乘}} \qquad vs \qquad \text{注意力}:\ \text{残留} = \underbrace{\alpha}_{\text{不加不减}}RNN:残留连乘ρLvs注意力:残留=不加不减α

RNN 的信息是乘法链,乘十次、乘五十次,越乘越小;注意力是直接加法,看一眼全句,开头就是开头,不经过任何连乘——所以它天然不衰减。这就是注意力解决"遗忘"的机制性答案,不是一句口号。


三、病根二:串行读 = 不能并行

等等——开头说"两个独立病根:遗忘、串行",可上一节只讲了"遗忘"?其实长依赖 = 遗忘的另一面:所谓"长依赖"问题,指的是模型要理解一个词,需要借助离它很远的另一个词(比如句子开头的"我"和结尾的"猫")。而"遗忘"说的是这个远方的信息传过来时已经稀了。同一个现象,从"信息传不动"看叫遗忘,从"需要远词却够不着"看叫长依赖——本质是同一件事,所以二、三两节讲的正是那两个独立病根:遗忘和串行。

除了遗忘,RNN 还有第二个硬伤:必须串行。因为第t tt步的隐藏状态依赖第t − 1 t-1t1步,整条链没法并行计算。一句话 100 个词,就得串行跑 100 步;GPU 再强,也没法把这条路拆开同时算。

注意力却不一样:它读每个词时,都是直接"回头看"整个句子的所有位置,彼此之间没有这种"必须等前一步"的依赖,所以所有位置可以一步并行算完

特性RNN 家族注意力
读取方式串行,逐词并行,一次看全句
记忆载体单一隐藏向量h t h_tht每个词自己的向量
长句开头传到结尾所剩无几直接看得到,不衰减
并行加速不能(串行链)能(全句并行打点)

为什么注意力能并行?看依赖关系就清楚了。RNN 里第t tt个词的结果依赖第t − 1 t-1t1个词的结果,形成一条前后依赖链;而注意力里,每个词的结果只依赖"全句所有词"和"它自己",不存在"必须等前一个词"的先后约束——所有位置的打分彼此独立,可以一次性同步算完。用前面的实验语言说:RNN 是"下一步等上一步"的串行链,注意力是"所有人同时举手、一起算分"的并行广场。


四、治本思路:别"传",直接"看"

既然"传"会丢,"串"会慢,那换个思路:不把信息从头传到尾,而是让模型在需要的时候,直接回头去看整个句子的每一个位置。这就是注意力(Attention)

打个生活比方。读一篇文章要做一道题,聪明的人不是把全文从头背到尾(那会忘),而是先扫一眼全文,找到讲这道题的那几段,重点看它们。注意力干的就是这件事:当模型理解某个词时,它不再只依赖"传过来的记忆",而是回头给整句话的每个词打分——跟当前词关系越大,注意力权重越高,越值得细看。

用一个小例子彻底说清"加权聚焦"。源句是"我 love 猫",现在要理解其中的"love"这个词。把每个词表示成一个向量,再算"当前词"和"每个源词"的相似度打分,最后用 softmax 归一化成权重。先说明:这里的词向量和查询向量是手工构造的"演示值",纯粹为了把"打分→加权"的机制讲清楚——真实模型里这些向量是训练学出来的,不是我们指定答案(第 2、3 章会讲它们怎么从输入里自动算出来)。这里只需看懂:相关度高的词拿到大权重,这就是注意力的全部。

importnumpyasnp words=['我','love','猫']vec=np.array([[1.0,0.0],# 我[0.0,1.0],# love[0.8,0.8]])# 猫query=np.array([0.0,1.0])# 当前要理解的目标,语义接近 lovescores=query @ vec.T# 逐词打相似度分weights=np.exp(scores)/np.exp(scores).sum()# softmax 归一化成权重print("相似度打分 =",scores.round(3))print("softmax 权重 =",weights.round(3))

真实运行结果:

源词词向量相似度分注意力权重
[1.0, 0.0]0.00.168
love[0.0, 1.0]1.00.457
[0.8, 0.8]0.80.374

把权重画出来,一目了然:

注意力:理解"love"时,回头给每个源词打分love当前词love权重 0.168权重 0.457(最相关)权重 0.374把注意力重点放在最相关的"love"上

模型把最大权重 0.457分给了最相关的"love",其次是"猫"(0.374)——因为它俩语义上有联系——"我"的相关度最低(0.168)。最后把这几个词按权重加权求和,就是"love"这个位置的上下文表示:既保留了它自己的含义,又融合了全句的相关信息。关键是——这一步从头到尾根本不用"传",直接平着看了一遍全句,所以开头、结尾的信息一个都不丢。

这一套"打分 → softmax 加权求和",就是注意力的核心骨架,也是第 2、3 章要手撕成 Q/K/V 三件套的雏形。


五、常见坑与自查

  • 误以为 LSTM/GRU 已经解决遗忘:它们只是把"乘一个系数"换成了"门控调控",但仍走"串行 + 单一向量"的路,长句照样会稀。注意力才是彻底绕开这条路。
  • 对"串行"理解含糊:串行不是"计算量大",而是"必须等前一步算完才能算下一步"。注意力把所有位置同时打分,一步到位,所以能并行。
  • 注意力权重 ≠ 理解程度:权重表示"该看哪个词",是模型学出来的偏好,不代表这个词一定"重要",只是"对当前任务相关"。
  • 把注意力当成黑盒:这一章先建立"回头看 + 加权聚焦"的直觉,千万不要急着跳去背公式。有了直觉,第 2、3 章的手撕才不迷路。

小结

这一章从 RNN 的收官处踩下刹车,把循环网络的老底掀开看了一遍:

  • 两个独立病根:遗忘(信息沿串行链越传越稀,配套"长依赖"是它的另一面)+ 串行(前后依赖,不能并行);
  • 必然衰减:用tanh ⁡ \tanhtanh导数恒≤ 1 \le11说明了"越传越稀"是数学必然,不是参数巧合;真实模拟 20 词剩 4.9%、50 词剩 0.2%;
  • 正反对照:同一场景下 RNN 残留指数式下跌、注意力纹丝不动——证明"注意力不衰减"是加法 vs 乘法链的机制差异,而非口号;
  • 加权聚焦:用"我 love 猫"的演示跑出 0.457 的最大权重落在最相关词上,理解了"打分→加权求和"的骨架(演示向量仅用于讲机制)。

这里种下的一颗种子,是后续所有章的总开关:第 2、3 章把它实现成 Q/K/V 三件套,第 4 章长出多头,第 5 章补上顺序,第 6 章拼成完整 Transformer。下一章,就正式走进自注意力的数学世界。


下一篇(三十一):自注意力(Self-Attention)——Q、K、V 三件套

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

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

立即咨询