查看全课大纲(第 83 / 93 节)
  1. 1.计算机视觉概述
  2. 2.计算机视觉应用场景
  3. 3.困难与挑战
  4. 4.开源库介绍
  5. 5.图像显示与存储
  6. 6.CLAHE
  7. 7.腐蚀膨胀与开闭运算
  8. 8.滤波与边缘填充
  9. 9.各种滤波
  10. 10.图像金字塔
  11. 11.傅里叶变换
  12. 12.短时傅里叶变换与小波变换
  13. 13.颜色特征与颜色直方图
  14. 14.几何特征_边缘
  15. 15.几何特征-角点与斑点
  16. 16.局部特征-SIFT特征描述子
  17. 17.局部特征-SURF特征描述子
  18. 18.局部特征-ORB特征描述子
  19. 19.其他特征描述子
  20. 20.传统图像分割技术
  21. 21.GraphCut
  22. 22.GrabCut
  23. 23.人脸检测的传统方法
  24. 24.行人检测的传统方法HOG+SVM
  25. 25.行人检测DPM
  26. 26.人工神经网络的架构
  27. 27.目标函数与梯度下降
  28. 28.误差反向传播
  29. 29.可手算的神经网络
  30. 30.改进目标函数与克服梯度消散
  31. 31.神经网络之传统神经网络与深度网络的差异
  32. 32.各种梯度下降算法
  33. 33.如何选择合适的梯度下降算法
  34. 34.避免过适应
  35. 35.CNN的架构
  36. 36.CNN的误差反向传播_卷积层
  37. 37.CNN的误差反向传播_池化层
  38. 38.分类所用的数据集
  39. 39.Alexnet详解
  40. 40.VGG详解
  41. 41.GoogLeNet_v1
  42. 42.BatchNormalization
  43. 43.GoogLeNet_v3v4
  44. 44.ResNet
  45. 45.ResNeXt
  46. 46.CNN设计准则
  47. 47.图像检索与推土机距离
  48. 48.基于颜色相似性
  49. 49.基于形状相似性
  50. 50.基于纹理相似性
  51. 51.基于局部特征相似性
  52. 52.DL的二值哈希编码
  53. 53.高维空间中检索KDTree
  54. 54.高维空间中检索LSH
  55. 55.目标识别任务综述
  56. 56.RCNN
  57. 57.SPP-Net
  58. 58.Fast R-CNN
  59. 59.Faster R-CNN
  60. 60.R-CNN系列模型回顾
  61. 61.R-FCN的设计思路
  62. 62.R-FCN的实现
  63. 63.YOLOv1
  64. 64.YOLOv2_9000
  65. 65.YOLOv3
  66. 66.图像分割应用场景与数据集
  67. 67.上采样的实现方式
  68. 68.FCN网络架构的由来
  69. 69.FCN构建细节
  70. 70.DeepLabv1
  71. 71.CRF
  72. 72.DeepLabv2
  73. 73.DeepLabv3
  74. 74.特定的医学影像分割任务
  75. 75.U-Net
  76. 76.3D U-Net
  77. 77.V-Net
  78. 78.DenseNet
  79. 79.病理切片分析任务及数据集
  80. 80.病理切片分析
  81. 81.图说任务及其传统处理方法
  82. 82.深度语言模型与RNN
  83. 83.LSTM及其变种
  84. 84.NIC模型
  85. 85.SAT模型
  86. 86.图说的性能指标
  87. 87.判别模型与生成模型
  88. 88.KL散度与JS散度
  89. 89.VAE
  90. 90.GAN的基本原理
  91. 91.DCGAN与CGAN
  92. 92.Wasserstein GAN
  93. 93.SRGAN

LSTM及其变种

小象实战讲义 · 基于深度学习的计算机视觉

前面我们已经进入图像描述任务:它通常由编码器(CNN)和解码器(语言模型)组成,解码器的核心任务是根据图像特征,生成连贯、准确的文字序列。为了处理这种序列数据,我们需要能够“记住”历史信息并理解上下文依赖关系的模型。本节我们将深入探讨循环神经网络(RNN)家族中一个至关重要的成员——长短时记忆网络(LSTM),它通过精巧的“门控”机制,有效缓解了传统RNN在长序列建模中的核心痛点,并曾在图像描述等序列生成任务中扮演关键角色。

