RNN

传统神经网络的局限性

  1. 输入输出固定
  2. 计算步骤固定
    • 层数固定,每次处理都走一遍计算图
    • 不具备处理任意长度输入序列的能力

RNN 的优势

  1. 能处理变长的序列(输入输出均可变长)
    • 一个时间步一个计算单元,不是增加网络层,而是增加时间步
  2. 每一步的计算共享权重,可无限展开
    • 相当于用同一个“程序块”在时间上不断迭代执行
    • 时间维度的”深度”是动态的,而不是结构上死板的堆层数
  3. 从函数式学习走向“可微程序设计”

If training vanilla neural nets is optimization over functions, training recurrent nets is optimization over programs.

RNN 的结构

基本的 pipeline

RNN 主要由输入层、隐藏层和输出层组成,其中隐藏层会在每个时间步(t)维护一个隐藏状态

2. RNN 的问题

(1) 梯度消失和梯度爆炸

  • 由于反向传播时梯度要经过多个时间步,如果权重矩阵的值过小,梯度会指数级衰减(梯度消失);如果权重矩阵的值过大,梯度会指数级增长(梯度爆炸)。
  • 解决方案
  • 梯度裁剪(Gradient Clipping):将梯度值限制在一定范围
  • 使用 LSTM / GRU(优化版本的 RNN)

(2) 长距离依赖问题

  • 由于普通 RNN 只能记住短期信息,当序列很长时,早期信息难以传递到后期
  • 解决方案
  • LSTM(Long Short-Term Memory)
  • GRU(Gated Recurrent Unit)

pytorch 代码

注意 pytorch 将循环步骤封装到了 nn. RNN 模块里了,具体实现参照原码和 cs231n 作业

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch
import torch.nn as nn

# 定义 RNN 模型
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)

def forward(self, x):
out, _ = self.rnn(x)
out = self.fc(out[:, -1, :]) # 取最后一个时间步的输出
return out

# 创建 RNN 实例
input_size = 10 # 输入特征维度
hidden_size = 20 # 隐藏层维度
output_size = 1 # 输出维度

model = SimpleRNN(input_size, hidden_size, output_size)
x = torch.randn(5, 3, 10) # (batch_size=5, seq_len=3, input_size=10)
output = model(x)
print(output.shape) # 输出 (5, 1)

RNN 是字符级语言模型(Character-level Language Model)**

字符级语言模型(Character-level Language Model)中推理阶段进行采样,增加输出多样性

阶段 是否采样 是否可微 用途
训练 不采样,用真实标签 可微分 用于学习参数
测试/生成 采样(或 argmax) 不可微 用于生成新文本
高级方法(Gumbel、REINFORCE) 近似采样 近似可微 特殊任务下尝试采样训练

测试阶段是否采样取决于任务的类型,若是翻译类型,则要求准确性;若为生成更丰富和多样化的文本时,比如文本生成(如 GPT 生成对话)、诗歌创作、自动写作等任务, 则要采样

**拓展:温度采样

在实际应用中,采样时可以引入**温度参数(temperature)**来控制采样的随机性:

  • 高温度 T > 1:增加探索性,生成更加随机和多样的文本。
  • 低温度 T < 1:减少随机性,更接近 argmax,但仍保留一定的随机性。
  • T=0.1 → 选择最高概率的单词(接近 argmax)
  • T=1.0 → 典型的采样
  • T=2.0 → 允许更多低概率的单词被选中(更加随机)

测试阶段 softmax 向量还是one-hot 向量

1. 训练 vs. 测试的一致性

训练过程中(training phase),RNN(或其他语言模型)通常使用one-hot 向量作为输入:

  • One-hot 向量是稀疏的,表示某个字符或单词的独特标识。
  • 例如,假设词汇表(Vocabulary)为 h, e, l, o,字符 ‘h’ 的 one-hot 表示为:

h = [1, 0, 0, 0]

‘e’ 则为:

e = [0, 1, 0, 0]

  • 训练时,模型习惯于接收 one-hot 输入,并学习如何从这些独立的输入映射到隐藏状态和输出。

但如果在测试时(inference),你改为输入整个 softmax 向量(即概率分布,而非 one-hot 向量),这与模型在训练时见过的数据完全不同,会导致模型行为异常,甚至产生不稳定或无意义的输出(即“爆炸”)。

为什么会“爆炸”或失败?

  • 训练时输入的是 one-hot 向量(稀疏的),而 softmax 向量是密集的概率分布
  • 模型在训练时并没有学习如何处理 softmax 向量输入
  • 输入分布的变化可能导致网络行为不可预测,输出垃圾结果

这一点可以类比于神经网络在训练时使用 RGB 图像,但测试时输入灰度图像,如果模型没有专门处理过这种输入变化,结果通常会非常糟糕。

2. 计算复杂性

假设词汇量很大,比如 10,000 个单词,如果我们在测试时输入 softmax 概率向量:

  • 这意味着每一步输入是一个长度 10,000 的 dense(密集)向量,而不是一个one-hot 稀疏向量(其中只有一个非零值)。
  • 计算负担:神经网络的权重矩阵需要和这个 dense 向量相乘,这会显著增加计算复杂性。
  • 存储问题:神经网络通常使用稀疏矩阵优化(sparse tensor operations),one-hot 只需索引操作,而 softmax 向量则需要完整存储和计算。

因此,在现实应用中,计算效率和内存管理也是为什么我们在测试时仍然使用 one-hot 的关键原因。

3. 什么时候可能使用 softmax 向量?

尽管一般来说测试时使用 one-hot,但在一些特定的情况下,输入 softmax 向量可能有用,比如:

  1. 温度采样(Temperature Sampling)
  • 通过 softmax 概率对字符进行采样,而不是总是选择最高概率的字符(argmax)。
  • 这样可以生成更加多样化的文本,而不是固定模式的输出。
  1. 连续 softmax 分布输入(Soft Sampling)
  • 在一些高级模型(如自回归变分自动编码器,Auto-Regressive VAE)中,可能使用 softmax 向量的插值,以产生更平滑的序列转换。
  1. 神经机器翻译中的 Beam Search
  • 可能在候选词之间进行概率混合,但最终仍然会选择一个确定的词,而不是直接将 softmax 向量输入到下一个时间步。

4. 结论

  • 在测试时,我们通常不会输入 softmax 向量,而是使用 one-hot 向量,因为:
  1. 训练和测试的一致性问题:模型在训练时习惯 one-hot 输入,测试时使用 softmax 可能导致不稳定的结果。
  2. 计算效率问题:softmax 向量是 dense(密集的),计算代价更高,而 one-hot 是稀疏的,计算和存储都更高效。
  3. 现实应用需求:在大规模词汇(如 NLP 任务)中,稀疏表示更易优化,而 softmax 输入会增加计算复杂性。

最终,除非有特殊需求(如温度采样),在测试阶段,我们一般仍然使用 one-hot 作为输入,而不是 softmax 向量。

RNN训练超长序列存在的问题以及解决方案

  1. 时间反向传播(BPTT):RNN 训练时,前向传播沿时间轴计算,反向传播沿时间轴回溯计算梯度。
  2. 超长序列的问题
  • 计算慢:整个序列的前向+反向传播后才更新一次参数,训练太慢。
  • 内存爆炸:长序列存储所有隐藏状态和梯度,导致内存溢出。
  1. 解决方案
  • 截断式 BPTT(Truncated BPTT):只计算固定长度窗口内的梯度。
  • Mini-batch 训练:按小块处理数据,提高效率。
  • 使用 Transformer 替代 RNN:Transformer 通过自注意力并行计算,不需要 BPTT,更高效。

截断式反向传播 TBPTT

