查看全课大纲(第 79 / 82 节)
  1. 1.函数与数据泛化
  2. 2.向量:从几何直觉到高维空间
  3. 3.矩阵与张量
  4. 4.特征值与特征向量:矩阵变换的不动方向
  5. 5.凸函数:从向量求导的矩阵记号到好优化的函数形状
  6. 6.概率与统计:机器学习的数学基础
  7. 7.独立性与随机变量:贝叶斯公式与二项分布
  8. 8.期望、方差与协方差:随机变量的数字画像
  9. 9.联合概率:当一条轴不够用的时候
  10. 10.概率分布(一):从伯努利到伽马函数
  11. 11.概率分布(二):贝塔、泊松、正态与两个贝叶斯故事
  12. 12.机器学习的模型:找输入与输出的关系
  13. 13.机器学习的应用
  14. 14.过拟合
  15. 15.机器学习的分类
  16. 16.有监督学习:从房价预测说起
  17. 17.损失函数
  18. 18.梯度下降(上):更新规则与学习速率
  19. 19.梯度下降(2)
  20. 20.牛顿方法
  21. 21.KNN简介
  22. 22.距离测算
  23. 23.朴素贝叶斯
  24. 24.朴素贝叶斯的实际应用
  25. 25.信息熵:决策树构建的度量基础
  26. 26.信息增益与决策树的构建
  27. 27.ID3算法
  28. 28.决策树节点的划分
  29. 29.CART树
  30. 30.Bagging
  31. 31.Boosting:自适应提升算法
  32. 32.Gradient Boosting
  33. 33.线性模型:回归与分类的同一副骨架
  34. 34.最小二乘:把最优参数直接解出来
  35. 35.最小二乘法的几何与概率视角
  36. 36.极大似然:给最小二乘一个概率出身
  37. 37.逻辑回归(1):把线性模型接到分类上
  38. 38.逻辑回归(2):损失函数与多分类
  39. 39.---- 手造数据: 5 个样本, 2 个特征; x2 几乎 = 2*x1 (偏差手工指定, 无随机) ----
  40. 40.讲师例: 向量 "23014" -- 三把范数尺子量同一个向量
  41. 41.优化方法(1):坐标下降与前向选择
  42. 42.优化方法(2):前向策略与最小角回归
  43. 43.拉格朗日乘子法(1):从无约束到等式约束
  44. 44.拉格朗日乘子法(2):等式约束下的平行条件与拉格朗日函数
  45. 45.拉格朗日乘子法(3):不等式约束与 KKT 条件
  46. 46.最大间隔:把分类问题变成优化问题
  47. 47.拉格朗日对偶问题:把 w 和 b 全部换成 α
  48. 48.支持向量:边界上的少数派
  49. 49.SVM 计算过程:一笔到底算完,再走进核方法
  50. 50.贝叶斯公式:概率即信念
  51. 51.隐含变量和概率模型:给看不见的原因建模
  52. 52.EM 算法:期望与最大化的迭代
  53. 53.琴生不等式:EM 的数学钥匙
  54. 54.三个硬币问题:EM 的复盘与衔接
  55. 55.高斯混合模型:用隐变量解释双峰数据
  56. 56.隐马尔可夫模型
  57. 57.隐马尔可夫模型(2):前向算法与动态混合模型
  58. 58.隐马尔可夫模型在 NLP 中的运用
  59. 59.EM 算法:把「为什么收敛」推到底
  60. 60.HMM 在 NLP 中的运用:从分词到机器翻译
  61. 61.HMM 参数估计:Baum-Welch 的三块拉格朗日
  62. 62.高维下的高斯模型
  63. 63.主题模型:从词袋到隐含主题
  64. 64.Dirichlet 分布(1)
  65. 65.Dirichlet 分布(2)
  66. 66.统计学习内容回顾
  67. 67.K-means 算法
  68. 68.PCA 降维
  69. 69.SVD 降维
  70. 70.从聚类到流形
  71. 71.局部线性嵌入
  72. 72.随机邻域嵌入:SNE 与 t-SNE
  73. 73.无参模型:让数据自己说话
  74. 74.高斯过程:函数空间里的正态分布
  75. 75.高斯过程(2):噪声、正则化与核的线性组合
  76. 76.神经网络简介:从感知器到自编码器
  77. 77.反向传播:把误差一层层传回去
  78. 78.卷积神经网络(CNN):让特征自己学出来
  79. 79.循环神经网络(RNN):给网络装上记忆
  80. 80.深度学习回顾:静态一类,动态一类
  81. 81.增强学习(1):蒙特卡洛、树搜索与 Q-learning
  82. 82.强化学习(2):从乌鸦吃核桃到井字棋的 value 回传

循环神经网络(RNN):给网络装上记忆

约 36 分钟

📺 正在播放小象官方高清录播(支持倍速与清晰度调节)

