Skip to main content

第 13 课:序列模型

(Sequence Models)

(一)序列模型的基础介绍

序列模型有很多,包括 循环神经网络(RNN)

序列模型有很多应用场景:
语音识别: 输入音频片段𝑥,输出对应的文字记录 𝑦。输入和输出数据都是序列模型,因为 𝑥是一个按时播放的音频片段,输出 𝑦是一系列单词。
音乐生成: 只有输出数据𝑦是序列,而输入数据可以是空集 或一个整数,这个数可能指代音乐风格,也可能是头几个音符
还有 DNA序列分析,机器翻译,视频动作识别,情感分类

假如你想要建立一个能够自动识别句中人名位置的序列模型,那么这就是一个 命名实体识别问题,这常用于搜索引擎

假设输入的一句话:“Harry Potter and Herminoe Granger invented a new spell.”
这是9个单词组成的序列,分别对应 x<1>,x<2>,,x<9>x^{<1>},x^{<2>},……, x^{<9>},中间的某一项用 x<t>x^{<t>} 表示,输入序列长度是 Tx=9T_x = 9
输出数据是 y<1>,y<2>,,y<9>y^{<1>},y^{<2>},……, y^{<9>}, 输出序列长度是 Ty=9T_y = 9

x(i)<t>x^{(i)<t>} 表示第 i 个训练样本的第 t 个输入元素,y(i)<t>y^{(i)<t>} 表示第 i 个训练样本的第 t 个输出元素
Tx(i)T_x^{(i)} 代表第 i 个训练样本的输入序列长度,Ty(i)T_y^{(i)} 代表第 i 个训练样本的输出序列长度

想要表示一个单词,首先要做一张词表,包含从'a','abandon',……,'zulu' 的所有词
x<t>x^{<t>} 就是一个和这张词表尺寸一样的向量,这个单词的位置是1,剩余位置都是0, 这称为 one-hot表示法
如果你的词典大小是 10000的话,那么这里的每个向量 x<t>x^{<t>} 都是 10,000维的

注意:

  1. 如果遇到了一个不在你词表中的单词,要创建一个新的标记 <UNK>,来表示不在词表中的单词
  2. 对于商业应用来说,或者对于一般规模的商业应用来说 30,000到 50,000词 的词典比较常见


(二) 循环神经网络模型(RNN)

由于单个 x<t>x^{<t>} 的体量太大,同时输入和输出数据的长度 在不同例子中不同,所以普通的标准神经网络不适用,
而且标准神经网络并不共享从文本的不同位置上学到的特征。
比如假设神经网络已经学习到了在位置 1出现的 Harry可能是人名,那么如果Harry出现在其他位置,它应该也要能够自动识别其为人名。
这类似于卷积神经网络能将部分图片里学到的内容快速推广到图片的其他部分,而我们希望对序列数据也有相似的效果。

因此需要使用 循环神经网络RNN

步骤如下:

  1. 首先在零时刻需要构造一个激活值a<0>a^{<0>},这通常是零向量。
  2. 将第一个词输入一个神经网络层,经过第一个神经网络的隐藏层,可以得到预测输出,判断这是否是人名的一部分
  3. 当网络读到第二个单词 x<2>x^{<2>} 时,它不是只用 x<2>x^{<2>} 就预测出 y<2>y^{<2>},而是结合上一步的激活值 a<1>a^{<1>}
  4. 如此循环直到最后一个时间步

每一步的计算过程如下:

a<t>=g1(Waaa<t1>+Waxx<t>+ba)a^{<t>} = g_1(W_{aa} a^{<t-1>} + W_{ax} x^{<t>} + b_a) y^<t>=g2(Wyaa<t>+by)\hat{y}^{<t>} = g_2(W_{ya} a^{<t>} + b_y)

我们可以将这里的符号简化一下:

a<t>=g1(Wa[a<t1>,x<t>]+ba)a^{<t>} = g_1(W_{a}[a^{<t-1>},x^{<t>}] + b_a) y^<t>=g2(Wya<t>+by)\hat{y}^{<t>} = g_2(W_{y} a^{<t>} + b_y)

其中:

[Waa:Wax]=Wa[a<t1>,x<t>]表示[a<t1>x<t>][W_{aa} : W_{ax}] = W_a \\ [a^{<t-1>},x^{<t>}] 表示 \begin{bmatrix} a^{<t-1>} \\ x^{<t>} \end{bmatrix}

如果 𝑎是 100维的,𝑥是 10,000维的,那么 𝑊𝑎𝑎就是个 100×100 维的矩阵, 𝑊𝑎𝑥就是个 100×10000 维的矩阵,𝑊𝑎就会是个 100×10000 维的矩阵。

