NLP任务的输入往往是一句话或者是一篇文章,他有几个特点:

  • 输入是一个一维线性序列
  • 输入是不定长的
  • 输入单词的相对位置非常的重要
  • 句子中的长特征对理解句子非常的重要(距离很远的词)

一个合格的特征抽取器应当很好的适配领域问题的特点,能够充分抽取数据中的特征。

NLP领域主要任务

该部分在上一篇文章中有简要提到,下面详细记录一下这些任务主要解决的问题:

  • 序列标注:这是NLP典型的任务,包括中文分词,词性标注,命名实体识别等等。
    • 特点:模型根据上下文给每个单词分配一个类别
  • 分类任务:常见有的文本分类和情感计算
    • 特点:不论文章多长,总体给出一个分类类别
  • 句子关系判断:问答系统,语义改写,自然语言推理
    • 特点:给出两个句子,判断这两个句子是否具有某种语义关系
  • 生成式任务:机器翻译,文本摘要,写诗造句,看图说话
    • 特点:输入文本后,需要自主生成另一段文字

从模型的角度来看,模型的特征抽取能力是至关重要的,下面开始详细介绍NLP的三个抽取器。

久经沙场RNN

RNN引入NLP之后,一直是一个明星模型,在各种模型中被广泛的应用。它采用线性序列结构不断从前往后收集输入信息。但是这种结构在反向传播过程中存在优化的困难。因为反向传播路径过长,导致严重的梯度消失或爆炸问题。于是很快建LSTM引入RNN作为标准模型中。

下面是非常典型的使用RNN来解决NLP任务的基本框架:

RNN本身结构就是个可以接纳不定长输入的由前向后进行信息线性传导的网络结构,而在LSTM引入三个门后,对于捕获长距离特征也是非常有效的。所以RNN特别适合NLP这种线形序列应用场景,这是RNN在NLP界如此流行的根本原因。

但是对于RNN来说,来自一些新型的特征提取器的挑战,以及RNN并行能力差的问题,导致了它很可能被替代。

RNN并性能力差的原因:RNNT时刻有两个输入,一个输入为当前的文本,另一个输入为T-1时刻隐藏层的输出S(T-1),这是最能体现RNN的一点,RNN的历史信息就是通过这个传输渠道向后传的。因此T时刻计算依赖于T-1时刻的结果,因此网络必须按照时序的顺序一个一个往后走。

而CNN与transformers不存在这种问题,他们是天生的并行计算结构。

RNN在并行化上也做了一些工作,通常的做法有打断隐层的连接,或者打断部分的连接,层间并行。

改造CNN

2014年CNN最早被引入NLP中:

每一行为一个单词的数值编码,卷积层将数值编码分割,在编码维度上移动,得到卷积后的特征,但仅仅在句子分类的任务上性能不错。

但是单个卷积层难以捕获远距离的特征,因此解决的方案有把卷积层做深;使用dilated 孔洞卷积。CNN能够捕获向量的位置信息,但是pooling结构通过会破坏掉这种顺序,因此通常使用全连接层替换掉pooling结构。

目前使用的比较多的CNN如下:

上图是现代CNN的主体结构,通常由1-D的卷积层来叠加深度,使用skip-connection来辅助优化,也可以使用dilated等手段。CNN在nlp中的引入,能够保持数据间的时序信息,要设法将CNN的深度做起来。

transformer结构

Transformer模型有很多好处,它改进了RNN最被人诟病的训练慢的缺点,利用self-attention机制实现快速并行。且Transformer可以增加到非常深的深度,充分发掘DNN模型的特性。下面具体的讲解一下transformer的机制。

下面通过引入一个NLP中经典问题的方式来解释这个结构:

我们打算将英语翻译为西班牙语:

X = [‘Hello’, ‘,’, ‘how’, ‘are’, ‘you’, ‘?’] (Input sequence)
Y = [‘Hola’, ‘,’, ‘como’, ‘estas’, ‘?’] (Target sequence)

transformer中encoder部分负责提取句子信息,decoder部分负责将encoder的输出与target相结合,得到接近target的翻译结果。

transformer

transformer结构是一个由encoder,decoder,ski-connection,layerNorm,FF共同作用的一个结构,在数据特征提取上有着明显的优势。

编码和解码的部分分别都由六个编码器组件组合而成:

将encoder与decoder模块展开来看:

encoder部分由一个自注意力层和一个前向网络构成,其中自注意力层关注句子中的每一个单词对当前编码单词的关系。

decoder部分由三层构成,其中中间那一层,用来关注句子中的相关部分(和seq2seq类似)。

decoder模块

decoder模块是对nlp数据提取特征的模块,将每一个编码器单元展开如下:

数据流动

  • 将单词转化成词向量(词向量的长度固定,BERT中为512),输入的维度:句子长度*词向量长度
  • 生成一个句子长度*词向量长度的位置编码信息,添加到输入中
  • 输入数据经过N个encoder单元,生成句子长度*词向量长度大小的向量

第一步对句子进行分词,将单词转化为词向量:

当我们训练一个batch的数据的时候,我们需要对一些较短的句子进行补充,通过在句首添加padding的方式,将句子长度对齐。

[“”, “”, “”, “Hello”, “, “, “how”, “are”, “you”, “?”] →

[5, 5, 5, 34, 90, 15, 684, 55, 193]

第二步,对位置信息进行编码,然后将位置信息加入到输入当中,对位置信息进行编码采用以下的公式:

其中i表示y方向即每一个单词,j表示在词向量的长度(emb-dim)上的位置,因此最终得到下面的结果:

将输入与上面的位置编码相加,得到最终的输入数据。

encoder block

