NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)

NLP-Beginner 任务二:基于深度学习的文本分类

  • 传送门
  • 一. 介绍
    • 1.1 任务简介
    • 1.2 数据集
    • 1.3 流程介绍
  • 二. 特征提取——Word embedding(词嵌入)
    • 2.1 词嵌入的定义
    • 2.2 词嵌入的词向量说明
    • 2.3 词嵌入模型的初始化
      • 2.3.1 随机初始化
      • 2.3.2 预训练模型初始化
    • 2.4 特征表示
  • 三. 神经网络
    • 3.1 卷积神经网络(CNN)
      • 3.1.1 卷积层(Convolution)
        • 3.1.1.1 卷积定义
        • 3.1.1.2 卷积的步长与零填充
        • 3.1.1.3 卷积层设计
        • 3.1.1.4 总结
      • 3.1.2 激活层(可选)
      • 3.1.3 汇聚层/池化层(Pooling)
      • 3.1.4 全连接层(Fully connected)
      • 3.1.5 总结
    • 3.2 循环神经网络(RNN)
      • 3.2.1 隐藏层(Hidden)
      • 3.2.2 激活层(可选)
      • 3.2.3 全连接层(Fully connected)
      • 3.2.4 总结
    • 3.3 训练神经网络
      • 3.3.1 神经网络参数
      • 3.3.2 损失函数
      • 3.3.3 参数求解——梯度下降
  • 四. 代码及实现
    • 4.1 实验设置
    • 4.2 结果展示
      • 4.2.1 Part 1
      • 4.2.2 Part 2
    • 4.3 代码
      • 4.3.1 主文件——main.py
      • 4.3.2 特征提取——feature_batch.py
      • 4.3.3 神经网络——Neural_network_batch.py
      • 4.3.4 结果&画图——comparison_plot_batch.py
  • 五. 总结
  • 六. 自我推销

传送门

NLP-Beginner 任务传送门

我的代码传送门

数据集传送门

一. 介绍

1.1 任务简介

本次的NLP(Natural Language Processing)任务是利用深度学习中的卷积神经网络(CNN)和循环神经网络(RNN)来对文本的情感进行分类。

1.2 数据集

数据集传送门

训练集共有15万余项,语言为英文,情感分为0~4共五种情感。

例子

输入: A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story .
输出: 1

输入: This quiet , introspective and entertaining independent is worth seeking .
输出: 4

输入:Even fans of Ismail Merchant 's work
输出: 2

输入: A positively thrilling combination of ethnography and all the intrigue , betrayal , deceit and murder of a Shakespearean tragedy or a juicy soap opera .
输出: 3

1.3 流程介绍

本篇博客将会一步一步讲解如何完成本次的NLP任务,具体流程为:

数据输入(英文句子)→特征提取(数字化数据)→神经网络设计(如何用pytorch建立神经网络模型)→结果输出(情感类别)

二. 特征提取——Word embedding(词嵌入)

2.1 词嵌入的定义

词嵌入模型即是把每一个词映射到一个高维空间里,每一个词代表着一个高维空间的向量,如下图:
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第1张图片
图中的例子是把每个单词映射到了一个7维的空间。

词嵌入模型有三点好处:

  1. 当向量数值设计合理时,词向量与词向量之间的距离能体现出词与词之间的相似性

比如,上面的四个单词,cat和kitten是近义词,所以他们在高维空间中很相近(为了更好地展现它们的空间距离,可以把7维向量做一个线性变换变成2维,然后进行展示)。
然而,dog和cat意思并不相近,所以离得稍远一点,而houses的意思和cat,kitten,dog更加不相近了,因此会离得更远。

  1. 当向量数值设计合理时,词向量与词向量之间的距离也有一定的语义。

比如下面的四个单词,man和woman是两种性别,king和queen也是对应的两种性别,这两对的单词的差异几乎一致(性别差异),因此他们的距离应该也应该是几乎相同的。
因此,可以从图中看到,man和woman的距离恰好与king和queen的距离相等。

  1. 用相对较少的维数展现多角度特征差异

词袋模型和N元特征所提取出来的的特征向量都是超高维0-1向量,而词嵌入模型的向量每一维是实数,即不仅仅是0或1。
也就是说,词袋模型和N元特征所形成的特征矩阵是稀疏的,但是规模又很大,因而信息利用率很低,其词向量与词向量之间的距离也不能体现词间相似性。
而词嵌入模型所形成的特征矩阵不是稀疏的,且规模相对较小,因此能更好的利用每一维的信息,不再只是局限于0或1。
词袋模型和N元特征的定义可以参考《NLP-Beginner 任务一:基于机器学习的文本分类》。

2.2 词嵌入的词向量说明

在词袋模型/N元特征中,只要设置好词和词组,就能把每一个句子(一堆词)转换成对应的0-1表示。

但是,在词嵌入模型中,并没有明确的转换规则,因此我们不可能提前知道每一个词对应的向量。

上面的图的7维向量分别对应(living being, feline, human, gender, royalty, verb, plural),然而这只是为了方便理解所强行设计出的分类,很明显houses在gender维度的取值很难确定。因此,给每一个维度定义好分类标准,是不可能的。

所以,在词嵌入模型中,我们选择不给每一个维度定义一个所谓的语义,我们只是单纯的把每一个词,对应到一个词向量上,我们不关心向量的数值大小代表什么意思,我们只关心这个数值设置得合不合理。换句话说,每个词向量都是参数,是待定的,需要求解,这点和词袋模型/N元特征是完全不同的。

2.3 词嵌入模型的初始化

既然词向量是一个参数,那么我们就要为它设置一个初始值。

而上面2.1提到的词嵌入前两大好处,是基于一个前提:向量数值设计合理,因此选取参数的初始值至关重要。

如果参数的初始值选的不好,那么优化模型求解的时候就会使参数值难以收敛,或者收敛到一个较差的极值;相反,如果选得好,就能求出一个更好的参数,甚至能起到加速模型优化的效果。

一般来说,有两种初始化的方法。

2.3.1 随机初始化

随机初始化这种方式十分简单粗暴。

给定一个维度d(比如50),对于每一个词 w w w,我们随机生成一个d维的向量 x ∈ R d x\in \mathbb{R}^d xRd

注:随机生成的方式有很多,比如 x ∼ N ( 0 , σ 2 I d ) x\sim N(\textbf{0},\sigma^2I_d) xN(0,σ2Id),即 x x x服从于一个简单的多元标准正态分布,等等。

这种初始化方式非常简单,但是有可能会生成较劣的初值,也没有一个良好的解释性。

2.3.2 预训练模型初始化

预训练模型初始化,顾名思义,就是拿别人已经训练好的模型作为初值。

也就是说,把别人已经设置好的词向量直接拿过来用。

这种方式的初始化时间会比较长,因为要从别人的词库里面找,需要一定的时间,但是这种初值无疑比随机初始化要好很多,毕竟是别人已经训练好的模型。

网上也有很多训练好的词嵌入模型,比如GloVe(本篇文章会用到)。

2.4 特征表示

给定每个词的词向量,那么就可以把一个句子量化成一个ID列表,再变成特征(矩阵)。

例子:(d=5)

(ID:1)I :    [ + 0.10 , + 0.20 , + 0.30 , + 0.50 , + 1.50 ] \quad\; [+0.10, +0.20, +0.30, +0.50, +1.50] [+0.10,+0.20,+0.30,+0.50,+1.50]
(ID:2)love : [ − 1.00 , − 2.20 , + 3.40 , + 1.00 , + 0.00 ] [-1.00, -2.20, +3.40, +1.00, +0.00] [1.00,2.20,+3.40,+1.00,+0.00]
(ID:3)you :   [ − 3.12 , − 1.14 , + 5.14 , + 1.60 , + 7.00 ] \, [-3.12, -1.14, +5.14, +1.60, +7.00] [3.12,1.14,+5.14,+1.60,+7.00]