注意:

  1. 循环神经网络是从左向右扫描数据,同时每个时间步的参数也是共享的。
    x<1>x^{<1>} 到隐藏层之间 的一系列参数用 𝑊ax𝑊_{ax} 来表示,每个时间步使用的都是相同的参数𝑊ax𝑊_{ax}
    时间步之间 激活值传递 的一系列参数用 𝑊aa𝑊_{aa} 来表示,每个时间步使用的都是相同的参数𝑊aa𝑊_{aa}
    隐藏层和输出结果之间 的一系列参数用 𝑊ya𝑊_{ya} 来表示,每个时间步使用的都是相同的参数𝑊ya𝑊_{ya}
  2. WaxW_{ax} 的第二个下标x意味着要乘以某个x类型的变量,第一个下标a表示它是用来计算某个a类型的变量,同理推广
  3. 循环神经网络用的激活函数经常是tanh,不过有时候也会用 ReLU。
    如果y输出类型是 二分问题,用 sigmoid函数作为激活函数,(这里采用)
    如果y输出类型是 𝑘类别分类问题,用 softmax作为激活函数

向前传播流程如下

某个时间步𝑡上某个单词的 预测值的 损失函数。:

L<t>(y^<t>,y<t>)=y<t>log(y^<t>)(1y<t>)log(1y^<t>)L^{<t>}(\hat{y}^{<t>}, y^{<t>}) = -y^{<t>} \log(\hat{y}^{<t>}) - (1 - y^{<t>}) \log(1 - \hat{y}^{<t>})

它对应的是序列中一个具体的词,如果它是某个人的名字,那么 𝑦<𝑡>𝑦^{<𝑡>} 的值就是 1,然后神经网络将输出这个词是名字的概率值,比如 0.1
这是 标准逻辑回归损失函数,也叫 交叉熵损失函数( Cross Entropy Loss),它和之前我们在二分类问题中看到的公式很像。

整个序列的损失函数:

L(y^,y)=t=1TxL<t>(y^<t>,y<t>)L(\hat{y}, y) = \sum_{t=1}^{T_x} L^{<t>}(\hat{y}^{<t>}, y^{<t>})

反向传播的计算过程如下:(框架会自动计算,所以仅供了解)

这个算法有一个很别致的名字,叫做 通过(穿越)时间反向传播 backpropagation through time)

我们现在看到的RNN结构中 输出尺寸 TxT_x 等于 输出尺寸 TyT_y, 然而当他们不相等时,RNN结构要做出改变
当输入序列有很多的输入,输出序列有很多的输出时,称为 多对多 (many-to-many)的结构,比如刚刚的人名识别和机器翻译
当输入序列有很多的输入,输出序列只有一个数字时,称为 多对一 (many-to-one)结构,比如句子情感识别
当输入序列只有一个数字,输出序列只有一个数字时,称为 一对一 (one-to-one)结构,这个不适合RNN,适合之前讲的网络
当输入序列只有一个数字,输出序列有很多的输出时,称为 一对多 (one-to-many)结构,比如音乐生成

注:one-to-many 那张图中的正方形之间也是有横向箭头的,被挡住了



(三)用 RNN构建一个语言模型

所以什么是语言模型呢?
比如你听到一个句子,“the apple and pear(pair) salad was delicious.”
我说的是 “the apple and pair”,还是 “the apple and pear” ?(pear和 pair是近音词)。
这就是语音识别系统要输出的东西,语音识别系统要使用一个语言模型,计算出这两句话各自的可能性。
比如 P(pair) = 3.2 10*-13 而 P(pear) = 5.7 10*-10, 那就是显然是第二种了,因为概率高很多

所以语言模型所做的就是,输入一个文本序列,它会告诉你某个特定的句子它出现的概率是多少

为了使用RNN建立语言模型,你首先需要一个很大的 文本语料库(corpus) 作为训练集
语料库是自然语言处理的一个专有名词,意思就是很长的或者说数量众多的句子组成的文本。