💡 核心导读

  • 从RNN的困境到LSTM的诞生:理解传统RNN为何难以捕捉长期依赖关系,以及LSTM提出的背景与核心设计思想。
  • LSTM的“门控”机制:深入剖析遗忘门、输入门和输出门的工作原理,以及它们如何协同工作来更新和传递细胞状态。
  • LSTM的数学模型与结构图:通过清晰的公式和结构分解图,掌握LSTM单元内部的计算流程。
  • LSTM的变种模型:了解Peephole、耦合门以及更精简的GRU(门控循环单元)等LSTM的改进版本及其适用场景。

传统RNN的长期依赖问题

在图像描述任务中,我们真正用到的解码器是一种深度语言模型。其中,循环神经网络(RNN)是处理序列数据的自然选择。一个典型的RNN(有时被称为Vanilla RNN或普通RNN)结构简单,其隐藏层状态会随时间步自我反馈,从而理论上能够利用之前的信息。

然而,在实际运算中,典型的RNN存在一个严重缺陷:它难以捕获很长时间之前的依赖关系。由于梯度在时间维度上通过链式法则反向传播时,会经历连续的乘法运算,这极易导致梯度消失(Gradient Vanishing)或梯度爆炸(Gradient Exploding)问题。梯度消失使得网络参数无法根据远距离的误差进行有效更新,因此RNN实际上只能捕捉离当前时刻比较近的依赖关系。

但在许多应用场景,包括语言生成中,模型常常需要捕捉长时间的依赖关系。例如,生成句子时,当前要输出的词可能取决于句子开头很早之前出现的某个主语或关键词。在这种情况下,基本的RNN就显得力不从心了。

LSTM:长短时记忆模型

为了解决长期依赖问题,学者们提出了长短时记忆模型。它由Sepp Hochreiter和Jürgen Schmidhuber于1997年提出。有趣的是,由于当时的硬件计算能力根本达不到要求,这个划时代的模型在提出后十多年,直到深度学习复兴,才逐渐流行起来,并一度成为处理语言模型和序列模型的标配。

LSTM最关键的特点就是能有效捕捉长期记忆。它的核心设计是一个贯穿时间的细胞状态,以及三个调控信息流的“门”。

理解“门”的概念

在LSTM中,“门”是一种让信息选择性通过的结构。它通常由一个Sigmoid激活函数层和一个逐元素乘法操作实现。

  • Sigmoid函数的输出范围在0到1之间。
  • 这个特性很像一个开关:当输出为1时,表示“门”完全打开,所有信息都通过;当输出为0时,表示“门”完全关闭,所有信息都被拦住;输出在0和1之间时,则表示部分信息通过。

把门放到整条时间链上看会更直观:某个时刻输入的信息,如果当前输出用不到,闸门就保持关闭,细胞状态把它原样带着往下传;等到后面某个时刻真正需要它时,再打开闸门放出来。这正是LSTM能把早期信息保留到很久之后的原因。

这三个门分别是:

  1. 遗忘门:决定从细胞状态中丢弃哪些旧信息。
  2. 输入门:决定有多少新信息会被存入细胞状态。
  3. 输出门:基于当前的细胞状态,决定输出什么。

LSTM单元结构详解

我们先通过一张简化清晰的LSTM结构图来建立整体概念,再逐一拆解每个部分。

LSTM单元在时间步t的结构示意图 (简化版)

输入: [h_{t-1}, x_t] (拼接)
        |
        |-----------------------|
        |                       |
        v                       v
    遗忘门(f_t)             输入门(i_t)
     σ(W_f·[h_{t-1}, x_t] + b_f)  σ(W_i·[h_{t-1}, x_t] + b_i)
        |                       |
        |                       |
        v                       v
细胞状态(C_{t-1}) ---(逐元素乘)---> f_t * C_{t-1}
                                    |
                                    |  候选记忆(C~_t)
                                    |   tanh(W_C·[h_{t-1}, x_t] + b_C)
                                    |       |
                                    v       v
新的细胞状态(C_t) <---(逐元素加)--- i_t * C~_t + f_t * C_{t-1}
        |
        |-----------------------|
        |                       |
        v                       v
    输出门(o_t)             细胞状态压缩
     σ(W_o·[h_{t-1}, x_t] + b_o)   tanh(C_t)
        |                       |
        |                       |
        v                       v
