第三十九周:文献阅读+Transformer

目录

摘要

Abstract

文献阅读:CNN与LSTM在水质预测中的应用

现有问题

提出方法

相关模型

CNN

LSTM

CNN-LSTM神经网络模型

模型框架

CNN-LSTM神经网络

研究实验

数据集

模型评估指标

数据预处理

实验设计与结果

研究贡献

Transformer

Encoder-Decoder架构

Encode(编码器)

Decode(解码器)

Transformer 输出结果

Q&&A

Transformer的代码实现

总结


摘要

这周阅读的文献,提出了一种CNN-LSTM组合模型,用于对水质溶解氧数据进行预测。CNN提取的数据特征可以存储在LSTM中进行长期记忆,突出了这些数据特征在预测过程中的作用,从而提高了模型的准确性。通过与基础LSTM模型进行对比试验,可以看出CNN-LSTM具有更强的鲁棒性预测性能。Transformer模型也是一个 Seq2Seq 模型(Encoder-Decoder 框架的模型),通过编码器把输入读进去,再由解码器得到输出。Transformer中最重要就是使用了Self-Attention机制,而不同层中使用的Self-Attention也有差别。总体而言就是从得到的部分结果即局部信息去全局信息中找到重点,从而提高生成结果的准确率。

Abstract

The literature read this week proposes a CNN-LSTM combined model for predicting dissolved oxygen data in water quality. The data features extracted by CNN can be stored in LSTM for long-term memory, highlighting the role of these data features in the prediction process, thereby improving the accuracy of the model. Through comparative experiments with the basic LSTM model, it can be seen that CNN-LSTM has stronger robust predictive performance. The Transformer model is also a Seq2Seq model (a model of the Encoder Decoder framework), which reads the input through an encoder and outputs it through a decoder. The most important thing in Transformer is the use of the Self Attention mechanism, and the Self Attention used in different layers also varies. Overall, it is about finding the key points from the obtained partial results, i.e. local information, to the global information, in order to improve the accuracy of the generated results.

文献阅读:CNN与LSTM在水质预测中的应用

IASC | Application of CNN and Long Short-Term Memory Network in Water Quality Predicting (techscience.com)

DOI: 10.32604/iasc.2022.029660         

现有问题

(1)水中溶解氧的含量是衡量水体自净能力的重要指标。当水中溶解氧减少时,水体恢复到初始状态所需的时间越短,说明水体的自净能力越强。提高溶解氧预报的准确性不仅有利于改善水质,而且有利于河流管理和预警。但溶解氧具有时间序列、不稳定性和非线性的特点,各种因素之间复杂的耦合关系以及各种因素的影响都会影响预测结果。

(2)水质指标符合时间序列特征,时间序列预测模型适用于水质数据预测。早期研究使用的统计预测模型,如ARIMA、SARIMA等模型。这些模型的统一特点是需要对数据的稳定性和白噪声进行检验。由于缺乏从非线性数据中提取特征的能力,传统的机器学习算法在精度、收敛速度和适用性方面或多或少存在局限性。

提出方法

为解决水质变化影响因素过多、难以预测的问题,本文提出了CNN-LSTM组合模型对水质溶解氧数据进行预测。CNN需要较少的参数,非常适合处理具有统计平稳性和局部相关性的数据。LSTM神经网络是专门为学习具有长期依赖关系的时间序列数据而设计的,在学习高级特征序列的长期依赖关系和时间性方面具有很大的优势,这些特性非常适合水质预测的需要。在该模型中,CNN提取的数据特征可以存储在LSTM中进行长期记忆,突出了这些数据特征在预测过程中的作用,从而提高了模型的准确性。

相关模型

CNN

CNN是目前最成功的深度学习算法之一,其网络结构分为一维CNN (1D-CNN)、二维CNN (2D-CNN)和三维CNN (3D-CNN)。1D-CNN通常用于序列数据处理,2D-CNN通常用于图像和文本处理,3D-CNN通常用于视频处理。CNN能够提取输入数据的点、线、面等抽象的视觉特征,通过CNN可以很好地识别数据中简单的模式,然后利用这些模式在更高的层次上形成更复杂的模式。当我们希望从整体水质数据集中较少的片段中获取更多可用信息,而片段中特征信息的位置相关性不高时,使用一维CNN对数据处理非常有利。一维CNN首先将卷积核作为窗口,在时间序列数据上移动窗口,提取局部序列片段并与权值相乘,然后在计算序列特征后将其池化,进一步滤除数据中对预测结果产生偏倚的噪声信息,最终使预测结果更加准确。

卷积网络的3种基本模块:卷积层、池化层和全连接层。输入一个矩阵,堆叠一个或多个卷积层对输入进行特征提取,然后接一个池化层进行空间尺寸缩小,之后重复此模式,直到空间尺寸足够小,最后接多个全连接层,卷积层和全连接层后面都需紧接ReLU激活层,但必须注意的是,最后一个全连接层后面不需接ReLU激活层,而是输出类别分值。

以单通道卷积层为例,通过一个2✖3的卷积核,将点积结果相加得到一个3✖1的矩阵,通过一个ReLU激活函数得到一个正数矩阵,再通过最大池化得到一个数值,将该值通过全连接层得到一个概率值。

LSTM

LSTM对时间序列的处理主要分为三个阶段:

(1)遗忘阶段。这一阶段主要是选择性地忘记前一个节点的输入顺序,“忘记不重要的,记住重要的”。也就是说,f_{t}的值用于控制在前一个状态C_{t-1}中需要记住什么和需要忘记什么。

(2)选择记忆阶段。在这个阶段,输入序列X_{t}被选择性地”记住”。电流单元的输入为计算后的输入,可由g_{t}选择性输出。

(3)输出阶段。这个阶段决定哪些状态将被视为当前状态的输出,主要由O_{t}控制,并使用tanh激活函数在C_{t}上缩放。

CNN-LSTM神经网络模型

模型框架