I love you 便可表示为 [ 1 , 2 , 3 ] [1,2,3] [1,2,3],经过词嵌入之后则得到
X = [ + 0.10 + 0.20 + 0.30 + 0.50 + 1.50 − 1.00 − 2.20 + 3.40 + 1.00 + 0.00 − 3.12 − 1.14 + 5.14 + 1.60 + 7.00 ] X=\left[ \begin{matrix} +0.10& +0.20& +0.30& +0.50& +1.50 \\ -1.00& -2.20& +3.40& +1.00& +0.00 \\ -3.12& -1.14& +5.14& +1.60& +7.00 \end{matrix} \right] X=+0.101.003.12+0.202.201.14+0.30+3.40+5.14+0.50+1.00+1.60+1.50+0.00+7.00

(ID:1)I :    [ + 0.10 , + 0.20 , + 0.30 , + 0.50 , + 1.50 ] \quad\, \, [+0.10, +0.20, +0.30, +0.50, +1.50] [+0.10,+0.20,+0.30,+0.50,+1.50]
(ID:4)hate : [ − 8.00 , − 6.40 , + 3.60 , + 2.00 , + 3.00 ] [-8.00, -6.40, +3.60, +2.00, +3.00] [8.00,6.40,+3.60,+2.00,+3.00]
(ID:3)you :   [ − 3.12 , − 1.14 , + 5.14 , + 1.60 , + 7.00 ] \ [-3.12, -1.14, +5.14, +1.60, +7.00]  [3.12,1.14,+5.14,+1.60,+7.00]

I hate you 便可表示为 [ 1 , 4 , 3 ] [1,4,3] [1,4,3],经过词嵌入之后则得到
X = [ + 0.10 + 0.20 + 0.30 + 0.50 + 1.50 − 8.00 − 6.40 + 3.60 + 2.00 + 3.00 − 3.12 − 1.14 + 5.14 + 1.60 + 7.00 ] X=\left[ \begin{matrix} +0.10& +0.20& +0.30& +0.50& +1.50 \\ -8.00& -6.40&+3.60&+2.00&+3.00\\ -3.12& -1.14& +5.14& +1.60& +7.00 \end{matrix} \right] X=+0.108.003.12+0.206.401.14+0.30+3.60+5.14+0.50+2.00+1.60+1.50+3.00+7.00

得到句子的特征矩阵X后,便可以把它放入到神经网络之中。

三. 神经网络

本部分详细内容可以参考神经网络与深度学习。

3.1 卷积神经网络(CNN)

CNN一般来说有3~4层

  1. 卷积层(convolution)
  2. 激活层(activation)(可选)
  3. 池化层(pooling)
  4. 全连接层(fully connected)

3.1.1 卷积层(Convolution)

3.1.1.1 卷积定义

首先需要搞清楚卷积的定义。

先介绍一维卷积,先看一张图:NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第2张图片
对于左边的图,待处理的向量 x ∈ R n x\in \mathbb{R}^n xRn是:
[ 1 , 1 , − 1 , 1 , 1 , 1 , − 1 , 1 , 1 ] [1, 1, -1, 1, 1, 1, -1, 1, 1] [1,1,1,1,1,1,1,1,1]
卷积核 w ∈ R K w\in \mathbb{R}^K wRK是:
[ 1 3 , 1 3 , 1 3 ] [\frac{1}{3}, \frac{1}{3}, \frac{1}{3}] [31,31,31]
结果 y ∈ R n − K + 1 y\in \mathbb{R}^{n-K+1} yRnK+1是:
[ 1 3 , 1 3 , 1 3 , 1 , 1 3 , 1 3 , 1 3 ] [\frac{1}{3}, \frac{1}{3}, \frac{1}{3}, 1, \frac{1}{3}, \frac{1}{3}, \frac{1}{3}] [31,31,31,1,31,31,31]
记为:
y = w ∗ x y=w * x y=wx
具体公式为:
y t = ∑ k = 1 K w k x t + k − 1 ,   t = 1 , 2 , . . . , n − K + 1 y_t=\sum_{k=1}^Kw_kx_{t+k-1},\ \small{t=1,2,...,n-K+1} yt=k=1Kwkxt+k1, t=1,2,...,nK+1

然后解释二维卷积,如下图:
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第3张图片
左边第一项,就是待处理的矩阵 X ∈ R n × d X\in \mathbb{R}^{n\times d} XRn×d

左边第二项,就是3*3的卷积核 W ∈ R K 1 × K 2 W\in \mathbb{R}^{K_1\times K_2} WRK1×K2

如图所示,对待处理矩阵的右上角,进行卷积操作,就可以得到右边矩阵 Y ∈ R ( n − K 1 + 1 ) × ( d − K 2 + 1 ) Y\in \mathbb{R}^{(n-K_1+1)\times (d-K_2+1)} YR(nK1+1)×(dK2+1)右上角的元素-1。

记为:
Y = W ∗ X Y=W * X Y=WX
具体公式为:
Y i j = ∑ k 1 = 1 K 1 ∑ k 2 = 1 K 2 W k 1 , k 2 X i + k 1 − 1 , j + k 2 − 1   i = 1 , 2 , . . . , n − K 1 + 1 ,   j = d − K 2 + 1 Y_{ij}=\sum_{k_1=1}^{K_1}\sum_{k_2=1}^{K_2}W_{k_1,k_2}X_{i+k_1-1,j+k_2-1}\\ ~\\ \small{i=1,2,...,n-K_1+1},\ \small{j=d-K_2+1} Yij=k1=1K1k2=1K2Wk1,k2Xi+k11,j+k21 i=1,2,...,nK1+1, j=dK2+1

3.1.1.2 卷积的步长与零填充

上面的卷积例子的步长都是1,但是步长可以不为1,见下图:
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第4张图片

上图的左部分步长为2,右部分的步长为1,不同的步长会得到长度不一样的结果,越长的步长,得到的结果长度越短。

二维卷积的步长也可以类似地进行定义,只不过除了横向的步长,也有纵向的步长,这里不详细叙述。

除此之外,还有值得注意的是padding(零填充),可以看到上图的右部分进行了零填充操作,使得待处理向量的边界元素能进行更多次数的卷积操作。

例子:

待处理的向量 x ∈ R n x\in \mathbb{R}^n xRn是:
[ 1 , 1 , − 1 , 1 , 1 , 1 , − 1 , 1 , 1 ] [1, 1, -1, 1, 1, 1, -1, 1, 1] [1,1,1,1,1,1,1,1,1]
进行了零填充的待处理向量 x ~ \tilde{x} x~是:
[ 0 , 1 , 1 , − 1 , 1 , 1 , 1 , − 1 , 1 , 1 , 0 ] [0, 1, 1, -1, 1, 1, 1, -1, 1, 1, 0] [0,1,1,1,1,1,1,1,1,1,0]
卷积核 w ∈ R K w\in \mathbb{R}^K wRK是:
[ 1 3 , 1 3 , 1 3 ] [\frac{1}{3}, \frac{1}{3}, \frac{1}{3}] [31,31,31]

可以看到, x x x的最左边的元素 1 1 1只被卷积到了1次,而经过了padding之后,最左边的 1 1 1元素可以被卷积两次(第一次是 [ 0 , 1 , 1 ] ∗ w [0,1,1]*w [0,1,1]w,第二次是 [ 1 , 1 , − 1 ] ∗ w [1,1,-1]*w [1,1,1]w).

因此,如果有了零填充的操作,待处理的向量边界的特征也能得意保留。

二维卷积的padding也可以进行类似地定义,只不过除了横向补0,还可以纵向补0。

3.1.1.3 卷积层设计

卷积层的设计参考了论文Convolutional Neural Networks for Sentence Classification。
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第5张图片
先定义一些符号, n n n是句子的长度,图中的例子(wait for the video and do n’t rent it)是 n = 9 n=9 n=9,词向量的长度为 d d d,图中的例子 d = 6 d=6 d=6,即该句子的特征矩阵 X ∈ R n × d = R 9 × 6 X\in \mathbb{R}^{n\times d}= \mathbb{R}^{9\times 6} XRn×d=R9×6

