讲解稿 · 第 07 章 第 03 节 · 循环神经网络¶
口播时长:约 18 分钟|建议语速:中速,公式处停两拍|字数:约 4000 字
承 Ch07 §02 文本处理与经典 NLP → Ch07 §03 循环神经网络 → Ch07 §04 Transformer 与语言模型
导语(约 80 秒)¶
你在外卖平台输入一句评论:“配送不慢,饭也不是不好吃,就是下次不会再点了。”如果情感分析模型只数“慢”“不好吃”这些词,它可能看到两个否定,算成正面;如果只看“不会再点”,又可能忽略前面的铺垫。真正决定语气的,是词出现的顺序,以及读到句尾时还记得多少前文。
机器翻译也一样。英文里的 “bank” 可能是银行,也可能是河岸。模型读到 “He sat by the bank of the river” 时,必须把后面的 “river” 与前面的 “bank” 联系起来,才能选对词义。ChatGPT 生成回答时,也是在已有 token 条件下预测下一个 token,只是现代系统主要使用 Transformer。
循环神经网络,也就是 RNN,曾经是处理这类序列问题的主力。它每读一个 token,就更新一次隐藏状态,像边读边写摘要。难点在于:摘要空间固定,句子却可长可短;训练时还要把误差信号沿时间反传,路径一长,梯度可能变小或变大。今天要讲清普通 RNN 怎样记忆、为什么会忘、LSTM 与 GRU 怎样用门控改善,以及双向 RNN、seq2seq 怎样把它用于真实 NLP 任务。
复习昨日 & 今日路线(约 45 秒)¶
上一节处理的是文本怎样进入模型:分词把句子切成 token,词表把 token 映射成编号,词袋和 TF-IDF 把文本变成向量。这些方法便于分类,但通常弱化甚至丢掉顺序。“狗咬人”和“人咬狗”词频几乎相同,事件角色却完全相反。
这一节把时间顺序放回模型。路线有四站:普通 RNN 的状态更新;时间反向传播及梯度问题;LSTM 与 GRU 的门控;双向 RNN 和编码器—解码器。讲完后,下一节会追问:既然循环必须逐步计算,能不能直接比较所有位置,并行建模长距离关系?那正是 Transformer 的出发点。
核心概念一 · 隐藏状态:给序列一份滚动摘要(约 3 分钟)¶
直觉。 读句子“这家店服务不算快,但味道很好”时,你不会把每个字孤立判断。读到“但”之前,倾向可能略负;读到“味道很好”,判断被转向正面。RNN 的隐藏状态就是一份不断改写的内部摘要:旧摘要与当前输入合并,得到新摘要。
定义。 默认工作在实数域。符号表:\(\mathbf{x}_t\in\mathbb{R}^{d\times1}\)——第 \(t\) 步输入向量;\(\mathbf{h}_t\in\mathbb{R}^{m\times1}\)——第 \(t\) 步隐藏状态;\(W_{xh}\in\mathbb{R}^{m\times d}\)、\(W_{hh}\in\mathbb{R}^{m\times m}\)、\(W_{hy}\in\mathbb{R}^{q\times m}\)——共享参数矩阵;\(\mathbf{b}_h\in\mathbb{R}^{m\times1}\)、\(\mathbf{b}_y\in\mathbb{R}^{q\times1}\)——偏置。普通 RNN 的更新为
$ anh$ 是双曲正切激活函数,把每个分量压到 \((-1,1)\)。例如标量输入为 \(0\) 时,\(\tanh(0)=0\)。这里的“循环”不是把图画成真正的圆,而是每个时间步复用同一组参数,并把前一步状态传给下一步。
例子。 为了能心算,取一维状态,\(h_0=0\),\(W_{xh}=1\),\(W_{hh}=0.5\),\(b_h=0\)。若两个输入依次为 \(x_1=1\)、\(x_2=0\),那么
这两个数由直接代入计算得到,保留五位有效数字。第二步虽然输入为零,状态仍不为零,说明第一步信息确实流到了第二步。
[黑板动作] 画三个横向方框,标成 \(t=1,2,3\);每个方框上方写输入 \(\mathbf{x}_t\),下方写输出 \(\mathbf{o}_t\),方框之间用 \(\mathbf{h}_{t-1}\to\mathbf{h}_t\) 相连。再用同一种颜色圈出三处 \(W_{hh}\),强调参数共享。
反例。 把整句词向量简单平均,不会区分“狗咬人”和“人咬狗”;两句包含相同词,平均结果相同。RNN 按顺序更新状态,两种输入次序通常得到不同的最终状态。另一个反例是把 \(\mathbf{h}_t\) 当成完整原文存档:它只有固定的 \(m\) 个分量,不能保证无损保存任意长文本。
一句话总结。 RNN 用“当前输入 + 旧隐藏状态”产生新隐藏状态,以共享参数处理任意长度序列。
学生常问。 “隐藏状态等于模型输出吗?”不等于。隐藏状态是内部表示;输出通常还要经过线性层与 softmax,才成为词表概率或分类概率。
核心概念二 · 时间反向传播:误差怎样穿过整句话(约 3 分钟)¶
直觉。 如果模型读完整句后才判错,那么第一词也可能承担责任。训练必须从句尾沿计算链往回追:最后一步受倒数第二步影响,倒数第二步又受更早状态影响。这叫时间反向传播,常缩写为 BPTT。
定义。 把同一个 RNN 单元按时间展开,就得到一条深计算图。设标量损失 \(L:\mathbb{R}^{q}\to\mathbb{R}\)。本书方向约定:梯度取列向量,Jacobian 取行向量;对参数列向量 \(\boldsymbol{\theta}\in\mathbb{R}^{p\times1}\),\(J_L\in\mathbb{R}^{1\times p}\),\(\nabla_{\boldsymbol{\theta}}L\in\mathbb{R}^{p\times1}\),且 \(J_L=(\nabla_{\boldsymbol{\theta}}L)^\top\)。反向传播并不显式构造完整 Jacobian,而是逐步计算向量—Jacobian 乘积。
从较晚状态传回较早状态时,会反复乘局部导数。若相隔 \(k\) 步,核心因子形如
其中 \(D_j\) 是第 \(j\) 步 $ anh$ 导数组成的对角矩阵,形状为 \(m\times m\)。$ anh'(z)=1-\tanh^2(z)$,绝对值不超过 \(1\)。连续相乘后,范数可能迅速接近零,也可能迅速增大。
例子。 先看一维近似。假设每一步局部导数都等于 \(0.5\),跨 \(10\) 步后影响因子为 \(0.5^{10}=0.0009765625\),保留五位有效数字是 \(0.00097656\)。若每步因子是 \(1.5\),跨 \(10\) 步得到 \(1.5^{10}=57.665\),保留五位有效数字是 \(57.665\)。前者是梯度消失,后者是梯度爆炸。
[黑板动作] 在刚才三个单元上方再画一条红色反向箭头,从 \(L\) 指回 \(t=3,2,1\)。箭头旁依次写“乘一次局部导数”,再补上 \(0.5^{10}\),让学生看到不是“突然消失”,而是小数连乘。
反例。 很多人把梯度消失理解成“前向状态一定变成零”。其实前向状态可以保持非零,消失的是损失对早期状态或参数的敏感度。另一个反例是仅用更大学习率补救:如果梯度已经接近零,统一放大学习率可能让短路径更新过猛,却未必恢复长路径信息。
工程上常用三类办法:梯度裁剪限制爆炸;更合适的初始化与归一化改善数值;LSTM、GRU 改造状态传递路径,减轻长期依赖训练困难。截断 BPTT 则只反传固定步数,节省显存与计算,但会主动舍弃更远的梯度联系。
一句话总结。 BPTT 把序列展开后应用链式法则,长链上的反复相乘造成梯度消失或爆炸。
学生常问。 “梯度裁剪能解决梯度消失吗?”不能。它主要限制过大的梯度,对已经很小的梯度没有直接修复作用。
核心概念三 · LSTM 与 GRU:用门控制保留和改写(约 3.5 分钟)¶
直觉。 普通 RNN 每一步都重写同一份摘要,像用橡皮擦不断修改便签,早期内容容易被覆盖。LSTM 多维护一条细胞状态,并用几个取值在 \(0\) 到 \(1\) 之间的门,决定旧信息保留多少、新信息写入多少、当前暴露多少。
定义。 LSTM 是长短期记忆网络。符号表:\(\mathbf{c}_t\in\mathbb{R}^{m\times1}\)——细胞状态;\(\mathbf{f}_t,\mathbf{i}_t,\mathbf{o}_t\in(0,1)^{m\times1}\)——遗忘门、输入门、输出门。这里 \(\mathbf{o}_t\) 专指 LSTM 输出门,不是上一小节的输出 logits;为避免静默改义,分类 logits 后文统一写 \(\mathbf{z}_t\)。门由 sigmoid 函数 \(\sigma(a)=1/(1+e^{-a})\) 产生,例如 \(\sigma(0)=0.5\)。核心更新是
其中 \(\odot\) 表示逐元素乘法,\(\widetilde{\mathbf{c}}_t\in(-1,1)^{m\times1}\) 是候选记忆。例如 \((1,2)^\top\odot(3,4)^\top=(3,8)^\top\)。
例子。 取一维状态:旧记忆 \(c_{t-1}=0.8\),遗忘门 \(f_t=0.9\),输入门 \(i_t=0.2\),候选记忆 \(\widetilde c_t=0.5\)。那么
这个数字可直接验算:旧记忆贡献 \(0.72\),新记忆贡献 \(0.10\),合计 \(0.82\)。若句子前面出现主语“孩子们”,网络可以让有关复数的信息在若干步内保持较大的保留比例,帮助后面选择“他们”而不是“他”。这不是手工指定规则,而是训练数据使门学出相应行为。
[黑板动作] 画一条横穿多个时间步的粗线表示 \(\mathbf{c}_t\)。在线上画两个阀门:一个乘 \(\mathbf{f}_t\),一个把 \(\mathbf{i}_t\odot\widetilde{\mathbf{c}}_t\) 加进来。别把门画成开或关两档,要标明它们通常取连续值。
反例。 LSTM 不是“永远不会忘”。若遗忘门长期小于 \(1\),旧信息仍会衰减;训练数据不足时,门也可能学不到正确依赖。门也不是布尔开关:\(0.2\)、\(0.73\) 都是合法值。把它讲成“零表示关、一表示开”只能作为极端直觉,不能当正式定义。
GRU,也就是门控循环单元,把结构简化为更新门与重置门,不单独维护 \(\mathbf{c}_t\)。参数通常少于同隐藏宽度的 LSTM,训练更快;但不能脱离数据、任务和调参断言谁必然更好。短文本分类可能用 GRU 已足够,依赖更复杂时可比较验证集结果再选。
一句话总结。 LSTM 与 GRU 通过可学习的连续门,给长期信息提供更直接的保留与更新路径。
学生常问。 “为什么用了 LSTM 还会梯度消失?”门控是缓解,不是数学保证;序列长度、门值、初始化和优化过程仍会影响梯度。
核心概念四 · 双向 RNN 与 seq2seq:从标注到翻译(约 3 分钟)¶
直觉。 给“苹果”做词性或实体标注时,左边的“我买了”与右边的“手机”都可能影响判断。只从左往右读,当前位置看不到右侧;双向 RNN 便安排两条读取路线。机器翻译更进一步:输入和输出长度不同,需要一个网络读源句,另一个网络逐词生成译句。
定义。 双向 RNN 在位置 \(t\) 计算前向状态 \(\overrightarrow{\mathbf{h}}_t\in\mathbb{R}^{m\times1}\) 与后向状态 \(\overleftarrow{\mathbf{h}}_t\in\mathbb{R}^{m\times1}\),再拼接为
例如两边状态各有 \(3\) 个分量,拼接后就是 \(6\) 个分量。seq2seq 则由编码器与解码器组成:编码器读取变长输入,经典做法把信息压进最终状态;解码器以起始 token 开始,按条件概率
逐步生成,直到结束 token。\(T\) 是输入长度,\(y_{<t}\) 表示目标端此前生成的 token。
例子。 在命名实体识别里,句子“我在苹果工作”中的“苹果”,右侧“工作”支持公司含义;“我吃了一个苹果”中的“苹果”,右侧句尾与左侧量词支持水果含义。双向表示能同时使用两侧上下文。翻译“我爱自然语言处理”时,编码器逐步读入源句,解码器可能依次生成 “I / love / natural / language / processing /
[黑板动作] 左边画两排箭头:上排从左到右,下排从右到左,在“苹果”位置把两支箭头合并。右边画编码器与解码器两个区域,中间先只画一根固定长度向量,随后把它圈成“长句瓶颈”。
反例。 双向 RNN 不能直接用于严格自回归的下一个 token 预测,因为训练时后向网络看到了未来,推理生成时未来尚不存在。经典 seq2seq 也不是输入再长都能稳定翻译:把整句压成一个固定大小向量,长句中的细节会丢失。这一瓶颈促成了注意力机制,让解码器每一步对全部编码器状态加权读取。
一句话总结。 双向 RNN 适合拥有完整上下文的序列标注;seq2seq 用编码器和解码器处理变长输入到变长输出。
学生常问。 “双向 RNN 是不是把同一个 RNN 倒着跑两次?”结构上是两个方向,通常各有独立参数,输出再拼接或组合,并非共享一次状态就结束。
一个走完全程的例子 · 读完一句评论再判断情感(约 2.5 分钟)¶
现在把流程走一遍。评论是:“服务慢,但味道很好”。为便于黑板演示,把它分成四个 token:“服务”“慢”“但”“很好”。真实系统会使用高维词嵌入;这里用一维数表示对正面情感的局部提示:\(x_1=0\)、\(x_2=-1\)、\(x_3=0.5\)、\(x_4=1\)。这些是教学设定,不是词典里的客观分数。
用简化 RNN:
逐步算:
- “服务”:\(h_1=\tanh(0)=0\);
- “慢”:\(h_2=\tanh(-1)\approx-0.76159\);
- “但”:\(h_3=\tanh(0.5+0.5\times(-0.76159))\approx0.11865\);
- “很好”:\(h_4=\tanh(1+0.5\times0.11865)\approx0.78548\)。
以上数值由公式逐步代入,保留五位有效数字。状态先负后正,符合转折句的阅读过程。分类器若取二分类正面概率 \(p=\sigma(2h_4)\),则 \(p=\sigma(1.57096)\approx0.82792\),也就是约 \(82.792\%\)。这个概率只是该玩具参数下的输出,不代表真实评论的客观置信度。
接着看训练。真实标签若是正面 \(y=1\),使用以自然对数定义的二元交叉熵,单位为 nat:
若改用以 \(2\) 为底的对数,则同一信息量约为 \(0.27239\ \text{bit}\),两种单位不能混写。误差从损失传到 \(h_4\),再沿 \(h_3,h_2,h_1\) 反向传播,更新输入与循环参数。若句子有几百步,普通 RNN 的长链梯度会变得难训;换成 LSTM 后,模型可在读到“但”时降低旧负面信息的影响,再让“很好”写入新判断。
这里停一下。这个例子不声称单个标量能处理真实语言,它只把完整机制压缩成可手算版本:token 数值化,状态按顺序更新,末状态进入分类器,交叉熵产生误差,BPTT 调整参数。真实模型只是把标量换成向量和矩阵,并从数据中学习所有权重。
易错点回头看(约 85 秒)¶
- 很多人以为隐藏状态保存了全部历史,其实它只是固定维度的有损摘要,因为任意长序列被压进有限个分量,早期信息可能被覆盖。
- 很多人以为双向 RNN 可直接做自回归生成,其实生成第 \(t\) 个 token 时不能使用未来 token,因为未来内容尚未产生。
- 很多人以为 LSTM 彻底消除了梯度问题,其实门控只提供更顺畅的状态与梯度路径,因为门值、序列长度和优化条件仍可能造成衰减或不稳定。
再补一个常见混淆:序列长度和隐藏维度不是一回事。长度 \(T\) 是时间步数量,隐藏维度 \(m\) 是每一步状态含多少分量。句子从 \(10\) 个 token 增加到 \(100\) 个 token,不会自动把 \(m\) 从 \(128\) 变成 \(1280\);它只让同一个单元多运行九十步。
与下节的钩子(约 30 秒)¶
RNN 的优势是顺序与历史进入同一状态,代价也来自这里:计算第 \(t\) 步必须先得到第 \(t-1\) 步,训练难以把所有 token 完全并行;两个相距很远的词,信息还要穿过很长的状态链。下一节的 Transformer 会取消循环,让任意两个位置通过自注意力直接交互。问题随即变成:没有 RNN 的先后状态,模型怎样知道词序;所有位置互相查看时,计算量又会怎样增长?