该模型由三个模块组成:数据输入层、隐藏处理层和预测结果输出层。

  • 数据输入层的主要功能是构建水质监测大数据和数据预处理;
  • 隐处理层主要通过CNN提取数据空间,并通过LSTM从数据的空间特征中进一步提取数据的时间序列特征;
  • 预测结果层主要采用全连通层将各节点与LSTM单元输出的所有数据特征连接起来,实现局部特征的融合,最终输出水质预测结果。

CNN-LSTM神经网络

水质数据随时间的变化具有一定的周期性,并会受到其他外部因素的影响,使数据呈非线性变化。因此,很难直接预测水质的变化。单独使用LSTM模型进行预测会影响数据结果的最大值和最小值,引入与预测无关的噪声。单独使用CNN模型会导致整个连接层中参数比例过大而导致过拟合问题。结合CNN和LSTM网络结构的优点,可以将两种模型组合形成CNN-LSTM模型,可以更好地提高模型预测效果的准确性。CNN-LSTM模型如图所示。

CNN-LSTM模型的第一部分是由卷积层和最大池化层组成的CNN层。卷积层将遍历从输入层传入的水质信息,利用卷积核的权值与输入的水质信息的局部序列进行卷积运算,从而得到一个比原始序列表达能力更高的初步矩阵。将得到的新矩阵作为最大池化层的输入,利用池化窗口在矩阵序列上滑动,每次滑动后取窗口的最大值进行池化,从而输出更具表现力的特征矩阵。将叠加部分通过Flatten层传递给LSTM网络,利用LSTM模型的遗忘门和输出门的特性对关联数据信息进行过滤和更新。

研究实验

数据集

水质监测数据来源于中国环境监测站,水质信息主要包括站点位置、监测类型和监测时间。站点位置包括站点所在的省份、流域和区段名称。监测类型包括水温、电导率、溶解氧、浊度、高锰酸盐指数、氨氮、总磷、总氮。监控时间显示最近四小时内更新的数据。