在本次的任务中,我们采用四个卷积核,大小分别是 2 × d 2\times d 2×d, 3 × d 3\times d 3×d, 4 × d 4\times d 4×d, 5 × d 5\times d 5×d

2 × d 2\times d 2×d 的卷积核在图中显示为红色的框框, 3 × d 3\times d 3×d 的卷积核在图中显示为黄色的框框。

例如:

“wait for” 这个词组,的特征矩阵的大小为 2 × d 2\times d 2×d,经过 2 × d 2\times d 2×d的卷积之后,会变成一个值。

对于某一个核 W W W,对特征矩阵 X X X进行卷积之后,会得到一个矩阵。

例如:

特征矩阵 X ∈ R n × d X\in \mathbb{R}^{n\times d} XRn×d与卷积核 W ∈ R 2 × d W\in \mathbb{R}^{2\times d} WR2×d卷积后,得到结果 Y ∈ R ( n − 2 + 1 ) × ( d + d − 1 ) = R ( n − 1 ) × 1 Y\in \mathbb{R}^{(n-2+1)\times (d+d-1)}=\mathbb{R}^{(n-1)\times 1} YR(n2+1)×(d+d1)=R(n1)×1

需要注意的是,这里采用四个核的原因是想挖掘词组的特征。

比如说, 2 × d 2\times d 2×d 的核是用来挖掘连续两个单词之间的关系的,而 5 × d 5\times d 5×d 的核用来连续挖掘五个单词之间的关系。

3.1.1.4 总结

卷积层的参数即是卷积核。

对于一个句子,特征矩阵是 X ∈ R n × d X\in \mathbb{R}^{n\times d} XRn×d,经过了四个卷积核 W W W的卷积后,得到了 Y 1 ∈ R ( n − 1 ) × 1 ,   Y 2 ∈ R ( n − 2 ) × 1 ,   Y 3 ∈ R ( n − 3 ) × 1 ,   Y 4 ∈ R ( n − 4 ) × 1 Y_1\in \mathbb{R}^{(n-1)\times 1},\ Y_2\in \mathbb{R}^{(n-2)\times 1},\ Y_3\in \mathbb{R}^{(n-3)\times 1},\ Y_4\in \mathbb{R}^{(n-4)\times 1} Y1R(n1)×1, Y2R(n2)×1, Y3R(n3)×1, Y4R(n4)×1的结果。

上面说的是一个通道的情况,我们可以多设置几个通道,每个通道都像上述一样操作,只不过每个通道的卷积核是不一样的,都是待定的参数。因此,设置 l l l_l ll个通道,就会得到 l l l_l ll ( Y 1 , Y 2 , Y 3 , Y 4 ) (Y_1, Y_2, Y_3, Y_4) (Y1,Y2,Y3,Y4)

3.1.2 激活层(可选)

激活函数可以参考维基百科。

在本次实战中,采用了ReLu函数:
R e L u ( x ) = max ( x , 0 ) ReLu(x)=\text{max}(x,0) ReLu(x)=max(x,0)

l l l_l ll ( Y 1 , Y 2 , Y 3 , Y 4 ) (Y_1, Y_2, Y_3, Y_4) (Y1,Y2,Y3,Y4)经过了激活之后,还是得到 l l l_l ll ( Y 1 , Y 2 , Y 3 , Y 4 ) (Y_1, Y_2, Y_3, Y_4) (Y1,Y2,Y3,Y4)

3.1.3 汇聚层/池化层(Pooling)

Pooling层相当于是对特征矩阵/向量提取出一些有用的信息,从而减少特征的规模,不仅减少了计算量,也能去除冗余特征。

Pooling有两种方法:

  1. 最大汇聚

对一个区域,取最大的一个元素
y m , n = max i ∈ R m , n x i y_{m,n}=\text{max}_{i\in R_{m,n}} x_i ym,n=maxiRm,nxi
即取 R m , n R_{m,n} Rm,n这个区域里,最大的元素

  1. 平均汇聚

对一个区域,取所有元素的平均值
y m , n = 1 ∣ R m , n ∣ ∑ i ∈ R m , n x i y_{m,n}=\frac{1}{|R_{m,n}|}\sum_{i\in R_{m,n}} x_i ym,n=Rm,n1iRm,nxi
即取 R m , n R_{m,n} Rm,n这个区域里,所有元素的平均值

看一张图:
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第6张图片

上面的是最大汇聚,下面的是平均汇聚。

在本次实战中,我用的是最大汇聚。

对于 l l l_l ll ( Y 1 , Y 2 , Y 3 , Y 4 ) (Y_1, Y_2, Y_3, Y_4) (Y1,Y2,Y3,Y4),对任意一组 l l l里的任意一个向量 Y i ( l ) ∈ R ( n − i ) × 1 Y^{(l)}_i\in \mathbb{R}^{(n-i)\times 1} Yi(l)R(ni)×1,我取其最大值,即
y i ( i ) = m a x j    Y i ( l ) ( j ) y_i^{(i)}=max_j\; Y^{(l)}_i(j) yi(i)=maxjYi(l)(j)
Y i ( l ) ( j ) Y^{(l)}_i(j) Yi(l)(j)表示 Y i ( l ) Y^{(l)}_i Yi(l)的第 j j j个元素。

经过最大汇聚后,我们可以得到 l l l_l ll ( y 1 , y 2 , y 3 , y 4 ) (y_1, y_2, y_3, y_4) (y1,y2,y3,y4)

把它们按结果类别拼接起来,可以得到一个长度为 l l ∗ 4 l_l*4 ll4的向量,即
Y = ( y 1 ( 1 ) , . . . , y 1 ( l l ) , y 2 ( 1 ) , . . . , y 2 ( l l ) , y 3 ( 1 ) . . . , y 3 ( l l ) , y 4 ( 1 ) , . . . , y 4 ( l l ) ) T ∈ R ( l l ∗ 4 ) × 1 Y=(y_1^{(1)}, ..., y_1^{(l_l)}, y_2^{(1)}, ..., y_2^{(l_l)}, y_3^{(1)}... , y_3^{(l_l)}, y_4^{(1)}, ..., y_4^{(l_l)})^T\in \mathbb{R}^{(l_l*4)\times 1} Y=(y1(1),...,y1(ll),y2(1),...,y2(ll),y3(1)...,y3(ll),y4(1),...,y4(ll))TR(ll4)×1

3.1.4 全连接层(Fully connected)

看一张示意图:
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第7张图片
左边的神经元便是我们上一部分得到的向量,长度为 l l ∗ 4 l_l*4 ll4

而我们的目的是输出一个句子的情感类别,参考上一次任务《NLP-Beginner 任务一:基于机器学习的文本分类》,我们的输出也应该是五类情感的概率,即(0~4共五类)
p = ( 0 , 0 , 0.7 , 0.25 , 0.05 ) T p=(0,0,0.7,0.25,0.05)^T p=(0,0,0.7,0.25,0.05)T
则代表,其是类别2的概率为0.7,类别3的概率为0.25,类别4的概率为0.05。

因此,在全连接层,我们要把长度为 l l ∗ 4 l_l*4 ll4的向量转换成长度为 5 5 5 的向量。

而最简单的转换方式便是线性变换 p = A Y + b p=AY+b p=AY+b,其中 A ∈ R 5 × ( l l ∗ 4 ) , Y ∈ R ( l l ∗ 4 ) × 1 , b ∈ R 5 × 1 A\in \mathbb{R}^{5\times (l_l*4)}, Y\in \mathbb{R}^{(l_l*4)\times 1}, b\in \mathbb{R}^{5\times 1} AR5×(ll4),YR(ll4)×1,bR5×1

最终,整个神经网络会输出一个长度为 5 5 5 的向量 p p p

如此一来 A A A b b b 便是需要待定的系数。