小象实战讲义 · 机器学习算法精讲

CNN 之后,我们面对一类新数据:动态的、持续的特性——这是循环神经网络(RNN,recurrent neural networks)与 CNN 最大的区别所在。本节路线:先把一个普通网络放到时间轴上,看清「隐层记住上一时刻」这一个改动;再对照 HMM——讲师的判词是「HMM 可以认为是 RNN 的一种特例」;然后进入本节主角 LSTM:细胞状态这条传送带,加上遗忘、输入、输出三道门,把「信息记多久」变成可学的参数;最后从图像那边的 pix2pix 桥接到序列这边的 Seq2Seq,看注意力机制怎么让每一步输出直接看到输入,以及这套东西生成出来的诗、假评论和嘻哈歌词。

💡 核心导读

  • 时序特征:语言、声音、股价都有强烈的前后依赖——说了「我爱北京」,后面就不会是鼠标、茶叶或垃圾桶;明天的股价也不能从 5 毛到 300 块之间随便估一个数。
  • RNN 核心机制:同一个网络在三个时间段做同一件事,但 tt 时刻隐层的输入里,除了当前的 xtx_t,还有上一时刻的隐层输出;课件公式 St=tanh⁡(Uxt+WSt−1)S_t = \tanh(U x_t + W S_{t-1})。
  • 与 HMM 的关系:一个用概率的状态转移矩阵描述时序,一个用隐层之间的权重关联描述时序——后者 capacity 更大、更 flexible。
  • LSTM:细胞状态 CC 像传送带,三道门(forget / input / output)决定信息留多少、写多少、放多少;「不是你来知道重要不重要,是训练数据告诉你的」。
  • 从 pix2pix 到 Seq2Seq:图像的 autoencoder 是 pixels to pixels,序列的 autoencoder 就是 sequence to sequence;注意力机制再给每一步输出配上对输入的权重。
  • 生成的诚实面:华丽但没有思想的诗、以假乱真的淘宝评论、押韵顺口的嘻哈歌词——规律学得到,中心思想没有。

一类新数据:时序特征

RNN 里的「循环」,跟前面讲 deep learning 时 CNN 的做法有蛮大的区别——它主要处理动态的、持续的特性,这是 CNN 有的时候做不到的。

比如时间序列、声音,包括我们的语言,都有明显的时序特征。讲师的例子:我说「我爱北京天安门」——说了「我爱北京」之后,后边就不会说「我爱北京鼠标」「我爱北京茶叶」或者「我爱北京垃圾桶」这种话。前面这些话一出口,后面出现这个词的概率就发生了变化。再比如预测明天某一只股票的价格——你认为它跟以往的价格会没有关系吗?能从大概 5 毛钱到 300 块钱之间随便估一个数吗?当然不是,它一定跟以前的价格有密切的关系。

这种持续信息,我们以前用 HMM(隐马尔可夫模型)处理过;现在看 RNN 怎么描述它。

把一个网络放到时间轴上

讲师在黑板上一步步画。先想象一个最简单的普通神经网络:有输出层,有一个隐层跟它相连,隐层再和输入节点相连——两层网络。因为处理的是时序信息,时刻必须标在上面:T−1T-1 时刻输入 X1,X2X_1, X_2,输出 YT−1Y_{T-1};接着画第二个网络,跟第一个长得一模一样,权重也相同(该是 ww 的还是 ww,该是 uu 的还是 uu),处理 TT 时刻的输入;T+1T+1 时刻同理。三个网络完全一样——就是一个网络在三个时间段所做的同一件事。

那时间上的关联体现在哪?关键一步:tt 时刻隐层的输入,除了输入层经权重送来的当前信号,还跟上一时刻隐层的输出相关——把上一时刻的隐层输出接到这一时刻的输入上;这一时刻的输出,又变为下一时刻的输入。

按板书的记号走一遍:某一时刻的隐层有两组输入——一组是 WW 乘上当前的 xx(设结果叫 zz);另一组是上一时刻的 zz 乘上一个新的矩阵 HH。两组信息合在一起,构成这一时刻隐藏的输入;再经过输出权重 uu,得到输出 yy。所以每两个时间节点之间,我们手里有三组系数。

换成课件上的简记(把隐藏状态记作 SS,输出记作 OO),循环神经网络的基本形式:

St=tanh⁡(Uxt+WSt−1),Ot=softmax(VSt)S_t = \tanh(U x_t + W S_{t-1}), \qquad O_t = \mathrm{softmax}(V S_t)

三个矩阵各管一段:UU 是输入到隐层、WW 是隐层之间、VV 是隐层到输出。课件上的原话很形象:一个 RNN 可以看作同一个网络的多份拷贝,每一份给后一份传递消息——把循环按时间展开,看到的就是这条链。用代码把「记忆」跑出来看:

import math

def rnn_forward(x_seq, U, W):
    """朴素 RNN 前向(标量版): S_t = tanh(U*x_t + W*S_{t-1}), 只看隐藏状态。
    U: 输入到隐层的权重; W: 隐层到自身的循环权重(记忆); S_0 初始为 0。"""
    s_prev = 0.0
    states = []
    for t, x_t in enumerate(x_seq):
        z = U * x_t + W * s_prev
        z = max(min(z, 500), -500)  # 数值稳健限幅, 防极端溢出
        s_t = math.tanh(z)
        states.append((t, x_t, s_t))
        s_prev = s_t  # 当前状态传给下一时间步
    return states

x_seq = [1.0, 0.5, 0.0, -0.5]

for label, W in [("W=0   (没有循环, 普通网络: 只看当前输入)", 0.0),
                 ("W=-0.2(有记忆的 RNN: 当前输入 + 过去状态)", -0.2)]:
    print(label)
    for t, x_t, s_t in rnn_forward(x_seq, U=0.5, W=W):
        print(f"  t={t}: 输入 x={x_t:+.1f}  隐藏状态 S={s_t:+.4f}")
    print()
W=0   (没有循环, 普通网络: 只看当前输入)
  t=0: 输入 x=+1.0  隐藏状态 S=+0.4621
  t=1: 输入 x=+0.5  隐藏状态 S=+0.2449
  t=2: 输入 x=+0.0  隐藏状态 S=+0.0000
  t=3: 输入 x=-0.5  隐藏状态 S=-0.2449

W=-0.2(有记忆的 RNN: 当前输入 + 过去状态)
  t=0: 输入 x=+1.0  隐藏状态 S=+0.4621
  t=1: 输入 x=+0.5  隐藏状态 S=+0.1563
  t=2: 输入 x=+0.0  隐藏状态 S=-0.0312
  t=3: 输入 x=-0.5  隐藏状态 S=-0.2390

对照着看:t=2t=2 那一行,输入是 00——没有记忆的网络,隐藏状态直接归零(它只能看到当前这一刻);有记忆的 RNN 还带着 −0.0312-0.0312,这是上一时刻的余温。再到 t=3t=3,两边的输入相同,但 RNN 的 SS 是拿上一步的 −0.0312-0.0312 又乘了一次循环权重之后算出来的。当前时刻的隐藏状态,同时包含当前输入和过去信息的影响——这就是 RNN 捕捉时序依赖的基本方式。

RNN 与 HMM:同一个时序问题的两种写法

讲师让大家想象以前学过的隐马尔可夫链,马上能找到相通的东西。HMM 里有隐含的状态序列 St−1,St,St+1…S_{t-1}, S_t, S_{t+1} \dots,我们观察不到;能看到的是它们输出的观测序列 Yt−1,Yt,Yt+1…Y_{t-1}, Y_t, Y_{t+1} \dots(阴影部分)。YY 和状态之间靠释放矩阵 P(Y∣S)P(Y \mid S) 连接;状态与状态之间靠状态转移矩阵 P(St∣St−1)P(S_t \mid S_{t-1}) 演进。

跟 RNN 摆在一起看:RNN 的隐藏状态对应 HMM 的隐含状态;RNN 的输出对应可观察的 YY;隐层之间由循环权重定义的转换,对应状态转移矩阵;隐藏到输出的映射,对应释放矩阵。讲师的结论:「我们至少可以认为,HMM 可以简单地看作 RNN 的一种特例」——如果 RNN 里的一个神经网络用来逼近那个概率的状态转移矩阵,那它就是一个普通的 HMM。而且 RNN 在每一时刻还有单独的 XX 输入,加上隐藏关系,最后变成实际能观察到的输出。

两者最主要的相关,是因为它们都是时序模型:一个用概率的状态转移矩阵描述,一个用隐层之间的权重关联描述;都有隐含状态、都有可被观察的状态。但 RNN 的模型 capacity(复杂度)更大、更 flexible——不要求严格的概率解释,能 model 的事情更多一些。

顺带一提结构的两种早期分岔。Elman network:把这一时刻的隐藏信息放到下一时刻的隐藏,就是前面画的连法。Jordan network:唯一的区别是,把这一时刻的隐藏信息不保留、算出输出,把输出作为下一时刻的输入放到下一时刻的隐藏里——中间其实就多乘了一个输出矩阵的区别。讲师提到他们曾经做过对比工作:对一个简单网络来讲,两者的区别非常小,大家也并没有特别关注该用哪种,表现是类似的。

LSTM:把「记多久」变成可学的参数

反而是有一种 RNN,是大家关注最多、讨论最多、经常使用的——讲师说,今天说用一个 RNN 的时候,基本用的都是 LSTM(long short-term memory)。

长期依赖抓不住