模型评估指标

  • MSE(均方误差)用于监测预测值与实际值之间的偏差。它是评价神经网络性能的一个指标。MSE的计算公式为:
  • MSE=\frac{1}{n}\sum_{i=1}^{n}(x_{i}-{x_{i}}')^{2}
  • RMSE(均方根误差)反映了系统误差的分散程度,表示预测值与实际值之间的偏差范围。与均方根误差不同的是,均方根误差会在操作过程中改变维度,因此可以通过均方根误差消除维度的影响。RMSE值越大,数据波动性越强。均方根误差的计算公式为:
  • RMSE=\sqrt{\frac{1}{n}\sum_{i=1}^{n}(x_{i}-{x_{i}}')^{2}}
  • MAPE(平均绝对百分比误差)反映的是实际误差与实际值的比值,本质上考虑的是实际误差与实际值的比值。它的功能是使用相同的数据来预测不同的模型。模型预测的MAPE越小,模型越好。MAPE的计算公式为:
  • MAPE=\frac{1}{n}\sum_{i=1}^{n}\left | \frac{x_{i}-{x_{i}}'}{x_{i}} \right |
  • PCC(Pearson相关系数)评价实际值与预测值之间的相关性。Pearson相关系数越接近1,恢复效果越好。PCC的计算公式为:
  • \rho _{X_{i},{X_{i}}'}=\frac{cov(X_{i},{X_{i}}')}{\sigma _{x}\sigma _{x{i}'}}

数据预处理

数据预处理包括特征选择、数据清洗和数据转换三个部分。特征选择分析水质特征,选择合适的水质特征进行分析;数据清洗主要包括处理缺失数据、过滤异常值、消除重复值;数据转换是通过规范化数据来实现的。

  • 特征值的选择:从各指标数据缺失数可以看出,该区域溶解氧指数缺失数最少,利用预测模型对该指标的预测具有较高的准确性和可靠性。因此,将溶解氧作为评价该地区水质的主要研究对象。
  • 缺少值处理:由于自然或者任务导致的小部分缺失值对整体情况的影响较小,可以直接消除,本实验直接删除缺失的数据值。
  • 异常值处理:箱线图以四分位数之间的距离作为判断依据,溶解氧最大值和最小值之间存在数据夸张的现象,因此,在数据分析中,这些数据可以视为异常值。
  • 规范化:为了使CNN-LSTM模型在训练过程中收敛更快,具有更高的稳定性,对溶解氧数据进行归一化,使溶解氧数据在[0,1]之间。
  • 数据分割:为了防止模型在训练集中表现良好而在测试集中表现普遍,泛化能力较弱。因此,将915个溶解氧浓度数据按天按时间顺序重新采样,并在训练过程中将数据按8:2的比例分为训练集和验证集。
  • 数据恢复。在对训练后的模型进行评估时,为了消除归一化对预测结果的影响,需要通过逆归一化对预测数据进行恢复,以评估模型预测值的误差。

实验设计与结果

根据水质变化的周期性和非线性特征,以溶解氧为研究对象,构建了卷积神经网络(CNN)和长短期记忆网络(LSTM)相结合的神经网络模型来预测水质溶解氧指数,CNN-LSTM模型主要由输入层、卷积层、池化层、LSTM层、全连接层和输出层组成。首先,对水质监测平台获取的水质数据集进行预处理。其次,使用CNN网络从预处理后的水质数据中提取局部特征,并将比原始水质信息表达能力更好的时间序列传递到LSTM层进行预测。我们通过设置LSTM网络中的神经元数量和CNN网络中卷积核的大小和数量来选择最优参数。最后,分别使用LSTM模型和CNN-LSTM模型对测试样品中的溶解氧浓度进行预测

从图中可以看出,LSTM虽然能较好地预测溶解氧的周期变化,但越显著的值与越小的值之间的拟合相对较差,导致相对偏差。而CNN-LSTM不仅继承了LSTM对时间序列之前数据的记忆和遗忘能力,而且解决了LSTM对大值和小值预测不准确的问题,对峰值的拟合效果更好。可以看出,CNN-LSTM具有更强的鲁棒性预测性能。

LSTM模型的预测曲线
CNN-LSTM模型的预测曲线

CNN-LSTM模型和LSTM模型的四个评价指标MAPE、PCC、MSE和RMSE如表所示。与原有的LSTM预测模型相比,CNN-LSTM在四个评价指标上都有一定程度的提高。混合模型的RMSE比单一模型低5.99%,PCC提高2.80%,MAE降低2.24%,MSE降低11.63%。

研究贡献

  1. 结合CNN模型和LSTM模型各自的优点,提出了一种用于水质预测的混合模型。经过CNN层的特征提取后,原始数据将得到一个比原始序列具有更重要特征能力的新序列。然后,将新序列放入对时间序列处理更敏感的LSTM模型中,通过全连通层输出预测结果。结果表明,该组合模型继承了传统LSTM对文具数据的预测精度,在峰值情况下具有较好的预测效果。
  2. 确定了所提组合神经网络的最优参数。与单一LSTM模型相比,混合模型评价溶解氧数据预测值的4项指标有明显提高,表明组合模型具有更好的预测性能和泛化能力。

Transformer

Encoder-Decoder架构

Transformer 也是一个 Seq2Seq 模型(Encoder-Decoder 框架的模型),左边一个 Encoders 把输入读进去,右边一个 Decoders 得到输出。

Encoder-Decoder 框架的模型是如何进行文本翻译的

  1. 将序列 (x1,x2,⋯,xn)作为 Encoders 的输入,得到输出序列 (z1,z2,⋯,zn)
  2. 把 Encoders 的输出序列 (z1,z2,⋯,zn)作为 Decoders 的输入,生成一个输出序列 (y1,y2,⋯,ym)。注:Decoders 每个时刻输出一个结果 

实际上,Decoders 里面是有N层的,也就是说,Encoders 的输出,会和每一层的 Decoder 进行结合,即Encode的输出传给每一个Decode作为输入。

 

其中每一层的内部细节如下图所示,一般Eecoders和Decode都是 N=6 层,Encoder 包括2个 sub-layers,Decoder 包括3个 sub-layers。注意:每一个子层的传输过程中即Self-Attention的输出都会有一个(残差网络+归一化),再将结果传入Feed Forward。

Encode(编码器)

Encoder 包括两个 sub-layers:

  • 第一个 sub-layer 是 multi-head self-attention,用来计算输入的 self-attention;
  • 第二个 sub-layer 是简单的前馈神经网络层 Feed Forward;

  • input:绿色的 X_{1} 即源词向量 表示 Embedding 层的输出 (可以通过 one-hot、word2vec 得到),给它叠加位置编码(代表 Positional Embedding 的向量),给 X_{1} 赋予位置属性得到黄色的 X_{1}
  • Self-Attention:黄色 X_{1} 输入到 Self-Attention 子层中,做注意力机制 (X_{1}X_{2} 拼接起来的一句话做),得到 Z_{1} (X_{1}X_{1}X_{2}拼接起来的句子做了自注意力机制的词向量,表征的仍然是 Thinking),也就是说 Z_{1} 拥有了位置特征、句法特征、语义特征的词向量;
  • Add&NormalizeX_{1}作为残差结构的直连向量,直接和 Z_{1} 相加,之后进行 Layer Norm 操作,得到粉色向量 Z_{1};(残差结构的作用:避免出现梯度消失的情况,如果 w 特别小,x就没了,[w3(w2(w1x+b1)+b2)+b3+x] ) ;Layer Norm 的作用:为了保证数据特征分布的稳定性,并且可以加速模型的收敛,如果数据变化太大反向传播的计算量就会很大);
  • Feed Forward:该前馈神经网络包括两个线性变换和一个ReLU激活函数,FFN(x)=max(0,W_{1}x+b_{1})W_{2}+b_{2}。线性变化的叠加永远都是线性变化 (线性变化就是空间中平移和扩大缩小),通过做一次非ReLu线性变换,这样的空间变换可以无限拟合任何一种状态了)。Z_{1}经过前馈神经网络(Feed Forward)层,经过残差结构与自身相加,之后经过 LN 层,得到一个输出向量 r_1。
  • 由于 Transformer 的 Encoders 具有 6 个 Encoder, r_1 也将会作为下一层 Encoder 的输入,代替 X_{1} 的角色,如此循环,直至最后一层 Encoder。

Decode(解码器)

解码器会接收编码器生成的词向量,然后通过这个词向量去生成翻译的结果,Decoders 也是 N=6 层,每层 Decoder 包括 3 个 sub-layers:

  • 第一个 sub-layer 是 Masked multi-head self-attention,也是计算输入的 self-attention;
  • 第二个 sub-layer 是 Encoder-Decoder Attention 计算,对 Encoder 的输入和 Decoder 的Masked multi-head self-attention 的输出进行 attention 计算;
  • 第三个 sub-layer 是前馈神经网络层,与 Encoder 相同。

第一个Self-Attention采用了masked操作,因为在翻译的过程中是顺序翻译的,即完成第一个单词才能继续翻译第二单词。采用mask是为了防止模型提前知道i+1单词的信息。

第二层Self-Attention,即Encode-Decode Attention, 与Multi-Head Attention要的区别在于其中 Self-Attention 的 K, V矩阵不是使用 上一个 Decoder block 的输出计算的,而是使用 Encoder 的编码信息矩阵 C 计算的。根据 Encoder 的输出 C计算得到 K, V,根据上一个 Decoder block 的输出 Z 计算 Q (如果是第一个 Decoder block 则使用输入矩阵 X 进行计算),后续的计算方法与之前描述的一致。这样做的好处是在 Decoder 的时候,每一位单词都可以利用到 Encoder 所有单词的信息 (这些信息无需 Mask)。

Transformer 输出结果

假设上图是训练模型的某一个阶段,我们来结合 Transformer 的完整框架描述下这个动态的流程图:

  1. 输入 “je suis etudiant” 到 Encoders,然后得到一个 K_{e}V_{e} 矩阵;
  2. 输入 “I am a student” 到 Decoders ,首先通过 Masked Multi-head Attention 层得到 “I am a student” 的 attention 值Q_{d},然后用 attention 值 Q_{d} 和 Encoders 的输出 K_{e}V_{e} 矩阵进行 attention 计算,得到第 1 个输出 “I”;
  3. 输入 “I am a student” 到 Decoders ,首先通过 Masked Multi-head Attention 层得到 “I am a student” 的 attention 值 Q_{d},然后用 attention 值 Q_{d} 和 Encoders 的输出 K_{e}V_{e}  矩阵进行 attention 计算,得到第 2 个输出 “am”;
  4. 以此重复直到得到终止符

从上图可以看出,Transformer 最后的工作是让解码器的输出通过线性层 Linear 后接上一个 softmax。

  • 其中线性层是一个简单的全连接神经网络,它将解码器产生的向量 A 投影到一个更高维度的向量 B 上,假设我们模型的词汇表是10000个词,那么向量 B 就有10000个维度,每个维度对应一个惟一的词的得分。
  • 之后的softmax层将这些分数转换为概率。选择概率最大的维度,并对应地生成与之关联的单词作为此时间步的输出就是最终的输出。

Q&&A

1、为什么 Decoder 需要做 Mask-Self-Attention?

  • 训练阶段:我们知道 “je suis etudiant” 的翻译结果为 “I am a student”,我们把 “I am a student” 的 Embedding 输入到 Decoders 里面,翻译第一个词 “I” 时

    • 如果对 “I am a student” attention 计算不做 mask,“am,a,student” 对 “I” 的翻译将会有一定的贡献
    • 如果对 “I am a student” attention 计算做 mask,“am,a,student” 对 “I” 的翻译将没有贡献
  • 测试阶段:我们不知道 “我爱中国” 的翻译结果为 “I love China”,我们只能随机初始化一个 Embedding 输入到 Decoders 里面,翻译第一个词 “I” 时:

    • 无论是否做 mask,“love,China” 对 “I” 的翻译都不会产生贡献
    • 但是翻译了第一个词 “I” 后,随机初始化的 Embedding 有了 “I” 的 Embedding,也就是说在翻译第二词 “love” 的时候,“I” 的 Embedding 将有一定的贡献,但是 “China” 对 “love” 的翻译毫无贡献,随之翻译的进行,已经翻译的结果将会对下一个要翻译的词都会有一定的贡献,这就和做了 mask 的训练阶段做到了一种匹配

总结下就是:Decoder 做 Mask,是为了让训练阶段和测试阶段行为一致,不会出现间隙,避免过拟合。

2、为什么 Encoder 给予 Decoders 的是 K、V 矩阵?

我们在讲解 Attention 机制中曾提到,Query 的目的是借助它从一堆信息中找到重要的信息。

现在 Encoder 提供了 K_{e}V_{e}  矩阵,Decoder 提供了 Q_{d} 矩阵,通过 “我爱中国” 翻译为 “I love China” 这句话说明。

当我们翻译 “I” 的时候,由于 Decoder 提供了 Q_{d} 矩阵,通过与 K_{e}V_{e}  矩阵的计算,它可以在 “我爱中国” 这四个字中找到对 “I” 翻译最有用的单词是哪几个,并以此为依据翻译出 “I” 这个单词,这就很好的体现了注意力机制想要达到的目的,把焦点放在对自己而言更为重要的信息上。

  • 其实上述说的就是 Attention 里的 soft attention机制,解决了曾经的 Encoder-Decoder 框架的一个问题,在这里不多做叙述,有兴趣的可以参考网上的一些资料。
    • 早期的 Encoder-Decoder 框架中的 Encoder 通过 LSTM 提取出源句(Source) “我爱中国” 的特征信息 C,然后 Decoder 做翻译的时候,目标句(Target)“I love China” 中的任何一个单词的翻译都来源于相同特征信息 C,这种做法是极其不合理的,例如翻译 “I” 时应该着眼于 “我”,翻译 “China” 应该着眼于 “中国”,而早期的这种做法并没有体现出,然而 Transformer 却通过 Attention 的做法解决了这个问题。

"拿到局部信息去全局信息里找重点 "

Transformer的代码实现

从功能角度上,可以将Transformer各个组件划分到四个功能模块,即输入模块、编码器模块、解码器模块、输出模块。

模块1:输入模块

  1. 将词库中每一个词以及开始标志转化为嵌入词向量,词嵌入向量的维度为embedding dimension,一句话中总共的单词长度为sequence length,因此可以得到一个sequence_length大小的矩阵,其中每一行代表的是一个词。
  2. 在矩阵中添加位置编码,随后将矩阵传入编码器模块。

输入模块有两个部分,一个是编码器的输入,一个是解码器的输入,功能实现是一模一样的,区别在于一个是对源数据进行编码,也就是我们现在第一步中所做的操作,另一个是对目标数据进行编码。

1)词嵌入:Word Embedding层 

class Embedding(nn.Module):
    # embedding层
    def __init__(self, d_model, vocab):
        # d_model: 词嵌入的维度(转换后获得的词向量的维度)
        # vocab:词表 的大小
        super(Embedding, self).__init__()
        # 定义Embedding层
        self.lut = nn.Embedding(vocab, d_model)
        # 将参数传入类中
        self.d_model = d_model
    
    def forward(self, x):
        # x:代表输入进模型的文本通过词汇映射后的数字张量
        return self.lut(x) * math.sqrt(self.d_model)  
        # 乘上d_model的开根号是为了对最后结果进行一个缩放

2、位置编码器:Position Embedding层

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout, max_len=5000):
        """
        位置编码器类的初始化函数
        d_model:词嵌入维度
        dropout:置0比率
        max_len:每个句子的最大长度
        """
        super(PositionalEncoding, self).__init__()
        
        # 实例化nn中预定义的Dropout层, 并将dropout传入其中,获得对象self.dropout
        self.dropout = nn.Dropout(p=dropout)
        
        # 初始化一个位置编码矩阵,他是一个0矩阵,矩阵的大小是  max_len * d_model  
        pe = torch.zeros(max_len, d_model)
        
        # 初始化一个绝对位置矩阵,在这里词汇的绝对位置就是用它的索引去表示,首先使用arange方法获                                
        得一个连续自然数向量,然后在使用unsqueeze方法拓展矩阵维度,又因为参数传的是1, 代表矩阵            
        拓展的位置,会使向量编程一个max_len * 1的矩阵。
        position = torch.arange(0, max_len).unsqueeze(1)
        # 绝对位置矩阵初始化之后,接下来就是考虑如何将这个位置信息加入到位置编码矩阵中
        # 最简单方法就是现将max_len * 1的绝对位置矩阵,变换成max_len * d_model形状, 然后直接覆    
        盖原来的初始化位置编码矩阵即可。
        # 要实现这种矩阵变化,就需要一个1 * d_model形状的变化矩阵div_term,我们对这个变化矩阵的    
        要求除了形状外,还希望他能够将自然数的绝对位置编码缩放层足够小的数字,有助于在之后的梯度     
        下降过程中更快地收敛,初始化得到一个自然数矩阵,只初始化了一半,即x*d_model/2的矩阵, 这     
        是因为这里并不是真正意义的初始化了一半的矩阵。可以把它看作初始化了两次,而两次初始化     
        的变化矩阵不同的处理,并把这两个矩阵分别填充在位置编码矩阵的偶数和奇数位置上,组成最终的 
        位置编码矩阵。
        
        div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(1000.0)/ d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        # 这样我们就得到了位置编码矩阵pe, pe现在还只是一个二维矩阵,要想和embeddding的输出(一         
        个三维矩阵)就必须多一个维度,所以这里使用unsequeeze拓展维度
        pe = pe.unsqueeze(0)  # 将二维张量扩充为三维张量
        # 最后把pe位置编码矩阵注册层模型的buffer,buffer把它认为是对模型效果有帮助的,但是却不 
        是模型结构中超参数或者参数,不需要说着优化步骤进行更新的增益
        # 注册之后就可以在模型保存后重新加载时,模型结构与参数已通被加载
        self.register_buffer('pe',  pe)
        
        def forward(self, x):
        """
        forward函数的参数是x,表示文本序列的词嵌入表示
        """
        # 在相加之前对pe做一些适配工作,将这个三维张量的第二位也就是句子最大长度的那一维度切     
        片与输入的x的第二维相同即x.size(1)
        # 因为默认max_len为5000,一般来讲是在太大了,很难有一个句子包含5000词汇,所以要进行 
        与输入转给你来那个的适配
        # 最后使用Variable进行封装,使其与x的样式相同,但是他是不需要进行梯度求解的,因为这里的     
        位置信息使用的是函数计算方式,且这一步中只是数字相加,因此把requuires_grad设置成False。
        
        x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
        # 最后就使用self.dropout对象精细‘丢弃’操作,并返回结果
        return self.dropout(x)