3.1.5 总结

  • 卷积层:特征矩阵 → \rightarrow l l l_l ll ( Y 1 , Y 2 , Y 3 , Y 4 ) (Y_1, Y_2, Y_3, Y_4) (Y1,Y2,Y3,Y4),神经网络参数: 4 l l 4l_l 4ll个卷积核 W W W
  • 激活层: l l l_l ll ( Y 1 , Y 2 , Y 3 , Y 4 ) (Y_1, Y_2, Y_3, Y_4) (Y1,Y2,Y3,Y4) → \rightarrow l l l_l ll ( Y 1 , Y 2 , Y 3 , Y 4 ) (Y_1, Y_2, Y_3, Y_4) (Y1,Y2,Y3,Y4),没有参数。
  • 汇聚层: l l l_l ll ( Y 1 , Y 2 , Y 3 , Y 4 ) (Y_1, Y_2, Y_3, Y_4) (Y1,Y2,Y3,Y4) → \rightarrow l l l_l ll ( y 1 , y 2 , y 3 , y 4 ) → Y (y_1, y_2, y_3, y_4)\rightarrow Y (y1,y2,y3,y4)Y,没有参数。
  • 全连接层: Y → p Y\rightarrow p Yp,神经网络参数: A , b A, b A,b

3.2 循环神经网络(RNN)

CNN一般来说有2~3层

  1. 隐藏层(hidden)
  2. 激活层(activation)(可选)
  3. 全连接层(fully connected)

3.2.1 隐藏层(Hidden)

看一张图:
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第8张图片
先回顾输入是什么。

输入是一个特征矩阵 X ∈ R n × d X\in \mathbb{R}^{n\times d} XRn×d,例如:(d=5)

I :    [ + 0.10 , + 0.20 , + 0.30 , + 0.50 , + 1.50 ] \quad\; [+0.10, +0.20, +0.30, +0.50, +1.50] [+0.10,+0.20,+0.30,+0.50,+1.50]
love : [ − 1.00 , − 2.20 , + 3.40 , + 1.00 , + 0.00 ] [-1.00, -2.20, +3.40, +1.00, +0.00] [1.00,2.20,+3.40,+1.00,+0.00]
you :   [ − 3.12 , − 1.14 , + 5.14 , + 1.60 , + 7.00 ] \, [-3.12, -1.14, +5.14, +1.60, +7.00] [3.12,1.14,+5.14,+1.60,+7.00]

I love you 可表示为
X = [ + 0.10 + 0.20 + 0.30 + 0.50 + 1.50 − 1.00 − 2.20 + 3.40 + 1.00 + 0.00 − 3.12 − 1.14 + 5.14 + 1.60 + 7.00 ] = [ x 1 , x 2 , x 3 ] T X=\left[ \begin{matrix} +0.10& +0.20& +0.30& +0.50& +1.50 \\ -1.00& -2.20& +3.40& +1.00& +0.00 \\ -3.12& -1.14& +5.14& +1.60& +7.00 \end{matrix} \right]=[x_1,x_2,x_3]^T X=+0.101.003.12+0.202.201.14+0.30+3.40+5.14+0.50+1.00+1.60+1.50+0.00+7.00=[x1,x2,x3]T

x i ∈ R d x_i\in \mathbb{R}^{d} xiRd

在CNN中,我们是直接对特征矩阵X进行操作,而在RNN中,我们是逐个对 x i x_i xi进行操作,步骤如下:

  1. 初始化 h 0 ∈ R l h h_0\in \mathbb{R}^{l_h} h0Rlh
  2. t = 1 , 2 , . . . n t=1,2,...n t=1,2,...n 计算以下两个公式

(1) z t = U h t − 1 + W x t + b \quad z_t=Uh_{t-1}+Wx_t+b zt=Uht1+Wxt+b, 其中 U ∈ R l h × l h ,   W ∈ R l h × d ,   z , b h ∈ R l h U\in \mathbb{R}^{l_h\times l_h},\ W\in \mathbb{R}^{l_h\times d},\ z,b_h\in \mathbb{R}^{l_h} URlh×lh, WRlh×d, z,bhRlh

(2) h t = f ( z t ) \quad h_t=f(z_t) ht=f(zt),其中 f ( ⋅ ) f(\cdot) f()激活函数,本任务用了 t a n h tanh tanh函数, t a n h ( x ) = exp ( x ) − exp ( − x ) exp ( x ) + exp ( − x ) tanh(x)=\frac{\text{exp}(x)-\text{exp}(-x)}{\text{exp}(x)+\text{exp}(-x)} tanh(x)=exp(x)+exp(x)exp(x)exp(x)

  1. 最终得到 h n ∈ R l h h_n\in \mathbb{R}^{l_h} hnRlh

这一层的目的,便是把序列 { x i } i = 1 n \{x_i\}_{i=1}^n {xi}i=1n逐个输入到隐藏层去,与参数发生作用,输出的结果 h t h_t ht也会参与到下一次循环计算之中,实现了一种记忆功能,使神经网络具有了(短期)的记忆能力。

这种记忆能力有助于神经网络中从输入中挖掘更多的特征,及其相互关系,而不再只是像CNN一样局限于2、3、4、5个词之间的关系。

3.2.2 激活层(可选)

RNN的激活层与CNN激活层是类似的,激活函数可以参考维基百科。

在本次实战中,我的RNN没有额外加入激活层。

3.2.3 全连接层(Fully connected)

RNN的全连接层与CNN全连接层也是类似的。

在全连接层,我们要把长度为 l h l_h lh的向量转换成长度为 5 5 5 的向量。

类似地,采取线性变换 p = A h n + b l p=Ah_n+b_l p=Ahn+bl,其中 A ∈ R 5 × l h , h t ∈ R l h × 1 , b l ∈ R 5 × 1 A\in \mathbb{R}^{5\times l_h}, h_t\in \mathbb{R}^{l_h\times 1}, b_l\in \mathbb{R}^{5\times 1} AR5×lh,htRlh×1,blR5×1, A A A b b b 也是需要待定的系数。

最终,整个神经网络会输出一个长度为 5 5 5 的向量 p p p

3.2.4 总结

  • 隐藏层:特征矩阵 → \rightarrow h n h_n hn,神经网络参数: W ,   U W,\ U W, U b h b_h bh
  • 全连接层: h n → p h_n\rightarrow p hnp,神经网络参数: A , b l A, b_l A,bl

3.3 训练神经网络

3.3.1 神经网络参数

有了上面的CNN和RNN的模型,接下来就是求解神经网络中的参数了。先回顾一下两个模型的参数:

  • CNN: 4 l l 4l_l 4ll个卷积核 W ; W;\quad W; A , b A, b A,b
  • RNN: W ,   U ,   b h ; A , b l W,\ U,\ b_h;\quad A,b_l W, U, bh;A,bl

对于任意一个网络,把它们的参数记作 θ \theta θ

整个流程:

句 子 x → Word embedding 特 征 矩 阵 X → Neural Network ( θ ) 类 别 概 率 向 量 p 句子x \xrightarrow{\text{Word\ embedding}} 特征矩阵X\xrightarrow{\text{Neural Network}(\theta)}类别概率向量p xWord embedding XNeural Network(θ) p

3.3.2 损失函数

有了模型,我们就要对模型的好坏做出一个评价。也就是说,给定一组参数 θ \theta θ,我们要去量化一个模型的好坏,那么我们就要定义一个损失函数。

一般来说,有以下几种损失函数:

函数 公式 注释
0-1损失函数 I ( y ≠ f ( x ) ) I(y\ne f(x)) I(y=f(x)) 不可导
绝对值损失函数 | y − f ( x ) y-f(x) yf(x)| 适用于连续值
平方值损失函数 ( y − f ( x ) ) 2 (y-f(x))^2 (yf(x))2 适用于连续值
交叉熵损失函数 − ∑ c y c log ⁡ f c ( x ) -\sum_c y_c\log{f_c(x)} cyclogfc(x) 适用于分类
指数损失函数 exp ⁡ ( − y f ( x ) ) \exp(-yf(x)) exp(yf(x)) 适用于二分类
合页损失函数 max ( 0 , 1 − y f ( x ) ) \text{max}(0,1-yf(x)) max(0,1yf(x)) 适用于二分类

