本人仅以 PaddlePaddle 深度学习 101 官网教程为指导,添加个人理解和笔记,仅作为学习练习使用,若有错误,还望批评指教。–ZJ
原文地址: PaddlePaddle 官网| PaddlePaddle 深度学习 101
环境:
- Python 2.7
- Ubuntu 16.04
本教程源代码目录在book/understand_sentiment, 初次使用请参考PaddlePaddle安装教程,更多内容请参考本教程的视频课堂。
在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤);也可以是三类,如(积极,消极,中性)等等。情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论;或为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。表格1 展示了对电影评论进行情感分析的例子:
电影评论 | 类别 |
---|---|
在冯小刚这几年的电影里,算最好的一部的了 | 正面 |
很不好看,好像一个地方台的电视剧 | 负面 |
圆方镜头全程炫技,色调背景美则美矣,但剧情拖沓,口音不伦不类,一直努力却始终无法入戏 | 负面 |
剧情四星。但是圆镜视角加上婺源的风景整个非常有中国写意山水画的感觉,看得实在太舒服了。。 | 正面 |
表格 1 电影评论情感分析
在自然语言处理中,情感分析属于典型的**文本分类**问题,即把需要进行情感分析的文本划分为其所属类别。文本分类涉及文本表示和分类方法两个问题。 **在深度学习的方法出现之前,**主流的文本表示方法为词袋模型 BOW (bag of words),话题模型等等;分类方法有 SVM (support vector machine), LR(logistic regression)等等。 对于一段文本, BOW 表示会忽略其词顺序、语法和句法,将这段文本仅仅看做是一个词集合,因此 BOW 方法并不能充分表示文本的语义信息。例如,句子“这部电影糟糕透了”和“一个乏味,空洞,没有内涵的作品”在情感分析中具有很高的语义相似度,但是它们的 BOW 表示的相似度为 0。又如,句子“一个空洞,没有内涵的作品”和“一个不空洞而且有内涵的作品”的 BOW 相似度很高,但实际上它们的意思很不一样。 本章我们所要介绍的深度学习模型克服了 BOW 表示的上述缺陷,它在考虑词顺序的基础上把文本映射到低维度的语义空间,**并且以端对端(end to end)的方式进行文本表示及分类,其性能相对于传统方法有显著的提升**循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的[4]。自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如 long short term memory [5]等)在自然语言处理的多个领域,如语言模型、句法解析、语义角色标注(或一般的序列标注)、语义表示、图文生成、对话、机器翻译等任务上均表现优异甚至成为目前效果最好的方法。
循环神经网络按时间展开后如图1 所示:在第 t t 时刻,网络读入第 t t 个输入 xt x t (向量表示)及前一时刻隐层的状态值 ht−1 h t − 1 (向量表示, h0 h 0 一般初始化为 0 0 向量),计算得出本时刻隐层的状态值 ht h t ,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为 f f ,则其公式可表示为:
其中 Wxh W x h 是输入到隐层的矩阵参数, Whh W h h 是隐层到隐层的矩阵参数, bh b h 为隐层的偏置向量(bias)参数, σ σ 为 sigmoid s i g m o i d 函数。
在处理自然语言时,一般会先将词(one-hot 表示)映射为其词向量(word embedding)表示,然后再作为循环神经网络每一时刻的输入 xt x t 。此外,可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。
对于较长的序列数据,循环神经网络的训练过程中容易出现梯度消失或爆炸现象[6]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了 LSTM (long short term memory[5])。
相比于简单的循环神经网络, LSTM 增加了记忆单元 c c 、输入门 i i 、遗忘门 f f 及输出门 o o 。这些门及记忆单元组合起来大大提升了循环神经网络处理长序列数据的能力。若将基于 LSTM 的循环神经网络表示的函数记为 F F ,则其公式为:
F F 由下列公式组合而成[7]:
其中, it,ft,ct,ot i t , f t , c t , o t 分别表示输入门,遗忘门,记忆单元及输出门的向量值,带角标的 W W 及 b b 为模型参数, tanh t a n h 为双曲正切函数, ⊙ ⊙ 表示逐元素(elementwise)的乘法操作。输入门控制着新输入进入记忆单元 c c 的强度,遗忘门控制着记忆单元维持上一时刻值的强度,输出门控制着输出记忆单元的强度。三种门的计算方式类似,但有着完全不同的参数,它们各自以不同的方式控制着记忆单元 c c ,如图2所示:
LSTM 通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的改进还有 Gated Recurrent Unit (GRU) [8],其设计更为简洁一些。这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断地循环这一过程直至输入处理完毕:
其中, Recrurent R e c r u r e n t 可以表示简单的循环神经网络、GRU 或 LSTM 。
对于正常顺序的循环神经网络, ht h t 包含了 t t 时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法(深层神经网络往往能得到更抽象和高级的特征表示),我们可以通过构建更加强有力的基于 LSTM 的栈式双向循环神经网络[9],来对时序数据进行建模。
如图3所示(以三层为例),奇数层 LSTM 正向,偶数层 LSTM 反向,高一层的 LSTM 使用低一层 LSTM 及之前所有层的信息作为输入,对最高层 LSTM 序列使用时间维度上的最大池化即可得到文本的定长向量表示(这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象),最后我们将文本表示连接至 softmax 构建分类模型。
图3. 栈式双向 LSTM 用于文本分类
我们以IMDB情感分析数据集为例进行介绍。IMDB 数据集的训练集和测试集分别包含 25000 个已标注过的电影评论。其中,负面评论的得分小于等于 4,正面评论的得分大于等于 7,满分 10 分。
aclImdb
|- test
|-- neg
|-- pos
|- train
|-- neg
|-- pos
Paddle在dataset/imdb.py
中提实现了 imdb 数据集的自动下载和读取,并提供了读取字典、训练数据、测试数据等 API。
import sys
import paddle.v2 as paddle
在该示例中,我们实现了两种文本分类算法,分别基于推荐系统一节介绍过的文本卷积神经网络,以及[栈式双向 LSTM ](#栈式双向 LSTM (Stacked Bidirectional LSTM ))。
def convolution_net(input_dim, class_dim=2, emb_dim=128, hid_dim=128, is_predict=False):
data = paddle.layer.data("word",
paddle.data_type.integer_value_sequence(input_dim))
emb = paddle.layer.embedding(input=data, size=emb_dim)
conv_3 = paddle.networks.sequence_conv_pool(
input=emb, context_len=3, hidden_size=hid_dim)
conv_4 = paddle.networks.sequence_conv_pool(
input=emb, context_len=4, hidden_size=hid_dim)
output = paddle.layer.fc(input=[conv_3, conv_4],
size=class_dim,
act=paddle.activation.Softmax())
if not is_predict:
lbl = paddle.layer.data("label", paddle.data_type.integer_value(2))
cost = paddle.layer.classification_cost(input=output, label=lbl)
return cost
else:
return output
网络的输入input_dim
表示的是词典的大小,class_dim
表示类别数。这里,我们使用sequence_conv_pool
API实现了卷积和池化操作。
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. """
assert stacked_num % 2 == 1
fc_para_attr = paddle.attr.Param(learning_rate=1e-3)
LSTM _para_attr = paddle.attr.Param(initial_std=0., learning_rate=1.)
para_attr = [fc_para_attr, LSTM _para_attr]
bias_attr = paddle.attr.Param(initial_std=0., l2_rate=0.)
relu = paddle.activation.Relu()
linear = paddle.activation.Linear()
data = paddle.layer.data("word",
paddle.data_type.integer_value_sequence(input_dim))
emb = paddle.layer.embedding(input=data, size=emb_dim)
fc1 = paddle.layer.fc(input=emb,
size=hid_dim,
act=linear,
bias_attr=bias_attr)
LSTM 1 = paddle.layer. LSTM emory(
input=fc1, act=relu, bias_attr=bias_attr)
inputs = [fc1, LSTM 1]
for i in range(2, stacked_num + 1):
fc = paddle.layer.fc(input=inputs,
size=hid_dim,
act=linear,
param_attr=para_attr,
bias_attr=bias_attr)
LSTM = paddle.layer. LSTM emory(
input=fc,
reverse=(i % 2) == 0,
act=relu,
bias_attr=bias_attr)
inputs = [fc, LSTM ]
fc_last = paddle.layer.pooling(input=inputs[0], pooling_type=paddle.pooling.Max())
LSTM _last = paddle.layer.pooling(input=inputs[1], pooling_type=paddle.pooling.Max())
output = paddle.layer.fc(input=[fc_last, LSTM _last],
size=class_dim,
act=paddle.activation.Softmax(),
bias_attr=bias_attr,
param_attr=para_attr)
if not is_predict:
lbl = paddle.layer.data("label", paddle.data_type.integer_value(2))
cost = paddle.layer.classification_cost(input=output, label=lbl)
return cost
else:
return output
网络的输入stacked_num
表示的是 LSTM 的层数,需要是奇数,确保最高层 LSTM 正向。Paddle 里面是通过一个 fc 和一个 LSTMemory 来实现基于 LSTM 的循环神经网络。
if __name__ == '__main__':
# init
paddle.init(use_gpu=False)
启动 paddle 程序,use_gpu=False 表示用 CPU 训练,如果系统支持 GPU 也可以修改成 True 使用 GPU 训练。
使用 Paddle 提供的数据集dataset.imdb
中的 API 来读取训练数据。
print 'load dictionary...'
word_dict = paddle.dataset.imdb.word_dict()
dict_dim = len(word_dict)
class_dim = 2
加载数据字典,这里通过word_dict()
API可以直接构造字典。class_dim
是指样本类别数,该示例中样本只有正负两类。
train_reader = paddle.batch(
paddle.reader.shuffle(
lambda: paddle.dataset.imdb.train(word_dict), buf_size=1000),
batch_size=100)
test_reader = paddle.batch(
lambda: paddle.dataset.imdb.test(word_dict),
batch_size=100)
这里,dataset.imdb.train()
和 dataset.imdb.test()
分别是 dataset.imdb
中的训练数据和测试数据 API。train_reader
在训练时使用,意义是将读取的训练数据进行 shuffle 后,组成一个 batch 数据。同理,test_reader
是在测试的时候使用,将读取的测试数据组成一个 batch。
feeding={'word': 0, 'label': 1}
feeding
用来指定train_reader
和test_reader
返回的数据与模型配置中 data_layer 的对应关系。这里表示 reader 返回的第 0 列数据对应word
层,第1列数据对应label
层。
# Please choose the way to build the network
# by uncommenting the corresponding line.
cost = convolution_net(dict_dim, class_dim=class_dim)
# cost = stacked_ LSTM _net(dict_dim, class_dim=class_dim, stacked_num=3)
该示例中默认使用convolution_net
网络,如果使用stacked_ LSTM _net
网络,注释相应的行即可。其中 cost 是网络的优化目标,同时 cost 包含了整个网络的拓扑信息。
# create parameters
parameters = paddle.parameters.create(cost)
根据网络的拓扑构造网络参数。这里 parameters 是整个网络的参数集。
# create optimizer
adam_optimizer = paddle.optimizer.Adam(
learning_rate=2e-3,
regularization=paddle.optimizer.L2Regularization(rate=8e-4),
model_average=paddle.optimizer.ModelAverage(average_window=0.5))
Paddle中提供了一系列优化算法的API,这里使用 Adam 优化算法。
可以通过paddle.trainer.SGD
构造一个 sgd trainer,并调用trainer.train
来训练模型。另外,通过给 train 函数传递一个event_handler
来获取每个batch 和每个 pass 结束的状态。
# End batch and end pass event handler
def event_handler(event):
if isinstance(event, paddle.event.EndIteration):
if event.batch_id % 100 == 0:
print "\nPass %d, Batch %d, Cost %f, %s" % (
event.pass_id, event.batch_id, event.cost, event.metrics)
else:
sys.stdout.write('.')
sys.stdout.flush()
if isinstance(event, paddle.event.EndPass):
with open('./params_pass_%d.tar' % event.pass_id, 'w') as f:
trainer.save_parameter_to_tar(f)
result = trainer.test(reader=test_reader, feeding=feeding)
print "\nTest with Pass %d, %s" % (event.pass_id, result.metrics)
比如,构造如下一个event_handler
可以在每 100 个 batch 结束后输出 cost 和 error;在每个 pass 结束后调用trainer.test
计算一遍测试集并获得当前模型在测试集上的 error。
from paddle.v2.plot import Ploter
train_title = "Train cost"
cost_ploter = Ploter(train_title)
step = 0
def event_handler_plot(event):
global step
if isinstance(event, paddle.event.EndIteration):
cost_ploter.append(train_title, step, event.cost)
cost_ploter.plot()
step += 1
或者构造一个event_handler_plot
画出 cost曲线。
# create trainer
trainer = paddle.trainer.SGD(cost=cost,
parameters=parameters,
update_equation=adam_optimizer)
trainer.train(
reader=train_reader,
event_handler=event_handler,
feeding=feeding,
num_passes=2)
程序运行之后的输出如下。
Pass 0, Batch 0, Cost 0.693721, {'classification_error_evaluator': 0.5546875}
...................................................................................................
Pass 0, Batch 100, Cost 0.294321, {'classification_error_evaluator': 0.1015625}
...............................................................................................
Test with Pass 0, {'classification_error_evaluator': 0.11432000249624252}
可以使用训练好的模型对电影评论进行分类,下面程序展示了如何使用paddle.infer
接口进行推断。
import numpy as np
# Movie Reviews, from imdb test
reviews = [
'Read the book, forget the movie!',
'This is a great movie.'
]
reviews = [c.split() for c in reviews]
UNK = word_dict['' ]
input = []
for c in reviews:
input.append([[word_dict.get(words, UNK) for words in c]])
# 0 stands for positive sample, 1 stands for negative sample
label = {0:'pos', 1:'neg'}
# Use the network used by trainer
out = convolution_net(dict_dim, class_dim=class_dim, is_predict=True)
# out = stacked_ LSTM _net(dict_dim, class_dim=class_dim, stacked_num=3, is_predict=True)
probs = paddle.infer(output_layer=out, parameters=parameters, input=input)
labs = np.argsort(-probs)
for idx, lab in enumerate(labs):
print idx, "predicting probability is", probs[idx], "label is", label[lab[0]]
本章我们以情感分析为例,介绍了使用深度学习的方法进行端对端的短文本分类,并且使用 PaddlePaddle 完成了全部相关实验。同时,我们简要介绍了两种文本处理模型:卷积神经网络和循环神经网络。在后续的章节中我们会看到这两种基本的深度学习模型在其它任务上的应用。
本教程 由 PaddlePaddle 创作,采用 知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。