python神经网络编程入门(三十)——Transformer 从 RNN 到注意力:模型为什么需要“瞄一眼“

📍 路标 :本篇位于《从零构建 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_t ht,每读一个词就更新一次记忆:

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(Whhht−1+Wxhxt)

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

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

把这两个运转特征画出来最直观:
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 ⁡ \tanh tanh 这道"压缩门":

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),∂zt∂ht=1−tanh2(zt)

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

python 复制代码
import numpy as np
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)和"不做连乘"(注意力)的区别,下面这个对照实验会一并说明。

python 复制代码
import numpy as np

def rnn_forward(seq, W_hh=0.9, W_xh=0.5):
    h = 0.0
    for x in seq:                 # 一个词一个词地读
        h = np.tanh(W_hh * h + W_xh * x)
    return h

for L in [5, 10, 20, 50]:
    seq = [1.0] + [0.0] * (L - 1)   # 只有开头是关键信息,其余都是 0
    h = rnn_forward(seq)
    print(f"序列长 L={L:3d}  末尾隐藏状态 = {h:.4f}  开头信息残留 ≈ {h*100:.1f}%")

真实的运行结果:

序列长度 L 末尾隐藏状态 开头信息残留
5 0.2610 26.1%
10 0.1452 14.5%
20 0.0493 4.9%
50 0.0021 0.2%

一句话只有 20 个词,开头的关键信息传到结尾就只剩 4.9%;到 50 个词,基本归零(0.2%)。把衰减画出来,是一条急转直下的曲线:
RNN 记忆衰减:开头的信息随序列变长急转直下 0% 10% 20% 30% 40% 50% 0 10 20 30 40 50 序列长度 L 26.1% 14.5% 4.9% 0.5% 信息越传越稀

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

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

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

python 复制代码
def attention_end(seq, alpha=0.6):
    weights = np.array([alpha] + [0.0]*(len(seq)-1))  # 只看开头,权重固定
    return float(weights @ np.array(seq))              # 线性加权,无连乘

for L in [5, 10, 20, 50]:
    seq = [1.0] + [0.0]*(L-1)
    print(f"L={L:4d}  注意力残留 = {attention_end(seq)*100:.1f}%")

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

序列长度 L RNN 残留 注意力残留
5 26.1% 60.0%
10 14.5% 60.0%
20 4.9% 60.0%
50 0.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 t t 步的隐藏状态依赖第 t − 1 t-1 t−1 步,整条链没法并行计算。一句话 100 个词,就得串行跑 100 步;GPU 再强,也没法把这条路拆开同时算。

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

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

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


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

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

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

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

python 复制代码
import numpy as np

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])          # 当前要理解的目标,语义接近 love

scores = 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.0 0.168
love 0.0, 1.0 1.0 0.457
0.8, 0.8 0.8 0.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 ⁡ \tanh tanh 导数恒 ≤ 1 \le1 ≤1 说明了"越传越稀"是数学必然,不是参数巧合;真实模拟 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 三件套

相关推荐
暴躁的小鸟2 小时前
附近口碑好的斜视配镜训练的眼视光中心
人工智能·python·深度学习
手写码匠3 小时前
华为云Flexus+DeepSeek征文|Agent 记忆系统实战:用 DeepSeek-R1/V3 + Dify 会话变量打造跨会话长期记忆
人工智能·深度学习·算法·aigc
bittersuite3 小时前
深层RNN和双向RNN简述
人工智能·rnn·深度学习
LaughingZhu3 小时前
Product Hunt 每日热榜 | 2026-08-09
人工智能·深度学习·神经网络·搜索引擎·百度
阿图灵3 小时前
基于 LSTM 的中文电商评论情感分类:从数据处理到 91% 准确率实战
人工智能·深度学习·分类·nlp·lstm·情感分类
xyz_CDragon4 小时前
MiniMax H3 vs Seedance 2.5 vs Kling 3.0:2026 AI视频生成模型横评(排行榜+价格+开源实测)
人工智能·开源·音视频·transformer·minmax h3
@Mr_LiuYang4 小时前
《深入理解 AI Agent:设计原理与工程实践 》实验2-2 2-7 大模型注意力权重分布可视化
人工智能·大模型·agent·注意力机制
满怀冰雪4 小时前
20-卷积神经网络基础:用 Paddle 构建 CNN
人工智能·深度学习·cnn·paddle
FellAveal5 小时前
【Transformer入门】从函数到Transformer
人工智能·深度学习·transformer