因此,总上述表来看,我们应该使用交叉熵损失函数。

给定一个神经网络 N N NN NN, 对于每一个样本n,其损失值为
L ( N N θ ( x ( n ) ) , y ( n ) ) = − ∑ c = 1 C y c ( n ) log ⁡ p c ( n ) = − ( y ( n ) ) T log ⁡ p ( n ) L(NN_\theta(x^{(n)}),y^{(n)})=-\sum_{c=1}^C y_c^{(n)}\log{p_c^{(n)}}=-(y^{(n)})^T\log{p^{(n)}} L(NNθ(x(n)),y(n))=c=1Cyc(n)logpc(n)=(y(n))Tlogp(n)
其中 y ( n ) = ( I ( c = 0 ) , I ( c = 2 ) , . . . , I ( c = C ) ) T y^{(n)}=\big(I(c=0),I(c=2),...,I(c=C)\big)^T y(n)=(I(c=0),I(c=2),...,I(c=C))T,是一个one-hot向量,即只有一个元素是1,其他全是0的向量。

注:下标 c c c 代表向量中的第 c c c 个元素,这里 C = 4 C=4 C=4

例子:

句子 x ( n ) x^{(n)} x(n) 的类别是第0类,则 y ( n ) = [ 1 , 0 , 0 , 0 , 0 ] T y^{(n)}=[1,0,0,0,0]^T y(n)=[1,0,0,0,0]T

而对于N个样本,总的损失值则是每个样本损失值的平均,即
L ( θ ) = L ( N N θ ( x ) , y ) = 1 N ∑ n = 1 N L ( N N θ ( x ( n ) ) , y ( n ) ) L(\theta)=L(NN_\theta(x),y)=\frac{1}{N}\sum_{n=1}^NL(NN_\theta(x^{(n)}),y^{(n)}) L(θ)=L(NNθ(x),y)=N1n=1NL(NNθ(x(n)),y(n))

有了损失函数,我们就可以通过找到损失函数的最小值,来求解最优的参数矩阵 θ \theta θ

3.3.3 参数求解——梯度下降

梯度下降的基本思想是,对于每个固定的参数,求其梯度(导数),然后利用梯度(导数),进行对参数的更新。

在这里,公式是
θ t + 1 ← θ t − α ∂ L ( θ t ) ∂ θ t \theta_{t+1}\leftarrow \theta_t-\alpha\frac{\partial L(\theta_t)}{\partial \theta_t} θt+1θtαθtL(θt)
由于Pytorch求解参数并不需要我们求梯度且梯度计算非常复杂,因此在这里就暂时不介绍具体如何求梯度过程。

感兴趣的同学可以参考神经网络与深度学习。

四. 代码及实现

4.1 实验设置

  • 样本个数:约150000
  • 训练集:测试集 : 7:3
  • 模型:CNN, RNN
  • 初始化:随机初始化,GloVe预训练模型初始化
  • 学习率:10-3
  • l h ,   d l_h,\ d lh, d:50
  • l l l_l ll:最长句子的单词数
  • Batch 大小:500

4.2 结果展示

4.2.1 Part 1

先展示总体结果。
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第9张图片
我们先比较CNNRNN

可以看到RNN在测试集的准确率(最大值)比CNN都要高,且测试集的损失值(最小值)也要比CNN的要低。

再比较随机初始化GloVe初始化

在同种模型下,GloVe初始化也要比随机初始化的效果好,即在测试集准确率大、测试集损失值小。

最终,测试集准确率大约在 66 % 66\% 66% 左右。

4.2.2 Part 2

以上结果并不能说明RNN在长句子情感分类方面的优势。因为RNN具有短期记忆,能处理好词与词之间的关系,所以我想看看RNN在长句子分类上是否有一个比较好的结果。

因此,在训练的过程中,我特别关注了测试集单词数大于20的句子的损失值和正确率,结果如图:

NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第10张图片
非常遗憾的是,RNN的效果并不比CNN好,而且无论是CNN还是RNN,长句子的情感分类准确率也只有大概 55 % 55\% 55% 左右,比总体的平均正确率低了约 10 % 10\% 10%

因此,在这一点上有待进一步挖掘。

4.3 代码

本次使用了Python中的torch库,并使用了cuda加速。

若不想要GPU加速,只需要把comparison_plot_batch.pyNeural_Network_batch.py中所有的.cuda().cpu()删去即可。

注1:可能在comparison_plot_batch.py中的所有.item()也要删去。


重要

注2:在理论部分,我们阐述的是一个样本从输入到输出的过程,但是实际神经网络里通常都是输入一批样本(batch)然后得到输出。

但是,一个batch内特征长短不一会使数据分batch失败,因此会进行一个零填充(padding)操作,把同一个batch内的所有输入(句子),补到一样长。

但是,由于句子长度可能会参差不齐(如一个句子只有3个单词,另一个有50个单词,那么就需要在前者的后面填充47个无意义的0。),插入过长的无意义零填充可能会对性能造成影响,因此在本次实战中,我先把数据按照句子长度进行了排序,尽量使同一个batch内句子长度一致,这样就可以避免零填充。

同时,设置padding的这个ID为0。

注3:本次实战中,还在词嵌入之后加入了一层Dropout层(丢弃法)。

解释:Dropout (丢弃法) 是指在深度网络的训练中,以一定的概率随机地“临时丢弃”一部分神经元节点。 具体来讲,Dropout 作用于每份小批量训练数据,由于其随机丢弃部分神经元的机制,相当于每次迭代都在训练不同结构的神经网络。

简单来讲,就是为了防止模型过拟合,且Dropout层在模型测试时不会有任何影响,训练时的效果如图:
NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)_第11张图片


4.3.1 主文件——main.py

import csv
import random
from feature_batch import Random_embedding,Glove_embedding
import torch
from comparison_plot_batch import NN_embedding_plot

# 数据读入
with open('train.tsv') as f:
    tsvreader = csv.reader (f, delimiter ='\t')
    temp = list ( tsvreader )

with open('glove.6B.50d.txt','rb') as f:  # for glove embedding
    lines=f.readlines()

# 用GloVe创建词典
trained_dict=dict()
n=len(lines)
for i in range(n):
    line=lines[i].split()
    trained_dict[line[0].decode("utf-8").upper()]=[float(line[j]) for j in range(1,51)]

# 初始化
iter_times=50  # 做50个epoch
alpha=0.001

# 程序开始
data = temp[1:]
batch_size=500

# 随机初始化
random.seed(2021)
random_embedding=Random_embedding(data=data)
random_embedding.get_words()  # 找到所有单词,并标记ID
random_embedding.get_id()  # 找到每个句子拥有的单词ID

# 预训练模型初始化
random.seed(2021)
glove_embedding=Glove_embedding(data=data,trained_dict=trained_dict)
glove_embedding.get_words()  # 找到所有单词,并标记ID
glove_embedding.get_id()  # 找到每个句子拥有的单词ID

NN_embedding_plot(random_embedding,glove_embedding,alpha,batch_size,iter_times))

4.3.2 特征提取——feature_batch.py

import random
from torch.utils.data import Dataset, DataLoader
from torch.nn.utils.rnn import pad_sequence
import torch


def data_split(data, test_rate=0.3):
    """把数据按一定比例划分成训练集和测试集"""
    train = list()
    test = list()
    for datum in data:
        if random.random() > test_rate:
            train.append(datum)
        else:
            test.append(datum)
    return train, test


