From dddc1509fc2fe91e0c1555f79d2a58dabc7d8432 Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Sun, 25 Dec 2016 16:26:40 +0800 Subject: [PATCH 01/11] add sentiment --- understand_sentiment/README.md | 67 +++++++++++++++++++- understand_sentiment/image/rnn.png | Bin 0 -> 7387 bytes understand_sentiment/image/stacked_lstm.jpg | Bin 0 -> 31077 bytes understand_sentiment/image/text_cnn.png | Bin 0 -> 31074 bytes 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100755 understand_sentiment/image/rnn.png create mode 100644 understand_sentiment/image/stacked_lstm.jpg create mode 100644 understand_sentiment/image/text_cnn.png diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index e420eb55..a4738345 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -1 +1,66 @@ -TODO: Write about https://github.com/PaddlePaddle/Paddle/tree/develop/demo/sentiment +# 情感分析 +## 背景介绍 +  在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤),也可以是三类,如(积极,消极,中性)等等。 +  情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论。为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。 +  对电影评论进行情感分析(正面,负面)的例子如下面的表格1所示。 + +| 电影评论 | 类别 | +| -------- | ----- | +| 在冯小刚这几年的电影里,算最好的一部的了| 正面 | +| 很不好看,好像一个地方台的电视剧 | 负面 | +| 为了讽刺官场刻意丑化农村人的傻片子,圆方镜头全程炫技,色调背景美则美矣,但剧情拖沓,口音不伦不类,一直努力却始终无法入戏。不建议进电影院观看,不然睡着了躺都没地方躺。| 负面| +|剧情四星。但是圆镜视角加上婺源的风景整个非常有中国写意山水画的感觉,看得实在太舒服了。。难怪作为今年TIFF special presentation的开幕电影。范爷美爆,再往上加一星。|正面| + +
表格 1 电影评论情感分析
+  实际上,在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类问题可以分解为两个子问题:文本表示和分类。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),分类方法有SVM,LR,Boosting等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,这种表示浮于表面,并未充分表示文本的语义信息。例如,句子`这部电影糟糕透了`和`一个乏味,空洞,没有内涵的作品`在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子`小明很喜欢小芳,但是小芳不喜欢小明`和`小芳很喜欢小明,但是小明不喜欢小芳`的BOW相似度为1,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 +## 模型概览 +  本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。我们首先介绍处理文本的卷积神经网络。 +### 文本卷积神经网络 +  卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为2D网格的像素点,自然语言可以视为1D的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示,且其对于数据的某些变化具有不变性。大量实验表明,卷积神经网络能高效的对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)。 +
![rnn](image/text_cnn.png)
+
图 1 卷积神经网络文本分类模型
+  假设一个句子的长度为$n$,其中第$i$个词的word embedding为$x_i\in\mathbb{R}^k$,其维度大小为$k$,我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把filter(也称为kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: +$$c_i=f(w\cdot x_{i:i+h-1}+b)$$ +  其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如sigmoid。将filter应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$序列,产生一个feature map: +$$c=[c_1,c_2,\ldots,c_{n-h+1}]$$ +  其中$c \in \mathbb{R}^{n-h+1}$。接下来我们对feature map采用max pooling over time操作得到此filter对应的特征: +$$\hat c=max(c)$$ +  即,$\hat c$是feature map中所有元素的最大值。pooling机制自动处理了句子长度不一的问题。在实际应用中,我们会使用多个filter来处理句子,窗口大小相同的filters堆叠起来形成一个矩阵(上文中的单个filter参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的filters来处理句子,最后,将所有filters得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 +  可以将上文所述的卷积神经网络的filter理解为特定语义n-gram的探测器(detector),其优点是避免了传统n-gram的高维稀疏表示问题,运算和训练速度十分快,准确率也很高(ref)。但是它难以扩展为深层文本卷积网络,基于此,N. Kalchbrenner, et al.(2014)提出了k-max pooling,使用其可以构建出深层文本卷积网络,有兴趣的读者可以参考相关文献。 +### 循环神经网络 +#### 简单的循环神经网络 +  循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的(Siegelmann, H. T. and Sontag, E. D., 1995)。 +  自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如lstm等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型,句法解析,语义角色标注(或一般的序列标注),语义表示,图文生成,对话,机器翻译等任务上均表现优异甚至成为目前效果最好的方法。 +
![rnn](image/rnn.png)
+
图 1 循环神经网络按时间展开的示意图
+  循环神经网络按时间展开后如图1所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐藏层的输出$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: +$$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$ +  其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为elementwise的sigmoid函数。在处理自然语言时,一般会先将词(one-hot表示)映射为其embedding表示,然后再作为循环神经网络每一时刻的输入$x_t$。可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 +  可以看出,隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象(Bengio Y, Simard P, Frasconi P., 1994)。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了lstm模型。 +#### 长时短期记忆循环神经网络 +  相比于简单的循环神经网络,lstm增加了记忆单元$c$,输入门$i$,遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm(无 peep-hole连接的版本)的循环神经网络表示的函数记为F,则其公式为: +$$ h_t=F(x_t,h_{t-1})$$ +  $F$由下列公式组合而成: +\begin{align} +i_t & = \sigma(W_{xi}x_t+W_{hi}h_{h-1}+b_i)\\\\ +f_t & = \sigma(W_{xf}x_t+W_{hf}h_{h-1}+b_f)\\\\ +c_t & = f_t\odot c_{t-1}+i_t\odot tanh(W_{xc}x_t+W_{hc}h_{h-1}+b_c)\\\\ +o_t & = \sigma(W_{xo}x_t+W_{ho}h_{h-1}+b_o)\\\\ +h_t & = o_t\odot tanh(c_t)\\\\ +\end{align} +  其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元(记忆单元一般对外不可见,$h_t$对外部可见)及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为elementwise的双曲正切函数,$\odot$表示elementwise的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数。这三种门各自以不同的方式控制着记忆单元$c$。实际上,lstm的思想正是通过给简单的循环神经网络增加记忆及控制门的方式增强了其处理远距离依赖问题的能力。类似原理的对于简单循环神经网络的改进还有Gated Recurrent Unit (GRU)( Cho K, Van Merriënboer B, Gulcehre C, et al. 2014),其设计更为简洁一些。**这些改进虽然各有不同,但是对他们的宏观描述却与简单的循环神经网络一样,如图1所示,隐状态依据当前输入及前一时刻的隐状态来改变,不断的循环这一过程直至输入处理完毕:** +$$ h_t=Recrurent(x_t,h_{t-1})$$ +  对于正常顺序的循环神经网络而言,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以构建更加强有力的深层双向循环神经网络(deep bi-directional recurrent neural networks)对时序数据进行建模。 +#### 使用循环神经网络的组合进行文本分类 +  一个简单的做法是分别使用正向lstm-rnn和反向lstm-rnn处理文本,取最后一个时刻的隐层值拼接起来做为文本的定长向量表示,将其连接至softmax得到文本分类模型。但是这样的文本分类模型是一个浅层模型。考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建stacked lstm-rnn。如图2所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用max pooling over time得到文本定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 +
![rnn](image/stacked_lstm.jpg)
+
图 2 stacked lstm-rnn for text classification
+ + + + + + + + +## 参考文献 \ No newline at end of file diff --git a/understand_sentiment/image/rnn.png b/understand_sentiment/image/rnn.png new file mode 100755 index 0000000000000000000000000000000000000000..1ab1fa974c8aadaf50a15d220f047b243d4d4c1a GIT binary patch literal 7387 zcmd6MXE>YT+qX?(6tQWE5)`#!Z>9DuHA|^lwSwAP%&NVKy=w2Gw%U6us#H<6cT358 z>;HX^=Y2oDU!D(7jw8u+uHX4P&s^7;J9i?rG!%)!bYKh&3}R&^c^wQ4EF`e*B*4bN zz+_V+t^fo~R~l9YDabmQ|OQum&2lF8&#tNr025Pw|F&_) ze7OO0tr>HrF<_cq_uv6={#@UDUOocQ{vj|`88TOo|Em6n4_Np7_-aiaRvFz{tBn~? zfByv>s{W+_Fst<$|B-p;34l`0KA%DMxlVW5H}r-XBcQS z4$;VnC08^0_rN!Tc}7kK{6Y#;BwR6*OHp#u*Cx8)Bv(ATkt=#@-# zHCkd>7tM4%ggF| zn(m|CJ)s(K?@E=28$g2On&*v=wK?@>Nr}4!1G#6(A#@*}cT33xQG8~tdiJ@=!m9t~UJda8E`vL}Vyu;NH$U->=_$qEix34K)>Bd2asJRAZ>r$*@b)w@FNf0LlZ{tsBnt%)qn(x6k{@S^+qxyYt zj*J)QM_Bu|sa+P!4zy1`iP9y@F#qZ+#R&@!O>6jun;$Pnr?F=4xWIm(1Rah0Ofg3j z)Imka1$*2dO<|GXPZNldh5*~3fTu`H7 z(I4hlo2H3(9~wIUM!3jt5S~=9*h|5u{3@8&GI!%1w&5H5#28*n>VY_`bcbJNGWE+3 zci4ZB+Ms@pVY#r7B!G{>QyK6dB+r4}BA=<#W?rdxOT;q@6?@ z>ed9@ZH?qJ#KM6}`pxNaGHW*bz6LwQ>@lgin2;pGVMo7ejwBO7L&8 zCXSt%yflf_gv1p`&|o4=bY+)G>;$i;bNxy@*`#z#QnHU)=j4p#Bl7#25Fv$UM#NNL zA}{f!PhwA3XS(&{_)qHarXydz#5M-CHMFHA�Solq~_F#Qq-!IlF0Nn;wTIMjH%8 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 DyXWBY literal 0 HcmV?d00001 diff --git a/understand_sentiment/image/stacked_lstm.jpg b/understand_sentiment/image/stacked_lstm.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4239055050966e0095e188a8c81d860711bce29d GIT binary patch literal 31077 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 literal 0 HcmV?d00001 diff --git a/understand_sentiment/image/text_cnn.png b/understand_sentiment/image/text_cnn.png new file mode 100644 index 0000000000000000000000000000000000000000..bb31c3fd6f68b83390591b306aff8f8f021ec497 GIT binary patch literal 31074 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 Date: Sun, 25 Dec 2016 16:43:51 +0800 Subject: [PATCH 02/11] add sentiment. --- understand_sentiment/README.md | 42 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index a4738345..b15e24af 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -1,8 +1,8 @@ # 情感分析 ## 背景介绍 -  在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤),也可以是三类,如(积极,消极,中性)等等。 -  情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论。为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。 -  对电影评论进行情感分析(正面,负面)的例子如下面的表格1所示。 +
  在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤),也可以是三类,如(积极,消极,中性)等等。 +
  情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论。为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。 +
  对电影评论进行情感分析(正面,负面)的例子如下面的表格1所示。 | 电影评论 | 类别 | | -------- | ----- | @@ -12,35 +12,35 @@ |剧情四星。但是圆镜视角加上婺源的风景整个非常有中国写意山水画的感觉,看得实在太舒服了。。难怪作为今年TIFF special presentation的开幕电影。范爷美爆,再往上加一星。|正面|
表格 1 电影评论情感分析
-  实际上,在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类问题可以分解为两个子问题:文本表示和分类。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),分类方法有SVM,LR,Boosting等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,这种表示浮于表面,并未充分表示文本的语义信息。例如,句子`这部电影糟糕透了`和`一个乏味,空洞,没有内涵的作品`在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子`小明很喜欢小芳,但是小芳不喜欢小明`和`小芳很喜欢小明,但是小明不喜欢小芳`的BOW相似度为1,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 +
  实际上,在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类问题可以分解为两个子问题:文本表示和分类。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),分类方法有SVM,LR,Boosting等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,这种表示浮于表面,并未充分表示文本的语义信息。例如,句子`这部电影糟糕透了`和`一个乏味,空洞,没有内涵的作品`在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子`小明很喜欢小芳,但是小芳不喜欢小明`和`小芳很喜欢小明,但是小明不喜欢小芳`的BOW相似度为1,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 ## 模型概览 -  本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。我们首先介绍处理文本的卷积神经网络。 +
  本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。我们首先介绍处理文本的卷积神经网络。 ### 文本卷积神经网络 -  卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为2D网格的像素点,自然语言可以视为1D的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示,且其对于数据的某些变化具有不变性。大量实验表明,卷积神经网络能高效的对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)。 +
  卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为2D网格的像素点,自然语言可以视为1D的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示,且其对于数据的某些变化具有不变性。大量实验表明,卷积神经网络能高效的对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)。
