循环神经网络
循环神经网络
-
序列数据输入和输出的长度可能不同,并且每个元素都有时间依赖性,传统的前馈神经网络无法有效建模序列信息,也无法共享特征。
典型的序列建模任务:
语音识别:输入一段语音音频,输出对应的文本记录,输入输出可能长度不同。
音乐生成:输入整数代表风格或者前几个音符,输出生成的音乐序列,仅输出是序列。
情感分类:输入语句,输出代表情感类别的标签,仅输入是序列。
DNA序列分析:输入DNA片段,输出该片段是否匹配某种蛋白质。
机器翻译:输入法语,输出英语,输入输出可能长度不同。
视频行为识别:输入一系列视频帧,输出食品中的行为。
命名实体识别:输入文本序列,输出句中实体,输入是序列,输出是标注的序列。
-
输入序列 \(X = (x^{<1>}, x^{<2>}, ..., x^{<T_x>})\),其中 \(x^{<t>}\) 表示输入序列中第 \(t\) 个单词,\(T_x\) 表示输入序列的长度。
输出序列 \(Y = (y^{<1>}, y^{<2>}, ..., y^{<T_y>})\),\(T_y\) 表示输出序列的长度,输入序列 \(T_x\) 和输出序列长度 \(T_y\) 可能不同,可以用 \(y^{(i)<t>}\) 来表示第 \(i\) 个训练样本中的第 \(t\) 个元素。
词表 \(V\) 是一个固定的单词集合,包含了常用的单词。通过统计训练数据中出现频率最高的单词,选择前 \(N\) 个常见单词构建而成。也可以直接使用公开的单词表,如 WordNet、GloVe 词表等。
每个单词被表示为一个one-hot 向量,即一个只有一个对应的元素是 1,其余都是 0 的 \(|V|\) 维向量。如果某个单词不在词表中,则使用
<UNK>
标签。 -
为什么使用循环神经网络而不使用标准神经网络?
- 输入输出长度不固定,\(T_x\) 和 \(T_y\) 可能不同,需要填充使输入句子达到固定长度,但这不是最优解。
- 标准神经网络无法共享位置特征,不能复用在某个位置学到的信息。例如,如果在 \(x^{<1>}\) 处学到的信息在 \(x^{<t>}\) 处无法自动泛化。在 CNN 中,卷积核可以共享特征,我们希望 RNN 也能在序列数据上共享特征。
- 标准神经网络参数过多,若使用 one-hot 向量 作为输入,维度可能很大(如 10,000),导致输入层连接的权重矩阵庞大,计算成本高。
-
循环神经网络通过时间步传播信息,在时间步 \(t\),当前隐藏状态 \(a^{<t>}\) 由前一时间步的隐藏状态 \(a^{<t-1>}\) 和当前输入 \(x^{<t>}\) 共同计算。输入 \(x^{<t>}\),通过隐藏状态 \(a^{<t>}\) 和所有时间步共享参数 \(W_{\text{ax}}\)、 \(W_{\text{aa}}\) 和 \(W_{\text{ya}}\),最终输出 \(\hat{y}^{<t>}\)。
初始隐藏状态 \(a^{<0>}\) 通常初始化为零向量。
\[a^{<t>}=g_1(W_{aa}a^{<t-1>}+W_{ax}x^{<t>}+b_a) \\ \hat{y}^{<t>}=g_2(W_{ya}a^{<t>}+b_y) \]其中 \(W_{\text{ax}}\) 表示输入到隐藏层的权重矩阵,\(W_{\text{aa}}\) 表示隐藏状态到隐藏状态的权重矩阵(自回归结构),\(b_a\) 表示隐藏层的偏置项,\(g_1\) 常用 tanh 作为激活函数。\(W_{\text{ya}}\) 表示隐藏状态到输出的权重矩阵,\(b_y\) 表示输出的偏置项。
将权重矩阵拼接为 \(W_a = [W_{\text{aa}}, W_{\text{ax}}]\),可以简化为
\[a^{<t>} = g(W_a \begin{bmatrix} a^{<t-1>} \\ x^{<t>} \end{bmatrix} + b_a) \\ \hat{y}^{<t>} = g(W_y a^{<t>} + b_y) \]但是传统RNN具有单向性和长期依赖问题两大缺点。单向性是指RNN只能使用过去的信息,无法利用未来的信息,可以通过双向RNN解决。长期依赖问题是指RNN无法有效学习长序列中的依赖关系,可以通过LSTM和GRU解决。
-
RNN的结构类似于一个展开的深度神经网络,每个时间步都是一个隐藏层。在前向传播时,信息从左到右流动,模拟时间前进。在反向传播时,梯度从右到左回传,类似时间倒流。
RNN的通过时间反向传播(Backpropagation Through Time):
首先计算单个时间步的交叉熵损失:
\[L^{<t>}(\hat{y}^{<t>}, y^{<t>}) = - y^{<t>} \log \hat{y}^{<t>} - (1 - y^{<t>}) \log (1 - \hat{y}^{<t>}) \]然后将所有时间步累加,得到整体损失函数:
\[L(\hat{y}, y) = \sum_{t=1}^{T_x} L^{<t>}(\hat{y}^{<t>}, y^{<t>}) \]由于RNN在时间步之间共享参数,梯度计算时需要累积所有时间步的梯度。
计算从 \(L\) 反向传播到 \(a^{<T_x>}\) 的梯度,然后逐步向前传递到 \(a^{<1>}\)。
计算从 \(a^{<t>}\) 反向传播到 \(W_a\) 和 \(b_a\) 的梯度。
计算从 \(\hat{y}^{<t>}\) 反向传播到 \(W_y\) 和 \(b_y\) 的梯度。
求得所有梯度后,使用梯度下降进行参数更新。
RNN的反向传播也会遇到梯度消失与梯度爆炸的问题,梯度爆炸可以通过梯度裁剪解决,梯度消失可以通过使用LSTM/GRU代替普通RNN的方法解决。
-
RNN的不同结构:
RNN结构 | 输入 | 输出 | 示例任务 |
---|---|---|---|
一对一 | 单个 \(x\) | 单个 \(y\) | 传统神经网络分类 |
一对多 | 单个 \(x\) | 序列 \(y\) | 音乐生成、图像描述 |
多对一 | 序列 \(x\) | 单个 \(y\) | 文本情感分类、语音情感分析 |
多对多 (\(T_x = T_y\)) | 序列 \(x\) | 序列 \(y\) | 命名实体识别、词性标注 |
多对多 (\(T_x \neq T_y\)) | 序列 \(x\) | 序列 \(y\) | 机器翻译、字幕生成 |
-
语言模型(LM)可以计算一个句子或单词序列的概率。例如计算句子概率:
\[P\left(\text{The apple and pear salad} \right) = 5.7 \times 10^{-10} \\ P\left(\text{The apple and pair salad} \right) = 3.2 \times 10^{-13} \]然后选择概率更大的句子,帮助语音识别做出正确决策。
训练语言模型需要大规模文本语料库,将新闻、书籍、网页中的文本语句拆分为单词,并转换为索引表示,选择最常见的 \(N\) 个单词构建字典,将不在词表中的单词处理为
UNK
标记,并在句末添加EOS
标记帮助识别句子结束。语言模型的训练目标是如何基于前文预测下一个单词,即输入前 \(t-1\) 个单词 \(\{y^{<1>}, y^{<2>}, ..., y^{<t-1>}\}\),预测下一个单词的概率分布 \(P(y^{<t>} | y^{<1>}, ..., y^{<t-1>})\)。
RNN语言模型的训练过程如下:
假设句子为
Cats average 15 hours of sleep a day.
处理输入为 \(y^{<1>} = \text{Cats}\) , \(y^{<8>} = \text{day}\) , \(y^{<9>} = \text{EOS}\) 。在时间步0,输入 \(x^{<1>}\) 设为 0 向量,计算隐藏状态 \(a^{<1>}\) ,通过 softmax 计算所有词的概率 \(\hat{y}^{<1>}\) 。在时间步1,输入 \(y^{<1>}\)(正确的单词 "Cats"),计算隐藏状态 \(a^{<2>}\) ,通过 softmax 计算 \(\hat{y}^{<2>}\)(预测下一个单词)。依此类推,直到遇到
EOS
句子结束。每个时间步的交叉熵损失函数为:
\[L\left( \hat y^{<t>}, y^{<t>} \right) = - \sum_{i} y_i^{<t>} \log \hat{y}_i^{<t>} \]对所有时间步求和计算总损失:
\[L = \sum_{t} L^{<t>}(\hat{y}^{<t>}, y^{<t>}) \]通过最小化损失 \(L\),使模型更准确地预测下一个单词。
-
在训练过程中,模型会学习生成一个特定单词序列的概率。在训练完成后,生成新的序列时,实际上是在从模型的概率分布中进行采样,每个时间步的目标是根据前一步的输出预测下一个单词。
\[P(y^{<1>}, y^{<2>}, y^{<3>}) = P(y^{<1>}) \times P(y^{<2>} | y^{<1>}) \times P(y^{<3>} | y^{<1>}, y^{<2>}) \]在第一个时间步输入 \(x^{<1>}\),初始化状态 \(a^{<0>}\),模型通过Softmax计算所有可能的输出,并从中随机采样一个单词。若生成未知标识
UNK
,可以选择重新采样,直到生成有效的单词。在后续时间步,将上一个时间步生成的单词作为输入,继续进行Softmax计算并采样,直到达到句子的结尾标志EOS
或满足设定的时间步数。 -
基于词汇的语言模型:字典中的单元是单词。
基于字符的语言模型:字典中的单元是字符,训练数据的单位是字符,而不是单词。
基于词汇的语言模型能够捕捉较长范围的依赖关系,因为它直接在单词级别进行建模,适用于大部分实际应用。
基于字符的语言模型虽然可以处理任何未知的词汇,但训练时需要更多的计算资源,并且生成的句子往往较长,不如基于词汇的模型那样能够捕捉句子内部的长距离依赖。
基于字符的语言模型特别适用于处理含有大量未知词汇的文本,或者需要处理专有名词的领域,例如技术文档、古文等。但无论是基于词汇还是字符的模型,在训练完成后,均可以通过采样生成符合训练语料特征的新文本。例如,基于莎士比亚文本训练的模型生成的句子就具有莎士比亚的风格。
-
RNN具有长期依赖问题。在语言模型中,句子的单复数形式对动词的时态(“was”或“were”)有影响,RNN需要记住句子前半部分的信息来影响后半部分的预测。但是基本的RNN在捕捉长期依赖时表现较差,这是因为RNN在进行反向传播时,梯度很难传递到序列的较前部分。随着时间步长的增加,模型对较早输入的记忆会逐渐丧失。
RNN具有梯度消失问题。在反向传播过程中,梯度随着网络层数增加而变得越来越小,导致较前面的层几乎没有更新,这种现象使得RNN在处理长期依赖关系时非常困难。
RNN具有梯度爆炸问题。在反向传播过程中,梯度的数值增长过大,导致模型参数变得极其大,甚至出现数值溢出,出现NaN。这种情况比较容易检测,因为参数会迅速变得非常大,导致模型计算失败。
梯度爆炸可以通过梯度修剪(gradient clipping)来解决,当梯度超过某个阈值时,进行缩放,确保其不会变得过大。
但梯度消失问题难以解决,需要采用GRU、LSTM等技术来解决。
-
门控循环单元(Gated Recurrent Unit, GRU)的输入包括上一时间步的隐藏状态 \(c^{<t-1>}\) 和当前输入 \(x^{<t>}\)。同时引入记忆细胞 \(c\) 来存储网络的长期记忆(如语法信息、上下文信息),与传统的RNN不同,GRU采用了一个更新机制来选择是否更新记忆,以及重置机制来选择是否遗忘或重置上一时刻的记忆信息。
在GRU中,重置门( \(\Gamma_r\))通过 sigmoid 激活函数计算得出,值在0到1之间,表示当前输入和前一个状态对当前记忆更新的影响程度。重置门控制的是候选记忆值(\(\tilde{c}^{<t>}\))的计算过程。
- \(\Gamma_r = 0\):完全忽略上一时刻的记忆,仅仅依赖当前时刻的输入信息。这种情况通常发生在需要“遗忘”先前记忆时。
- \(\Gamma_r = 1\):保留上一时刻的记忆,完全保留历史信息,不做任何“遗忘”操作。
候选记忆 \({\tilde{c}}^{<t>}\) 是一个临时的记忆值,它通过tanh激活函数来计算,考虑了上一时间步的记忆和当前输入,重置门决定了上一时刻的记忆在候选记忆值中的作用。
\[\Gamma_{r}= \sigma(W_{r}\left\lbrack c^{<t-1>},x^{<t>} \right\rbrack + b_{r}) \\ {\tilde{c}}^{<t>} =tanh(W_{c}\left\lbrack \Gamma_{r} \cdot c^{<t-1>},x^{<t>} \right\rbrack +b_{c}) \]GRU的核心是更新门 \(\Gamma_u\),其值通过sigmoid函数计算得出,范围在0到1之间。这个门决定了是否更新记忆细胞。
- \(\Gamma_u = 1\):完全更新记忆细胞,用新的候选记忆 \({\tilde{c}}^{<t>}\)。
- \(\Gamma_u = 0\):保持记忆不变,直接继承上一时间步的记忆 \(c^{<t-1>}\)。
\[\Gamma_{u}= \sigma(W_{u}\left\lbrack c^{<t-1>},x^{<t>} \right\rbrack +b_{u}) \]最终的记忆细胞 \(c^{<t>}\) 的更新通过以下公式计算:
\[c^{<t>} = \Gamma_u \cdot {\tilde{c}}^{<t>} + (1 - \Gamma_u) \cdot c^{<t-1>} \]这里,\(\Gamma_u\) 决定了是否完全替换记忆,或者保持上一时刻的记忆。它会根据当前时刻的输入和上一时刻的状态来决定多少比例的候选记忆值将替换当前时刻的记忆。
重置门决定是否“遗忘”上一时刻的记忆。通过调整重置门,GRU可以在处理时间序列时决定哪些历史信息需要丢弃,哪些需要保留。它有助于捕捉短期依赖。
更新门决定当前时刻的记忆是从候选记忆中更新,还是从上一时刻的记忆继承。更新门则帮助模型学习长期依赖,并防止过度遗忘重要信息。
-
长短时记忆网络(Long Short-Term Memory Network, LSTM)是一种特殊类型的RNN,通过引入记忆单元来存储长期信息,并使用门控机制来控制信息流动,从而有效捕获长期依赖关系。
- 遗忘门(Forget Gate):决定当前记忆细胞(\(c^{<t-1>}\))的哪些部分应该被遗忘。
- 输入门(Input Gate):决定当前输入(\(x^{<t>}\))以及上一个隐藏状态(\(a^{<t-1>}\))中哪些信息应该被添加到记忆细胞中。
- 输出门(Output Gate):决定记忆细胞中的信息是否应该输出到下一个隐藏状态。
给定当前输入 \(x^{<t>}\) 和上一个时刻的隐藏状态 \(a^{<t-1>}\) ,遗忘门决定上一时刻的记忆细胞 \(c^{<t-1>}\) 有多少比例应该被遗忘。它的输出值 \(\Gamma_f\) 是一个0到1之间的值,表示遗忘的程度:
\[\Gamma_f = \sigma(W_f [a^{<t-1>}, x^{<t>} ] + b_f) \]其中,\(W_f\) 是遗忘门的权重矩阵, \(b_f\) 是偏置项,输出值 \(\Gamma_f\) 决定了当前记忆细胞中哪些信息需要被丢弃。
输入门决定了当前输入 \(x^{<t>}\) 和上一个隐藏状态 \(a^{<t-1>}\) 的哪些部分应该被加入到记忆细胞中。输入门的输出包括两个部分:
- \(\Gamma_i\) :输入门的激活值,用来控制有多少信息可以进入记忆细胞。
- \(\tilde{c}^{<t>}\) :当前输入的候选记忆,表示新的候选信息,它与输入门的输出一起决定了更新记忆细胞的多少信息。
\[\Gamma_i = \sigma(W_i [a^{<t-1>}, x^{<t>} ] + b_i) \\ \tilde{c}^{<t>} = \tanh(W_c [a^{<t-1>}, x^{<t>} ] + b_c) \]更新后的记忆细胞是通过遗忘门和输入门的组合来计算的。它的更新公式为:
\[c^{<t>} = \Gamma_f \cdot c^{<t-1>} + \Gamma_i \cdot \tilde{c}^{<t>} \]- \(\Gamma_f\) 控制上一时刻的记忆细胞 \(c^{<t-1>}\) 保留的部分。
- \(\Gamma_i\) 控制当前候选记忆 \(\tilde{c}^{<t>}\) 进入记忆细胞的比例。
输出门控制记忆细胞中哪些部分的信息应该被输出。输出门的激活值 \(\Gamma_o\) 决定了从记忆细胞中读取的信息,并通过tanh激活函数进行处理,得到当前时刻的隐藏状态 \(a^{<t>}\) :
\[\Gamma_o = \sigma(W_o [a^{<t-1>}, x^{<t>} ] + b_o) \\ a^{<t>} = \Gamma_o \cdot \tanh(c^{<t>}) \]其中, \(\Gamma_o\) 是输出门,控制哪些信息会输出到下一个隐藏状态。最终的隐藏状态 \(a^{<t>}\) 就是基于当前记忆细胞的状态输出。
LSTM与GRU都属于RNN的改进版本,旨在解决长期依赖问题。它们的主要区别在于:
- LSTM使用三个门(遗忘门、输入门、输出门)来控制信息的流动,而GRU只有两个门(更新门和重置门),因此GRU结构相对更简洁,计算量较小。
- LSTM的门控机制更加复杂,但可以提供更高的灵活性和精细的控制,适用于更复杂的任务。
-
在传统的单向RNN中,信息的传播是按时间顺序从前往后的(从 \(t=1\) 到 \(t=T\) )。在某些任务中(例如命名实体识别),某个词的预测不仅仅依赖于前面的词,还可能依赖于后面的词。
单向RNN只能获取前文的信息,而不能考虑未来的信息,因此存在预测准确性上的局限。
双向BiRNN通过在每个时刻同时考虑正向(过去的信息)和反向(未来的信息)的输入序列,弥补了单向RNN的不足,能够对当前时刻的预测综合考虑上下文信息。
对于输入序列 \(x^{<1>}\) 到 \(x^{<T>}\),通过一个前向循环单元(例如LSTM或GRU)逐步处理序列,计算出每个时刻的状态激活值 \(\overrightarrow{a}^{<t>}\) ,并输出预测结果 \(\hat{y}^{<t>} = g(W_g [\overrightarrow{a}^{<t>} , \overleftarrow{a}^{<t>} ] + b_y)\) 。
除了正向的传播,双向RNN还包括反向传播的循环单元。这些反向单元处理的是输入序列的倒序,从 \(t=T\) 开始向前计算反向状态 \(\overleftarrow{a}^{<t>}\) 。这两个方向的计算结果会结合,最终生成每个时刻的预测。
对于某个时刻 \(t\) 的预测 \(\hat{y}^{<t>}\), 它既考虑了前向的序列信息 \(x^{<1>}\) 到 \(x^{<t>}\),也考虑了反向的序列信息 \(x^{<T>}\) 到 \(x^{<t+1>}\)。这种双向的信息流使得模型可以利用上下文的所有信息。
由于双向RNN需要同时处理正向和反向的序列信息,这意味着它必须看到整个序列后才能进行预测。因此,模型在实际应用中对实时序列处理存在困难,尤其是在语音识别等应用中,无法像传统的单向模型那样即时输出结果。
-
要学习非常复杂的函数,可以把RNN的多个层堆叠在一起构建更深的模型,增加层数通常可以使网络学习更加复杂的特征。
深层RNN在标准的RNN模型中堆叠多个循环层,每一层的激活值用 \(a^{\lbrack l\rbrack <t>}\) 表示,其中 \(l\) 表示层数, \(t\) 表示时间步。每一层的激活值依赖于上一层的输出和当前输入。例如,第二层的激活值 \(a^{\lbrack 2\rbrack <3>}\) 是由第一层的激活值 \(a^{\lbrack 1\rbrack <3>}\) 和第二层上一时间步的激活值 \(a^{\lbrack 2\rbrack <2>}\) 共同决定。不同层之间的激活值通过前一层的输出传递,形成纵向的连接。而每一层的计算是基于前一层的激活值与当前时刻的输入。
深层RNN会对时间序列的长远依赖进行建模,处理复杂的序列数据。它们适用于大多数时间序列任务,但由于计算量大、训练慢,因此超过三层的深层RNN网络不常见。