假如说,你在训练集中得到这么一句话:“Cats average 15 hours of sleep a day.”
训练过程如下:

  1. 标记化: 建立一个字典,然后将每个单词都转换成对应的 one-hot向量
    你可以自行决定要不要把标点符号看成标记,在本例中 ,我们忽略了标点符号,反之需要把标点符号也加入字典
  2. 在句尾增加一个额外的标记,叫 <EOS>,用于定义句子的结尾。
    EOS标记要被附加到训练集中每一个句子的结尾。 注意 <EOS> 也算入输入输出。
  3. 遇到未知的词时,把原词替换为 <UNK>,我们只针对 <UNK> 建立概率 模型,而不是针对这个具体的词
  4. 在第 0 个时间步,你要计算激活项 𝑎<1>𝑎^{<1>},它是以 𝑥<1>𝑥^{<1>}作为输入的函数,而 𝑥<1>𝑥^{<1>} 是 0向量
  5. a<1>a^{<1>} 会通过 softmax 来预测第一个词的各种概率,其结果就是 y^<1>\hat y^{<1>}
    y^<1>\hat y^{<1>} 是个 10002 维向量,包含了各个词的概率,其中包括字典中的 10000个词 以及<UNK><EOS>
  6. RNN进入下个时间步,使用激活项 𝑎<1>𝑎^{<1>} 和正确的第一个词 y^<1>\hat y^{<1>} 计算出第二个词 y^<2>\hat y^{<2>},这就是为什么 𝑦<1>=𝑥<2>𝑦^{<1>}=𝑥^{<2>}
  7. 如此循环,始终保持 x<t>=y^<t1> x^{<t>} = \hat y^{<t-1>}
  8. 最后在第9个时间步,然后把 𝑎<9>𝑎^{<9>}𝑥<9>𝑥^{<9>}(也就是 𝑦<8>𝑦^{<8>})传给它, 会输出 𝑦<9>𝑦^{<9>},理想情况下 最后的得到结果会是 <EOS>,因此停止
  9. 为了训练这个网络,需要定义 某个时间步t 的代价函数
L<t>(y^<t>,y<t>)=iyi<t>logy^i<t>L^{<t>}(\hat{y}^{<t>}, y^{<t>}) = -\sum_i y_i^{<t>} \log \hat{y}_i^{<t>}

总体损失函数

L=tL<t>(y^<t>,y<t>)L = \sum_{t} L^{<t>}(\hat{y}^{<t>}, y^{<t>})
  1. 更细参数,梯度下降

所以RNN中的每一步都会考虑前面得到的单词,比如给它前 3个单词,让它给出下个词的分布,这就是 RNN如何学习从左往右地每次预测一个词。

现在有一个新句子,它只包含 3个词 y<1>,y<2>,y<3>y^{<1>},y^{<2>},y^{<3>},现在 要计算出整个句子中各个单词的概率,
第一个softmax层会告诉你 𝑦<1>𝑦^{<1>}的概率,这也是第一个输出,
第二个 softmax层会告诉你在考虑 𝑦<1>𝑦^{<1>} 的情况下 𝑦<2>𝑦^{<2>}的概率
第三个 softmax层告诉你在考虑 𝑦<1>𝑦^{<1>}𝑦<2>𝑦^{<2>}的情况下的𝑦<3>𝑦^{<3>}概率
把这三个概率相乘,最后得到这个含 3个词的整个句子的概率。

新序列采样(Sampling novel sequences) (略)

这种方法用于训练一个序列模型之后,了解这个模型学到了什么

下图中编号 1所示的网络是经过训练的

步骤如下:

  1. 输入 𝑥<1>=0𝑥^{<1>}=0𝑎<0>=0𝑎^{<0>}=0,在第一个时间步经过 softmax层 后输出各个词的概率向量,在这个向量中随机采样作为 y<1>y^{<1>}
    不管你在第一个时间步得到的是什么词,都要把它传递到下一个位置作为输入
  2. 和训练的流程一样,除了 每个时间步都在 softmax层 的输出中随机采样,输入下一层
  3. 一直进行采样直到得到 EOS标识 ,这代表着已经抵达结尾,可以停止采样了。


(四)双向循环神经网络 BRNN

前面我们讲到了RNN的网络结构,但是存在一个问题,就是它 在某一时刻的预测 仅使用了从序列之前的 输入信息 ,并没有使用序列之后的信息
而我们知道句子的语境是根据上下文得出的,所以识别的效果不好
这个问题可以用 双向循环神经网络BRNN 来解决

给定一个输入序列 𝑥<1>𝑥^{<1>}𝑥<4>𝑥^{<4>},这个序列首先计算前向的 a<1>\vec{a}^{<1>},然后计算前向的 a<2>\vec{a}^{<2>},a<3>\vec{a}^{<3>},a<4>\vec{a}^{<4>}
而反向序列从计算 a<4>\vec{a}^{<4>} 开始,反向进行,计算反向的 a<3>\vec{a}^{<3>}, a<2>\vec{a}^{<2>}, 𝑥<1>𝑥^{<1>}
把所有这些激活值都计算完了就可以计算预测结果了。

y^<t>=g(Wg[𝑎<t>,𝑎<t>]+by)\hat{y}^{<t>} = g\left(W_g [𝑎⃗^{<t>}, 𝑎⃖^{<t>}] + b_y\right)