一个特别有意思的例子(讲师注明「这是别人的例子」):一句话 “I grew up in France”,中间隔了好长一段,最后一句是 “I speak fluent French”。最后的 French 跟前面的 France 相不相关?怎么能不相关。可如果只用 trigram 模型看 “speak fluent”,多半只能断定这是一种 language——could be Chinese、German、French、Dutch 都行;但给定前面那句,French 的概率要远远大于其他。这个 dependency,普通的 RNN 没办法 capture 到——而语料库里大量存在这种长期的、常关联的现象。

这里有一对值得单独记住的判断:short term dependency 告诉你,它一定是个语言(不会是杯子、鼠标、电脑或类似的词);long term dependency 告诉你,它是某一种语言。所以需要把 RNN 加以改进、使自由度增大——有长期的关联,考虑长期;有短期的关联,考虑短期——看谁的关联比较强,通过参数自动地学习到。这就是 LSTM 所做的事。

细胞状态:一条传送带

LSTM 是 97 年由德国人做出来的结果(Hochreiter 与 Schmidhuber,1997)。它整体还是一个 RNN,只是分解成了几个更细的子模块,子模块合在一起,使模型有更大的复杂度。

它新增的核心 idea 是 CC——细胞状态。CC 代表信息流,像一条传送带:它告诉你,前面的信息以多大的程度要往后保留。上一时刻来的 Ct−1C_{t-1} 输入到当前节点,这一时刻通过它的权重和训练数据调整出「保留多少」,作为下一时刻的输入。如果当前信息跟后面关联不大,就用这一时刻的信息顶替上一时刻的;如果关联大——比如 France 这件事,跟后面 French 关联会比较大——那这个信息就在传送带上得以保留。

那你哪知道这个重要不重要?讲师的原话是「其实不是你来知道的,是训练数据告诉你的」。大量的训练数据会告诉你:碰到这种类型的信息,它对后面有影响——它们之间的关系不独立,有关系的权重就会加大;其他的词对后面不会影响,权重自然变小。模型只提供了这种可能和 capacity,让你去调整这些系数,这些系数的调整会使长期依赖的现象在模型里得到体现——以前的 RNN 做不到。

三道门,逐个看

每一个 LSTM 单元里有一个 forget 门、一个 input 门、一个 output 门。共同点先记住:每个门的输入,仍然是上一时刻的隐藏和这一时刻的输入——跟所有 RNN 一样,只是各自乘上自己的权重、加上偏置,再过激活函数。

  1. 遗忘门 ftf_t:决定以多大程度 forget(get rid of)旧信息。过 sigmoid,输出在 0 到 1 之间——像概率一样可以解释:1 是「完全保留」,0 是「完全丢弃」。
  2. 输入门 iti_t 与候选值 C~t\tilde{C}_t:同样输入构造出 0 到 1 的 iti_t;同时经 tanh 造一个新的候选值 C~t\tilde{C}_t(讲师:那个波浪线「我不知道怎么读,比如叫 c t bar」),作为这一时刻要写入的内容。
  3. 细胞状态更新:Ct=ft⊙Ct−1+it⊙C~tC_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t(⊙\odot 是逐元素相乘)。讲师给了两个极端读法——若 ftf_t 为 0,CtC_t 全是这一时刻更新出来的,跟前面没有任何关系;若 iti_t 为 0,这一时刻的信息没被写入,CtC_t 全部来源于上一时刻。
  4. 输出门 oto_t:由 oo 权重控制;最后隐藏的结果就是 ht=ot⊙tanh⁡(Ct)h_t = o_t \odot \tanh(C_t)。

把一个标量版的 LSTM 单元完整算一遍(向量运算里门输入是 ht−1h_{t-1} 与 xtx_t 的拼接,此处以相加简化):

import math

def sigmoid(x):
    x = max(min(x, 500), -500)  # 防 exp 溢出
    return 1.0 / (1.0 + math.exp(-x))

def lstm_cell_scalar(h_prev, C_prev, x_t, W_f, W_i, W_C, W_o, b_f, b_i, b_C, b_o):
    """标量版 LSTM 单元前向(仅为演示机制)。"""
    gate_in = h_prev + x_t                       # 门输入 = 上一时刻隐藏 + 当前输入
    f_t = sigmoid(W_f * gate_in + b_f)           # 遗忘门: 0~1
    i_t = sigmoid(W_i * gate_in + b_i)           # 输入门: 0~1
    C_tilde = math.tanh(W_C * gate_in + b_C)    # 候选值: -1~1
    o_t = sigmoid(W_o * gate_in + b_o)           # 输出门: 0~1
    C_t = f_t * C_prev + i_t * C_tilde           # 细胞状态: 传送带上的新旧混合
    h_t = o_t * math.tanh(C_t)                   # 隐藏输出
    return f_t, i_t, C_tilde, o_t, C_t, h_t