隐藏状态输出(h_t) <---(逐元素乘)--- o_t * tanh(C_t)
        |
        v
当前时刻输出(y_t, 例如经过Softmax)

关键点说明:

  • 所有门的输入都是一样的:上一个时刻的隐藏状态 h_{t-1} 和 当前时刻的输入 x_t 的拼接向量 [h_{t-1}, x_t]。
  • 区别不同门和候选记忆的,是它们各自独有的权重矩阵 W 和偏置 b。
  • 水平穿过顶部的线代表细胞状态 C_t,它像一条传送带,在整个链路上运行,只发生线性交互,信息可以很容易地保持不变地流过。这是LSTM能保持长期记忆的关键。
  • 隐藏状态 h_t 是细胞状态 C_t 经过过滤(输出门)后的“曝光”版本,它会传递给下一个时间步,也用于计算当前输出。

下面我们分步拆解图中的计算。

步骤一:遗忘门

遗忘门决定我们需要从细胞状态中丢弃多少旧信息。它查看 h_{t-1} 和 x_t,并为细胞状态 C_{t-1} 中的每个元素输出一个0到1之间的数。1表示“完全保留”,0表示“完全丢弃”。

ft=σ(Wf⋅[ht−1,xt]+bf)f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)

其中,σ 是Sigmoid函数,W_f 和 b_f 是遗忘门特有的权重和偏置。

步骤二:输入门与候选记忆

这一步决定我们要往细胞状态里写入多少新信息。它包含两部分:

  1. 输入门:一个Sigmoid层,决定细胞状态的每个位置要写入多少新信息。注意它给出的是写入比例,而不是写入的内容本身。 it=σ(Wi⋅[ht−1,xt]+bi)i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)
  2. 候选记忆:一个tanh层,创建一个新的候选值向量 \tilde{C}_t,这些值可能会被加入到细胞状态中。tanh的输出范围在-1到1之间,适合作为要存储的内容。 C~t=tanh⁡(WC⋅[ht−1,xt]+bC)\tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C)

步骤三:更新细胞状态

现在我们可以将旧的细胞状态 C_{t-1} 更新为新的细胞状态 C_t。更新规则是:先让旧状态乘以 f_t,忘掉我们决定要忘记的部分;然后加上 i_t * \tilde{C}_t,即我们决定要添加的新信息的部分。

Ct=ft∗Ct−1+it∗C~tC_t = f_t * C_{t-1} + i_t * \tilde{C}_t

这里的 * 和 + 都是逐元素操作。这个公式是LSTM的核心,它以一种相对线性、受控的方式实现了信息的长期保留与更新。另外注意,三个门和候选记忆各自有一套独立的 W 和 b,这也是LSTM单元参数量偏大的原因。

步骤四:输出门与隐藏状态

最终,我们需要基于细胞状态来决定输出什么。这个输出将是我们的隐藏状态 h_t。

  1. 输出门:首先,运行一个Sigmoid层来决定细胞状态的每个位置有多少比例被输出。 ot=σ(Wo⋅[ht−1,xt]+bo)o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)
  2. 细胞状态过滤:然后,将细胞状态 C_t 通过tanh函数(将其值缩放到-1和1之间),再与输出门 o_t 相乘。这样,我们就只输出了我们决定输出的那部分。 ht=ot∗tanh⁡(Ct)h_t = o_t * \tanh(C_t)

这个 h_t 一方面作为当前LSTM单元的输出(可用于预测下一个词),另一方面会作为下一个时间步的输入之一(h_{t-1})。细胞状态 C_t 也会被完整地传递到下一个时间步。

把上面四步写成代码,一个时间步的前向计算如下(四个部件各用一套随机初始化的小权重,仅为演示计算流程):

import numpy as np

np.set_printoptions(precision=4, suppress=True)
np.random.seed(42)

d_x, d_h = 2, 3
h_prev = np.array([0.10, 0.30, -0.40])
x_t = np.array([0.50, -0.20])
c_prev = np.array([0.20, -0.10, 0.05])

def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-z))

W = {name: np.random.randn(d_h, d_h + d_x) * 0.5 for name in ("f", "i", "o", "c")}
b = {name: np.zeros(d_h) for name in ("f", "i", "o", "c")}