模块2:编码器

编码器模块接收到输入模块传输过来的矩阵后,要完成多头注意力计算、规范化、前向传播等等一系列操作,而且这些操作还不止进行一次(8次),最终将输出结果传入解码器模块。

1)掩码层 

def subsequent_mask(size):
    """
    生成向后遮掩的掩码张量,参数size是掩码张量最后两个维度的大小,最后两维形成一个方阵
    """
    # 在函数中,首先定义掩码张量的形状
    attn_shape = (1, size, size)
    # 然后使用np.ones方法想这个形状中添加1元素,形成上三角阵,最后为了节约空间
    # 再使用其中的数据类型变为无符号8为整型unit8
    sub_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    # 最后将numpy类型转化为torch中的tensor,内部做一个1 - 的操作
    # 在这个其实是做了一个三角阵的反转,subsequent_mask中的每个元素都会被1减,
    # 如果是0, subsequent_mask中的该位置由0变1
    # 如果是1, subsequent_mask中的该位置由1变0
    return torch.from_numpy(1 - sub_mask)

 2)注意力层

def attention(query, key, value, mask=None, dropout=None):
    """
    注意力机制的实现,输入分别是query,key,value,mask:掩码张量,
    dropout是nn.Dropout层的实例化对象,默认为None
    """
    # 在函数中,首先去query的最后一维的大小,一般情况下就等同于我们的词嵌入维度,命名为d_k
    d_k = query.size(-1)
    # 按照注意力公式,将query与key的转置相乘,这里面key是将最后两个维度进行转置(转置后才满足矩阵 
    乘法),key转置之后shape为[1, 4, 6],[1, 6, 4] * [1, 4, 6] = [1, 6, 6]
    # 再除以缩放系数,就得到注意力得分张量scores
    scores = torch.matmul(query, key.transpose(-2, -1)) // math.sqrt(d_k)
    # 接着判断是否使用掩码张量
    if mask is not None:
        # 使用tensor的masked_fill方法,将掩码张量和scores张量每个位置一一比较,如果掩码张量
        # 则对应的scores张量用-1e9这个值来替换
        scores = scores.masked_fill(mask == 0, -1e9)
    # 对scores的最后一维进行softmax操作,使用F.softmax方法,第一个参数softmax对象,第二个
    # 这样获得最终的注意力张量
    p_attn = F.softmax(scores, dim=-1)
    # 之后判断是否使用dropout进行随机置零
    if dropout is not None:
        # 将p_attn传入dropout对象中进行“丢弃”处理
        p_attn = dropout(p_attn)
    #最后,根据公司将p_atten与value张量向曾获得最终的query注意力表示,同时返回注意力权重张量
    return torch.matmul(p_attn, value), p_attn

