本专栏深入探究从循环神经网络(RNN)到Transformer等自然语言处理(NLP)模型的架构,以及基于这些模型构建的应用程序。
本系列文章内容:
- NLP自然语言处理基础
- 词嵌入(Word Embeddings)
- 循环神经网络(RNN)、长短期记忆网络(LSTM)和门控循环单元(GRU)
3.1 循环神经网络(RNN)
3.2 长短期记忆网络(LSTM)
3.3 门控循环单元(GRU) - 编码器-解码器架构(Encoder-Decoder Architecture)(本文)
- 注意力机制(Attention Mechanism)
- Transformer
- 编写Transformer代码
- 双向编码器表征来自Transformer(BERT)
- 生成式预训练Transformer(GPT)
- 大语言模型(LLama)
- Mistral
1. 简介
自然语言处理(Natural Language Processing,NLP)的快速发展以诸多重要的里程碑为标志,其中没有什么比大语言模型(Large Language Models,LLMs)的出现更具变革性了。这些模型重新定义了机器理解和生成人类语言的可能性边界。许多大语言模型成功的核心是编码器-解码器架构,这一框架在机器翻译、文本摘要和对话式人工智能等任务中实现了突破。
编码器-解码器架构的引入是为了解决序列到序列(Sequence-to-Sequence,Seq2Seq)问题,这标志着在处理序列数据方面的重大突破。
1.1 数据处理方面的主要发展
- 表格数据:最初,重点是利用人工神经网络(Artificial Neural Networks,ANNs)来处理表格数据。这种方法通过增加层数演变成了深度神经网络(Deep Neural Networks,DNNs),从而增强了模型捕捉数据中复杂模式的能力。
- 图像数据:在目标识别等任务中,例如判断一张图像中是有一只狗还是一只猫,图像作为二维数据网格。人工神经网络在处理这种类型的结构化数据方面并不是特别有效。这一限制促使了卷积神经网络(Convolutional Neural Networks,CNNs)的发展,卷积神经网络是专门为解释和分析网格格式的视觉信息而设计的。
- 序列数据:像文本或时间序列这样的序列数据具有时间依赖性和有序性。人工神经网络和卷积神经网络不太适合处理此类数据,因为它们缺乏捕捉序列关系的机制。这一空白由循环神经网络(Recurrent Neural Networks,RNNs)及其高级变体——长短期记忆(Long Short-Term Memory,LSTM)网络和门控循环单元(Gated Recurrent Units,GRUs)填补,它们能够对时间模式进行建模并从中学习。
- 序列到序列数据:在某些应用中,输入和输出都是序列,就像在机器翻译任务中看到的那样。传统模型在处理这种类型的数据时会遇到困难,因为在对齐可变长度的输入和输出序列时存在复杂性。这一挑战需要开发能够有效处理序列到序列数据的专门架构,为自然语言处理中更复杂的模型铺平了道路。
本文的重点是处理序列到序列(Seq2Seq)数据问题。
2. 理解序列建模
序列建模的应用场景涉及输入、输出或两者都由数据序列(如单词或字母)组成的问题。
考虑一个非常简单的预测电影评论是正面还是负面的问题。这里我们的输入是一个单词序列,输出是一个介于0和1之间的单个数字。如果我们使用传统的深度神经网络,那么我们通常必须使用词袋模型(Bag of Words,BOW)、词向量(Word2Vec)等技术将输入文本编码为固定长度的向量。但请注意,这里单词的顺序没有被保留,因此当我们将输入向量输入到模型中时,模型不知道单词的顺序,从而遗漏了关于输入的一个非常重要的信息。
因此,为了解决这个问题,循环神经网络(RNNs)应运而生。本质上,对于任何具有可变数量特征的输入X = (x₀, x₁, x₂, … xₜ),在每个时间步,一个循环神经网络单元将一个元素(或token)xₜ作为输入,并产生一个输出hₜ,同时将一些信息传递到下一个时间步。这些输出可以根据手头的问题来使用。
电影评论预测问题是一个非常基本的序列问题的例子,称为多对一预测。对于不同类型的序列问题,会使用这种循环神经网络架构的修改版本。
序列问题可以大致分为以下几类:
每个矩形是一个向量,箭头表示函数(例如矩阵乘法)。输入向量是红色的,输出向量是蓝色的,绿色向量保存循环神经网络的状态(很快会详细介绍)。
从左到右:
(1)没有循环神经网络的普通处理模式,从固定大小的输入到固定大小的输出(例如图像分类)。
(2)序列输出(例如图像字幕生成,输入一张图像并输出一个单词句子)。
(3)序列输入(例如情感分析,对给定的句子进行分类,判断其表达的是正面还是负面情感)。
(4)序列输入和序列输出(例如机器翻译:一个循环神经网络读取一个英语句子,然后输出一个法语句子)。
(5)同步的序列输入和输出(例如视频分类,我们希望标记视频的每一帧)。
请注意,在每种情况下,序列的长度都没有预先指定的限制,因为循环变换(绿色部分)是固定的,并且可以根据需要应用多次。
2.1 利用编码器-解码器架构掌握序列数据
序列在我们的世界中无处不在——在语言、语音、金融时间序列以及基因组数据中都能找到,在这些领域中元素的顺序至关重要。与固定大小的数据不同,序列在理解、预测和生成信息方面带来了独特的挑战。传统的深度神经网络(Deep Neural Networks,DNNs)在处理具有固定维度输入和输出的任务时表现出色,但在诸如机器翻译这样的序列到序列任务中却面临困难,因为在机器翻译中,输入和输出的长度是可变的,并且没有对齐。因此,需要专门的模型来有效地处理序列数据。
研究论文:《使用神经网络进行序列到序列学习》(Sequence to Sequence Learning with Neural
Networks)
动机
编码器-解码器架构相对较新,在2016年底被谷歌翻译服务采用作为核心技术。它构成了诸如注意力模型(Attention models)、GPT模型、Transformer模型和BERT等先进的序列到序列模型的基础。因此,在深入研究更高级的机制之前,理解这一架构至关重要。
关键组件
- 编码器:编码器处理输入序列,并将信息编码为固定长度的上下文向量(或向量序列)。这种编码捕捉了输入数据的本质,对其信息内容进行了总结。
- 解码器:解码器接收编码器提供的上下文向量生成输出序列,期间一次生成一个元素。它利用上下文向量中的信息来生成与输入相关且连贯的输出。
3. 编码器-解码器架构
3.1 神经机器翻译问题
为了说明这个概念,让我们以神经机器翻译(Neural Machine Translation,NMT)为例进行讲解。在神经机器翻译中,输入是一个逐个处理的单词序列,输出是相应的单词序列。
任务:预测一个英语句子的法语翻译。
示例:
输入:英语句子:“nice to meet you”
输出:法语翻译:“ravi de vous rencontrer”
使用的术语:
- 输入句子“nice to meet you”将被称为X或输入序列。
- 输出句子“ravi de vous rencontrer”被称为Y_true或目标序列,这是我们希望模型预测的真实结果。
- 模型预测的句子是Y_pred,也称为预测序列。
- 英语和法语句子中的每个单词都被称为一个标记(token)。
因此,给定输入序列“nice to meet you”,模型的目标是预测目标序列Y_true,即“ravi de vous rencontrer”。
3.2 概述
从很高的层面来看,编码器-解码器模型可以被看作是两个模块,即编码器和解码器,它们由一个我们称之为“上下文向量”的向量连接起来。
- 编码器:编码器处理输入序列中的每个标记。它试图将关于输入序列的所有信息压缩到一个固定长度的向量中,也就是“上下文向量”。在处理完所有标记后,编码器将这个向量传递给解码器。
- 上下文向量:构建这个向量的方式是期望它封装输入序列的全部含义,并帮助解码器做出准确的预测。我们稍后会看到,这是编码器模块的最终内部状态。
- 解码器:解码器读取上下文向量,并试图逐个标记地预测目标序列。
3.3 内部机制
序列到序列(Seq2Seq)模型是一种基于循环神经网络(RNN)的模型,专为翻译和摘要等任务而设计,在这些任务中,输入是一个序列,输出也是一个序列。
这是一个将英语句子“I am a student.”翻译成法语“Je suis étudiant.”的序列到序列(Seq2Seq)模型。左边的橙色矩形代表编码器,右边的绿色矩形代表解码器。编码器接收输入句子(“I am a student.”)并输出一个上下文向量,而解码器将上下文向量(以及标记)作为输入,并输出句子(“Je suis étudiant.”)。
就架构而言,它相当简单明了。这个模型可以被看作是两个之间有某种连接的长短期记忆(LSTM)单元。这里的关键在于我们如何处理输入和输出。我将逐一解释每个部分。
3.4 编码器模块
编码器部分是一个长短期记忆(LSTM)单元。随着时间推移,它接收输入序列,并试图封装输入序列的所有信息,并将其存储在最终的内部状态hₜ(隐藏状态)和cₜ(细胞状态)中。然后,这些内部状态被传递到解码器部分,解码器将利用这些状态来尝试生成目标序列。这就是我们之前提到的“上下文向量”。
编码器部分在每个时间步的输出都被丢弃。
注意:上面的图表是当我们在时间轴上展开长短期记忆(LSTM)/门控循环单元(GRU)单元时的样子。也就是说,它是单个的长短期记忆(LSTM)/门控循环单元(GRU)单元,在每个时间戳接收单个单词/标记。
在论文中,他们使用了长短期记忆(LSTM)单元而不是传统的循环神经网络(RNN),因为长短期记忆(LSTM)在处理长期依赖关系方面表现更好。
3.4.1 数学基础:编码器
给定一个输入序列:
编码器按顺序处理每个元素:
初始化:编码器的初始隐藏状态 h 0 h_0 h0通常初始化为零或通过学习得到的参数。
隐藏状态更新:对于输入序列中的每个时间步 t t t:
其中:
- h t h_t ht是时间步 t t t的隐藏状态。
- f e n c f_{enc} fenc是编码器的激活函数(例如,长短期记忆(LSTM)单元或门控循环单元(GRU)单元)。
上下文向量:在处理完整个输入序列后,最终的隐藏状态 h T x h_{T_x} hTx成为上下文向量 c c c:
3.5 解码器模块
在读取完整个输入序列后,编码器将内部状态传递给解码器,从这里开始预测输出序列。
解码器模块也是一个长短期记忆(LSTM)单元。这里需要注意的主要一点是,解码器的初始状态 ( h 0 , c 0 ) (h_0, c_0) (h0,c0)被设置为编码器的最终状态 ( h t , c t ) (h_t, c_t) (ht,ct)。这些状态充当“上下文”向量,帮助解码器生成期望的目标序列。
现在来解释解码器的工作方式,它在任何时间步 t t t的输出应该是目标序列/ Y t r u e Y_{true} Ytrue(“ravi de vous rencontrer”)中的第 t t t个单词。为了解释这一点,让我们看看每个时间步会发生什么。
在时间步1
在第一个时间步输入到解码器的是一个特殊符号<START>
,这用于表示输出序列的开始。现在解码器使用这个输入以及内部状态 ( h t , c t ) (h_t, c_t) (ht,ct)来生成第一个时间步的输出,该输出应该是目标序列中的第一个单词/标记,即“ravi”。
在时间步2
在时间步2,第一个时间步的输出“ravi”被作为输入送到第二个时间步。第二个时间步的输出应该是目标序列中的第二个单词,即“de”。
类似地,每个时间步的输出都被作为输入送到下一个时间步。这个过程一直持续,直到我们得到<END>
符号,这同样是一个用于标记输出序列结束的特殊符号。解码器的最终内部状态将被丢弃。
注意,这些特殊符号不一定只能是
<START>
和<END>
。只要这些符号不在我们的数据语料库中,它们可以是任何字符串,这样模型就不会将它们与其他任何单词混淆。在论文中,他们使用了符号<EOS>
,并且方式略有不同。我稍后会再详细介绍这一点。
上述过程是理想情况下解码器在测试阶段的工作方式。但在训练阶段,需要稍微不同的实现方式,以便更快地进行训练。我将在下一部分对此进行解释。
3.5.1 数学基础:解码器
解码器生成一个输出序列:
使用上下文向量 c c c:
初始化:解码器的初始隐藏状态 s 0 s_0 s0被设置为上下文向量:
输出生成:对于输出序列中的每个时间步 t t t:
并且,
其中:
- s t s_t st是时间步 t t t时解码器的隐藏状态。
- f d e c f_{dec} fdec是解码器的激活函数。
- y t − 1 y_{t - 1} yt−1是之前生成的输出( y 0 y_0 y0是一个序列开始标记)。
- W W W是输出层的权重矩阵。
- p ( y t ∣ y < t , X ) p(y_t | y_{<t}, X) p(yt∣y<t,X)是在时间步 t t t时可能输出的概率分布。
4. 训练编码器-解码器模型
4.1 数据向量化
在深入了解其细节之前,我们首先需要对数据进行向量化处理。
我们拥有的原始数据是:
X = “nice to meet you” → Y_true = “ravi de vous rencontrer”
现在我们在目标序列的开头和结尾分别添加特殊符号<START>
和<END>
:
X = “nice to meet you” → Y_true = “<START> ravi de vous rencontrer <END>”
接下来,使用独热编码(one-hot-encoding,ohe)对输入和输出数据进行向量化。设输入和输出表示为:
X = (x1, x2, x3, x4) → Y_true = (y0_true, y1_true, y2_true, y3_true, y4_true, y5_true)
其中 x i x_i xi和 y i y_i yi分别表示输入序列和输出序列的独热编码向量。它们可以表示为:
对于输入X
‘nice’ → x1 : [1 0 0 0]
‘to’ → x2 : [0 1 0 0 ]
‘meet’ →x3 : [0 0 1 0]
‘you’ → x4 : [0 0 0 1]
对于输出 Y t r u e Y_{true} Ytrue
‘<START>’ → y0_true : [1 0 0 0 0 0]
‘ravi’ → y1_true : [0 1 0 0 0 0]
‘de’ → y2_true : [0 0 1 0 0 0]
‘vous’ → y3_true : [0 0 0 1 0 0]
‘rencontrer’ → y4_true : [0 0 0 0 1 0]
‘<END>’ → y5_true : [0 0 0 0 0 1]
注意:我使用这种表示方式是为了更方便地解释。“真实序列”和“目标序列”这两个术语都用于指代我们希望模型学习的同一个句子“ravi de vous rencontrer”。
4.2 编码器的训练与测试
编码器在训练阶段和测试阶段的工作方式是相同的。它逐个接收输入序列的每个标记/单词,并将最终状态发送给解码器。随着时间的推移,其参数通过反向传播进行更新。
4.3 训练阶段的解码器:教师强制法
与编码器部分不同,解码器在训练阶段和测试阶段的工作方式是不一样的。因此,我们将分别来看这两个阶段。
为了训练我们的解码器模型,我们使用一种称为“教师强制法”的技术。在这种技术中,我们将上一个时间步的真实输出/标记(而不是预测的输出/标记)作为当前时间步的输入。
为了解释这一点,让我们来看一下训练的第一次迭代。在这里,我们将输入序列输入到编码器中,编码器对其进行处理,并将其最终的内部状态传递给解码器。现在看解码器部分,参考下面的图表。
在继续之前,请注意在解码器中,在任何时间步 t t t,输出 y t _ p r e d y_{t\_pred} yt_pred是输出数据集中整个词汇表上的概率分布,它是通过使用Softmax激活函数生成的。具有最大概率的标记被选作预测的单词。
例如,参考上面的图表, y 1 _ p r e d = [ 0.02 0.12 0.36 0.1 0.3 0.1 ] y_{1\_pred} = [0.02\ 0.12\ 0.36\ 0.1\ 0.3\ 0.1] y1_pred=[0.02 0.12 0.36 0.1 0.3 0.1]告诉我们,我们的模型认为输出序列中的第一个标记是<START>
的概率是0.02,是“ravi”的概率是0.12,是“de”的概率是0.36,以此类推。我们将预测的单词取为概率最高的那个。因此,这里预测的单词/标记是“de”,概率为0.36。
在时间步1
单词<START>
对应的向量 [ 1 0 0 0 0 0 ] [1\ 0\ 0\ 0\ 0\ 0] [1 0 0 0 0 0]被作为输入向量输入。现在,我希望我的模型将输出预测为 y 1 _ t r u e = [ 0 1 0 0 0 0 ] y_{1\_true}=[0\ 1\ 0\ 0\ 0\ 0] y1_true=[0 1 0 0 0 0],但由于我的模型刚刚开始训练,它会输出一些随机的值。设时间步1的预测值为 y 1 _ p r e d = [ 0.02 0.12 0.36 0.1 0.3 0.1 ] y_{1\_pred}=[0.02\ 0.12\ 0.36\ 0.1\ 0.3\ 0.1] y1_pred=[0.02 0.12 0.36 0.1 0.3 0.1],这意味着它预测第一个标记是“de”。现在,我们应该将这个 y 1 _ p r e d y_{1\_pred} y1_pred作为时间步2的输入吗?我们可以这么做,但在实践中发现,这会导致一些问题,比如收敛速度慢、模型不稳定以及性能不佳,仔细想想这是很合理的。
因此,引入了教师强制法来纠正这个问题。在教师强制法中,我们将上一个时间步的真实输出/标记(而不是预测的输出)作为当前时间步的输入。这意味着时间步2的输入将是 y 1 _ t r u e = [ 0 1 0 0 0 0 ] y_{1\_true}=[0\ 1\ 0\ 0\ 0\ 0] y1_true=[0 1 0 0 0 0],而不是 y 1 _ p r e d y_{1\_pred} y1_pred。
现在时间步2的输出将是某个随机向量 y 2 _ p r e d y_{2\_pred} y2_pred。但在时间步3,我们将使用 y 2 _ t r u e = [ 0 0 1 0 0 0 ] y_{2\_true}=[0\ 0\ 1\ 0\ 0\ 0] y2_true=[0 0 1 0 0 0]作为输入,而不是 y 2 _ p r e d y_{2\_pred} y2_pred。类似地,在每个时间步,我们都将使用上一个时间步的真实输出。
最后,根据每个时间步的预测输出计算损失,并通过时间反向传播误差来更新模型的参数。使用的损失函数是目标序列/ Y t r u e Y_{true} Ytrue和预测序列/ Y p r e d Y_{pred} Ypred之间的分类交叉熵损失函数,即:
Y_true = [y0_true, y1_true, y2_true, y3_true, y4_true, y5_true]
Y_pred = [‘<START>’, y1_pred, y2_pred, y3_pred, y4_pred, y5_pred]
解码器的最终状态将被丢弃。
4.4 测试阶段的解码器
在实际应用中,我们不会有 Y t r u e Y_{true} Ytrue,而只有 X X X。因此,我们不能使用在训练阶段的做法,因为我们没有目标序列/ Y t r u e Y_{true} Ytrue。所以当我们测试模型时,上一个时间步的预测输出(与训练阶段不同,不是真实输出)被作为当前时间步的输入。其余部分与训练阶段相同。
假设我们已经训练好了模型,现在我们用训练时使用的单个句子来测试它。如果我们把模型训练得很好,而且只在一个句子上训练,那么它应该表现得几乎完美,但为了解释方便,假设我们的模型训练得不好或者只是部分训练好了,现在我们来测试它。让我们用下面的图表来描述这个场景。
在时间步1
y 1 _ p r e d = [ 0 0.92 0.08 0 0 0 ] y_{1\_pred} = [0\ 0.92\ 0.08\ 0\ 0\ 0] y1_pred=[0 0.92 0.08 0 0 0]表明模型预测输出序列中的第一个标记/单词是“ravi”的概率为(0.92),所以现在在下一个时间步,这个预测的单词/标记将被用作输入。
在时间步2
第一个时间步预测的单词/标记“ravi”在这里被用作输入。在这里,模型预测输出序列中的下一个单词/标记是“de”的概率为(0.98),然后这个“de”在时间步3被用作输入。
类似的过程在每个时间步重复,直到到达<END>
标记。
更好的可视化表示如下:
所以根据我们训练好的模型,测试时的预测序列是“ravi de rencontrer rencontrer”。因此,尽管模型在第三次预测时是错误的,但我们仍然将其作为下一个时间步的输入。模型的正确性取决于可用数据的数量以及训练的好坏程度。模型可能会预测出错误的输出,但在测试阶段,无论如何,这个输出仍会被输入到下一个时间步。
4.5 嵌入层
我之前没有提到的一个重要细节是,编码器和解码器都会通过一个嵌入层来处理输入序列。这一步骤会降低输入单词向量的维度,因为在实际中,独热编码的向量往往维度非常大。嵌入向量为单词提供了一种更高效且有意义的表示方式。对于编码器来说,这可以通过嵌入层如何压缩单词向量维度来体现,例如,将维度从4维降低到3维。
这个嵌入层可以像词向量(Word2Vec)嵌入那样进行预训练,也可以与模型本身一起进行训练。
4.6 测试时的最终可视化
- 在左边,编码器处理输入序列(“nice to meet you”),每个单词先经过一个嵌入层(以降低维度),然后再经过一系列的长短期记忆(LSTM)层。编码器输出一个包含隐藏状态 h t h_t ht和细胞状态 c t c_t ct的上下文向量,这个上下文向量总结了输入序列。
- 在右边,解码器接收上下文向量并生成输出序列(“ravi de rencontrer”)。解码器使用长短期记忆(LSTM)单元来生成每个单词,将前一个单词作为输入(从一个特殊标记“”开始),经过另一个嵌入层,并通过Softmax层生成预测结果。
这张图展示了模型如何使用嵌入层和循环层将输入序列翻译成目标序列。
5. 编码器-解码器模型的缺点
这种架构存在两个主要缺点,都与长度相关。
首先,和人类一样,这种架构的记忆能力有限。长短期记忆网络(Long Short Term Memory,LSTM)的最终隐藏状态S或W,负责封装整个待翻译的句子。通常,S或W仅由几百个单元(即浮点数)组成。然而,往这个固定维度的向量中塞入过多信息会增加神经网络的模糊性。从“有损压缩”的角度来理解神经网络必须执行的操作,有时是非常有用的。
其次,一般来说,神经网络越深,训练难度就越大。对于循环神经网络而言,序列越长,沿时间维度的神经网络就越深。这会导致梯度消失问题,即循环神经网络从目标中学习到的梯度信号在反向传播时会消失。即使是专门为防止梯度消失而设计的循环神经网络,比如长短期记忆网络(LSTM),这仍然是一个根本性的问题。
此外,对于更复杂和更长的句子,我们还有诸如注意力模型(Attention Models)和Transformer模型。
6. 编码器-解码器架构的改进
6.1 添加嵌入层
嵌入层将输入标记转换为密集的向量表示,使模型能够学习输入序列中单词或标记的有意义表示。
通过使用可训练的嵌入层,并探索诸如预训练词嵌入或上下文嵌入等技术,我们可以丰富输入表示,使模型能够更有效地捕捉细微的语义和句法信息。这种增强有助于更好地理解和生成序列数据。
6.2 使用深度长短期记忆网络(LSTM)
长短期记忆网络(LSTM)是循环神经网络(RNN)的一种变体,以其捕捉序列数据中长距离依赖关系的能力而闻名。加深长短期记忆网络(LSTM)层能使模型学习输入和输出序列的层次表示,从而提高性能。
增加长短期记忆网络(LSTM)层的深度,并结合残差连接或层归一化等技术,有助于缓解梯度消失等问题,并促进更深层次网络的训练。这些改进使模型能够学习数据中更复杂的模式和依赖关系,从而实现更好的序列生成和理解。
6.3 反转输入
在机器翻译中,例如英语到印地语或英语到法语的转换,反转输入序列在某些情况下已被证明可以通过帮助捕捉长距离依赖关系和缓解梯度消失问题来提高模型性能。
然而,其有效性可能因语言特征和数据集的复杂性而异,并且它可能不会在所有情况下都持续提高性能。需要进行仔细的评估和实验,以确定反转输入序列对于特定任务和数据集是否有益。
我们现在已经理解了编码器-解码器的概念。现在,如果我们研读伊利亚·苏茨克弗(Ilya Sutskever)撰写的著名研究论文《使用神经网络进行序列到序列学习》(“Sequence to Sequence Learning with Neural Networks”),那么我们将能很好地理解这篇论文的概念。下面我总结一下这篇论文的内容:
- 在翻译中的应用:该模型专注于将英语翻译成法语,展示了序列到序列学习在神经机器翻译中的有效性。
- 特殊的句尾符号:数据集中的每个句子都以一个独特的句尾符号
<EOS>
结束,使模型能够识别序列的结束。 - 数据集:该模型在一个包含1200万个句子的子集上进行训练,这些句子包含3.48亿个法语单词和3.04亿个英语单词,数据来自一个公开可用的数据集。
- 词汇限制:为了控制计算复杂度,两种语言都使用了固定的词汇表,英语使用了最常见的16万个单词,法语使用了8万个单词。不在这些词汇表中的单词被替换为一个特殊的“UNK”标记。
- 反转输入序列:在将输入句子输入模型之前将其反转,结果发现这显著提高了模型的学习效率,特别是对于较长的句子。
- 词嵌入:该模型使用了一个1000维的词嵌入层来表示输入单词,为每个单词提供了密集且有意义的表示。
- 架构细节:输入(编码器)和输出(解码器)模型都有4层,每层包含1000个单元,展示了一个基于深度长短期记忆网络(LSTM)的架构。
- 输出层和训练:输出层使用SoftMax函数在最大词汇表上生成概率分布。该模型在这些设置下进行端到端的训练。
- 性能——BLEU分数:该模型的BLEU分数达到了34.81,超过了同一数据集上基于统计机器翻译系统的基础文件的分数33.30,标志着神经机器翻译的重大进步。
7. 示例:基于神经网络的编码器-解码器架构
我们可以在编码器-解码器架构中使用卷积神经网络(CNN)、循环神经网络(RNN)和长短期记忆网络(LSTM)来解决不同类型的问题。结合使用不同类型的网络有助于捕捉输入和输出数据序列之间的复杂关系。以下是一些可以使用卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆网络(LSTM)、Transformer等的不同场景或问题示例:
- 卷积神经网络(CNN)作为编码器,循环神经网络(RNN)/长短期记忆网络(LSTM)作为解码器:这种架构可用于图像字幕生成等任务,其中输入是一张图像,输出是描述该图像的单词序列。卷积神经网络(CNN)可以从图像中提取特征,而循环神经网络(RNN)/长短期记忆网络(LSTM)可以生成相应的文本序列。回想一下,卷积神经网络(CNN)擅长从图像中提取特征,这就是为什么在涉及图像的任务中它们可以用作编码器。此外,循环神经网络(RNN)/长短期记忆网络(LSTM)擅长处理诸如单词序列之类的序列数据,并且可以在涉及文本序列的任务中用作解码器。
- 循环神经网络(RNN)/长短期记忆网络(LSTM)作为编码器,循环神经网络(RNN)/长短期记忆网络(LSTM)作为解码器:这种架构可用于机器翻译等任务,其中输入和输出都是长度可变的单词序列。编码器中的循环神经网络(RNN)/长短期记忆网络(LSTM)可以将输入的单词序列编码为隐藏状态或数值表示,而解码器中的循环神经网络(RNN)/长短期记忆网络(LSTM)可以生成不同语言的相应单词输出序列。下面的图片展示了在编码器和解码器网络中都使用循环神经网络(RNN)的编码器-解码器架构。作为输入的单词序列是英语,输出是德语的机器翻译结果。
在编码器-解码器架构中使用循环神经网络(RNN)存在一个缺点。编码器网络中的最终数值表示或隐藏状态必须表示数据序列的整个上下文和含义。如果数据序列足够长,这可能会变得具有挑战性,并且在以数值表示的形式压缩整个信息的过程中,关于序列开头的信息可能会丢失。
在编码器-解码器架构中使用不同类型的神经网络,如卷积神经网络(CNN)、循环神经网络(RNN)、长短期记忆网络(LSTM)等时,需要记住以下一些限制:
- 卷积神经网络(CNN)的计算成本可能很高,并且可能需要大量的训练数据。
- 循环神经网络(RNN)/长短期记忆网络(LSTM)可能会受到梯度消失/梯度爆炸的影响,并且可能需要仔细的初始化和正则化。
- 结合使用不同类型的网络会使模型更加复杂,并且难以训练。
8. 编码器-解码器神经网络架构的应用
以下是编码器-解码器神经网络架构在现实生活/实际世界中的一些应用:
- Transformer模型:正如瓦斯瓦尼(Vaswani)等人在论文《注意力就是你所需要的一切》(“Attention Is All You Need”)中最初提出的那样,Transformer模型由编码器和解码器组成。每个部分都由使用自注意力机制的层构成。编码器处理输入数据(如文本)并创建其富含上下文的表示。解码器使用这些表示以及它的输入(如句子中的前一个单词)来生成输出序列。T5(文本到文本转换Transformer)使用了编码器-解码器架构。还有另一个例子是BART(双向自回归Transformer),它将双向编码器(如BERT)与自回归解码器(如GPT)结合在一起。
- Make-a-Video:脸书/元(Facebook / Meta)最近推出的人工智能系统Make-a-Video可能由深度学习技术驱动,可能包括用于将文本提示转换为视频内容的编码器-解码器架构。这种常用于序列到序列转换的架构,将使用编码器将输入文本转换为密集向量表示,并使用解码器将该向量转换为视频内容。然而,考虑到从文本创建视频的复杂性,该系统可能还会采用诸如生成对抗网络(Generative Adversarial Networks,GANs)或变分自编码器(Variational Autoencoders,VAEs)等先进的生成模型,这些模型在生成高质量、逼真的图像方面表现出色。此外,为了学习从文本到视觉的映射并理解世界的动态,它可能会利用大量的配对文本-图像数据和视频片段,可能采用无监督学习或自监督学习技术。
- 机器翻译:编码器-解码器架构最常见的应用之一是机器翻译。正如上面所示(使用循环神经网络(RNN)的编码器-解码器架构),将一种语言的单词序列翻译成另一种语言。编码器-解码器模型可以在大量的双语文本语料库上进行训练,以学习如何将一种语言的单词序列映射到另一种语言的等效序列。
- 图像字幕生成:图像字幕生成是编码器-解码器架构的另一个应用。在这种应用中,图像由编码器(使用卷积神经网络(CNN))进行处理,输出被传递给解码器(循环神经网络(RNN)或长短期记忆网络(LSTM)),解码器生成图像的文本描述。这可用于自动图像标记和字幕生成等应用。
9. 用神经网络进行序列到序列学习(实践操作)
在本教程中,我们将逐步介绍如何使用PyTorch实现一个基于编码器-解码器架构的基本序列到序列(Seq2Seq)模型。序列到序列(Seq2Seq)模型广泛用于机器翻译、文本摘要和问答等任务。它的工作方式是将一个序列(如一个句子)作为输入,并生成一个序列(如一个翻译后的句子)作为输出。
我们将使用Multi30k
数据集,执行一个简化的机器翻译任务,即把德语翻译成英语。
什么是Multi30k数据集?
Multi30k
数据集是一组英语-德语平行句子的集合。数据集中的每个句子都是一对德语句子及其相应的英语翻译。这个数据集通常用于训练机器翻译模型。
对于我们的模型,我们将对句子进行分词,为两种语言构建词汇表,并训练模型将德语句子翻译成英语。
项目概述
- 数据预处理:我们将对德语和英语句子进行分词,并为训练准备数据集。
- 模型架构:我们将创建两个组件:
- 编码器:读取源语言(德语)句子,并将其转换为上下文向量。
- 解码器:使用上下文向量生成目标语言(英语)句子。
- 训练:在数据集上训练模型,以最小化预测的目标句子与实际目标句子之间的损失。
- 评估:我们将通过翻译新句子并计算BLEU分数来评估模型,BLEU分数通常用于评估机器翻译的质量。
步骤1:安装和导入库
我们将使用几个库:用于深度学习的torch
、用于处理数据集的torchtext
,以及用于分词的spacy
。
使用以下命令安装所需的库:
pip install torch torchtext spacy
python -m spacy download de_core_news_sm
python -m spacy download en_core_web_sm
接下来,导入库:
import torch
import torch.nn as nn
import torch.optim as optim
import random
import spacy
from torchtext.data import Field, BucketIterator
from torchtext.datasets import Multi30k
步骤2:数据预处理
我们将使用Multi30k
数据集,它提供了英语和德语的句子对。为了帮助模型更好地学习源语言和目标语言句子之间的对齐关系,德语句子将被反转。
分词
为了将句子拆分成单词,我们将使用spacy
:
spacy_de = spacy.load('de_core_news_sm')
spacy_en = spacy.load('en_core_web_sm')
def tokenize_de(text):
return [tok.text for tok in spacy_de.tokenizer(text)][::-1]
def tokenize_en(text):
return [tok.text for tok in spacy_en.tokenizer(text)]
字段定义
我们使用torchtext
的Field
来定义数据将如何处理。我们指定如何对数据进行分词,以及如何处理起始(<sos>
)和结束(<eos>
)标记。
SRC = Field(tokenize=tokenize_de, init_token='<sos>', eos_token='<eos>', lower=True)
TRG = Field(tokenize=tokenize_en, init_token='<sos>', eos_token='<eos>', lower=True)
加载和构建词汇表
现在我们加载数据集,并为源语言(德语)和目标语言(英语)构建词汇表。
train_data, valid_data, test_data = Multi30k.splits(exts=('.de', '.en'), fields=(SRC, TRG))
SRC.build_vocab(train_data, min_freq=2)
TRG.build_vocab(train_data, min_freq=2)
min_freq=2
确保只有在训练数据中至少出现两次的单词才会被包含在词汇表中。
步骤3:构建编码器
编码器是一个循环神经网络(RNN),它读取单词序列(在这种情况下是德语句子),并将其编码为上下文向量。
class Encoder(nn.Module):
def __init__(self, input_dim, emb_dim, hid_dim, n_layers, dropout):
super().__init__()
self.embedding = nn.Embedding(input_dim, emb_dim)
self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout)
self.dropout = nn.Dropout(dropout)
def forward(self, src):
# src = [src_len, batch_size]
embedded = self.dropout(self.embedding(src)) # [src_len, batch_size, emb_dim]
outputs, hidden = self.rnn(embedded)
return hidden
input_dim
:源语言词汇表的大小。emb_dim
:嵌入维度(将每个单词转换为固定大小的向量)。hid_dim
:门控循环单元(GRU)的隐藏维度(循环神经网络(RNN)每层的单元数量)。n_layers
:门控循环单元(GRU)的层数。dropout
:用于防止过拟合的正则化。
步骤4:构建解码器
解码器根据编码器生成的上下文向量,一次生成一个目标句子(英语)中的单词。
class Decoder(nn.Module):
def __init__(self, output_dim, emb_dim, hid_dim, n_layers, dropout):
super().__init__()
self.embedding = nn.Embedding(output_dim, emb_dim)
self.rnn = nn.GRU(emb_dim, hid_dim, n_layers, dropout=dropout)
self.fc_out = nn.Linear(hid_dim, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, input, hidden):
# input = [batch_size]
input = input.unsqueeze(0) # [1, batch_size]
embedded = self.dropout(self.embedding(input)) # [1, batch_size, emb_dim]
output, hidden = self.rnn(embedded, hidden) # [1, batch_size, hid_dim], [n_layers, batch_size, hid_dim]
prediction = self.fc_out(output.squeeze(0)) # [batch_size, output_dim]
return prediction, hidden
output_dim
:目标语言词汇表的大小(英语)。embedding
:将单词转换为向量。fc_out
:全连接层,用于将隐藏状态映射到输出词汇表。
步骤5:构建序列到序列(Seq2Seq)模型
序列到序列(Seq2Seq)模型将编码器和解码器结合在一起。它使用编码器将源序列编码为上下文向量,并使用解码器生成目标序列。
class Seq2Seq(nn.Module):
def __init__(self, encoder, decoder, device):
super().__init__()
self.encoder = encoder
self.decoder = decoder
self.device = device
def forward(self, src, trg, teacher_forcing_ratio=0.5):
trg_len = trg.shape[0]
batch_size = trg.shape[1]
trg_vocab_size = self.decoder.fc_out.out_features
outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
hidden = self.encoder(src)
input = trg[0, :]
for t in range(1, trg_len):
output, hidden = self.decoder(input, hidden)
outputs[t] = output
top1 = output.argmax(1)
input = trg[t] if random.random() < teacher_forcing_ratio else top1
return outputs
teacher_forcing_ratio
:确定在训练期间是使用实际的目标标记还是预测的标记作为下一个输入(有助于稳定训练)。outputs
:存储每个时间步的预测结果。
步骤6:训练模型
我们定义训练循环,在其中输入德语句子,通过序列到序列(Seq2Seq)模型运行它,并使用交叉熵计算损失。
def train(model, iterator, optimizer, criterion, clip):
model.train()
epoch_loss = 0
for i, batch in enumerate(iterator):
src = batch.src
trg = batch.trg
optimizer.zero_grad()
output = model(src, trg)
output_dim = output.shape[-1]
output = output[1:].view(-1, output_dim)
trg = trg[1:].view(-1)
loss = criterion(output, trg)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
optimizer.step()
epoch_loss += loss.item()
return epoch_loss / len(iterator)
- 梯度裁剪:用于防止梯度爆炸。
- 交叉熵损失:衡量预测标记与实际标记之间的误差。
步骤7:评估模型
评估的工作方式与训练类似,但我们禁用反向传播,因为我们只对模型的性能感兴趣。
def evaluate(model, iterator, criterion):
model.eval()
epoch_loss = 0
with torch.no_grad():
for i, batch in enumerate(iterator):
src = batch.src
trg = batch.trg
output = model(src, trg, 0)
output_dim = output.shape[-1]
output = output[1:].view(-1, output_dim)
trg = trg[1:].view(-1)
loss = criterion(output, trg)
epoch_loss += loss.item()
return epoch_loss / len(iterator)
步骤8:初始化模型和超参数
现在,我们初始化模型、优化器和损失函数,并指定超参数。
INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
HID_DIM = 512
N_LAYERS = 2
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, HID_DIM, N_LAYERS, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, HID_DIM, N_LAYERS, DEC_DROPOUT)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = Seq2Seq(enc, dec, device).to(device)
optimizer = optim.Adam(model.parameters())
TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]
criterion = nn.CrossEntropyLoss(ignore_index=TRG_PAD_IDX)
步骤9:训练循环
我们在一定数量的训练轮次(epoch)中训练模型。
N_EPOCHS = 10
CLIP = 1
for epoch in range(N_EPOCHS):
train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
valid_loss = evaluate(model, valid_iterator, criterion)
print(f'Epoch: {
epoch+1}')
print(f'Train Loss: {
train_loss:.3f} | Val. Loss: {
valid_loss:.3f}')
步骤10:使用BLEU分数进行测试
最后,我们可以通过翻译新句子并评估BLEU分数来测试我们的模型。
def translate_sentence(sentence, src_field, trg_field, model, device, max_len=50):
model.eval()
tokens = [token.text.lower() for token in spacy_de(sentence)]
tokens = [src_field.init_token] + tokens + [src_field.eos_token]
src_indexes = [src_field.vocab.stoi[token] for token in tokens]
src_tensor = torch.LongTensor(src_indexes).unsqueeze(1).to(device)
with torch.no_grad():
hidden = model.encoder(src_tensor)
trg_indexes = [trg_field.vocab.stoi[trg_field.init_token]]
for i in range(max_len):
trg_tensor = torch.LongTensor([trg_indexes[-1]]).to(device)
with torch.no_grad():
output, hidden = model.decoder(trg_tensor, hidden)
pred_token = output.argmax(1).item()
trg_indexes.append(pred_token)
if pred_token == trg_field.vocab.stoi[trg_field.eos_token]:
break
trg_tokens = [trg_field.vocab.itos[i] for i in trg_indexes]
return trg_tokens[1:]
这个函数使用训练好的Seq2Seq模型将德语句子翻译成英语。
10. 总结
在这篇博客中,我们借助强大的编码器 - 解码器架构探索了序列建模。从处理序列数据面临的挑战入手,我们探讨了该架构如何高效处理诸如神经机器翻译等序列到序列的任务。我们深入研究了编码器在捕捉输入信息方面的作用,以及解码器在生成连贯输出方面的功能,并阐述了它们背后的数学原理。
我们还介绍了像“教师强制”这样的训练技巧、实际的实现步骤,并探讨了“苏茨克弗模型”作为编码器 - 解码器方法在现实世界中的应用。实践示例为构建和评估序列到序列模型提供了详细的分步指南。
该架构的应用范围不止于翻译,还广泛用于文本摘要、语音识别等领域。通过掌握其理论和实际应用,我们能够利用编码器 - 解码器架构解决不同领域的各种复杂问题。
11. 知识测试!
- 编码器中使用双向LSTM如何提升Seq2Seq模型的性能?
- 预期答案:双向LSTM会从正向和反向两个方向处理输入序列,能从过去和未来的状态中捕捉更多上下文信息。这有助于编码器构建更全面的输入序列表示,从而提升翻译或摘要等下游任务的性能。
- 描述如何为实时翻译系统实现一个Seq2Seq模型,可能会面临哪些挑战?
- 预期答案:实时翻译系统需要低延迟,因此在不降低准确性的前提下优化模型速度至关重要。挑战包括处理不同长度的句子、保持上下文连贯以及确保目标语言表达流畅。像注意力机制和束搜索这样的技术会有所帮助,但必须仔细调整以平衡准确性和速度。
- 解释注意力机制与传统Seq2Seq模型的区别,以及它们如何处理长序列?
- 预期答案:传统Seq2Seq模型会将输入序列压缩成固定大小的向量,这可能导致信息丢失,尤其是在处理长序列时。而注意力机制允许模型在生成输出的每个单词时聚焦于输入序列的特定部分,并根据上下文动态调整“焦点”,从而更有效地处理长序列。
- 在哪些场景下你会更倾向于使用Transformer模型而非传统基于RNN的Seq2Seq模型?
- 预期答案:在处理非常长的序列或者并行计算至关重要的情况下,会优先选择Transformer模型,因为它们不像RNN那样依赖顺序处理。通过自注意力机制,Transformer模型还能有效捕捉长距离依赖关系,适用于语言建模、翻译等任务,甚至非语言任务,如图像处理。
- Transformer模型中位置编码的作用是什么,为什么它是必要的?
- 预期答案:由于Transformer模型是并行处理输入的,而非按顺序处理,位置编码能为模型提供序列中每个标记的位置信息。这种编码使模型能够捕捉标记的顺序,这对于理解输入序列的结构和含义至关重要。
- 如何解决Seq2Seq模型中的过拟合问题?
- 预期答案:可以通过一些技术来缓解Seq2Seq模型的过拟合问题,如正则化(例如L2正则化、丢弃法)、使用更大的数据集、数据增强、提前停止训练,以及采用更复杂的架构(如注意力机制)来提高泛化能力。此外,监控验证损失并根据需要调整模型复杂度也有助于防止过拟合。
- 请讨论在Seq2Seq模型中使用束搜索和贪心搜索的权衡?
- 预期答案:贪心搜索在每一步都选择最可能的单词,速度更快,但可能会错过全局最优序列。而束搜索在每一步会考虑多个潜在序列(束宽),更有可能找到最优序列,但会增加计算复杂度。选择哪种方法取决于应用对准确性和速度的需求。
- 如何修改Seq2Seq模型以处理多语言翻译任务?
- 预期答案:对于多语言翻译任务,可以引入一个共享编码器和多个解码器,每个解码器对应一种不同的目标语言,或者使用一个带有特殊标记来指示目标语言的单一解码器。在多种语言上微调模型并结合迁移学习也能提升跨语言的性能。
- 描述如何为聊天机器人应用实现一个Seq2Seq模型,关键考虑因素有哪些?
- 预期答案:为聊天机器人实现Seq2Seq模型需要确保模型能够生成连贯且符合上下文的回复。关键考虑因素包括处理多轮对话、在交互过程中保持上下文连贯、确保回复的多样性以及减少生成文本中的偏差。引入强化学习或根据人类反馈进行微调可以提高聊天机器人的有效性。
- Seq2Seq模型在处理文本摘要或翻译等任务时有哪些局限性,如何解决这些问题?
- 预期答案:Seq2Seq模型可能难以保持连贯性、处理长序列以及生成语法正确的输出。可以通过使用注意力机制、引入更大且更多样化的数据集、采用预训练模型(如BERT或GPT)并在特定任务上进行微调来解决这些局限性。像BLEU和ROUGE这样的评估指标有助于评估和提升性能。
- 编码器和解码器必须是同一类型的神经网络吗?
- 预期答案:不,编码器和解码器不必是同一类型的神经网络。在许多实现中,两者都是RNN(例如LSTM或GRU),但它们可以根据任务需求有所不同。例如,编码器可以是用于处理图像的卷积神经网络(CNN),而解码器可以是用于生成序列的RNN。关键要求是编码器的输出应与解码器的输入兼容。
- 除了机器翻译,你能想到编码器 - 解码器架构还可以应用于哪些其他场景?
- 预期答案:可以,编码器 - 解码器架构可应用于多种任务,包括文本摘要。在文本摘要中,编码器处理输入文档并创建压缩表示,解码器则生成捕捉关键信息的简短版本。其他应用包括图像字幕生成(编码器是处理图像的CNN,解码器是生成字幕的RNN)和语音识别(编码器处理音频信号,解码器生成相应的文本)。
- 在编码器 - 解码器模型中训练解码器时,“教师强制”是什么,为什么要使用它?讨论这种方法可能存在的缺点。
- 预期答案:“教师强制”是一种训练技术,在训练过程中,将实际的目标输出作为解码器的下一个输入,而非模型的预测结果。它通过在每一步提供正确的上下文来加速收敛并提高学习效果。
- 从数学角度看,编码器如何处理输入序列?
- 预期答案:在每个时间步t,编码器使用 h t = f ( e t , h t − 1 ) h_t = f(e_t, h_{t - 1}) ht=f(et,ht−1)更新其隐藏状态,其中 e t e_t et是嵌入后的输入, f f f是像LSTM或GRU这样的循环函数。
- 讨论解码器测试阶段嵌入层的作用。如果有区别的话,它与训练阶段的作用有何不同?
- 预期答案:嵌入层将标记转换为捕捉语义关系的密集向量表示,使模型能够更有效地处理和学习文本数据。
- 举例说明编码器 - 解码器架构在机器翻译之外的应用场景。描述问题以及该架构如何解决该问题。
- 预期答案:在文本摘要中,编码器 - 解码器模型将长篇文档(编码器)浓缩为简短摘要(解码器),有效捕捉要点。
- 编码器 - 解码器架构中解码器模块的数学基础是什么?解释解码器如何生成输出序列。
- 预期答案:解码器在每一步计算 s t = f ( e t , s t − 1 , c ) s_t = f(e_t, s_{t - 1}, c) st=f(et,st−1,c),其中 e t e_t et是前一个标记的嵌入, s t − 1 s_{t - 1} st−1是前一个状态, c c c是上下文向量,然后预测下一个标记。
- 假设我们使用神经网络来实现编码器 - 解码器架构,编码器和解码器必须是同一类型的神经网络吗?
- 预期答案:不,编码器和解码器不必是同一类型的神经网络。例如,在图像字幕生成等实际应用中,通常使用卷积神经网络(CNN)作为编码器处理图像,而使用循环神经网络(RNN)或Transformer作为解码器生成描述性文本。
参考文献
- https://medium.com/@vipra_singh/llm-architectures-explained-encoder-decoder-architecture-part-4-b96ace71394c