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

传送门

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 词嵌入的定义

词嵌入模型即是把每一个词映射到一个高维空间里,每一个词代表着一个高维空间的向量,如下图:
在这里插入图片描述
图中的例子是把每个单词映射到了一个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 卷积定义

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

先介绍一维卷积,先看一张图:在这里插入图片描述
对于左边的图,待处理的向量 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

然后解释二维卷积,如下图:
在这里插入图片描述
左边第一项,就是待处理的矩阵 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,见下图:
步长和零填充

上图的左部分步长为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
在这里插入图片描述
先定义一些符号, 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这个区域里,所有元素的平均值

看一张图:
在这里插入图片描述

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

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

对于 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)

看一张示意图:
在这里插入图片描述
左边的神经元便是我们上一部分得到的向量,长度为 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)

看一张图:
在这里插入图片描述
先回顾输入是什么。

输入是一个特征矩阵 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

先展示总体结果。
在这里插入图片描述
我们先比较CNNRNN

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

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

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

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

4.2.2 Part 2

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

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

在这里插入图片描述
非常遗憾的是,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层在模型测试时不会有任何影响,训练时的效果如图:
在这里插入图片描述


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的任务二,谢谢各位的阅读,欢迎各位对本文章指正或者进行讨论,希望可以帮助到大家!

六. 自我推销

  • 37
    点赞
  • 88
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
基于深度学习文本分类任务是指利用深度学习模型对文本进行情感分类。在这个任务中,我们使用了CNN和RNN模型来进行文本分类。数据集包含了15万余项英文文本,情感分为0-4共五类情感。任务的流程如下:输入数据→特征提取→神经网络设计→结果输出。 在特征提取阶段,我们使用了词嵌入(Word embedding)技术。词嵌入是一种将单词映射到低维向量空间的方法,它可以将单词的语义信息编码为向量表示。在本次任务中,我们参考了博客\[NLP-Beginner 任务:基于深度学习文本分类\](https://pytorch.org/Convolutional Neural Networks for Sentence Classification)中的方法,使用了预训练的词嵌入模型。 神经网络设计阶段,我们采用了卷积神经网络(CNN)和循环神经网络(RNN)的结合。具体来说,我们使用了四个卷积核,大小分别为2×d, 3×d, 4×d, 5×d。这样设计的目的是为了挖掘词组的特征。例如,2×d的卷积核用于挖掘两个连续单词之间的关系。在模型中,2×d的卷积核用红色框表示,3×d的卷积核用黄色框表示。 最后,我们将模型的输出结果进行分类,得到文本的情感分类结果。这个任务的目标是通过深度学习模型对文本进行情感分类,以便更好地理解和分析文本数据。 #### 引用[.reference_title] - *1* *3* [NLP-Brginner 任务:基于深度学习文本分类](https://blog.csdn.net/m0_61688615/article/details/128713638)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [NLP基本任务:基于深度学习文本分类](https://blog.csdn.net/Mr_green_bean/article/details/90480918)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值