W_f, b_f = 0.2, 1.5     # 遗忘门: 偏保留
W_i, b_i = 0.4, 0.05    # 输入门: 看当前输入行事
W_C, b_C = 0.8, 0.1     # 候选值
W_o, b_o = 0.9, 0.1     # 输出门

h_prev, C_prev, x_t = 0.3, 0.5, 0.7
f_t, i_t, C_tilde, o_t, C_t, h_t = lstm_cell_scalar(
    h_prev, C_prev, x_t, W_f, W_i, W_C, W_o, b_f, b_i, b_C, b_o)

print("标量 LSTM 单元单步计算:")
print(f"  门输入 = h_prev + x_t = {h_prev} + {x_t} = {h_prev + x_t:.1f}")
print(f"  遗忘门 f_t  = sigmoid({W_f}*1.0 + {b_f}) = {f_t:.4f}")
print(f"  输入门 i_t  = sigmoid({W_i}*1.0 + {b_i}) = {i_t:.4f}")
print(f"  候选值 C~_t = tanh({W_C}*1.0 + {b_C}) = {C_tilde:.4f}")
print(f"  输出门 o_t  = sigmoid({W_o}*1.0 + {b_o}) = {o_t:.4f}")
print(f"  细胞状态 C_t = {f_t:.4f}*{C_prev} + {i_t:.4f}*{C_tilde:.4f} = {C_t:.4f}")
print(f"  隐藏输出 h_t = {o_t:.4f} * tanh({C_t:.4f}) = {h_t:.4f}")
标量 LSTM 单元单步计算:
  门输入 = h_prev + x_t = 0.3 + 0.7 = 1.0
  遗忘门 f_t  = sigmoid(0.2*1.0 + 1.5) = 0.8455
  输入门 i_t  = sigmoid(0.4*1.0 + 0.05) = 0.6106
  候选值 C~_t = tanh(0.8*1.0 + 0.1) = 0.7163
  输出门 o_t  = sigmoid(0.9*1.0 + 0.1) = 0.7311
  细胞状态 C_t = 0.8455*0.5 + 0.6106*0.7163 = 0.8602
  隐藏输出 h_t = 0.7311 * tanh(0.8602) = 0.5091

这一组手设的小参数下:遗忘门开到 0.85(偏保留),旧状态贡献 0.8455×0.50.8455 \times 0.5;输入门 0.61,候选值贡献 0.6106×0.71630.6106 \times 0.7163——下一时刻的 CC,就是由上一时刻和这一时刻按这两个比例组成的。再把「France 场景」压到更新式上看极端情形:

def update_C(f, i, c_prev, c_tilde):
    """LSTM 细胞状态更新: C_t = f*C_prev + i*C_tilde (标量演示)。"""
    return f * c_prev + i * c_tilde

c_prev, c_tilde = 0.8, 0.05   # 细胞里已存着重要信息(比如"France"), 新来的词不重要

cases = [
    ("f=1.0, i=0.0: 遗忘门全开 + 输入门全关 -> 旧信息原样保留", 1.0, 0.0),
    ("f=0.0, i=1.0: 遗忘门全关 + 输入门全开 -> 完全换成这一时刻", 0.0, 1.0),
    ("f=0.7, i=0.3: 按比例混合(七分旧、三分新)", 0.7, 0.3),
]
for name, f, i in cases:
    c_t = update_C(f, i, c_prev, c_tilde)
    print(f"{name}")
    print(f"   C_t = {f}*{c_prev} + {i}*{c_tilde} = {c_t:.4f}")
    print()
f=1.0, i=0.0: 遗忘门全开 + 输入门全关 -> 旧信息原样保留
   C_t = 1.0*0.8 + 0.0*0.05 = 0.8000

f=0.0, i=1.0: 遗忘门全关 + 输入门全开 -> 完全换成这一时刻
   C_t = 0.0*0.8 + 1.0*0.05 = 0.0500

f=0.7, i=0.3: 按比例混合(七分旧、三分新)
   C_t = 0.7*0.8 + 0.3*0.05 = 0.5750

第一种情形就是「France 被长期保留」:网络遇到 France 时学会把遗忘门开到接近 1、输入门关小,这条信息就在细胞状态的传送带上原样流过很多个时间步,直到要预测 French 时再经输出门放出来;对不重要的中间词,输入门小、写不进去,扰动不了长期记忆。

更省的变体:把两道门捆成一道

很多人在 LSTM 之后又造了更简单的模型:把 input 门的系数全部扔掉,根本不要它——用 1−ft1 - f_t 替换 iti_t。道理在于:旧信息和这一时刻要写入的新信息,两者是「你死我活」的博弈关系——这边大、那边就小,这边小、那边就大。ftf_t 本身就是 0 到 1 之间的数,1−ft1 - f_t 正好把另一半补上,由它来决定「以前时刻的信息保留多少」。课件把这层关系写成两句话:只在要输入新东西的时候才遗忘;只在忘掉旧东西的时候才输入。(这一「耦合门」的思路,正是后来各种简化变体——如 GRU——的核心做法,本课不展开。)