xh = np.concatenate([h_prev, x_t])

f_t = sigmoid(W["f"] @ xh + b["f"])
i_t = sigmoid(W["i"] @ xh + b["i"])
o_t = sigmoid(W["o"] @ xh + b["o"])
c_candidate = np.tanh(W["c"] @ xh + b["c"])

c_t = f_t * c_prev + i_t * c_candidate
h_t = o_t * np.tanh(c_t)

print("f_t =", f_t)
print("i_t =", i_t)
print("o_t =", o_t)
print("C~_t =", c_candidate)
print("C_t =", c_t)
print("h_t =", h_t)
print("单个单元参数量 =", sum(w.size for w in W.values()) + sum(v.size for v in b.values()))
f_t = [0.5692 0.475  0.39  ]
i_t = [0.4186 0.4315 0.4102]
o_t = [0.476  0.5026 0.5395]
C~_t = [-0.0543  0.1427 -0.0322]
C_t = [0.0911 0.0141 0.0063]
h_t = [0.0433 0.0071 0.0034]
单个单元参数量 = 72

LSTM的变种

标准的LSTM结构非常成功,但研究者们也提出了多种变体以寻求更好的性能或更高的效率。

1. Peephole连接

在标准LSTM中,门的控制仅依赖于 h_{t-1} 和 x_t。Peephole变体让门也能“窥视”细胞状态本身。具体来说,在计算遗忘门 f_t 和输入门 i_t 时,额外读取尚未更新的 C_{t-1};在计算输出门 o_t 时,则读取已经更新后的 C_t。

  • 公式变为(以遗忘门为例): f_t = σ(W_f·[h_{t-1}, x_t] + P_f ⊙ C_{t-1} + b_f)
  • 其中 P_f 是一组作用于细胞状态的对角权重(逐元素相乘 ⊙),而不是把 C_{t-1} 简单拼进输入向量。
  • 这使得门的决策能更直接地感知细胞状态的内容。

2. 耦合的遗忘门与输入门

一些研究发现,遗忘门和输入门是紧密相关的:当我们决定要记住新信息时,可能也需要忘记一些旧信息来腾出空间。因此,一个简单的改动是让这两个门耦合起来,即强制遗忘门和输入门的和为1。这样,我们只需计算遗忘门 f_t,然后令输入门 i_t = 1 - f_t。这减少了参数数量,并在某些任务上表现相似。

3. GRU:门控循环单元

GRU是Cho等人在2014年提出的一种更著名的LSTM变体。它做出了两项主要简化:

  1. 合并了遗忘门和输入门:GRU使用一个单一的更新门 z_t 来同时控制遗忘和输入的程度。
  2. 合并了细胞状态和隐藏状态:GRU没有单独的细胞状态 C_t,隐藏状态 h_t 直接承担了信息传递的责任。

GRU只有两个门:

  • 重置门 r_t:决定如何将新的输入与之前的记忆结合。
  • 更新门 z_t:决定新旧状态在最终隐藏状态中的混合比例。

其核心公式如下: zt=σ(Wz⋅[ht−1,xt])rt=σ(Wr⋅[ht−1,xt])h~t=tanh⁡(W⋅[rt∗ht−1,xt])ht=(1−zt)∗ht−1+zt∗h~t\begin{aligned} z_t &= \sigma(W_z \cdot [h_{t-1}, x_t]) \\ r_t &= \sigma(W_r \cdot [h_{t-1}, x_t]) \\ \tilde{h}_t &= \tanh(W \cdot [r_t * h_{t-1}, x_t]) \\ h_t &= (1 - z_t) * h_{t-1} + z_t * \tilde{h}_t \end{aligned} (为简洁,公式中省略了偏置项 b,实际实现中通常包含)

GRU的工作流程:

  1. 更新门 z_t 决定候选状态 \tilde{h}_t 占多大比重,相应地 1 - z_t 决定旧状态 h_{t-1} 保留多少。
  2. 重置门 r_t 控制旧状态 h_{t-1} 有多少信息被用于计算候选状态 \tilde{h}_t。如果 r_t 接近0,则意味着“重置”或忽略之前的隐藏状态,只依赖当前输入 x_t。
  3. 候选状态 \tilde{h}_t 结合了经重置门过滤后的旧状态和当前输入。
  4. 最终隐藏状态 h_t 是旧状态 h_{t-1} 和候选状态 \tilde{h}_t 的加权和,权重由更新门 z_t 控制。