注意:

  1. 这里的正向和反向计算 都属于 正向传播!因为都是为了算激活值。
  2. BRNN的缺点就是你需要完整的数据的序列, 你才能预测任意位置。
    比如一个语音识别系统,需要等待这个人说完,获取整个语音才能处理。


(五)GRU门控单元

训练很深的神经网络时,随着层数的增加,导数有可能指数型的下降或者指数型的增加,也就是 梯度消失或者梯度爆炸。

其中梯度爆炸很明显也很好解决,
因为指数级大的梯度会让你的参数变得极其大 ,导致数值溢出。你会看到很多 NaN 或者不是数字的情况。
梯度爆炸可以用 梯度修剪 解决,也就是 当梯度向量大于某个阈值时,缩放梯度向量,保证它不会太大

而 RNN 中的梯度消失问题比较棘手
举个例子,我们知道英语语法中有三单原则,但是如果 动作主体 离 动词 很远,就不能保证输出第三人称单数的准确性了
这是因为很深的神经网络,从输出 y^\hat y 得到的梯度很难传播回去影响靠前层的权重,也就是梯度消失
所以RNN 很难处理这种长期依赖效应,因此我们使用 门控循环单元GRU 解决这个问题

我们之前看到的RNN单元如下

整个GRU单元的结构图如下

而GRU单元将会有个新的变量称为 𝑐,代表记忆细胞
在时间 𝑡处,有记忆细胞 c<t>c^{<t>},对于GRU而言 c<t>=a<t>c^{<t>}=a^{<t>}
在每个时间步,我们计算一个候选值 c~<t>\tilde{c}^{<t>}

c~<t>=tanh(Wc[Γrc<t1>,x<t>]+bc)\tilde{c}^{<t>} = \tanh(W_c [\Gamma_r * c^{<t-1>}, x^{<t>}] + b_c)

然后由下面的“门” 决定候选值是否替代记忆细胞 c<t>c^{<t>} 的值

GRU中还有一个叫“门”的结构 Γu,Γr(0,1)\Gamma_u,\Gamma_r \in (0,1), 下标u代表更新门,下标r代表相关性。
Γr\Gamma_r 相关门 代表 c<t1>c^{<t-1>}c~<t>\tilde{c}^{<t>} 有多大的相关性。
Γu\Gamma_u 更新门 决定 候选值 c~<t>\tilde{c}^{<t>} 是否代替 记忆细胞 c<t>c^{<t>} 的值

Γu=σ(Wu[c<t1>,x<t>]+bu)Γr=σ(Wr[c<t1>,x<t>]+br)\Gamma_u = \sigma(W_u [c^{<t-1>}, x^{<t>}] + b_u) \\ \Gamma_r = \sigma(W_r [c^{<t-1>}, x^{<t>}] + b_r)

对于大多数输入, sigmoid函数的输出总是非常接近 0 或 1, 因此 Γu\Gamma_u 在大多数情况下非常接近0或1

c<t>=Γuc~<t>+(1Γu)c<t1>c^{<t>} = \Gamma_u \ast \tilde{c}^{<t>} + (1 - \Gamma_u) \ast c^{<t-1>}

Γu=1\Gamma_u = 1 时更新,Γu=0\Gamma_u = 0时不更新

由于存在记忆单元把前面的值传递到了网络的深层,因此可以解决梯度消失问题

注意:

  1. Γu\Gamma_uc<t>c^{<t>}c~<t>\tilde{c}^{<t>} 的维度一样
  2. Γu\Gamma_u 不会真的等于 0或者 1,只是很接近


(六)长短期记忆(LSTM)

除了GRU单元,LSTM单元也可以做到防止梯度消失,建立深度链接的作用,甚至效果更好

对于GRU而言 c<t>=a<t>c^{<t>}=a^{<t>}。对LSTM来说则不是

他们的计算过程对比如下:

可见我们去除了 相关门Γr\Gamma_r ,增加了遗忘门 Γf\Gamma_f 和 输出门Γ0\Gamma_0

其结构如下:

把这些网络连接起来即:

反向传播如下(看看就好,框架会自己算的)

image-20250619222931542

image-20250619222947221



(七) 深层循环神经网络

(Deep RNNs)

这是一种结合标准神经网络和 RNN 的构造方式

结构如下:

我们看一个具体的计算例子:

a[2]<3>=g(Wa[2][a[2]<2>,a[1]<3>]+ba[2])a^{[2]<3>} = g\left(W_a^{[2]} [a^{[2]<2>}, a^{[1]<3>}] + b_a^{[2]}\right)

对于标准的神经网络,你可能见过很深的网络,甚至于 100层深,而对于RNN来说,有三层就已经不少了。
因为时间的维度,Deep RNN会变得相当大