3)多头注意力机制层

# 首先需要定义克隆函数,因为在多头注意力机制的实现中,用到多个结构相同的线形层
# 将使用clone函数将他们已通初始化在同一个网络层列表对象中,之后的结构中也会用到该函数
def clone(model, N):
    # 用于生成相同网络层的克隆函数,它的参数module表示要克隆的目标网络层,N代表需要克隆的数量
    # 在函数中,通过for循环对module进行N次深度拷贝,使其每个module称为独立的层
    return nn.ModuleList([copy.deepcopy(model) for _ in range(N)])


# 使用一个类来实现多头注意力机制的处理
class MultiHeadAttention(nn.Module):
    def __init__(self, head, embedding_dim, dropout=0.1):
        """
        在类的初始化时,会传入三个参数,head代表头数,embedding_dim代表词嵌入的维度,dropout代表进行dropout操作时置零比率,默认是0.1
        """
        super(MultiHeadAttention, self).__init__()
        # 在函数中,首先使用了一个测试中常用的assert语句,判断h是否能被d_model整除
        # 这是因为之后要给每个头分配等量的词特征,也就是embedding_dim//head个
        assert embedding_dim % head == 0
        self.head = head  # 传入头数
        self.embedding_dim = embedding_dim
        self.dropout = nn.Dropout(p=dropout)
        self.d_k = embedding_dim // head  # 得到每个头获得的分割词向量维度d_k
        # 然后获得线形层对象,通过nn的Linear实例化,它的内部变化矩阵是embedding_dim * Embedding_dim
        self.linears = clone(nn.Linear(embedding_dim, embedding_dim), 4)
        self.attn = None
    
    def forward(self, query, key, value, mask=None):
        """前向逻辑函数,它的输入参数有4个,前三个就是注意力机制需要的Q、K、V,
        最后一个是注意力机制中可能需要的mask掩码张量,默认是None"""
        if mask is not None:  # 如果存在掩码张量
            mask = mask.unsqueeze(0)  # 使用unsqueeze拓展维度,代表多头中的第n头
        # 接着,我们获得一个batch_size的变量,它是query尺寸的第1个数字,代表有多少条样本
        batch_size = query.size(0)
        # 之后就进入多头处理环节
        # 首先利用zip将输入QKV与三个线形层组到一起,然后使用for循环,将输入QKV分别传入到线形层中
        # 完成线性变换后,开始为每个头分割输入,这里使用view方法对线性变化的结果进行维度重塑
        # 这样就意味着每个头可以获得一部分词特征组成的句子,其中的-1代表自适应维度
        # 计算机会根据这种变换自动计算这里的值,然后对第二维和第三维进行转置操作
        # 为了让代表句子长度维度和词向量维度能够相邻,这样注意力机制才能找到迟疑与句子位置的关系
        # 从attention函数中可以看到,利用的是原始输入的倒数第一和第二,这样就得到了每个头的
        print('----------')
        print(query.shape)
        print(key.shape)
        print(value.shape)
        # 此时,query, key, value的shape为[1, 6, 4], 即[batch size, sequence length, embedding dimension]
        query, key, value = \
               [model(x).view(batch_size, -1, self.head, self.d_k).transpose(1, 2) 
                for model, x in zip(self.linears, (query, key, value))]
        print('-----------')
        print( query.shape)
        print(key.shape)
        print(value.shape)
        print('-----------')
        # 此时,query, key, value的shape修改为:[1, 2, 6, 2] , [batch size, sequence length, h, embedding dimension / h]
        # 得到每个头的输入后,接下来就是将他们传入attention中
        # 这里直接调用之前实现的attention函数,同时也将mask和dropout传入其中
        x, self.attn = attention(query, key, value, mask, self.dropout)
        print('x- after attention: ', x.shape)  # 经过注意力机制后,输出x的shape为:[1, 2, 6, 2], [batch size, h, sequence length, embedding dimension / h]
        # 通过多头注意力计算后,我们就得到了每个头计算结果组成的4维张量,需要将其转换为输入的
        # 因此这里开始进行第一步处理环节的逆操作,先对第二和第三维惊喜转置,然后使用contiguous方法
        # 这个方法的作用就是能够让转置后的张量应用view方法,否则将无法直接使用
        # 所以,下一步就是使用view方法重塑形状,变成和输入形状相同
        x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.head * self.d_k)  # 对x进行reshape
        print('x', x.shape)  # x的shape回到[1, 6, 4],  [batch size, sequence length, embedding dimension]
        return self.linears[-1](x)