注:不同框架对更新门的写法相反——PyTorch 写作 ht=(1−zt)nt+ztht−1h_t = (1-z_t)n_t + z_t h_{t-1}(z→1z\to1 时保留旧状态);本节采用 Cho et al. (2014) 的原始约定(z→1z\to1 时采用候选状态)。两者只是把 zz 与 1−z1-z 互换,功能等价。

LSTM与GRU的比较

  • LSTM:模型相对复杂,参数较多,拟合能力很强。适合于大规模、复杂度高的序列数据。
  • GRU:模型结构更精简,参数更少,训练速度通常更快,拟合能力相对较弱。适合于规模较小、复杂度不高的数据。

实际选型应结合数据规模与任务复杂度,用验证集结果说话。

📝 动手练一练

  1. 概念辨析:请简述LSTM中“遗忘门”和“输入门”在更新细胞状态 C_t 时分别扮演什么角色?如果我们将遗忘门 f_t 的输出全部设置为1,输入门 i_t 的输出全部设置为0,细胞状态 C_t 会如何变化?
  2. 公式理解:假设在某时间步,遗忘门 f_t = 0.8,输入门 i_t = 0.5,旧的细胞状态 C_{t-1} = [1.0, -0.5],候选记忆 \tilde{C}_t = [0.2, 0.6]。请手动计算新的细胞状态 C_t。
👉 点击查看参考答案
  1. 角色简述:

    • 遗忘门 f_t:控制上一个细胞状态 C_{t-1} 中有多少信息被保留到当前细胞状态 C_t 中。f_t 中的每个值(0~1)与 C_{t-1} 中对应位置的值相乘,决定该位置信息的保留比例。
    • 输入门 i_t:控制当前候选记忆 \tilde{C}_t 中有多少新信息被添加到当前细胞状态 C_t 中。i_t 中的每个值与 \tilde{C}_t 中对应位置的值相乘,决定该位置新信息的添加比例。
    • 极端情况:如果 f_t 全为1,i_t 全为0,则根据公式 C_t = 1 * C_{t-1} + 0 * \tilde{C}_t = C_{t-1}。这意味着细胞状态完全保持不变,既没有忘记任何旧信息,也没有添加任何新信息。
  2. 手动计算: 根据公式 C_t = f_t * C_{t-1} + i_t * \tilde{C}_t,进行逐元素计算:

    • 第一个元素:0.8 * 1.0 + 0.5 * 0.2 = 0.8 + 0.1 = 0.9
    • 第二个元素:0.8 * (-0.5) + 0.5 * 0.6 = -0.4 + 0.3 = -0.1 因此,新的细胞状态 C_t = [0.9, -0.1]。

本章小结

本节我们深入学习了LSTM及其变种,它们曾是构建强大序列模型(如图像描述解码器)的重要基石。我们理解了传统RNN在处理长序列时的局限性,并掌握了LSTM如何通过遗忘门、输入门、输出门和细胞状态这一精巧设计,实现了对长期依赖关系的有效建模。我们还了解了包括GRU在内的几种重要变体,它们在不同场景下提供了效率与性能的平衡。

需要说明的是,LSTM(1997年提出)在2014年前后成为序列建模的主流方案,GRU(2014年提出)随后被广泛采用;自2017年Transformer提出后,序列建模的主流已转向自注意力机制,图像描述的解码器也普遍改用Transformer。今天再学LSTM,价值在于理解门控与长程依赖的经典解法,以及在数据量小、算力受限或需要流式逐帧处理时仍可作为轻量选择。

📋 行动清单

  • 理清信息流:在白纸上画出LSTM单元的结构图,并标注出 C_{t-1}, h_{t-1}, x_t, f_t, i_t, \tilde{C}_t, C_t, o_t, h_t 的数据流向。
  • 对比记忆:总结LSTM与GRU在门控机制和状态管理上的主要区别,思考它们各自的优缺点。
  • 联系应用:回顾图像描述任务,设想LSTM作为解码器时,其输入 x_t 和隐藏状态 h_t 在生成描述句子过程中的具体含义。

—— 小象教研组