
【斯坦福CS231n 学习笔记4】循环神经网络与长短期记忆网络
介绍了循环神经网络与长短期记忆网络。
训练非循环神经网络方法
- One time setup:第一类决策发生在真正开始训练之前,通常只需要做一次,但是会深刻影响整个优化过程是否可行、是否稳定。在这一层包括激活函数的选择、输入数据的预处理方式、权重初始化策略、以及是否使用归一化层,以及在数据规模不足时是否采用迁移学习。这些选择的共同特征在于,它们并不直接参与每一次参数更新,但是会从根本上塑造损失函数的几何形态。
- Training dynamics:第二类决策贯穿于整个训练过程,这部分关注的核心不再是模型结构本身,而是优化过程如何随时间演化。训练神经网络需要持续监控损失曲线、梯度规模、训练/验证误差的分离情况,并且据此不断调整学习率、动量、正则化强度或 batch size 等超参数。
- Evaluation:模型性能并不等同于训练误差,而是要通过验证集来评估泛化能力。
而接下来要处理的序列模型,会在这一套方法的每一个层面上都带来新的挑战。
序列建模
之前讨论的模型,其输入都可以被表示为一个固定维度的向量或张量,维度在模型设计阶段是已知且固定的,模型的输出同样通常被设计为一个固定维度的向量,例如类型分数或回归值。
但是在现实世界中,存在大量任务,其本质并不是对一个整体对象做一次性判断,而是对一个随时间或顺序展开的过程进行建模。在这类问题中,输入的长度、结构乃至输出的形式,往往都无法事先固定。
例如在图像分类中,一张图被视为一个整体,模型的任务是从中提取判别性特征并输出一个类别;但是在图像描述中,输入仍然是一张图像,输出却不再是一个单一的标签,而是一个词序列,其长度依赖于语义内容本身。类似地,在视频理解任务中,输入不再是一个二维空间结构,而是一系列按照时间顺序排列的帧。模型不仅仅需要理解单帧的空间语义,还必须建模帧与帧之间的时间依赖关系;在语言建模、语音识别、金融时间序列预测等问题中,这种“输入即过程”的特征更加明显。这一类问题的共同点是输入本身携带顺序信息,而这个顺序对输出具有决定性影响。简单地将所有输入拼接成一个超长向量,不仅在计算上不可行,还会破坏这种顺序结构,因此需要引入序列建模来解决这一问题。
序列任务的输入-输出结构类型
- one-to-one:这一类型作为一个对照基准,即使在形式上使用了 RNN 的计算单元,只要输入和输出都只发生在单一时间步上,在语义层面上其仍然等价于一个普通的前馈模型,并不构成真正意义上的序列建模问题。典型的应用是图像分类。
- one-to-many:这一类型的模型只在起始时刻接收一次外部输入,随后便需要在没有额外输入信号的情况下,依靠自身的内部状态逐步生成一个完整的输出序列。此处隐含的建模假设是,初始输入必须被有效地编码进模型的隐状态之中,并且这种编码需要在后续多个时间步中持续发挥作用,从而保证生成序列在语义上的连贯性与一致性。典型的应用是图像描述,通过输入一张图像来输出一段描述文字,其中描述文字的长度是不固定的。
- many-to-one:这一类型的模型需要处理一个完整的输入序列,并且在所有的时间步结束后输出一个单一结果,这一结果往往代表对整个序列的综合判断或高层语义概括。此处的假设在于模型可以通过不断更新隐状态的方式将输入序列中分散在不同时间位置的信息逐步压缩进一个固定维度的表示之中,并且最终通过这一表示完成预测。典型的应用是视频动作分类与情感分析。
- many-to-many(异步):输入是一个序列,输出一个一个序列,但是输入的长度与输出的长度不一定相等,并且输出通常在输入完毕后才开始生成,这种结构常被称为 Sequence-to-Sequence 模型。该类型模型的挑战在于,所有与输入相关的信息都必须通过一个固定维度的内部状态跨越输入阶段与输出阶段的时间间隔再进行传递。典型的应用是机器翻译。
- many-to-many(同步):输入与输出都是序列,且具有时间同步性,即对于每个输入 ,网络都立即产生一个对应的输出 。这种结构在语义上非常自然地对应于逐时间步预测问题,例如序列标注或逐帧分类,其中每一个中间状态都拥有明确的监督信号。
RNN
输入 经过 RNN 得到输出 ,这看似之前的前馈网络没有什么本质区别,但是实际上 RNN 并不是一次性处理整个输入,而是拥有一个随着序列而不断更新的内部状态,这一内部状态并不是某种额外的存储结构,而是网络计算过程本身的一部分,在每个时间步都会被重新计算。
通过将 RNN 沿时间轴展开,可以将其理解为一个在结构上类似于深层前馈网络的计算图。
其中每一个“层”对应序列中的一个时间步,层与层之间通过内部状态相连接。但是与普通深层网络不同的是,这些时间步对应的计算单元并不拥有各自独立的参数,而是严格共享同一组权重,正是这种参数共享,使得 RNN 能够以固定规模的参数来处理任意长度的序列。
而这种隐藏状态的更新,在每一时间步内,新的隐状态是由“上一时刻的隐状态”和“当前输入”共同决定的,并且这一更新由一个带参数的函数来实现,通过这种方式,模型被迫在每一步将历史信息与新信息进行融合,而不是丢弃过去或忽略当前:
而新的输出则是通过另一个带权重的映射函数从隐状态中产生,这也说明隐藏状态可以被视为一种内部表示,其维度和语义并不必然与输出空间相一致,输出只是这一内部状表示在特定任务下的一种特殊的输出:
Vanilla RNN
在 Vanilla RNN 中,通用的更新公式 被具体化为线性变化后接非线性激活函数的形式。
其中 为循环权重矩阵,负责捕捉时间序列上的长期依赖, 为输入权重矩阵,负责将当前输入映射到隐空间。而 为双曲正切函数,将隐状态的值域约束在 之间,这一选择是为了防止在多层递归计算中数值发散,但是从其图像中也可以预见其在梯度回传时的潜在问题。 为输出权重矩阵。
课程通过举一个具体的手工构造 RNN案例来进行说明:定义一个任务,其输入序列为一个二值序列,例如 ,目标任务是检测序列中是否出现了连续的两个 1,为了实现这个逻辑,隐藏状态 被人为赋予了明确的物理含义,将 设定为一个 3 维向量,这三个维度分别充当了不同的“寄存器”角色:
第 0 维存储当前时刻的输入 ,第 1 维存储上一时刻的输入 ,第 2 维存储常数 1,作为一个偏置项用于后续的阈值计算。
其中 被初始化为 ,这是为了将当前输入 写入到隐藏状态的第 0 维,确保输入信号只改变 的第一个分量,而不直接干扰 和 。
被设计为以下矩阵:
第 0 行是为了让上一时刻的信息不保留在当前时刻的第 0 维中,因为第零维度是留给新输入 的。第 1 行将上一时刻的第 0 维复制到当前时刻的第 1 维,实现记忆功能。第三行则是恒等映射,保证第 2维永远保持为 1,充当一个稳定的偏置。
最终得到的完整的状态更新公式实际上执行了以下操作:
这个系统就像是一个移位寄存器,新的 进来填补 ,旧的 移位变成 ,常数 1 保持在 。
最后的输出权重 用于生成输出:
但是在复杂问题中,依靠手工设定是不可能的,因此目前的问题就变成了,应该怎么去找到 。
RNN Computational Graph
通过引入计算图视角,可以看到每一个时间步都是一个独立的节点实例,但是它们在参数层面是完全共享的,这种节点独立、参数共享可以让 RNN 在训练时既能表现出深层网络的特征,又具有与普通深层网络截然不同的优化行为。
在 many-to-many 的情形下,每个时间步都会产生一个输出,并且通常会在每一个时间步定义一个对应的损失项,这意味着整个序列的总损失可以被看作是所有时间步损失的累加,而梯度信号也会在时间维度上被多次注入到隐状态更新的过程中。从训练的角度来看,这种结构相对友好,模型不会完全依赖于最后一个时间步来获得监督信号。
在 many-to-one 的计算图中,隐状态仍然会在每一个时间步被更新,但输出和损失只在序列的最后时刻出现。这意味着所有关于早期输入的梯度信息,都必须通过一条横跨整个时间轴的反向传播路径,才能对早期的状态更新参数产生影响。
在 one-to-many 的计算图中,模型在初始时刻接收一次外部输入,随后完全依赖隐状态递推来生成输出序列。在这一过程中,隐状态不仅承担了“记忆输入”的职责,还成为输出生成的唯一信息来源。因此,任何关于初始输入的误差反馈,都必须通过输出序列的多个时间步反向传播回初始状态。这种结构在生成任务中非常常见,但它也意味着模型在训练时对初始状态表示的质量极其敏感。
时间反向传播
对以上的时间展开计算图进行反向传播的过程,就是所谓的Backpropagation Through Time。在标准的时间反向传播中,模型会首先沿时间维度完整地执行一次前向传播,从初始的隐状态开始,依次计算出每一个时间步的隐状态与输出,并且最终得到一个关于整个序列的标量损失。
但是这样会导致一个问题,当序列很长的时候会导致显存不足,且训练数值不稳定,时间反向传播不是一个纯理论动作,落到具体的训练流程上会付出非常直接的工程代价。
具体的解决方案是可以通过人为设定一个窗口长度,在训练时假设模型目前只看到了这一段,在一段窗口中从 开始,按照时间步读入输入、更新隐藏状态、生成输出并计算损失函数,然后在这个窗口内做反向传播。
这个方法实际上是工程上的妥协,而不是一个理论上的完美解决方案。当时间轴变得很长,而梯度又被截断时,模型对远距离依赖的学习能力在机制层面上已经被削弱了。
隐藏状态的数值传递决定模型在推理阶段“记得住什么”,而梯度的反向传递,决定的是模型在训练阶段“能学会记住什么”,这种截断导致模型在前向时似乎拥有长期记忆,但是在训练时则缺乏塑造这种记忆的有效信号。
接着 Lecture 7 举了一个案例:Character-level Language Model。目的是训练一个 RNN,使其在看到一串字符之后,学会预测下一个字符。
这是一个标准的 many-to-many 序列任务,由于字符本身是离散符号,模型需要先就将每个字符编码为一个 one-hot 向量,one-hot 向量乘以权重矩阵,本质上等价于一次嵌入查询,在每一个时间步,embedding 向量会与上一个时刻的隐藏状态一起,经过同一个状态更新函数得到新的隐藏状态,而后 RNN 的输出会经过一个线性层和 softmax,得到所有可能字符的概率分布,而训练的目标就是让这个分布在真实“下一个字符”处的概率尽可能大。
寻找可解释的神经单元
这部分内容主要是为了探究 RNN 内部到底学到了什么而进行可视化分析。如果 RNN 真的学会了语言,那么其隐藏向量 中的某些维度应该对应具体的语言学特征,随机选取隐藏向量 中的某一个维度,将其激活值用颜色映射到对应的文本上,观察其数值随时间序列的变化规律。
- 引用检测单元:有一个特定的神经元,在遇到开引号
"时其激活值瞬间飙升(变红/正值),并在引号内的所有字符上保持高激活状态,直到遇到闭引号"时瞬间关闭(变蓝/复位)。
这说明模型学会了长距离依赖和状态保持,模型知道自身处于一个引用字符串的内部,这个状态必须保持直到特定的结束符号出现。
- 行长/换行预测单元:另一个神经元在每一行的开头处于低激活状态(蓝色),随着一行字符数的增加,其激活值线性上升(逐渐变红),直到接近行尾时达到峰值,并在换行符
\n出现后瞬间重置为零。
这证明模型学会了计数或边界检测,在没有任何显式的计数指令的情况下学会了在何时应该换行。
- 代码结构/缩进单元:在训练生成 Linux 内核代码的模型中,发现一个神经元能够追踪
if语句或代码块的嵌套深度,在进入if块时激活值增加,随着嵌套层级加深而变化,在退出代码块时恢复。
这证明模型学会了层级结构,模型通过隐状态捕获了高度结构化的编程语言语法树。
这部分的讨论导出了关键的结论,实际上大部分的单元都是无法解释的,无法对应到具体的规则,这也符合分布式表示,即大部分概念并不是由单一神经元编码的,而是由多个神经元的组合模式共同编码的。
RNN tradeoffs
RNN 的优势在于:
- 能够处理任意长度的输入。
- 理论上的无限历史回溯。
- 模型的大小不随着输入而增加。
- 处理的对称性,即参数共享带来的时间平移不变性,模型学习到的特征规则适用于时间轴上的任何位置,无需为不同位置重复学习相同的规则。
于此同时也存在其缺点:
- 递归计算缓慢,无法并行化,具有顺序依赖性。
- 实际上难以访问久远的历史信息,只能学习到短期的局部依赖。
图像描述
Lecture 7 使用图像描述展示了多模态和变长输出的综合案例,将计算机视觉组件与自然语言处理的组件嫁接在一起。
可以看到去除掉了原本图像分类任务中的检测头,提取出固定维度的特征向量输入到 RNN 中,RNN 负责生成相应的文本序列。CNN 输出的向量 经过一个线性变换 赋值给 RNN 的隐藏状态,这也意味着在生成每一个单词的过程中, 都会对隐藏层的更新产生影响,这样能够增强图片信息对生成过程的影响,防止 RNN 在生成长句子时“忘掉”图片中到底有什么。
RNN Variants: Long Short Term Memory (LSTM)
Vanilla RNN Gradient Flow
为了说明为什么要提出 LSTM,对 Vanilla RNN 进行了梯度流分析。当进行时间反向传播时,目标是计算损失函数 关于初始状态或者某个早期参数的梯度。考虑时间步 反向传播到时间步 (其中 ),根据链式法则,需要计算隐状态 关于 的偏导数:
此处的关键项是 这一项展开是一系列雅可比矩阵的连乘:
将 根据 Vanilla RNN 的前向公式 展开,得到
将单步结果带入连乘公式,发现梯度在回传每一步时,都会被乘以该因子:
如果序列很长,这意味着将同一个矩阵 连乘了很多次,结果只有两种极端:
- 当 的最大特征值 时,连乘会将其放大导致梯度数值变得极大,更新权重步长过大导致梯度爆炸。这时可以进行梯度裁剪,再更新参数之前,检查梯度的 L2 范数,如果 ,则按比例缩小:,这能够使梯度方向保持不变,但是模长被限制,通常能够有效解决爆炸问题。
- 当 的最大特征值 时,连乘会将其缩小导致梯度数值无限接近于 0,更新权重较小导致梯度消失,这导致模型只能够看到最近的几个时间步的内容。这里梯度裁剪无法解决这个问题,只能够引入更复杂的单元结构,所以才需要 LSTM。
LSTM
与 Vanilla RNN 仅维护一个单一的隐藏状态 不同,LSTM 在每个时间步维护两个状态向量:
- (Cell state):LSTM 的核心内部状态,可以视为网络的长期记忆,在时间轴上以一种近似线性的方式传递,极少发生剧烈的非线性变换,因此梯度可以很容易地回传。
- (Hidden state):对外部可见的状态,用于输出预测或传递给下一层,是 经过非线性激活和“输出门”过滤后的版本,承载“短期记忆”或者当前时刻的特征。
此外还有四个门控信号:在时间步 ,LSTM 首先接收当前输入 和上一时刻的隐藏状态 ,通过一个大的权重矩阵 进行线性变换,然后分割成四个向量,分别通过激活函数计算出四个控制信号:
四个向量的具体物理含义与数学定义如下:
- :输入门,使用了 Sigmoid 函数 (),决定了想要写入多少信息到细胞状态 中,如果是 0,表示完全忽略当前输入,如果是 1,表示完全写入。
- :遗忘门,使用了Sigmoid 函数 (),决定了想要遗忘多少上一时刻的细胞状态 ,如果是 1,表示完全清空历史,如果是 1,表示完全保留历史。
- :输出门,使用了Sigmoid 函数 (),决定了想要输出多少信息给隐藏状态 。
- :门控值,使用了 函数,是当前时刻计算出的候选新信息。
一旦计算出四个控制信号,LSTM 会执行以下两步更新操作:
- 更新细胞状态 :
其中 说明历史信息 被遗忘门 逐元素地衰减, 说明新信息 被输入门 逐元素地加权。最终通过加法运算将新旧信息进行融合。
- 更新隐藏状态 :
其中 将细胞状态的值域规范化到 , 根据输出门 的指示,选择性地将细胞内部的状态暴露给外界,形成 。
LSTM 如何解决梯度消失
根据加法更新公式 ,对 求偏导,得到:
这说明在梯度流回传的时候,梯度不再乘以一个可能导致爆炸或者消失的稠密矩阵 而是乘以遗忘门向量 , 的每个元素都在 之间,如果 ,梯度几乎就能够无损通过,如果 ,则梯度被截断。梯度的消失与否完全由网络自己控制。如果网络认为这部分历史信息对未来很重要,它就会把遗忘门设为 1,从而保护梯度不消失;而在 Vanilla RNN 中,梯度消失是结构性的缺陷,网络无法控制。
从另一个角度看,根据微积分的加法法则,梯度在通过加法节点时会原样传递,不会发生改变,因此梯度可以长时间保留并且传递到过去。
Modern RNNs
现在序列建模的新趋势是去 Transformer 化 (Beyond Transformers)。通过引入状态空间(State Space)的数学工具,Mamba 和 RWKV 等模型证明了不需要为了高性能而忍受 的计算代价,RNN 的“线性推理”特性在 LLM 时代重新焕发了光彩。
小结
Lecture 7 的内容标志着深度学习从处理静态固定输入转向对动态序列过程的建模。首先确立了 RNN 通过参数共享和隐状态递归来处理任意长度序列的通用范式,但深入分析揭示了 Vanilla RNN 在反向传播中因矩阵连乘导致的梯度消失与爆炸问题。为此,重点引入了 LSTM 架构,通过设计精巧的门控机制与加法更新的细胞状态(Cell State),构建了无阻断的梯度流通道,从而在数学和工程上有效解决了长距离依赖的学习难题,为图像描述等复杂时序任务奠定了基础。
参考如下:
https://cs231n.stanford.edu/index.html