从 pix2pix 到 Seq2Seq

先看图像那边是怎么做的

还记得讲 CNN 时的 autoencoder 形式:一个卷积网络压到隐层,再接一个反卷积的网络展开回来。这种成对的图像变换模型,讲师叫它 pix to pix(pixel to pixel):给定大量成对图像——比如白天和黑天——训练之后,权重就学会了「白是如何变黑的」;再比如戴着眼镜和摘了眼镜的成对照片,训练好之后,一个新的人戴着眼镜进来,就能生成出他没有眼镜的样子。图像风格变换也是同一族玩法:把一张普通照片变成毕加索式、或者莫奈式的风格——莫奈是法国印象派画家,画的都是模模糊糊的东西(有人开玩笑说他晚年是个近视眼,看所有东西都是模糊的,画出来的正是人脑中模模糊糊的印象)。做法上:把图像放进卷积神经网络,风格都是高层特征——让新照片的高层特征去逼近莫奈那些画的风格(把距离或 cos 距离变小);同时底层细节跟原始照片很相近——细节没有大变化、风格接近给定画家。这类工作很多,甚至已经有现成的 app。

讲师还展示了自己团队做过的漫画自动上色:反卷积过程根据一对一对的训练数据(素描图和颜色图),把颜色做到最好的估计再上上去。用的 UNet 结构有一个关键点——底层的线条特征在反卷积层里仍然有体现,所以上色图像的线条跟原黑白图是一致的;反卷积层的权重负责涂上黄色、蓝色各种颜色——它学的就是「黑白到彩色」这个变换关系,而且这个关系很大程度上由线条特征决定。于是有了那个有趣的细节:小黄人的皮肤是黄的、背带裤是蓝色的——因为训练数据里的小黄人都是这个模样。换日本动漫做训练,又能生成不同风格的动漫图像。(这套框架里还用了对抗网络等等。)

序列这边:sequence to sequence

回到 RNN。RNN 做的 autoencoder 过程,叫 sequence to sequence model——图像那边是 pixels to pixels,这边是一个序列到另一个序列:说一句汉语翻译成英文、一段声音变成其他的声音、弹一段音乐变成另外一种音乐。

讲师用一个小例子逐步推演:输入是 ABC 这种语言,输出是 XYZ 这种语言。A、B、C 是不同时刻的输入(每一个字就是一个时刻),它们之间是 LSTM 的关系;链走到最后,输到 EOS(end of sentence),把最后这个节点看成 C 的节点,它要生成下一个数据 W——W 是根据 A、B、C 和 EOS 所有的输入、以及它们之间的 LSTM 关系,得到的一个最好的估计。有了这个估计,把 W 再输入到下一步:W 做输入、隐藏做输入,找到最合适的词是 X;再往下把 X 输进去,得到 Y;最后到 Z 和 EOS——于是序列「W X Y Z EOS」和「A B C EOS」之间是更 match 的。因为在所有的训练库里,它们服从这样的特征和关系。

结构上分两半:前半是编码(encoding)的过程,中间是隐藏,后半是解码(decoding)的过程——encoder 和 decoder 都是一个 LSTM。课件把解码循环总结成六步:把输入序列编码成状态向量;从长度为 1 的目标序列(只有起始符)开始;把状态向量和目标序列喂给 decoder,产生下一个字符的预测;用 argmax 采样出下一个字符;把它拼回目标序列;重复,直到生成结束符或达到长度上限。另外还有很多不同的结构,会把 encoder 换成双向 RNN(双向 LSTM)——不但考虑跟前面的关系,还考虑所有双向的关系;而中间的 c 是隐层,对于所有的输出,是隐层 c 的作用加上它上一时刻的输出结果,共同作为下一时刻的重要信息。

注意力机制:让每一步输出直接看到输入

Seq2Seq 里有一个「致命的问题」:任意时刻的输出,都跟上一时刻有直接的关联——StS_t 跟上一个状态相关,同时跟上一时刻得出的 Yt−1Y_{t-1} 相关。我们希望有一个简单的办法,把输出跟输入之间的关系直接找到——这个关系用另外一个权重来表示。这就是注意力机制(attention mechanism)。