class Random_embedding():
	"""随机初始化"""
    def __init__(self, data, test_rate=0.3):
        self.dict_words = dict()  # 单词->ID的映射
        data.sort(key=lambda x:len(x[2].split()))  # 按照句子长度排序,短着在前,这样做可以避免后面一个batch内句子长短不一,导致padding过度
        self.data = data
        self.len_words = 0  # 单词数目(包括padding的ID:0)
        self.train, self.test = data_split(data, test_rate=test_rate)  # 训练集测试集划分
        self.train_y = [int(term[3]) for term in self.train]  # 训练集类别
        self.test_y = [int(term[3]) for term in self.test]  # 测试集类别
        self.train_matrix = list()  # 训练集的单词ID列表,叠成一个矩阵
        self.test_matrix = list()  # 测试集的单词ID列表,叠成一个矩阵
        self.longest=0  # 记录最长的单词

    def get_words(self):
        for term in self.data:
            s = term[2]  # 取出句子
            s = s.upper()  # 记得要全部转化为大写!!(或者全部小写,否则一个单词例如i,I会识别成不同的两个单词)
            words = s.split()
            for word in words:  # 一个一个单词寻找
                if word not in self.dict_words:
                    self.dict_words[word] = len(self.dict_words)+1  # padding是第0个,所以要+1
        self.len_words=len(self.dict_words)  # 单词数目(暂未包括padding的ID:0)

    def get_id(self):
        for term in self.train:  # 训练集
            s = term[2]
            s = s.upper()
            words = s.split()
            item=[self.dict_words[word] for word in words]  # 找到id列表(未进行padding)
            self.longest=max(self.longest,len(item))  # 记录最长的单词
            self.train_matrix.append(item)
        for term in self.test:
            s = term[2]
            s = s.upper()
            words = s.split()
            item = [self.dict_words[word] for word in words]  # 找到id列表(未进行padding)
            self.longest = max(self.longest, len(item))  # 记录最长的单词
            self.test_matrix.append(item)
        self.len_words += 1   # 单词数目(包括padding的ID:0)


class Glove_embedding():
    def __init__(self, data,trained_dict,test_rate=0.3):
        self.dict_words = dict()  # 单词->ID的映射
        self.trained_dict=trained_dict  # 记录预训练词向量模型
        data.sort(key=lambda x:len(x[2].split()))  # 按照句子长度排序,短着在前,这样做可以避免后面一个batch内句子长短不一,导致padding过度
        self.data = data
        self.len_words = 0  # 单词数目(包括padding的ID:0)
        self.train, self.test = data_split(data, test_rate=test_rate)  # 训练集测试集划分
        self.train_y = [int(term[3]) for term in self.train]  # 训练集类别
        self.test_y = [int(term[3]) for term in self.test]  # 测试集类别
        self.train_matrix = list()  # 训练集的单词ID列表,叠成一个矩阵
        self.test_matrix = list()  # 测试集的单词ID列表,叠成一个矩阵
        self.longest=0  # 记录最长的单词
        self.embedding=list()  # 抽取出用到的(预训练模型的)单词

    def get_words(self):
        self.embedding.append([0] * 50)  # 先加padding的词向量
        for term in self.data:
            s = term[2]  # 取出句子
            s = s.upper()  # 记得要全部转化为大写!!(或者全部小写,否则一个单词例如i,I会识别成不同的两个单词)
            words = s.split()
            for word in words:  # 一个一个单词寻找
                if word not in self.dict_words:
                    self.dict_words[word] = len(self.dict_words)+1  # padding是第0个,所以要+1
                    if word in self.trained_dict:  # 如果预训练模型有这个单词,直接记录词向量
                        self.embedding.append(self.trained_dict[word])
                    else:  # 预训练模型没有这个单词,初始化该词对应的词向量为0向量
                        # print(word)
                        # raise Exception("words not found!")
                        self.embedding.append([0]*50)
        self.len_words=len(self.dict_words)  # 单词数目(暂未包括padding的ID:0)

    def get_id(self):
        for term in self.train:  # 训练集
            s = term[2]
            s = s.upper()
            words = s.split()
            item=[self.dict_words[word] for word in words]  # 找到id列表(未进行padding)
            self.longest=max(self.longest,len(item))  # 记录最长的单词
            self.train_matrix.append(item)
        for term in self.test:
            s = term[2]
            s = s.upper()
            words = s.split()
            item = [self.dict_words[word] for word in words]  # 找到id列表(未进行padding)
            self.longest = max(self.longest, len(item))  # 记录最长的单词
            self.test_matrix.append(item)
        self.len_words += 1  # 单词数目(暂未包括padding的ID:0)


class ClsDataset(Dataset):
	"""自定义数据集的结构,pytroch基本功!!!"""
    def __init__(self, sentence, emotion):
        self.sentence = sentence  # 句子
        self.emotion= emotion  # 情感类别

    def __getitem__(self, item):
        return self.sentence[item], self.emotion[item]

    def __len__(self):
        return len(self.emotion)


def collate_fn(batch_data):
	"""自定义数据集的内数据返回方式,pytroch基本功!!!并进行padding!!!"""
    sentence, emotion = zip(*batch_data)
    sentences = [torch.LongTensor(sent) for sent in sentence]  # 把句子变成Longtensor类型
    padded_sents = pad_sequence(sentences, batch_first=True, padding_value=0)  # 自动padding操作!!!
    return torch.LongTensor(padded_sents), torch.LongTensor(emotion)


def get_batch(x,y,batch_size):
	"""利用dataloader划分batch,pytroch基本功!!!"""
    dataset = ClsDataset(x, y)
    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False,drop_last=True,collate_fn=collate_fn)
    #  shuffle是指每个epoch都随机打乱数据排列再分batch,
    #  这里一定要设置成false,否则之前的排序会直接被打乱,
    #  drop_last是指不利用最后一个不完整的batch(数据大小不能被batch_size整除)
    return dataloader

4.3.3 神经网络——Neural_network_batch.py

import torch
import torch.nn as nn
import torch.nn.functional as F


class MY_RNN(nn.Module):
	"""自己设计的RNN网络"""
    def __init__(self, len_feature, len_hidden, len_words, typenum=5, weight=None, layer=1, nonlinearity='tanh',
                 batch_first=True, drop_out=0.5):
        super(MY_RNN, self).__init__()
        self.len_feature = len_feature  # d的大小
        self.len_hidden = len_hidden  # l_h的大小
        self.len_words = len_words  # 单词的个数(包括padding)
        self.layer = layer  # 隐藏层层数
        self.dropout=nn.Dropout(drop_out)  # dropout层
        if weight is None:  # 随机初始化
            x = nn.init.xavier_normal_(torch.Tensor(len_words, len_feature))
            self.embedding = nn.Embedding(num_embeddings=len_words, embedding_dim=len_feature, _weight=x).cuda()
        else:  # GloVe初始化
            self.embedding = nn.Embedding(num_embeddings=len_words, embedding_dim=len_feature, _weight=weight).cuda()
        # 用nn.Module的内置函数定义隐藏层
        self.rnn = nn.RNN(input_size=len_feature, hidden_size=len_hidden, num_layers=layer, nonlinearity=nonlinearity,
                          batch_first=batch_first, dropout=drop_out).cuda()
        # 全连接层
        self.fc = nn.Linear(len_hidden, typenum).cuda()
        # 冗余的softmax层,可以不加
        # self.act = nn.Softmax(dim=1)

    def forward(self, x):
    	"""x:数据,维度为[batch_size, 句子长度]"""
        x = torch.LongTensor(x).cuda()
        batch_size = x.size(0)
        """经过词嵌入后,维度为[batch_size,句子长度,d]"""
        out_put = self.embedding(x)  # 词嵌入
        out_put=self.dropout(out_put)  # dropout层
		
		# 另一种初始化h_0的方式
        # h0 = torch.randn(self.layer, batch_size, self.len_hidden).cuda()
        # 初始化h_0为0向量
        h0 = torch.autograd.Variable(torch.zeros(self.layer, batch_size, self.len_hidden)).cuda()
        """dropout后不变,经过隐藏层后,维度为[1,batch_size, l_h]"""
        _, hn = self.rnn(out_put, h0)  # 隐藏层计算
        """经过全连接层后,维度为[1,batch_size, 5]"""
        out_put = self.fc(hn).squeeze(0)  # 全连接层
        """挤掉第0维度,返回[batch_size, 5]的数据"""
        # out_put = self.act(out_put)  # 冗余的softmax层,可以不加
        return out_put