接下来进入encoder内部,编码器内部采用一层的自注意力层以及一个前向的全连接层。将数据输入编码器,首先遇到的是 multi-head attention结构。

multi-head attention结构共同训练h次注意力层,这种做法能够扩展专注于不同位置的能力,同时给出了注意力层的多个表示子空间。

对于每一个head来说,我们训练三个向量,Q,K,V,与输入embedding向量相乘得到中间结果,用于最后计算每一个词最终的得分:

将上面的运算合并为矩阵运算,则算法如下:

利用上面的结果计算每个单词的得分:
$$
\begin{equation}
\text { Attention }(Q, K, V)=\operatorname{softmax}\left(\frac{Q K^{T}}{\sqrt{d_{k}}}\right) V
\end{equation}
$$

对于multi-head来说,我们将X输入h个head中,将会得到h个句子不同词之间的得分Z:

对于一个句子来说,我们只希望得到一组表示词语间的相互关系,于是我们将Z拼接起来,通过训练一个权重$W^0$使得最终得到一个 句子长度*词向量长度

通过融合注意力机制的多头的结果,每个词与句子的其他成分之间的关系得到了充分的挖掘:

当我们计算出句子单词之间的注意力分布,下一步为添加残差后归一化:

完成残差之后是一个正向的全连接层(Free Forward),即一个两层的全连接层,第一层的激活函数为ReLU,第二层的激活函数为线性激活函数:
$$
\begin{equation}
\mathrm{FFN}(x)=\max \left(0, x W_{1}+b_{1}\right) W_{2}+b_{2}
\end{equation}
$$
其中W1位第一层,W2为第二层,max函数表示ReLU激活函数,b2为线性激活函数的偏移。最终的输出添加残差,归一化之后得到一个decoder的输出,随后将这个输出输入下一个decoder模块中,直到所有的模块都完成输出,将输出传至decoder模块。

Decoder

Decoder部分网络结构相比较于decoder部分,多出了一个encoder to decoder的模块,这个模块的的输入来自于decoder的输出:

encoder和decoder中信息传播如下:

每一个decoder模块都将接受encoder的输出。Decoder的一个单元具体结构如下:

数据流动

  • 首先将target进行分词,编码成词向量,维度为 target句子长度*词向量长度
  • 将第一步得到的数据输入N个Decoder模块中,在每次迭代过程中,接收decoder的输出作为一个额外的输入,最终得到的输出维度为 target句子长度*词向量的长度
  • 将decoder得到的输出,输入到一个全连接层,并且每一层做一个逐行的softmax,最终得到的输出是翻译的结果,即维度为句子长度*每个单词的长度

输入

由于input的句子长度和target的句子的长度不一致,因此首先对target句子分词后,进行偏移:

[“Hola”, “, “, “como”, “estás”, “?”]→[“”, “Hola”, “, “, “como”, “estás”, “?”]

train vs test

train阶段和test阶段对于decoder部分来说有一个重要的差别:

  • 在test阶段,我们不知道groundTruth,因此我们将会根据之前给出的单词来预测当前位置的单词,即无法使用当前位置之后的单词的信息。
  • 在train阶段,我们知道GT,我们会直接告诉模型我们的target是什么,然后根据和test一样的顺序进行预测,但是这将会出现一个问题,模型可能根据target句子本身的位置关系来预测target,也就是使用了target的信息,这是不允许的,因为在实际情况中我们不可能提前知道target,因此这样的训练是不充分的。

因此我们在Decoder的训练阶段必须消除target提供当前词之后的词所提供的信息。例如下面例子,当要预测estás的时候,我们就只能使用绿色部分所使用的信息,而红色部分的信息不能使用:

为了解决上面这个问题,我们提出了mask multi-head attention,即对output的数据进行处理。

mask multi-head attention

首先通过与encoder相同的操作,即multi-head attention得到一个 target句子长度*词向量的一个输出矩阵,然后进行mask操作,即将矩阵右上角的数值置为负无穷。

原始multi head结果:

mask后的结果:

这就意味着当前单词的预测无法使用其后出现的单词信息。

Encoder to decoder

将上述的输出添加输入以及归一化之后,输入到下一层encoder to decoder,这一部分接受的输入由两部分组成,第一部分就是decoder的第一阶段的输出,另一个部分就是encoder最终的输出。

与decoder同样的操作,我们训练三个向量,Q,K,V,与输入embedding向量相乘得到中间结果,用于最后计算每一个词最终的得分,唯一的不同在于这三个向量使用的训练数据不同,如下图:

即Q向量由decoder第一阶段的数据来训练,K,V由encoder最后输出的数据来训练。

同样的利用与encoder相同的attention公式计算每一个词与句子中其他的成分的关系:
$$
\begin{equation}\text { Attention }(Q, K, V)=\operatorname{softmax}\left(\frac{Q K^{T}}{\sqrt{d_{k}}}\right) V\end{equation}
$$
接下来与encoder相同,训练h个head,然后组合在一起通过一个$W^0$矩阵进行组成得到输出,最后传入decoder模块的第三阶段,即全连接层进行前向的传播。

linear and softmax

重复上面decoder的基础模块N次,最后得到的输出的维度为 target句子长度*词向量长度,然后将这个向量输入一个linear全连接层中,全连接层输出的维度为翻译后句子的真实长度,其实际含义在对每一个词赋予一个权重:
$$
\begin{equation}
x W_{1}
\end{equation}
$$
最后,将上面的输出输入到softmax当中,计算出当前位置上,所有可能出现的翻译的结果的概率,然后根据最大的概率得到模型预测的翻译的结果:

根据第一行的结果,我们可以判断,ss对应的翻译是hello。

最后放一张encoder和decoder的合照,以便于回顾transformer的各种细节:

最最最后小彩蛋:

inference