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 三件套的引子。本章目标说清 RNN 处理长句的两个独立病根遗忘长依赖是它的另一面、串行用真实数值实验亲眼看到 RNN 怎么把开头忘掉用一个生活类比 简易计算理解注意力加权聚焦的思想铺垫清楚为什么回头看能同时解决遗忘和串行两座大山。一、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(Whh​ht−1​Wxh​xt​)这句公式说了两件重要的事串行算h 2 h_2h2​必须等h 1 h_1h1​先算出来算h 3 h_3h3​又得等h 2 h_2h2​。整句话像一条单行的传送带后面的工位必须等前面的工位干完才能开工。单一记忆所有信息都被塞进同一个向量h t h_tht​里。这个向量既是读过的全部内容又是最近一个词的样子——它要同时记住开头的我和结尾的猫容量有限只能和稀泥。把这两个运转特征画出来最直观RNN一条单行传送带记忆越传越稀我love猫· · ·结尾h_t tanh(W_hh · h_{t-1} W_xh · x_t)所有信息都往这一个向量里塞传送带第 2 个词必须等第 1 个词读完第 3 个词再等第 2 个左边开头的我要经过长途跋涉才能传到最后的记忆里沿途每过一个词都要被稀释一次。传得越远留下的越少。二、病根一遗忘越传越稀——真实数值实验理论说得再好不如动手跑一遍。这里用一个极简的 RNN 前向模拟开头有一个关键信息后面全是无关内容的场景。因为后面没有新信息输入最后一步的隐藏状态就纯粹反映开头的关键信息传到这里还剩多少。先解释代码里那个W_hh0.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​),∂zt​∂ht​​1−tanh2(zt​)tanh ⁡ \tanhtanh的导数恒满足1 − tanh ⁡ 2 ( z t ) ≤ 1 1-\tanh^2(z_t)\le 11−tanh2(zt​)≤1而且除了z t 0 z_t0zt​0这个点其余处处都严格小于 1。也就是说信息每经过一个时间步无论网络怎么调权重都至少要被压缩一次。跑一下导数就能看到importnumpyasnp znp.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]# 除了 z0处处 1 —— 这就是越传越稀的数学根源所以W_hh0.9只是给每步至少压缩一次一个具体数值它代表的是那个恒小于 1 的压缩因子。真正要对比的是连乘压缩RNN和不做连乘注意力的区别下面这个对照实验会一并说明。importnumpyasnpdefrnn_forward(seq,W_hh0.9,W_xh0.5):h0.0forxinseq:# 一个词一个词地读hnp.tanh(W_hh*hW_xh*x)returnhforLin[5,10,20,50]:seq[1.0][0.0]*(L-1)# 只有开头是关键信息其余都是 0hrnn_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,alpha0.6):weightsnp.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(fL{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:残留∝连乘ρL​​vs注意力:残留不加不减α​​RNN 的信息是乘法链乘十次、乘五十次越乘越小注意力是直接加法看一眼全句开头就是开头不经过任何连乘——所以它天然不衰减。这就是注意力解决遗忘的机制性答案不是一句口号。三、病根二串行读 不能并行等等——开头说两个独立病根遗忘、串行可上一节只讲了遗忘其实长依赖 遗忘的另一面所谓长依赖问题指的是模型要理解一个词需要借助离它很远的另一个词比如句子开头的我和结尾的猫。而遗忘说的是这个远方的信息传过来时已经稀了。同一个现象从信息传不动看叫遗忘从需要远词却够不着看叫长依赖——本质是同一件事所以二、三两节讲的正是那两个独立病根遗忘和串行。除了遗忘RNN 还有第二个硬伤必须串行。因为第t tt步的隐藏状态依赖第t − 1 t-1t−1步整条链没法并行计算。一句话 100 个词就得串行跑 100 步GPU 再强也没法把这条路拆开同时算。注意力却不一样它读每个词时都是直接回头看整个句子的所有位置彼此之间没有这种必须等前一步的依赖所以所有位置可以一步并行算完。特性RNN 家族注意力读取方式串行逐词并行一次看全句记忆载体单一隐藏向量h t h_tht​每个词自己的向量长句开头传到结尾所剩无几直接看得到不衰减并行加速不能串行链能全句并行打点为什么注意力能并行看依赖关系就清楚了。RNN 里第t tt个词的结果依赖第t − 1 t-1t−1个词的结果形成一条前后依赖链而注意力里每个词的结果只依赖全句所有词和它自己不存在必须等前一个词的先后约束——所有位置的打分彼此独立可以一次性同步算完。用前面的实验语言说RNN 是下一步等上一步的串行链注意力是所有人同时举手、一起算分的并行广场。四、治本思路别传直接看既然传会丢串会慢那换个思路不把信息从头传到尾而是让模型在需要的时候直接回头去看整个句子的每一个位置。这就是注意力Attention。打个生活比方。读一篇文章要做一道题聪明的人不是把全文从头背到尾那会忘而是先扫一眼全文找到讲这道题的那几段重点看它们。注意力干的就是这件事当模型理解某个词时它不再只依赖传过来的记忆而是回头给整句话的每个词打分——跟当前词关系越大注意力权重越高越值得细看。用一个小例子彻底说清加权聚焦。源句是我 love 猫现在要理解其中的love这个词。把每个词表示成一个向量再算当前词和每个源词的相似度打分最后用 softmax 归一化成权重。先说明这里的词向量和查询向量是手工构造的演示值纯粹为了把打分→加权的机制讲清楚——真实模型里这些向量是训练学出来的不是我们指定答案第 2、3 章会讲它们怎么从输入里自动算出来。这里只需看懂相关度高的词拿到大权重这就是注意力的全部。importnumpyasnp words[我,love,猫]vecnp.array([[1.0,0.0],# 我[0.0,1.0],# love[0.8,0.8]])# 猫querynp.array([0.0,1.0])# 当前要理解的目标语义接近 lovescoresquery vec.T# 逐词打相似度分weightsnp.exp(scores)/np.exp(scores).sum()# softmax 归一化成权重print(相似度打分 ,scores.round(3))print(softmax 权重 ,weights.round(3))真实运行结果源词词向量相似度分注意力权重我[1.0, 0.0]0.00.168love[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 \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 三件套