class MY_CNN(nn.Module):
    def __init__(self, len_feature, len_words, longest, typenum=5, weight=None,drop_out=0.5):
        super(MY_CNN, self).__init__()
        self.len_feature = len_feature  # d的大小
        self.len_words = len_words  # 单词数目
        self.longest = longest  # 最长句子单词书目
        self.dropout = nn.Dropout(drop_out)  # Dropout层
        if weight is None:  # 随机初始化
            x = nn.init.xavier_normal_(torch.Tensor(len_words, len_feature))
            self.embedding = nn.Embedding(num_embeddings=len_words, embedding_dim=len_feature, _weight=x).cuda()
        else:  # GloVe初始化
            self.embedding = nn.Embedding(num_embeddings=len_words, embedding_dim=len_feature, _weight=weight).cuda()
         # Conv2d参数详解:(输入通道数:1,输出通道数:l_l,卷积核大小:(行数,列数))
         # padding是指往句子两侧加 0,因为有的句子只有一个单词
         # 那么 X 就是 1*50 对 W=2*50 的卷积核根本无法进行卷积操作
         # 因此要在X两侧行加0(两侧列不加),(padding=(1,0))变成 3*50
         # 又比如 padding=(2,0)变成 5*50
        self.conv1 = nn.Sequential(nn.Conv2d(1, longest, (2, len_feature), padding=(1, 0)), nn.ReLU()).cuda()  # 第1个卷积核+激活层
        self.conv2 = nn.Sequential(nn.Conv2d(1, longest, (3, len_feature), padding=(1, 0)), nn.ReLU()).cuda()  # 第2个卷积核+激活层
        self.conv3 = nn.Sequential(nn.Conv2d(1, longest, (4, len_feature), padding=(2, 0)), nn.ReLU()).cuda()  # 第3个卷积核+激活层
        self.conv4 = nn.Sequential(nn.Conv2d(1, longest, (5, len_feature), padding=(2, 0)), nn.ReLU()).cuda()  # 第4个卷积核+激活层
        # 全连接层
        self.fc = nn.Linear(4 * longest, typenum).cuda()
        # 冗余的softmax层,可以不加
        # self.act = nn.Softmax(dim=1)

    def forward(self, x):
    	"""x:数据,维度为[batch_size, 句子长度]"""
    	
        x = torch.LongTensor(x).cuda()
        """经过词嵌入后,维度为[batch_size,1,句子长度,d]"""
        out_put = self.embedding(x).view(x.shape[0], 1, x.shape[1], self.len_feature)  # 词嵌入
        """dropout后不变,记为X"""
        out_put=self.dropout(out_put)  # dropout层
		
		"""X经过2*d卷积后,维度为[batch_size,l_l,句子长度+2-1,1]"""
    	"""挤掉第三维度(维度从0开始),[batch_size,l_l,句子长度+2-1]记为Y_1"""
    	"""注意:句子长度+2-1的2是padding造成的行数扩张"""
        conv1 = self.conv1(out_put).squeeze(3)  # 第1个卷积
        
		"""X经过3*d卷积后,维度为[batch_size,l_l,句子长度+2-2,1]"""
    	"""挤掉第三维度(维度从0开始),[batch_size,l_l,句子长度+2-2]记为Y_2"""
        conv2 = self.conv2(out_put).squeeze(3)  # 第2个卷积
        
		"""X经过4*d卷积后,维度为[batch_size,l_l,句子长度+4-3,1]"""
    	"""挤掉第三维度(维度从0开始),[batch_size,l_l,句子长度+4-3]记为Y_3"""
        conv3 = self.conv3(out_put).squeeze(3)  # 第3个卷积
        
		"""X经过5*d卷积后,维度为[batch_size,l_l,句子长度+4-4,1]"""
    	"""挤掉第三维度(维度从0开始),[batch_size,l_l,句子长度+4-4]记为Y_4"""
        conv4 = self.conv4(out_put).squeeze(3)  # 第4个卷积
        
		"""分别对(Y_1,Y_2,Y_3,Y_4)的第二维(维度从0开始)进行pooling"""
		"""得到4个[batch_size,,l_l,1]的向量"""
		pool1 = F.max_pool1d(conv1, conv1.shape[2])
		pool2 = F.max_pool1d(conv2, conv2.shape[2])
		pool3 = F.max_pool1d(conv3, conv3.shape[2])
		pool4 = F.max_pool1d(conv4, conv4.shape[2])
		
		"""拼接得到[batch_size,,l_l*4,1]的向量"""
		"""挤掉第二维(维度从0开始)为[batch_size,,l_l*4]"""
        pool = torch.cat([pool1, pool2, pool3, pool4], 1).squeeze(2)  # 拼接起来
        """经过全连接层后,维度为[batch_size, 5]"""
        out_put = self.fc(pool)  # 全连接层
        # out_put = self.act(out_put)  # 冗余的softmax层,可以不加
        return out_put

4.3.4 结果&画图——comparison_plot_batch.py

import matplotlib.pyplot
import torch
import torch.nn.functional as F
from torch import optim
from Neural_Network_batch import MY_RNN,MY_CNN
from feature_batch import get_batch


def NN_embdding(model, train,test, learning_rate, iter_times):
	# 定义优化器(求参数)
    optimizer = optim.Adam(model.parameters(), lr=learning_rate)
    # 损失函数  
    loss_fun = F.cross_entropy
    # 损失值记录
    train_loss_record=list()
    test_loss_record=list()
    long_loss_record=list()
    # 准确率记录
    train_record=list()
    test_record=list()
    long_record=list()
    # torch.autograd.set_detect_anomaly(True)
	# 训练阶段
    for iteration in range(iter_times):
        model.train()  # 重要!!!进入非训练模式
        for i, batch in enumerate(train):
            x, y = batch  # 取一个batch
            y=y.cuda()
            pred = model(x).cuda()  # 计算输出
            optimizer.zero_grad()  # 梯度初始化
            loss = loss_fun(pred, y).cuda()  # 损失值计算
            loss.backward()  # 反向传播梯度
            optimizer.step()  # 更新参数

        model.eval()  # 重要!!!进入非训练模式(测试模式)
        # 本轮正确率记录
        train_acc = list()
        test_acc = list()
        long_acc = list()
        length = 20
        # 本轮损失值记录
        train_loss = 0
        test_loss = 0
        long_loss=0
        for i, batch in enumerate(train):
            x, y = batch  # 取一个batch
            y=y.cuda()
            pred = model(x).cuda()  # 计算输出
            loss = loss_fun(pred, y).cuda()    # 损失值计算
            train_loss += loss.item()  # 损失值累加
            _, y_pre = torch.max(pred, -1)
            # 计算本batch准确率
            acc = torch.mean((torch.tensor(y_pre == y, dtype=torch.float)))
            train_acc.append(acc)

        for i, batch in enumerate(test):
            x, y = batch  # 取一个batch
            y=y.cuda()
            pred = model(x).cuda()  # 计算输出
            loss = loss_fun(pred, y).cuda()  # 损失值计算
            test_loss += loss.item()  # 损失值累加
            _, y_pre = torch.max(pred, -1)
            # 计算本batch准确率
            acc = torch.mean((torch.tensor(y_pre == y, dtype=torch.float)))
            test_acc.append(acc)
            if(len(x[0]))>length:  # 长句子侦测
              long_acc.append(acc)
              long_loss+=loss.item()

        trains_acc = sum(train_acc) / len(train_acc)
        tests_acc = sum(test_acc) / len(test_acc)
        longs_acc = sum(long_acc) / len(long_acc)

        train_loss_record.append(train_loss / len(train_acc))
        test_loss_record.append(test_loss / len(test_acc))
        long_loss_record.append(long_loss/len(long_acc))
        train_record.append(trains_acc.cpu())
        test_record.append(tests_acc.cpu())
        long_record.append(longs_acc.cpu())
        print("---------- Iteration", iteration + 1, "----------")
        print("Train loss:", train_loss/ len(train_acc))
        print("Test loss:", test_loss/ len(test_acc))
        print("Train accuracy:", trains_acc)
        print("Test accuracy:", tests_acc)
        print("Long sentence accuracy:", longs_acc)

    return train_loss_record,test_loss_record,long_loss_record,train_record,test_record,long_record