4)前馈全连接层

class PositionalwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        """
        d_model: 线形层的输入维,因为希望输入通过前馈全连接层后输入和输出的维度不变
        d_ff: 第二个线形层
        droupout:置零比率
        """
        super(PositionalwiseFeedForward, self).__init__()
        # 首先使用nn实例化了两个线形层对象
        # 他们的参数分别是d_model, d_ff和d_ff, d_model
        self.w1 = nn.Linear(d_model, d_ff)
        self.w2 = nn.Linear(d_ff, d_model)
        # 然后使用nn的Dropout实例化了对象self.dropout
        self.dropout = nn.Dropout(p=dropout)
    
    def forward(self, x):
        """输入参数为x,代表来自上一层的输出"""
        # 首先经过第一个线性层,然后使用Funtional中relu函数进行激活,
        # 之后再使用dropout进行随机置0,最后通过第二个线性层w2,返回最终结果.
        return self.w2(self.dropout(F.relu(self.w1(x))))

5)规范化层

class LayerNorm(nn.Module):
    def __init__(self, features, eps=1e-6):
        # features:词嵌入的维度
        # eps:一个足够小的数,在规范化公式的分母中出现,房子分母为0。
        super(LayerNorm, self).__init__()
        # 根据features的形状初始化两个参数张量a2和b2,a2为元素全为1的张量,b2为元素全为0的张量。这两个张量就是规范化层的参数
        # 因为直接对上一层得到的结果做规范化公式计算,将改变结果的正常表征,因此就需要有参数作为调节因子
        # 使其既能满足规范化要求,又能不改变针对目标的表征,最后使用nn.parameter封装,代表它们是模型的参数
        self.eps = eps
        self.a2 = nn.Parameter(torch.ones(features))
        self.b2 = nn.Parameter(torch.zeros(features))
        
    def forward(self, x):
        # x来自于上一层的输出
        # 在函数中,首先对输入变量x求其最后一个维度的均值,并保持输出维度与输入维度一致
        # 接着再求最后一个维度的标准差,然后就是根据规范化公式,用x减去均值除以标准差获得规范化的结果
        # 最后对结果乘以缩放参数,即a2, *号代表同型点乘,即对应位置进行乘法操作,加上位移参数b2返回即可
        # 
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a2 * (x - mean) / (std + self.eps) + self.b2