TBPTT 是一种近似方法,它的核心思想是:

  1. 仅在固定长度的时间窗口内执行反向传播
  • 例如,我们选择100 个时间步作为一个小块(batch)。
  • 前向传播正常进行,输入完整序列,但每次只计算最近 100 个时间步的损失
  • 反向传播仅回溯这 100 个时间步,不会影响更早的时间步。
  • 这样,梯度计算和存储需求大大降低,提高训练速度。
  1. 跨批次传递隐藏状态
  • 在计算下一批数据时,我们不会重新初始化 RNN 的隐藏状态,而是继续使用上一个 batch 计算出的隐藏状态,保证序列信息的连续性。
  • 这样可以让模型记住较长时间的上下文信息,避免截断导致的信息丢失。

1. 具体执行流程

(1) 训练步骤

  1. 输入整个序列(假设非常长),但只在固定长度窗口(如 100)内计算损失:
  • 例如,假设输入是一个 10,000 词的文本,我们只在前 100 词计算损失。
  1. 在窗口内进行梯度计算和参数更新(反向传播只处理最近 100 词)。
  2. 跨批次传递隐藏状态
  • 计算下一批数据(第 101-200 词)时,继续使用**前一批(第 1-100 词)**的隐藏状态,而不是重新初始化。
  1. 重复上述过程,直到遍历完整个数据集

2. 为什么这样做是有效的?

  1. 计算更快
  • 普通 BPTT:对整个序列反向传播,计算慢、梯度更新间隔长。
  • TBPTT:每 100 个时间步就更新一次,训练速度大幅提高。
  1. 节省内存
  • 只存储 100 个时间步的梯度,而不是整个长序列的数据,减少 GPU/CPU 占用。
  1. 保持长期依赖
  • 通过跨批次传递隐藏状态,模型仍然能够学习长时间依赖的信息,而不会因为截断完全遗忘之前的内容。

3. TBPTT 在大规模数据集上的应用

在训练像 RNN 这样的模型时,整个数据集可能非常大(如维基百科文本、语音信号、时间序列数据),计算每个数据点的完整梯度成本太高

因此,我们通常:

  1. 采用小批量训练(Mini-batch Training)
  • 不是一次处理整个数据集,而是取小批量(如 batch size=32, 64),在这些小样本上更新模型。
  • 这样可以提高计算效率,并行计算多个样本。
  1. 使用 TBPTT 计算梯度
  • 时间维度上截断,只计算最近 100 个时间步的梯度,而不是整个长序列。
  • 结合 mini-batch 训练,优化计算资源的利用。

RNN 可解释性研究

值得一提,该论文由 Andrej KarpathyFei-Fei Li 发表,[1506.02078] Visualizing and Understanding Recurrent Networks

1. 背景:理解 RNN 内部工作机制

RNN 通过**隐藏状态向量(hidden state vector)存储序列信息,每个时间步都会更新隐藏状态。这些向量中的每个元素(cell)**可能在学习过程中捕捉到不同的信息。

RNN 的隐藏状态是一个高维向量,通常难以解释

  • 大多数隐藏状态的变化看起来像“随机噪声”。
  • 部分单元可能学习到了有意义的模式,比如识别引号、计算行长度、跟踪代码结构等。

2. 研究方法

研究者们训练了一个字符级语言模型(character-level language model),然后分析隐藏状态的某个单元在不同时间步的变化:

  • 选定一个隐藏单元(cell),观察它在不同字符输入时的值。
  • 颜色可视化:用颜色(如红色、蓝色)来表示隐藏单元的激活强度,分析它对哪些字符或模式敏感。

3. 发现的可解释单元

研究发现,一些隐藏单元可以自动学会捕捉某些语法或结构信息,包括:

(1) 引号检测单元(Quote Detection Cell)

  • 现象
  • 这个单元在没有引号时是“关闭”的(蓝色)。
  • 当遇到 第一个引号 时,它会被“激活”(变红)。
  • 在引号闭合后(遇到第二个引号),它又会关闭。
  • 意义
  • 说明这个 RNN 训练过程中自动学会了检测引号对,尽管它只是一个字符预测模型,并未被明确教导去识别引号结构。