讲师给的语言翻译例子:英文 “the agreement on the european economic area was designed in the august 1992”,翻译成法语。模型的底层结构:输入的 encoding 是一个双向 RNN——两向的双向 LSTM,向左向右两个箭头,产生强关联;输出层是一个单向的 LSTM,从左到右;它们之间由注意力权重描述。效果上:生成法语里对应 economic 的那个词时,跟它最相关的输入词,就是英文里的 economic;它跟 european 或其他词也都可能有关系,但那个特别重的关系,代表它们之间的关系特别强——就像图上有的连线粗、有的连线细。某一个输出词跟前面输入 sequence 之间的权重关系,加上它前面隐藏 LSTM 的关系,共同选择,得到最好的结果——翻译的质量非常地提高。

注意力怎么训练?每一个连接的权重,就通过训练数据得到 α\alpha:α\alpha 是关于 ee 的函数,是 ee 的 softmax 形式;而 ee 通过权重 W1W_1、W2W_2,由目前的隐藏和现在的状态来调节——α\alpha 也是一个需要不断调整的权重,根据它们之间的关系确定。写成课件上的公式(hjh_j 是编码器各时刻的隐藏状态,sis_i 是解码器状态):

ci=∑j=1Txαijhj,αij=exp⁡(eij)∑k=1Txexp⁡(eik),eij=v⊤tanh⁡(W1hj+W2si−1)c_i = \sum_{j=1}^{T_x} \alpha_{ij} h_j, \qquad \alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^{T_x} \exp(e_{ik})}, \qquad e_{ij} = v^{\top} \tanh(W_1 h_j + W_2 s_{i-1})

上下文向量 cic_i 是各隐藏状态的加权和——每个解码步都有一份自己的、按相关性分配的输入视图;最终的输出概率 p(yi∣y1,…,yi−1,x)=g(yi−1,si,ci)p(y_i \mid y_1, \dots, y_{i-1}, x) = g(y_{i-1}, s_i, c_i)。

再往后一个有意思的结果:既然输入端已经用 attention 直接建立了联系,为什么还需要输出端那个 LSTM 关系?讲师提到有一个研究说「All you need is attention」——把输出端的 LSTM 关系都去掉,只用 attention 的关系,就能得到一个比较好的翻译。(这就是后来著名的 Transformer 架构的起点,本课不展开。)

生成:华丽但没有思想的诗

Seq2Seq 这套模型还能做很多生成的事情,讲师现场念了三组例子。

生成诗歌:

满怀风月一枝春,未见梅花亦可人。不畏东风无此客,世间何处是前身。

白鹭归于地,青山照水开。夜来风不动,明月见楼台。

看起来蛮好的——非常华丽、押韵。原因也很直接:它所有的训练数据,都是古代大诗人的杰出句子。但讲师点出一个诚实的问题:生成的诗自己并没有感觉,它不明白自己的中心思想是什么——「没有思想的诗」,只是为了追求字数的华丽。古人书里,一旦出现春花,你就对秋月;一遥望,你就扶栏杆——这些特别潜在的规律被模型提取到,就能组成特别漂亮、特别华丽的诗。

自动评论(讲师团队做过的例子):拿淘宝做训练——爬了淘宝、亚马逊等一共三个数据集的评论。生成的评论长这样:「高端大气上档次,质量很好,衣服摸起来挺舒服,挺凉爽的。客服人员服务非常好。」讲师特别强调:「想告诉大家,这些评论都是假的。」——都是根据爬下来的评论生成的,看起来跟真实的评论一模一样。

嘻哈歌词(讲师和同学们前两天刚做的新工作):爬「中国有嘻哈」里的歌词作为训练数据,还用 topic model 做了规范——什么类型的词用在什么位置,韵脚也可以处理。以爱情为主题生成的歌词:「是否不在乎这感情,导致我留在这梦境,我坠入深渊的证明,寻找下一段自己从来不需要想起,让自己看不起,忘记他们就是玩玩而已。」再来一段:「成长需要他描述在你身上,让我错过它的翅膀。经历和青春一场,陪我度过多少的风和雨,各自风雨里的过去,能让我战胜所有崎岖。本来属于我们的道路,献给我们的兄弟。」听起来朗朗上口、比较顺。讲师的彩蛋:大家还没听到这个 idea——「尽量不要先把我们的 idea 抢完」——下一步准备写个 paper。也留给你:这些技术还能用到什么地方,可以根据自己的思路想想。

📝 动手练一练

  1. 信息瓶颈:Seq2Seq 的编码器要把整个输入序列的信息送进解码。如果输入序列非常长,只靠「上一时刻」这条链传递,会出什么问题?注意力机制是怎么缓解的?
👉 点击查看参考答案

问题:解码端任意时刻的输出都只跟上一时刻的状态、上一时刻的输出直接相关——序列开头的信息要沿着整条时间链一步步传到结尾,途中不断被后来的信息稀释、覆盖,等到生成靠后的词时,开头那些关键信息(比如 France)早已磨损,这就是信息瓶颈。

