注意:本文引用自专业人工智能社区Venus AI
更多AI知识请参考原站 ([www.aideeplearning.cn])
引言
在之前的博文中,我们探讨了全连接神经网络(FCNN)和卷积神经网络(CNN)的结构,以及它们的训练方法和使用场景。值得注意的是,这两种网络结构都是处理独立的输入数据,即它们无法记忆或理解输入数据之间的序列关系——每个输入都被视为与其他输入无关的独立单元。但是,某些任务需要能够更好的处理序列的信息,即前面的输入和后面的输入是有关系的。比如,当我们在理解一句话意思时,孤立的理解这句话的每个词是不够的,而是需要处理这些词连接起来的整个序列;在处理视频的时候,也不能只单独的去分析每一帧,而要分析这些帧连接起来的整个序列。像文本、语言、视频这种信息被称为序列数据,解决序列数据相关的问题,就需要用到深度学习领域中另一类非常重要神经网络:循环神经网络(Recurrent Neural Network,RNN)。
序列数据
序列数据是由一系列有序的元素组成,这些元素按照一定的顺序排列。序列数据的顺序通常包含重要的信息。在深度学习和自然语言处理中,常见的序列数据包括时间序列数据、文本数据、音频数据和视频数据等。
时间序列数据:按照时间顺序记录的数据,例如股票价格、气象数据等。
文本数据:由一系列字符、单词或句子组成,例如新闻文章、书籍等。
音频数据:由一系列音频信号样本组成的数据,例如语音、音乐等。
视频数据:由一系列图像帧组成的数据,例如电影、动画等。
上述的四种序列数据其实存在一个共性,即都存在时间概念。时间序列数据本身就有时间信息,文本数据和音频数据的产生也有时间顺序,视频数据的播放也存在时间概念。所以可以对序列数据某时刻的信息用以下数学公式进行定义:
如果在时间 下观察到数据, 那么预测时刻某数据出现的概率应该与时刻和之前时刻出现的所有信息有关, 即 。我们希望能够对进行建模,这样就可以根据现有的数据, 来预测未来的数据。我们可以使用条件概率对其进行展开, 即:
从上述公式中, 我们可以发现一个明显的问题, 就是 t 是动态的, 且当 t 变的很大时, 的计算将变的非常复杂。有两个方案可以解决这个问题, 一个是基于马尔可夫假设, 另一个是基于设计潜变量的思想。
马尔可夫假设是一个关于序列数据概率建模的假设, 其基本思想是: 当前状态仅依赖于有限个前驱状态, 而与更早的状态无关。在许多实际应用中, 基于马尔可夫假设的模型能够在计算复杂性和建模精度之间找到一个平衡。以一阶马尔可夫链为例, 其假设当前状态仅依赖于它的前一个状态, 数学表达如下:
其中, xt 表示序列中的第 t 个状态。推广下去, n 阶马尔可夫链的表示如下:
通过上述公式, 我们把动态的 t 变成了一个静态的超参数 n, 这时 就可以通过神经网络的方法进行拟合。具体来说, 可以设计一个接受 n 维信息作为输入的 MLP, 这个 MLP 的输出层只有一个节点, 这个节点输出的标量即为预测的 。然后通过有监督训练, 去拟合模型中的参数即可。
第二个解决方案是通过设计潜变量的思想。潜变量 (latent variable) 是一种特殊的变量。在概率图模型中, 它们用于表示未观察到的随机变量。通过这种方式, 潜变量可以帮助捕捉数据中的隐藏结构。在序列信息建模中,设计潜变量有助于理解序列中的抽象特征和关系。例如, 可以设计长度为 n维的潜变量 W, 用这个 W 来代表 的信息, 此时 就变成了 。由于 W 的维度是固定的, 因此问题就变得可解了。当然, 关键是如何求得一个这样的 W 。实际上, RNN 就是基于隐变量求解的过程, 算法中使用神经网络层来计算得到 W, 接下来对 RNN 进行详细介绍。
RNN模型
循环神经网络的简单表达如图1所示。但是因为抽象了时间这个序列概念,所以看上去不是特别的直观,接下来会对其进行一系列拆解讲解。
如果把图1中有 W 的那个带箭头的圈去掉, 它就变成了最普通的全连接神经网络。 x是一个向量, 它表示输入层的值(这里把输入向量抽象成了一个点 x ); s 也是一个向量,它表示隐藏层的值(这里把隐藏层向量也抽象成了一个点 s ); U 是输入层到隐藏层的权重矩阵; V 是隐藏层到输出层的权重矩阵。最后, o 也是一个向量, 它表示输出层的值, 如图2所示。
那么,现在我们来看看 w 是什么。如果把图1按训练时间的维度展开,循环神经网络如图3所示。
当加上 W 之后, 在训练的 t 时刻, 循环神经网络隐藏层的输入信息不仅仅取决于当前时刻 t 的输入, 还取决于上一次隐藏层的计算结果。
为了更清晰的表示 RNN 的结构, 对其进一步的可视化, 如图4所示。图4描述的是RNN模型在四个训练时刻下的状态, 其中贯穿四个时刻模型的直线就是上面解释的 W 。实际上, W 的含义就是在训练的 T 时刻, 循环神经网络隐藏层的计算结果 不仅仅作为当前时刻的输出, 也作为 T+1 时刻模型的输入。
以下是一个简单的 RNN 模型的数学公式描述:
对于时间步 t, 输入向量为 隐状态向量为 , 输出向量是输入权重矩阵, 是隐状态权重矩阵, 是输出权重矩阵, 是偏置向量。
RNN 的更新公式为:
其中 σ 是激活函数, 通常为 tanh 或 ReLU 激活函数。
在每个时间步, RNN 的隐状态通过输入 和上一个时间步的隐状态计算得到。 可以被视为存储了过去信息的状态向量。通过将隐状态 作为输入传递给输出层, 可以预测当前时间步的输出 。
在训练过程中, 我们使用交叉熵损失函数来计算模型预测与实际标签之间的差异, 并使用反向传播算法更新模型参数 和 以最小化损失函数。
进一步思考一下, 如果不考虑 , 即让 , 那么 RNN 就退化成了一个朴素的神经网络模型。就是循环神经网络和神经网络之间的区别, 也是之前讲解的潜变量的实现。
语言模型
RNN是在自然语言处理领域中最先被用起来的,比如,RNN可以为语言模型建模。语言模型(Language Model,LM)是自然语言处理领域中的一种重要模型。它的主要任务是学习和预测语言的结构和规律,评估文本序列(如单词、字符或其他符号)的概率分布。简单来说,语言模型就是用来理解、生成和评估自然语言的一种数学模型。下面介绍一个非常经典语言模型“预测下一个词”。
举例来说,我们写出一个句子前面的一些词,然后,让模型帮忙写接下来的一个词。比如:我昨天上学迟到了,老师批评了____。
在这个例子中,接下来的这个词最有可能是“我”,而不太可能是“小明”,更不可能是“吃饭”。如何让模型预测出“我”呢?下面分别用NN和RNN两种模型尝试解决这个问题,如图5所示。用NN解决此问题如图5(a)所示,用RNN解决此问题如图5(b)所示。
以“The most common example of learning a programming language is the output :hello world!”这句话为例,当以神经网络的方式进行预测时,在 t 时刻输入“hello”,我们希望模型能预测出单词“world”;在 t+1 时刻输入 “world”时,希望模型能预测出“!”。这种方法看上去好像很合理,但是几乎不可行。原因很简单,思考一个问题,如果输入模型的数据有且仅有“hello”这一个信息的时候,模型能输出“world”的可能性微乎其微,因为语言的特点是只有结合上下文信息才能做出准确的预测。例如,如果模型知道“hello”这个单词前面还有“The most common example of learning a programming language is the output :”,那么模型预测出单词“world”的概率就会更大。因此,我们可以看出以神经网络的方式并不能抓取语言中的上下文关系,这也是使用神经网络来处理文本数据效果不好的根本原因。
RNN模型就可以很好的解决这个问题。举个例子,在 t+1 时刻模型接受的输入不仅仅有“world”这个单词,还有一个“Hidden status”,也就是我们之前多次提到的“W”。这个“Hidden status”其实是时刻模型隐藏层的计算结果,这个计算结果来源于 t 时刻的输入“Hello”,某种程度上可以代表之前时刻的文本信息。因此,这意味着我们在预测当前时刻的单词时,不仅仅能观察到当前时刻的输入信息,还可以观察到之前的语境,结合起来进行预测,其预测准确率会有很大的提升。从某种程度上说,可以把这个“Hidden status W”看作是一个“记忆单元”的概念,它存储的就是网络之前时刻处理过的文本信息,即上文的语境信息。下面我们详细讲解一下这个经典语言模型预测下一个词的实现流程。
文本预处理
在使用循环神经网络解决自然语言处理问题之前,通常需要对文本数据进行一定的预处理。预处理是将原始文本数据转换为适合机器学习模型输入的格式的过程。以下是一些常见的文本预处理步骤。
1.分词
分词(Tokenization)是将文本拆分为更小的单元(如单词、短语或字符)的过程。对于英文文本,可以根据空格和标点符号进行分词;对于中文文本,由于词之间没有明显的分隔符,可能需要采用分词工具进行处理。分词后的文本通常以单词序列(token sequence)的形式表示,可以将“token”翻译成“词元”。我们需要根据不同的任务类型和文本特性来选择不同的token方法,以下是一些将文本转换为 token 的方法示例:
(1)将句子拆分成单词:“The quick brown fox jumped over the lazy dog” →[“The”, “quick”, “brown”, “fox”, “jumped”, “over”, “the”, “lazy”, “dog”]
(2)将句子拆分成单词和标点符号: “Hello, how are you?” →[“Hello”, “,”, “how”, “are”, “you”, “?”]
(3)将句子拆分成短语或词组:“I love ice cream” →[“I”, “love”, “ice cream”]
(4)将代码转换为token: “print(‘Hello, World!’)” →[“print”, “(”, “‘Hello, World!’”, “)”, “”]
(5)将 DNA 序列转换为token: “ATCGATCGATCG” →[“ATCG”, “ATCG”, “ATCG”]
(6)将句子拆分成字符串:“Hello, world” → [“H”, “e”, “l”, “l”, “o”, “w”, “o”, “r”, “l”, “d”]
2.清洗
清洗(Cleaning)指移除文本中的特殊字符、数字、标点符号和其他无关信息,使文本更加整洁。在某些情况下,可能还需要删除停用词(stop words)。停用词是自然语言处理中一类常见的词汇,通常是指那些出现频率较高但在语境中没有实际含义或作用的常用词汇,例如 “a”, “an”, “the”, “and”, “of” 等。在文本分析或信息检索任务中,这些常用词汇往往会对文本处理的结果产生负面影响,因为它们可能会占据大量的计算资源、存储空间和时间,并且无法提供有意义的信息。
3.词干提取/词形还原
词干提取/词形还原(Stemming/Lemmatization)指将单词转换为其词干(stem)或词元(lemma)形式,以减少词汇的多样性并提高模型的泛化能力。词干提取通常基于规则,可能导致非标准化的输出;词形还原需要词汇和语法知识,输出通常更加规范。
4.序列填充/截断
RNN本身可以处理可变长度的输入序列。RNN通过在序列的每个时间步上进行循环计算,并将前一个时间步的隐藏状态作为后一个时间步的输入,从而处理不同长度的序列。然而,在实际应用中,为了提高计算效率和便于在GPU上进行并行计算,通常会将多个输入序列组合成一个批次(batch)。为了将不同长度的序列放入一个批次,我们需要将它们统一为相同的长度。这可以通过填充(padding)或截断(truncating)来实现。
填充是在较短的序列后面添加特殊的填充符号(例如0或特殊的标记),使得所有序列具有相同的长度。截断是将较长的序列缩短到指定的最大长度。在处理填充后的序列时,可以使用掩码(masking)技术来确保填充符号不会影响计算结果。因此,虽然RNN本身可以处理可变长度的输入序列,但出于计算效率和实际操作的考虑,通常需要将输入序列统一为固定长度。
5.词嵌入/词向量表示
词嵌入/词向量表示(Word Embeddings/Word Vectors)指将文本中的单词转换为向量形式,以便于神经网络进行处理。常见的词向量表示方法包括词袋模型(Bag-of-Words,BoW)、独热编码、TF-IDF、Word2Vec、GloVe等。这些方法可以将单词映射到固定长度的连续向量空间,捕捉单词之间的语义关系。词的向量化是数据预处理中非常重要的一个环节。
6.词汇表
为了将文本信息送入RNN中进行处理,我们需要将文本信息进行词嵌入。同时,这也意味着模型的输入、中间结果、输出等信息也全部变成了词向量。考虑NLP中的任务,我们希望模型最终的输出是文本,而不是词向量,因为我们想要直接理解词向量的表示是非常困难的。所有这里需要一种词向量和文本信息的对应关系,称为词汇表(vocabulary)。词汇表是一个包含在训练数据中出现的所有不同单词(或称为词汇、token)的集合。词汇表是文本数据预处理过程的一个重要组成部分,它将文本中的单词与唯一索引关联起来,从而将文本转换为计算机可以处理的数值形式。
词汇表的构建通常包括以下几个步骤:
(1)分词(Tokenization):将文本数据拆分为单词序列。在英文文本中,这通常意味着根据空格和标点符号进行分词;在中文文本中,可能需要使用分词工具来完成。
(2)统计词频:计算训练数据中每个单词的出现频率。有助于识别高频词、低频词以及停用词。
(3)限制词汇表大小:为了限制模型的计算复杂度,可以选择一个最大词汇表大小,并根据单词出现的频率保留最常见的单词。对于不在词汇表中的单词,可以使用一个特殊的未知(unknown)标记(如“<UNK> ”)来表示。
(4)分配索引:为词汇表中的每个单词分配一个唯一的整数索引。通常,还需要为特殊标记(如填充符“<PAD>”、句子开始符“<SOS>”、句子结束符“<EOS>”和未知符“<UNK>”)分配索引。
构建词汇表后,可以将原始文本数据转换为数值表示。通常包括将单词替换为其对应的整数索引,以及将整数索引序列转换为词向量表示(如one-hot编码、词嵌入等)。此外,词汇表还可以用于将模型预测结果(通常是整数索引)转换回文本形式。
建模和预测
在对文本数据进行预处理之后,我们需要构建训练数据。针对“预测下一个词”这个任务,我们将使用有监督训练的方式对模型进行训练,因此需要将文本划分为输入序列和目标序列。输入序列是原始文本中的一个词窗口(例如,从第一个词到第 n 个词),目标序列是紧随输入序列之后的词(即从第二个词到第 n+1 个词)。对整个文本重复此操作,直到将所有可能的词窗口作为输入序列处理完毕。
接下来的任务是构建RNN模型。RNN模型的本质实际上就是一个MLP,包括输入层、隐藏层和输出层。输入层负责接收词向量序列,隐藏层用于处理序列中的时序信息,输出层负责生成预测结果。输出层的大小通常设置为词汇表的大小,并使用softmax激活函数,以便将输出转换为各个词的概率分布。
下一步是使用训练数据训练RNN模型。将输入序列传入RNN,然后将RNN的输出与目标序列进行比较。通常使用交叉熵损失作为损失函数,以衡量模型预测的概率分布与实际目标的差异。通过优化算法(如梯度下降或Adam)最小化损失函数,更新模型的参数。
最后,在预测阶段,给定一个输入序列,将其传入训练好的RNN模型。模型会输出一个概率分布,表示各个词作为下一个词的概率。选择概率最高的词作为预测结果。如果需要生成一个完整的文本序列,可以将预测结果添加到输入序列的末尾,并重复这个过程,直到达到所需的序列长度或遇到特定的结束标记。
循环神经网络的变体模型
RNN 作为一种能够处理序列数据的神经网络模型,其经典变体模型有以下几种:
(1)GRU(Gated Recurrent Unit):GRU 是一种与 LSTM 类似的 RNN 变体模型,它只引入了两个门控机制(更新门、重置门),可以在一定程度上解决梯度消失和梯度爆炸问题,同时具有更少的参数。
(2)LSTM(Long Short-Term Memory):LSTM 是一种特殊的 RNN,它通过引入三个门控机制(输入门、遗忘门、输出门)来控制信息的流动,从而解决了传统 RNN 在处理长序列数据时出现的梯度消失或梯度爆炸问题。
(3)Bi-RNN(Bidirectional Recurrent Neural Network):Bi-RNN 是一种能够同时考虑过去和未来的信息的 RNN 变体模型,它由两个RNN模块组成,一个是正向RNN,另一个是反向RNN,最终输出是正向和反向RNN输出的拼接。
(4)Deep RNN(Deep Recurrent Neural Network):Deep RNN是一种增加多个循环层来实现深度学习的RNN变体模型,它能够更好地捕捉序列数据中的长期依赖关系。
(5)Attention-Based RNN:Attention-Based RNN 是一种基于注意力机制的RNN变体模型,它通过引入注意力机制来对序列中的不同部分进行加权,从而能够更好地捕捉序列数据中的重要信息。