RNN
递归神经网络, Recurrent Neural Networks, RNN, 用于处理序列化的数据. 文字或者自然语言就是一种序列化的数据, 段落就是单词的序列, 每个段落可能有不同数量的单词, 每个段落中可能包含有长距离依赖关系的单词, 如`The dog in that house is aggressive"和"The dogs in that house are aggressive"中的🐕和谓语之间的长距离依赖关系. 处理这种类型的数据需要模型能够"记住"之前序列中的某些信息.
架构¶
RNN一次处理序列中的一个元素, 例如一个单词. 举例来说, 句子"The dog in the house is aggreesive"会按照时间顺序分解为: \(t\)时刻处理"the", \(t+1\)时刻处理"dog"依次类推. RNN之所以被称为"递归神经网络"是因为它包含反馈连接, 即输出会反馈给输入, 这与前馈神经网络不同, 前馈神经网络是有向无环图, acyclic graph, 而递归神经网络是有向有环图, cyclic graph, 更具体的说, 一个神经元在某个时间步的输出会反馈到下一个时间步的相同神经元中, 因此, RNN具备一定的记忆能力, 可以记住之前的激活状态, 由于RNN能够记住过去的激活状态, 因此它能够捕捉长距离的依赖关系, 这种特性使得RNN特别适合用于处理序列数据, 例如语言模型和时间序列预测.
RNN有不同的架构, 其中包括简单的RNN, 又称为Elman网络和长短期记忆网络, LSTM.
简单RNN¶
简单RNN含有由\(1\)个隐藏层构成的前馈神经网络, 这个隐藏层特别的, 含有一个记忆缓存, 会存储隐藏层之前一个时间步的状态. 在每一个时间步, 记忆缓存中的数据会和下一组输入结合作为隐藏层神经元的下一次输入.
我们定义如下符号:
- \(x_t\): \(t\)时刻的输入向量
- \(h_t\): \(t\)时刻的隐藏层激活值, 表示经过隐藏层计算后的中间状态. 在图中\(h_t\)会结合前一个时间步的隐藏状态\(h_{t-1}\)以及当前时间步的输入向量\(x_t\)进行计算
- \(y_t\): \(t\)时刻的输出向量
- \(\bm{w}_{xh}\): 输入层和隐藏层之间的权重
- \(\bm{w}_{hy}\): 隐藏层和输出层之间的权重
- \(\bm{w}_{hh}\): 记忆缓存和隐藏层之间的权重
我们就来考虑上述的情况, \(2\)个输出层神经元, \(3\)个隐藏层神经元, \(2\)个输出层神经元. 在每一个时间步都会有一个新的输入向量. 每个隐藏层神经元都会接收到所有输入层神经元的输出和记忆缓存中的上一个状态信息. 隐藏层处理后, 将结果传递给输出层神经元, 同时将状态写到记忆缓存中.
\(h_t\)的计算公式为\(h_t = f_{\bm{w}}(h_{t-1}, x_t)\). 其中\(h_{t-1}\)是旧状态, \(x_t\)是本次的输入向量. 函数\(f_{\bm{w}}\)通常为反正切函数, 即\(h_t=\tanh (\bm{w}_{hh}h_{t-1}+\bm{w}_{xh}x_t+b_h)\). 而输出\(y_t = \bm{w}_{hy}h_t+b_y\).
展开¶
在上面RNN的架构图中, 这种循环结构使得理解在多个时间步之间的信息流动变得困难. 为了更好地理解RNN的信息流动的过程, 我们可以将RNN沿着时间展开, unroll, 然后每一个时间步的计算看作是一个独立的前馈神经网络. 展开后的RNN就像一系列相互连接的前馈神经网络层, 这样可以帮助我们更清晰地看到每个时间步的输入, 隐藏状态和输出之间的关系, 如下图所示.
注意, 上图中可以明显看到, 权重\(\bm{w}_{xh}\), \(\bm{w}_{hy}\), \(\bm{w}_{hh}\)在每个时间步都是"共享"的, RNN在处理数据的时候, 会不断通过多个时间步来传播信息, 如果每个时间步的权重都不同, 那么模型会变得非常复杂, 不容易训练, 因此, 这种RNN采用的是权重共享机制.
通过时间反向传播¶
通过时间反向传播, Backpropagation Through Time, BPTT是训练RNN的关键算法, 它是标准的反向传播算法的扩展, 用来处理时间序列数据中的依赖关系.
RNN的损失函数是每个时间步的输出\(y_t\)和真实标签\(y_t^{true}\)之间的误差之和, 假设我们使用均方误差作为损失函数, 那么损失\(L\)为\(L=\sum_{t=1}^T(y_t-y_t^{true})^2\). 目标是通过最小化这个损失函数来更新RNN的权重. 在\(t\)这个时间步的误差被反向传播到所有对其有贡献的参数, 这意味着它向网络先前的状态传播, 所以叫做"时间倒流". 如图所示.
根据课件上所讲的, 在每个时间步都要计算前面所有时间步需要更新的权重, 所以这些权重会被累积, 直到这一轮的最后, 前面各个时间步的更新量被累积之后用于更新权重. 反向传播算法会被使用多次, 直到收敛. 在训练结束之后, 我们可以使用RNN来生成文字, 一个字接着一个字.
RNN非常灵活, 它可以是一对一的, 也可以是一对多, 多对一, 多对多.
在一些任务中, 如情感分析, RNN会在处理完整个句子之后输出一个单一的值, 代表正面或者负面的情感分类: 输入是一句包含多个单词的句子, 模型逐字处理, 每个单词被输入到RNN中, 生成一个隐藏状态\(h_i\), 这些隐藏状态会被传递给下一个时间步, 在处理玩所有单词后, 模型将最后的隐藏状态用于生成最终的输出.
缺陷¶
简单RNN常常会面临"梯度消失"问题, 这是因为在反向传播过程中, 误差梯度会在每个时间步中与\(\bm{w}_{hh}\)多次相乘, 如果这些权重过小, 这种多次相乘会导致梯度逐渐变得非常小, 最终几乎消失. 由于这种问题的存在, 它在实践中往往表现地不够好, 尤其当依赖距离较大的时候. 例如句子"The cloud are in the sky", 这里预测"sky"这个单词很容易, 因为前面的上下文信息"The clouds are in the"与它距离很近. 而句子"I grew up in Italy... I speak fluent Italian", 这里需要依赖较远的上下文信息, 由于距离远, 传统RNN很难有效学习这种长期依赖关系.
LSTM¶
LSTM, 长短期记忆网络, 是一种改进型的RNN, 专门用于解决RNN在处理长期依赖关系时的局限性. 它由Hochreiter和Schmidhuber在1997年提出, 解决了传统RNN中梯度消失问题. LSTM的关键思想是网络可以选择记住什么, 丢弃什么以及从哪里读取信息, 它通过一系列的门来控制信息流动, 包括遗忘门, 输入门和输出门. 如下图所示, 左边为普通的RNN, 右边是LSTM.
LSTM单元是LSTM网络的基本构件, 当前的输入向量\(x_t\)和之前的短期状态\(h_{t-1}\)会被喂给\(4\)个不同的全连接层(而不是像简单RNN那样之后一个), 新的\(3\)个层都有sigmoid激活函数, 用\(\sigma\)表示, 它们的输出在\(0\)和\(1\)之间. 如下图所示.
在LSTM中, 状态分为两种: 细胞状态和隐藏状态, 它们分别对应长期记忆和短期记忆. 长期记忆, 或者称为细胞状态, cell state, 它类似于一个贯穿多个时间步的信息传递通道, 通过遗忘门和输入门控制, 允许信息在多个时间步之间持续保持或者更新. 短期记忆, 或者称为隐藏状态, hidden state, 反映了每个时间步的即时输出, 它携带的是当前输入和前一时间步信息的组合.
遗忘门层¶
遗忘门的作用是从上一个时间步的细胞状态\(C_{i-1}\)中决定哪些信息应该被丢弃或者保留. 遗忘门使用上一个时间步的隐藏状态\(h_{t-1}\)和当前输入\(x_t\), 将它们传递给一个sigmoid函数, sigmoid函数会输出一个\(0\)到\(1\)之间的值, 如果输出值接近于\(0\), 表示遗忘该信息; 如果输出值接近\(1\), 表示保留该信息, 数学公式表示为: \(f_t=\sigma(w_f[h_{t-1}, x_t]+b_f)\). 例如, 在语言模型中, 当预测下一个单词的时候, 可能前面的细胞状态包含了当前主语的性别信息, 如"he"或者"she", 如果在句子中出现了一个新主语, 遗忘门将决定是否遗忘旧的新别信息, 以适应新主语的信息.
输入门层¶
输入门的作用是控制当前时间步的输入\(x_t\)和上一时间步的隐藏状态\(h_{t-1}\)如何影响当前的细胞状态\(C_t\), 它通过两个步骤来决定哪些信息应该被添加到细胞状态中. 首先使用Tanh激活函数生成一个新的候选状态\(\tilde{C_t}=\tanh(W_C[h_{t-1}, x_t]+b_C)\), 这个候选状态的值在\(-1\)和\(1\)之间, 然后这个候选状态会和一个输入门的输出相乘\(i_t\cdot \tilde{C_t}\), 输入门的输出计算公式为\(i_t = \sigma(W_i[h_{t-1}, x_t]+b_i)\), 得到要更新到细胞状态中的新信息. 例如, 我们在预测一个新的主语的性别, 输入门层将会控制将新主语的性别信息更新到细胞状态中.
更新细胞¶
结合遗忘门层和输入门层, 我们会得到这两个信息: 旧的细胞状态和遗忘门层的输出相乘\(f_t\cdot C_{t-1}\), 输入门的输出和候选状态相乘\(i_t\cdot \tilde{C_t}\). 将这两个部分相加, 我们会得到新的细胞状态\(C_t = f_t\cdot C_{t-1}+i_t\cdot \tilde{C_t}\). 这意味着LSTM细胞既保留了过去重要的信息, 又根据当前输入加入了新的信息.
输出门层¶
输出门层决定到底输出什么, 输出的结果会基于当前细胞状态, 并且会经过过滤. 首先, 经过一个sigmoid激活函数, 计算出一个向量\(o_t\), 这个向量的每个元素取值在\(0\)到\(1\)之间, 决定了细胞状态中哪些信息需要被输出, 起到了过滤器的作用, 接下来, 先前更新后的细胞状态\(C_t\)通过tanh激活函数处理, 将数值压缩到\(-1\)和\(1\)之间, 这一步使得数据更加稳定, 更适合下一步的计算, 然后, 将值和\(o_t\)主元素相乘, 得到最终的输出结果.