def NN_embedding_plot(random_embedding,glove_embedding,learning_rate, batch_size, iter_times):
	# 获得训练集和测试集的batch
    train_random = get_batch(random_embedding.train_matrix,
                             random_embedding.train_y, batch_size)
    test_random = get_batch(random_embedding.test_matrix,
                            random_embedding.test_y, batch_size)
    train_glove = get_batch(glove_embedding.train_matrix,
                            glove_embedding.train_y, batch_size)
    test_glove = get_batch(random_embedding.test_matrix,
                           glove_embedding.test_y, batch_size)
    # 模型建立             
    torch.manual_seed(2021)
    torch.cuda.manual_seed(2021)
    random_rnn = MY_RNN(50, 50, random_embedding.len_words)
    torch.manual_seed(2021)
    torch.cuda.manual_seed(2021)
    random_cnn = MY_CNN(50, random_embedding.len_words, random_embedding.longest)
    torch.manual_seed(2021)
    torch.cuda.manual_seed(2021)
    glove_rnn = MY_RNN(50, 50, glove_embedding.len_words, weight=torch.tensor(glove_embedding.embedding, dtype=torch.float))
    torch.manual_seed(2021)
    torch.cuda.manual_seed(2021)
    glove_cnn = MY_CNN(50, glove_embedding.len_words, glove_embedding.longest,weight=torch.tensor(glove_embedding.embedding, dtype=torch.float))
    # rnn+random
    torch.manual_seed(2021)
    torch.cuda.manual_seed(2021)
    trl_ran_rnn,tel_ran_rnn,lol_ran_rnn,tra_ran_rnn,tes_ran_rnn,lon_ran_rnn=\
        NN_embdding(random_rnn,train_random,test_random,learning_rate,  iter_times)
    # cnn+random
    torch.manual_seed(2021)
    torch.cuda.manual_seed(2021)
    trl_ran_cnn,tel_ran_cnn,lol_ran_cnn, tra_ran_cnn, tes_ran_cnn, lon_ran_cnn = \
        NN_embdding(random_cnn, train_random,test_random, learning_rate, iter_times)
    # rnn+glove
    torch.manual_seed(2021)
    torch.cuda.manual_seed(2021)
    trl_glo_rnn,tel_glo_rnn,lol_glo_rnn, tra_glo_rnn, tes_glo_rnn, lon_glo_rnn = \
        NN_embdding(glove_rnn, train_glove,test_glove, learning_rate, iter_times)
    # cnn+glove
    torch.manual_seed(2021)
    torch.cuda.manual_seed(2021)
    trl_glo_cnn,tel_glo_cnn,lol_glo_cnn, tra_glo_cnn, tes_glo_cnn, lon_glo_cnn= \
        NN_embdding(glove_cnn,train_glove,test_glove, learning_rate, iter_times)
   	# 画图部分 
    x=list(range(1,iter_times+1))
    matplotlib.pyplot.subplot(2, 2, 1)
    matplotlib.pyplot.plot(x, trl_ran_rnn, 'r--', label='RNN+random')
    matplotlib.pyplot.plot(x, trl_ran_cnn, 'g--', label='CNN+random')
    matplotlib.pyplot.plot(x, trl_glo_rnn, 'b--', label='RNN+glove')
    matplotlib.pyplot.plot(x, trl_glo_cnn, 'y--', label='CNN+glove')
    matplotlib.pyplot.legend()
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Train Loss")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Loss")
    matplotlib.pyplot.subplot(2, 2, 2)
    matplotlib.pyplot.plot(x, tel_ran_rnn, 'r--', label='RNN+random')
    matplotlib.pyplot.plot(x, tel_ran_cnn, 'g--', label='CNN+random')
    matplotlib.pyplot.plot(x, tel_glo_rnn, 'b--', label='RNN+glove')
    matplotlib.pyplot.plot(x, tel_glo_cnn, 'y--', label='CNN+glove')
    matplotlib.pyplot.legend()
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Test Loss")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Loss")
    matplotlib.pyplot.subplot(2, 2, 3)
    matplotlib.pyplot.plot(x, tra_ran_rnn, 'r--', label='RNN+random')
    matplotlib.pyplot.plot(x, tra_ran_cnn, 'g--', label='CNN+random')
    matplotlib.pyplot.plot(x, tra_glo_rnn, 'b--', label='RNN+glove')
    matplotlib.pyplot.plot(x, tra_glo_cnn, 'y--', label='CNN+glove')
    matplotlib.pyplot.legend()
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Train Accuracy")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Accuracy")
    matplotlib.pyplot.ylim(0, 1)
    matplotlib.pyplot.subplot(2, 2, 4)
    matplotlib.pyplot.plot(x, tes_ran_rnn, 'r--', label='RNN+random')
    matplotlib.pyplot.plot(x, tes_ran_cnn, 'g--', label='CNN+random')
    matplotlib.pyplot.plot(x, tes_glo_rnn, 'b--', label='RNN+glove')
    matplotlib.pyplot.plot(x, tes_glo_cnn, 'y--', label='CNN+glove')
    matplotlib.pyplot.legend()
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Test Accuracy")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Accuracy")
    matplotlib.pyplot.ylim(0, 1)
    matplotlib.pyplot.tight_layout()
    fig = matplotlib.pyplot.gcf()
    fig.set_size_inches(8, 8, forward=True)
    matplotlib.pyplot.savefig('main_plot.jpg')
    matplotlib.pyplot.show()
    matplotlib.pyplot.subplot(2, 1, 1)
    matplotlib.pyplot.plot(x, lon_ran_rnn, 'r--', label='RNN+random')
    matplotlib.pyplot.plot(x, lon_ran_cnn, 'g--', label='CNN+random')
    matplotlib.pyplot.plot(x, lon_glo_rnn, 'b--', label='RNN+glove')
    matplotlib.pyplot.plot(x, lon_glo_cnn, 'y--', label='CNN+glove')
    matplotlib.pyplot.legend()
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Long Sentence Accuracy")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Accuracy")
    matplotlib.pyplot.ylim(0, 1)
    matplotlib.pyplot.subplot(2, 1, 2)
    matplotlib.pyplot.plot(x, lol_ran_rnn, 'r--', label='RNN+random')
    matplotlib.pyplot.plot(x, lol_ran_cnn, 'g--', label='CNN+random')
    matplotlib.pyplot.plot(x, lol_glo_rnn, 'b--', label='RNN+glove')
    matplotlib.pyplot.plot(x, lol_glo_cnn, 'y--', label='CNN+glove')
    matplotlib.pyplot.legend()
    matplotlib.pyplot.legend(fontsize=10)
    matplotlib.pyplot.title("Long Sentence Loss")
    matplotlib.pyplot.xlabel("Iterations")
    matplotlib.pyplot.ylabel("Loss")
    matplotlib.pyplot.tight_layout()
    fig = matplotlib.pyplot.gcf()
    fig.set_size_inches(8, 8, forward=True)
    matplotlib.pyplot.savefig('sub_plot.jpg')
    matplotlib.pyplot.show()

五. 总结

本次实验跑完了15万数据,比上次任务一好多了,推荐用Google的Colab(需要科学上网),或者Kaggle(不需要科学上网)的GPU来跑代码,速度会快很多,比纯CPU快多了。

还有一点需要注意的是,尽管很多地方设置了随机种子,但好像还是每次跑出来的结果不一样?不知道是什么原因,不过结果大体上是相同的·,正确率最高到接近 67 % 67\% 67%.

以上就是本次NLP-Beginner的任务二,谢谢各位的阅读,欢迎各位对本文章指正或者进行讨论,希望可以帮助到大家!

六. 自我推销

  • 我的代码&其它NLP作业传送门

  • LeetCode练习

你可能感兴趣的:(NLP-Beginner,pytorch,神经网络,nlp,python)