diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2edd051db681da02979e05411225cd07c3bb76f4..0d8234b69cb092a25eb884a754600168f9a67f75 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,7 +10,6 @@
- repo: https://github.com/pre-commit/pre-commit-hooks
sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469
hooks:
- - id: check-added-large-files
- id: check-merge-conflict
- id: check-symlinks
- id: detect-private-key
diff --git a/README.md b/README.md
index 900a6b7e92de5879b30c669beb04982831341f7a..5c71bb2c44ea7827af081bf8e5edf8fb17c8c643 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,13 @@
# 深度学习入门
-1. 新手入门[[.html](fit_a_line/README.html)] [[.pdf](fit_a_line/README.pdf)] [[.src](fit_a_line/)]
-1. 个性化推荐[[.html](recommender_system/README.html)] [[.pdf](recommender_system/README.pdf)] [[.src](recommender_system/)]
-1. 识别数字[[.html](recognize_digits/README.html)] [[.pdf](recognize_digits/README.pdf)] [[.src](recognize_digits/)]
-1. 图像分类[[.html](classify_images/README.html)] [[.pdf](classify_images/README.pdf)] [[.src](classify_images/)]
-1. 词向量[[.html](word2vec/README.html)] [[.pdf](word2vec/)] [[.src](word2vec/README.pdf)]
-1. 情感分析[[.html](understand_sentiment/README.html)] [[.pdf](understand_sentiment/README.pdf)] [[.src](understand_sentiment/)]
-1. 理解单词的语义[[.html](label_semantic_roles/README.html)] [[.pdf](label_semantic_roles/README.pdf)] [[.src](label_semantic_roles/)]
-1. 机器翻译[[.html](machine_translation/README.html)] [[.pdf](machine_translation/README.pdf)] [[.src](machine_translation/)]
+1. 新手入门 [[.html](http://htmlpreview.github.io/?https://github.com/PaddlePaddle/book/tree/develop/fit_a_line/README.html)] [[.pdf](fit_a_line/README.pdf)] [[.src](fit_a_line/)]
+1. 个性化推荐 [[.html](http://htmlpreview.github.io/?https://github.com/PaddlePaddle/book/tree/develop/recommender_system/README.html)] [[.pdf](recommender_system/README.pdf)] [[.src](recommender_system/)]
+1. 识别数字 [[.html](http://htmlpreview.github.io/?https://github.com/PaddlePaddle/book/tree/develop/recognize_digits/README.html)] [[.pdf](recognize_digits/README.pdf)] [[.src](recognize_digits/)]
+1. 图像分类 [[.html](http://htmlpreview.github.io/?https://github.com/PaddlePaddle/book/tree/develop/image_classification/README.html)] [[.pdf](image_classification/README.pdf)] [[.src](classify_images/)]
+1. 词向量 [[.html](http://htmlpreview.github.io/?https://github.com/PaddlePaddle/book/tree/develop/word2vec/README.html)] [[.pdf](word2vec/README.pdf)] [[.src](word2vec/)]
+1. 情感分析 [[.html](http://htmlpreview.github.io/?https://github.com/PaddlePaddle/book/tree/develop/understand_sentiment/README.html)] [[.pdf](understand_sentiment/README.pdf)] [[.src](understand_sentiment/)]
+1. 理解单词的语义 [[.html](http://htmlpreview.github.io/?https://github.com/PaddlePaddle/book/tree/develop/label_semantic_roles/README.html)] [[.pdf](label_semantic_roles/README.pdf)] [[.src](label_semantic_roles/)]
+1. 机器翻译 [[.html](http://htmlpreview.github.io/?https://github.com/PaddlePaddle/book/tree/develop/machine_translation/README.html)] [[.pdf](machine_translation/README.pdf)] [[.src](machine_translation/)]
+
本教程 由 PaddlePaddle 创作,采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
diff --git a/fit_a_line/README.html b/fit_a_line/README.html
new file mode 100644
index 0000000000000000000000000000000000000000..6c52a5b844c8b3d737bd33bb70a247c421a71be4
--- /dev/null
+++ b/fit_a_line/README.html
@@ -0,0 +1,34 @@
+
+
+
让我们从经典的线性回归(Linear Regression [1])模型开始这份教程。在这一章里,你将使用真实的数据集建立起一个房价预测模型,并且了解到机器学习中的若干重要概念。
给定一个大小为的数据集 ,其中是第个样本个属性上的取值,是该样本待预测的目标。线性回归模型假设目标可以被属性间的线性组合描述,即
例如,在我们将要建模的房价预测问题里,是描述房子的各种属性(比如房间的个数、周围学校和医院的个数、交通状况等),而 是房屋的价格。
初看起来,这个假设实在过于简单了,变量间的真实关系很难是线性的。但由于线性回归模型有形式简单和易于建模分析的优点,它在实际问题中得到了大量的应用。很多经典的统计学习、机器学习书籍[2,3,4]也选择对线性模型独立成章重点讲解。
我们使用从UCI Housing Data Set获得的波士顿房价数据集进行模型的训练和预测。下面的散点图展示了使用模型对部分房屋价格进行的预测。其中,每个点的横坐标表示同一类房屋真实价格的中位数,纵坐标表示线性回归模型根据特征预测的结果,当二者值完全相等的时候就会落在虚线上。所以模型预测得越准确,则点离虚线越近。
+
+ 图1. 预测值 V.S. 真实值
+
在波士顿房价数据集中,和房屋相关的值共有14个:前13个用来描述房屋相关的各种信息,即模型中的 ;最后一个值为我们要预测的该类房屋价格的中位数,即模型中的 。因此,我们的模型就可以表示成:
表示模型的预测结果,用来和真实值区分。模型要学习的参数即:。
建立模型后,我们需要给模型一个优化目标,使得学到的参数能够让预测值尽可能地接近真实值。这里我们引入损失函数(Loss Function,或Cost Function)这个概念。 输入任意一个数据样本的目标值和模型给出的预测值,损失函数输出一个非负的实值。这个实质通常用来反映模型误差的大小。
对于线性回归模型来讲,最常见的损失函数就是均方误差(Mean Squared Error, MSE)了,它的形式是:
即对于一个大小为的测试集,是个数据预测结果误差平方的均值。
定义好模型结构之后,我们要通过以下几个步骤进行模型训练
+ 1. 初始化参数,其中包括权重和偏置,对其进行初始化(如0均值,1方差)。
+ 2. 网络正向传播计算网络输出和损失函数。
+ 3. 根据损失函数进行反向误差传播 (backpropagation),将网络误差从输出层依次向前传递, 并更新网络中的参数。
+ 4. 重复2~3步骤,直至网络训练误差达到规定的程度或训练轮次达到设定值。
执行以下命令来准备数据:
cd data && python prepare_data.py
+
+ 图2. 各维属性的取值范围
+
我们将数据集分割为两份:一份用于调整模型的参数,即进行模型的训练,模型在这份数据集上的误差被称为训练误差;另外一份被用来测试,模型在这份数据集上的误差被称为测试误差。我们训练模型的目的是为了通过从训练数据中找到规律来预测未知的新数据,所以测试误差是更能反映模型表现的指标。分割数据的比例要考虑到两个因素:更多的训练数据会降低参数估计的方差,从而得到更可信的模型;而更多的测试数据会降低测试误差的方差,从而得到更可信的测试误差。一种常见的分割比例为,感兴趣的读者朋友们也可以尝试不同的设置来观察这两种误差的变化。
执行如下命令可以分割数据集,并将训练集和测试集的地址分别写入train.list 和 test.list两个文件中,供PaddlePaddle读取。
python prepare_data.py -r 0.8 #默认使用8:2的比例进行分割
在更复杂的模型训练过程中,我们往往还会多使用一种数据集:验证集。因为复杂的模型中常常还有一些超参数(Hyperparameter)需要调节,所以我们会尝试多种超参数的组合来分别训练多个模型,然后对比它们在验证集上的表现选择相对最好的一组超参数,最后才使用这组参数下训练的模型在测试集上评估测试误差。由于本章训练的模型比较简单,我们暂且忽略掉这个过程。
准备好数据之后,我们使用一个Python data provider来为PaddlePaddle的训练过程提供数据。一个 data provider 就是一个Python函数,它会被PaddlePaddle的训练过程调用。在这个例子里,只需要读取已经保存好的数据,然后一行一行地返回给PaddlePaddle的训练进程即可。
from paddle.trainer.PyDataProvider2 import *
import numpy as np
#定义数据的类型和维度
@provider(input_types=[dense_vector(13), dense_vector(1)])
def process(settings, input_file):
data = np.load(input_file.strip())
for row in data:
yield row[:-1].tolist(), row[-1:].tolist()
首先,通过 define_py_data_sources2
来配置PaddlePaddle从上面的dataprovider.py
里读入训练数据和测试数据。 PaddlePaddle接受从命令行读入的配置信息,例如这里我们传入一个名为is_predict
的变量来控制模型在训练和测试时的不同结构。
from paddle.trainer_config_helpers import *
is_predict = get_config_arg('is_predict', bool, False)
define_py_data_sources2(
train_list='data/train.list',
test_list='data/test.list',
module='dataprovider',
obj='process')
接着,指定模型优化算法的细节。由于线性回归模型比较简单,我们只要设置基本的batch_size
即可,它指定每次更新参数的时候使用多少条数据计算梯度信息。
settings(batch_size=2)
最后,使用fc_layer
和LinearActivation
来表示线性回归的模型本身。
#输入数据,13维的房屋信息
x = data_layer(name='x', size=13)
y_predict = fc_layer(
input=x,
param_attr=ParamAttr(name='w'),
size=1,
act=LinearActivation(),
bias_attr=ParamAttr(name='b'))
if not is_predict: #训练时,我们使用MSE,即regression_cost作为损失函数
y = data_layer(name='y', size=1)
cost = regression_cost(input=y_predict, label=y)
outputs(cost) #训练时输出MSE来监控损失的变化
else: #测试时,输出预测值
outputs(y_predict)
在对应代码的根目录下执行PaddlePaddle的命令行训练程序。这里指定模型配置文件为trainer_config.py
,训练30轮,结果保存在output
路径下。
./train.sh
现在来看下如何使用已经训练好的模型进行预测。
python predict.py
这里默认使用output/pass-00029
中保存的模型进行预测,并将数据中的房价与预测结果进行对比,结果保存在 predictions.png
中。
+如果你想使用别的模型或者其它的数据进行预测,只要传入新的路径即可:
python predict.py -m output/pass-00020 -t data/housing.test.npy
在这章里,我们借助波士顿房价这一数据集,介绍了线性回归模型的基本概念,以及如何使用PaddlePaddle实现训练和测试的过程。很多的模型和技巧都是从简单的线性回归模型演化而来,因此弄清楚线性模型的原理和局限非常重要。
+
+本教程由PaddlePaddle创作,采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
图像相比文字能够提供更加生动、容易理解及更具艺术感的信息,是人们转递与交换信息的重要来源。在本教程中,我们专注于图像识别领域的一个重要问题,即图像分类。
图像分类是根据图像的语义信息将不同类别图像区分开来,是计算机视觉中重要的基本问题,也是图像检测、图像分割、物体跟踪、行为分析等其他高层视觉任务的基础。图像分类在很多领域有广泛应用,包括安防领域的人脸识别和智能视频分析等,交通领域的交通场景识别,互联网领域基于内容的图像检索和相册自动归类,医学领域的图像识别等。
一般来说,图像分类通过手工特征或特征学习方法对整个图像进行全部描述,然后使用分类器判别物体类别,因此如何提取图像的特征至关重要。在深度学习算法之前使用较多的是基于词袋(Bag of Words)模型的物体分类方法。词袋方法从自然语言处理中引入,即一句话可以用一个装了词的袋子表示其特征,袋子中的词为句子中的单词、短语或字。对于图像而言,词袋方法需要构建字典。最简单的词袋模型框架可以设计为底层特征抽取、特征编码、分类器设计三个过程。
而基于深度学习的图像分类方法,可以通过有监督或无监督的方式学习层次化的特征描述,从而取代了手工设计或选择图像特征的工作。深度学习模型中的卷积神经网络(Convolution Neural Network, CNN)近年来在图像领域取得了惊人的成绩,CNN直接利用图像像素信息作为输入,最大程度上保留了输入图像的所有信息,通过卷积操作进行特征的提取和高层抽象,模型输出直接是图像识别的结果。这种基于"输入-输出"直接端到端的学习方法取得了非常好的效果,得到了广泛的应用。
本教程主要介绍图像分类的深度学习模型,以及如何使用PaddlePaddle训练CNN模型。
图像分类包括通用图像分类、细粒度图像分类等。图1展示了通用图像分类效果,即模型可以正确识别图像上的主要物体。
+
+图1. 通用图像分类展示
+
图2展示了细粒度图像分类-花卉识别的效果,要求模型可以正确识别花的类别。
+
+图2. 细粒度图像分类展示
+
一个好的模型既要对不同类别识别正确,同时也应该能够对不同视角、光照、背景、变形或部分遮挡的图像正确识别(这里我们统一称作图像扰动)。图3展示了一些图像的扰动,较好的模型会像聪明的人类一样能够正确识别。
+
+图3. 扰动图片展示[22]
+
图像识别领域大量的研究成果都是建立在PASCAL VOC、ImageNet等公开的数据集上,很多图像识别算法通常在这些数据集上进行测试和比较。PASCAL VOC是2005年发起的一个视觉挑战赛,ImageNet是2010年发起的大规模视觉识别竞赛(ILSVRC)的数据集,在本章中我们基于这些竞赛的一些论文介绍图像分类模型。
在2012年之前的传统图像分类方法可以用背景描述中提到的三步完成,但通常完整建立图像识别模型一般包括底层特征学习、特征编码、空间约束、分类器设计、模型融合等几个阶段。
+ 1). 底层特征提取: 通常从图像中按照固定步长、尺度提取大量局部特征描述。常用的局部特征包括SIFT(Scale-Invariant Feature Transform, 尺度不变特征转换) [1]、HOG(Histogram of Oriented Gradient, 方向梯度直方图) [2]、LBP(Local Bianray Pattern, 局部二值模式) [3] 等,一般也采用多种特征描述子,防止丢失过多的有用信息。
+ 2). 特征编码: 底层特征中包含了大量冗余与噪声,为了提高特征表达的鲁棒性,需要使用一种特征变换算法对底层特征进行编码,称作特征编码。常用的特征编码包括向量量化编码 [4]、稀疏编码 [5]、局部线性约束编码 [6]、Fisher向量编码 [7] 等。
+ 3). 空间特征约束: 特征编码之后一般会经过空间特征约束,也称作特征汇聚。特征汇聚是指在一个空间范围内,对每一维特征取最大值或者平均值,可以获得一定特征不变形的特征表达。金字塔特征匹配是一种常用的特征聚会方法,这种方法提出将图像均匀分块,在分块内做特征汇聚。
+ 4). 通过分类器分类: 经过前面步骤之后一张图像可以用一个固定维度的向量进行描述,接下来就是经过分类器对图像进行分类。通常使用的分类器包括SVM(Support Vector Machine, 支持向量机)、随机森林等。而使用核方法的SVM是最为广泛的分类器,在传统图像分类任务上性能很好。
这种方法在PASCAL VOC竞赛中的图像分类算法中被广泛使用 [18]。NEC实验室在ILSVRC2010中采用SIFT和LBP特征,两个非线性编码器以及SVM分类器获得图像分类的冠军 [8]。
Alex Krizhevsky在2012年ILSVRC提出的CNN模型 [9] 取得了历史性的突破,效果大幅度超越传统方法,获得了ILSVRC2012冠军,该模型被称作AlexNet。这也是首次将深度学习用于大规模图像分类中。从AlexNet之后,涌现了一系列CNN模型,不断地在ImageNet上刷新成绩,如图4展示。随着模型变得越来越深以及精妙的结构设计,Top-5的错误率也越来越低,降到了3.5%附近。而在同样的ImageNet数据集上,人眼的辨识错误率大概在5.1%,也就是目前的深度学习模型的识别能力已经超过了人眼。
+
+图4. ILSVRC图像分类Top-5错误率
+
传统CNN包含卷积层、全连接层等组件,并采用softmax多类别分类器和多类交叉熵损失函数,一个典型的卷积神经网络如图5所示,我们先介绍用来构造CNN的常见组件。
+
+图5. CNN网络示例[20]
+
另外,在训练过程中由于每层参数不断更新,会导致下一次输入分布发生变化,这样导致训练过程需要精心设计超参数。如2015年Sergey Ioffe和Christian Szegedy提出了Batch Normalization (BN)算法 [14] 中,每个batch对网络中的每一层特征都做归一化,使得每层分布相对稳定。BN算法不仅起到一定的正则作用,而且弱化了一些超参数的设计。经过实验证明,BN算法加速了模型收敛过程,在后来较深的模型中被广泛使用。
接下来我们主要介绍VGG,GoogleNet和ResNet网络结构。
牛津大学VGG(Visual Geometry Group)组在2014年ILSVRC提出的模型被称作VGG模型 [11] 。该模型相比以往模型进一步加宽和加深了网络结构,它的核心是五组卷积操作,每两组之间做Max-Pooling空间降维。同一组内采用多次连续的3X3卷积,卷积核的数目由较浅组的64增多到最深组的512,同一组内的卷积核数目是一样的。卷积之后接两层全连接层,之后是分类层。由于每组内卷积层的不同,有11、13、16、19层这几种模型,下图展示一个16层的网络结构。VGG模型结构相对简洁,提出之后也有很多文章基于此模型进行研究,如在ImageNet上首次公开超过人眼识别的模型[19]就是借鉴VGG模型的结构。
+
+图6. 基于ImageNet的VGG16模型
+
GoogleNet [12] 在2014年ILSVRC的获得了冠军,在介绍该模型之前我们先来了解NIN(Network in Network)模型 [13] 和Inception模块,因为GoogleNet模型由多组Inception模块组成,模型设计借鉴了NIN的一些思想。
NIN模型主要有两个特点:1) 引入了多层感知卷积网络(Multi-Layer Perceptron Convolution, MLPconv)代替一层线性卷积网络。MLPconv是一个微小的多层卷积网络,即在线性卷积后面增加若干层1x1的卷积,这样可以提取出高度非线性特征。2) 传统的CNN最后几层一般都是全连接层,参数较多。而NIN模型设计最后一层卷积层包含类别维度大小的特征图,然后采用全局均值池化(Avg-Pooling)替代全连接层,得到类别维度大小的向量,再进行分类。这种替代全连接层的方式有利于减少参数。
Inception模块如下图7所示,图(a)是最简单的设计,输出是3个卷积层和一个池化层的特征拼接。这种设计的缺点是池化层不会改变特征通道数,拼接后会导致特征的通道数较大,经过几层这样的模块堆积后,通道数会越来越大,导致参数和计算量也随之增大。为了改善这个缺点,图(b)引入3个1x1卷积层进行降维,所谓的降维就是减少通道数,同时如NIN模型中提到的1x1卷积也可以修正线性特征。
+
+图7. Inception模块
+
GoogleNet由多组Inception模块堆积而成。另外,在网络最后也没有采用传统的多层全连接层,而是像NIN网络一样采用了均值池化层;但与NIN不同的是,池化层后面接了一层到类别数映射的全连接层。除了这两个特点之外,由于网络中间层特征也很有判别性,GoogleNet在中间层添加了两个辅助分类器,在后向传播中增强梯度并且增强正则化,而整个网络的损失函数是这个三个分类器的损失加权求和。
GoogleNet整体网络结构如图8所示,总共22层网络:开始由3层普通的卷积组成;接下来由三组子网络组成,第一组子网络包含2个Inception模块,第二组包含5个Inception模块,第三组包含2个Inception模块;然后接均值池化层、全连接层。
+
+图8. GoogleNet[12]
+
上面介绍的是GoogleNet第一版模型(称作GoogleNet-v1)。GoogleNet-v2 [14] 引入BN层;GoogleNet-v3 [16] 对一些卷积层做了分解,进一步提高网络非线性能力和加深网络;GoogleNet-v4 [17] 引入下面要讲的ResNet设计思路。从v1到v4每一版的改进都会带来准确度的提升,介于篇幅,这里不再详细介绍v2到v4的结构。
ResNet(Residual Network) [15] 是2015年ImageNet图像分类、图像物体定位和图像物体检测比赛的冠军。针对训练卷积神经网络时加深网络导致准确度下降的问题,ResNet提出了采用残差学习。在已有设计思路(BN, 小卷积核,全卷积网络)的基础上,引入了残差模块。每个残差模块包含两条路径,其中一条路径是输入特征的直连通路,另一条路径对该特征做两到三次卷积操作得到该特征的残差,最后再将两条路径上的特征相加。
残差模块如图9所示,左边是基本模块连接方式,由两个输出通道数相同的3x3卷积组成。右边是瓶颈模块(Bottleneck)连接方式,之所以称为瓶颈,是因为上面的1x1卷积用来降维(图示例即256->64),下面的1x1卷积用来升维(图示例即64->256),这样中间3x3卷积的输入和输出通道数都较小(图示例即64->64)。
+
+图9. 残差模块
+
图10展示了50、101、152层网络连接示意图,使用的是瓶颈模块。这三个模型的区别在于每组中残差模块的重复次数不同(见图右上角)。ResNet训练收敛较快,成功的训练了上百乃至近千层的卷积神经网络。
+
+图10. 基于ImageNet的ResNet模型
+
通用图像分类公开的标准数据集常用的有CIFAR、ImageNet、COCO等,常用的细粒度图像分类数据集包括CUB-200-2011、Stanford Dog、Oxford-flowers等。其中ImageNet数据集规模相对较大,如模型概览一章所讲,大量研究成果基于ImageNet。ImageNet数据从2010年来稍有变化,常用的是ImageNet-2012数据集,该数据集包含1000个类别:训练集包含1,281,167张图片,每个类别数据732至1300张不等,验证集包含50,000张图片,平均每个类别50张图片。
由于ImageNet数据集较大,下载和训练较慢,为了方便大家学习,我们使用CIFAR10数据集。CIFAR10数据集包含60,000张32x32的彩色图片,10个类别,每个类包含6,000张。其中50,000张图片作为训练集,10000张作为测试集。图11从每个类别中随机抽取了10张图片,展示了所有的类别。
+
+图11. CIFAR10数据集[21]
+
下面命令用于下载数据和基于训练集计算图像均值,在网络输入前,基于该均值对输入数据做预处理。
./data/get_data.sh
+
我们使用Python接口传递数据给系统,下面 dataprovider.py
针对CIFAR10数据给出了完整示例。
initializer
函数进行dataprovider的初始化,这里加载图像的均值,定义了输入image和label两个字段的类型。
process
函数将数据逐条传输给系统,在图像分类任务里,可以在该函数中完成数据扰动操作,再传输给PaddlePaddle。这里对训练集做随机左右翻转,并将原始图片减去均值后传输给系统。
import numpy as np
import cPickle
from paddle.trainer.PyDataProvider2 import *
def initializer(settings, mean_path, is_train, **kwargs):
settings.is_train = is_train
settings.input_size = 3 * 32 * 32
settings.mean = np.load(mean_path)['mean']
settings.input_types = {
'image': dense_vector(settings.input_size),
'label': integer_value(10)
}
@provider(init_hook=initializer, cache=CacheType.CACHE_PASS_IN_MEM)
def process(settings, file_list):
with open(file_list, 'r') as fdata:
for fname in fdata:
fo = open(fname.strip(), 'rb')
batch = cPickle.load(fo)
fo.close()
images = batch['data']
labels = batch['labels']
for im, lab in zip(images, labels):
if settings.is_train and np.random.randint(2):
im = im[:,:,::-1]
im = im - settings.mean
yield {
'image': im.astype('float32'),
'label': int(lab)
}
在模型配置中,定义通过 define_py_data_sources2
函数从 dataprovider 中读入数据, 其中 args 指定均值文件的路径。如果该配置文件用于预测,则不需要数据定义部分。
from paddle.trainer_config_helpers import *
+
+is_predict = get_config_arg("is_predict", bool, False)
+if not is_predict:
+ define_py_data_sources2(
+ train_list='data/train.list',
+ test_list='data/test.list',
+ module='dataprovider',
+ obj='process',
+ args={'mean_path': 'data/mean.meta'})
+
在模型配置中,通过 settings
设置训练使用的优化算法,并指定batch size 、初始学习率、momentum以及L2正则。
settings(
+ batch_size=128,
+ learning_rate=0.1 / 128.0,
+ learning_rate_decay_a=0.1,
+ learning_rate_decay_b=50000 * 100,
+ learning_rate_schedule='discexp',
+ learning_method=MomentumOptimizer(0.9),
+ regularization=L2Regularization(0.0005 * 128),)
+
通过 learning_rate_decay_a
(简写) 、learning_rate_decay_b
(简写) 和 learning_rate_schedule
指定学习率调整策略,这里采用离散指数的方式调节学习率,计算公式如下, 代表已经处理过的累计总样本数, 即为 settings
里设置的 learning_rate
。
本教程中我们提供了VGG和ResNet两个模型的配置。
首先介绍VGG模型结构,由于CIFAR10图片大小和数量相比ImageNet数据小很多,因此这里的模型针对CIFAR10数据做了一定的适配。卷积部分引入了BN和Dropout操作。
定义数据输入及其维度
+ +网络输入定义为 data_layer
(数据层),在图像分类中即为图像像素信息。CIFRAR10是RGB 3通道32x32大小的彩色图,因此输入数据大小为3072(3x32x32),类别大小为10,即10分类。
datadim = 3 * 32 * 32
classdim = 10
data = data_layer(name='image', size=datadim)
定义VGG网络核心模块
+ ++ +
net = vgg_bn_drop(data)
VGG核心模块的输入是数据层,vgg_bn_drop
定义了16层VGG结构,每层卷积后面引入BN层和Dropout层,详细的定义如下:
+ +
def vgg_bn_drop(input, num_channels):
def conv_block(ipt, num_filter, groups, dropouts, num_channels_=None):
return img_conv_group(
input=ipt,
num_channels=num_channels_,
pool_size=2,
pool_stride=2,
conv_num_filter=[num_filter] * groups,
conv_filter_size=3,
conv_act=ReluActivation(),
conv_with_batchnorm=True,
conv_batchnorm_drop_rate=dropouts,
pool_type=MaxPooling())
conv1 = conv_block(input, 64, 2, [0.3, 0], 3)
conv2 = conv_block(conv1, 128, 2, [0.4, 0])
conv3 = conv_block(conv2, 256, 3, [0.4, 0.4, 0])
conv4 = conv_block(conv3, 512, 3, [0.4, 0.4, 0])
conv5 = conv_block(conv4, 512, 3, [0.4, 0.4, 0])
drop = dropout_layer(input=conv5, dropout_rate=0.5)
fc1 = fc_layer(input=drop, size=512, act=LinearActivation())
bn = batch_norm_layer(
input=fc1, act=ReluActivation(), layer_attr=ExtraAttr(drop_rate=0.5))
fc2 = fc_layer(input=bn, size=512, act=LinearActivation())
return fc2
2.1. 首先定义了一组卷积网络,即conv_block。卷积核大小为3x3,池化窗口大小为2x2,窗口滑动大小为2,groups决定每组VGG模块是几次连续的卷积操作,dropouts指定Dropout操作的概率。所使用的img_conv_group
是在paddle.trainer_config_helpers
中预定义的模块,由若干组 Conv->BN->ReLu->Dropout
和 一组 Pooling
组成,
2.2. 五组卷积操作,即 5个conv_block。 第一、二组采用两次连续的卷积操作。第三、四、五组采用三次连续的卷积操作。每组最后一个卷积后面Dropout概率为0,即不使用Dropout操作。
+ +2.3. 最后接两层512维的全连接。
定义分类器
+ +通过上面VGG网络提取高层特征,然后经过全连接层映射到类别维度大小的向量,再通过Softmax归一化得到每个类别的概率,也可称作分类器。
+ +
out = fc_layer(input=net, size=class_num, act=SoftmaxActivation())
定义损失函数和网络输出
+ +在有监督训练中需要输入图像对应的类别信息,同样通过data_layer
来定义。训练中采用多类交叉熵作为损失函数,并作为网络的输出,预测阶段定义网络的输出为分类器得到的概率信息。
if not is_predict:
lbl = data_layer(name="label", size=class_num)
cost = classification_cost(input=out, label=lbl)
outputs(cost)
else:
outputs(out)
ResNet模型的第1、3、4步和VGG模型相同,这里不再介绍。主要介绍第2步即CIFAR10数据集上ResNet核心模块。
net = resnet_cifar10(data, depth=56)
+
先介绍resnet_cifar10
中的一些基本函数,再介绍网络连接过程。
conv_bn_layer
: 带BN的卷积层。shortcut
: 残差模块的"直连"路径,"直连"实际分两种形式:残差模块输入和输出特征通道数不等时,采用1x1卷积的升维操作;残差模块输入和输出通道相等时,采用直连操作。basicblock
: 一个基础残差模块,即图9左边所示,由两组3x3卷积组成的路径和一条"直连"路径组成。bottleneck
: 一个瓶颈残差模块,即图9右边所示,由上下1x1卷积和中间3x3卷积组成的路径和一条"直连"路径组成。layer_warp
: 一组残差模块,由若干个残差模块堆积而成。每组中第一个残差模块滑动窗口大小与其他可以不同,以用来减少特征图在垂直和水平方向的大小。
def conv_bn_layer(input,
ch_out,
filter_size,
stride,
padding,
active_type=ReluActivation(),
ch_in=None):
tmp = img_conv_layer(
input=input,
filter_size=filter_size,
num_channels=ch_in,
num_filters=ch_out,
stride=stride,
padding=padding,
act=LinearActivation(),
bias_attr=False)
return batch_norm_layer(input=tmp, act=active_type)
def shortcut(ipt, n_in, n_out, stride):
if n_in != n_out:
return conv_bn_layer(ipt, n_out, 1, stride, 0, LinearActivation())
else:
return ipt
def basicblock(ipt, ch_out, stride):
ch_in = ipt.num_filters
tmp = conv_bn_layer(ipt, ch_out, 3, stride, 1)
tmp = conv_bn_layer(tmp, ch_out, 3, 1, 1, LinearActivation())
short = shortcut(ipt, ch_in, ch_out, stride)
return addto_layer(input=[ipt, short], act=ReluActivation())
def bottleneck(ipt, ch_out, stride):
ch_in = ipt.num_filter
tmp = conv_bn_layer(ipt, ch_out, 1, stride, 0)
tmp = conv_bn_layer(tmp, ch_out, 3, 1, 1)
tmp = conv_bn_layer(tmp, ch_out * 4, 1, 1, 0, LinearActivation())
short = shortcut(ipt, ch_in, ch_out, stride)
return addto_layer(input=[ipt, short], act=ReluActivation())
def layer_warp(block_func, ipt, features, count, stride):
tmp = block_func(ipt, features, stride)
for i in range(1, count):
tmp = block_func(tmp, features, 1)
return tmp
resnet_cifar10
的连接结构主要有以下几个过程。
conv_bn_layer
,即带BN的卷积层。 layer_warp
,每组采用图 10 左边残差模块组成。最后对网络做均值池化并返回该层。
+ +
def resnet_cifar10(ipt, depth=56):
# depth should be one of 20, 32, 44, 56, 110, 1202
assert (depth - 2) % 6 == 0
n = (depth - 2) / 6
nStages = {16, 64, 128}
conv1 = conv_bn_layer(ipt,
ch_in=3,
ch_out=16,
filter_size=3,
stride=1,
padding=1)
res1 = layer_warp(basicblock, conv1, 16, n, 1)
res2 = layer_warp(basicblock, res1, 32, n, 2)
res3 = layer_warp(basicblock, res2, 64, n, 2)
pool = img_pool_layer(input=res3,
pool_size=8,
stride=1,
pool_type=AvgPooling())
return pool
注意:除过第一层卷积层和最后一层全连接层之外,要求三组 layer_warp
总的含参层数能够被6整除,即 resnet_cifar10
的 depth 要满足 。
执行脚本 train.sh 进行模型训练, 其中指定配置文件、设备类型、线程个数、总共训练的轮数、模型存储路径等。
sh train.sh
+
脚本 train.sh
如下:
#cfg=models/resnet.py
+cfg=models/vgg.py
+output=output
+log=train.log
+
+paddle train \
+ --config=$cfg \
+ --use_gpu=true \
+ --trainer_count=1 \
+ --log_period=100 \
+ --num_passes=300 \
+ --save_dir=$output \
+ 2>&1 | tee $log
+
--config=$cfg
: 指定配置文件,默认是 models/vgg.py
。--use_gpu=true
: 指定使用GPU训练,若使用CPU,设置为false。--trainer_count=1
: 指定线程个数或GPU个数。--log_period=100
: 指定日志打印的batch间隔。--save_dir=$output
: 指定模型存储路径。一轮训练log示例如下所示,经过1个pass, 训练集上平均error为0.79958 ,测试集上平均error为0.7858 。
TrainerInternal.cpp:165] Batch=300 samples=38400 AvgCost=2.07708 CurrentCost=1.96158 Eval: classification_error_evaluator=0.81151 CurrentEval: classification_error_evaluator=0.789297
TrainerInternal.cpp:181] Pass=0 Batch=391 samples=50000 AvgCost=2.03348 Eval: classification_error_evaluator=0.79958
Tester.cpp:115] Test samples=10000 cost=1.99246 Eval: classification_error_evaluator=0.7858
+
+图12. CIFAR10数据集上VGG模型的分类错误率
+
在训练完成后,模型会保存在路径 output/pass-%05d
下,例如第300个pass的模型会保存在路径 output/pass-00299
。 可以使用脚本 classify.py
对图片进行预测或提取特征,注意该脚本默认使用模型配置为 models/vgg.py
,
可以按照下面方式预测图片的类别,默认使用GPU预测,如果使用CPU预测,在后面加参数 -c
即可。
python classify.py --job=predict --model=output/pass-00299 --data=image/dog.png # -c
+
预测结果为:
Label of image/dog.png is: 5
+
可以按照下面方式对图片提取特征,和预测使用方式不同的是指定job类型为extract,并需要指定提取的层。classify.py
默认以第一层卷积特征为例提取特征,并画出了类似图13的可视化图。VGG模型的第一层卷积有64个通道,图13展示了每个通道的灰度图。
python classify.py --job=extract --model=output/pass-00299 --data=image/dog.png # -c
+
+
+图13. 卷积特征可视化图
+
传统图像分类方法由多个阶段构成,框架较为复杂,而端到端的CNN模型结构可一步到位,而且大幅度提升了分类准确率。本文我们首先介绍VGG、GoogleNet、ResNet三个经典的模型;然后基于CIFAR10数据集,介绍如何使用PaddlePaddle配置和训练CNN模型,尤其是VGG和ResNet模型;最后介绍如何使用PaddlePaddle的API接口对图片进行预测和特征提取。对于其他数据集比如ImageNet,配置和训练流程是同样的,大家可以自行进行实验。
[1] D. G. Lowe, Distinctive image features from scale-invariant keypoints. IJCV, 60(2):91-110, 2004.
[2] N. Dalal, B. Triggs, Histograms of Oriented Gradients for Human Detection, Proc. IEEE Conf. Computer Vision and Pattern Recognition, 2005.
[3] Ahonen, T., Hadid, A., and Pietikinen, M. (2006). Face description with local binary patterns: Application to face recognition. PAMI, 28.
[4] J. Sivic, A. Zisserman, Video Google: A Text Retrieval Approach to Object Matching in Videos, Proc. Ninth Int'l Conf. Computer Vision, pp. 1470-1478, 2003.
[5] B. Olshausen, D. Field, Sparse Coding with an Overcomplete Basis Set: A Strategy Employed by V1?, Vision Research, vol. 37, pp. 3311-3325, 1997.
[6] Wang, J., Yang, J., Yu, K., Lv, F., Huang, T., and Gong, Y. (2010). Locality-constrained Linear Coding for image classification. In CVPR.
[7] Perronnin, F., Sánchez, J., & Mensink, T. (2010). Improving the fisher kernel for large-scale image classification. In ECCV (4).
[8] Lin, Y., Lv, F., Cao, L., Zhu, S., Yang, M., Cour, T., Yu, K., and Huang, T. (2011). Large-scale image clas- sification: Fast feature extraction and SVM training. In CVPR.
[9] Krizhevsky, A., Sutskever, I., and Hinton, G. (2012). ImageNet classification with deep convolutional neu- ral networks. In NIPS.
[10] G.E. Hinton, N. Srivastava, A. Krizhevsky, I. Sutskever, and R.R. Salakhutdinov. Improving neural networks by preventing co-adaptation of feature detectors. arXiv preprint arXiv:1207.0580, 2012.
[11] K. Chatfield, K. Simonyan, A. Vedaldi, A. Zisserman. Return of the Devil in the Details: Delving Deep into Convolutional Nets. BMVC, 2014。
[12] Szegedy, C., Liu, W., Jia, Y., Sermanet, P., Reed, S., Anguelov, D., Erhan, D., Vanhoucke, V., Rabinovich, A., Going deeper with convolutions. In: CVPR. (2015)
[13] Lin, M., Chen, Q., and Yan, S. Network in network. In Proc. ICLR, 2014.
[14] S. Ioffe and C. Szegedy. Batch normalization: Accelerating deep network training by reducing internal covariate shift. In ICML, 2015.
[15] K. He, X. Zhang, S. Ren, J. Sun. Deep Residual Learning for Image Recognition. CVPR 2016.
[16] Szegedy, C., Vanhoucke, V., Ioffe, S., Shlens, J., Wojna, Z. Rethinking the incep-tion architecture for computer vision. In: CVPR. (2016).
[17] Szegedy, C., Ioffe, S., Vanhoucke, V. Inception-v4, inception-resnet and the impact of residual connections on learning. arXiv:1602.07261 (2016).
[18] Everingham, M., Eslami, S. M. A., Van Gool, L., Williams, C. K. I., Winn, J. and Zisserman, A. The Pascal Visual Object Classes Challenge: A Retrospective. International Journal of Computer Vision, 111(1), 98-136, 2015.
[19] He, K., Zhang, X., Ren, S., and Sun, J. Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification. ArXiv e-prints, February 2015.
[20] http://deeplearning.net/tutorial/lenet.html
[21] https://www.cs.toronto.edu/~kriz/cifar.html
[22] http://cs231n.github.io/classification/
+
本教程由PaddlePaddle创作,采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
然而,完全句法分析需要确定句子所包含的全部句法信息,并确定句子各成分之间的关系,是一个非常困难的任务,目前技术下的句法分析准确率并不高,句法分析的细微错误都会导致SRL的错误。为了降低问题的复杂度,同时获得一定的句法结构信息,“浅层句法分析”的思想应运而生。浅层句法分析也称为部分句法分析(partial parsing)或语块划分(chunking)。和完全句法分析得到一颗完整的句法树不同,浅层句法分析只需要识别句子中某些结构相对简单的独立成分,例如:动词短语,这些被识别出来的结构称为语块。为了回避 “无法获得准确率较高的句法树” 所带来的困难,一些研究[1]也提出了基于语块(chunk)的SRL方法。基于语块的SRL方法将SRL作为一个序列标注问题来解决。序列标注任务一般都会采用BIO表示方式来定义序列标注的标签集,我们先来介绍这种表示方法。在BIO表示法中,B代表语块的开始,I代表语块的中间,O代表语块结束。通过B、I、O 三种标记将不同的语块赋予不同的标签,例如:对于一个角色为A的论元,将它所包含的第一个语块赋予标签B-A,将它所包含的其它语块赋予标签I-A,不属于任何论元的语块赋予标签O。
我们继续以上面的这句话为例,图1展示了BIO表示方法。
从上面的例子可以看到,根据序列标注结果可以直接得到论元的语义角色标注结果,是一个相对简单的过程。这种简单性体现在:(1)依赖浅层句法分析,降低了句法分析的要求和难度;(2)没有了候选论元剪除这一步骤;(3)论元的识别和论元标注是同时实现的。这种一体化处理论元识别和论元标注的方法,简化了流程,降低了错误累积的风险,往往能够取得更好的结果。
与基于语块的SRL方法类似,在本教程中我们也将SRL看作一个序列标注问题,不同的是,我们只依赖输入文本序列,不依赖任何额外的语法解析结果或是复杂的人造特征,利用深度神经网络构建一个端到端学习的SRL系统。我们以CoNLL-2004 and CoNLL-2005 Shared Tasks任务中SRL任务的公开数据集为例,实践下面的任务:给定一句话和这句话里的一个谓词,通过序列标注的方式,从句子中找到谓词对应的论元,同时标注它们的语义角色。
循环神经网络(Recurrent Neural Network)是一种对序列建模的重要模型,在自然语言处理任务中有着广泛地应用。不同于前馈神经网络(Feed-forward Neural Network),RNN能够处理输入之间前后关联的问题。LSTM是RNN的一种重要变种,常用来学习长序列中蕴含的长程依赖关系,我们在情感分析一篇中已经介绍过,这一篇中我们依然利用LSTM来解决SRL问题。
深层网络有助于形成层次化特征,网络上层在下层已经学习到的初级特征基础上,形成更复杂的高级特征。尽管LSTM沿时间轴展开后等价于一个非常“深”的前馈网络,但由于LSTM各个时间步参数共享,时刻状态到时刻的映射,始终只经过了一次非线性映射,也就是说单层LSTM对状态转移的建模是 “浅” 的。堆叠多个LSTM单元,令前一个LSTM时刻的输出,成为下一个LSTM单元时刻的输入,帮助我们构建起一个深层网络,我们把它称为第一个版本的栈式循环神经网络。深层网络提高了模型拟合复杂模式的能力,能够更好地建模跨不同时间步的模式[2]。
然而,训练一个深层LSTM网络并非易事。纵向堆叠多个LSTM单元可能遇到梯度在纵向深度上传播受阻的问题。通常,堆叠4层LSTM单元可以正常训练,当层数达到4~8层时,会出现性能衰减,这时必须考虑一些新的结构以保证梯度纵向顺畅传播,这是训练深层LSTM网络必须解决的问题。我们可以借鉴LSTM解决 “梯度消失梯度爆炸” 问题的智慧之一:在记忆单元(Memory Cell)这条信息传播的路线上没有非线性映射,当梯度反向传播时既不会衰减、也不会爆炸。因此,深层LSTM模型也可以在纵向上添加一条保证梯度顺畅传播的路径。
一个LSTM单元完成的运算可以被分为三部分:(1)输入到隐层的映射(input-to-hidden) :每个时间步输入信息会首先经过一个矩阵映射,再作为遗忘门,输入门,记忆单元,输出门的输入,注意,这一次映射没有引入非线性激活;(2)隐层到隐层的映射(hidden-to-hidden):这一步是LSTM计算的主体,包括遗忘门,输入门,记忆单元更新,输出门的计算;(3)隐层到输出的映射(hidden-to-output):通常是简单的对隐层向量进行激活。我们在第一个版本的栈式网络的基础上,加入一条新的路径:除上一层LSTM输出之外,将前层LSTM的输入到隐层的映射作为的一个新的输入,同时加入一个线性映射去学习一个新的变换。
图3是最终得到的栈式循环神经网络结构示意图。
+
+图3. 基于LSTM的栈式循环神经网络结构示意图
+
在LSTM中,时刻的隐藏层向量编码了到时刻为止所有输入的信息,但时刻的LSTM可以看到历史,却无法看到未来。在绝大多数自然语言处理任务中,我们几乎总是能拿到整个句子。这种情况下,如果能够像获取历史信息一样,得到未来的信息,对序列学习任务会有很大的帮助。
为了克服这一缺陷,我们可以设计一种双向循环网络单元,它的思想简单且直接:对上一节的栈式循环神经网络进行一个小小的修改,堆叠多个LSTM单元,让每一层LSTM单元分别以:正向、反向、正向 …… 的顺序学习上一层的输出序列。于是,从第2层开始,时刻我们的LSTM单元便总是可以看到历史和未来的信息。图4是基于LSTM的双向循环神经网络结构示意图。
+
+图4. 基于LSTM的双向循环神经网络结构示意图
+
需要说明的是,这种双向RNN结构和Bengio等人在机器翻译任务中使用的双向RNN结构[3, 4] 并不相同,我们会在后续机器翻译任务中,介绍另一种双向循环神经网络。
使用神经网络模型解决问题的思路通常是:前层网络学习输入的特征表示,网络的最后一层在特征基础上完成最终的任务。在SRL任务中,深层LSTM网络学习输入的特征表示,条件随机场(Conditional Random Filed, CRF)在特征的基础上完成序列标注,处于整个网络的末端。
CRF是一种概率化结构模型,可以看作是一个概率无向图模型,结点表示随机变量,边表示随机变量之间的概率依赖关系。简单来讲,CRF学习条件概率,其中 是输入序列, 是标记序列;解码过程是给定 序列求解令最大的序列,即。
序列标注任务只需要考虑输入和输出都是一个线性序列,并且由于我们只是将输入序列作为条件,不做任何条件独立假设,因此输入序列的元素之间并不存在图结构。综上,在序列标注任务中使用的是如图5所示的定义在链式图上的CRF,称之为线性链条件随机场(Linear Chain Conditional Random Field)。
+
+图5. 序列标注任务中使用的线性链条件随机场
+
根据线性链条件随机场上的因子分解定理[5],在给定观测序列时,一个特定标记序列的概率可以定义为:
其中是归一化因子, 是定义在边上的特征函数,依赖于当前和前一个位置,称为转移特征,表示对于输入序列及其标注序列在 及位置上标记的转移概率。是定义在结点上的特征函数,称为状态特征,依赖于当前位置,表示对于观察序列及其位置的标记概率。 和 分别是转移特征函数和状态特征函数对应的权值。实际上,和可以用相同的数学形式表示,再对转移特征和状态特在各个位置求和有:,把统称为特征函数,于是可表示为:
是特征函数对应的权值,是CRF模型要学习的参数。训练时,对于给定的输入序列和对应的标记序列集合 ,通过正则化的极大似然估计,求解如下优化目标:
这个优化目标可以通过反向传播算法和整个神经网络一起求解。解码时,对于给定的输入序列,通过解码算法(通常有:维特比算法、Beam Search)求令出条件概率最大的输出序列 。
在SRL任务中,输入是 “谓词” 和 “一句话”,目标是从这句话中找到谓词的论元,并标注论元的语义角色。如果一个句子含有个谓词,这个句子会被处理次。一个最为直接的模型是下面这样:
大家可以尝试上面这种方法。这里,我们提出一些改进,引入两个简单但对提高系统性能非常有效的特征:
修改后的模型如下(图6是一个深度为4的模型结构示意图):
+
+图6. SRL任务上的深层双向LSTM模型
+
在此教程中,我们选用CoNLL 2005SRL任务开放出的数据集作为示例。运行 sh ./get_data.sh
会自动从官方网站上下载原始数据。需要特别说明的是,CoNLL 2005 SRL任务的训练数集和开发集在比赛之后并非免费进行公开,目前,能够获取到的只有测试集,包括Wall Street Journal的23节和Brown语料集中的3节。在本教程中,我们以测试集中的WSJ数据为训练集来讲解模型。但是,由于测试集中样本的数量远远不够,如果希望训练一个可用的神经网络SRL系统,请考虑付费获取全量数据。
原始数据中同时包括了词性标注、命名实体识别、语法解析树等多种信息。本教程中,我们使用test.wsj文件夹中的数据进行训练和测试,并只会用到words文件夹(文本序列)和props文件夹(标注结果)下的数据。本教程使用的数据目录如下:
conll05st-release/
└── test.wsj
├── props # 标注结果
└── words # 输入文本序列
标注信息源自Penn TreeBank[7]和PropBank[8]的标注结果。PropBank标注结果的标签和我们在文章一开始示例中使用的标注结果标签不同,但原理是相同的,关于标注结果标签含义的说明,请参考论文[9]。
除数据之外,get_data.sh
同时下载了以下资源:
文件名称 | +说明 | +
---|---|
word_dict | +输入句子的词典,共计44068个词 | +
label_dict | +标记的词典,共计106个标记 | +
predicate_dict | +谓词的词典,共计3162个词 | +
emb | +一个训练好的词表,32维 | +
我们在英文维基百科上训练语言模型得到了一份词向量用来初始化SRL模型。在SRL模型训练过程中,词向量不再被更新。关于语言模型和词向量可以参考词向量 这篇教程。我们训练语言模型的语料共有995,000,000个token,词典大小控制为4900,000词。CoNLL 2005训练语料中有5%的词不在这4900,000个词中,我们将它们全部看作未登录词,用<unk>
表示。
脚本在下载数据之后,又调用了extract_pair.py
和extract_dict_feature.py
两个子脚本进行数据预处理,前者完成了下面的第1步,后者完成了下面的2~4步:
data/feature
文件是处理好的模型输入,一行是一条训练样本,以"\t"分隔,共9列,分别是:句子序列、谓词、谓词上下文(占 5 列)、谓词上下区域标志、标注序列。下表是一条训练样本的示例。
句子序列 | +谓词 | +谓词上下文(窗口 = 5) | +谓词上下文区域标记 | +标注序列 | +
---|---|---|---|---|
A | +set | +n't been set . × | +0 | +B-A1 | +
record | +set | +n't been set . × | +0 | +I-A1 | +
date | +set | +n't been set . × | +0 | +I-A1 | +
has | +set | +n't been set . × | +0 | +O | +
n't | +set | +n't been set . × | +1 | +B-AM-NEG | +
been | +set | +n't been set . × | +1 | +O | +
set | +set | +n't been set . × | +1 | +B-V | +
. | +set | +n't been set . × | +1 | +O | +
使用hook函数进行PaddlePaddle输入字段的格式定义。
+ +
def hook(settings, word_dict, label_dict, predicate_dict, **kwargs):
settings.word_dict = word_dict # 获取句子序列的字典
settings.label_dict = label_dict # 获取标记序列的字典
settings.predicate_dict = predicate_dict # 获取谓词的字典
# 所有输入特征都是使用one-hot表示序列,在PaddlePaddle中是interger_value_sequence类型
# input_types是一个字典,字典中每个元素对应着配置中的一个data_layer,key恰好就是data_layer的名字
settings.input_types = {
'word_data': integer_value_sequence(len(word_dict)), # 句子序列
'ctx_n2_data': integer_value_sequence(len(word_dict)), # 谓词上下文中的第1个词
'ctx_n1_data': integer_value_sequence(len(word_dict)), # 谓词上下文中的第2个词
'ctx_0_data': integer_value_sequence(len(word_dict)), # 谓词上下文中的第3个词
'ctx_p1_data': integer_value_sequence(len(word_dict)), # 谓词上下文中的第4个词
'ctx_p2_data': integer_value_sequence(len(word_dict)), # 谓词上下文中的第5个词
'verb_data': integer_value_sequence(len(predicate_dict)), # 谓词
'mark_data': integer_value_sequence(2), # 谓词上下文区域标记
'target': integer_value_sequence(len(label_dict)) # 标记序列
}
使用process将数据逐一提供给PaddlePaddle,只需要考虑如何从原始数据文件中返回一条训练样本。
+ +
def process(settings, file_name):
with open(file_name, 'r') as fdata:
for line in fdata:
sentence, predicate, ctx_n2, ctx_n1, ctx_0, ctx_p1, ctx_p2, mark, label = \
line.strip().split('\t')
# 句子文本
words = sentence.split()
sen_len = len(words)
word_slot = [settings.word_dict.get(w, UNK_IDX) for w in words]
# 一个谓词,这里将谓词扩展成一个和句子一样长的序列
predicate_slot = [settings.predicate_dict.get(predicate)] * sen_len
# 在教程中,我们使用一个窗口为 5 的谓词上下文窗口:谓词和这个谓词前后隔两个词
# 这里会将窗口中的每一个词,扩展成和输入句子一样长的序列
ctx_n2_slot = [settings.word_dict.get(ctx_n2, UNK_IDX)] * sen_len
ctx_n1_slot = [settings.word_dict.get(ctx_n1, UNK_IDX)] * sen_len
ctx_0_slot = [settings.word_dict.get(ctx_0, UNK_IDX)] * sen_len
ctx_p1_slot = [settings.word_dict.get(ctx_p1, UNK_IDX)] * sen_len
ctx_p2_slot = [settings.word_dict.get(ctx_p2, UNK_IDX)] * sen_len
# 谓词上下文区域标记,是一个二值特征
marks = mark.split()
mark_slot = [int(w) for w in marks]
label_list = label.split()
label_slot = [settings.label_dict.get(w) for w in label_list]
yield {
'word_data': word_slot,
'ctx_n2_data': ctx_n2_slot,
'ctx_n1_data': ctx_n1_slot,
'ctx_0_data': ctx_0_slot,
'ctx_p1_data': ctx_p1_slot,
'ctx_p2_data': ctx_p2_slot,
'verb_data': predicate_slot,
'mark_data': mark_slot,
'target': label_slot
}
首先通过 define_py_data_sources2 从dataprovider中读入数据。配置文件中会读取三个字典:输入文本序列的字典、标记的字典、谓词的字典,并传给data provider,data provider会利用这三个字典,将相应的文本输入转换成one-hot序列。
define_py_data_sources2(
train_list=train_list_file,
test_list=test_list_file,
module='dataprovider',
obj='process',
args={
'word_dict': word_dict, # 输入文本序列的字典
'label_dict': label_dict, # 标记的字典
'predicate_dict': predicate_dict # 谓词的词典
}
)
在这里,我们指定了模型的训练参数,选择了正则、学习率和batch size,并使用带Momentum的随机梯度下降法作为优化算法。
settings(
batch_size=150,
learning_method=MomentumOptimizer(momentum=0),
learning_rate=2e-2,
regularization=L2Regularization(8e-4),
model_average=ModelAverage(average_window=0.5, max_average_window=10000)
)
定义输入数据维度及模型超参数。
+ ++ +
mark_dict_len = 2 # 谓上下文区域标志的维度,是一个0-1 2值特征,因此维度为2
word_dim = 32 # 词向量维度
mark_dim = 5 # 谓词上下文区域通过词表被映射为一个实向量,这个是相邻的维度
hidden_dim = 512 # LSTM隐层向量的维度 : 512 / 4
depth = 8 # 栈式LSTM的深度
word = data_layer(name='word_data', size=word_dict_len)
predicate = data_layer(name='verb_data', size=pred_len)
ctx_n2 = data_layer(name='ctx_n2_data', size=word_dict_len)
ctx_n1 = data_layer(name='ctx_n1_data', size=word_dict_len)
ctx_0 = data_layer(name='ctx_0_data', size=word_dict_len)
ctx_p1 = data_layer(name='ctx_p1_data', size=word_dict_len)
ctx_p2 = data_layer(name='ctx_p2_data', size=word_dict_len)
mark = data_layer(name='mark_data', size=mark_dict_len)
if not is_predict:
target = data_layer(name='target', size=label_dict_len) # 标记序列只在训练和测试流程中定义
这里需要特别说明的是hidden_dim = 512指定了LSTM隐层向量的维度为128维,关于这一点请参考PaddlePaddle官方文档中lstmemory的说明。
将句子序列、谓词、谓词上下文、谓词上下文区域标记通过词表,转换为实向量表示的词向量序列。
+ +
# 在本教程中,我们加载了预训练的词向量,这里设置了:is_static=True
# is_static 为 True 时保证了在训练 SRL 模型过程中,词表不再更新
emb_para = ParameterAttribute(name='emb', initial_std=0., is_static=True)
word_input = [word, ctx_n2, ctx_n1, ctx_0, ctx_p1, ctx_p2]
emb_layers = [
embedding_layer(
size=word_dim, input=x, param_attr=emb_para) for x in word_input
]
emb_layers.append(predicate_embedding)
mark_embedding = embedding_layer(
name='word_ctx-in_embedding', size=mark_dim, input=mark, param_attr=std_0)
emb_layers.append(mark_embedding)
8个LSTM单元以“正向/反向”的顺序对所有输入序列进行学习。
+ +
# std_0 指定的参数以均值为0的高斯分布初始化,用在LSTM的bias初始化中
std_0 = ParameterAttribute(initial_std=0.)
hidden_0 = mixed_layer(
name='hidden0',
size=hidden_dim,
bias_attr=std_default,
input=[
full_matrix_projection(
input=emb, param_attr=std_default) for emb in emb_layers
])
lstm_0 = lstmemory(
name='lstm0',
input=hidden_0,
act=ReluActivation(),
gate_act=SigmoidActivation(),
state_act=SigmoidActivation(),
bias_attr=std_0,
param_attr=lstm_para_attr)
input_tmp = [hidden_0, lstm_0]
for i in range(1, depth):
mix_hidden = mixed_layer(
name='hidden' + str(i),
size=hidden_dim,
bias_attr=std_default,
input=[
full_matrix_projection(
input=input_tmp[0], param_attr=hidden_para_attr),
full_matrix_projection(
input=input_tmp[1], param_attr=lstm_para_attr)
])
lstm = lstmemory(
name='lstm' + str(i),
input=mix_hidden,
act=ReluActivation(),
gate_act=SigmoidActivation(),
state_act=SigmoidActivation(),
reverse=((i % 2) == 1),
bias_attr=std_0,
param_attr=lstm_para_attr)
input_tmp = [mix_hidden, lstm]
取最后一个栈式LSTM的输出和这个LSTM单元的输入到隐层映射,经过一个全连接层映射到标记字典的维度,得到最终的特征向量表示。
+ +
feature_out = mixed_layer(
name='output',
size=label_dict_len,
bias_attr=std_default,
input=[
full_matrix_projection(
input=input_tmp[0], param_attr=hidden_para_attr),
full_matrix_projection(
input=input_tmp[1], param_attr=lstm_para_attr)
], )
CRF层在网络的末端,完成序列标注。
+ +
crf_l = crf_layer(
name='crf',
size=label_dict_len,
input=feature_out,
label=target,
param_attr=ParameterAttribute(
name='crfw', initial_std=default_std, learning_rate=mix_hidden_lr))
执行sh train.sh
进行模型的训练,其中指定了总共需要训练150个pass。
paddle train \
--config=./db_lstm.py \
--save_dir=./output \
--trainer_count=1 \
--dot_period=500 \
--log_period=10 \
--num_passes=200 \
--use_gpu=false \
--show_parameter_stats_period=10 \
--test_all_data_in_one_period=1 \
2>&1 | tee 'train.log'
训练日志示例如下。
I1224 18:11:53.661479 1433 TrainerInternal.cpp:165] Batch=880 samples=145305 AvgCost=2.11541 CurrentCost=1.8645 Eval: __sum_evaluator_0__=0.607942 CurrentEval: __sum_evaluator_0__=0.59322
I1224 18:11:55.254021 1433 TrainerInternal.cpp:165] Batch=885 samples=146134 AvgCost=2.11408 CurrentCost=1.88156 Eval: __sum_evaluator_0__=0.607299 CurrentEval: __sum_evaluator_0__=0.494572
I1224 18:11:56.867604 1433 TrainerInternal.cpp:165] Batch=890 samples=146987 AvgCost=2.11277 CurrentCost=1.88839 Eval: __sum_evaluator_0__=0.607203 CurrentEval: __sum_evaluator_0__=0.590856
I1224 18:11:58.424069 1433 TrainerInternal.cpp:165] Batch=895 samples=147793 AvgCost=2.11129 CurrentCost=1.84247 Eval: __sum_evaluator_0__=0.607099 CurrentEval: __sum_evaluator_0__=0.588089
I1224 18:12:00.006893 1433 TrainerInternal.cpp:165] Batch=900 samples=148611 AvgCost=2.11148 CurrentCost=2.14526 Eval: __sum_evaluator_0__=0.607882 CurrentEval: __sum_evaluator_0__=0.749389
I1224 18:12:00.164089 1433 TrainerInternal.cpp:181] Pass=0 Batch=901 samples=148647 AvgCost=2.11195 Eval: __sum_evaluator_0__=0.60793
经过150个 pass 后,得到平均 error 约为 0.0516055。
训练好的个pass,会得到个模型,我们需要从中选择一个最优模型进行预测。通常做法是在开发集上进行调参,并基于我们关心的某个性能指标选择最优模型。本教程的predict.sh
脚本简单地选择了测试集上标记错误最少的那个pass(这里是pass-00100)用于预测。
预测时,我们需要将配置中的 crf_layer
删掉,替换为 crf_decoding_layer
,如下所示:
crf_dec_l = crf_decoding_layer(
name='crf_dec_l',
size=label_dict_len,
input=feature_out,
param_attr=ParameterAttribute(name='crfw'))
运行python predict.py
脚本,便可使用指定的模型进行预测。
python predict.py
-c db_lstm.py # 指定配置文件
-w output/pass-00100 # 指定预测使用的模型所在的路径
-l data/targetDict.txt # 指定标记的字典
-p data/verbDict.txt # 指定谓词的词典
-d data/wordDict.txt # 指定输入文本序列的字典
-i data/feature # 指定输入数据的路径
-o predict.res # 指定标记结果输出到文件的路径
预测结束后,在 - o 参数所指定的标记结果文件中,我们会得到如下格式的输出:每行是一条样本,以 “\t” 分隔的 2 列,第一列是输入文本,第二列是标记的结果。通过BIO标记可以直接得到论元的语义角色标签。
The interest-only securities were priced at 35 1\/2 to yield 10.72 % . B-A0 I-A0 I-A0 O O O O O O B-V B-A1 I-A1 O
语义角色标注是许多自然语言理解任务的重要中间步骤。这篇教程中我们以语义角色标注任务为例,介绍如何利用PaddlePaddle进行序列标注任务。教程中所介绍的模型来自我们发表的论文[10]。由于 CoNLL 2005 SRL任务的训练数据目前并非完全开放,教程中只使用测试数据作为示例。在这个过程中,我们希望减少对其它自然语言处理工具的依赖,利用神经网络数据驱动、端到端学习的能力,得到一个和传统方法可比、甚至更好的模型。在论文中我们证实了这种可能性。关于模型更多的信息和讨论可以在论文中找到。
+
本教程由PaddlePaddle创作,采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
机器翻译(machine translation, MT)是用计算机来实现不同语言之间翻译的技术。被翻译的语言通常称为源语言(source language),翻译成的结果语言称为目标语言(target language)。机器翻译即实现从源语言到目标语言转换的过程,是自然语言处理的重要研究领域之一。
早期机器翻译系统多为基于规则的翻译系统,需要由语言学家编写两种语言之间的转换规则,再将这些规则录入计算机。该方法对语言学家的要求非常高,而且我们几乎无法总结一门语言会用到的所有规则,更何况两种甚至更多的语言。因此,传统机器翻译方法面临的主要挑战是无法得到一个完备的规则集合[1]。
为解决以上问题,统计机器翻译(Statistical Machine Translation, SMT)技术应运而生。在统计机器翻译技术中,转化规则是由机器自动从大规模的语料中学习得到的,而非我们人主动提供规则。因此,它克服了基于规则的翻译系统所面临的知识获取瓶颈的问题,但仍然存在许多挑战:1)人为设计许多特征(feature),但永远无法覆盖所有的语言现象;2)难以利用全局的特征;3)依赖于许多预处理环节,如词语对齐、分词或符号化(tokenization)、规则抽取、句法分析等,而每个环节的错误会逐步累积,对翻译的影响也越来越大。
近年来,深度学习技术的发展为解决上述挑战提供了新的思路。将深度学习应用于机器翻译任务的方法大致分为两类:1)仍以统计机器翻译系统为框架,只是利用神经网络来改进其中的关键模块,如语言模型、调序模型等(见图1的左半部分);2)不再以统计机器翻译系统为框架,而是直接用神经网络将源语言映射到目标语言,即端到端的神经网络机器翻译(End-to-End Neural Machine Translation, End-to-End NMT)(见图1的右半部分),简称为NMT模型。
+
+图1. 基于神经网络的机器翻译系统
+
本教程主要介绍NMT模型,以及如何用PaddlePaddle来训练一个NMT模型。
以中英翻译(中文翻译到英文)的模型为例,当模型训练完毕时,如果输入如下已分词的中文句子:
这些 是 希望 的 曙光 和 解脱 的 迹象 .
0 -5.36816 these are signs of hope and relief . <e>
1 -6.23177 these are the light of hope and relief . <e>
2 -7.7914 these are the light of hope and the relief of hope . <e>
+
+图2. GRU(门控循环单元)
+
一般来说,具有短距离依赖属性的序列,其重置门比较活跃;相反,具有长距离依赖属性的序列,其更新门比较活跃。另外,Chung等人[3]通过多组实验表明,GRU虽然参数更少,但是在多个任务上都和LSTM有相近的表现。
我们已经在语义角色标注一章中介绍了一种双向循环神经网络,这里介绍Bengio团队在论文[2,4]中提出的另一种结构。该结构的目的是输入一个序列,得到其在每个时刻的特征表示,即输出的每个时刻都用定长向量表示到该时刻的上下文语义信息。
具体来说,该双向循环神经网络分别在时间维以顺序和逆序——即前向(forward)和后向(backward)——依次处理输入序列,并将每个时间步RNN的输出拼接成为最终的输出层。这样每个时间步的输出节点,都包含了输入序列中当前时刻完整的过去和未来的上下文信息。下图展示的是一个按时间步展开的双向循环神经网络。该网络包含一个前向和一个后向RNN,其中有六个权重矩阵:输入到前向隐层和后向隐层的权重矩阵(),隐层到隐层自己的权重矩阵(),前向隐层和后向隐层到输出层的权重矩阵()。注意,该网络的前向隐层和后向隐层之间没有连接。
+
+图3. 按时间步展开的双向循环神经网络
+
编码器-解码器(Encoder-Decoder)[2]框架用于解决由一个任意长度的源序列到另一个任意长度的目标序列的变换问题。即编码阶段将整个源序列编码成一个向量,解码阶段通过最大化预测序列概率,从中解码出整个目标序列。编码和解码的过程通常都使用RNN实现。
+
+图4. 编码器-解码器框架
+
编码阶段分为三步:
one-hot vector表示:将源语言句子的每个词表示成一个列向量。这个向量的维度与词汇表大小 相同,并且只有一个维度上有值1(该位置对应该词在词汇表中的位置),其余全是0。
映射到低维语义空间的词向量:one-hot vector表示存在两个问题,1)生成的向量维度往往很大,容易造成维数灾难;2)难以刻画词与词之间的关系(如语义相似性,也就是无法很好地表达语义)。因此,需再one-hot vector映射到低维的语义空间,由一个固定维度的稠密向量(称为词向量)表示。记映射矩阵为,用表示第个词的词向量,为向量维度。
用RNN编码源语言词序列:这一过程的计算公式为,其中是一个全零的向量,是一个非线性激活函数,最后得到的就是RNN依次读入源语言个词的状态编码序列。整句话的向量表示可以采用在最后一个时间步的状态编码,或使用时间维上的池化(pooling)结果。
第3步也可以使用双向循环神经网络实现更复杂的句编码表示,具体可以用双向GRU实现。前向GRU按照词序列的顺序依次编码源语言端词,并得到一系列隐层状态。类似的,后向GRU按照的顺序依次编码源语言端词,得到。最后对于词,通过拼接两个GRU的结果得到它的隐层状态,即。
+
+图5. 使用双向GRU的编码器
+
机器翻译任务的训练过程中,解码阶段的目标是最大化下一个正确的源语言词的概率。思路是:
每一个时刻,根据源语言句子的编码信息(又叫上下文向量,context vector)、真实目标语言序列的第个词和时刻RNN的隐层状态,计算出下一个隐层状态。计算公式如下:
+ +其中是一个非线性激活函数;是源语言句子的上下文向量,在不使用注意力机制时,如果编码器的输出是源语言句子编码后的最后一个元素,则可以定义;是目标语言序列的第个单词,是目标语言序列的开始标记<s>
,表示解码开始;是时刻解码RNN的隐层状态,是一个全零的向量。
将通过softmax
归一化,得到目标语言序列的第个单词的概率分布。概率分布公式如下:
其中是对每个可能的输出单词进行打分,再用softmax归一化就可以得到第个词的概率。
根据和计算代价。
机器翻译任务的生成过程,通俗来讲就是根据预先训练的模型来翻译源语言句子。生成过程中的解码阶段和上述训练过程的有所差异,具体介绍请见柱搜索算法。
如果编码阶段的输出是一个固定维度的向量,会带来以下两个问题:1)不论源语言序列的长度是5个词还是50个词,如果都用固定维度的向量去编码其中的语义和句法结构信息,对模型来说是一个非常高的要求,特别是对长句子序列而言;2)直觉上,当人类翻译一句话时,会对与当前译文更相关的源语言片段上给予更多关注,且关注点会随着翻译的进行而改变。而固定维度的向量则相当于,任何时刻都对源语言所有信息给予了同等程度的关注,这是不合理的。因此,Bahdanau等人[4]引入注意力(attention)机制,可以对编码后的上下文片段进行解码,以此来解决长句子的特征学习问题。下面介绍在注意力机制下的解码器结构。
与简单的解码器不同,这里的计算公式为:
可见,源语言句子的编码向量表示为第个词的上下文片段,即针对每一个目标语言中的词,都有一个特定的与之对应。的计算公式如下:
从公式中可以看出,注意力机制是通过对编码器中各时刻的RNN状态进行加权平均实现的。权重表示目标语言中第个词对源语言中第个词的注意力大小,的计算公式如下:
其中,可以看作是一个对齐模型,用来衡量目标语言中第个词和源语言中第个词的匹配程度。具体而言,这个程度是通过解码RNN的第个隐层状态和源语言句子的第个上下文片段计算得到的。传统的对齐模型中,目标语言的每个词明确对应源语言的一个或多个词(hard alignment);而在注意力模型中采用的是soft alignment,即任何两个目标语言和源语言词间均存在一定的关联,且这个关联强度是由模型计算得到的实数,因此可以融入整个NMT框架,并通过反向传播算法进行训练。
+
+图6. 基于注意力机制的解码器
+
柱搜索(beam search)是一种启发式图搜索算法,用于在图或树中搜索有限集合中的最优扩展节点,通常用在解空间非常大的系统(如机器翻译、语音识别)中,原因是内存无法装下图或树中所有展开的解。如在机器翻译任务中希望翻译“<s>你好<e>
”,就算目标语言字典中只有3个词(<s>
, <e>
, hello
),也可能生成无限句话(hello
循环出现的次数不定),为了找到其中较好的翻译结果,我们可采用柱搜索算法。
柱搜索算法使用广度优先策略建立搜索树,在树的每一层,按照启发代价(heuristic cost)(本教程中,为生成词的log概率之和)对节点进行排序,然后仅留下预先确定的个数(文献中通常称为beam width、beam size、柱宽度等)的节点。只有这些节点会在下一层继续扩展,其他节点就被剪掉了,也就是说保留了质量较高的节点,剪枝了质量较差的节点。因此,搜索所占用的空间和时间大幅减少,但缺点是无法保证一定获得最优解。
使用柱搜索算法的解码阶段,目标是最大化生成序列的概率。思路是:
softmax
归一化,得到目标语言序列的第个单词的概率分布。<e>
或超过句子的最大生成长度为止。注意:和的计算公式同解码器中的一样。且由于生成时的每一步都是通过贪心法实现的,因此并不能保证得到全局最优解。
本教程使用WMT-14数据集中的bitexts(after selection)作为训练集,dev+test data作为测试集和生成集。
在Linux下,只需简单地运行以下命令:
cd data
./wmt14_data.sh
+
文件夹名 | +法英平行语料文件 | +文件数 | +文件大小 | +
train | +ccb2_pc30.src, ccb2_pc30.trg, etc | +12 | +3.55G | +
test | +ntst1213.src, ntst1213.trg | +2 | +1636k | +
gen | +ntst14.src, ntst14.trg | +2 | +864k | +
XXX.src
是源法语文件,XXX.trg
是目标英语文件,文件中的每行存放一个句子XXX.src
和XXX.trg
的行数一致,且两者任意第行的句子之间都有着一一对应的关系。如果您想使用自己的数据集,只需按照如下方式组织,并将它们放在data
目录下:
user_dataset
├── train
│ ├── train_file1.src
│ ├── train_file1.trg
│ └── ...
├── test
│ ├── test_file1.src
│ ├── test_file1.trg
│ └── ...
├── gen
│ ├── gen_file1.src
│ ├── gen_file1.trg
│ └── ...
user_dataset
:用户自定义的数据集名字。train
、test
和gen
:必须使用这三个文件夹名字。.src
和.trg
。我们的预处理流程包括两步:
+- 将每个源语言到目标语言的平行语料库文件合并为一个文件:
+ - 合并每个XXX.src
和XXX.trg
文件为XXX
。
+ - XXX
中的第行内容为XXX.src
中的第行和XXX.trg
中的第行连接,用'\t'分隔。
+- 创建训练数据的“源字典”和“目标字典”。每个字典都有DICTSIZE个单词,包括:语料中词频最高的(DICTSIZE - 3)个单词,和3个特殊符号<s>
(序列的开始)、<e>
(序列的结束)和<unk>
(未登录词)。
预处理可以使用preprocess.py
:
python preprocess.py -i INPUT [-d DICTSIZE] [-m]
-i INPUT
:输入的原始数据集路径。-d DICTSIZE
:指定的字典单词数,如果没有设置,字典会包含输入数据集中的所有单词。-m --mergeDict
:合并“源字典”和“目标字典”,即这两个字典的内容完全一样。本教程的具体命令如下:
python preprocess.py -i data/wmt14 -d 30000
请耐心等待几分钟的时间,您会在屏幕上看到:
concat parallel corpora for dataset
build source dictionary for train data
build target dictionary for train data
dictionary size is 30000
预处理好的数据集存放在data/pre-wmt14
目录下:
pre-wmt14
├── train
│ └── train
├── test
│ └── test
├── gen
│ └── gen
├── train.list
├── test.list
├── gen.list
├── src.dict
└── trg.dict
train
、test
和gen
:分别包含了法英平行语料库的训练、测试和生成数据。其每个文件的每一行以“\t”分为两列,第一列是法语序列,第二列是对应的英语序列。train.list
、test.list
和gen.list
:分别记录了train
、test
和gen
文件夹中的文件路径。src.dict
和trg.dict
:源(法语)和目标(英语)字典。每个字典都含有30000个单词,包括29997个最高频单词和3个特殊符号。我们通过dataprovider.py
将数据提供给PaddlePaddle。具体步骤如下:
首先,引入PaddlePaddle的PyDataProvider2包,并定义三个特殊符号。
+ +
from paddle.trainer.PyDataProvider2 import *
UNK_IDX = 2 #未登录词
START = "<s>" #序列的开始
END = "<e>" #序列的结束
其次,使用初始化函数hook
,分别定义了训练模式和生成模式下的数据输入格式(input_types
)。
hook
函数中的src_dict_path
是源语言字典路径,trg_dict_path
是目标语言字典路径,is_generating
(训练或生成模式)是从模型配置中传入的对象。hook
函数的具体调用方式请见训练模型配置说明。
def hook(settings, src_dict_path, trg_dict_path, is_generating, file_list,
**kwargs):
# job_mode = 1: 训练模式;0: 生成模式
settings.job_mode = not is_generating
def fun(dict_path): # 根据字典路径加载字典
out_dict = dict()
with open(dict_path, "r") as fin:
out_dict = {
line.strip(): line_count
for line_count, line in enumerate(fin)
}
return out_dict
settings.src_dict = fun(src_dict_path)
settings.trg_dict = fun(trg_dict_path)
if settings.job_mode: #训练模式
settings.input_types = {
'source_language_word': #源语言序列
integer_value_sequence(len(settings.src_dict)),
'target_language_word': #目标语言序列
integer_value_sequence(len(settings.trg_dict)),
'target_language_next_word': #目标语言的下一个词序列
integer_value_sequence(len(settings.trg_dict))
}
else: #生成模式
settings.input_types = {
'source_language_word': #源语言序列
integer_value_sequence(len(settings.src_dict)),
'sent_id': #源语言序列编号
integer_value_sequence(len(open(file_list[0], "r").readlines()))
}
最后,使用process
函数打开文本文件file_name
,读取每一行,将行中的数据转换成与input_types
一致的格式,再用yield
关键字返回给PaddlePaddle进程。具体来说,
<s>
、末尾补上结束符号<e>
,得到“source_language_word”;<s>
,得到“target_language_word”;<e>
,作为目标语言的下一个词序列(“target_language_next_word”)。+ +
def _get_ids(s, dictionary): # 获得源语言序列中的每个单词在字典中的位置
words = s.strip().split()
return [dictionary[START]] + \
[dictionary.get(w, UNK_IDX) for w in words] + \
[dictionary[END]]
@provider(init_hook=hook, pool_size=50000)
def process(settings, file_name):
with open(file_name, 'r') as f:
for line_count, line in enumerate(f):
line_split = line.strip().split('\t')
if settings.job_mode and len(line_split) != 2:
continue
src_seq = line_split[0]
src_ids = _get_ids(src_seq, settings.src_dict)
if settings.job_mode:
trg_seq = line_split[1]
trg_words = trg_seq.split()
trg_ids = [settings.trg_dict.get(w, UNK_IDX) for w in trg_words]
# 如果任意一个序列长度超过80个单词,在训练模式下会移除这条样本,以防止RNN过深。
if len(src_ids) > 80 or len(trg_ids) > 80:
continue
trg_ids_next = trg_ids + [settings.trg_dict[END]]
trg_ids = [settings.trg_dict[START]] + trg_ids
yield {
'source_language_word': src_ids,
'target_language_word': trg_ids,
'target_language_next_word': trg_ids_next
}
else:
yield {'source_language_word': src_ids, 'sent_id': [line_count]}
注意:由于本示例中的训练数据有3.55G,对于内存较小的机器,不能一次性加载进内存,所以推荐使用pool_size
变量来设置内存中暂存的数据条数。
首先,定义数据集路径和源/目标语言字典路径,并用is_generating
变量定义当前配置是训练模式(默认)还是生成模式。该变量接受从命令行传入的参数,使用方法见应用命令与结果。
import os
from paddle.trainer_config_helpers import *
data_dir = "./data/pre-wmt14" # 数据集路径
src_lang_dict = os.path.join(data_dir, 'src.dict') # 源语言字典路径
trg_lang_dict = os.path.join(data_dir, 'trg.dict') # 目标语言字典路径
is_generating = get_config_arg("is_generating", bool, False) # 配置模式
其次,通过define_py_data_sources2
函数从dataprovider.py
中读取数据,并用args
变量传入源/目标语言的字典路径以及配置模式。
if not is_generating:
train_list = os.path.join(data_dir, 'train.list')
test_list = os.path.join(data_dir, 'test.list')
else:
train_list = None
test_list = os.path.join(data_dir, 'gen.list')
define_py_data_sources2(
train_list,
test_list,
module="dataprovider",
obj="process",
args={
"src_dict_path": src_lang_dict, # 源语言字典路径
"trg_dict_path": trg_lang_dict, # 目标语言字典路径
"is_generating": is_generating # 配置模式
})
settings(
learning_method = AdamOptimizer(),
batch_size = 50,
learning_rate = 5e-4)
本教程使用默认的SGD随机梯度下降算法和Adam学习方法,并指定学习率为5e-4。注意:生成模式下的batch_size = 50
,表示同时生成50条序列。
首先,定义了一些全局变量。
+ +
source_dict_dim = len(open(src_lang_dict, "r").readlines()) # 源语言字典维度
target_dict_dim = len(open(trg_lang_dict, "r").readlines()) # 目标语言字典维度
word_vector_dim = 512 # dimension of word vector # 词向量维度
encoder_size = 512 # 编码器中的GRU隐层大小
decoder_size = 512 # 解码器中的GRU隐层大小
if is_generating:
beam_size=3 # # 柱搜索算法中的宽度
max_length=250 # 生成句子的最大长度
gen_trans_file = get_config_arg("gen_trans_file", str, None) # 生成后的文件
其次,实现编码器框架。分为三步:
+ +2.1 传入已经在dataprovider.py
转换成one-hot vector表示的源语言序列。
+ +
src_word_id = data_layer(name='source_language_word', size=source_dict_dim)
2.2 将上述编码映射到低维语言空间的词向量。
+ ++ +
src_embedding = embedding_layer(
input=src_word_id,
size=word_vector_dim,
param_attr=ParamAttr(name='_source_language_embedding'))
2.3 用双向GRU编码源语言序列,拼接两个GRU的编码结果得到。
+ +
src_forward = simple_gru(input=src_embedding, size=encoder_size)
src_backward = simple_gru(
input=src_embedding, size=encoder_size, reverse=True)
encoded_vector = concat_layer(input=[src_forward, src_backward])
接着,定义基于注意力机制的解码器框架。分为三步:
+ +3.1 对源语言序列编码后的结果(见2.3),过一个前馈神经网络(Feed Forward Neural Network),得到其映射。
+ ++ +
with mixed_layer(size=decoder_size) as encoded_proj:
encoded_proj += full_matrix_projection(input=encoded_vector)
3.2 构造解码器RNN的初始状态。由于解码器需要预测时序目标序列,但在0时刻并没有初始值,所以我们希望对其进行初始化。这里采用的是将源语言序列逆序编码后的最后一个状态进行非线性映射,作为该初始值,即。
+ ++ +
backward_first = first_seq(input=src_backward)
with mixed_layer(
size=decoder_size,
act=TanhActivation(), ) as decoder_boot:
decoder_boot += full_matrix_projection(input=backward_first)
3.3 定义解码阶段每一个时间步的RNN行为,即根据当前时刻的源语言上下文向量、解码器隐层状态和目标语言中第个词,来预测第个词的概率。
+ +simple_attention
函数,实现公式。其中,enc_vec是,enc_proj是的映射(见3.1),权重的计算已经封装在simple_attention
函数中。gru_step_layer
函数,在decoder_inputs和decoder_mem上做了激活操作,即实现公式。
def gru_decoder_with_attention(enc_vec, enc_proj, current_word):
decoder_mem = memory(
name='gru_decoder', size=decoder_size, boot_layer=decoder_boot)
context = simple_attention(
encoded_sequence=enc_vec,
encoded_proj=enc_proj,
decoder_state=decoder_mem, )
with mixed_layer(size=decoder_size * 3) as decoder_inputs:
decoder_inputs += full_matrix_projection(input=context)
decoder_inputs += full_matrix_projection(input=current_word)
gru_step = gru_step_layer(
name='gru_decoder',
input=decoder_inputs,
output_mem=decoder_mem,
size=decoder_size)
with mixed_layer(
size=target_dict_dim, bias_attr=True,
act=SoftmaxActivation()) as out:
out += full_matrix_projection(input=gru_step)
return out
训练模式与生成模式下的解码器调用区别。
+ +4.1 定义解码器框架名字,和gru_decoder_with_attention
函数的前两个输入。注意:这两个输入使用StaticInput
,具体说明可见StaticInput文档。
+ +
decoder_group_name = "decoder_group"
group_input1 = StaticInput(input=encoded_vector, is_seq=True)
group_input2 = StaticInput(input=encoded_proj, is_seq=True)
group_inputs = [group_input1, group_input2]
4.2 训练模式下的解码器调用:
+ +gru_decoder_with_attention
函数。recurrent_group
函数循环调用gru_decoder_with_attention
函数。classification_cost
来计算损失值。+ +
if not is_generating:
trg_embedding = embedding_layer(
input=data_layer(
name='target_language_word', size=target_dict_dim),
size=word_vector_dim,
param_attr=ParamAttr(name='_target_language_embedding'))
group_inputs.append(trg_embedding)
decoder = recurrent_group(
name=decoder_group_name,
step=gru_decoder_with_attention,
input=group_inputs)
lbl = data_layer(name='target_language_next_word', size=target_dict_dim)
cost = classification_cost(input=decoder, label=lbl)
outputs(cost)
4.3 生成模式下的解码器调用:
+ +GeneratedInput
来自动完成这一过程。具体说明可见GeneratedInput文档。beam_search
函数循环调用gru_decoder_with_attention
函数,生成出序列id。seqtext_printer_evaluator
函数,根据目标字典trg_lang_dict
,打印出完整的句子保存在gen_trans_file
中。+ +
else:
trg_embedding = GeneratedInput(
size=target_dict_dim,
embedding_name='_target_language_embedding',
embedding_size=word_vector_dim)
group_inputs.append(trg_embedding)
beam_gen = beam_search(
name=decoder_group_name,
step=gru_decoder_with_attention,
input=group_inputs,
bos_id=0,
eos_id=1,
beam_size=beam_size,
max_length=max_length)
seqtext_printer_evaluator(
input=beam_gen,
id_input=data_layer(
name="sent_id", size=1),
dict_file=trg_lang_dict,
result_file=gen_trans_file)
outputs(beam_gen)
注意:我们提供的配置在Bahdanau的论文[4]上做了一些简化,可参考issue #1133。
可以通过以下命令来训练模型:
./train.sh
其中train.sh
的内容为:
paddle train \
--config='seqToseq_net.py' \
--save_dir='model' \
--use_gpu=false \
--num_passes=16 \
--show_parameter_stats_period=100 \
--trainer_count=4 \
--log_period=10 \
--dot_period=5 \
2>&1 | tee 'train.log'
训练的损失函数每隔10个batch打印一次,您将会看到如下消息:
I0719 19:16:45.952062 15563 TrainerInternal.cpp:160] Batch=10 samples=500 AvgCost=198.475 CurrentCost=198.475 Eval: classification_error_evaluator=0.737155 CurrentEval: classification_error_evaluator=0.737155
I0719 19:17:56.707319 15563 TrainerInternal.cpp:160] Batch=20 samples=1000 AvgCost=157.479 CurrentCost=116.483 Eval: classification_error_evaluator=0.698392 CurrentEval: classification_error_evaluator=0.659065
.....
当classification_error_evaluator的值低于0.35时,模型就训练成功了。
由于NMT模型的训练非常耗时,我们在50个物理节点(每节点含有2颗6核CPU)的集群中,花了5天时间训练了16个pass,其中每个pass耗时7个小时。因此,我们提供了一个预先训练好的模型(pass-00012)供大家直接下载使用。该模型大小为205MB,在所有16个模型中有最高的BLEU评估值26.92。下载并解压模型的命令如下:
cd pretrained
./wmt14_model.sh
可以通过以下命令来进行法英翻译:
./gen.sh
其中gen.sh
的内容为:
paddle train \
--job=test \
--config='seqToseq_net.py' \
--save_dir='pretrained/wmt14_model' \
--use_gpu=true \
--num_passes=13 \
--test_pass=12 \
--trainer_count=1 \
--config_args=is_generating=1,gen_trans_file="gen_result" \
2>&1 | tee 'translation/gen.log'
与训练命令不同的参数如下:
+- job:设置任务的模式为测试。
+- save_dir:设置存放预训练模型的路径。
+- num_passes和test_pass:加载第轮的模型参数,这里只加载 data/wmt14_model/pass-00012
。
+- config_args:将命令行中的自定义参数传递给模型配置。is_generating=1
表示当前为生成模式,gen_trans_file="gen_result"
表示生成结果的存储文件。
翻译结果请见效果展示。
BLEU(Bilingual Evaluation understudy)是一种广泛使用的机器翻译自动评测指标,由IBM的watson研究中心于2002年提出[5],基本出发点是:机器译文越接近专业翻译人员的翻译结果,翻译系统的性能越好。其中,机器译文与人工参考译文之间的接近程度,采用句子精确度(precision)的计算方法,即比较两者的n元词组相匹配的个数,匹配的个数越多,BLEU得分越好。
Moses 是一个统计学的开源机器翻译系统,我们使用其中的 multi-bleu.perl 来做BLEU评估。下载脚本的命令如下:
./moses_bleu.sh
BLEU评估可以使用eval_bleu
脚本如下,其中FILE为需要评估的文件名,BEAMSIZE为柱宽度,默认使用data/wmt14/gen/ntst14.trg
作为标准的翻译结果。
./eval_bleu.sh FILE BEAMSIZE
本教程的具体命令如下:
./eval_bleu.sh gen_result 3
您会在屏幕上看到:
BLEU = 26.92
端到端的神经网络机器翻译是近几年兴起的一种全新的机器翻译方法。本章中,我们介绍了NMT中典型的“编码器-解码器”框架和“注意力”机制。由于NMT是一个典型的Seq2Seq(Sequence to Sequence,序列到序列)学习问题,因此,Seq2Seq中的query改写(query rewriting)、摘要、单轮对话等问题都可以用本教程的模型来解决。
+
本教程由PaddlePaddle创作,采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
当我们学习编程的时候,编写的第一个程序一般是实现打印"Hello World"。而机器学习(或深度学习)的入门教程,一般都是 MNIST 数据库上的手写识别问题。原因是手写识别属于典型的图像分类问题,比较简单,同时MNIST数据集也很完备。MNIST数据集作为一个简单的计算机视觉数据集,包含一系列如图1所示的手写数字图片和对应的标签。图片是28x28的像素矩阵,标签则对应着0~9的10个数字。每张图片都经过了大小归一化和居中处理。
+
+图1. MNIST图片示例
+
MNIST数据集是从 NIST 的Special Database 3(SD-3)和Special Database 1(SD-1)构建而来。由于SD-3是由美国人口调查局的员工进行标注,SD-1是由美国高中生进行标注,因此SD-3比SD-1更干净也更容易识别。Yann LeCun等人从SD-1和SD-3中各取一半作为MNIST的训练集(60000条数据)和测试集(10000条数据),其中训练集来自250位不同的标注员,此外还保证了训练集和测试集的标注员是不完全相同的。
Yann LeCun早先在手写字符识别上做了很多研究,并在研究过程中提出了卷积神经网络(Convolutional Neural Network),大幅度地提高了手写字符的识别能力,也因此成为了深度学习领域的奠基人之一。如今的深度学习领域,卷积神经网络占据了至关重要的地位,从最早Yann LeCun提出的简单LeNet,到如今ImageNet大赛上的优胜模型VGGNet、GoogLeNet、ResNet等(请参见图像分类 教程),人们在图像分类领域,利用卷积神经网络得到了一系列惊人的结果。
有很多算法在MNIST上进行实验。1998年,LeCun分别用单层线性分类器、多层感知器(Multilayer Perceptron, MLP)和多层卷积神经网络LeNet进行实验,使得测试集上的误差不断下降(从12%下降到0.7%)[1]。此后,科学家们又基于K近邻(K-Nearest Neighbors)算法[2]、支持向量机(SVM)[3]、神经网络[4-7]和Boosting方法[8]等做了大量实验,并采用多种预处理方法(如去除歪曲、去噪、模糊等)来提高识别的准确率。
本教程中,我们从简单的模型Softmax回归开始,带大家入门手写字符识别,并逐步进行模型优化。
基于MNIST数据训练一个分类器,在介绍本教程使用的三个基本图像分类网络前,我们先给出一些定义:
+- 是输入:MNIST图片是 的二维图像,为了进行计算,我们将其转化为维向量,即。
+- 是输出:分类器的输出是10类数字(0-9),即,每一维代表图片分类为第类数字的概率。
+- 是图片的真实标签:也是10维,但只有一维为1,其他都为0。
最简单的Softmax回归模型是先将输入层经过一个全连接层得到的特征,然后直接通过softmax 函数进行多分类[9]。
输入层的数据传到输出层,在激活操作之前,会乘以相应的权重 ,并加上偏置变量 ,具体如下:
其中
对于有 个类别的多分类问题,指定 个输出节点, 维输入特征经过softmax将归一化为 个[0,1]范围内的实数值,分别表示该样本属于这 个类别的概率。此处的 即对应该图片为数字 的预测概率。
在分类问题中,我们一般采用交叉熵代价损失函数(cross entropy),公式如下:
图2为softmax回归的网络图,图中权重用黑线表示、偏置用红线表示、+1代表偏置参数的系数为1。
+
+图2. softmax回归网络结构图
+
Softmax回归模型采用了最简单的两层神经网络,即只有输入层和输出层,因此其拟合能力有限。为了达到更好的识别效果,我们考虑在输入层和输出层中间加上若干个隐藏层[10]。
图3为多层感知器的网络结构图,图中权重用黑线表示、偏置用红线表示、+1代表偏置参数的系数为1。
+
+图3. 多层感知器网络结构图
+
+
+图4. 卷积层图片
+
卷积层是卷积神经网络的核心基石。该层的参数由一组可学习的过滤器(也叫作卷积核)组成。在前向过程中,每个卷积核在输入层进行横向和纵向的扫描,与输入层对应扫描位置进行卷积,得到的结果加上偏置并用相应的激活函数进行激活,结果能够得到一个二维的激活图(activation map)。每个特定的卷积核都能得到特定的激活图(activation map),如有的卷积核可能对识别边角,有的可能识别圆圈,那这些卷积核可能对于对应的特征响应要强。
图4是卷积层的一个动态图。由于3D量难以表示,所有的3D量(输入的3D量(蓝色),权重3D量(红色),输出3D量(绿色))通过将深度在行上堆叠来表示。如图4,输入层是,我们常见的彩色图片其实就是类似这样的输入层,彩色图片的宽和高对应这里的和,而彩色图片有RGB三个颜色通道,对应这里的;卷积层的参数为,这里的是卷积核的数量,如图4中有和两个卷积核,对应卷积核的大小,图中和在每一层深度上都是的矩阵,对应卷积核扫描的步长,从动态图中可以看到,方框每次左移或下移2个单位,对应Padding扩展,是对输入层的扩展,图中输入层,原始数据为蓝色部分,可以看到灰色部分是进行了大小为1的扩展,用0来进行扩展;图4的动态可视化对输出层结果(绿色)进行迭代,显示每个输出元素是通过将突出显示的输入(蓝色)与滤波器(红色)进行元素相乘,将其相加,然后通过偏置抵消结果来计算的。
+
+图5. 池化层图片
+
池化是非线性下采样的一种形式,主要作用是通过减少网络的参数来减小计算量,并且能够在一定程度上控制过拟合。通常在卷积层的后面会加上一个池化层。池化包括最大池化、平均池化等。其中最大池化是用不重叠的矩形框将输入层分成不同的区域,对于每个矩形框的数取最大值作为输出层,如图5所示。
+
+图6. LeNet-5卷积神经网络结构
+
LeNet-5是一个最简单的卷积神经网络。图6显示了其结构:输入的二维图像,先经过两次卷积层到池化层,再经过全连接层,最后使用softmax分类作为输出层。卷积的如下三个特性,决定了LeNet-5能比同样使用全连接层的多层感知器更好地识别图像:
更详细的关于卷积神经网络的具体知识可以参考斯坦福大学公开课和图像分类教程。
sigmoid激活函数:
tanh激活函数:
+ +实际上,tanh函数只是规模变化的sigmoid函数,将sigmoid函数值放大2倍之后再向下平移1个单位:tanh(x) = 2sigmoid(2x) - 1 。
ReLU激活函数:
更详细的介绍请参考维基百科激活函数。
执行以下命令,下载MNIST数据库并解压缩,然后将训练集和测试集的地址分别写入train.list和test.list两个文件,供PaddlePaddle读取。
./data/get_mnist_data.sh
./load_data.py
# Define a py data provider
@provider(
input_types={'pixel': dense_vector(28 * 28),
'label': integer_value(10)})
def process(settings, filename): # settings is not used currently.
# 打开图片文件
with open( filename + "-images-idx3-ubyte", "rb") as f:
# 读取开头的四个参数,magic代表数据的格式,n代表数据的总量,rows和cols分别代表行数和列数
magic, n, rows, cols = struct.upack(">IIII", f.read(16))
# 以无符号字节为单位一个一个的读取数据
images = np.fromfile(
f, 'ubyte',
count=n * rows * cols).reshape(n, rows, cols).astype('float32')
# 将0~255的数据归一化到[-1,1]的区间
images = images / 255.0 * 2.0 - 1.0
# 打开标签文件
with open( filename + "-labels-idx1-ubyte", "rb") as l:
# 读取开头的两个参数
magic, n = struct.upack(">II", l.read(8))
# 以无符号字节为单位一个一个的读取数据
labels = np.fromfile(l, 'ubyte', count=n).astype("int")
for i in xrange(n):
yield {"pixel": images[i, :], 'label': labels[i]}
if not is_predict:
data_dir = './data/'
define_py_data_sources2(
train_list=data_dir + 'train.list',
test_list=data_dir + 'test.list',
module='mnist_provider',
obj='process')
settings(
batch_size=128,
learning_rate=0.1 / 128.0,
learning_method=MomentumOptimizer(0.9),
regularization=L2Regularization(0.0005 * 128))
data_size = 1 * 28 * 28
label_size = 10
img = data_layer(name='pixel', size=data_size)
predict = softmax_regression(img) # Softmax回归
#predict = multilayer_perceptron(img) #多层感知器
#predict = convolutional_neural_network(img) #LeNet5卷积神经网络
if not is_predict:
lbl = data_layer(name="label", size=label_size)
inputs(img, lbl)
outputs(classification_cost(input=predict, label=lbl))
else:
outputs(predict)
def softmax_regression(img):
predict = fc_layer(input=img, size=10, act=SoftmaxActivation())
return predict
def multilayer_perceptron(img):
# 第一个全连接层,激活函数为ReLU
hidden1 = fc_layer(input=img, size=128, act=ReluActivation())
# 第二个全连接层,激活函数为ReLU
hidden2 = fc_layer(input=hidden1, size=64, act=ReluActivation())
# 以softmax为激活函数的全连接输出层,输出层的大小必须为数字的个数10
predict = fc_layer(input=hidden2, size=10, act=SoftmaxActivation())
return predict
def convolutional_neural_network(img):
# 第一个卷积-池化层
conv_pool_1 = simple_img_conv_pool(
input=img,
filter_size=5,
num_filters=20,
num_channel=1,
pool_size=2,
pool_stride=2,
act=TanhActivation())
# 第二个卷积-池化层
conv_pool_2 = simple_img_conv_pool(
input=conv_pool_1,
filter_size=5,
num_filters=50,
num_channel=20,
pool_size=2,
pool_stride=2,
act=TanhActivation())
# 全连接层
fc1 = fc_layer(input=conv_pool_2, size=128, act=TanhActivation())
# 以softmax为激活函数的全连接输出层,输出层的大小必须为数字的个数10
predict = fc_layer(input=fc1, size=10, act=SoftmaxActivation())
return predict
config=mnist_model.py # 在mnist_model.py中可以选择网络
output=./softmax_mnist_model
log=softmax_train.log
paddle train \
--config=$config \ # 网络配置的脚本
--dot_period=10 \ # 每训练 `dot_period` 个批次后打印一个 `.`
--log_period=100 \ # 每隔多少batch打印一次日志
--test_all_data_in_one_period=1 \ # 每次测试是否用所有的数据
--use_gpu=0 \ # 是否使用GPU
--trainer_count=1 \ # 使用CPU或GPU的个数
--num_passes=100 \ # 训练进行的轮数(每次训练使用完所有数据为1轮)
--save_dir=$output \ # 模型存储的位置
2>&1 | tee $log
python -m paddle.utils.plotcurve -i $log > plot.png
I0117 12:52:29.628617 4538 TrainerInternal.cpp:165] Batch=100 samples=12800 AvgCost=2.63996 CurrentCost=2.63996 Eval: classification_error_evaluator=0.241172 CurrentEval: classification_error_evaluator=0.241172
.........
I0117 12:52:29.768741 4538 TrainerInternal.cpp:165] Batch=200 samples=25600 AvgCost=1.74027 CurrentCost=0.840582 Eval: classification_error_evaluator=0.185234 CurrentEval: classification_error_evaluator=0.129297
.........
I0117 12:52:29.916970 4538 TrainerInternal.cpp:165] Batch=300 samples=38400 AvgCost=1.42119 CurrentCost=0.783026 Eval: classification_error_evaluator=0.167786 CurrentEval: classification_error_evaluator=0.132891
.........
I0117 12:52:30.061213 4538 TrainerInternal.cpp:165] Batch=400 samples=51200 AvgCost=1.23965 CurrentCost=0.695054 Eval: classification_error_evaluator=0.160039 CurrentEval: classification_error_evaluator=0.136797
......I0117 12:52:30.223270 4538 TrainerInternal.cpp:181] Pass=0 Batch=469 samples=60000 AvgCost=1.1628 Eval: classification_error_evaluator=0.156233
I0117 12:52:30.366894 4538 Tester.cpp:109] Test samples=10000 cost=0.50777 Eval: classification_error_evaluator=0.0978
python plot_cost.py softmax_train.log
python evaluate.py softmax_train.log
+
+图7. softmax回归的误差曲线图
+
评估模型结果如下:
Best pass is 00013, testing Avgcost is 0.484447
The classification accuracy is 90.01%
+
+图8. 多层感知器的误差曲线图
+
评估模型结果如下:
Best pass is 00085, testing Avgcost is 0.164746
The classification accuracy is 94.95%
+
+图9. 卷积神经网络的误差曲线图
+
评估模型结果如下:
Best pass is 00076, testing Avgcost is 0.0244684
The classification accuracy is 99.20%
从评估结果可以看到,卷积神经网络的最好分类准确率达到惊人的99.20%。说明对于图像问题而言,卷积神经网络能够比一般的全连接网络达到更好的识别效果,而这与卷积层具有局部连接和共享权重的特性是分不开的。同时,从图9中可以看到,卷积神经网络在很早的时候就能达到很好的效果,说明其收敛速度非常快。
脚本 predict.py
可以对训练好的模型进行预测,例如softmax回归中:
python predict.py -c softmax_mnist.py -d data/raw_data/ -m softmax_mnist_model/pass-00047
根据提示,输入需要预测的图片序号,分类器能够给出各个数字的生成概率、预测的结果(取最大生成概率对应的数字)和实际的标签。
Input image_id [0~9999]: 3
Predicted probability of each digit:
[[ 1.00000000e+00 1.60381094e-28 1.60381094e-28 1.60381094e-28
1.60381094e-28 1.60381094e-28 1.60381094e-28 1.60381094e-28
1.60381094e-28 1.60381094e-28]]
Predict Number: 0
Actual Number: 0
从结果看出,该分类器接近100%地认为第3张图片上面的数字为0,而实际标签给出的类也确实如此。
本教程的softmax回归、多层感知器和卷积神经网络是最基础的深度学习模型,后续章节中复杂的神经网络都是从它们衍生出来的,因此这几个模型对之后的学习大有裨益。同时,我们也观察到从最简单的softmax回归变换到稍复杂的卷积神经网络的时候,MNIST数据集上的识别准确率有了大幅度的提升,原因是卷积层具有局部连接和共享权重的特性。在之后学习新模型的时候,希望大家也要深入到新模型相比原模型带来效果提升的关键之处。此外,本教程还介绍了PaddlePaddle模型搭建的基本流程,从dataprovider的编写、网络层的构建,到最后的训练和预测。对这个流程熟悉以后,大家就可以用自己的数据,定义自己的网络模型,并完成自己的训练和预测任务了。
+
本教程由PaddlePaddle创作,采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
在网络技术不断发展和电子商务规模不断扩大的背景下,商品数量和种类快速增长,用户需要花费大量时间才能找到自己想买的商品,这就是信息超载问题。为了解决这个难题,推荐系统(Recommender System)应运而生。
个性化推荐系统是信息过滤系统(Information Filtering System)的子集,它可以用在很多领域,如电影、音乐、电商和 Feed 流推荐等。推荐系统通过分析、挖掘用户行为,发现用户的个性化需求与兴趣特点,将用户可能感兴趣的信息或商品推荐给用户。与搜索引擎不同,推荐系统不需要用户准确地描述出自己的需求,而是根据分析历史行为建模,主动提供满足用户兴趣和需求的信息。
传统的推荐系统方法主要有:
其中协同过滤是应用最广泛的技术之一,它又可以分为多个子类:基于用户 (User-Based)的推荐[3] 、基于物品(Item-Based)的推荐[4]、基于社交网络关系(Social-Based)的推荐[5]、基于模型(Model-based)的推荐等。1994年明尼苏达大学推出的GroupLens系统[3]一般被认为是推荐系统成为一个相对独立的研究方向的标志。该系统首次提出了基于协同过滤来完成推荐任务的思想,此后,基于该模型的协同过滤推荐引领了推荐系统十几年的发展方向。
深度学习具有优秀的自动提取特征的能力,能够学习多层次的抽象特征表示,并对异质或跨域的内容信息进行学习,可以一定程度上处理推荐系统冷启动问题[6]。本教程主要介绍个性化推荐的深度学习模型,以及如何使用PaddlePaddle实现模型。
我们使用包含用户信息、电影信息与电影评分的数据集作为个性化推荐的应用场景。当我们训练好模型后,只需要输入对应的用户ID和电影ID,就可以得出一个匹配的分数(范围[1,5],分数越高视为兴趣越大),然后根据所有电影的推荐得分排序,推荐给用户可能感兴趣的电影。
Input movie_id: 1962
Input user_id: 1
Prediction Score is 4.25
+
+图1. YouTube 推荐系统结构
+
候选生成网络将推荐问题建模为一个类别数极大的多类分类问题:对于一个Youtube用户,使用其观看历史(视频ID)、搜索词记录(search tokens)、人口学信息(如地理位置、用户登录设备)、二值特征(如性别,是否登录)和连续特征(如用户年龄)等,对视频库中所有视频进行多分类,得到每一类别的分类结果(即每一个视频的推荐概率),最终输出概率较高的几百个视频。
首先,将观看历史及搜索词记录这类历史信息,映射为向量后取平均值得到定长表示;同时,输入人口学特征以优化新用户的推荐效果,并将二值特征和连续特征归一化处理到[0, 1]范围。接下来,将所有特征表示拼接为一个向量,并输入给非线形多层感知器(MLP,详见识别数字教程)处理。最后,训练时将MLP的输出给softmax做分类,预测时计算用户的综合特征(MLP的输出)与所有视频的相似度,取得分最高的个作为候选生成网络的筛选结果。图2显示了候选生成网络结构。
+
+图2. 候选生成网络结构
+
对于一个用户,预测此刻用户要观看的视频为视频的概率公式为:
其中为用户的特征表示,为视频库集合,为视频库中第个视频的特征表示。和为长度相等的向量,两者点积可以通过全连接层实现。
考虑到softmax分类的类别数非常多,为了保证一定的计算效率:1)训练阶段,使用负样本类别采样将实际计算的类别数缩小至数千;2)推荐(预测)阶段,忽略softmax的归一化计算(不影响结果),将类别打分问题简化为点积(dot product)空间中的最近邻(nearest neighbor)搜索问题,取与最近的个视频作为生成的候选。
排序网络的结构类似于候选生成网络,但是它的目标是对候选进行更细致的打分排序。和传统广告排序中的特征抽取方法类似,这里也构造了大量的用于视频排序的相关特征(如视频 ID、上次观看时间等)。这些特征的处理方式和候选生成网络类似,不同之处是排序网络的顶部是一个加权逻辑回归(weighted logistic regression),它对所有候选视频进行打分,从高到底排序后将分数较高的一些视频返回给用户。
在下文的电影推荐系统中:
首先,使用用户特征和电影特征作为神经网络的输入,其中:
+ +用户特征融合了四个属性信息,分别是用户ID、性别、职业和年龄。
电影特征融合了三个属性信息,分别是电影ID、电影类型ID和电影名称。
对用户特征,将用户ID映射为维度大小为256的向量表示,输入全连接层,并对其他三个属性也做类似的处理。然后将四个属性的特征表示分别全连接并相加。
对电影特征,将电影ID以类似用户ID的方式进行处理,电影类型ID以向量的形式直接输入全连接层,电影名称用文本卷积神经网络(详见第5章)得到其定长向量表示。然后将三个属性的特征表示分别全连接并相加。
得到用户和电影的向量表示后,计算二者的余弦相似度作为推荐系统的打分。最后,用该相似度打分和用户真实打分的差异的平方作为该回归模型的损失函数。
+
+图3. 融合推荐模型
+
我们以 MovieLens 百万数据集(ml-1m)为例进行介绍。ml-1m 数据集包含了 6,000 位用户对 4,000 部电影的 1,000,000 条评价(评分范围 1~5 分,均为整数),由 GroupLens Research 实验室搜集整理。
您可以运行 data/getdata.sh
下载数据,如果数椐获取成功,您将在目录data/ml-1m
中看到下面的文件:
movies.dat ratings.dat users.dat README
电影ID::电影名称::电影类型
用户ID::电影ID::评分::时间戳
用户ID::性别::年龄::职业::邮编
首先安装 Python 第三方库(推荐使用 Virtualenv):
pip install -r data/requirements.txt
其次在预处理./preprocess.sh
过程中,我们将字段配置文件data/config.json
转化为meta配置文件meta_config.json
,并生成对应的meta文件meta.bin
,以完成数据文件的序列化。然后再将ratings.dat
分为训练集、测试集两部分,把它们的地址写入train.list
和test.list
。
运行成功后目录./data
新增以下文件:
meta_config.json meta.bin ratings.dat.train ratings.dat.test train.list test.list
我们使用 Python 接口传递数据给系统,下面 dataprovider.py
给出了完整示例。
from paddle.trainer.PyDataProvider2 import *
from common_utils import meta_to_header
def __list_to_map__(lst): # 将list转为map
ret_val = dict()
for each in lst:
k, v = each
ret_val[k] = v
return ret_val
def hook(settings, meta, **kwargs): # 读取meta.bin
# 定义电影特征
movie_headers = list(meta_to_header(meta, 'movie'))
settings.movie_names = [h[0] for h in movie_headers]
headers = movie_headers
# 定义用户特征
user_headers = list(meta_to_header(meta, 'user'))
settings.user_names = [h[0] for h in user_headers]
headers.extend(user_headers)
# 加载评分信息
headers.append(("rating", dense_vector(1)))
settings.input_types = __list_to_map__(headers)
settings.meta = meta
@provider(init_hook=hook, cache=CacheType.CACHE_PASS_IN_MEM)
def process(settings, filename):
with open(filename, 'r') as f:
for line in f:
# 从评分文件中读取评分
user_id, movie_id, score = map(int, line.split('::')[:-1])
# 将评分平移到[-2, +2]范围内的整数
score = float(score - 3)
movie_meta = settings.meta['movie'][movie_id]
user_meta = settings.meta['user'][user_id]
# 添加电影ID与电影特征
outputs = [('movie_id', movie_id - 1)]
for i, each_meta in enumerate(movie_meta):
outputs.append((settings.movie_names[i + 1], each_meta))
# 添加用户ID与用户特征
outputs.append(('user_id', user_id - 1))
for i, each_meta in enumerate(user_meta):
outputs.append((settings.user_names[i + 1], each_meta))
# 添加评分
outputs.append(('rating', [score]))
# 将数据返回给 paddle
yield __list_to_map__(outputs)
加载meta.bin
文件并定义通过define_py_data_sources2
从dataprovider中读入数据:
from paddle.trainer_config_helpers import *
try:
import cPickle as pickle
except ImportError:
import pickle
is_predict = get_config_arg('is_predict', bool, False)
META_FILE = 'data/meta.bin'
# 加载 meta 文件
with open(META_FILE, 'rb') as f:
meta = pickle.load(f)
if not is_predict:
define_py_data_sources2(
'data/train.list',
'data/test.list',
module='dataprovider',
obj='process',
args={'meta': meta})
这里我们设置了batch size、网络初始学习率和RMSProp自适应优化方法。
settings(
batch_size=1600, learning_rate=1e-3, learning_method=RMSPropOptimizer())
定义数据输入和参数维度。
+ +
movie_meta = meta['movie']['__meta__']['raw_meta']
user_meta = meta['user']['__meta__']['raw_meta']
movie_id = data_layer('movie_id', size=movie_meta[0]['max']) # 电影ID
title = data_layer('title', size=len(movie_meta[1]['dict'])) # 电影名称
genres = data_layer('genres', size=len(movie_meta[2]['dict'])) # 电影类型
user_id = data_layer('user_id', size=user_meta[0]['max']) # 用户ID
gender = data_layer('gender', size=len(user_meta[1]['dict'])) # 用户性别
age = data_layer('age', size=len(user_meta[2]['dict'])) # 用户年龄
occupation = data_layer('occupation', size=len(user_meta[3]['dict'])) # 用户职业
embsize = 256 # 向量维度
构造“电影”特征。
+ +
# 电影ID和电影类型分别映射到其对应的特征隐层(256维)。
movie_id_emb = embedding_layer(input=movie_id, size=embsize)
movie_id_hidden = fc_layer(input=movie_id_emb, size=embsize)
genres_emb = fc_layer(input=genres, size=embsize)
# 对于电影名称,一个ID序列表示的词语序列,在输入卷积层后,
# 将得到每个时间窗口的特征(序列特征),然后通过在时间维度
# 降采样得到固定维度的特征,整个过程在text_conv_pool实现
title_emb = embedding_layer(input=title, size=embsize)
title_hidden = text_conv_pool(
input=title_emb, context_len=5, hidden_size=embsize)
# 将三个属性的特征表示分别全连接并相加,结果即是电影特征的最终表示
movie_feature = fc_layer(
input=[movie_id_hidden, title_hidden, genres_emb], size=embsize)
构造“用户”特征。
+ +
# 将用户ID,性别,职业,年龄四个属性分别映射到其特征隐层。
user_id_emb = embedding_layer(input=user_id, size=embsize)
user_id_hidden = fc_layer(input=user_id_emb, size=embsize)
gender_emb = embedding_layer(input=gender, size=embsize)
gender_hidden = fc_layer(input=gender_emb, size=embsize)
age_emb = embedding_layer(input=age, size=embsize)
age_hidden = fc_layer(input=age_emb, size=embsize)
occup_emb = embedding_layer(input=occupation, size=embsize)
occup_hidden = fc_layer(input=occup_emb, size=embsize)
# 同样将这四个属性分别全连接并相加形成用户特征的最终表示。
user_feature = fc_layer(
input=[user_id_hidden, gender_hidden, age_hidden, occup_hidden],
size=embsize)
计算余弦相似度,定义损失函数和网络输出。
+ +
similarity = cos_sim(a=movie_feature, b=user_feature, scale=2)
# 训练时,采用regression_cost作为损失函数计算回归误差代价,并作为网络的输出。
# 预测时,网络的输出即为余弦相似度。
if not is_predict:
lbl=data_layer('rating', size=1)
cost=regression_cost(input=similarity, label=lbl)
outputs(cost)
else:
outputs(similarity)
执行sh train.sh
开始训练模型,将日志写入文件 log.txt
并打印在屏幕上。其中指定了总共需要执行 50 个pass。
set -e
paddle train \
--config=trainer_config.py \ # 神经网络配置文件
--save_dir=./output \ # 模型保存路径
--use_gpu=false \ # 是否使用GPU(默认不使用)
--trainer_count=4\ # 一台机器上面的线程数量
--test_all_data_in_one_period=true \ # 每个训练周期训练一次所有数据,否则每个训练周期测试batch_size个batch数据
--log_period=100 \ # 训练log_period个batch后打印日志
--dot_period=1 \ # 每训练dot_period个batch后打印一个"."
--num_passes=50 2>&1 | tee 'log.txt'
成功的输出类似如下:
I0117 01:01:48.585651 9998 TrainerInternal.cpp:165] Batch=100 samples=160000 AvgCost=0.600042 CurrentCost=0.600042 Eval: CurrentEval:
...................................................................................................
I0117 01:02:53.821918 9998 TrainerInternal.cpp:165] Batch=200 samples=320000 AvgCost=0.602855 CurrentCost=0.605668 Eval: CurrentEval:
...................................................................................................
I0117 01:03:58.937922 9998 TrainerInternal.cpp:165] Batch=300 samples=480000 AvgCost=0.605199 CurrentCost=0.609887 Eval: CurrentEval:
...................................................................................................
I0117 01:05:04.083251 9998 TrainerInternal.cpp:165] Batch=400 samples=640000 AvgCost=0.608693 CurrentCost=0.619175 Eval: CurrentEval:
...................................................................................................
I0117 01:06:09.155859 9998 TrainerInternal.cpp:165] Batch=500 samples=800000 AvgCost=0.613273 CurrentCost=0.631591 Eval: CurrentEval:
.................................................................I0117 01:06:51.109654 9998 TrainerInternal.cpp:181]
Pass=49 Batch=565 samples=902826 AvgCost=0.614772 Eval:
I0117 01:07:04.205142 9998 Tester.cpp:115] Test samples=97383 cost=0.721995 Eval:
I0117 01:07:04.205281 9998 GradientMachine.cpp:113] Saving parameters to ./output/pass-00049
在训练了几轮以后,您可以对模型进行评估。运行以下命令,可以通过选择最小训练误差的一轮参数得到最好轮次的模型。
./evaluate.py log.txt
您将看到:
Best pass is 00036, error is 0.719281, which means predict get error as 0.424052
evaluating from pass output/pass-00036
预测任何用户对于任何一部电影评价的命令如下:
python prediction.py 'output/pass-00036/'
预测程序将读取用户的输入,然后输出预测分数。您会看到如下命令行界面:
Input movie_id: 1962
Input user_id: 1
Prediction Score is 4.25
本章介绍了传统的推荐系统方法和YouTube的深度神经网络推荐系统,并以电影推荐为例,使用PaddlePaddle训练了一个个性化推荐神经网络模型。推荐系统几乎涵盖了电商系统、社交网络、广告推荐、搜索引擎等领域的方方面面,而在图像处理、自然语言处理等领域已经发挥重要作用的深度学习技术,也将会在推荐系统领域大放异彩。
+
本教程由PaddlePaddle创作,采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
在自然语言处理中,情感分析一般是指判断一段文本所表达的情绪状态。其中,一段文本可以是一个句子,一个段落或一个文档。情绪状态可以是两类,如(正面,负面),(高兴,悲伤);也可以是三类,如(积极,消极,中性)等等。情感分析的应用场景十分广泛,如把用户在购物网站(亚马逊、天猫、淘宝等)、旅游网站、电影评论网站上发表的评论分成正面评论和负面评论;或为了分析用户对于某一产品的整体使用感受,抓取产品的用户评论并进行情感分析等等。表格1展示了对电影评论进行情感分析的例子:
电影评论 | +类别 | +
---|---|
在冯小刚这几年的电影里,算最好的一部的了 | +正面 | +
很不好看,好像一个地方台的电视剧 | +负面 | +
圆方镜头全程炫技,色调背景美则美矣,但剧情拖沓,口音不伦不类,一直努力却始终无法入戏 | +负面 | +
剧情四星。但是圆镜视角加上婺源的风景整个非常有中国写意山水画的感觉,看得实在太舒服了。。 | +正面 | +
表格 1 电影评论情感分析
+
+图1. 卷积神经网络文本分类模型
+
假设待处理句子的长度为,其中第个词的词向量(word embedding)为,为维度大小。
首先,进行词向量的拼接操作:将每个词拼接起来形成一个大小为的词窗口,记为,它表示词序列的拼接,其中,表示词窗口中第一个词在整个句子中的位置,取值范围从到,。
其次,进行卷积操作:把卷积核(kernel)应用于包含个词的窗口,得到特征,其中为偏置项(bias),为非线性激活函数,如。将卷积核应用于句子中所有的词窗口,产生一个特征图(feature map):
接下来,对特征图采用时间维度上的最大池化(max pooling over time)操作得到此卷积核对应的整句话的特征,它是特征图中所有元素的最大值:
在实际应用中,我们会使用多个卷积核来处理句子,窗口大小相同的卷积核堆叠起来形成一个矩阵(上文中的单个卷积核参数相当于矩阵的某一行),这样可以更高效的完成运算。另外,我们也可使用窗口大小不同的卷积核来处理句子(图1作为示意画了四个卷积核,不同颜色表示不同大小的卷积核操作)。
最后,将所有卷积核得到的特征拼接起来即为文本的定长向量表示,对于文本分类问题,将其连接至softmax即构建出完整的模型。
对于一般的短文本分类问题,上文所述的简单的文本卷积网络即可达到很高的正确率[1]。若想得到更抽象更高级的文本特征表示,可以构建深层文本卷积神经网络[2,3]。
循环神经网络是一种能对序列数据进行精确建模的有力工具。实际上,循环神经网络的理论计算能力是图灵完备的[4]。自然语言是一种典型的序列数据(词序列),近年来,循环神经网络及其变体(如long short term memory[5]等)在自然语言处理的多个领域,如语言模型、句法解析、语义角色标注(或一般的序列标注)、语义表示、图文生成、对话、机器翻译等任务上均表现优异甚至成为目前效果最好的方法。
+
+图2. 循环神经网络按时间展开的示意图
+
循环神经网络按时间展开后如图2所示:在第时刻,网络读入第个输入(向量表示)及前一时刻隐层的状态值(向量表示,一般初始化为向量),计算得出本时刻隐层的状态值,重复这一步骤直至读完所有输入。如果将循环神经网络所表示的函数记为,则其公式可表示为:
其中是输入到隐层的矩阵参数,是隐层到隐层的矩阵参数,为隐层的偏置向量(bias)参数,为函数。
在处理自然语言时,一般会先将词(one-hot表示)映射为其词向量(word embedding)表示,然后再作为循环神经网络每一时刻的输入。此外,可以根据实际需要的不同在循环神经网络的隐层上连接其它层。如,可以把一个循环神经网络的隐层输出连接至下一个循环神经网络的输入构建深层(deep or stacked)循环神经网络,或者提取最后一个时刻的隐层状态作为句子表示进而使用分类模型等等。
对于较长的序列数据,循环神经网络的训练过程中容易出现梯度消失或爆炸现象[6]。为了解决这一问题,Hochreiter S, Schmidhuber J. (1997)提出了LSTM(long short term memory[5])。
相比于简单的循环神经网络,LSTM增加了记忆单元、输入门、遗忘门及输出门。这些门及记忆单元组合起来大大提升了循环神经网络处理长序列数据的能力。若将基于LSTM的循环神经网络表示的函数记为,则其公式为:
由下列公式组合而成[7]:
+
+图3. 时刻的LSTM [7]
+
LSTM通过给简单的循环神经网络增加记忆及控制门的方式,增强了其处理远距离依赖问题的能力。类似原理的改进还有Gated Recurrent Unit (GRU)[8],其设计更为简洁一些。这些改进虽然各有不同,但是它们的宏观描述却与简单的循环神经网络一样(如图2所示),即隐状态依据当前输入及前一时刻的隐状态来改变,不断地循环这一过程直至输入处理完毕:
其中,可以表示简单的循环神经网络、GRU或LSTM。
对于正常顺序的循环神经网络,包含了时刻之前的输入信息,也就是上文信息。同样,为了得到下文信息,我们可以使用反方向(将输入逆序处理)的循环神经网络。结合构建深层循环神经网络的方法(深层神经网络往往能得到更抽象和高级的特征表示),我们可以通过构建更加强有力的基于LSTM的栈式双向循环神经网络[9],来对时序数据进行建模。
如图4所示(以三层为例),奇数层LSTM正向,偶数层LSTM反向,高一层的LSTM使用低一层LSTM及之前所有层的信息作为输入,对最高层LSTM序列使用时间维度上的最大池化即可得到文本的定长向量表示(这一表示充分融合了文本的上下文信息,并且对文本进行了深层次抽象),最后我们将文本表示连接至softmax构建分类模型。
+
+图4. 栈式双向LSTM用于文本分类
+
我们以IMDB情感分析数据集为例进行介绍。IMDB数据集的训练集和测试集分别包含25000个已标注过的电影评论。其中,负面评论的得分小于等于4,正面评论的得分大于等于7,满分10分。您可以使用下面的脚本下载 IMDB 数椐集和Moses工具:
./data/get_imdb.sh
如果数椐获取成功,您将在目录data
中看到下面的文件:
aclImdb get_imdb.sh imdb mosesdecoder-master
我们使用的预处理脚本为preprocess.py
。该脚本会调用Moses工具中的tokenizer.perl
脚本来切分单词和标点符号,并会将训练集随机打乱排序再构建字典。注意:我们只使用已标注的训练集和测试集。执行下面的命令就可以预处理数椐:
data_dir="./data/imdb"
python preprocess.py -i $data_dir
运行成功后目录./data/pre-imdb
结构如下:
dict.txt labels.list test.list test_part_000 train.list train_part_000
PaddlePaddle可以读取Python写的传输数据脚本,下面dataprovider.py
文件给出了完整例子,主要包括两部分:
integer_value_sequence
,类别被定义为整数integer_value
。'\t\t'
分隔的类别ID和文本信息,并用yield关键字返回。
from paddle.trainer.PyDataProvider2 import *
def hook(settings, dictionary, **kwargs):
settings.word_dict = dictionary
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)))
@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': word_slot,
'label': label
}
trainer_config.py
是一个配置文件的例子。
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}) # 额外的参数,这里指定词典。
settings(
batch_size=128,
learning_rate=2e-3,
learning_method=AdamOptimizer(),
regularization=L2Regularization(8e-4),
gradient_clipping_threshold=25)
我们用PaddlePaddle实现了两种文本分类算法,分别基于上文所述的文本卷积神经网络和[栈式双向LSTM](#栈式双向LSTM(Stacked Bidirectional LSTM))。
def convolution_net(input_dim,
class_dim=2,
emb_dim=128,
hid_dim=128,
is_predict=False):
# 网络输入: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
方法就实现了卷积和池化操作,卷积核的数量为hidden_size参数。
def stacked_lstm_net(input_dim,
class_dim=2,
emb_dim=128,
hid_dim=512,
stacked_num=3,
is_predict=False):
# LSTM的层数stacked_num为奇数,确保最高层LSTM正向
assert stacked_num % 2 == 1
# 设置神经网络层的属性
layer_attr = ExtraLayerAttribute(drop_rate=0.5)
# 设置参数的属性
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.)
# 激活函数
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)))
我们的模型配置trainer_config.py
默认使用stacked_lstm_net
网络,如果要使用convolution_net
,注释相应的行即可。
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
脚本可以开启本地的训练:
./train.sh
train.sh内容如下:
paddle train --config=trainer_config.py \
--save_dir=./model_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'
如果运行成功,输出日志保存在 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
测试是指使用训练出的模型评估已标记的数据集。
./test.sh
测试脚本test.sh
的内容如下,其中函数get_best_pass
通过对分类错误率进行排序来获得最佳模型:
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'
与训练不同,测试时需要指定--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
脚本提供了一个预测接口。预测IMDB中未标记评论的示例如下:
./predict.sh
predict.sh的内容如下(注意应该确保默认模型路径model_output/pass-00002
存在或更改为其它模型路径):
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大小。本示例的预测结果:
Loading parameters from model_output/pass-00002/
predicting label is pos
10007_10.txt
在路径./data/aclImdb/test/pos
下面,而这里预测的标签也是pos,说明预测正确。
本章我们以情感分析为例,介绍了使用深度学习的方法进行端对端的短文本分类,并且使用PaddlePaddle完成了全部相关实验。同时,我们简要介绍了两种文本处理模型:卷积神经网络和循环神经网络。在后续的章节中我们会看到这两种基本的深度学习模型在其它任务上的应用。
+
本教程由PaddlePaddle创作,采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
本章我们介绍词的向量表征,也称为word embedding。词向量是自然语言处理中常见的一个操作,是搜索引擎、广告系统、推荐系统等互联网服务背后常见的基础技术。
在这些互联网服务里,我们经常要比较两个词或者两段文本之间的相关性。为了做这样的比较,我们往往先要把词表示成计算机适合处理的方式。最自然的方式恐怕莫过于向量空间模型(vector space model)。
+在这种方式里,每个词被表示成一个实数向量(one-hot vector),其长度为字典大小,每个维度对应一个字典里的每个词,除了这个词对应维度上的值是1,其他元素都是0。
One-hot vector虽然自然,但是用处有限。比如,在互联网广告系统里,如果用户输入的query是“母亲节”,而有一个广告的关键词是“康乃馨”。虽然按照常理,我们知道这两个词之间是有联系的——母亲节通常应该送给母亲一束康乃馨;但是这两个词对应的one-hot vectors之间的距离度量,无论是欧氏距离还是余弦相似度(cosine similarity),由于其向量正交,都认为这两个词毫无相关性。 得出这种与我们相悖的结论的根本原因是:每个词本身的信息量都太小。所以,仅仅给定两个词,不足以让我们准确判别它们是否相关。要想精确计算相关性,我们还需要更多的信息——从大量数据里通过机器学习方法归纳出来的知识。
在机器学习领域里,各种“知识”被各种模型表示,词向量模型(word embedding model)就是其中的一类。通过词向量模型可将一个 one-hot vector映射到一个维度更低的实数向量(embedding vector),如。在这个映射到的实数向量表示中,希望两个语义(或用法)上相似的词对应的词向量“更像”,这样如“母亲节”和“康乃馨”的对应词向量的余弦相似度就不再为零了。
词向量模型可以是概率模型、共生矩阵(co-occurrence matrix)模型或神经元网络模型。在用神经网络求词向量之前,传统做法是统计一个词语的共生矩阵。是一个 大小的矩阵,表示在所有语料中,词汇表V
(vocabulary)中第i个词和第j个词同时出现的词数,为词汇表的大小。对做矩阵分解(如奇异值分解,Singular Value Decomposition [5]),得到的即视为所有词的词向量:
但这样的传统做法有很多问题:
+1) 由于很多词没有出现,导致矩阵极其稀疏,因此需要对词频做额外处理来达到好的矩阵分解效果;
+2) 矩阵非常大,维度太高(通常达到的数量级);
+3) 需要手动去掉停用词(如although, a,...),不然这些频繁出现的词也会影响矩阵分解的效果。
基于神经网络的模型不需要计算存储一个在全语料上统计的大表,而是通过学习语义信息得到词向量,因此能很好地解决以上问题。在本章里,我们将展示基于神经网络训练词向量的细节,以及如何用PaddlePaddle训练一个词向量模型。
本章中,当词向量训练好后,我们可以用数据可视化算法t-SNE[4]画出词语特征在二维上的投影(如下图所示)。从图中可以看出,语义相关的词语(如a, the, these; big, huge)在投影上距离很近,语意无关的词(如say, business; decision, japan)在投影上的距离很远。
+
+ 图1. 词向量的二维投影
+
另一方面,我们知道两个向量的余弦值在的区间内:两个完全相同的向量余弦值为1, 两个相互垂直的向量之间余弦值为0,两个方向完全相反的向量余弦值为-1,即相关性和余弦值大小成正比。因此我们还可以计算两个词向量的余弦相似度:
similarity: 0.899180685161
+please input two words: big huge
+
+please input two words: from company
+similarity: -0.0997506977351
+
以上结果可以通过运行calculate_dis.py
, 加载字典里的单词和对应训练特征结果得到,我们将在应用模型中详细描述用法。
在这里我们介绍三个训练词向量的模型:N-gram模型,CBOW模型和Skip-gram模型,它们的中心思想都是通过上下文得到一个词出现的概率。对于N-gram模型,我们会先介绍语言模型的概念,并在之后的训练模型中,带大家用PaddlePaddle实现它。而后两个模型,是近年来最有名的神经元词向量模型,由 Tomas Mikolov 在Google 研发[3],虽然它们很浅很简单,但训练效果很好。
在介绍词向量模型之前,我们先来引入一个概念:语言模型。
+语言模型旨在为语句的联合概率函数建模, 其中表示句子中的第i个词。语言模型的目标是,希望模型对有意义的句子赋予大概率,对没意义的句子赋予小概率。
+这样的模型可以应用于很多领域,如机器翻译、语音识别、信息检索、词性标注、手写识别等,它们都希望能得到一个连续序列的概率。 以信息检索为例,当你在搜索“how long is a football bame”时(bame是一个医学名词),搜索引擎会提示你是否希望搜索“how long is a football game”, 这是因为根据语言模型计算出“how long is a football bame”的概率很低,而与bame近似的,可能引起错误的词中,game会使该句生成的概率最大。
对语言模型的目标概率,如果假设文本中每个词都是相互独立的,则整句话的联合概率可以表示为其中所有词语条件概率的乘积,即:
然而我们知道语句中的每个词出现的概率都与其前面的词紧密相关, 所以实际上通常用条件概率表示语言模型:
在计算语言学中,n-gram是一种重要的文本表示方法,表示一个文本中连续的n个项。基于具体的应用场景,每一项可以是一个字母、单词或者音节。 n-gram模型也是统计语言模型中的一种重要方法,用n-gram训练语言模型时,一般用每个n-gram的历史n-1个词语组成的内容来预测第n个词。
Yoshua Bengio等科学家就于2003年在著名论文 Neural Probabilistic Language Models [1] 中介绍如何学习一个神经元网络表示的词向量模型。文中的神经概率语言模型(Neural Network Language Model,NNLM)通过一个线性映射和一个非线性隐层连接,同时学习了语言模型和词向量,即通过学习大量语料得到词语的向量表达,通过这些向量得到整个句子的概率。用这种方法学习语言模型可以克服维度灾难(curse of dimensionality),即训练和测试数据不同导致的模型不准。注意:由于“神经概率语言模型”说法较为泛泛,我们在这里不用其NNLM的本名,考虑到其具体做法,本文中称该模型为N-gram neural model。
我们在上文中已经讲到用条件概率建模语言模型,即一句话中第个词的概率和该句话的前个词相关。可实际上越远的词语其实对该词的影响越小,那么如果考虑一个n-gram, 每个词都只受其前面n-1
个词的影响,则有:
给定一些真实语料,这些语料中都是有意义的句子,N-gram模型的优化目标则是最大化目标函数:
其中表示根据历史n-1个词得到当前词的条件概率,表示参数正则项。
+
+图2. N-gram神经网络模型
+
图2展示了N-gram神经网络模型,从下往上看,该模型分为以下几个部分:
对于每个样本,模型输入, 输出句子第t个词为字典中|V|
个词的概率。
每个输入词首先通过映射矩阵映射到词向量。
然后所有词语的词向量连接成一个大向量,并经过一个非线性映射得到历史词语的隐层表示:
+ +其中,为所有词语的词向量连接成的大向量,表示文本历史特征;、、、和分别为词向量层到隐层连接的参数。表示未经归一化的所有输出单词概率,表示未经归一化的字典中第个单词的输出概率。
根据softmax的定义,通过归一化, 生成目标词的概率为:
+ +整个网络的损失值(cost)为多类分类交叉熵,用公式表示为
+ +其中表示第个样本第类的真实标签(0或1),表示第i个样本第k类softmax输出的概率。
CBOW模型通过一个词的上下文(各N个词)预测当前词。当N=2时,模型如下图所示:
+
+图3. CBOW模型
+
具体来说,不考虑上下文的词语输入顺序,CBOW是用上下文词语的词向量的均值来预测当前词。即:
其中为第个词的词向量,分类分数(score)向量 ,最终的分类采用softmax,损失函数采用多类分类交叉熵。
CBOW的好处是对上下文词语的分布在词向量上进行了平滑,去掉了噪声,因此在小数据集上很有效。而Skip-gram的方法中,用一个词预测其上下文,得到了当前词上下文的很多样本,因此可用于更大的数据集。
+
+图4. Skip-gram模型
+
如上图所示,Skip-gram模型的具体做法是,将一个词的词向量映射到个词的词向量(表示当前输入词的前后各个词),然后分别通过softmax得到这个词的分类损失值之和。
本教程使用Penn Tree Bank (PTB)数据集。PTB数据集较小,训练速度快,应用于Mikolov的公开语言模型训练工具[2]中。其统计情况如下:
+
训练数据 | +验证数据 | +测试数据 | +
ptb.train.txt | +ptb.valid.txt | +ptb.test.txt | +
42068句 | +3370句 | +3761句 | +
执行以下命令,可下载该数据集,并分别将训练数据和验证数据输入train.list
和test.list
文件中,供PaddlePaddle训练时使用。
./data/getdata.sh
+
使用initializer函数进行dataprovider的初始化,包括字典的建立(build_dict函数中)和PaddlePaddle输入字段的格式定义。注意:这里N为n-gram模型中的n
, 本章代码中,定义, 表示在PaddlePaddle训练时,每条数据的前4个词用来预测第5个词。大家也可以根据自己的数据和需求自行调整N,但调整的同时要在模型配置文件中加入/减少相应输入字段。
from paddle.trainer.PyDataProvider2 import *
import collections
import logging
import pdb
logging.basicConfig(
format='[%(levelname)s %(asctime)s %(filename)s:%(lineno)s] %(message)s', )
logger = logging.getLogger('paddle')
logger.setLevel(logging.INFO)
N = 5 # Ngram
cutoff = 50 # select words with frequency > cutoff to dictionary
def build_dict(ftrain, fdict):
sentences = []
with open(ftrain) as fin:
for line in fin:
line = ['<s>'] + line.strip().split() + ['<e>']
sentences += line
wordfreq = collections.Counter(sentences)
wordfreq = filter(lambda x: x[1] > cutoff, wordfreq.items())
dictionary = sorted(wordfreq, key = lambda x: (-x[1], x[0]))
words, _ = list(zip(*dictionary))
for word in words:
print >> fdict, word
word_idx = dict(zip(words, xrange(len(words))))
logger.info("Dictionary size=%s" %len(words))
return word_idx
def initializer(settings, srcText, dictfile, **xargs):
with open(dictfile, 'w') as fdict:
settings.dicts = build_dict(srcText, fdict)
input_types = []
for i in xrange(N):
input_types.append(integer_value(len(settings.dicts)))
settings.input_types = input_types
使用process函数中将数据逐一提供给PaddlePaddle。具体来说,将每句话前面补上N-1个开始符号 <s>
, 末尾补上一个结束符号 <e>
,然后以N为窗口大小,从头到尾每次向右滑动窗口并生成一条数据。
+ +
@provider(init_hook=initializer)
def process(settings, filename):
UNKID = settings.dicts['<unk>']
with open(filename) as fin:
for line in fin:
line = ['<s>']*(N-1) + line.strip().split() + ['<e>']
line = [settings.dicts.get(w, UNKID) for w in line]
for i in range(N, len(line) + 1):
yield line[i-N: i]
如"I have a dream" 一句提供了5条数据:
+ +++
<s> <s> <s> <s> I
+<s> <s> <s> I have
+<s> <s> I have a
+<s> I have a dream
+I have a dream <e>
通过define_py_data_sources2
函数从dataprovider中读入数据,其中args指定了训练文本(srcText)和词汇表(dictfile)。
from paddle.trainer_config_helpers import *
+import math
+
+args = {'srcText': 'data/simple-examples/data/ptb.train.txt',
+ 'dictfile': 'data/vocabulary.txt'}
+
+define_py_data_sources2(
+ train_list="data/train.list",
+ test_list="data/test.list",
+ module="dataprovider",
+ obj="process",
+ args=args)
+
在这里,我们指定了模型的训练参数, L2正则项系数、学习率和batch size。
settings(
+ batch_size=100, regularization=L2Regularization(8e-4), learning_rate=3e-3)
+
本配置的模型结构如下图所示:
+
+ 图5. 模型配置中的N-gram神经网络模型
+
定义参数维度和和数据输入。
+ +
dictsize = 1953 # 字典大小
embsize = 32 # 词向量维度
hiddensize = 256 # 隐层维度
firstword = data_layer(name = "firstw", size = dictsize)
secondword = data_layer(name = "secondw", size = dictsize)
thirdword = data_layer(name = "thirdw", size = dictsize)
fourthword = data_layer(name = "fourthw", size = dictsize)
nextword = data_layer(name = "fifthw", size = dictsize)
将之前的个词 ,通过的矩阵映射到D维词向量(本例中取D=32)。
+ +
def wordemb(inlayer):
wordemb = table_projection(
input = inlayer,
size = embsize,
param_attr=ParamAttr(name = "_proj",
initial_std=0.001, # 参数初始化标准差
l2_rate= 0,)) # 词向量不需要稀疏化,因此其l2_rate设为0
return wordemb
Efirst = wordemb(firstword)
Esecond = wordemb(secondword)
Ethird = wordemb(thirdword)
Efourth = wordemb(fourthword)
接着,将这n-1个词向量经过concat_layer连接成一个大向量作为历史文本特征。
+ +
contextemb = concat_layer(input = [Efirst, Esecond, Ethird, Efourth])
然后,将历史文本特征经过一个全连接得到文本隐层特征。
+ +
hidden1 = fc_layer(
input = contextemb,
size = hiddensize,
act = SigmoidActivation(),
layer_attr = ExtraAttr(drop_rate=0.5),
bias_attr = ParamAttr(learning_rate = 2),
param_attr = ParamAttr(
initial_std = 1./math.sqrt(embsize*8),
learning_rate = 1))
最后,将文本隐层特征,再经过一个全连接,映射成一个维向量,同时通过softmax归一化得到这|V|
个词的生成概率。
# use context embedding to predict nextword
predictword = fc_layer(
input = hidden1,
size = dictsize,
bias_attr = ParamAttr(learning_rate = 2),
act = SoftmaxActivation())
网络的损失函数为多分类交叉熵,可直接调用classification_cost
函数。
cost = classification_cost(
input = predictword,
label = nextword)
# network input and output
outputs(cost)
模型训练命令为./train.sh
。脚本内容如下,其中指定了总共需要执行30个pass。
paddle train \
--config ngram.py \
--use_gpu=1 \
--dot_period=100 \
--log_period=3000 \
--test_period=0 \
--save_dir=model \
--num_passes=30
一个pass的训练日志如下所示:
.............................
I1222 09:27:16.477841 12590 TrainerInternal.cpp:162] Batch=3000 samples=300000 AvgCost=5.36135 CurrentCost=5.36135 Eval: classification_error_evaluator=0.818653 CurrentEval: class
ification_error_evaluator=0.818653
.............................
I1222 09:27:22.416700 12590 TrainerInternal.cpp:162] Batch=6000 samples=600000 AvgCost=5.29301 CurrentCost=5.22467 Eval: classification_error_evaluator=0.814542 CurrentEval: class
ification_error_evaluator=0.81043
.............................
I1222 09:27:28.343756 12590 TrainerInternal.cpp:162] Batch=9000 samples=900000 AvgCost=5.22494 CurrentCost=5.08876 Eval: classification_error_evaluator=0.810088 CurrentEval: class
ification_error_evaluator=0.80118
..I1222 09:27:29.128582 12590 TrainerInternal.cpp:179] Pass=0 Batch=9296 samples=929600 AvgCost=5.21786 Eval: classification_error_evaluator=0.809647
I1222 09:27:29.627616 12590 Tester.cpp:111] Test samples=73760 cost=4.9594 Eval: classification_error_evaluator=0.79676
I1222 09:27:29.627713 12590 GradientMachine.cpp:112] Saving parameters to model/pass-00000
经过30个pass,我们将得到平均错误率为classification_error_evaluator=0.735611。
训练模型后,我们可以加载模型参数,用训练出来的词向量初始化其他模型,也可以将模型参数从二进制格式转换成文本格式进行后续应用。
训练好的模型参数可以用来初始化其他模型。具体方法如下:
+在PaddlePaddle 训练命令行中,用--init_model_path
来定义初始化模型的位置,用--load_missing_parameter_strategy
指定除了词向量以外的新模型其他参数的初始化策略。注意,新模型需要和原模型共享被初始化参数的参数名。
PaddlePaddle训练出来的参数为二进制格式,存储在对应训练pass的文件夹下。这里我们提供了文件format_convert.py
用来互转PaddlePaddle训练结果的二进制文件和文本格式特征文件。
python format_convert.py --b2t -i INPUT -o OUTPUT -d DIM
其中,INPUT是输入的(二进制)词向量模型名称,OUTPUT是输出的文本模型名称,DIM是词向量参数维度。
用法如:
python format_convert.py --b2t -i model/pass-00029/_proj -o model/pass-00029/_proj.txt -d 32
转换后得到的文本文件如下:
0,4,62496
-0.7444070,-0.1846171,-1.5771370,0.7070392,2.1963732,-0.0091410, ......
-0.0721337,-0.2429973,-0.0606297,0.1882059,-0.2072131,-0.7661019, ......
......
其中,第一行是PaddlePaddle 输出文件的格式说明,包含3个属性:
+1) PaddlePaddle的版本号,本例中为0;
+2) 浮点数占用的字节数,本例中为4;
+3) 总计的参数个数, 本例中为62496(即1953*32);
+第二行及之后的每一行都按顺序表示字典里一个词的特征,用逗号分隔。
我们可以对词向量进行修改,并转换成PaddlePaddle参数二进制格式,方法:
python format_convert.py --t2b -i INPUT -o OUTPUT
其中,INPUT是输入的输入的文本词向量模型名称,OUTPUT是输出的二进制词向量模型名称
输入的文本格式如下(注意,不包含上面二进制转文本后第一行的格式说明):
-0.7444070,-0.1846171,-1.5771370,0.7070392,2.1963732,-0.0091410, ......
-0.0721337,-0.2429973,-0.0606297,0.1882059,-0.2072131,-0.7661019, ......
......
两个向量之间的距离可以用余弦值来表示,余弦值在的区间内,向量间余弦值越大,其距离越近。这里我们在calculate_dis.py
中实现不同词语的距离度量。
+用法如下:
python calculate_dis.py VOCABULARY EMBEDDINGLAYER`
其中,VOCABULARY
是字典,EMBEDDINGLAYER
是词向量模型,示例如下:
python calculate_dis.py data/vocabulary.txt model/pass-00029/_proj.txt
本章中,我们介绍了词向量、语言模型和词向量的关系、以及如何通过训练神经网络模型获得词向量。在信息检索中,我们可以根据向量间的余弦夹角,来判断query和文档关键词这二者间的相关性。在句法分析和语义分析中,训练好的词向量可以用来初始化模型,以得到更好的效果。在文档分类中,有了词向量之后,可以用聚类的方法将文档中同义词进行分组。希望大家在本章后能够自行运用词向量进行相关领域的研究。
+
本教程由PaddlePaddle创作,采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。