NLP-Beginner 任务传送门
我的代码传送门
数据集传送门
本次的NLP(Natural Language Processing)任务是利用深度学习中的卷积神经网络(CNN)和循环神经网络(RNN)来对文本的情感进行分类。
数据集传送门
训练集共有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
本篇博客将会一步一步讲解如何完成本次的NLP任务,具体流程为:
数据输入(英文句子)→特征提取(数字化数据)→神经网络设计(如何用pytorch建立神经网络模型)→结果输出(情感类别)
词嵌入模型即是把每一个词映射到一个高维空间里,每一个词代表着一个高维空间的向量,如下图:
图中的例子是把每个单词映射到了一个7维的空间。
词嵌入模型有三点好处:
比如,上面的四个单词,cat和kitten是近义词,所以他们在高维空间中很相近(为了更好地展现它们的空间距离,可以把7维向量做一个线性变换变成2维,然后进行展示)。
然而,dog和cat意思并不相近,所以离得稍远一点,而houses的意思和cat,kitten,dog更加不相近了,因此会离得更远。
比如下面的四个单词,man和woman是两种性别,king和queen也是对应的两种性别,这两对的单词的差异几乎一致(性别差异),因此他们的距离应该也应该是几乎相同的。
因此,可以从图中看到,man和woman的距离恰好与king和queen的距离相等。
词袋模型和N元特征所提取出来的的特征向量都是超高维的0-1向量,而词嵌入模型的向量每一维是实数,即不仅仅是0或1。
也就是说,词袋模型和N元特征所形成的特征矩阵是稀疏的,但是规模又很大,因而信息利用率很低,其词向量与词向量之间的距离也不能体现词间相似性。
而词嵌入模型所形成的特征矩阵不是稀疏的,且规模相对较小,因此能更好的利用每一维的信息,不再只是局限于0或1。
词袋模型和N元特征的定义可以参考《NLP-Beginner 任务一:基于机器学习的文本分类》。
在词袋模型/N元特征中,只要设置好词和词组,就能把每一个句子(一堆词)转换成对应的0-1表示。
但是,在词嵌入模型中,并没有明确的转换规则,因此我们不可能提前知道每一个词对应的向量。
上面的图的7维向量分别对应(living being, feline, human, gender, royalty, verb, plural),然而这只是为了方便理解所强行设计出的分类,很明显houses在gender维度的取值很难确定。因此,给每一个维度定义好分类标准,是不可能的。
所以,在词嵌入模型中,我们选择不给每一个维度定义一个所谓的语义,我们只是单纯的把每一个词,对应到一个词向量上,我们不关心向量的数值大小代表什么意思,我们只关心这个数值设置得合不合理。换句话说,每个词向量都是参数,是待定的,需要求解,这点和词袋模型/N元特征是完全不同的。
既然词向量是一个参数,那么我们就要为它设置一个初始值。
而上面2.1提到的词嵌入前两大好处,是基于一个前提:向量数值设计合理,因此选取参数的初始值至关重要。
如果参数的初始值选的不好,那么优化模型求解的时候就会使参数值难以收敛,或者收敛到一个较差的极值;相反,如果选得好,就能求出一个更好的参数,甚至能起到加速模型优化的效果。
一般来说,有两种初始化的方法。
随机初始化这种方式十分简单粗暴。
给定一个维度d(比如50),对于每一个词 w w w,我们随机生成一个d维的向量 x ∈ R d x\in \mathbb{R}^d x∈Rd。
注:随机生成的方式有很多,比如 x ∼ N ( 0 , σ 2 I d ) x\sim N(\textbf{0},\sigma^2I_d) x∼N(0,σ2Id),即 x x x服从于一个简单的多元标准正态分布,等等。
这种初始化方式非常简单,但是有可能会生成较劣的初值,也没有一个良好的解释性。
预训练模型初始化,顾名思义,就是拿别人已经训练好的模型作为初值。
也就是说,把别人已经设置好的词向量直接拿过来用。
这种方式的初始化时间会比较长,因为要从别人的词库里面找,需要一定的时间,但是这种初值无疑比随机初始化要好很多,毕竟是别人已经训练好的模型。
网上也有很多训练好的词嵌入模型,比如GloVe(本篇文章会用到)。
给定每个词的词向量,那么就可以把一个句子量化成一个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.10−1.00−3.12+0.20−2.20−1.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.10−8.00−3.12+0.20−6.40−1.14+0.30+3.60+5.14+0.50+2.00+1.60+1.50+3.00+7.00⎦⎤
得到句子的特征矩阵X后,便可以把它放入到神经网络之中。
本部分详细内容可以参考神经网络与深度学习。
CNN一般来说有3~4层
首先需要搞清楚卷积的定义。
先介绍一维卷积,先看一张图:
对于左边的图,待处理的向量 x ∈ R n x\in \mathbb{R}^n x∈Rn是:
[ 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 w∈RK是:
[ 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} y∈Rn−K+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=w∗x
具体公式为:
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=1∑Kwkxt+k−1, t=1,2,...,n−K+1
然后解释二维卷积,如下图:
左边第一项,就是待处理的矩阵 X ∈ R n × d X\in \mathbb{R}^{n\times d} X∈Rn×d。
左边第二项,就是3*3的卷积核 W ∈ R K 1 × K 2 W\in \mathbb{R}^{K_1\times K_2} W∈RK1×K2。
如图所示,对待处理矩阵的右上角,进行卷积操作,就可以得到右边矩阵 Y ∈ R ( n − K 1 + 1 ) × ( d − K 2 + 1 ) Y\in \mathbb{R}^{(n-K_1+1)\times (d-K_2+1)} Y∈R(n−K1+1)×(d−K2+1)右上角的元素-1。
记为:
Y = W ∗ X Y=W * X Y=W∗X
具体公式为:
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=1∑K1k2=1∑K2Wk1,k2Xi+k1−1,j+k2−1 i=1,2,...,n−K1+1, j=d−K2+1
上图的左部分步长为2,右部分的步长为1,不同的步长会得到长度不一样的结果,越长的步长,得到的结果长度越短。
二维卷积的步长也可以类似地进行定义,只不过除了横向的步长,也有纵向的步长,这里不详细叙述。
除此之外,还有值得注意的是padding(零填充),可以看到上图的右部分进行了零填充操作,使得待处理向量的边界元素能进行更多次数的卷积操作。
例子:
待处理的向量 x ∈ R n x\in \mathbb{R}^n x∈Rn是:
[ 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 w∈RK是:
[ 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。
卷积层的设计参考了论文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} X∈Rn×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} X∈Rn×d与卷积核 W ∈ R 2 × d W\in \mathbb{R}^{2\times d} W∈R2×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} Y∈R(n−2+1)×(d+d−1)=R(n−1)×1。
需要注意的是,这里采用四个核的原因是想挖掘词组的特征。
比如说, 2 × d 2\times d 2×d 的核是用来挖掘连续两个单词之间的关系的,而 5 × d 5\times d 5×d 的核用来连续挖掘五个单词之间的关系。
卷积层的参数即是卷积核。
对于一个句子,特征矩阵是 X ∈ R n × d X\in \mathbb{R}^{n\times d} X∈Rn×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} Y1∈R(n−1)×1, Y2∈R(n−2)×1, Y3∈R(n−3)×1, Y4∈R(n−4)×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)。
激活函数可以参考维基百科。
在本次实战中,采用了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)。
Pooling层相当于是对特征矩阵/向量提取出一些有用的信息,从而减少特征的规模,不仅减少了计算量,也能去除冗余特征。
Pooling有两种方法:
对一个区域,取最大的一个元素
y m , n = max i ∈ R m , n x i y_{m,n}=\text{max}_{i\in R_{m,n}} x_i ym,n=maxi∈Rm,nxi
即取 R m , n R_{m,n} Rm,n这个区域里,最大的元素
对一个区域,取所有元素的平均值
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,n∣1i∈Rm,n∑xi
即取 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(n−i)×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 ll∗4的向量,即
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))T∈R(ll∗4)×1
看一张示意图:
左边的神经元便是我们上一部分得到的向量,长度为 l l ∗ 4 l_l*4 ll∗4。
而我们的目的是输出一个句子的情感类别,参考上一次任务《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 ll∗4的向量转换成长度为 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} A∈R5×(ll∗4),Y∈R(ll∗4)×1,b∈R5×1。
最终,整个神经网络会输出一个长度为 5 5 5 的向量 p p p。
如此一来 A A A 和 b b b 便是需要待定的系数。
CNN一般来说有2~3层
输入是一个特征矩阵 X ∈ R n × d X\in \mathbb{R}^{n\times d} X∈Rn×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.10−1.00−3.12+0.20−2.20−1.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} xi∈Rd
在CNN中,我们是直接对特征矩阵X进行操作,而在RNN中,我们是逐个对 x i x_i xi进行操作,步骤如下:
(1) z t = U h t − 1 + W x t + b \quad z_t=Uh_{t-1}+Wx_t+b zt=Uht−1+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} U∈Rlh×lh, W∈Rlh×d, z,bh∈Rlh
(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)
这一层的目的,便是把序列 { x i } i = 1 n \{x_i\}_{i=1}^n {xi}i=1n逐个输入到隐藏层去,与参数发生作用,输出的结果 h t h_t ht也会参与到下一次循环计算之中,实现了一种记忆功能,使神经网络具有了(短期)的记忆能力。
这种记忆能力有助于神经网络中从输入中挖掘更多的特征,及其相互关系,而不再只是像CNN一样局限于2、3、4、5个词之间的关系。
RNN的激活层与CNN激活层是类似的,激活函数可以参考维基百科。
在本次实战中,我的RNN没有额外加入激活层。
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} A∈R5×lh,ht∈Rlh×1,bl∈R5×1, A A A 和 b b b 也是需要待定的系数。
最终,整个神经网络会输出一个长度为 5 5 5 的向量 p p p。
有了上面的CNN和RNN的模型,接下来就是求解神经网络中的参数了。先回顾一下两个模型的参数:
对于任意一个网络,把它们的参数记作 θ \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
有了模型,我们就要对模型的好坏做出一个评价。也就是说,给定一组参数 θ \theta θ,我们要去量化一个模型的好坏,那么我们就要定义一个损失函数。
一般来说,有以下几种损失函数:
函数 | 公式 | 注释 |
---|---|---|
0-1损失函数 | I ( y ≠ f ( x ) ) I(y\ne f(x)) I(y=f(x)) | 不可导 |
绝对值损失函数 | | y − f ( x ) y-f(x) y−f(x)| | 适用于连续值 |
平方值损失函数 | ( y − f ( x ) ) 2 (y-f(x))^2 (y−f(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,1−yf(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=1∑Cyc(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=1∑NL(NNθ(x(n)),y(n))
有了损失函数,我们就可以通过找到损失函数的最小值,来求解最优的参数矩阵 θ \theta θ。
梯度下降的基本思想是,对于每个固定的参数,求其梯度(导数),然后利用梯度(导数),进行对参数的更新。
在这里,公式是
θ t + 1 ← θ t − α ∂ L ( θ t ) ∂ θ t \theta_{t+1}\leftarrow \theta_t-\alpha\frac{\partial L(\theta_t)}{\partial \theta_t} θt+1←θt−α∂θt∂L(θt)
由于Pytorch求解参数并不需要我们求梯度且梯度计算非常复杂,因此在这里就暂时不介绍具体如何求梯度过程。
感兴趣的同学可以参考神经网络与深度学习。
可以看到RNN在测试集的准确率(最大值)比CNN都要高,且测试集的损失值(最小值)也要比CNN的要低。
再比较随机初始化与GloVe初始化。
在同种模型下,GloVe初始化也要比随机初始化的效果好,即在测试集准确率大、测试集损失值小。
最终,测试集准确率大约在 66 % 66\% 66% 左右。
以上结果并不能说明RNN在长句子情感分类方面的优势。因为RNN具有短期记忆,能处理好词与词之间的关系,所以我想看看RNN在长句子分类上是否有一个比较好的结果。
因此,在训练的过程中,我特别关注了测试集中单词数大于20的句子的损失值和正确率,结果如图:
非常遗憾的是,RNN的效果并不比CNN好,而且无论是CNN还是RNN,长句子的情感分类准确率也只有大概 55 % 55\% 55% 左右,比总体的平均正确率低了约 10 % 10\% 10%。
因此,在这一点上有待进一步挖掘。
本次使用了Python中的torch库,并使用了cuda加速。
若不想要GPU加速,只需要把comparison_plot_batch.py
和Neural_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层在模型测试时不会有任何影响,训练时的效果如图:
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))
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
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
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练习