![rnn](image/text_cnn.png)
图 1 卷积神经网络文本分类模型
-  假设一个句子的长度为$n$,其中第$i$个词的word embedding为$x_i\in\mathbb{R}^k$,其维度大小为$k$,我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把filter(也称为kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: +
  假设一个句子的长度为$n$,其中第$i$个词的word embedding为$x_i\in\mathbb{R}^k$,其维度大小为$k$,我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把filter(也称为kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: $$c_i=f(w\cdot x_{i:i+h-1}+b)$$ -  其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如sigmoid。将filter应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$序列,产生一个feature map: +
  其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如sigmoid。将filter应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$序列,产生一个feature map: $$c=[c_1,c_2,\ldots,c_{n-h+1}]$$ -  其中$c \in \mathbb{R}^{n-h+1}$。接下来我们对feature map采用max pooling over time操作得到此filter对应的特征: +
  其中$c \in \mathbb{R}^{n-h+1}$。接下来我们对feature map采用max pooling over time操作得到此filter对应的特征: $$\hat c=max(c)$$ -  即,$\hat c$是feature map中所有元素的最大值。pooling机制自动处理了句子长度不一的问题。在实际应用中,我们会使用多个filter来处理句子,窗口大小相同的filters堆叠起来形成一个矩阵(上文中的单个filter参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的filters来处理句子,最后,将所有filters得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 -  可以将上文所述的卷积神经网络的filter理解为特定语义n-gram的探测器(detector),其优点是避免了传统n-gram的高维稀疏表示问题,运算和训练速度十分快,准确率也很高(ref)。但是它难以扩展为深层文本卷积网络,基于此,N. Kalchbrenner, et al.(2014)提出了k-max pooling,使用其可以构建出深层文本卷积网络,有兴趣的读者可以参考相关文献。 +
  即,$\hat c$是feature map中所有元素的最大值。pooling机制自动处理了句子长度不一的问题。在实际应用中,我们会使用多个filter来处理句子,窗口大小相同的filters堆叠起来形成一个矩阵(上文中的单个filter参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的filters来处理句子,最后,将所有filters得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 +
  可以将上文所述的卷积神经网络的filter理解为特定语义n-gram的探测器(detector),其优点是避免了传统n-gram的高维稀疏表示问题,运算和训练速度十分快,准确率也很高(ref)。但是它难以扩展为深层文本卷积网络,基于此,N. Kalchbrenner, et al.(2014)提出了k-max pooling,使用其可以构建出深层文本卷积网络,有兴趣的读者可以参考相关文献。 ### 循环神经网络 #### 简单的循环神经网络 -  循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的(Siegelmann, H. T. and Sontag, E. D., 1995)。 -  自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如lstm等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型,句法解析,语义角色标注(或一般的序列标注),语义表示,图文生成,对话,机器翻译等任务上均表现优异甚至成为目前效果最好的方法。 +
  循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的(Siegelmann, H. T. and Sontag, E. D., 1995)。 +
  自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如lstm等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型,句法解析,语义角色标注(或一般的序列标注),语义表示,图文生成,对话,机器翻译等任务上均表现优异甚至成为目前效果最好的方法。
![rnn](image/rnn.png)
图 1 循环神经网络按时间展开的示意图
-  循环神经网络按时间展开后如图1所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐藏层的输出$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: +
  循环神经网络按时间展开后如图1所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐藏层的输出$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: $$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$ -  其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为elementwise的sigmoid函数。在处理自然语言时,一般会先将词(one-hot表示)映射为其embedding表示,然后再作为循环神经网络每一时刻的输入$x_t$。可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 -  可以看出,隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象(Bengio Y, Simard P, Frasconi P., 1994)。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了lstm模型。 +
  其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为elementwise的sigmoid函数。在处理自然语言时,一般会先将词(one-hot表示)映射为其embedding表示,然后再作为循环神经网络每一时刻的输入$x_t$。可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 +
  可以看出,隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象(Bengio Y, Simard P, Frasconi P., 1994)。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了lstm模型。 #### 长时短期记忆循环神经网络 -  相比于简单的循环神经网络,lstm增加了记忆单元$c$,输入门$i$,遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm(无 peep-hole连接的版本)的循环神经网络表示的函数记为F,则其公式为: +
  相比于简单的循环神经网络,lstm增加了记忆单元$c$,输入门$i$,遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm(无 peep-hole连接的版本)的循环神经网络表示的函数记为F,则其公式为: $$ h_t=F(x_t,h_{t-1})$$ -  $F$由下列公式组合而成: +
  $F$由下列公式组合而成: \begin{align} i_t & = \sigma(W_{xi}x_t+W_{hi}h_{h-1}+b_i)\\\\ f_t & = \sigma(W_{xf}x_t+W_{hf}h_{h-1}+b_f)\\\\ @@ -48,11 +48,11 @@ c_t & = f_t\odot c_{t-1}+i_t\odot tanh(W_{xc}x_t+W_{hc}h_{h-1}+b_c)\\\\ o_t & = \sigma(W_{xo}x_t+W_{ho}h_{h-1}+b_o)\\\\ h_t & = o_t\odot tanh(c_t)\\\\ \end{align} -  其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元(记忆单元一般对外不可见,$h_t$对外部可见)及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为elementwise的双曲正切函数,$\odot$表示elementwise的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数。这三种门各自以不同的方式控制着记忆单元$c$。实际上,lstm的思想正是通过给简单的循环神经网络增加记忆及控制门的方式增强了其处理远距离依赖问题的能力。类似原理的对于简单循环神经网络的改进还有Gated Recurrent Unit (GRU)( Cho K, Van Merriënboer B, Gulcehre C, et al. 2014),其设计更为简洁一些。**这些改进虽然各有不同,但是对他们的宏观描述却与简单的循环神经网络一样,如图1所示,隐状态依据当前输入及前一时刻的隐状态来改变,不断的循环这一过程直至输入处理完毕:** +
  其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元(记忆单元一般对外不可见,$h_t$对外部可见)及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为elementwise的双曲正切函数,$\odot$表示elementwise的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数。这三种门各自以不同的方式控制着记忆单元$c$。实际上,lstm的思想正是通过给简单的循环神经网络增加记忆及控制门的方式增强了其处理远距离依赖问题的能力。类似原理的对于简单循环神经网络的改进还有Gated Recurrent Unit (GRU)( Cho K, Van Merriënboer B, Gulcehre C, et al. 2014),其设计更为简洁一些。**这些改进虽然各有不同,但是对他们的宏观描述却与简单的循环神经网络一样,如图1所示,隐状态依据当前输入及前一时刻的隐状态来改变,不断的循环这一过程直至输入处理完毕:** $$ h_t=Recrurent(x_t,h_{t-1})$$ -  对于正常顺序的循环神经网络而言,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以构建更加强有力的深层双向循环神经网络(deep bi-directional recurrent neural networks)对时序数据进行建模。 +
  对于正常顺序的循环神经网络而言,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以构建更加强有力的深层双向循环神经网络(deep bi-directional recurrent neural networks)对时序数据进行建模。 #### 使用循环神经网络的组合进行文本分类 -  一个简单的做法是分别使用正向lstm-rnn和反向lstm-rnn处理文本,取最后一个时刻的隐层值拼接起来做为文本的定长向量表示,将其连接至softmax得到文本分类模型。但是这样的文本分类模型是一个浅层模型。考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建stacked lstm-rnn。如图2所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用max pooling over time得到文本定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 +
  一个简单的做法是分别使用正向lstm-rnn和反向lstm-rnn处理文本,取最后一个时刻的隐层值拼接起来做为文本的定长向量表示,将其连接至softmax得到文本分类模型。但是这样的文本分类模型是一个浅层模型。考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建stacked lstm-rnn。如图2所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用max pooling over time得到文本定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。
![rnn](image/stacked_lstm.jpg)
图 2 stacked lstm-rnn for text classification
From 5f61b0c06451c9c11e797aee2029ea48845aa62c Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Tue, 27 Dec 2016 15:21:01 +0800 Subject: [PATCH 03/11] add *.sh and *.py, add readme.md other sections --- understand_sentiment/.gitignore | 12 + understand_sentiment/README.md | 437 ++++++++++++++++++++++++- understand_sentiment/data/get_imdb.sh | 51 +++ understand_sentiment/dataprovider.py | 35 ++ understand_sentiment/predict.py | 150 +++++++++ understand_sentiment/predict.sh | 27 ++ understand_sentiment/preprocess.py | 359 ++++++++++++++++++++ understand_sentiment/preprocess.sh | 22 ++ understand_sentiment/sentiment_net.py | 162 +++++++++ understand_sentiment/test.sh | 39 +++ understand_sentiment/train.sh | 29 ++ understand_sentiment/trainer_config.py | 40 +++ 12 files changed, 1354 insertions(+), 9 deletions(-) create mode 100644 understand_sentiment/.gitignore create mode 100755 understand_sentiment/data/get_imdb.sh create mode 100755 understand_sentiment/dataprovider.py create mode 100755 understand_sentiment/predict.py create mode 100755 understand_sentiment/predict.sh create mode 100755 understand_sentiment/preprocess.py create mode 100755 understand_sentiment/preprocess.sh create mode 100644 understand_sentiment/sentiment_net.py create mode 100755 understand_sentiment/test.sh create mode 100755 understand_sentiment/train.sh create mode 100644 understand_sentiment/trainer_config.py diff --git a/understand_sentiment/.gitignore b/understand_sentiment/.gitignore new file mode 100644 index 00000000..40d91f5b --- /dev/null +++ b/understand_sentiment/.gitignore @@ -0,0 +1,12 @@ +data/aclImdb +data/imdb +data/pre-imdb +data/mosesdecoder-master +logs/ +model_output +dataprovider_copy_1.py +model.list +test.log +train.log +*.pyc +.DS_Store diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index b15e24af..43eba6d6 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -11,14 +11,16 @@ | 为了讽刺官场刻意丑化农村人的傻片子,圆方镜头全程炫技,色调背景美则美矣,但剧情拖沓,口音不伦不类,一直努力却始终无法入戏。不建议进电影院观看,不然睡着了躺都没地方躺。| 负面| |剧情四星。但是圆镜视角加上婺源的风景整个非常有中国写意山水画的感觉,看得实在太舒服了。。难怪作为今年TIFF special presentation的开幕电影。范爷美爆,再往上加一星。|正面| -
表格 1 电影评论情感分析
+

表格 1 电影评论情感分析


  实际上,在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类问题可以分解为两个子问题:文本表示和分类。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),分类方法有SVM,LR,Boosting等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,这种表示浮于表面,并未充分表示文本的语义信息。例如,句子`这部电影糟糕透了`和`一个乏味,空洞,没有内涵的作品`在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子`小明很喜欢小芳,但是小芳不喜欢小明`和`小芳很喜欢小明,但是小明不喜欢小芳`的BOW相似度为1,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 ## 模型概览
  本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。我们首先介绍处理文本的卷积神经网络。 ### 文本卷积神经网络
  卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为2D网格的像素点,自然语言可以视为1D的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示,且其对于数据的某些变化具有不变性。大量实验表明,卷积神经网络能高效的对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)。 -
![rnn](image/text_cnn.png)
-
图 1 卷积神经网络文本分类模型
+

+
+图 1 卷积神经网络文本分类模型 +


  假设一个句子的长度为$n$,其中第$i$个词的word embedding为$x_i\in\mathbb{R}^k$,其维度大小为$k$,我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把filter(也称为kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: $$c_i=f(w\cdot x_{i:i+h-1}+b)$$
  其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如sigmoid。将filter应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$序列,产生一个feature map: @@ -31,9 +33,11 @@ $$\hat c=max(c)$$ #### 简单的循环神经网络
  循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的(Siegelmann, H. T. and Sontag, E. D., 1995)。
  自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如lstm等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型,句法解析,语义角色标注(或一般的序列标注),语义表示,图文生成,对话,机器翻译等任务上均表现优异甚至成为目前效果最好的方法。 -
![rnn](image/rnn.png)
-
图 1 循环神经网络按时间展开的示意图
-
  循环神经网络按时间展开后如图1所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐藏层的输出$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: +

+
+图 2 循环神经网络按时间展开的示意图 +

+
  循环神经网络按时间展开后如图2所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐藏层的输出$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: $$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$
  其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为elementwise的sigmoid函数。在处理自然语言时,一般会先将词(one-hot表示)映射为其embedding表示,然后再作为循环神经网络每一时刻的输入$x_t$。可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。
  可以看出,隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象(Bengio Y, Simard P, Frasconi P., 1994)。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了lstm模型。 @@ -52,15 +56,430 @@ h_t & = o_t\odot tanh(c_t)\\\\ $$ h_t=Recrurent(x_t,h_{t-1})$$
  对于正常顺序的循环神经网络而言,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以构建更加强有力的深层双向循环神经网络(deep bi-directional recurrent neural networks)对时序数据进行建模。 #### 使用循环神经网络的组合进行文本分类 -
  一个简单的做法是分别使用正向lstm-rnn和反向lstm-rnn处理文本,取最后一个时刻的隐层值拼接起来做为文本的定长向量表示,将其连接至softmax得到文本分类模型。但是这样的文本分类模型是一个浅层模型。考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建stacked lstm-rnn。如图2所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用max pooling over time得到文本定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 -
![rnn](image/stacked_lstm.jpg)
-
图 2 stacked lstm-rnn for text classification
+
  一个简单的做法是分别使用正向lstm-rnn和反向lstm-rnn处理文本,取最后一个时刻的隐层值拼接起来做为文本的定长向量表示,将其连接至softmax得到文本分类模型。但是这样的文本分类模型是一个浅层模型。考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建stacked lstm-rnn。如图3所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用max pooling over time得到文本定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 +

+
+图 3 stacked lstm-rnn for text classification +

+## 数据准备 +### 数据介绍与下载 +我们以IMDB情感分析数据集为例进行介绍。训练模型之前, 我们需要预处理数椐并构建一个字典。 首先, 你可以使用下面的脚本下载 IMDB 数椐集和[Moses](http://www.statmt.org/moses/)工具, 我们提供了一个数据预处理脚本,它不仅能够处理IMDB数据,还能处理其他用户自定义的数据。 为了使用提前编写的脚本,需要将标记的训练和测试样本移动到另一个路径,这已经在`get_imdb.sh`中完成。 +``` +./get_imdb.sh +``` +如果数椐获取成功,你将在目录```data```中看到下面的文件: +``` +aclImdb get_imdb.sh imdb mosesdecoder-master +``` +* aclImdb: 从外部网站上下载的原始数椐集。 +* imdb: 仅包含训练和测试数椐集。 +* mosesdecoder-master: Moses 工具。 +IMDB数据集包含25,000个已标注过的电影评论用于训练,25,000个用于测试。负面的评论的得分小于等于4,正面的评论的得分大于等于7,满分10分。 +### 数据预处理 +在这个例子中,我们只使用已经标注过的训练集和测试集,且默认在训练集上构建字典。训练集已经做了随机打乱排序。 Moses 工具中的脚本`tokenizer.perl` 用于切分单单词和标点符号。执行下面的命令就可以预处理数椐。 +``` +./preprocess.sh +``` +preprocess.sh: +``` +data_dir="./data/imdb" +python preprocess.py -i data_dir +``` +* data_dir: 输入数椐所在目录。 +* preprocess.py: 预处理脚本。 + +运行成功后目录`data/pre-imdb` 结构如下: + +``` +dict.txt labels.list test.list test_part_000 train.list train_part_000 +``` + +* test\_part\_000 and train\_part\_000: 所有标记的测试集和训练集, 训练集已经随机打乱。 +* train.list and test.list: 训练集和测试集文件列表。 +* dict.txt: 利用训练集生成的字典。 +* labels.txt: neg 0, pos 1, 含义:标签0表示负面的评论,标签1表示正面的评论。 + +### 提供数据给PaddlePaddle +PaddlePaddle可以读取Python写的传输数据脚本,下面dataprovider.py文件给出了完整例子,主要包括两部分: + +* hook: 定义文本信息、类别Id的数据类型。文本被定义为整数序列`integer_value_sequence`,类别被定义为整数`integer_value` +* process: yield文本信息和类别Id,和hook里定义顺序一致。process读取的文件的行为类别和评论文本,以`'\t\t'`分隔。 + +```python +from paddle.trainer.PyDataProvider2 import * + +def hook(settings, dictionary, **kwargs): + settings.word_dict = dictionary + settings.input_types = [ + integer_value_sequence(len(settings.word_dict)), integer_value(2) + ] + settings.logger.info('dict len : %d' % (len(settings.word_dict))) + + +@provider(init_hook=hook) +def process(settings, file_name): + with open(file_name, 'r') as fdata: + for line_count, line in enumerate(fdata): + label, comment = line.strip().split('\t\t') + label = int(label) + words = comment.split() + word_slot = [ + settings.word_dict[w] for w in words if w in settings.word_dict + ] + yield word_slot, label +``` + +## 模型配置说明 +`trainer_config.py` 是一个配置文件的例子。第一行从`sentiment_net.py`中导出预定义的网络。 + +trainer_config.py: + +```python +from sentiment_net import * + +data_dir = "./data/pre-imdb" +# whether this config is used for test +is_test = get_config_arg('is_test', bool, False) +# whether this config is used for prediction +is_predict = get_config_arg('is_predict', bool, False) +dict_dim, class_dim = sentiment_data(data_dir, is_test, is_predict) + +################## Algorithm Config ##################### + +settings( + batch_size=128, + learning_rate=2e-3, + learning_method=AdamOptimizer(), + regularization=L2Regularization(8e-4), + gradient_clipping_threshold=25 +) + +#################### Network Config ###################### +stacked_lstm_net(dict_dim, class_dim=class_dim, + stacked_num=3, is_predict=is_predict) +# bidirectional_lstm_net(dict_dim, class_dim=class_dim, is_predict=is_predict) +# convolution_net(dict_dim, class_dim=class_dim, is_predict=is_predict) +``` + +get\_config\_arg(): 获取通过 `--config_args=xx` 设置的命令行参数。 +### 优化算法配置 + + * 使用随机梯度下降(sgd)算法。 + * 使用 adam 优化。 + * 设置batch size大小为128。 + * 设置全局学习率。 + * 设置L2正则。 + * 设置梯度裁剪(clipping)阈值。 + +### 数据定义 +数据定义在方法sentiment_data之中,其实现在文件`sentiment_net.py`中: +```python +def sentiment_data(data_dir=None, + is_test=False, + is_predict=False, + train_list="train.list", + test_list="test.list", + dict_file="dict.txt"): + """ + Predefined data provider for sentiment analysis. + is_test: whether this config is used for test. + is_predict: whether this config is used for prediction. + train_list: text file name, containing a list of training set. + test_list: text file name, containing a list of testing set. + dict_file: text file name, containing dictionary. + """ + dict_dim = len(open(join_path(data_dir, "dict.txt")).readlines()) + class_dim = len(open(join_path(data_dir, 'labels.list')).readlines()) + if is_predict: + return dict_dim, class_dim + + if data_dir is not None: + train_list = join_path(data_dir, train_list) + test_list = join_path(data_dir, test_list) + dict_file = join_path(data_dir, dict_file) + + train_list = train_list if not is_test else None + word_dict = dict() + with open(dict_file, 'r') as f: + for i, line in enumerate(open(dict_file, 'r')): + word_dict[line.split('\t')[0]] = i + + define_py_data_sources2( + train_list, + test_list, + module="dataprovider", + obj="process", + args={'dictionary': word_dict}) + + return dict_dim, class_dim +``` + +在模型配置中利用define_py_data_sources2加载数据: + +* train.list,test.list: 指定训练、测试数据 +* module="dataprovider": 数据处理Python文件名 +* obj="process": 指定生成数据的函数 +* args={"dictionary": word_dict}: 额外的参数,这里指定词典 + +### 模型结构 + + * `convolution_net`: 在`sentiment_net.py`中定义。其论述详见`文本卷积神经网络`小结。 + ```python + def convolution_net(input_dim, + class_dim=2, + emb_dim=128, + hid_dim=128, + is_predict=False): + data = data_layer("word", input_dim) # one-hot表示的词序列 + emb = embedding_layer(input=data, size=emb_dim) # 将one-hot表示的词序列映射为embedding序列 + conv_3 = sequence_conv_pool(input=emb, context_len=3, hidden_size=hid_dim) #窗口大小为3的convolution及max pooling操作 + conv_4 = sequence_conv_pool(input=emb, context_len=4, hidden_size=hid_dim) #窗口大小为4的convolution及max pooling操作 + output = fc_layer(input=[conv_3,conv_4], size=class_dim, act=SoftmaxActivation()) #将conv_3和conv_4拼接起来输入给softmax分类 + + if not is_predict: + lbl = data_layer("label", 1) #类别标签 + outputs(classification_cost(input=output, label=lbl)) + else: + outputs(output) + ``` + + 其中,我们仅用一个`sequence_conv_pool`方法就实现了convolution和pooling操作,filter的数量为hidden_size参数。`sequence_conv_pool`的实现详见`Paddle/python/paddle/trainer_config_helpers/networks.py`。 + + * `bidirectional_lstm_net`: 在`sentiment_net.py`中定义。其论述详见`使用循环神经网络的组合进行文本分类`小结。 + ```python + def bidirectional_lstm_net(input_dim, + class_dim=2, + emb_dim=128, + lstm_dim=128, + is_predict=False): + data = data_layer("word", input_dim) + emb = embedding_layer(input=data, size=emb_dim) + bi_lstm = bidirectional_lstm(input=emb, size=lstm_dim) # 双向lstm,其默认返回值为正向lstm-rnn和反向lstm-rnn最后一个时刻的隐层值的拼接。其实现详见`Paddle/python/paddle/trainer_config_helpers/networks.py` + dropout = dropout_layer(input=bi_lstm, dropout_rate=0.5) + output = fc_layer(input=dropout, size=class_dim, act=SoftmaxActivation()) + + if not is_predict: + lbl = data_layer("label", 1) + outputs(classification_cost(input=output, label=lbl)) + else: + outputs(output) + ``` + + * `stacked_lstm_net`: 在`sentiment_net.py`中定义,默认情况下使用此网络。其论述详见`使用循环神经网络的组合进行文本分类`小结。 + ```python + def stacked_lstm_net(input_dim, + class_dim=2, + emb_dim=128, + hid_dim=512, + stacked_num=3, + is_predict=False): + """ + A Wrapper for sentiment classification task. + This network uses bi-directional recurrent network, + consisting three LSTM layers. This configure is referred to + the paper as following url, but use fewer layrs. + http://www.aclweb.org/anthology/P15-1109 + + input_dim: here is word dictionary dimension. + class_dim: number of categories. + emb_dim: dimension of word embedding. + hid_dim: dimension of hidden layer. + stacked_num: number of stacked lstm-hidden layer. + is_predict: is predicting or not. + Some layers is not needed in network when predicting. + """ + hid_lr = 1e-3 + assert stacked_num % 2 == 1 + + layer_attr = ExtraLayerAttribute(drop_rate=0.5) + fc_para_attr = ParameterAttribute(learning_rate=hid_lr) + lstm_para_attr = ParameterAttribute(initial_std=0., learning_rate=1.) + para_attr = [fc_para_attr, lstm_para_attr] + bias_attr = ParameterAttribute(initial_std=0., l2_rate=0.) + relu = ReluActivation() + linear = LinearActivation() + + data = data_layer("word", input_dim) + emb = embedding_layer(input=data, size=emb_dim) + + fc1 = fc_layer(input=emb, size=hid_dim, act=linear, bias_attr=bias_attr) + lstm1 = lstmemory( + input=fc1, act=relu, bias_attr=bias_attr, layer_attr=layer_attr) #基于lstm的循环神经网络 + + inputs = [fc1, lstm1] + for i in range(2, stacked_num + 1): #由fc_layer和lstmemory构建双向stacked_lstm_net + fc = fc_layer( + input=inputs, + size=hid_dim, + act=linear, + param_attr=para_attr, + bias_attr=bias_attr) + lstm = lstmemory( + input=fc, + reverse=(i % 2) == 0, + act=relu, + bias_attr=bias_attr, + layer_attr=layer_attr) + inputs = [fc, lstm] + + fc_last = pooling_layer(input=inputs[0], pooling_type=MaxPooling()) #对最后一层fc_layer使用max pooling over time得到定长向量 + lstm_last = pooling_layer(input=inputs[1], pooling_type=MaxPooling()) #对最后一层lstmemory使用max pooling over time得到定长向量 + output = fc_layer( + input=[fc_last, lstm_last], + size=class_dim, + act=SoftmaxActivation(), + bias_attr=bias_attr, + param_attr=para_attr) + + if is_predict: + outputs(output) + else: + outputs(classification_cost(input=output, label=data_layer('label', 1))) + ``` + + +## 训练模型 +首先安装PaddlePaddle。 然后使用下面的脚本 `train.sh` 来开启本地的训练。 + +``` +./train.sh +``` + +train.sh: + +``` +config=trainer_config.py +output=./model_output +paddle train --config=$config \ + --save_dir=$output \ + --job=train \ + --use_gpu=false \ + --trainer_count=4 \ + --num_passes=10 \ + --log_period=20 \ + --dot_period=20 \ + --show_parameter_stats_period=100 \ + --test_all_data_in_one_period=1 \ + 2>&1 | tee 'train.log' +``` + +* \--config=$config: 设置网络配置。 +* \--save\_dir=$output: 设置输出路径以保存训练完成的模型。 +* \--job=train: 设置工作模式为训练。 +* \--use\_gpu=false: 使用CPU训练,如果你安装GPU版本的PaddlePaddle,并想使用GPU来训练可将此设置为true。 +* \--trainer\_count=4:设置线程数(或GPU个数)。 +* \--num\_passes=15: 设置pass,PaddlePaddle中的一个pass意味着对数据集中的所有样本进行一次训练。 +* \--log\_period=20: 每20个batch打印一次日志。 +* \--show\_parameter\_stats\_period=100: 每100个batch打印一次统计信息。 +* \--test\_all_data\_in\_one\_period=1: 每次测试都测试所有数据。 + +如果运行成功,输出日志保存在 `train.log`中,模型保存在目录`model_output/`中。 输出日志说明如下: + +``` +Batch=20 samples=2560 AvgCost=0.681644 CurrentCost=0.681644 Eval: classification_error_evaluator=0.36875 CurrentEval: classification_error_evaluator=0.36875 +... +Pass=0 Batch=196 samples=25000 AvgCost=0.418964 Eval: classification_error_evaluator=0.1922 +Test samples=24999 cost=0.39297 Eval: classification_error_evaluator=0.149406 +``` + +* Batch=xx: 表示训练了xx个Batch。 +* samples=xx: 表示训练了xx个样本。 +* AvgCost=xx: 从第0个batch到当前batch的平均损失。 +* CurrentCost=xx: 最新log_period个batch的损失。 +* Eval: classification\_error\_evaluator=xx: 表示第0个batch到当前batch的分类错误。 +* CurrentEval: classification\_error\_evaluator: 最新log_period个batch的分类错误。 +* Pass=0: 通过所有训练集一次称为一个Pass。 0表示第一次经过训练集。 + +默认情况下,我们使用`stacked_lstm_net`网络,如果要使用双向LSTM或卷积网络,注释相应的行即可。 +## 应用模型 +### 测试模型 + +测试模型是指使用训练出的模型评估已标记的数据集。 + +``` +./test.sh +``` + +test.sh: + +```bash +function get_best_pass() { + cat $1 | grep -Pzo 'Test .*\n.*pass-.*' | \ + sed -r 'N;s/Test.* error=([0-9]+\.[0-9]+).*\n.*pass-([0-9]+)/\1 \2/g' | \ + sort | head -n 1 +} + +log=train.log +LOG=`get_best_pass $log` +LOG=(${LOG}) +evaluate_pass="model_output/pass-${LOG[1]}" + +echo 'evaluating from pass '$evaluate_pass + +model_list=./model.list +touch $model_list | echo $evaluate_pass > $model_list +net_conf=trainer_config.py +paddle train --config=$net_conf \ + --model_list=$model_list \ + --job=test \ + --use_gpu=false \ + --trainer_count=4 \ + --config_args=is_test=1 \ + 2>&1 | tee 'test.log' +``` + +函数`get_best_pass`依据分类错误率获取最佳模型。 与训练不同,测试时需要指定`--job = test`和模型路径,即`--model_list = $model_list`。如果运行成功,日志将保存在“test.log”中。例如,在我们的测试中,最好的模型是`model_output / pass-00002`,分类误差是0.115645,如下: + +``` +Pass=0 samples=24999 AvgCost=0.280471 Eval: classification_error_evaluator=0.115645 +``` + +### 预测 +`predict.py`脚本提供了一个预测接口。在使用它之前请安装PaddlePaddle的python api。 预测IMDB的未标记评论的一个实例如下: + +``` +./predict.sh +``` +predict.sh: + +```bash +#Note the default model is pass-00002, you shold make sure the model path +#exists or change the mode path. +model=model_output/pass-00002/ +config=trainer_config.py +label=data/pre-imdb/labels.list +cat ./data/aclImdb/test/pos/10007_10.txt | python predict.py \ + --tconf=$config\ + --model=$model \ + --label=$label \ + --dict=./data/pre-imdb/dict.txt \ + --batch_size=1 +``` + +* `cat ./data/aclImdb/test/pos/10007_10.txt` : 输入预测样本。 +* `predict.py` : 预测接口脚本。 +* `--tconf=$config` : 设置网络配置。 +* `--model=$model` : 设置模型路径。 +* `--label=$label` : 设置标签类别字典,这个字典是整数标签和字符串标签的一个对应。 +* `--dict=data/pre-imdb/dict.txt` : 设置文本数据字典文件。 +* `--batch_size=1` : 预测时将batch size设置为1。 + +注意应该确保默认模型路径`model_output / pass-00002`存在或更改为其它模型路径。 + +本示例的预测结果: + +``` +Loading parameters from model_output/pass-00002/ +./data/aclImdb/test/pos/10014_7.txt: predicting label is pos +``` + +## 总结 ## 参考文献 \ No newline at end of file diff --git a/understand_sentiment/data/get_imdb.sh b/understand_sentiment/data/get_imdb.sh new file mode 100755 index 00000000..7600af6f --- /dev/null +++ b/understand_sentiment/data/get_imdb.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e +set -x + +DIR="$( cd "$(dirname "$0")" ; pwd -P )" +cd $DIR + +#download the dataset +echo "Downloading aclImdb..." +#http://ai.stanford.edu/%7Eamaas/data/sentiment/ +wget http://ai.stanford.edu/%7Eamaas/data/sentiment/aclImdb_v1.tar.gz + +echo "Downloading mosesdecoder..." +#https://github.com/moses-smt/mosesdecoder +wget https://github.com/moses-smt/mosesdecoder/archive/master.zip + +#extract package +echo "Unzipping..." +tar -zxvf aclImdb_v1.tar.gz +unzip master.zip + +#move train and test set to imdb_data directory +#in order to process when traing +mkdir -p imdb/train +mkdir -p imdb/test + +cp -r aclImdb/train/pos/ imdb/train/pos +cp -r aclImdb/train/neg/ imdb/train/neg + +cp -r aclImdb/test/pos/ imdb/test/pos +cp -r aclImdb/test/neg/ imdb/test/neg + +#remove compressed package +rm aclImdb_v1.tar.gz +rm master.zip + +echo "Done." diff --git a/understand_sentiment/dataprovider.py b/understand_sentiment/dataprovider.py new file mode 100755 index 00000000..00f72cec --- /dev/null +++ b/understand_sentiment/dataprovider.py @@ -0,0 +1,35 @@ +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from paddle.trainer.PyDataProvider2 import * + + +def hook(settings, dictionary, **kwargs): + settings.word_dict = dictionary + settings.input_types = [ + integer_value_sequence(len(settings.word_dict)), integer_value(2) + ] + settings.logger.info('dict len : %d' % (len(settings.word_dict))) + + +@provider(init_hook=hook) +def process(settings, file_name): + with open(file_name, 'r') as fdata: + for line_count, line in enumerate(fdata): + label, comment = line.strip().split('\t\t') + label = int(label) + words = comment.split() + word_slot = [ + settings.word_dict[w] for w in words if w in settings.word_dict + ] + yield word_slot, label diff --git a/understand_sentiment/predict.py b/understand_sentiment/predict.py new file mode 100755 index 00000000..8ec490f6 --- /dev/null +++ b/understand_sentiment/predict.py @@ -0,0 +1,150 @@ +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os, sys +import numpy as np +from optparse import OptionParser +from py_paddle import swig_paddle, DataProviderConverter +from paddle.trainer.PyDataProvider2 import integer_value_sequence +from paddle.trainer.config_parser import parse_config +""" +Usage: run following command to show help message. + python predict.py -h +""" + + +class SentimentPrediction(): + def __init__(self, train_conf, dict_file, model_dir=None, label_file=None): + """ + train_conf: trainer configure. + dict_file: word dictionary file name. + model_dir: directory of model. + """ + self.train_conf = train_conf + self.dict_file = dict_file + self.word_dict = {} + self.dict_dim = self.load_dict() + self.model_dir = model_dir + if model_dir is None: + self.model_dir = os.path.dirname(train_conf) + + self.label = None + if label_file is not None: + self.load_label(label_file) + + conf = parse_config(train_conf, "is_predict=1") + self.network = swig_paddle.GradientMachine.createFromConfigProto( + conf.model_config) + self.network.loadParameters(self.model_dir) + input_types = [integer_value_sequence(self.dict_dim)] + self.converter = DataProviderConverter(input_types) + + def load_dict(self): + """ + Load dictionary from self.dict_file. + """ + for line_count, line in enumerate(open(self.dict_file, 'r')): + self.word_dict[line.strip().split('\t')[0]] = line_count + return len(self.word_dict) + + def load_label(self, label_file): + """ + Load label. + """ + self.label = {} + for v in open(label_file, 'r'): + self.label[int(v.split('\t')[1])] = v.split('\t')[0] + + def get_index(self, data): + """ + transform word into integer index according to the dictionary. + """ + words = data.strip().split() + word_slot = [self.word_dict[w] for w in words if w in self.word_dict] + return word_slot + + def batch_predict(self, data_batch): + input = self.converter(data_batch) + output = self.network.forwardTest(input) + prob = output[0]["value"] + labs = np.argsort(-prob) + for idx, lab in enumerate(labs): + if self.label is None: + print("predicting label is %d" % (lab[0])) + else: + print("predicting label is %s" % (self.label[lab[0]])) + + +def option_parser(): + usage = "python predict.py -n config -w model_dir -d dictionary -i input_file " + parser = OptionParser(usage="usage: %s [options]" % usage) + parser.add_option( + "-n", + "--tconf", + action="store", + dest="train_conf", + help="network config") + parser.add_option( + "-d", + "--dict", + action="store", + dest="dict_file", + help="dictionary file") + parser.add_option( + "-b", + "--label", + action="store", + dest="label", + default=None, + help="dictionary file") + parser.add_option( + "-c", + "--batch_size", + type="int", + action="store", + dest="batch_size", + default=1, + help="the batch size for prediction") + parser.add_option( + "-w", + "--model", + action="store", + dest="model_path", + default=None, + help="model path") + return parser.parse_args() + + +def main(): + options, args = option_parser() + train_conf = options.train_conf + batch_size = options.batch_size + dict_file = options.dict_file + model_path = options.model_path + label = options.label + swig_paddle.initPaddle("--use_gpu=0") + predict = SentimentPrediction(train_conf, dict_file, model_path, label) + + batch = [] + for line in sys.stdin: + batch.append([predict.get_index(line)]) + if len(batch) == batch_size: + predict.batch_predict(batch) + batch = [] + if len(batch) > 0: + predict.batch_predict(batch) + + +if __name__ == '__main__': + main() diff --git a/understand_sentiment/predict.sh b/understand_sentiment/predict.sh new file mode 100755 index 00000000..c72a8e86 --- /dev/null +++ b/understand_sentiment/predict.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -e + +#Note the default model is pass-00002, you shold make sure the model path +#exists or change the mode path. +model=model_output/pass-00002/ +config=trainer_config.py +label=data/pre-imdb/labels.list +cat ./data/aclImdb/test/pos/10007_10.txt | python predict.py \ + --tconf=$config\ + --model=$model \ + --label=$label \ + --dict=./data/pre-imdb/dict.txt \ + --batch_size=1 diff --git a/understand_sentiment/preprocess.py b/understand_sentiment/preprocess.py new file mode 100755 index 00000000..29b3682b --- /dev/null +++ b/understand_sentiment/preprocess.py @@ -0,0 +1,359 @@ +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import random +import operator +import numpy as np +from subprocess import Popen, PIPE +from os.path import join as join_path +from optparse import OptionParser + +from paddle.utils.preprocess_util import * +""" +Usage: run following command to show help message. + python preprocess.py -h +""" + + +def save_dict(dict, filename, is_reverse=True): + """ + Save dictionary into file. + dict: input dictionary. + filename: output file name, string. + is_reverse: True, descending order by value. + False, ascending order by value. + """ + f = open(filename, 'w') + for k, v in sorted(dict.items(), key=operator.itemgetter(1),\ + reverse=is_reverse): + f.write('%s\t%s\n' % (k, v)) + f.close() + + +def tokenize(sentences): + """ + Use tokenizer.perl to tokenize input sentences. + tokenizer.perl is tool of Moses. + sentences : a list of input sentences. + return: a list of processed text. + """ + dir = './data/mosesdecoder-master/scripts/tokenizer/tokenizer.perl' + tokenizer_cmd = [dir, '-l', 'en', '-q', '-'] + assert isinstance(sentences, list) + text = "\n".join(sentences) + tokenizer = Popen(tokenizer_cmd, stdin=PIPE, stdout=PIPE) + tok_text, _ = tokenizer.communicate(text) + toks = tok_text.split('\n')[:-1] + return toks + + +def read_lines(path): + """ + path: String, file path. + return a list of sequence. + """ + seqs = [] + with open(path, 'r') as f: + for line in f.readlines(): + line = line.strip() + if len(line): + seqs.append(line) + return seqs + + +class SentimentDataSetCreate(): + """ + A class to process data for sentiment analysis task. + """ + + def __init__(self, + data_path, + output_path, + use_okenizer=True, + multi_lines=False): + """ + data_path: string, traing and testing dataset path + output_path: string, output path, store processed dataset + multi_lines: whether a file has multi lines. + In order to shuffle fully, it needs to read all files into + memory, then shuffle them if one file has multi lines. + """ + self.output_path = output_path + self.data_path = data_path + + self.train_dir = 'train' + self.test_dir = 'test' + + self.train_list = "train.list" + self.test_list = "test.list" + + self.label_list = "labels.list" + self.classes_num = 0 + + self.batch_size = 50000 + self.batch_dir = 'batches' + + self.dict_file = "dict.txt" + self.dict_with_test = False + self.dict_size = 0 + self.word_count = {} + + self.tokenizer = use_okenizer + self.overwrite = False + + self.multi_lines = multi_lines + + self.train_dir = join_path(data_path, self.train_dir) + self.test_dir = join_path(data_path, self.test_dir) + self.train_list = join_path(output_path, self.train_list) + self.test_list = join_path(output_path, self.test_list) + self.label_list = join_path(output_path, self.label_list) + self.dict_file = join_path(output_path, self.dict_file) + + def data_list(self, path): + """ + create dataset from path + path: data path + return: data list + """ + label_set = get_label_set_from_dir(path) + data = [] + for lab_name in label_set.keys(): + file_paths = list_files(join_path(path, lab_name)) + for p in file_paths: + data.append({"label" : label_set[lab_name],\ + "seq_path": p}) + return data, label_set + + def create_dict(self, data): + """ + create dict for input data. + data: list, [sequence, sequnce, ...] + """ + for seq in data: + for w in seq.strip().lower().split(): + if w not in self.word_count: + self.word_count[w] = 1 + else: + self.word_count[w] += 1 + + def create_dataset(self): + """ + create file batches and dictionary of train data set. + If the self.overwrite is false and train.list already exists in + self.output_path, this function will not create and save file + batches from the data set path. + return: dictionary size, class number. + """ + out_path = self.output_path + if out_path and not os.path.exists(out_path): + os.makedirs(out_path) + + # If self.overwrite is false or self.train_list has existed, + # it will not process dataset. + if not (self.overwrite or not os.path.exists(self.train_list)): + print "%s already exists." % self.train_list + return + + # Preprocess train data. + train_data, train_lab_set = self.data_list(self.train_dir) + print "processing train set..." + file_lists = self.save_data(train_data, "train", self.batch_size, True, + True) + save_list(file_lists, self.train_list) + + # If have test data path, preprocess test data. + if os.path.exists(self.test_dir): + test_data, test_lab_set = self.data_list(self.test_dir) + assert (train_lab_set == test_lab_set) + print "processing test set..." + file_lists = self.save_data(test_data, "test", self.batch_size, + False, self.dict_with_test) + save_list(file_lists, self.test_list) + + # save labels set. + save_dict(train_lab_set, self.label_list, False) + self.classes_num = len(train_lab_set.keys()) + + # save dictionary. + save_dict(self.word_count, self.dict_file, True) + self.dict_size = len(self.word_count) + + def save_data(self, + data, + prefix="", + batch_size=50000, + is_shuffle=False, + build_dict=False): + """ + Create batches for a Dataset object. + data: the Dataset object to process. + prefix: the prefix of each batch. + batch_size: number of data in each batch. + build_dict: whether to build dictionary for data + + return: list of batch names + """ + if is_shuffle and self.multi_lines: + return self.save_data_multi_lines(data, prefix, batch_size, + build_dict) + + if is_shuffle: + random.shuffle(data) + num_batches = int(math.ceil(len(data) / float(batch_size))) + batch_names = [] + for i in range(num_batches): + batch_name = join_path(self.output_path, + "%s_part_%03d" % (prefix, i)) + begin = i * batch_size + end = min((i + 1) * batch_size, len(data)) + # read a batch of data + label_list, data_list = self.get_data_list(begin, end, data) + if build_dict: + self.create_dict(data_list) + self.save_file(label_list, data_list, batch_name) + batch_names.append(batch_name) + + return batch_names + + def get_data_list(self, begin, end, data): + """ + begin: int, begining index of data. + end: int, ending index of data. + data: a list of {"seq_path": seqquence path, "label": label index} + + return a list of label and a list of sequence. + """ + label_list = [] + data_list = [] + for j in range(begin, end): + seqs = read_lines(data[j]["seq_path"]) + lab = int(data[j]["label"]) + #File may have multiple lines. + for seq in seqs: + data_list.append(seq) + label_list.append(lab) + if self.tokenizer: + data_list = tokenize(data_list) + return label_list, data_list + + def save_data_multi_lines(self, + data, + prefix="", + batch_size=50000, + build_dict=False): + """ + In order to shuffle fully, there is no need to load all data if + each file only contains one sample, it only needs to shuffle list + of file name. But one file contains multi lines, each line is one + sample. It needs to read all data into memory to shuffle fully. + This interface is mainly for data containning multi lines in each + file, which consumes more memory if there is a great mount of data. + + data: the Dataset object to process. + prefix: the prefix of each batch. + batch_size: number of data in each batch. + build_dict: whether to build dictionary for data + + return: list of batch names + """ + assert self.multi_lines + label_list = [] + data_list = [] + + # read all data + label_list, data_list = self.get_data_list(0, len(data), data) + if build_dict: + self.create_dict(data_list) + + length = len(label_list) + perm_list = np.array([i for i in xrange(length)]) + random.shuffle(perm_list) + + num_batches = int(math.ceil(length / float(batch_size))) + batch_names = [] + for i in range(num_batches): + batch_name = join_path(self.output_path, + "%s_part_%03d" % (prefix, i)) + begin = i * batch_size + end = min((i + 1) * batch_size, length) + sub_label = [label_list[perm_list[i]] for i in range(begin, end)] + sub_data = [data_list[perm_list[i]] for i in range(begin, end)] + self.save_file(sub_label, sub_data, batch_name) + batch_names.append(batch_name) + + return batch_names + + def save_file(self, label_list, data_list, filename): + """ + Save data into file. + label_list: a list of int value. + data_list: a list of sequnece. + filename: output file name. + """ + f = open(filename, 'w') + print "saving file: %s" % filename + for lab, seq in zip(label_list, data_list): + f.write('%s\t\t%s\n' % (lab, seq)) + f.close() + + +def option_parser(): + parser = OptionParser(usage="usage: python preprcoess.py "\ + "-i data_dir [options]") + parser.add_option( + "-i", + "--data", + action="store", + dest="input", + help="Input data directory.") + parser.add_option( + "-o", + "--output", + action="store", + dest="output", + default=None, + help="Output directory.") + parser.add_option( + "-t", + "--tokenizer", + action="store", + dest="use_tokenizer", + default=True, + help="Whether to use tokenizer.") + parser.add_option("-m", "--multi_lines", action="store", + dest="multi_lines", default=False, + help="If input text files have multi lines and they "\ + "need to be shuffled, you should set -m True,") + return parser.parse_args() + + +def main(): + options, args = option_parser() + data_dir = options.input + output_dir = options.output + use_tokenizer = options.use_tokenizer + multi_lines = options.multi_lines + if output_dir is None: + outname = os.path.basename(options.input) + output_dir = join_path(os.path.dirname(data_dir), 'pre-' + outname) + data_creator = SentimentDataSetCreate(data_dir, output_dir, use_tokenizer, + multi_lines) + data_creator.create_dataset() + + +if __name__ == '__main__': + main() diff --git a/understand_sentiment/preprocess.sh b/understand_sentiment/preprocess.sh new file mode 100755 index 00000000..19ec34d4 --- /dev/null +++ b/understand_sentiment/preprocess.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -e + +echo "Start to preprcess..." + +data_dir="./data/imdb" +python preprocess.py -i $data_dir + +echo "Done." diff --git a/understand_sentiment/sentiment_net.py b/understand_sentiment/sentiment_net.py new file mode 100644 index 00000000..1a92d655 --- /dev/null +++ b/understand_sentiment/sentiment_net.py @@ -0,0 +1,162 @@ +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from os.path import join as join_path + +from paddle.trainer_config_helpers import * + + +def sentiment_data(data_dir=None, + is_test=False, + is_predict=False, + train_list="train.list", + test_list="test.list", + dict_file="dict.txt"): + """ + Predefined data provider for sentiment analysis. + is_test: whether this config is used for test. + is_predict: whether this config is used for prediction. + train_list: text file name, containing a list of training set. + test_list: text file name, containing a list of testing set. + dict_file: text file name, containing dictionary. + """ + dict_dim = len(open(join_path(data_dir, "dict.txt")).readlines()) + class_dim = len(open(join_path(data_dir, 'labels.list')).readlines()) + if is_predict: + return dict_dim, class_dim + + if data_dir is not None: + train_list = join_path(data_dir, train_list) + test_list = join_path(data_dir, test_list) + dict_file = join_path(data_dir, dict_file) + + train_list = train_list if not is_test else None + word_dict = dict() + with open(dict_file, 'r') as f: + for i, line in enumerate(open(dict_file, 'r')): + word_dict[line.split('\t')[0]] = i + + define_py_data_sources2( + train_list, + test_list, + module="dataprovider", + obj="process", + args={'dictionary': word_dict}) + + return dict_dim, class_dim + +def convolution_net(input_dim, + class_dim=2, + emb_dim=128, + hid_dim=128, + is_predict=False): + data = data_layer("word", input_dim) + emb = embedding_layer(input=data, size=emb_dim) + conv_3 = sequence_conv_pool(input=emb, context_len=3, hidden_size=hid_dim) + conv_4 = sequence_conv_pool(input=emb, context_len=4, hidden_size=hid_dim) + output = fc_layer(input=[conv_3,conv_4], size=class_dim, act=SoftmaxActivation()) + + if not is_predict: + lbl = data_layer("label", 1) + outputs(classification_cost(input=output, label=lbl)) + else: + outputs(output) + + +def bidirectional_lstm_net(input_dim, + class_dim=2, + emb_dim=128, + lstm_dim=128, + is_predict=False): + data = data_layer("word", input_dim) + emb = embedding_layer(input=data, size=emb_dim) + bi_lstm = bidirectional_lstm(input=emb, size=lstm_dim) + dropout = dropout_layer(input=bi_lstm, dropout_rate=0.5) + output = fc_layer(input=dropout, size=class_dim, act=SoftmaxActivation()) + + if not is_predict: + lbl = data_layer("label", 1) + outputs(classification_cost(input=output, label=lbl)) + else: + outputs(output) + + +def stacked_lstm_net(input_dim, + class_dim=2, + emb_dim=128, + hid_dim=512, + stacked_num=3, + is_predict=False): + """ + A Wrapper for sentiment classification task. + This network uses bi-directional recurrent network, + consisting three LSTM layers. This configure is referred to + the paper as following url, but use fewer layrs. + http://www.aclweb.org/anthology/P15-1109 + + input_dim: here is word dictionary dimension. + class_dim: number of categories. + emb_dim: dimension of word embedding. + hid_dim: dimension of hidden layer. + stacked_num: number of stacked lstm-hidden layer. + is_predict: is predicting or not. + Some layers is not needed in network when predicting. + """ + hid_lr = 1e-3 + assert stacked_num % 2 == 1 + + layer_attr = ExtraLayerAttribute(drop_rate=0.5) + fc_para_attr = ParameterAttribute(learning_rate=hid_lr) + lstm_para_attr = ParameterAttribute(initial_std=0., learning_rate=1.) + para_attr = [fc_para_attr, lstm_para_attr] + bias_attr = ParameterAttribute(initial_std=0., l2_rate=0.) + relu = ReluActivation() + linear = LinearActivation() + + data = data_layer("word", input_dim) + emb = embedding_layer(input=data, size=emb_dim) + + fc1 = fc_layer(input=emb, size=hid_dim, act=linear, bias_attr=bias_attr) + lstm1 = lstmemory( + input=fc1, act=relu, bias_attr=bias_attr, layer_attr=layer_attr) + + inputs = [fc1, lstm1] + for i in range(2, stacked_num + 1): + fc = fc_layer( + input=inputs, + size=hid_dim, + act=linear, + param_attr=para_attr, + bias_attr=bias_attr) + lstm = lstmemory( + input=fc, + reverse=(i % 2) == 0, + act=relu, + bias_attr=bias_attr, + layer_attr=layer_attr) + inputs = [fc, lstm] + + fc_last = pooling_layer(input=inputs[0], pooling_type=MaxPooling()) + lstm_last = pooling_layer(input=inputs[1], pooling_type=MaxPooling()) + output = fc_layer( + input=[fc_last, lstm_last], + size=class_dim, + act=SoftmaxActivation(), + bias_attr=bias_attr, + param_attr=para_attr) + + if is_predict: + outputs(output) + else: + outputs(classification_cost(input=output, label=data_layer('label', 1))) diff --git a/understand_sentiment/test.sh b/understand_sentiment/test.sh new file mode 100755 index 00000000..8af827c3 --- /dev/null +++ b/understand_sentiment/test.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -e + +function get_best_pass() { + cat $1 | grep -Pzo 'Test .*\n.*pass-.*' | \ + sed -r 'N;s/Test.* classification_error_evaluator=([0-9]+\.[0-9]+).*\n.*pass-([0-9]+)/\1 \2/g' |\ + sort -n | head -n 1 +} + +log=train.log +LOG=`get_best_pass $log` +LOG=(${LOG}) +evaluate_pass="model_output/pass-${LOG[1]}" + +echo 'evaluating from pass '$evaluate_pass + +model_list=./model.list +touch $model_list | echo $evaluate_pass > $model_list +net_conf=trainer_config.py +paddle train --config=$net_conf \ + --model_list=$model_list \ + --job=test \ + --use_gpu=false \ + --trainer_count=4 \ + --config_args=is_test=1 \ + 2>&1 | tee 'test.log' diff --git a/understand_sentiment/train.sh b/understand_sentiment/train.sh new file mode 100755 index 00000000..5ce8bf4b --- /dev/null +++ b/understand_sentiment/train.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -e + +config=trainer_config.py +output=./model_output +paddle train --config=$config \ + --save_dir=$output \ + --job=train \ + --use_gpu=false \ + --trainer_count=4 \ + --num_passes=10 \ + --log_period=10 \ + --dot_period=20 \ + --show_parameter_stats_period=100 \ + --test_all_data_in_one_period=1 \ + 2>&1 | tee 'train.log' diff --git a/understand_sentiment/trainer_config.py b/understand_sentiment/trainer_config.py new file mode 100644 index 00000000..42deac40 --- /dev/null +++ b/understand_sentiment/trainer_config.py @@ -0,0 +1,40 @@ +# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from sentiment_net import * +from paddle.trainer_config_helpers import * + +# whether this config is used for test +is_test = get_config_arg('is_test', bool, False) +# whether this config is used for prediction +is_predict = get_config_arg('is_predict', bool, False) + +data_dir = "./data/pre-imdb" +dict_dim, class_dim = sentiment_data(data_dir, is_test, is_predict) + +################## Algorithm Config ##################### + +settings( + batch_size=128, + learning_rate=2e-3, + learning_method=AdamOptimizer(), + average_window=0.5, + regularization=L2Regularization(8e-4), + gradient_clipping_threshold=25) + +#################### Network Config ###################### +stacked_lstm_net( + dict_dim, class_dim=class_dim, stacked_num=3, is_predict=is_predict) +# bidirectional_lstm_net(dict_dim, class_dim=class_dim, is_predict=is_predict) +# convolution_net(dict_dim, class_dim=class_dim, is_predict=is_predict) \ No newline at end of file From 7d77257967272f6c4b9318a03ec565bca7c53194 Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Tue, 27 Dec 2016 15:49:37 +0800 Subject: [PATCH 04/11] mini. error --- understand_sentiment/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index 43eba6d6..52025aaa 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -469,7 +469,7 @@ cat ./data/aclImdb/test/pos/10007_10.txt | python predict.py \ * `--model=$model` : 设置模型路径。 * `--label=$label` : 设置标签类别字典,这个字典是整数标签和字符串标签的一个对应。 * `--dict=data/pre-imdb/dict.txt` : 设置文本数据字典文件。 -* `--batch_size=1` : 预测时将batch size设置为1。 +* `--batch_size=1` : 预测时的batch size大小。 注意应该确保默认模型路径`model_output / pass-00002`存在或更改为其它模型路径。 From ce71f9692754eb0bd2ff5b0482579a57e5c4f428 Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Wed, 28 Dec 2016 19:16:01 +0800 Subject: [PATCH 05/11] del one paragraph --- understand_sentiment/README.md | 4 ++-- understand_sentiment/sentiment_net.py | 4 +++- understand_sentiment/trainer_config.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index 52025aaa..40a10963 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -28,7 +28,7 @@ $$c=[c_1,c_2,\ldots,c_{n-h+1}]$$
  其中$c \in \mathbb{R}^{n-h+1}$。接下来我们对feature map采用max pooling over time操作得到此filter对应的特征: $$\hat c=max(c)$$
  即,$\hat c$是feature map中所有元素的最大值。pooling机制自动处理了句子长度不一的问题。在实际应用中,我们会使用多个filter来处理句子,窗口大小相同的filters堆叠起来形成一个矩阵(上文中的单个filter参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的filters来处理句子,最后,将所有filters得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 -
  可以将上文所述的卷积神经网络的filter理解为特定语义n-gram的探测器(detector),其优点是避免了传统n-gram的高维稀疏表示问题,运算和训练速度十分快,准确率也很高(ref)。但是它难以扩展为深层文本卷积网络,基于此,N. Kalchbrenner, et al.(2014)提出了k-max pooling,使用其可以构建出深层文本卷积网络,有兴趣的读者可以参考相关文献。 + ### 循环神经网络 #### 简单的循环神经网络
  循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的(Siegelmann, H. T. and Sontag, E. D., 1995)。 @@ -482,4 +482,4 @@ Loading parameters from model_output/pass-00002/ ## 总结 -## 参考文献 \ No newline at end of file +## 参考文献 diff --git a/understand_sentiment/sentiment_net.py b/understand_sentiment/sentiment_net.py index 1a92d655..7ab50313 100644 --- a/understand_sentiment/sentiment_net.py +++ b/understand_sentiment/sentiment_net.py @@ -56,6 +56,7 @@ def sentiment_data(data_dir=None, return dict_dim, class_dim + def convolution_net(input_dim, class_dim=2, emb_dim=128, @@ -65,7 +66,8 @@ def convolution_net(input_dim, emb = embedding_layer(input=data, size=emb_dim) conv_3 = sequence_conv_pool(input=emb, context_len=3, hidden_size=hid_dim) conv_4 = sequence_conv_pool(input=emb, context_len=4, hidden_size=hid_dim) - output = fc_layer(input=[conv_3,conv_4], size=class_dim, act=SoftmaxActivation()) + output = fc_layer( + input=[conv_3, conv_4], size=class_dim, act=SoftmaxActivation()) if not is_predict: lbl = data_layer("label", 1) diff --git a/understand_sentiment/trainer_config.py b/understand_sentiment/trainer_config.py index 42deac40..b327d09f 100644 --- a/understand_sentiment/trainer_config.py +++ b/understand_sentiment/trainer_config.py @@ -37,4 +37,4 @@ stacked_lstm_net( dict_dim, class_dim=class_dim, stacked_num=3, is_predict=is_predict) # bidirectional_lstm_net(dict_dim, class_dim=class_dim, is_predict=is_predict) -# convolution_net(dict_dim, class_dim=class_dim, is_predict=is_predict) \ No newline at end of file +# convolution_net(dict_dim, class_dim=class_dim, is_predict=is_predict) From 01e08be96435c106323d46d3049858f316370d2c Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Thu, 29 Dec 2016 17:28:27 +0800 Subject: [PATCH 06/11] complete --- understand_sentiment/README.md | 58 ++++++++++++++++++++-------- understand_sentiment/image/lstm.png | Bin 0 -> 131072 bytes 2 files changed, 42 insertions(+), 16 deletions(-) create mode 100644 understand_sentiment/image/lstm.png diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index 40a10963..15c28284 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -12,7 +12,7 @@ |剧情四星。但是圆镜视角加上婺源的风景整个非常有中国写意山水画的感觉,看得实在太舒服了。。难怪作为今年TIFF special presentation的开幕电影。范爷美爆,再往上加一星。|正面|

表格 1 电影评论情感分析

-
  实际上,在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类问题可以分解为两个子问题:文本表示和分类。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),分类方法有SVM,LR,Boosting等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,这种表示浮于表面,并未充分表示文本的语义信息。例如,句子`这部电影糟糕透了`和`一个乏味,空洞,没有内涵的作品`在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子`小明很喜欢小芳,但是小芳不喜欢小明`和`小芳很喜欢小明,但是小明不喜欢小芳`的BOW相似度为1,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 +
  实际上,在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类问题可以分解为两个子问题:文本表示和分类。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),分类方法有SVM,LR,Boosting等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,这种表示浮于表面,并未充分表示文本的语义信息。例如,句子`这部电影糟糕透了`和`一个乏味,空洞,没有内涵的作品`在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子`一个空洞,没有内涵的作品`和`一个不空洞而且有内涵的作品`的BOW相似度很高,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 ## 模型概览
  本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。我们首先介绍处理文本的卷积神经网络。 ### 文本卷积神经网络 @@ -22,44 +22,61 @@ 图 1 卷积神经网络文本分类模型


  假设一个句子的长度为$n$,其中第$i$个词的word embedding为$x_i\in\mathbb{R}^k$,其维度大小为$k$,我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把filter(也称为kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: + $$c_i=f(w\cdot x_{i:i+h-1}+b)$$ +
  其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如sigmoid。将filter应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$序列,产生一个feature map: + $$c=[c_1,c_2,\ldots,c_{n-h+1}]$$ -
  其中$c \in \mathbb{R}^{n-h+1}$。接下来我们对feature map采用max pooling over time操作得到此filter对应的特征: + +
  其中$c \in \mathbb{R}^{n-h+1}$。接下来我们对feature map采用max pooling over time操作得到此filter对应的整句话的特征: + $$\hat c=max(c)$$ -
  即,$\hat c$是feature map中所有元素的最大值。pooling机制自动处理了句子长度不一的问题。在实际应用中,我们会使用多个filter来处理句子,窗口大小相同的filters堆叠起来形成一个矩阵(上文中的单个filter参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的filters来处理句子,最后,将所有filters得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 +
  即,$\hat c$是feature map中所有元素的最大值。pooling机制自动处理了句子长度不一的问题。在实际应用中,我们会使用多个filter来处理句子,窗口大小相同的filters堆叠起来形成一个矩阵(上文中的单个filter参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的filters来处理句子,最后,将所有filters得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 +
  对于一般的短文本分类问题,上文所述的简单的文本卷积网络即可达到很高的正确率\[[1](#参考文献)\]。若想得到更抽象更高级的文本特征表示,可以参考N. Kalchbrenner, et al.(2014)\[[2](#参考文献)\]或 Yann N. Dauphin, et al.(2016)\[[3](#参考文献)\]的构建深层文本卷积神经网络的方法。 ### 循环神经网络 #### 简单的循环神经网络 -
  循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的(Siegelmann, H. T. and Sontag, E. D., 1995)。 -
  自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如lstm等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型,句法解析,语义角色标注(或一般的序列标注),语义表示,图文生成,对话,机器翻译等任务上均表现优异甚至成为目前效果最好的方法。 +
  循环神经网络(rnn)是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的\[[4](#参考文献)\]。 +
  自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory\[[5](#参考文献)\]等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型,句法解析,语义角色标注(或一般的序列标注),语义表示,图文生成,对话,机器翻译等任务上均表现优异甚至成为目前效果最好的方法。


图 2 循环神经网络按时间展开的示意图


  循环神经网络按时间展开后如图2所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐藏层的输出$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: + $$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$ +
  其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为elementwise的sigmoid函数。在处理自然语言时,一般会先将词(one-hot表示)映射为其embedding表示,然后再作为循环神经网络每一时刻的输入$x_t$。可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 -
  可以看出,隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象(Bengio Y, Simard P, Frasconi P., 1994)。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了lstm模型。 -#### 长时短期记忆循环神经网络 -
  相比于简单的循环神经网络,lstm增加了记忆单元$c$,输入门$i$,遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm(无 peep-hole连接的版本)的循环神经网络表示的函数记为F,则其公式为: +
  可以看出,隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象\[[6](#参考文献)\]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)\[[5](#参考文献)\]提出了lstm(long short term memory)模型。 +#### lstm-rnn +
  相比于简单的循环神经网络,lstm增加了记忆单元$c$,输入门$i$,遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm的循环神经网络表示的函数记为F,则其公式为: + $$ h_t=F(x_t,h_{t-1})$$ -
  $F$由下列公式组合而成: + +
  $F$由下列公式组合而成\[[7](#参考文献)\]: \begin{align} -i_t & = \sigma(W_{xi}x_t+W_{hi}h_{h-1}+b_i)\\\\ -f_t & = \sigma(W_{xf}x_t+W_{hf}h_{h-1}+b_f)\\\\ +i_t & = \sigma(W_{xi}x_t+W_{hi}h_{h-1}+W_{ci}c_{t-1}+b_i)\\\\ +f_t & = \sigma(W_{xf}x_t+W_{hf}h_{h-1}+W_{cf}c_{t-1}+b_f)\\\\ c_t & = f_t\odot c_{t-1}+i_t\odot tanh(W_{xc}x_t+W_{hc}h_{h-1}+b_c)\\\\ -o_t & = \sigma(W_{xo}x_t+W_{ho}h_{h-1}+b_o)\\\\ +o_t & = \sigma(W_{xo}x_t+W_{ho}h_{h-1}+W_{co}c_{t}+b_o)\\\\ h_t & = o_t\odot tanh(c_t)\\\\ \end{align} -
  其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元(记忆单元一般对外不可见,$h_t$对外部可见)及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为elementwise的双曲正切函数,$\odot$表示elementwise的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数。这三种门各自以不同的方式控制着记忆单元$c$。实际上,lstm的思想正是通过给简单的循环神经网络增加记忆及控制门的方式增强了其处理远距离依赖问题的能力。类似原理的对于简单循环神经网络的改进还有Gated Recurrent Unit (GRU)( Cho K, Van Merriënboer B, Gulcehre C, et al. 2014),其设计更为简洁一些。**这些改进虽然各有不同,但是对他们的宏观描述却与简单的循环神经网络一样,如图1所示,隐状态依据当前输入及前一时刻的隐状态来改变,不断的循环这一过程直至输入处理完毕:** +
  其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元(记忆单元一般对外不可见,$h_t$对外部可见)及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为elementwise的双曲正切函数,$\odot$表示elementwise的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,这三种门各自以不同的方式控制着记忆单元$c$,如图3所示: +

+
+图 3 时刻$t$的lstm\[[7](#参考文献)\] +

+
  实际上,lstm的思想正是通过给简单的循环神经网络增加记忆及控制门的方式增强了其处理远距离依赖问题的能力。类似原理的对于简单循环神经网络的改进还有Gated Recurrent Unit (GRU)\[[8](#参考文献)\],其设计更为简洁一些。**这些改进虽然各有不同,但是对他们的宏观描述却与简单的循环神经网络一样,如图2所示,隐状态依据当前输入及前一时刻的隐状态来改变,不断的循环这一过程直至输入处理完毕:** + $$ h_t=Recrurent(x_t,h_{t-1})$$ +
  对于正常顺序的循环神经网络而言,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以构建更加强有力的深层双向循环神经网络(deep bi-directional recurrent neural networks)对时序数据进行建模。 #### 使用循环神经网络的组合进行文本分类 -
  一个简单的做法是分别使用正向lstm-rnn和反向lstm-rnn处理文本,取最后一个时刻的隐层值拼接起来做为文本的定长向量表示,将其连接至softmax得到文本分类模型。但是这样的文本分类模型是一个浅层模型。考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建stacked lstm-rnn。如图3所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用max pooling over time得到文本定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 +
  一个简单的做法是分别使用正向lstm-rnn和反向lstm-rnn处理文本,取最后一个时刻的隐层值拼接起来做为文本的定长向量表示,将其连接至softmax得到文本分类模型。但是这样的文本分类模型是一个浅层模型。考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建stacked lstm-rnn\[[9](#参考文献)\]。如图4所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用max pooling over time得到文本定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。


-图 3 stacked lstm-rnn for text classification +图 4 stacked lstm-rnn for text classification

## 数据准备 ### 数据介绍与下载 @@ -481,5 +498,14 @@ Loading parameters from model_output/pass-00002/ ``` ## 总结 - +本章我们以情感分析为例介绍了使用深度学习的方法进行端对端的短文本分类,并且使用PaddlePaddle完成了全部相关实验。我们简要论述了两种文本处理模型:卷积神经网络和循环神经网络。在后续的章节中我们会看到这两种基本的深度学习模型在其它任务上的应用。 ## 参考文献 +1. Kim Y. [Convolutional neural networks for sentence classification](http://arxiv.org/pdf/1408.5882)[J]. arXiv preprint arXiv:1408.5882, 2014. +2. Kalchbrenner N, Grefenstette E, Blunsom P. [A convolutional neural network for modelling sentences](http://arxiv.org/pdf/1404.2188.pdf?utm_medium=App.net&utm_source=PourOver)[J]. arXiv preprint arXiv:1404.2188, 2014. +3. Yann N. Dauphin, et al. [Language Modeling with Gated Convolutional Networks](https://arxiv.org/pdf/1612.08083v1.pdf)[J] arXiv preprint arXiv:1612.08083, 2016. +4. Siegelmann H T, Sontag E D. [On the computational power of neural nets](http://research.cs.queensu.ca/home/akl/cisc879/papers/SELECTED_PAPERS_FROM_VARIOUS_SOURCES/05070215382317071.pdf)[C]//Proceedings of the fifth annual workshop on Computational learning theory. ACM, 1992: 440-449. +5. Hochreiter S, Schmidhuber J. [Long short-term memory](http://web.eecs.utk.edu/~itamar/courses/ECE-692/Bobby_paper1.pdf)[J]. Neural computation, 1997, 9(8): 1735-1780. +6. Bengio Y, Simard P, Frasconi P. [Learning long-term dependencies with gradient descent is difficult](http://www-dsi.ing.unifi.it/~paolo/ps/tnn-94-gradient.pdf)[J]. IEEE transactions on neural networks, 1994, 5(2): 157-166. +7. Graves A. [Generating sequences with recurrent neural networks](http://arxiv.org/pdf/1308.0850)[J]. arXiv preprint arXiv:1308.0850, 2013. +8. Cho K, Van Merriënboer B, Gulcehre C, et al. [Learning phrase representations using RNN encoder-decoder for statistical machine translation](http://arxiv.org/pdf/1406.1078)[J]. arXiv preprint arXiv:1406.1078, 2014. +9. Zhou J, Xu W. [End-to-end learning of semantic role labeling using recurrent neural networks](http://www.aclweb.org/anthology/P/P15/P15-1109.pdf)[C]//Proceedings of the Annual Meeting of the Association for Computational Linguistics. 2015. diff --git a/understand_sentiment/image/lstm.png b/understand_sentiment/image/lstm.png new file mode 100644 index 0000000000000000000000000000000000000000..376afe66c61fe53d4ba383823ac685f4d6941161 GIT binary patch literal 131072 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
Yxx-#{>#$NOHN^IK) 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 literal 0 HcmV?d00001 From 5ec489cc2b2e5e191bdafd5824293a85f38ba598 Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Thu, 5 Jan 2017 19:14:19 +0800 Subject: [PATCH 07/11] revised but unfinished --- understand_sentiment/.gitignore | 2 - understand_sentiment/README.md | 442 ++++++++++--------------- understand_sentiment/dataprovider.py | 9 +- understand_sentiment/predict.sh | 2 +- understand_sentiment/preprocess.sh | 22 -- understand_sentiment/sentiment_net.py | 164 --------- understand_sentiment/train.sh | 6 +- understand_sentiment/trainer_config.py | 119 ++++++- 8 files changed, 305 insertions(+), 461 deletions(-) delete mode 100755 understand_sentiment/preprocess.sh delete mode 100644 understand_sentiment/sentiment_net.py diff --git a/understand_sentiment/.gitignore b/understand_sentiment/.gitignore index 40d91f5b..30b44230 100644 --- a/understand_sentiment/.gitignore +++ b/understand_sentiment/.gitignore @@ -6,7 +6,5 @@ logs/ model_output dataprovider_copy_1.py model.list -test.log -train.log *.pyc .DS_Store diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index 15c28284..837cee7f 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -1,60 +1,64 @@ # 情感分析 ## 背景介绍 -
  在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤),也可以是三类,如(积极,消极,中性)等等。 -
  情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论。为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。 -
  对电影评论进行情感分析(正面,负面)的例子如下面的表格1所示。 +在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤);也可以是三类,如(积极,消极,中性)等等。 + +情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论。为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。表格1展示了对电影评论进行情感分析的例子: | 电影评论 | 类别 | | -------- | ----- | | 在冯小刚这几年的电影里,算最好的一部的了| 正面 | | 很不好看,好像一个地方台的电视剧 | 负面 | -| 为了讽刺官场刻意丑化农村人的傻片子,圆方镜头全程炫技,色调背景美则美矣,但剧情拖沓,口音不伦不类,一直努力却始终无法入戏。不建议进电影院观看,不然睡着了躺都没地方躺。| 负面| -|剧情四星。但是圆镜视角加上婺源的风景整个非常有中国写意山水画的感觉,看得实在太舒服了。。难怪作为今年TIFF special presentation的开幕电影。范爷美爆,再往上加一星。|正面| +| 圆方镜头全程炫技,色调背景美则美矣,但剧情拖沓,口音不伦不类,一直努力却始终无法入戏| 负面| +|剧情四星。但是圆镜视角加上婺源的风景整个非常有中国写意山水画的感觉,看得实在太舒服了。。|正面|

表格 1 电影评论情感分析

-
  实际上,在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类问题可以分解为两个子问题:文本表示和分类。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),分类方法有SVM,LR,Boosting等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,这种表示浮于表面,并未充分表示文本的语义信息。例如,句子`这部电影糟糕透了`和`一个乏味,空洞,没有内涵的作品`在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子`一个空洞,没有内涵的作品`和`一个不空洞而且有内涵的作品`的BOW相似度很高,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 + +在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类涉及文本表示和分类方法。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),话题模型等等;分类方法有SVM(support vector machine), LR(logistic regression), [Boosting](https://en.wikipedia.org/wiki/Boosting_(machine_learning))等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,它并不能充分表示文本的语义信息。例如,句子“这部电影糟糕透了”和“一个乏味,空洞,没有内涵的作品”在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子“一个空洞,没有内涵的作品”和“一个不空洞而且有内涵的作品”的BOW相似度很高,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 ## 模型概览 -
  本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。我们首先介绍处理文本的卷积神经网络。 -### 文本卷积神经网络 -
  卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为2D网格的像素点,自然语言可以视为1D的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示,且其对于数据的某些变化具有不变性。大量实验表明,卷积神经网络能高效的对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)。 +本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其拓展。我们首先介绍处理文本的卷积神经网络。 +### 文本卷积神经网络(CNN) +卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为2D网格的像素点,自然语言可以视为1D的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示,且其对于数据的某些变化具有不变性。大量实验表明,卷积神经网络能高效的对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)\[[1](#参考文献)\]。

-
+
图 1 卷积神经网络文本分类模型

-
  假设一个句子的长度为$n$,其中第$i$个词的word embedding为$x_i\in\mathbb{R}^k$,其维度大小为$k$,我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把filter(也称为kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: +假设一个句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,维度大小为$k$。我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到这$h$个词的特征值$c_i$: $$c_i=f(w\cdot x_{i:i+h-1}+b)$$ -
  其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如sigmoid。将filter应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$序列,产生一个feature map: +其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如$sigmoid$。将卷积核应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$,产生一个特征图(feature map): -$$c=[c_1,c_2,\ldots,c_{n-h+1}]$$ +$$c=[c_1,c_2,\ldots,c_{n-h+1}], c \in \mathbb{R}^{n-h+1}$$ -
  其中$c \in \mathbb{R}^{n-h+1}$。接下来我们对feature map采用max pooling over time操作得到此filter对应的整句话的特征: +接下来我们对特征图采用时间维度上的最大池化(max pooling over time)操作得到此卷积核对应的整句话的特征$\hat c$,它是特征图中所有元素的最大值: $$\hat c=max(c)$$ -
  即,$\hat c$是feature map中所有元素的最大值。pooling机制自动处理了句子长度不一的问题。在实际应用中,我们会使用多个filter来处理句子,窗口大小相同的filters堆叠起来形成一个矩阵(上文中的单个filter参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的filters来处理句子,最后,将所有filters得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 -
  对于一般的短文本分类问题,上文所述的简单的文本卷积网络即可达到很高的正确率\[[1](#参考文献)\]。若想得到更抽象更高级的文本特征表示,可以参考N. Kalchbrenner, et al.(2014)\[[2](#参考文献)\]或 Yann N. Dauphin, et al.(2016)\[[3](#参考文献)\]的构建深层文本卷积神经网络的方法。 -### 循环神经网络 -#### 简单的循环神经网络 -
  循环神经网络(rnn)是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的\[[4](#参考文献)\]。 -
  自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory\[[5](#参考文献)\]等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型,句法解析,语义角色标注(或一般的序列标注),语义表示,图文生成,对话,机器翻译等任务上均表现优异甚至成为目前效果最好的方法。 +在实际应用中,我们会使用多个卷积核来处理句子,窗口大小相同的卷积核堆叠起来形成一个矩阵(上文中的单个卷积核参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的卷积核来处理句子,最后,将所有卷积核得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。图1是使用卷积神经网络进行文本分类的一个示意图(只画了四个卷积核,黄色的卷积核窗口大小为3,红色的为2)。 + +对于一般的短文本分类问题,上文所述的简单的文本卷积网络即可达到很高的正确率\[[1](#参考文献)\]。若想得到更抽象更高级的文本特征表示,可以构建深层文本卷积神经网络\[[2](#参考文献),[3](#参考文献)\]。 +### 循环神经网络(RNN) +循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的\[[4](#参考文献)\]。自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory\[[5](#参考文献)\]等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型、句法解析、语义角色标注(或一般的序列标注)、语义表示、图文生成、对话、机器翻译等任务上均表现优异甚至成为目前效果最好的方法。

-
+
图 2 循环神经网络按时间展开的示意图

-
  循环神经网络按时间展开后如图2所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐藏层的输出$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: +循环神经网络按时间展开后如图2所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐层的状态$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: $$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$ -
  其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为elementwise的sigmoid函数。在处理自然语言时,一般会先将词(one-hot表示)映射为其embedding表示,然后再作为循环神经网络每一时刻的输入$x_t$。可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 -
  可以看出,隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象\[[6](#参考文献)\]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)\[[5](#参考文献)\]提出了lstm(long short term memory)模型。 -#### lstm-rnn -
  相比于简单的循环神经网络,lstm增加了记忆单元$c$,输入门$i$,遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm的循环神经网络表示的函数记为F,则其公式为: +其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为逐元素(elementwise)的$sigmoid$函数。 + +在处理自然语言时,一般会先将词(one-hot表示)映射为其词向量(word embedding)表示,然后再作为循环神经网络每一时刻的输入$x_t$。此外,可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 + +### 长时短期记忆(LSTM) +循环神经网络隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象\[[6](#参考文献)\]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)\[[5](#参考文献)\]提出了lstm(long short term memory)。 + +相比于简单的循环神经网络,lstm增加了记忆单元$c$、输入门$i$、遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm的循环神经网络表示的函数记为$F$,则其公式为: $$ h_t=F(x_t,h_{t-1})$$ -
  $F$由下列公式组合而成\[[7](#参考文献)\]: +$F$由下列公式组合而成\[[7](#参考文献)\]: \begin{align} i_t & = \sigma(W_{xi}x_t+W_{hi}h_{h-1}+W_{ci}c_{t-1}+b_i)\\\\ f_t & = \sigma(W_{xf}x_t+W_{hf}h_{h-1}+W_{cf}c_{t-1}+b_f)\\\\ @@ -62,30 +66,30 @@ c_t & = f_t\odot c_{t-1}+i_t\odot tanh(W_{xc}x_t+W_{hc}h_{h-1}+b_c)\\\\ o_t & = \sigma(W_{xo}x_t+W_{ho}h_{h-1}+W_{co}c_{t}+b_o)\\\\ h_t & = o_t\odot tanh(c_t)\\\\ \end{align} -
  其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元(记忆单元一般对外不可见,$h_t$对外部可见)及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为elementwise的双曲正切函数,$\odot$表示elementwise的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,这三种门各自以不同的方式控制着记忆单元$c$,如图3所示: +其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为逐元素的双曲正切函数,$\odot$表示逐元素的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,即各自以不同的方式控制着记忆单元$c$,如图3所示:

-
-图 3 时刻$t$的lstm\[[7](#参考文献)\] +
+图 3 时刻$t$的lstm

-
  实际上,lstm的思想正是通过给简单的循环神经网络增加记忆及控制门的方式增强了其处理远距离依赖问题的能力。类似原理的对于简单循环神经网络的改进还有Gated Recurrent Unit (GRU)\[[8](#参考文献)\],其设计更为简洁一些。**这些改进虽然各有不同,但是对他们的宏观描述却与简单的循环神经网络一样,如图2所示,隐状态依据当前输入及前一时刻的隐状态来改变,不断的循环这一过程直至输入处理完毕:** +lstm通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的对于简单循环神经网络的改进还有Gated Recurrent Unit (GRU)\[[8](#参考文献)\],其设计更为简洁一些。**这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断的循环这一过程直至输入处理完毕:** $$ h_t=Recrurent(x_t,h_{t-1})$$ -
  对于正常顺序的循环神经网络而言,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以构建更加强有力的深层双向循环神经网络(deep bi-directional recurrent neural networks)对时序数据进行建模。 -#### 使用循环神经网络的组合进行文本分类 -
  一个简单的做法是分别使用正向lstm-rnn和反向lstm-rnn处理文本,取最后一个时刻的隐层值拼接起来做为文本的定长向量表示,将其连接至softmax得到文本分类模型。但是这样的文本分类模型是一个浅层模型。考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建stacked lstm-rnn\[[9](#参考文献)\]。如图4所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用max pooling over time得到文本定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 +对于正常顺序的循环神经网络,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以通过构建更加强有力的栈式双向循环神经网络,来对时序数据进行建模。 +#### 栈式双向LSTM(Stacked Bidirectional LSTM) +考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建基于lstm的栈式双向循环神经网络\[[9](#参考文献)\]。如图4所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用时间维度上的最大池化即可得到文本的定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。


-图 4 stacked lstm-rnn for text classification +图 4 栈式双向LSTM用于文本分类

## 数据准备 ### 数据介绍与下载 -我们以IMDB情感分析数据集为例进行介绍。训练模型之前, 我们需要预处理数椐并构建一个字典。 首先, 你可以使用下面的脚本下载 IMDB 数椐集和[Moses](http://www.statmt.org/moses/)工具, 我们提供了一个数据预处理脚本,它不仅能够处理IMDB数据,还能处理其他用户自定义的数据。 为了使用提前编写的脚本,需要将标记的训练和测试样本移动到另一个路径,这已经在`get_imdb.sh`中完成。 +我们以IMDB情感分析数据集为例进行介绍。IMDB数据集的训练集和测试集分别包含25000个已标注过的电影评论。其中,负面评论的得分小于等于4,正面评论的得分大于等于7,满分10分。您可以使用下面的脚本下载 IMDB 数椐集和[Moses](http://www.statmt.org/moses/)工具: ``` ./get_imdb.sh ``` -如果数椐获取成功,你将在目录```data```中看到下面的文件: +如果数椐获取成功,您将在目录```data```中看到下面的文件: ``` aclImdb get_imdb.sh imdb mosesdecoder-master @@ -95,14 +99,8 @@ aclImdb get_imdb.sh imdb mosesdecoder-master * imdb: 仅包含训练和测试数椐集。 * mosesdecoder-master: Moses 工具。 -IMDB数据集包含25,000个已标注过的电影评论用于训练,25,000个用于测试。负面的评论的得分小于等于4,正面的评论的得分大于等于7,满分10分。 ### 数据预处理 -在这个例子中,我们只使用已经标注过的训练集和测试集,且默认在训练集上构建字典。训练集已经做了随机打乱排序。 Moses 工具中的脚本`tokenizer.perl` 用于切分单单词和标点符号。执行下面的命令就可以预处理数椐。 - -``` -./preprocess.sh -``` -preprocess.sh: +我们只使用已标注的训练集和测试集。将训练集随机打乱排序,默认在训练集上构建字典。Moses 工具中的脚本`tokenizer.perl` 用于切分单词和标点符号。执行下面的命令就可以预处理数椐: ``` data_dir="./data/imdb" @@ -112,31 +110,32 @@ python preprocess.py -i data_dir * data_dir: 输入数椐所在目录。 * preprocess.py: 预处理脚本。 -运行成功后目录`data/pre-imdb` 结构如下: +运行成功后目录`./data/pre-imdb` 结构如下: ``` dict.txt labels.list test.list test_part_000 train.list train_part_000 ``` -* test\_part\_000 and train\_part\_000: 所有标记的测试集和训练集, 训练集已经随机打乱。 -* train.list and test.list: 训练集和测试集文件列表。 +* test\_part\_000 和 train\_part\_000: 所有标记的测试集和训练集, 训练集已经随机打乱。 +* train.list 和 test.list: 训练集和测试集文件列表。 * dict.txt: 利用训练集生成的字典。 -* labels.txt: neg 0, pos 1, 含义:标签0表示负面的评论,标签1表示正面的评论。 +* labels.list: 类别标签列表,标签0表示负面评论,标签1表示正面评论。 ### 提供数据给PaddlePaddle -PaddlePaddle可以读取Python写的传输数据脚本,下面dataprovider.py文件给出了完整例子,主要包括两部分: +PaddlePaddle可以读取Python写的传输数据脚本,下面`dataprovider.py`文件给出了完整例子,主要包括两部分: -* hook: 定义文本信息、类别Id的数据类型。文本被定义为整数序列`integer_value_sequence`,类别被定义为整数`integer_value` -* process: yield文本信息和类别Id,和hook里定义顺序一致。process读取的文件的行为类别和评论文本,以`'\t\t'`分隔。 +* hook函数: 定义文本信息、类别Id的数据类型。文本被定义为整数序列`integer_value_sequence`,类别被定义为整数`integer_value`。 +* process函数: 使用yield关键字返回文本信息和类别Id,和hook里定义顺序一致。process读取的文件的行为类别和评论文本,以`'\t\t'`分隔。 ```python from paddle.trainer.PyDataProvider2 import * def hook(settings, dictionary, **kwargs): settings.word_dict = dictionary - settings.input_types = [ - integer_value_sequence(len(settings.word_dict)), integer_value(2) - ] + settings.input_types = { + 'word': integer_value_sequence(len(settings.word_dict)), + 'label': integer_value(2) + } settings.logger.info('dict len : %d' % (len(settings.word_dict))) @@ -150,232 +149,157 @@ def process(settings, file_name): word_slot = [ settings.word_dict[w] for w in words if w in settings.word_dict ] - yield word_slot, label + yield { + 'word': word_slot, + 'label': label + } ``` ## 模型配置说明 -`trainer_config.py` 是一个配置文件的例子。第一行从`sentiment_net.py`中导出预定义的网络。 - -trainer_config.py: - +`trainer_config.py` 是一个配置文件的例子。 +### 数据定义 ```python -from sentiment_net import * - -data_dir = "./data/pre-imdb" -# whether this config is used for test -is_test = get_config_arg('is_test', bool, False) -# whether this config is used for prediction -is_predict = get_config_arg('is_predict', bool, False) -dict_dim, class_dim = sentiment_data(data_dir, is_test, is_predict) +define_py_data_sources2( + train_list, + test_list, + module="dataprovider", + obj="process", + args={'dictionary': word_dict}) +``` -################## Algorithm Config ##################### +在模型配置中利用define_py_data_sources2加载数据: -settings( - batch_size=128, - learning_rate=2e-3, - learning_method=AdamOptimizer(), - regularization=L2Regularization(8e-4), - gradient_clipping_threshold=25 -) - -#################### Network Config ###################### -stacked_lstm_net(dict_dim, class_dim=class_dim, - stacked_num=3, is_predict=is_predict) -# bidirectional_lstm_net(dict_dim, class_dim=class_dim, is_predict=is_predict) -# convolution_net(dict_dim, class_dim=class_dim, is_predict=is_predict) -``` +* train.list、test.list: 指定训练、测试数据。 +* module="dataprovider": 数据处理Python文件名。 +* obj="process": 指定生成数据的函数。 +* args={"dictionary": word_dict}: 额外的参数,这里指定词典。 -get\_config\_arg(): 获取通过 `--config_args=xx` 设置的命令行参数。 ### 优化算法配置 - * 使用随机梯度下降(sgd)算法。 - * 使用 adam 优化。 - * 设置batch size大小为128。 - * 设置全局学习率。 - * 设置L2正则。 - * 设置梯度裁剪(clipping)阈值。 - -### 数据定义 -数据定义在方法sentiment_data之中,其实现在文件`sentiment_net.py`中: ```python -def sentiment_data(data_dir=None, - is_test=False, - is_predict=False, - train_list="train.list", - test_list="test.list", - dict_file="dict.txt"): - """ - Predefined data provider for sentiment analysis. - is_test: whether this config is used for test. - is_predict: whether this config is used for prediction. - train_list: text file name, containing a list of training set. - test_list: text file name, containing a list of testing set. - dict_file: text file name, containing dictionary. - """ - dict_dim = len(open(join_path(data_dir, "dict.txt")).readlines()) - class_dim = len(open(join_path(data_dir, 'labels.list')).readlines()) - if is_predict: - return dict_dim, class_dim - - if data_dir is not None: - train_list = join_path(data_dir, train_list) - test_list = join_path(data_dir, test_list) - dict_file = join_path(data_dir, dict_file) - - train_list = train_list if not is_test else None - word_dict = dict() - with open(dict_file, 'r') as f: - for i, line in enumerate(open(dict_file, 'r')): - word_dict[line.split('\t')[0]] = i - - define_py_data_sources2( - train_list, - test_list, - module="dataprovider", - obj="process", - args={'dictionary': word_dict}) - - return dict_dim, class_dim +settings( + batch_size=128, + learning_rate=2e-3, + learning_method=AdamOptimizer(), + regularization=L2Regularization(8e-4), + gradient_clipping_threshold=25) ``` -在模型配置中利用define_py_data_sources2加载数据: - -* train.list,test.list: 指定训练、测试数据 -* module="dataprovider": 数据处理Python文件名 -* obj="process": 指定生成数据的函数 -* args={"dictionary": word_dict}: 额外的参数,这里指定词典 +* 设置batch size大小为128。 +* 设置全局学习率。 +* 使用 adam 优化。 +* 设置L2正则。 +* 设置梯度截断(clipping)阈值。 ### 模型结构 - - * `convolution_net`: 在`sentiment_net.py`中定义。其论述详见`文本卷积神经网络`小结。 - ```python - def convolution_net(input_dim, +我们用PaddlePaddle分别基于[文本卷积神经网络](#文本卷积神经网络(CNN))和[栈式双向LSTM](#栈式双向LSTM(Stacked Bidirectional LSTM))实现文本分类。 +#### 文本卷积神经网络的实现 +```python +def convolution_net(input_dim, class_dim=2, emb_dim=128, hid_dim=128, is_predict=False): - data = data_layer("word", input_dim) # one-hot表示的词序列 - emb = embedding_layer(input=data, size=emb_dim) # 将one-hot表示的词序列映射为embedding序列 - conv_3 = sequence_conv_pool(input=emb, context_len=3, hidden_size=hid_dim) #窗口大小为3的convolution及max pooling操作 - conv_4 = sequence_conv_pool(input=emb, context_len=4, hidden_size=hid_dim) #窗口大小为4的convolution及max pooling操作 - output = fc_layer(input=[conv_3,conv_4], size=class_dim, act=SoftmaxActivation()) #将conv_3和conv_4拼接起来输入给softmax分类 - - if not is_predict: - lbl = data_layer("label", 1) #类别标签 - outputs(classification_cost(input=output, label=lbl)) - else: - outputs(output) - ``` - - 其中,我们仅用一个`sequence_conv_pool`方法就实现了convolution和pooling操作,filter的数量为hidden_size参数。`sequence_conv_pool`的实现详见`Paddle/python/paddle/trainer_config_helpers/networks.py`。 - - * `bidirectional_lstm_net`: 在`sentiment_net.py`中定义。其论述详见`使用循环神经网络的组合进行文本分类`小结。 - ```python - def bidirectional_lstm_net(input_dim, - class_dim=2, - emb_dim=128, - lstm_dim=128, - is_predict=False): - data = data_layer("word", input_dim) - emb = embedding_layer(input=data, size=emb_dim) - bi_lstm = bidirectional_lstm(input=emb, size=lstm_dim) # 双向lstm,其默认返回值为正向lstm-rnn和反向lstm-rnn最后一个时刻的隐层值的拼接。其实现详见`Paddle/python/paddle/trainer_config_helpers/networks.py` - dropout = dropout_layer(input=bi_lstm, dropout_rate=0.5) - output = fc_layer(input=dropout, size=class_dim, act=SoftmaxActivation()) - - if not is_predict: - lbl = data_layer("label", 1) - outputs(classification_cost(input=output, label=lbl)) - else: - outputs(output) - ``` - - * `stacked_lstm_net`: 在`sentiment_net.py`中定义,默认情况下使用此网络。其论述详见`使用循环神经网络的组合进行文本分类`小结。 - ```python - def stacked_lstm_net(input_dim, + # 网络输入:id表示的词序列,词典大小为input_dim + data = data_layer("word", input_dim) + # 将id表示的词序列映射为embedding序列 + emb = embedding_layer(input=data, size=emb_dim) + # 卷积及最大化池操作,卷积核窗口大小为3 + conv_3 = sequence_conv_pool(input=emb, context_len=3, hidden_size=hid_dim) + # 卷积及最大化池操作,卷积核窗口大小为4 + conv_4 = sequence_conv_pool(input=emb, context_len=4, hidden_size=hid_dim) + # 将conv_3和conv_4拼接起来输入给softmax分类,类别数为class_dim + output = fc_layer( + input=[conv_3, conv_4], size=class_dim, act=SoftmaxActivation()) + + if not is_predict: + lbl = data_layer("label", 1) #网络输入:类别标签 + outputs(classification_cost(input=output, label=lbl)) + else: + outputs(output) +``` + +其中,我们仅用一个[`sequence_conv_pool`](https://github.com/PaddlePaddle/Paddle/blob/develop/python/paddle/trainer_config_helpers/networks.py)方法就实现了卷积和池化操作,卷积核的数量为hidden_size参数。 +#### 栈式双向LSTM的实现 + +```python +def stacked_lstm_net(input_dim, class_dim=2, emb_dim=128, hid_dim=512, stacked_num=3, is_predict=False): - """ - A Wrapper for sentiment classification task. - This network uses bi-directional recurrent network, - consisting three LSTM layers. This configure is referred to - the paper as following url, but use fewer layrs. - http://www.aclweb.org/anthology/P15-1109 - - input_dim: here is word dictionary dimension. - class_dim: number of categories. - emb_dim: dimension of word embedding. - hid_dim: dimension of hidden layer. - stacked_num: number of stacked lstm-hidden layer. - is_predict: is predicting or not. - Some layers is not needed in network when predicting. - """ - hid_lr = 1e-3 - assert stacked_num % 2 == 1 - - layer_attr = ExtraLayerAttribute(drop_rate=0.5) - fc_para_attr = ParameterAttribute(learning_rate=hid_lr) - lstm_para_attr = ParameterAttribute(initial_std=0., learning_rate=1.) - para_attr = [fc_para_attr, lstm_para_attr] - bias_attr = ParameterAttribute(initial_std=0., l2_rate=0.) - relu = ReluActivation() - linear = LinearActivation() - - data = data_layer("word", input_dim) - emb = embedding_layer(input=data, size=emb_dim) - - fc1 = fc_layer(input=emb, size=hid_dim, act=linear, bias_attr=bias_attr) - lstm1 = lstmemory( - input=fc1, act=relu, bias_attr=bias_attr, layer_attr=layer_attr) #基于lstm的循环神经网络 - - inputs = [fc1, lstm1] - for i in range(2, stacked_num + 1): #由fc_layer和lstmemory构建双向stacked_lstm_net - fc = fc_layer( - input=inputs, - size=hid_dim, - act=linear, - param_attr=para_attr, - bias_attr=bias_attr) - lstm = lstmemory( - input=fc, - reverse=(i % 2) == 0, - act=relu, - bias_attr=bias_attr, - layer_attr=layer_attr) - inputs = [fc, lstm] - - fc_last = pooling_layer(input=inputs[0], pooling_type=MaxPooling()) #对最后一层fc_layer使用max pooling over time得到定长向量 - lstm_last = pooling_layer(input=inputs[1], pooling_type=MaxPooling()) #对最后一层lstmemory使用max pooling over time得到定长向量 - output = fc_layer( - input=[fc_last, lstm_last], - size=class_dim, - act=SoftmaxActivation(), - bias_attr=bias_attr, - param_attr=para_attr) - - if is_predict: - outputs(output) - else: - outputs(classification_cost(input=output, label=data_layer('label', 1))) - ``` + hid_lr = 1e-3 + assert stacked_num % 2 == 1 + # 设置神经网络层的属性 + layer_attr = ExtraLayerAttribute(drop_rate=0.5) + # 设置参数的属性 + fc_para_attr = ParameterAttribute(learning_rate=hid_lr) + lstm_para_attr = ParameterAttribute(initial_std=0., learning_rate=1.) + para_attr = [fc_para_attr, lstm_para_attr] + bias_attr = ParameterAttribute(initial_std=0., l2_rate=0.) + # 激活函数 + relu = ReluActivation() + linear = LinearActivation() + # 网络输入:id表示的词序列,词典大小为input_dim + data = data_layer("word", input_dim) + # 将id表示的词序列映射为embedding序列 + emb = embedding_layer(input=data, size=emb_dim) + + fc1 = fc_layer(input=emb, size=hid_dim, act=linear, bias_attr=bias_attr) + # 基于lstm的循环神经网络 + lstm1 = lstmemory( + input=fc1, act=relu, bias_attr=bias_attr, layer_attr=layer_attr) + + # 由fc_layer和lstmemory构建深度为stacked_num的栈式双向LSTM + inputs = [fc1, lstm1] + for i in range(2, stacked_num + 1): + fc = fc_layer( + input=inputs, + size=hid_dim, + act=linear, + param_attr=para_attr, + bias_attr=bias_attr) + lstm = lstmemory( + input=fc, + # 奇数层正向,偶数层反向。 + reverse=(i % 2) == 0, + act=relu, + bias_attr=bias_attr, + layer_attr=layer_attr) + inputs = [fc, lstm] + + # 对最后一层fc_layer使用时间维度上的最大池化得到定长向量 + fc_last = pooling_layer(input=inputs[0], pooling_type=MaxPooling()) + # 对最后一层lstmemory使用时间维度上的最大池化得到定长向量 + lstm_last = pooling_layer(input=inputs[1], pooling_type=MaxPooling()) + # 将fc_last和lstm_last拼接起来输入给softmax分类,类别数为class_dim + output = fc_layer( + input=[fc_last, lstm_last], + size=class_dim, + act=SoftmaxActivation(), + bias_attr=bias_attr, + param_attr=para_attr) + + if is_predict: + outputs(output) + else: + outputs(classification_cost(input=output, label=data_layer('label', 1))) +``` ## 训练模型 -首先安装PaddlePaddle。 然后使用下面的脚本 `train.sh` 来开启本地的训练。 +使用`train.sh`脚本可以开启本地的训练: ``` ./train.sh ``` -train.sh: +train.sh内容如下: -``` -config=trainer_config.py -output=./model_output -paddle train --config=$config \ - --save_dir=$output \ +```bash +paddle train --config=trainer_config.py \ + --save_dir=./model_output \ --job=train \ --use_gpu=false \ --trainer_count=4 \ @@ -387,10 +311,10 @@ paddle train --config=$config \ 2>&1 | tee 'train.log' ``` -* \--config=$config: 设置网络配置。 -* \--save\_dir=$output: 设置输出路径以保存训练完成的模型。 +* \--config=trainer_config.py: 设置模型配置。 +* \--save\_dir=./model_output: 设置输出路径以保存训练完成的模型。 * \--job=train: 设置工作模式为训练。 -* \--use\_gpu=false: 使用CPU训练,如果你安装GPU版本的PaddlePaddle,并想使用GPU来训练可将此设置为true。 +* \--use\_gpu=false: 使用CPU训练,如果您安装GPU版本的PaddlePaddle,并想使用GPU来训练可将此设置为true。 * \--trainer\_count=4:设置线程数(或GPU个数)。 * \--num\_passes=15: 设置pass,PaddlePaddle中的一个pass意味着对数据集中的所有样本进行一次训练。 * \--log\_period=20: 每20个batch打印一次日志。 @@ -414,7 +338,7 @@ Test samples=24999 cost=0.39297 Eval: classification_error_evaluator=0.149406 * CurrentEval: classification\_error\_evaluator: 最新log_period个batch的分类错误。 * Pass=0: 通过所有训练集一次称为一个Pass。 0表示第一次经过训练集。 -默认情况下,我们使用`stacked_lstm_net`网络,如果要使用双向LSTM或卷积网络,注释相应的行即可。 +我们的模型配置`trainer_config.py`默认使用`stacked_lstm_net`网络,如果要使用`convolution_net`,注释相应的行即可。 ## 应用模型 ### 测试模型 @@ -424,7 +348,7 @@ Test samples=24999 cost=0.39297 Eval: classification_error_evaluator=0.149406 ./test.sh ``` -test.sh: +测试脚本`test.sh`的内容如下,其中函数`get_best_pass`通过对分类错误率进行排序来获得最佳模型: ```bash function get_best_pass() { @@ -452,14 +376,14 @@ paddle train --config=$net_conf \ 2>&1 | tee 'test.log' ``` -函数`get_best_pass`依据分类错误率获取最佳模型。 与训练不同,测试时需要指定`--job = test`和模型路径,即`--model_list = $model_list`。如果运行成功,日志将保存在“test.log”中。例如,在我们的测试中,最好的模型是`model_output / pass-00002`,分类误差是0.115645,如下: +与训练不同,测试时需要指定`--job = test`和模型路径`--model_list = $model_list`。如果测试成功,日志将保存在`test.log`中。 在我们的测试中,最好的模型是`model_output/pass-00002`,分类错误率是0.115645: ``` Pass=0 samples=24999 AvgCost=0.280471 Eval: classification_error_evaluator=0.115645 ``` ### 预测 -`predict.py`脚本提供了一个预测接口。在使用它之前请安装PaddlePaddle的python api。 预测IMDB的未标记评论的一个实例如下: +`predict.py`脚本提供了一个预测接口。预测IMDB中未标记评论的示例如下: ``` ./predict.sh @@ -467,13 +391,11 @@ Pass=0 samples=24999 AvgCost=0.280471 Eval: classification_error_evaluator=0.115 predict.sh: ```bash -#Note the default model is pass-00002, you shold make sure the model path -#exists or change the mode path. model=model_output/pass-00002/ config=trainer_config.py label=data/pre-imdb/labels.list cat ./data/aclImdb/test/pos/10007_10.txt | python predict.py \ - --tconf=$config\ + --tconf=$config \ --model=$model \ --label=$label \ --dict=./data/pre-imdb/dict.txt \ @@ -488,7 +410,7 @@ cat ./data/aclImdb/test/pos/10007_10.txt | python predict.py \ * `--dict=data/pre-imdb/dict.txt` : 设置文本数据字典文件。 * `--batch_size=1` : 预测时的batch size大小。 -注意应该确保默认模型路径`model_output / pass-00002`存在或更改为其它模型路径。 +注意应该确保默认模型路径`model_output/pass-00002`存在或更改为其它模型路径。 本示例的预测结果: @@ -498,7 +420,7 @@ Loading parameters from model_output/pass-00002/ ``` ## 总结 -本章我们以情感分析为例介绍了使用深度学习的方法进行端对端的短文本分类,并且使用PaddlePaddle完成了全部相关实验。我们简要论述了两种文本处理模型:卷积神经网络和循环神经网络。在后续的章节中我们会看到这两种基本的深度学习模型在其它任务上的应用。 +本章我们以情感分析为例,介绍了使用深度学习的方法进行端对端的短文本分类,并且使用PaddlePaddle完成了全部相关实验。同时,我们简要介绍了两种文本处理模型:卷积神经网络和循环神经网络。在后续的章节中我们会看到这两种基本的深度学习模型在其它任务上的应用。 ## 参考文献 1. Kim Y. [Convolutional neural networks for sentence classification](http://arxiv.org/pdf/1408.5882)[J]. arXiv preprint arXiv:1408.5882, 2014. 2. Kalchbrenner N, Grefenstette E, Blunsom P. [A convolutional neural network for modelling sentences](http://arxiv.org/pdf/1404.2188.pdf?utm_medium=App.net&utm_source=PourOver)[J]. arXiv preprint arXiv:1404.2188, 2014. diff --git a/understand_sentiment/dataprovider.py b/understand_sentiment/dataprovider.py index 00f72cec..976351ab 100755 --- a/understand_sentiment/dataprovider.py +++ b/understand_sentiment/dataprovider.py @@ -16,9 +16,10 @@ def hook(settings, dictionary, **kwargs): settings.word_dict = dictionary - settings.input_types = [ - integer_value_sequence(len(settings.word_dict)), integer_value(2) - ] + settings.input_types = { + 'word': integer_value_sequence(len(settings.word_dict)), + 'label': integer_value(2) + } settings.logger.info('dict len : %d' % (len(settings.word_dict))) @@ -32,4 +33,4 @@ def process(settings, file_name): word_slot = [ settings.word_dict[w] for w in words if w in settings.word_dict ] - yield word_slot, label + yield {'word': word_slot, 'label': label} diff --git a/understand_sentiment/predict.sh b/understand_sentiment/predict.sh index c72a8e86..20adee8a 100755 --- a/understand_sentiment/predict.sh +++ b/understand_sentiment/predict.sh @@ -20,7 +20,7 @@ model=model_output/pass-00002/ config=trainer_config.py label=data/pre-imdb/labels.list cat ./data/aclImdb/test/pos/10007_10.txt | python predict.py \ - --tconf=$config\ + --tconf=$config \ --model=$model \ --label=$label \ --dict=./data/pre-imdb/dict.txt \ diff --git a/understand_sentiment/preprocess.sh b/understand_sentiment/preprocess.sh deleted file mode 100755 index 19ec34d4..00000000 --- a/understand_sentiment/preprocess.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -set -e - -echo "Start to preprcess..." - -data_dir="./data/imdb" -python preprocess.py -i $data_dir - -echo "Done." diff --git a/understand_sentiment/sentiment_net.py b/understand_sentiment/sentiment_net.py deleted file mode 100644 index 7ab50313..00000000 --- a/understand_sentiment/sentiment_net.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from os.path import join as join_path - -from paddle.trainer_config_helpers import * - - -def sentiment_data(data_dir=None, - is_test=False, - is_predict=False, - train_list="train.list", - test_list="test.list", - dict_file="dict.txt"): - """ - Predefined data provider for sentiment analysis. - is_test: whether this config is used for test. - is_predict: whether this config is used for prediction. - train_list: text file name, containing a list of training set. - test_list: text file name, containing a list of testing set. - dict_file: text file name, containing dictionary. - """ - dict_dim = len(open(join_path(data_dir, "dict.txt")).readlines()) - class_dim = len(open(join_path(data_dir, 'labels.list')).readlines()) - if is_predict: - return dict_dim, class_dim - - if data_dir is not None: - train_list = join_path(data_dir, train_list) - test_list = join_path(data_dir, test_list) - dict_file = join_path(data_dir, dict_file) - - train_list = train_list if not is_test else None - word_dict = dict() - with open(dict_file, 'r') as f: - for i, line in enumerate(open(dict_file, 'r')): - word_dict[line.split('\t')[0]] = i - - define_py_data_sources2( - train_list, - test_list, - module="dataprovider", - obj="process", - args={'dictionary': word_dict}) - - return dict_dim, class_dim - - -def convolution_net(input_dim, - class_dim=2, - emb_dim=128, - hid_dim=128, - is_predict=False): - data = data_layer("word", input_dim) - emb = embedding_layer(input=data, size=emb_dim) - conv_3 = sequence_conv_pool(input=emb, context_len=3, hidden_size=hid_dim) - conv_4 = sequence_conv_pool(input=emb, context_len=4, hidden_size=hid_dim) - output = fc_layer( - input=[conv_3, conv_4], size=class_dim, act=SoftmaxActivation()) - - if not is_predict: - lbl = data_layer("label", 1) - outputs(classification_cost(input=output, label=lbl)) - else: - outputs(output) - - -def bidirectional_lstm_net(input_dim, - class_dim=2, - emb_dim=128, - lstm_dim=128, - is_predict=False): - data = data_layer("word", input_dim) - emb = embedding_layer(input=data, size=emb_dim) - bi_lstm = bidirectional_lstm(input=emb, size=lstm_dim) - dropout = dropout_layer(input=bi_lstm, dropout_rate=0.5) - output = fc_layer(input=dropout, size=class_dim, act=SoftmaxActivation()) - - if not is_predict: - lbl = data_layer("label", 1) - outputs(classification_cost(input=output, label=lbl)) - else: - outputs(output) - - -def stacked_lstm_net(input_dim, - class_dim=2, - emb_dim=128, - hid_dim=512, - stacked_num=3, - is_predict=False): - """ - A Wrapper for sentiment classification task. - This network uses bi-directional recurrent network, - consisting three LSTM layers. This configure is referred to - the paper as following url, but use fewer layrs. - http://www.aclweb.org/anthology/P15-1109 - - input_dim: here is word dictionary dimension. - class_dim: number of categories. - emb_dim: dimension of word embedding. - hid_dim: dimension of hidden layer. - stacked_num: number of stacked lstm-hidden layer. - is_predict: is predicting or not. - Some layers is not needed in network when predicting. - """ - hid_lr = 1e-3 - assert stacked_num % 2 == 1 - - layer_attr = ExtraLayerAttribute(drop_rate=0.5) - fc_para_attr = ParameterAttribute(learning_rate=hid_lr) - lstm_para_attr = ParameterAttribute(initial_std=0., learning_rate=1.) - para_attr = [fc_para_attr, lstm_para_attr] - bias_attr = ParameterAttribute(initial_std=0., l2_rate=0.) - relu = ReluActivation() - linear = LinearActivation() - - data = data_layer("word", input_dim) - emb = embedding_layer(input=data, size=emb_dim) - - fc1 = fc_layer(input=emb, size=hid_dim, act=linear, bias_attr=bias_attr) - lstm1 = lstmemory( - input=fc1, act=relu, bias_attr=bias_attr, layer_attr=layer_attr) - - inputs = [fc1, lstm1] - for i in range(2, stacked_num + 1): - fc = fc_layer( - input=inputs, - size=hid_dim, - act=linear, - param_attr=para_attr, - bias_attr=bias_attr) - lstm = lstmemory( - input=fc, - reverse=(i % 2) == 0, - act=relu, - bias_attr=bias_attr, - layer_attr=layer_attr) - inputs = [fc, lstm] - - fc_last = pooling_layer(input=inputs[0], pooling_type=MaxPooling()) - lstm_last = pooling_layer(input=inputs[1], pooling_type=MaxPooling()) - output = fc_layer( - input=[fc_last, lstm_last], - size=class_dim, - act=SoftmaxActivation(), - bias_attr=bias_attr, - param_attr=para_attr) - - if is_predict: - outputs(output) - else: - outputs(classification_cost(input=output, label=data_layer('label', 1))) diff --git a/understand_sentiment/train.sh b/understand_sentiment/train.sh index 5ce8bf4b..df8d464d 100755 --- a/understand_sentiment/train.sh +++ b/understand_sentiment/train.sh @@ -14,10 +14,8 @@ # limitations under the License. set -e -config=trainer_config.py -output=./model_output -paddle train --config=$config \ - --save_dir=$output \ +paddle train --config=trainer_config.py \ + --save_dir=./model_output \ --job=train \ --use_gpu=false \ --trainer_count=4 \ diff --git a/understand_sentiment/trainer_config.py b/understand_sentiment/trainer_config.py index b327d09f..8b426d9c 100644 --- a/understand_sentiment/trainer_config.py +++ b/understand_sentiment/trainer_config.py @@ -12,16 +12,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -from sentiment_net import * +from os.path import join as join_path from paddle.trainer_config_helpers import * - # whether this config is used for test is_test = get_config_arg('is_test', bool, False) # whether this config is used for prediction is_predict = get_config_arg('is_predict', bool, False) data_dir = "./data/pre-imdb" -dict_dim, class_dim = sentiment_data(data_dir, is_test, is_predict) +train_list = "train.list" +test_list = "test.list" +dict_file = "dict.txt" + +dict_dim = len(open(join_path(data_dir, "dict.txt")).readlines()) +class_dim = len(open(join_path(data_dir, 'labels.list')).readlines()) + +if not is_predict: + train_list = join_path(data_dir, train_list) + test_list = join_path(data_dir, test_list) + dict_file = join_path(data_dir, dict_file) + train_list = train_list if not is_test else None + word_dict = dict() + with open(dict_file, 'r') as f: + for i, line in enumerate(open(dict_file, 'r')): + word_dict[line.split('\t')[0]] = i + + define_py_data_sources2( + train_list, + test_list, + module="dataprovider", + obj="process", + args={'dictionary': word_dict}) ################## Algorithm Config ##################### @@ -34,7 +55,97 @@ gradient_clipping_threshold=25) #################### Network Config ###################### + + +def convolution_net(input_dim, + class_dim=2, + emb_dim=128, + hid_dim=128, + is_predict=False): + data = data_layer("word", input_dim) + emb = embedding_layer(input=data, size=emb_dim) + conv_3 = sequence_conv_pool(input=emb, context_len=3, hidden_size=hid_dim) + conv_4 = sequence_conv_pool(input=emb, context_len=4, hidden_size=hid_dim) + output = fc_layer( + input=[conv_3, conv_4], size=class_dim, act=SoftmaxActivation()) + + if not is_predict: + lbl = data_layer("label", 1) + outputs(classification_cost(input=output, label=lbl)) + else: + outputs(output) + + +def stacked_lstm_net(input_dim, + class_dim=2, + emb_dim=128, + hid_dim=512, + stacked_num=3, + is_predict=False): + """ + A Wrapper for sentiment classification task. + This network uses bi-directional recurrent network, + consisting three LSTM layers. This configure is referred to + the paper as following url, but use fewer layrs. + http://www.aclweb.org/anthology/P15-1109 + + input_dim: here is word dictionary dimension. + class_dim: number of categories. + emb_dim: dimension of word embedding. + hid_dim: dimension of hidden layer. + stacked_num: number of stacked lstm-hidden layer. + is_predict: is predicting or not. + Some layers is not needed in network when predicting. + """ + hid_lr = 1e-3 + assert stacked_num % 2 == 1 + + layer_attr = ExtraLayerAttribute(drop_rate=0.5) + fc_para_attr = ParameterAttribute(learning_rate=hid_lr) + lstm_para_attr = ParameterAttribute(initial_std=0., learning_rate=1.) + para_attr = [fc_para_attr, lstm_para_attr] + bias_attr = ParameterAttribute(initial_std=0., l2_rate=0.) + relu = ReluActivation() + linear = LinearActivation() + + data = data_layer("word", input_dim) + emb = embedding_layer(input=data, size=emb_dim) + + fc1 = fc_layer(input=emb, size=hid_dim, act=linear, bias_attr=bias_attr) + lstm1 = lstmemory( + input=fc1, act=relu, bias_attr=bias_attr, layer_attr=layer_attr) + + inputs = [fc1, lstm1] + for i in range(2, stacked_num + 1): + fc = fc_layer( + input=inputs, + size=hid_dim, + act=linear, + param_attr=para_attr, + bias_attr=bias_attr) + lstm = lstmemory( + input=fc, + reverse=(i % 2) == 0, + act=relu, + bias_attr=bias_attr, + layer_attr=layer_attr) + inputs = [fc, lstm] + + fc_last = pooling_layer(input=inputs[0], pooling_type=MaxPooling()) + lstm_last = pooling_layer(input=inputs[1], pooling_type=MaxPooling()) + output = fc_layer( + input=[fc_last, lstm_last], + size=class_dim, + act=SoftmaxActivation(), + bias_attr=bias_attr, + param_attr=para_attr) + + if is_predict: + outputs(output) + else: + outputs(classification_cost(input=output, label=data_layer('label', 1))) + + stacked_lstm_net( dict_dim, class_dim=class_dim, stacked_num=3, is_predict=is_predict) -# bidirectional_lstm_net(dict_dim, class_dim=class_dim, is_predict=is_predict) # convolution_net(dict_dim, class_dim=class_dim, is_predict=is_predict) From 9d6d8fe1caa4a1e1734ab5cc36ae804ce8d1173e Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Fri, 6 Jan 2017 10:37:29 +0800 Subject: [PATCH 08/11] run all code, revised completed. --- understand_sentiment/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index 837cee7f..d8e375e2 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -15,14 +15,14 @@ 在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类涉及文本表示和分类方法。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),话题模型等等;分类方法有SVM(support vector machine), LR(logistic regression), [Boosting](https://en.wikipedia.org/wiki/Boosting_(machine_learning))等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,它并不能充分表示文本的语义信息。例如,句子“这部电影糟糕透了”和“一个乏味,空洞,没有内涵的作品”在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子“一个空洞,没有内涵的作品”和“一个不空洞而且有内涵的作品”的BOW相似度很高,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 ## 模型概览 -本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其拓展。我们首先介绍处理文本的卷积神经网络。 +本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。我们首先介绍处理文本的卷积神经网络。 ### 文本卷积神经网络(CNN) 卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为2D网格的像素点,自然语言可以视为1D的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示,且其对于数据的某些变化具有不变性。大量实验表明,卷积神经网络能高效的对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)\[[1](#参考文献)\]。


图 1 卷积神经网络文本分类模型

-假设一个句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,维度大小为$k$。我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到这$h$个词的特征值$c_i$: +假设一个句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,维度大小为$k$。我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: $$c_i=f(w\cdot x_{i:i+h-1}+b)$$ @@ -43,7 +43,7 @@ $$\hat c=max(c)$$
图 2 循环神经网络按时间展开的示意图

-循环神经网络按时间展开后如图2所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐层的状态$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐藏层的值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: +循环神经网络按时间展开后如图2所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐层的状态值$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐层的状态值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: $$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$ @@ -52,7 +52,7 @@ $$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$ 在处理自然语言时,一般会先将词(one-hot表示)映射为其词向量(word embedding)表示,然后再作为循环神经网络每一时刻的输入$x_t$。此外,可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 ### 长时短期记忆(LSTM) -循环神经网络隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象\[[6](#参考文献)\]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)\[[5](#参考文献)\]提出了lstm(long short term memory)。 +循环神经网络隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象\[[6](#参考文献)\]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了lstm(long short term memory\[[5](#参考文献)\])。 相比于简单的循环神经网络,lstm增加了记忆单元$c$、输入门$i$、遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm的循环神经网络表示的函数记为$F$,则其公式为: @@ -66,7 +66,7 @@ c_t & = f_t\odot c_{t-1}+i_t\odot tanh(W_{xc}x_t+W_{hc}h_{h-1}+b_c)\\\\ o_t & = \sigma(W_{xo}x_t+W_{ho}h_{h-1}+W_{co}c_{t}+b_o)\\\\ h_t & = o_t\odot tanh(c_t)\\\\ \end{align} -其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为逐元素的双曲正切函数,$\odot$表示逐元素的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,即各自以不同的方式控制着记忆单元$c$,如图3所示: +其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为逐元素的双曲正切函数,$\odot$表示逐元素的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,它们各自以不同的方式控制着记忆单元$c$,如图3所示:


图 3 时刻$t$的lstm @@ -86,8 +86,8 @@ $$ h_t=Recrurent(x_t,h_{t-1})$$ ### 数据介绍与下载 我们以IMDB情感分析数据集为例进行介绍。IMDB数据集的训练集和测试集分别包含25000个已标注过的电影评论。其中,负面评论的得分小于等于4,正面评论的得分大于等于7,满分10分。您可以使用下面的脚本下载 IMDB 数椐集和[Moses](http://www.statmt.org/moses/)工具: -``` -./get_imdb.sh +```bash +data/get_imdb.sh ``` 如果数椐获取成功,您将在目录```data```中看到下面的文件: @@ -104,7 +104,7 @@ aclImdb get_imdb.sh imdb mosesdecoder-master ``` data_dir="./data/imdb" -python preprocess.py -i data_dir +python preprocess.py -i $data_dir ``` * data_dir: 输入数椐所在目录。 @@ -416,7 +416,7 @@ cat ./data/aclImdb/test/pos/10007_10.txt | python predict.py \ ``` Loading parameters from model_output/pass-00002/ -./data/aclImdb/test/pos/10014_7.txt: predicting label is pos +predicting label is pos ``` ## 总结 From f561dccce24ee6eda61e318583d173c77f86ac4b Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Tue, 10 Jan 2017 10:13:43 +0800 Subject: [PATCH 09/11] revised according to reviews --- understand_sentiment/.gitignore | 2 +- understand_sentiment/README.md | 134 ++++++++++++++++++++------------ 2 files changed, 84 insertions(+), 52 deletions(-) diff --git a/understand_sentiment/.gitignore b/understand_sentiment/.gitignore index 30b44230..667762d3 100644 --- a/understand_sentiment/.gitignore +++ b/understand_sentiment/.gitignore @@ -2,7 +2,7 @@ data/aclImdb data/imdb data/pre-imdb data/mosesdecoder-master -logs/ +*.log model_output dataprovider_copy_1.py model.list diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index d8e375e2..55f34542 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -1,8 +1,6 @@ # 情感分析 ## 背景介绍 -在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤);也可以是三类,如(积极,消极,中性)等等。 - -情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论。为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。表格1展示了对电影评论进行情感分析的例子: +在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤);也可以是三类,如(积极,消极,中性)等等。情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论;或为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。表格1展示了对电影评论进行情感分析的例子: | 电影评论 | 类别 | | -------- | ----- | @@ -13,20 +11,24 @@

表格 1 电影评论情感分析

-在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类涉及文本表示和分类方法。在深度学习的方法出现之前,主流的文本表示方法为BOW(bag of words),话题模型等等;分类方法有SVM(support vector machine), LR(logistic regression), [Boosting](https://en.wikipedia.org/wiki/Boosting_(machine_learning))等等。BOW忽略了词的顺序信息,而且是高维度的稀疏向量表示,它并不能充分表示文本的语义信息。例如,句子“这部电影糟糕透了”和“一个乏味,空洞,没有内涵的作品”在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子“一个空洞,没有内涵的作品”和“一个不空洞而且有内涵的作品”的BOW相似度很高,但实际上它们的意思很不一样。本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词的顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升。 +在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类涉及文本表示和分类方法两个问题。在深度学习的方法出现之前,主流的文本表示方法为词袋模型BOW(bag of words),话题模型等等;分类方法有SVM(support vector machine), LR(logistic regression)等等。 + +BOW假定对于一段文本,忽略其词顺序和语法、句法,将其仅仅看做是一个词集合,它并不能充分表示文本的语义信息。例如,句子“这部电影糟糕透了”和“一个乏味,空洞,没有内涵的作品”在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子“一个空洞,没有内涵的作品”和“一个不空洞而且有内涵的作品”的BOW相似度很高,但实际上它们的意思很不一样。 + +本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升\[[1](#参考文献)\]。 ## 模型概览 -本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。我们首先介绍处理文本的卷积神经网络。 +本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。下面依次介绍这几个模型。 ### 文本卷积神经网络(CNN) -卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为2D网格的像素点,自然语言可以视为1D的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示,且其对于数据的某些变化具有不变性。大量实验表明,卷积神经网络能高效的对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)\[[1](#参考文献)\]。 +卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为二维网格的像素点,自然语言可以视为一维的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示。实验表明,卷积神经网络能高效地对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)\[[1](#参考文献)\]。


-图 1 卷积神经网络文本分类模型 +图1. 卷积神经网络文本分类模型

-假设一个句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,维度大小为$k$。我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: +假设一个句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,$k$为维度大小。我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: $$c_i=f(w\cdot x_{i:i+h-1}+b)$$ -其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如$sigmoid$。将卷积核应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$,产生一个特征图(feature map): +其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如$sigmoid$。将卷积核应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$,产生一个特征图(feature map): $$c=[c_1,c_2,\ldots,c_{n-h+1}], c \in \mathbb{R}^{n-h+1}$$ @@ -38,23 +40,23 @@ $$\hat c=max(c)$$ 对于一般的短文本分类问题,上文所述的简单的文本卷积网络即可达到很高的正确率\[[1](#参考文献)\]。若想得到更抽象更高级的文本特征表示,可以构建深层文本卷积神经网络\[[2](#参考文献),[3](#参考文献)\]。 ### 循环神经网络(RNN) -循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的\[[4](#参考文献)\]。自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory\[[5](#参考文献)\]等)在自然语言处理的多个领域取得了丰硕的成果,如在语言模型、句法解析、语义角色标注(或一般的序列标注)、语义表示、图文生成、对话、机器翻译等任务上均表现优异甚至成为目前效果最好的方法。 +循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的\[[4](#参考文献)\]。自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory\[[5](#参考文献)\]等)在自然语言处理的多个领域,如语言模型、句法解析、语义角色标注(或一般的序列标注)、语义表示、图文生成、对话、机器翻译等任务上均表现优异甚至成为目前效果最好的方法。


-图 2 循环神经网络按时间展开的示意图 +图2. 循环神经网络按时间展开的示意图

循环神经网络按时间展开后如图2所示:在第$t$时刻,网络读入第$t$个输入$x_t$(向量表示)及前一时刻隐层的状态值$h_{t-1}$(向量表示,$h_0$一般初始化为$0$向量),计算得出本时刻隐层的状态值$h_t$,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为$f$,则其公式可表示为: $$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$ -其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为逐元素(elementwise)的$sigmoid$函数。 +其中$W_{xh}$是输入到隐层的矩阵参数,$W_{hh}$是隐层到隐层的矩阵参数,$b_h$为隐层的偏置向量(bias)参数,$\sigma$为$sigmoid$函数。 在处理自然语言时,一般会先将词(one-hot表示)映射为其词向量(word embedding)表示,然后再作为循环神经网络每一时刻的输入$x_t$。此外,可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 -### 长时短期记忆(LSTM) -循环神经网络隐状态的输入来源于当前输入和前一时刻隐状态的值,这会导致很久以前的输入容易被覆盖掉。实际上,人们发现当序列很长时,循环神经网络就会表现很差(远距离依赖问题),训练过程中会出现梯度消失或爆炸现象\[[6](#参考文献)\]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了lstm(long short term memory\[[5](#参考文献)\])。 +### 长短期记忆(LSTM) +对于较长的序列数据,循环神经网络的训练过程中容易出现梯度消失或爆炸现象\[[6](#参考文献)\]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了LSTM(long short term memory\[[5](#参考文献)\])。 -相比于简单的循环神经网络,lstm增加了记忆单元$c$、输入门$i$、遗忘门$f$及输出门$o$,这些门及记忆单元组合起来大大提升了循环神经网络处理远距离依赖问题的能力,若将基于lstm的循环神经网络表示的函数记为$F$,则其公式为: +相比于简单的循环神经网络,LSTM增加了记忆单元$c$、输入门$i$、遗忘门$f$及输出门$o$。这些门及记忆单元组合起来大大提升了循环神经网络处理长序列数据的能力。若将基于LSTM的循环神经网络表示的函数记为$F$,则其公式为: $$ h_t=F(x_t,h_{t-1})$$ @@ -66,28 +68,28 @@ c_t & = f_t\odot c_{t-1}+i_t\odot tanh(W_{xc}x_t+W_{hc}h_{h-1}+b_c)\\\\ o_t & = \sigma(W_{xo}x_t+W_{ho}h_{h-1}+W_{co}c_{t}+b_o)\\\\ h_t & = o_t\odot tanh(c_t)\\\\ \end{align} -其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为逐元素的双曲正切函数,$\odot$表示逐元素的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,它们各自以不同的方式控制着记忆单元$c$,如图3所示: +其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为双曲正切函数,$\odot$表示逐元素(elementwise)的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,它们各自以不同的方式控制着记忆单元$c$,如图3所示:

-
-图 3 时刻$t$的lstm +
+图3. 时刻$t$的LSTM

-lstm通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的对于简单循环神经网络的改进还有Gated Recurrent Unit (GRU)\[[8](#参考文献)\],其设计更为简洁一些。**这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断的循环这一过程直至输入处理完毕:** +LSTM通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的改进还有Gated Recurrent Unit (GRU)\[[8](#参考文献)\],其设计更为简洁一些。**这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断地循环这一过程直至输入处理完毕:** $$ h_t=Recrurent(x_t,h_{t-1})$$ 对于正常顺序的循环神经网络,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以通过构建更加强有力的栈式双向循环神经网络,来对时序数据进行建模。 -#### 栈式双向LSTM(Stacked Bidirectional LSTM) -考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建基于lstm的栈式双向循环神经网络\[[9](#参考文献)\]。如图4所示(以三层为例),奇数层lstm正向,偶数层lstm反向,高一层的lstm使用低一层lstm及之前所有层的信息作为输入,对最高层lstm序列使用时间维度上的最大池化即可得到文本的定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 +### 栈式双向LSTM(Stacked Bidirectional LSTM) +考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建基于LSTM的栈式双向循环神经网络\[[9](#参考文献)\]。如图4所示(以三层为例),奇数层LSTM正向,偶数层LSTM反向,高一层的LSTM使用低一层LSTM及之前所有层的信息作为输入,对最高层LSTM序列使用时间维度上的最大池化即可得到文本的定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。


-图 4 栈式双向LSTM用于文本分类 +图4. 栈式双向LSTM用于文本分类

## 数据准备 ### 数据介绍与下载 -我们以IMDB情感分析数据集为例进行介绍。IMDB数据集的训练集和测试集分别包含25000个已标注过的电影评论。其中,负面评论的得分小于等于4,正面评论的得分大于等于7,满分10分。您可以使用下面的脚本下载 IMDB 数椐集和[Moses](http://www.statmt.org/moses/)工具: +我们以[IMDB情感分析数据集](http://ai.stanford.edu/%7Eamaas/data/sentiment/)为例进行介绍。IMDB数据集的训练集和测试集分别包含25000个已标注过的电影评论。其中,负面评论的得分小于等于4,正面评论的得分大于等于7,满分10分。您可以使用下面的脚本下载 IMDB 数椐集和[Moses](http://www.statmt.org/moses/)工具: ```bash -data/get_imdb.sh +./data/get_imdb.sh ``` 如果数椐获取成功,您将在目录```data```中看到下面的文件: @@ -100,23 +102,20 @@ aclImdb get_imdb.sh imdb mosesdecoder-master * mosesdecoder-master: Moses 工具。 ### 数据预处理 -我们只使用已标注的训练集和测试集。将训练集随机打乱排序,默认在训练集上构建字典。Moses 工具中的脚本`tokenizer.perl` 用于切分单词和标点符号。执行下面的命令就可以预处理数椐: +我们使用的预处理脚本为`preprocess.py`。该脚本会调用Moses工具中的`tokenizer.perl`脚本来切分单词和标点符号,并会将训练集随机打乱排序再构建字典。注意:我们只使用已标注的训练集和测试集。执行下面的命令就可以预处理数椐: ``` data_dir="./data/imdb" python preprocess.py -i $data_dir ``` -* data_dir: 输入数椐所在目录。 -* preprocess.py: 预处理脚本。 - 运行成功后目录`./data/pre-imdb` 结构如下: ``` dict.txt labels.list test.list test_part_000 train.list train_part_000 ``` -* test\_part\_000 和 train\_part\_000: 所有标记的测试集和训练集, 训练集已经随机打乱。 +* test\_part\_000 和 train\_part\_000: 所有标记的测试集和训练集,训练集已经随机打乱。 * train.list 和 test.list: 训练集和测试集文件列表。 * dict.txt: 利用训练集生成的字典。 * labels.list: 类别标签列表,标签0表示负面评论,标签1表示正面评论。 @@ -124,8 +123,8 @@ dict.txt labels.list test.list test_part_000 train.list train_part_000 ### 提供数据给PaddlePaddle PaddlePaddle可以读取Python写的传输数据脚本,下面`dataprovider.py`文件给出了完整例子,主要包括两部分: -* hook函数: 定义文本信息、类别Id的数据类型。文本被定义为整数序列`integer_value_sequence`,类别被定义为整数`integer_value`。 -* process函数: 使用yield关键字返回文本信息和类别Id,和hook里定义顺序一致。process读取的文件的行为类别和评论文本,以`'\t\t'`分隔。 +* hook: 定义文本信息、类别Id的数据类型。文本被定义为整数序列`integer_value_sequence`,类别被定义为整数`integer_value`。 +* process: 按行读取以`'\t\t'`分隔的类别ID和文本信息,并用yield关键字返回。 ```python from paddle.trainer.PyDataProvider2 import * @@ -159,22 +158,45 @@ def process(settings, file_name): `trainer_config.py` 是一个配置文件的例子。 ### 数据定义 ```python -define_py_data_sources2( - train_list, - test_list, - module="dataprovider", - obj="process", - args={'dictionary': word_dict}) +from os.path import join as join_path +from paddle.trainer_config_helpers import * +# 是否是测试模式 +is_test = get_config_arg('is_test', bool, False) +# 是否是预测模式 +is_predict = get_config_arg('is_predict', bool, False) + +# 数据路径 +data_dir = "./data/pre-imdb" +# 文件名 +train_list = "train.list" +test_list = "test.list" +dict_file = "dict.txt" + +# 字典大小 +dict_dim = len(open(join_path(data_dir, "dict.txt")).readlines()) +# 类别个数 +class_dim = len(open(join_path(data_dir, 'labels.list')).readlines()) + +if not is_predict: + train_list = join_path(data_dir, train_list) + test_list = join_path(data_dir, test_list) + dict_file = join_path(data_dir, dict_file) + train_list = train_list if not is_test else None + # 构造字典 + word_dict = dict() + with open(dict_file, 'r') as f: + for i, line in enumerate(open(dict_file, 'r')): + word_dict[line.split('\t')[0]] = i + # 通过define_py_data_sources2函数从dataprovider.py中读取数据 + define_py_data_sources2( + train_list, + test_list, + module="dataprovider", + obj="process", # 指定生成数据的函数。 + args={'dictionary': word_dict}) # 额外的参数,这里指定词典。 ``` -在模型配置中利用define_py_data_sources2加载数据: - -* train.list、test.list: 指定训练、测试数据。 -* module="dataprovider": 数据处理Python文件名。 -* obj="process": 指定生成数据的函数。 -* args={"dictionary": word_dict}: 额外的参数,这里指定词典。 - -### 优化算法配置 +### 算法配置 ```python settings( @@ -187,12 +209,12 @@ settings( * 设置batch size大小为128。 * 设置全局学习率。 -* 使用 adam 优化。 +* 使用adam优化。 * 设置L2正则。 * 设置梯度截断(clipping)阈值。 ### 模型结构 -我们用PaddlePaddle分别基于[文本卷积神经网络](#文本卷积神经网络(CNN))和[栈式双向LSTM](#栈式双向LSTM(Stacked Bidirectional LSTM))实现文本分类。 +我们用PaddlePaddle实现了两种文本分类算法,分别基于上文所述的[文本卷积神经网络](#文本卷积神经网络(CNN))和[栈式双向LSTM](#栈式双向LSTM(Stacked Bidirectional LSTM))。 #### 文本卷积神经网络的实现 ```python def convolution_net(input_dim, @@ -242,13 +264,15 @@ def stacked_lstm_net(input_dim, # 激活函数 relu = ReluActivation() linear = LinearActivation() + + # 网络输入:id表示的词序列,词典大小为input_dim data = data_layer("word", input_dim) # 将id表示的词序列映射为embedding序列 emb = embedding_layer(input=data, size=emb_dim) fc1 = fc_layer(input=emb, size=hid_dim, act=linear, bias_attr=bias_attr) - # 基于lstm的循环神经网络 + # 基于LSTM的循环神经网络 lstm1 = lstmemory( input=fc1, act=relu, bias_attr=bias_attr, layer_attr=layer_attr) @@ -288,6 +312,13 @@ def stacked_lstm_net(input_dim, outputs(classification_cost(input=output, label=data_layer('label', 1))) ``` +我们的模型配置`trainer_config.py`默认使用`stacked_lstm_net`网络,如果要使用`convolution_net`,注释相应的行即可。 +```python +stacked_lstm_net( + dict_dim, class_dim=class_dim, stacked_num=3, is_predict=is_predict) +# convolution_net(dict_dim, class_dim=class_dim, is_predict=is_predict) +``` + ## 训练模型 使用`train.sh`脚本可以开启本地的训练: @@ -338,11 +369,11 @@ Test samples=24999 cost=0.39297 Eval: classification_error_evaluator=0.149406 * CurrentEval: classification\_error\_evaluator: 最新log_period个batch的分类错误。 * Pass=0: 通过所有训练集一次称为一个Pass。 0表示第一次经过训练集。 -我们的模型配置`trainer_config.py`默认使用`stacked_lstm_net`网络,如果要使用`convolution_net`,注释相应的行即可。 + ## 应用模型 -### 测试模型 +### 测试 -测试模型是指使用训练出的模型评估已标记的数据集。 +测试是指使用训练出的模型评估已标记的数据集。 ``` ./test.sh @@ -419,6 +450,7 @@ Loading parameters from model_output/pass-00002/ predicting label is pos ``` +`10007_10.txt`在路径`./data/aclImdb/test/pos`下面,而这里预测的标签也是pos,说明预测正确。 ## 总结 本章我们以情感分析为例,介绍了使用深度学习的方法进行端对端的短文本分类,并且使用PaddlePaddle完成了全部相关实验。同时,我们简要介绍了两种文本处理模型:卷积神经网络和循环神经网络。在后续的章节中我们会看到这两种基本的深度学习模型在其它任务上的应用。 ## 参考文献 From 44ed444ba50bf68f3d83ab31797147cfff71306f Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Wed, 11 Jan 2017 15:40:53 +0800 Subject: [PATCH 10/11] according to comments --- understand_sentiment/README.md | 33 +++++++++++++------------- understand_sentiment/trainer_config.py | 3 +-- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index 55f34542..6cc149e9 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -11,24 +11,24 @@

表格 1 电影评论情感分析

-在自然语言处理中,情感分析属于典型的**文本分类**问题,即,把需要进行情感分析的文本划分为其所属类别。文本分类涉及文本表示和分类方法两个问题。在深度学习的方法出现之前,主流的文本表示方法为词袋模型BOW(bag of words),话题模型等等;分类方法有SVM(support vector machine), LR(logistic regression)等等。 +在自然语言处理中,情感分析属于典型的**文本分类**问题,即把需要进行情感分析的文本划分为其所属类别。文本分类涉及文本表示和分类方法两个问题。在深度学习的方法出现之前,主流的文本表示方法为词袋模型BOW(bag of words),话题模型等等;分类方法有SVM(support vector machine), LR(logistic regression)等等。 -BOW假定对于一段文本,忽略其词顺序和语法、句法,将其仅仅看做是一个词集合,它并不能充分表示文本的语义信息。例如,句子“这部电影糟糕透了”和“一个乏味,空洞,没有内涵的作品”在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子“一个空洞,没有内涵的作品”和“一个不空洞而且有内涵的作品”的BOW相似度很高,但实际上它们的意思很不一样。 +对于一段文本,BOW表示会忽略其词顺序、语法和句法,将这段文本仅仅看做是一个词集合,因此BOW方法并不能充分表示文本的语义信息。例如,句子“这部电影糟糕透了”和“一个乏味,空洞,没有内涵的作品”在情感分析中具有很高的语义相似度,但是它们的BOW表示的相似度为0。又如,句子“一个空洞,没有内涵的作品”和“一个不空洞而且有内涵的作品”的BOW相似度很高,但实际上它们的意思很不一样。 本章我们所要介绍的深度学习模型克服了BOW表示的上述缺陷,它在考虑词顺序的基础上把文本映射到低维度的语义空间,并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升\[[1](#参考文献)\]。 ## 模型概览 本章所使用的文本表示模型为卷积神经网络(Convolutional Neural Networks)和循环神经网络(Recurrent Neural Networks)及其扩展。下面依次介绍这几个模型。 ### 文本卷积神经网络(CNN) -卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为二维网格的像素点,自然语言可以视为一维的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示。实验表明,卷积神经网络能高效地对图像及文本问题进行建模处理。本小结我们讲解如何使用卷积神经网络处理文本(以句子为例)\[[1](#参考文献)\]。 +卷积神经网络经常用来处理具有类似网格拓扑结构(grid-like topology)的数据。例如,图像可以视为二维网格的像素点,自然语言可以视为一维的词序列。卷积神经网络可以提取多种局部特征,并对其进行组合抽象得到更高级的特征表示。实验表明,卷积神经网络能高效地对图像及文本问题进行建模处理。 + +卷积神经网络主要由卷积(convolution)和池化(pooling)操作构成,其应用及组合方式灵活多变,种类繁多。本小结我们以一种简单的文本分类卷积神经网络为例进行讲解\[[1](#参考文献)\],如图1所示:


图1. 卷积神经网络文本分类模型

-假设一个句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,$k$为维度大小。我们可以将整个句子表示为$x_{1:n}=x_1\oplus x_2\oplus \ldots \oplus x_n$,其中,$\oplus$表示拼接(concatenation)操作。一般地,我们用$x_{i:i+j}$表示词序列$x_{i},x_{i+1},\ldots,x_{i+j}$的拼接。卷积操作把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i$: - -$$c_i=f(w\cdot x_{i:i+h-1}+b)$$ +假设一个句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,$k$为维度大小。首先我们进行词向量的拼接操作:将每$h$个词拼接起来形成一个大小为$h$的词窗口,记为$x_{i:i+h-1}$,它表示词序列$x_{i},x_{i+1},\ldots,x_{i+h-1}$的拼接,其中,$i$表示词窗口中第一个词在整个句子中的位置,取值范围从$1$到$n-h+1$,$x_{i:i+h-1}\in\mathbb{R}^{hk}$。 -其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如$sigmoid$。将卷积核应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$,产生一个特征图(feature map): +其次我们进行卷积操作:把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i=f(w\cdot x_{i:i+h-1}+b)$,其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如$sigmoid$。将卷积核应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$,产生一个特征图(feature map): $$c=[c_1,c_2,\ldots,c_{n-h+1}], c \in \mathbb{R}^{n-h+1}$$ @@ -36,11 +36,11 @@ $$c=[c_1,c_2,\ldots,c_{n-h+1}], c \in \mathbb{R}^{n-h+1}$$ $$\hat c=max(c)$$ -在实际应用中,我们会使用多个卷积核来处理句子,窗口大小相同的卷积核堆叠起来形成一个矩阵(上文中的单个卷积核参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的卷积核来处理句子,最后,将所有卷积核得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。图1是使用卷积神经网络进行文本分类的一个示意图(只画了四个卷积核,黄色的卷积核窗口大小为3,红色的为2)。 +在实际应用中,我们会使用多个卷积核来处理句子,窗口大小相同的卷积核堆叠起来形成一个矩阵(上文中的单个卷积核参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的卷积核来处理句子(图1作为示意画了四个卷积核,黄色的卷积核窗口大小为3,红色的为2),最后,将所有卷积核得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 对于一般的短文本分类问题,上文所述的简单的文本卷积网络即可达到很高的正确率\[[1](#参考文献)\]。若想得到更抽象更高级的文本特征表示,可以构建深层文本卷积神经网络\[[2](#参考文献),[3](#参考文献)\]。 ### 循环神经网络(RNN) -循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的\[[4](#参考文献)\]。自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory\[[5](#参考文献)\]等)在自然语言处理的多个领域,如语言模型、句法解析、语义角色标注(或一般的序列标注)、语义表示、图文生成、对话、机器翻译等任务上均表现优异甚至成为目前效果最好的方法。 +循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的\[[4](#参考文献)\]。自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory\[[5](#参考文献)\]等)在自然语言处理的多个领域,如语言模型、句法解析、语义角色标注(或一般的序列标注)、语义表示、图文生成、对话、机器翻译等任务上均表现优异甚至成为目前效果最好的方法。


图2. 循环神经网络按时间展开的示意图 @@ -53,7 +53,7 @@ $$h_t=f(x_t,h_{t-1})=\sigma(W_{xh}x_t+W_{hh}h_{h-1}+b_h)$$ 在处理自然语言时,一般会先将词(one-hot表示)映射为其词向量(word embedding)表示,然后再作为循环神经网络每一时刻的输入$x_t$。此外,可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。 -### 长短期记忆(LSTM) +### 长短期记忆网络(LSTM) 对于较长的序列数据,循环神经网络的训练过程中容易出现梯度消失或爆炸现象\[[6](#参考文献)\]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了LSTM(long short term memory\[[5](#参考文献)\])。 相比于简单的循环神经网络,LSTM增加了记忆单元$c$、输入门$i$、遗忘门$f$及输出门$o$。这些门及记忆单元组合起来大大提升了循环神经网络处理长序列数据的能力。若将基于LSTM的循环神经网络表示的函数记为$F$,则其公式为: @@ -73,13 +73,13 @@ h_t & = o_t\odot tanh(c_t)\\\\
图3. 时刻$t$的LSTM

-LSTM通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的改进还有Gated Recurrent Unit (GRU)\[[8](#参考文献)\],其设计更为简洁一些。**这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断地循环这一过程直至输入处理完毕:** +LSTM通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的改进还有Gated Recurrent Unit (GRU)\[[8](#参考文献)\],其设计更为简洁一些。**这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断地循环这一过程直至输入处理完毕:** $$ h_t=Recrurent(x_t,h_{t-1})$$ -对于正常顺序的循环神经网络,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法,我们可以通过构建更加强有力的栈式双向循环神经网络,来对时序数据进行建模。 +其中,$Recrurent$可以表示简单的循环神经网络、GRU或LSTM。 ### 栈式双向LSTM(Stacked Bidirectional LSTM) -考虑到深层神经网络往往能得到更抽象和高级的特征表示,我们构建基于LSTM的栈式双向循环神经网络\[[9](#参考文献)\]。如图4所示(以三层为例),奇数层LSTM正向,偶数层LSTM反向,高一层的LSTM使用低一层LSTM及之前所有层的信息作为输入,对最高层LSTM序列使用时间维度上的最大池化即可得到文本的定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 +对于正常顺序的循环神经网络,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法(深层神经网络往往能得到更抽象和高级的特征表示),我们可以通过构建更加强有力的基于LSTM的栈式双向循环神经网络\[[9](#参考文献)\],来对时序数据进行建模。如图4所示(以三层为例),奇数层LSTM正向,偶数层LSTM反向,高一层的LSTM使用低一层LSTM及之前所有层的信息作为输入,对最高层LSTM序列使用时间维度上的最大池化即可得到文本的定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。


图4. 栈式双向LSTM用于文本分类 @@ -252,12 +252,12 @@ def stacked_lstm_net(input_dim, stacked_num=3, is_predict=False): - hid_lr = 1e-3 + # LSTM的层数stacked_num为奇数,确保最高层LSTM正向 assert stacked_num % 2 == 1 # 设置神经网络层的属性 layer_attr = ExtraLayerAttribute(drop_rate=0.5) # 设置参数的属性 - fc_para_attr = ParameterAttribute(learning_rate=hid_lr) + fc_para_attr = ParameterAttribute(learning_rate=1e-3) lstm_para_attr = ParameterAttribute(initial_std=0., learning_rate=1.) para_attr = [fc_para_attr, lstm_para_attr] bias_attr = ParameterAttribute(initial_std=0., l2_rate=0.) @@ -419,7 +419,7 @@ Pass=0 samples=24999 AvgCost=0.280471 Eval: classification_error_evaluator=0.115 ``` ./predict.sh ``` -predict.sh: +predict.sh的内容如下(注意应该确保默认模型路径`model_output/pass-00002`存在或更改为其它模型路径): ```bash model=model_output/pass-00002/ @@ -441,7 +441,6 @@ cat ./data/aclImdb/test/pos/10007_10.txt | python predict.py \ * `--dict=data/pre-imdb/dict.txt` : 设置文本数据字典文件。 * `--batch_size=1` : 预测时的batch size大小。 -注意应该确保默认模型路径`model_output/pass-00002`存在或更改为其它模型路径。 本示例的预测结果: diff --git a/understand_sentiment/trainer_config.py b/understand_sentiment/trainer_config.py index 8b426d9c..9b9b9863 100644 --- a/understand_sentiment/trainer_config.py +++ b/understand_sentiment/trainer_config.py @@ -97,11 +97,10 @@ def stacked_lstm_net(input_dim, is_predict: is predicting or not. Some layers is not needed in network when predicting. """ - hid_lr = 1e-3 assert stacked_num % 2 == 1 layer_attr = ExtraLayerAttribute(drop_rate=0.5) - fc_para_attr = ParameterAttribute(learning_rate=hid_lr) + fc_para_attr = ParameterAttribute(learning_rate=1e-3) lstm_para_attr = ParameterAttribute(initial_std=0., learning_rate=1.) para_attr = [fc_para_attr, lstm_para_attr] bias_attr = ParameterAttribute(initial_std=0., l2_rate=0.) From c164305d2750683f9fad8da9c35440ed5a7aeab7 Mon Sep 17 00:00:00 2001 From: wangxuguang Date: Wed, 11 Jan 2017 16:37:09 +0800 Subject: [PATCH 11/11] finished --- understand_sentiment/README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/understand_sentiment/README.md b/understand_sentiment/README.md index 6cc149e9..f3527427 100644 --- a/understand_sentiment/README.md +++ b/understand_sentiment/README.md @@ -26,17 +26,21 @@
图1. 卷积神经网络文本分类模型

-假设一个句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,$k$为维度大小。首先我们进行词向量的拼接操作:将每$h$个词拼接起来形成一个大小为$h$的词窗口,记为$x_{i:i+h-1}$,它表示词序列$x_{i},x_{i+1},\ldots,x_{i+h-1}$的拼接,其中,$i$表示词窗口中第一个词在整个句子中的位置,取值范围从$1$到$n-h+1$,$x_{i:i+h-1}\in\mathbb{R}^{hk}$。 +假设待处理句子的长度为$n$,其中第$i$个词的词向量(word embedding)为$x_i\in\mathbb{R}^k$,$k$为维度大小。 -其次我们进行卷积操作:把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i=f(w\cdot x_{i:i+h-1}+b)$,其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如$sigmoid$。将卷积核应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$,产生一个特征图(feature map): +首先,进行词向量的拼接操作:将每$h$个词拼接起来形成一个大小为$h$的词窗口,记为$x_{i:i+h-1}$,它表示词序列$x_{i},x_{i+1},\ldots,x_{i+h-1}$的拼接,其中,$i$表示词窗口中第一个词在整个句子中的位置,取值范围从$1$到$n-h+1$,$x_{i:i+h-1}\in\mathbb{R}^{hk}$。 + +其次,进行卷积操作:把卷积核(kernel)$w\in\mathbb{R}^{hk}$应用于包含$h$个词的窗口$x_{i:i+h-1}$,得到特征$c_i=f(w\cdot x_{i:i+h-1}+b)$,其中$b\in\mathbb{R}$为偏置项(bias),$f$为非线性激活函数,如$sigmoid$。将卷积核应用于句子中所有的词窗口${x_{1:h},x_{2:h+1},\ldots,x_{n-h+1:n}}$,产生一个特征图(feature map): $$c=[c_1,c_2,\ldots,c_{n-h+1}], c \in \mathbb{R}^{n-h+1}$$ -接下来我们对特征图采用时间维度上的最大池化(max pooling over time)操作得到此卷积核对应的整句话的特征$\hat c$,它是特征图中所有元素的最大值: +接下来,对特征图采用时间维度上的最大池化(max pooling over time)操作得到此卷积核对应的整句话的特征$\hat c$,它是特征图中所有元素的最大值: $$\hat c=max(c)$$ -在实际应用中,我们会使用多个卷积核来处理句子,窗口大小相同的卷积核堆叠起来形成一个矩阵(上文中的单个卷积核参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的卷积核来处理句子(图1作为示意画了四个卷积核,黄色的卷积核窗口大小为3,红色的为2),最后,将所有卷积核得到的特征拼接起来即为文本的定长向量表示。对于文本分类问题,将其连接至softmax即构建出完整的模型。 +在实际应用中,我们会使用多个卷积核来处理句子,窗口大小相同的卷积核堆叠起来形成一个矩阵(上文中的单个卷积核参数$w$相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的卷积核来处理句子(图1作为示意画了四个卷积核,不同颜色表示不同大小的卷积核操作)。 + +最后,将所有卷积核得到的特征拼接起来即为文本的定长向量表示,对于文本分类问题,将其连接至softmax即构建出完整的模型。 对于一般的短文本分类问题,上文所述的简单的文本卷积网络即可达到很高的正确率\[[1](#参考文献)\]。若想得到更抽象更高级的文本特征表示,可以构建深层文本卷积神经网络\[[2](#参考文献),[3](#参考文献)\]。 ### 循环神经网络(RNN) @@ -71,7 +75,7 @@ h_t & = o_t\odot tanh(c_t)\\\\ 其中,$i_t, f_t, c_t, o_t$分别表示输入门,遗忘门,记忆单元及输出门的向量值,带角标的$W$及$b$为模型参数,$tanh$为双曲正切函数,$\odot$表示逐元素(elementwise)的乘法操作。输入门控制着新输入进入记忆单元$c$的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,它们各自以不同的方式控制着记忆单元$c$,如图3所示:


-图3. 时刻$t$的LSTM +图3. 时刻$t$的LSTM\[[7](#参考文献)\]

LSTM通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的改进还有Gated Recurrent Unit (GRU)\[[8](#参考文献)\],其设计更为简洁一些。**这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断地循环这一过程直至输入处理完毕:** @@ -79,7 +83,9 @@ $$ h_t=Recrurent(x_t,h_{t-1})$$ 其中,$Recrurent$可以表示简单的循环神经网络、GRU或LSTM。 ### 栈式双向LSTM(Stacked Bidirectional LSTM) -对于正常顺序的循环神经网络,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法(深层神经网络往往能得到更抽象和高级的特征表示),我们可以通过构建更加强有力的基于LSTM的栈式双向循环神经网络\[[9](#参考文献)\],来对时序数据进行建模。如图4所示(以三层为例),奇数层LSTM正向,偶数层LSTM反向,高一层的LSTM使用低一层LSTM及之前所有层的信息作为输入,对最高层LSTM序列使用时间维度上的最大池化即可得到文本的定长向量表示。**这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象。**最后我们将文本表示连接至softmax构建分类模型。 +对于正常顺序的循环神经网络,$h_t$包含了$t$时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法(深层神经网络往往能得到更抽象和高级的特征表示),我们可以通过构建更加强有力的基于LSTM的栈式双向循环神经网络\[[9](#参考文献)\],来对时序数据进行建模。 + +如图4所示(以三层为例),奇数层LSTM正向,偶数层LSTM反向,高一层的LSTM使用低一层LSTM及之前所有层的信息作为输入,对最高层LSTM序列使用时间维度上的最大池化即可得到文本的定长向量表示(这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象),最后我们将文本表示连接至softmax构建分类模型。


图4. 栈式双向LSTM用于文本分类