(2) 行长度追踪单元(Line Length Tracking Cell)

  • 现象
  • 每行的开头,该单元值从 0 开始
  • 逐渐变红,表示它在累积当前行的字符数量
  • 遇到换行符 \n 后,值重置为 0。
  • 意义
  • 这个单元可能在帮助 RNN 预测文本结构,例如判断何时需要插入换行符

(3) 代码缩进跟踪单元(Code Depth Cell)

  • 现象
  • 这个单元在 if 语句的条件内部变红,在 if 语句外部则保持蓝色。
  • 可能对代码缩进层级敏感,能区分代码块的结构。
  • 意义
  • 说明 RNN 自动学会了代码的层次结构,这对自动代码补全或静态分析很有帮助。

(4) 代码注释检测单元(Comment Detection Cell)

  • 现象
  • 这个单元在代码注释(// 或 /* … */)的区域变红。
  • 在正常代码区域恢复蓝色。
  • 意义
  • 说明 RNN 能够区分代码和注释,这对代码理解任务(如代码摘要生成)可能有帮助。

4. 关键结论

  • 即使 RNN 只是训练来预测下一个字符,它也会自动学习到输入数据的结构信息!
  • 隐藏单元的部分维度可以捕捉有意义的语法或结构特征,如:
  • 句子中的引号匹配
  • 代码的 if 语句范围
  • 换行符的位置
  • 代码注释区域
  • 这说明 RNN 内部包含了比“字符预测”更丰富的语义信息,这对解释性 AI、可视化和优化 RNN 设计非常重要。

5. 该研究的意义

  1. 提升 RNN 可解释性
  • 过去,RNN 被认为是“黑箱”,难以理解其内部工作原理。
  • 这项研究表明,RNN 内部单元可能学到了有意义的模式,可以用可视化方法进行解释。
  1. 优化 NLP 任务
  • 发现 RNN 自动学会了某些语言特性,意味着可以设计更有效的模型
  • 例如,在代码补全、语法分析、自动文本摘要等任务中,可以利用这些“特定单元”。
  1. 启发 Transformer 研究
  • 这一研究帮助人们理解了循环网络的局限性,推动了Transformer 模型的发展。
  • Transformer 通过**自注意力机制(self-attention)**来显式建模这种结构信息,而不依赖隐式的隐藏状态。

图像字幕生成(Image Captioning) 任务的架构

课程中的方法

改进的 RNN 公式如下:

这里,新增的 主要有两个作用

(1) 让图像信息在整个文本生成过程中持续发挥作用

  • 在标准 RNN 结构中,隐藏状态 h 负责保留过去的信息,并影响未来的输出。
  • 但如果只在第一个时间步注入图像信息,那么随着序列生成的推进,图像的影响会逐渐衰减
  • 额外的 让图像特征能够在每个时间步都影响隐藏状态,从而更稳定地指导文本生成

(2) 让图像信息有独立的学习路径

  • 文本输入 x 和图像信息 v 具有不同的特征空间
  • 文本是离散数据(词嵌入),通常来自 NLP 任务(如 Word2Vec、BERT)。
  • 图像是连续数据,通常来自 CNN(如 ResNet、VGG)。
  • 如果直接用同一个权重矩阵 处理两者,可能会导致信息混杂,学习效果下降
  • 引入单独的 可以让模型独立学习图像信息对文本生成的影响,提高文本的连贯性和语义一致性。

后来的改进

  • CNN 负责提取图像特征(VGG、ResNet),并通过 结合 RNN。
  • RNN 负责生成文本,在每个时间步使用 让图像信息持续影响生成过程。
  • 改进方法:后续引入了 注意力机制(Attention),动态调整不同时间步对图像区域的关注程度,而不是使用全局

RNN 记忆力有限的原因

(1) 梯度消失问题(Vanishing Gradient Problem)

在**反向传播(Backpropagation Through Time, BPTT)**过程中,误差信号需要沿着时间方向传播。但在传统 RNN 中:

  • 由于激活函数(如 tanh, sigmoid)的梯度范围在 (-1, 1) 之间,在长时间序列中进行多次梯度相乘后,梯度可能趋近于 0,导致早期时间步的信息无法有效传递到后面的时间步
  • 这就是RNN 无法有效处理长序列的主要原因

(2) 解决方案

为了解决 RNN 遗忘远距离信息的问题,提出了两种改进:

  1. LSTM(Long Short-Term Memory)
  • LSTM 通过引入记忆单元(Memory Cell)和门控机制(输入门、遗忘门、输出门),可以选择性地记住或忘记信息,避免信息丢失。
  • 公式:

其中:

  • 是 LSTM 的长期记忆单元。
  • 是遗忘门,决定是否丢弃旧信息。
  • 是输入门,决定是否接收新信息。
  1. GRU(Gated Recurrent Unit)
  • GRU 是 LSTM 的简化版本,只有更新门(update gate)和重置门(reset gate),计算更高效,同时仍然能够捕捉长期依赖信息。

初探多模态

视觉问答(VQA)多模态学习(Multimodal Learning) 中,模型需要处理不同类型的数据,例如:

  • 图像(Image):用 CNN 提取的图像特征向量。
  • 文本(Text):用 RNN / Transformer 编码的问题向量。

为了让模型能够同时利用这两种信息,我们需要将它们组合起来,然后输入到后续的神经网络层进行处理。

如何组合不同的输入?

(1) 直接连接(Concatenation)

最简单的方法是直接拼接(Concatenate)

z = [v; q]

其中:

  • v 是图像特征向量(由 CNN 提取)。
  • q 是文本问题向量(由 RNN/Transformer 编码)。
  • z 是拼接后的新向量,表示组合后的输入
1
2
3
4
5
6
7
import torch

v = torch.randn(1, 512) # CNN 提取的图像特征
q = torch.randn(1, 256) # RNN 提取的问题向量

# 直接拼接(Concatenation)
z = torch.cat([v, q], dim=1) # 组合后得到 (1, 768) 维向量

(2) 乘法交互(Multiplicative Interaction)

除了简单拼接,有时候我们希望让两个向量之间产生更复杂的关系,可以使用乘法交互,例如:

  1. 逐元素乘法(Element-wise Multiplication)

  • 这样可以增强输入之间的特定特征交互。
  • 但是要求 v q 具有相同的维度,否则不能直接相乘。
  1. 双线性变换(Bilinear Transformation)

  • 这里 W 是一个学习的权重矩阵,它让两个向量进行更复杂的交互。
  • 优点:可以捕捉更复杂的关系。
  • 缺点:计算量较大,可能导致过拟合。

示例代码(PyTorch):

1
2
3
4
5
6
# 逐元素乘法
z_mult = v * q # 需要 v 和 q 维度相同

# 双线性变换
W = torch.randn(512, 256) # 需要学习的权重矩阵
z_bilinear = v @ W @ q.T # 结果是一个标量

RNN 反向传播的问题

原因

  • 梯度的反向传播,相当于不断地左乘

or

解决方案

(1) 梯度裁剪(Gradient Clipping)

  • 在每次梯度更新时,将梯度限制在一个最大值(如 5)。
1
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5)

(2) 使用正则化

  • 可以在训练过程中对权重矩阵 W_h 施加约束,比如:
  • L2 正则化(权重衰减):避免权重值过大。
  • Spectral Normalization(谱归一化):约束最大奇异值,使其接近 1。

(3) 使用 LSTM / GRU

  • LSTM 和 GRU 通过门控机制(gates),让梯度可以更好地控制,不会完全消失或爆炸。
  • LSTM 的长期记忆单元通过加法(不是乘法)存储信息,使其更稳定。