6)子层连接结构

class SubLayerConnection(nn.Module):
    def __init__(self, size, dropout=0.1):
        # size: 词嵌入的维度大小
        # dropout:随机置零比率
        super(SubLayerConnection, self).__init__()
        self.size = size
        self.dropout = nn.Dropout(p=dropout)
        # 实例化了规范化对象self.norm
        self.norm = LayerNorm(size)
    
    def forward(self, x, sublayer):
        # 前向逻辑函数中,接收上一层或者子层的输入作为第一个参数
        # 将该子层连接中的子层函数作为第二个函数
        # 首先对输出进行规范化,然后将结果传给子层处理,之后再对子层进行dropout操作
        # 随机停止一些网络中神经元的作用,来房子过拟合,最后还有一个add操作
        # 因为存在跳跃连接,所以是将输入x与dropout后的子层输出结果相加作为最终的子层连接输出
        return x + self.dropout(sublayer(self.norm(x)))

7)编码器层

class EncoderLayer(nn.Module):
    def __init__(self, size, self_attn, feed_forward, dropout):
        # size:词嵌入维度的大小
        # self_attn:传入多头注意力子层实例化对象,并且是自注意力机制
        # feed_forward:前馈全连接层实例化对象
        # dropout:置零比率
        super(EncoderLayer, self).__init__()
        self.size = size
        # 首先将self_attn和feed_forward传入其中
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        self.dropout = nn.Dropout(p=dropout)
        # 编码器层中有两个子层连接结构,所以使用clones函数进行克隆
        self.sublayer = clone(SubLayerConnection(size, dropout), 2)
    
    def forward(self, x, mask):
        # x:上一层输出
        # mask:掩码张量
        # 里面就是按照结构图左侧的流程,首先通过第一个子层连接结构,其中包含多头注意力子层,然后通过第二个子层连接结构,
        #其中包含前馈全连接子层,最后返回结果
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        return self.sublayer[1](x, self.feed_forward)

8)编码器

class Encoder(nn.Module):
    def __init__(self, layer, N):
        # layer:编码器层
        # N: 编码器层的个数
        super(Encoder, self).__init__()
        # 首先使用clones函数克隆N个编码器层放在self.layers中
        self.layers = clone(layer, N)
        # 再初始化一个规范化层,将用在编码器的最后面
        self.norm = LayerNorm(layer.size)
 
    def forward(self, x, mask):
        # forward函数的输入和编码器层相同,x代表上一层的输出,mask代表掩码张量
        # 首先就是对我们克隆的编码器层进行循环,每次都会得到一个新的x
        # 这个循环的过程,就相当于输出的x经过N个编码器层的处理
        # 最后在通过规范化层的对象self.norm进行处理,最后返回结果
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

模块3:解码器

输出矩阵A从编码器传到了解码器模块,解码器接收到矩阵A后,解码器对应的输入模块也开始工作,这个输入模块会对目标数据进行词嵌入、添加位置编码等一些列操作,形成另一个矩阵B。矩阵B进入解码器后,进行掩码,再进行一次注意力计算,然后往前传递,与矩阵A“胜利会师”。会师后,两者共同进行注意力计算,生成新的矩阵,并进行前向传播和规范化。进行一系列类似操作后,将矩阵传入输出模块。从这里可以看出,解码器模块,是有两个输入的,一个来自于编码器,一个来自于输入模块对目标数据的编码。

1)解码器层

class DecoderLayer(nn.Module):
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout=0.1):
        # size:词嵌入的维度大小,同时也代表解码器层的尺寸
        # self_attn:多头注意力对象,也就是说这个注意力机制需要Q=K=V
        # src_attn:多头注意力对象,这里Q!=K=V
        # feed_forward:前馈全连接层对象
        super(DecoderLayer, self).__init__()
        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.dropout = nn.Dropout(p=dropout)
        # 按照结构图使用clones函数克隆三个子层连接对象
        self.sublayer = clone(SubLayerConnection(size, dropout), 3)
    
    def forward(self, x, memory, source_mask, target_mask):
        # x:来自上一层的输出
        # mermory:来自编码器层的语义存储变量
        # 源数据掩码张量和目标数据掩码张量
        m = memory
        # 将x传入第一个子层结构,第一个子层结构分别是x和self-attn函数,因为是自注意力机制,所以Q,K,V都是x
        # 最后一个参数的目标数据掩码张量,这是要对目标数据进行遮掩,因为此时模型可能还没有生成任何目标数据
        # 比如扎起解码器准备生成第一个字符或者词汇时,其实已经传入了第一个字符以便计算损失
        # 但是我们不希望在生成第一个字符时模型还能利用这个信息,因此我们会将其遮掩,同样生成第二个字符或词汇时
        # 模型只能使用第一个字符或者词汇信息,第二个字符以及之后的信息都不允许模型使用
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, target_mask))
        # 接着进入第二个子层,这个职称中常规的注意力机制,q是输入x;k,v是编码层输出的memory
        # 同样也传入source_mask,但是进行源数据遮掩的原因并非是抑制信息泄露,而是这笔掉对结果没有意义的字符而陈胜的注意力值
        # 以此提升模型效果和训练速度,这样就完成了第二个子层的处理
        x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, source_mask))
        # 最后一个子层就是前馈全连接子层,经过它的处理后,就可以返回结果,这就是解码器层结构。
        return self.sublayer[2](x, self.feed_forward)

2)解码器 

class Decoder(nn.Module):
    def __init__(self, layer, N):
        # layer:解码器层layer
        # N:解码器层的个数N
        super(Decoder, self).__init__()
        # 首先使用clones方法克隆了N个layer,然后实例化了一个规范化层
        # 因为数据走过了所有的解码器层后,最后要做规范化处理
        self.N = N
        self.layers = clone(layer, N)
        self.norm = LayerNorm(layer.size)
    
    def forward(self, x, memory, source_mask, target_mask):
        # x:数据的嵌入表示
        # memory:编码器层的输出
        # source_mask:源数据掩码张量
        # target_mask:目标数据掩码张量
        
        # 对每个层进行循环,淡然这个循环就是变量x通过每一个层的处理
        # 得出最后的结果,再进行一次规范化返回即可
        for layer in self.layers:
            x = layer(x, memory, source_mask, target_mask)
        return self.norm(x)