注意力的缓解:给解码的每一步配上对输入所有时刻的权重 αij\alpha_{ij}——按相关性加权求和,得到这一步自己的上下文向量 cic_i。这样无论输入多长,每一步都能直接看到输入里最相关的部分(生成法语 economic 时,权重最重的连线就连在英文 economic 上),不必只依赖链末端那个固定向量,长序列的翻译质量显著提高。

  1. 门的读法:对照细胞状态更新式 Ct=ft⋅Ct−1+it⋅C~tC_t = f_t \cdot C_{t-1} + i_t \cdot \tilde{C}_t(标量情形即正文代码块 3)。(a) 若 ft→0f_t \to 0、it→1i_t \to 1,CtC_t 变成什么?(b) 若 ft→1f_t \to 1、it→0i_t \to 0 呢?(c) 哪种情形对应「France 被长期保留到预测 French」?(d) 取 ft=0.7f_t = 0.7、it=0.3i_t = 0.3、Ct−1=0.8C_{t-1} = 0.8、C~t=0.05\tilde{C}_t = 0.05,算出 CtC_t,并说出新旧信息各占多少。
👉 点击查看参考答案

(a) Ct≈C~tC_t \approx \tilde{C}_t——细胞状态完全换成这一时刻的候选值,跟前面没有任何关系。(b) Ct≈Ct−1C_t \approx C_{t-1}——这一时刻的信息没被写入,全部来源于上一时刻。(c) 情形 (b):France 写入之后,遗忘门保持接近 1、输入门保持接近 0,这条信息在传送带上原样流过很多时间步。(d) Ct=0.7×0.8+0.3×0.05=0.56+0.015=0.575C_t = 0.7 \times 0.8 + 0.3 \times 0.05 = 0.56 + 0.015 = 0.575(正文代码块 3 第三种情形同款)——其中旧信息贡献 0.56、占七成,新信息贡献 0.015、占三成权重。

本章小结

本节把「记忆」装进网络。RNN 的核心一步:同一个网络在各个时间段做同一件事,但 tt 时刻隐层的输入里有上一时刻的隐层输出——St=tanh⁡(Uxt+WSt−1)S_t = \tanh(U x_t + W S_{t-1}),UU/WW/VV 三个矩阵分别管输入、隐层之间、输出。与 HMM 对照:都是时序模型,一个用概率状态转移矩阵描述、一个用隐层权重关联描述;HMM 可看作 RNN 的一种特例,而 RNN 的 capacity 更大、更 flexible。LSTM:新增细胞状态 CC 这条传送带,遗忘、输入、输出三道门各自由「上一时刻隐藏 + 当前输入」算出,Ct=ft⊙Ct−1+it⊙C~tC_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t 决定新旧信息的配比——f 全开则旧信息原样保留(France)、i 全开则完全顶替;门值不是人定的,是训练数据调出来的。更省的变体干脆用 1−ft1 - f_t 顶替 iti_t,两门捆一门。从 pix2pix 到 Seq2Seq:图像的成对变换是 pixels to pixels,序列的 autoencoder 就是 encoder-decoder 两个 LSTM;注意力机制再给每个解码步配上对输入的 softmax 权重 αij\alpha_{ij}(eij=v⊤tanh⁡(W1hj+W2si−1)e_{ij} = v^{\top}\tanh(W_1 h_j + W_2 s_{i-1})),甚至「All you need is attention」。生成的诚实面:华丽但没有思想的诗、以假乱真的假评论、押韵的嘻哈歌词——统计规律学得到,中心思想没有。

收官再补两句(本课深度学习部分的最后一节)。课件后两页还有 word2vec(CBOW、Skip-Gram),讲师不展开:用 vector 的形式去表示自然语言,是生活里特别常用的一件事。把深度学习最基础的东西收成三句话:普通的神经网络,是一层层的非线性关系;卷积网络,是通过多层次的层次去做特征学习,使各种不同的特征满足你分类或其他的需求;RNN,一般来讲用一个 LSTM 加上 attention,就可以做很多很多不同的事情。无论在自然语言、在 image、在图像到文字、文字到图像,都有特别多的应用,也正是现在特别多的研究重点。

📋 行动清单

  • 手推一遍 LSTM 单步:对照代码块 2,在纸上从「上一时刻隐藏 + 当前输入」出发,把三道门、候选值、CtC_t、hth_t 全部手算一遍。
  • 口述 RNN 与 HMM 的异同:都用什么描述时序、各自的隐含状态和可观察状态在哪、为什么说 HMM 是 RNN 的特例。
  • 改代码做实验:把代码块 1 的输入序列换成全 1.0,对比有记忆与无记忆两条轨迹的差别;再改循环权重 WW 的正负,看记忆是「正向放大」还是「衰减」。
  • 找一首生成诗找规律:对照「春花对秋月、遥望对扶栏杆」,在生成诗里再找出两组「潜在的规律」。

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第19章课件:神经网络(BP/CNN/RNN)
    下载