模块4:输出模块

最后一步即输出模块对解码器模块的输出矩阵进行一次线性变换,然后通过softmax层转换为概率分布矩阵,矩阵中概率最大值对应的英文词汇为这一次传输的输出结果,即翻译结果。

# nn.functional工具包装载了网络层中那些只惊喜年计算,而没有参数的层
# 将线形层和softmax计算层一起实现,因为二者的共同目标是生成最后的结构
# 因此把类的名字叫做Generator,生成器类
class Generator(nn.Module):
    def __init__(self, d_model, vocab_size):
        # d_model:词嵌入维度
        # vocab_size:词表大小
        super(Generator, self).__init__()
        # 首先就是使用nn中的预定义线形层进行实例化,得到一个对象self.project等待使用
        # 这个线性层的参数有两个,计时初始化函数时传进来的 d_model和vocab_size
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.project = nn.Linear(d_model, vocab_size)
    
    def forward(self, x):
        # 前向逻辑函数中输入是上一层输出张量x
        # 在函数中,首先使用上一步得到的self.project对x进行线性变化
        # 然后使用F中已经实现的log_softmax进行的softmax处理
        return F.log_softmax(self.project(x), dim=-1)

编码器-解码器模型构建

# 编码器-解码器结构实现
class EncoderDecoder(nn.Module):
    def __init__(self, encoder, decoder, source_embed, target_embed, generator):
        # encoder:编码器对象
        # decoder:解码器对象
        # source_embed:源数据嵌入函数
        # target_embed:目标数据嵌入函数
        # generator:输出部分的类别生成器对象
        super(EncoderDecoder, self).__init__()
        # 将参数传入类中
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = source_embed
        self.tgt_embed = target_embed
        self.generator = generator
    
    def encode(self, source, source_mask):
        # 使用src_embed对source做处理,然后和source_mask一起传给self.encoder
        return self.encoder(self.src_embed(source), source_mask)
    
    def decode(self, memory, source_mask, target, target_mask):
        # 解码函数
        # 使用tgt_embed对target做处理,然后和source_mask、target_mask、memory一起传给self.decoder
        return self.decoder(self.tgt_embed(target), memory, source_mask, 
                            target_mask)
    
    def forward(self, source, target, source_mask, target_mask):
        # source:源数据
        # target:目标数据
        # source_mask、target_mask:对应的掩码张量
        
        # 在函数中,将source、source_mask传入编码函数,得到结果后,与source-mask、target、target_mask一同传给解码函数
        return self.decode(self.encode(source, source_mask), source_mask, 
                           target, target_mask)

堆叠成一个完整的Transformer模型

def make_model(source_vocab, target_vocab, N=6, d_model=512,
               d_ff=2048, head=8, dropout=0.1):
    # source_vocab:源数据特征(词汇)总数
    # target_vocab:目标数据特征(词汇)总数
    # N:编码器和解码器堆叠数
    # d_model:词向量映射维度
    # d_ff:前馈全连接网络中变化矩阵的维度
    # head:多头注意力机制中多头数
    # dropout:置零比率
    
    # 首先得到一个深度拷贝命令,接下来很多结构都要进行深度拷贝,从而保证它们彼此之间相互独立,不受干扰
    c = copy.deepcopy
    # 实例化了多头注意力机制类,
    attn = MultiHeadAttention(head, d_model, dropout)
    # 然后实例化前馈全连接类
    ff = PositionalwiseFeedForward(d_model, d_ff, dropout)
    # 实例化位置编码器类
    position = PositionalEncoding(d_model, dropout)
    # 根据结构图,最外层是EncoderDecode,在EncoderDecoder中
    # 分别是编码器层,解码器层,源数据Embedding层和位置编码组成的有序结构
    # 目标数据Embedding层和位置编码组成的有序结构,以及类别生成器层
    # 在编码器层中有attention子层以及前馈全连接子层
    # 在解码器层中有两个attention子层以及前馈全连接层
    model = EncoderDecoder(
            Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
            Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
            nn.Sequential(Embedding(d_model, source_vocab), c(position)),
            nn.Sequential(Embedding(d_model, target_vocab), c(position)),
            Generator(d_model, target_vocab))
    # 模型构建完成后,接下来就是初始化模型中的参数,比如线形层的变化矩阵
    # 这里一旦判断参数的维度大于1,则会将其初始化成为一个服从均匀分布的矩阵
    for p in model.parameters():
        if p.dim() > 1:
            nn.init.xavier_uniform(p)
    return model

总结

Transformer模型中最重要就是使用了Self-Attention机制,其中用到了多头自注意力和掩码多头自注意力,其中不同的是在解码器中的第二个Self-Attention中的K、V来自于编码器的输出即来自于源语句,Q来自于解码器的输入即已经生成的结果,通过Q去查询源语言中最有可能是下一个生成输出的词,再将新生成的词加入到解码器的输入中,再继续生成下一个目标词。输入可以是各种数据,因为可以反向更新。编码器主要是特征提取,而解码器主要是生成。

CNNRNNLSTMTransformer
概念通过卷积抽取特征序列特征抽取,融合前文的状态信息在RNN的基础上,增加了cel状态和三个门,用来保存历史状态信息提出self-attention和multi-head attention
优点简单快速能够融合序列的时序特征解决了梯度消失的问题,可以保存更长期的依赖更好的保存长期依赖信息,无视序列中的距离;可以并行化计算;语义表示更丰富
缺点无法对时间建模无法解决长期依赖的问题;存在梯度消失和梯度爆炸的问题太长的序列仍会丢失信息;时序模型,无法并行化运算,速度慢位置信息利用不明显,计算量大

  • 15
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值