# 文本分类
-文本分类是机器学习中的一项常见任务,主要目的是根据一条文本的内容,判断该文本所属的类别。在本例子中,我们利用有标注的语料库训练二分类DNN和CNN模型,完成对输入文本的分类任务。
-DNN与CNN模型之间最大的区别在于:
+以下是本例目录包含的文件以及对应说明(`images` 文件夹以及 `index.html` 与使用无关可不关心):
+
+```text
+.
+├── images
+│ ├── cnn_net.png
+│ └── dnn_net.png
+├── index.html
+├── infer.py # 预测任务脚本
+├── network_conf.py # 本例中涉及的各种网络结构均定义在此文件中,希望进一步修改模型结构,请修改此文件
+├── reader.py # 读取数据接口,若使用自定义格式的数据,可直接修改此文件
+├── README.md # 文档
+├── run.sh # 运行此脚本,可以以默认参数直接开始训练任务
+├── train.py # 训练任务脚本
+└── utils.py # 定义通用的函数,例如:打印日志、解析命令行参数、构建字典、加载字典等
+```
-- DNN不属于序列模型,大多使用基本的全连接结构,只能接受固定维度的特征向量作为输入。
+## 简介
-- CNN属于序列模型,能够提取一个局部区域之内的特征,能够处理变长的序列输入。
+文本分类任务根据给定一条文本的内容,判断该文本所属的类别,是自然语言处理领域的一项重要的基础任务。[PaddleBook](https://github.com/PaddlePaddle/book) 中的[情感分类](https://github.com/PaddlePaddle/book/blob/develop/06.understand_sentiment/README.cn.md)一课,正是一个典型的文本分类任务,任务流程如下:
-举例来说,情感分类是一项常见的文本分类任务,在情感分类中,我们希望训练一个模型来判断句子中表现出的情感是正向还是负向。例如,"The apple is not bad",其中的"not bad"是决定这个句子情感的关键。
+1. 收集电影评论网站的用户评论数据。
+2. 清洗,标记。
+3. 模型设计。
+4. 模型学习效果评估。
-- 对于DNN模型来说,只能知道句子中有一个"not"和一个"bad",但两者之间的顺序关系在输入时已经丢失,网络不再有机会学习序列之间的顺序信息。
+训练好的分类器能够**自动判断**新出现的用户评论的情感是正面还是负面,在舆情监控、营销策划、产品品牌价值评估等任务中,能够起到重要作用。以上过程也是我们去完成一个新的文本分类任务需要遵循的常规流程。可以看到,深度学习方法的巨大优势体现在:**免除复杂的特征的设计,只需要对原始文本进行基础的清理、标注即可**。
-- CNN模型接受文本序列作为输入,保留了"not bad"之间的顺序信息。因此,在大多数文本分类任务上,CNN模型的表现要好于DNN。
+[PaddleBook](https://github.com/PaddlePaddle/book) 中的[情感分类](https://github.com/PaddlePaddle/book/blob/develop/06.understand_sentiment/README.cn.md)介绍了一个较为复杂的栈式双向 LSTM 模型,循环神经网络在一些需要理解语言语义的复杂任务中有着明显的优势,但计算量大,通常对调参技巧也有着更高的要求。在对计算时间有一定限制的任务中,也会考虑其它模型。除了计算时间的考量,更重要的一点:**模型选择往往是机器学习任务成功的基础**。机器学习任务的目标始终是提高泛化能力,也就是对未知的新的样本预测的能力:
-## 实验数据
-本例子的实验在[IMDB数据集](http://ai.stanford.edu/%7Eamaas/data/sentiment/aclImdb_v1.tar.gz)上进行。IMDB数据集包含了来自IMDB(互联网电影数据库)网站的5万条电影影评,并被标注为正面/负面两种评价。数据集被划分为train和test两部分,各2.5万条数据,正负样本的比例基本为1:1。样本直接以英文原文的形式表示。
+1. 简单模型拟合能力不足,无法精确拟合训练样本,更加无法期待模型能够准确地预测没有出现在训练样本集中的未知样本,这就是**欠拟合**问题。
+2. 然而,过于复杂的模型轻松“记忆”了训练样本集中的每一个样本,但对于没有出现在训练样本集中的未知样本却毫无识别能力,这就是**过拟合**问题。
-## DNN模型
+"No Free Lunch (NFL)" 是机器学习任务基本原则之一:没有任何一种模型是天生优于其他模型的。模型的设计和选择建立在了解不同模型特性的基础之上,但同时也是一个多次实验评估的过程。在本例中,我们继续向大家介绍几种最常用的文本分类模型,它们的能力和复杂程度不同,帮助大家对比学习这些模型学习效果之间的差异,针对不同的场景选择使用。
-**DNN的模型结构入下图所示:**
+### DNN 模型与 CNN 模型
-
-![](images/dnn_net.png)
-图1. DNN文本分类模型
-
+`network_conf.py` 中包括以下模型:
-**可以看到,模型主要分为如下几个部分:**
+1. `fc_net`: DNN 模型,是一个非序列模型。使用基本的全连接结构。
+2. `convolution_net`:浅层 CNN 模型,是一个基础的序列模型,能够处理变长的序列输入,提取一个局部区域之内的特征。
-- **词向量层**:IMDB的样本由原始的英文单词组成,为了更好地表示不同词之间语义上的关系,首先将英文单词转化为固定维度的向量。训练完成后,词与词语义上的相似程度可以用它们的词向量之间的距离来表示,语义上越相似,距离越近。关于词向量的更多信息请参考PaddleBook中的[词向量](https://github.com/PaddlePaddle/book/tree/develop/04.word2vec)一节。
+我们以情感分类任务为例,简单说明序列模型和非序列模型之间的差异。情感分类是一项常见的文本分类任务,模型自动判断文本中表现出的情感是正向还是负向。以句子 "The apple is not bad" 为例,"not bad" 是决定这个句子情感的关键:
-- **最大池化层**:最大池化在时间序列上进行,池化过程消除了不同语料样本在单词数量多少上的差异,并提炼出词向量中每一下标位置上的最大值。经过池化后,词向量层输出的向量序列被转化为一条固定维度的向量。例如,假设最大池化前向量的序列为`[[2,3,5],[7,3,6],[1,4,0]]`,则最大池化的结果为:`[7,4,6]`。
+- 对于 DNN 模型来说,只知道句子中有一个 "not" 和一个 "bad",两者之间的顺序关系在输入网络时已丢失,网络不再有机会学习序列之间的顺序信息。
+- CNN 模型接受文本序列作为输入,保留了 "not bad" 之间的顺序信息。
-- **全连接隐层**:经过最大池化后的向量被送入两个连续的隐层,隐层之间为全连接结构。
+两者各自的一些特点简单总结如下:
+1. DNN 的计算量可以远低于 CNN / RNN 模型,在对响应时间有要求的任务中具有优势。
+2. DNN 刻画的往往是频繁词特征,潜在会受到分词错误的影响,但对一些依赖关键词特征也能做的不错的任务:如 Spam 短信检测,依然是一个有效的模型。
+3. 在大多数需要一定语义理解(例如,借助上下文消除语义中的歧义)的文本分类任务上,以 CNN / RNN 为代表的序列模型的效果往往好于 DNN 模型。
-- **输出层**:输出层的神经元数量和样本的类别数一致,例如在二分类问题中,输出层会有2个神经元。通过Softmax激活函数,输出结果是一个归一化的概率分布,和为1,因此第$i$个神经元的输出就可以认为是样本属于第$i$类的预测概率。
+## 模型详解
+### 1. DNN 模型
-**通过PaddlePaddle实现该DNN结构的代码如下:**
+**DNN 模型结构入下图所示:**
-```python
-import paddle.v2 as paddle
+
+![](images/dnn_net.png)
+图1. 本例中的 DNN 文本分类模型
+
-def fc_net(dict_dim, class_dim=2, emb_dim=28):
- """
- dnn network definition
-
- :param dict_dim: size of word dictionary
- :type input_dim: int
- :params class_dim: number of instance class
- :type class_dim: int
- :params emb_dim: embedding vector dimension
- :type emb_dim: int
- """
+在 PaddlePaddle 实现该 DNN 结构的代码见 `network_conf.py` 中的 `fc_net` 函数,模型主要分为如下几个部分:
- # input layers
- data = paddle.layer.data("word",
- paddle.data_type.integer_value_sequence(dict_dim))
- lbl = paddle.layer.data("label", paddle.data_type.integer_value(class_dim))
-
- # embedding layer
- emb = paddle.layer.embedding(input=data, size=emb_dim)
- # max pooling
- seq_pool = paddle.layer.pooling(
- input=emb, pooling_type=paddle.pooling.Max())
-
- # two hidden layers
- hd_layer_size = [28, 8]
- hd_layer_init_std = [1.0 / math.sqrt(s) for s in hd_layer_size]
- hd1 = paddle.layer.fc(
- input=seq_pool,
- size=hd_layer_size[0],
- act=paddle.activation.Tanh(),
- param_attr=paddle.attr.Param(initial_std=hd_layer_init_std[0]))
- hd2 = paddle.layer.fc(
- input=hd1,
- size=hd_layer_size[1],
- act=paddle.activation.Tanh(),
- param_attr=paddle.attr.Param(initial_std=hd_layer_init_std[1]))
-
- # output layer
- output = paddle.layer.fc(
- input=hd2,
- size=class_dim,
- act=paddle.activation.Softmax(),
- param_attr=paddle.attr.Param(initial_std=1.0 / math.sqrt(class_dim)))
-
- cost = paddle.layer.classification_cost(input=output, label=lbl)
-
- return cost, output, lbl
+- **词向量层**:为了更好地表示不同词之间语义上的关系,首先将词语转化为固定维度的向量。训练完成后,词与词语义上的相似程度可以用它们的词向量之间的距离来表示,语义上越相似,距离越近。关于词向量的更多信息请参考PaddleBook中的[词向量](https://github.com/PaddlePaddle/book/tree/develop/04.word2vec)一节。
-```
-该DNN模型默认对输入的语料进行二分类(`class_dim=2`),embedding的词向量维度默认为28(`emd_dim=28`),两个隐层均使用Tanh激活函数(`act=paddle.activation.Tanh()`)。
+- **最大池化层**:最大池化在时间序列上进行,池化过程消除了不同语料样本在单词数量多少上的差异,并提炼出词向量中每一下标位置上的最大值。经过池化后,词向量层输出的向量序列被转化为一条固定维度的向量。例如,假设最大池化前向量的序列为`[[2,3,5],[7,3,6],[1,4,0]]`,则最大池化的结果为:`[7,4,6]`。
-需要注意的是,该模型的输入数据为整数序列,而不是原始的英文单词序列。事实上,为了处理方便我们一般会事先将单词根据词频顺序进行id化,即将单词用整数替代, 也就是单词在字典中的序号。这一步一般在DNN模型之外完成。
+- **全连接隐层**:经过最大池化后的向量被送入两个连续的隐层,隐层之间为全连接结构。
-## CNN模型
+- **输出层**:输出层的神经元数量和样本的类别数一致,例如在二分类问题中,输出层会有2个神经元。通过Softmax激活函数,输出结果是一个归一化的概率分布,和为1,因此第$i$个神经元的输出就可以认为是样本属于第$i$类的预测概率。
-**CNN的模型结构如下图所示:**
+该 DNN 模型默认对输入的语料进行二分类(`class_dim=2`),embedding(词向量)维度默认为28(`emd_dim=28`),两个隐层均使用Tanh激活函数(`act=paddle.activation.Tanh()`)。需要注意的是,该模型的输入数据为整数序列,而不是原始的单词序列。事实上,为了处理方便,我们一般会事先将单词根据词频顺序进行 id 化,即将词语转化成在字典中的序号。
+
+## 2. CNN 模型
+
+**CNN 模型结构如下图所示:**
![](images/cnn_net.png)
-图2. CNN文本分类模型
+图2. 本例中的 CNN 文本分类模型
-**可以看到,模型主要分为如下几个部分:**
+通过 PaddlePaddle 实现该 CNN 结构的代码见 `network_conf.py` 中的 `convolution_net` 函数,模型主要分为如下几个部分:
-- **词向量层**:与DNN中词向量层的作用一样,将英文单词转化为固定维度的向量,利用向量之间的距离来表示词之间的语义相关程度。如图2中所示,将得到的词向量定义为行向量,再将语料中所有的单词产生的行向量拼接在一起组成矩阵。假设词向量维度为5,语料“The cat sat on the read mat”包含7个单词,那么得到的矩阵维度为7*5。关于词向量的更多信息请参考PaddleBook中的[词向量](https://github.com/PaddlePaddle/book/tree/develop/04.word2vec)一节。
+- **词向量层**:与 DNN 中词向量层的作用一样,将词语转化为固定维度的向量,利用向量之间的距离来表示词之间的语义相关程度。如图2所示,将得到的词向量定义为行向量,再将语料中所有的单词产生的行向量拼接在一起组成矩阵。假设词向量维度为5,句子 “The cat sat on the read mat” 含 7 个词语,那么得到的矩阵维度为 7*5。关于词向量的更多信息请参考 PaddleBook 中的[词向量](https://github.com/PaddlePaddle/book/tree/develop/04.word2vec)一节。
-- **卷积层**: 文本分类中的卷积在时间序列上进行,即卷积核的宽度和词向量层产出的矩阵一致,卷积沿着矩阵的高度方向进行。卷积后得到的结果被称为“特征图”(feature map)。假设卷积核的高度为$h$,矩阵的高度为$N$,卷积的步长为1,则得到的特征图为一个高度为$N+1-h$的向量。可以同时使用多个不同高度的卷积核,得到多个特征图。
+- **卷积层**: 文本分类中的卷积在时间序列上进行,即卷积核的宽度和词向量层产出的矩阵一致,卷积沿着矩阵的高度方向进行。卷积后得到的结果被称为“特征图”(feature map)。假设卷积核的高度为 $h$,矩阵的高度为 $N$,卷积的步长为 1,则得到的特征图为一个高度为 $N+1-h$ 的向量。可以同时使用多个不同高度的卷积核,得到多个特征图。
-- **最大池化层**: 对卷积得到的各个特征图分别进行最大池化操作。由于特征图本身已经是向量,因此这里的最大池化实际上就是简单地选出各个向量中的最大元素。各个最大元素又被拼接在一起,组成新的向量,显然,该向量的维度等于特征图的数量,也就是卷积核的数量。举例来说,假设我们使用了四个不同的卷积核,卷积产生的特征图分别为:`[2,3,5]`、`[8,2,1]`、`[5,7,7,6]`和`[4,5,1,8]`,由于卷积核的高度不同,因此产生的特征图尺寸也有所差异。分别在这四个特征图上进行最大池化,结果为:`[5]`、`[8]`、`[7]`和`[8]`,最后将池化结果拼接在一起,得到`[5,8,7,8]`。
+- **最大池化层**: 对卷积得到的各个特征图分别进行最大池化操作。由于特征图本身已经是向量,因此这里的最大池化实际上就是简单地选出各个向量中的最大元素。各个最大元素又被拼接在一起,组成新的向量,显然,该向量的维度等于特征图的数量,也就是卷积核的数量。举例来说,假设我们使用了四个不同的卷积核,卷积产生的特征图分别为:`[2,3,5]`、`[8,2,1]`、`[5,7,7,6]` 和 `[4,5,1,8]`,由于卷积核的高度不同,因此产生的特征图尺寸也有所差异。分别在这四个特征图上进行最大池化,结果为:`[5]`、`[8]`、`[7]`和`[8]`,最后将池化结果拼接在一起,得到`[5,8,7,8]`。
-- **全连接与输出层**:将最大池化的结果通过全连接层输出,与DNN模型一样,最后输出层的神经元个数与样本的类别数量一致,且输出之和为1。
+- **全连接与输出层**:将最大池化的结果通过全连接层输出,与 DNN 模型一样,最后输出层的神经元个数与样本的类别数量一致,且输出之和为 1。
-**通过PaddlePaddle实现该CNN结构的代码如下:**
+CNN 网络的输入数据类型和 DNN 一致。PaddlePaddle 中已经封装好的带有池化的文本序列卷积模块:`paddle.networks.sequence_conv_pool`,可直接调用。该模块的 `context_len` 参数用于指定卷积核在同一时间覆盖的文本长度,即图 2 中的卷积核的高度。`hidden_size` 用于指定该类型的卷积核的数量。本例代码默认使用了 128 个大小为 3 的卷积核和 128 个大小为 4 的卷积核,这些卷积的结果经过最大池化和结果拼接后产生一个 256 维的向量,向量经过一个全连接层输出最终的预测结果。
-```python
-import paddle.v2 as paddle
+## 运行
+### 使用 PaddlePaddle 内置的情感分类数据
-def convolution_net(dict_dim, class_dim=2, emb_dim=28, hid_dim=128):
- """
- cnn network definition
-
- :param dict_dim: size of word dictionary
- :type input_dim: int
- :params class_dim: number of instance class
- :type class_dim: int
- :params emb_dim: embedding vector dimension
- :type emb_dim: int
- :params hid_dim: number of same size convolution kernels
- :type hid_dim: int
- """
+- 运行`sh run.sh` 将以 PaddlePaddle 内置的情感分类数据集:`paddle.dataset.imdb` 运行本例
+- 运行 `python infer.py` 脚本加载训练好的模型进行预测。通过修改 `infer.py` 脚本中 `__main__` 函数中以下变量修改使用的模型和指定测试数据。脚本默认对 `paddle.dataset.imdb` 数据集中的测试数据进行测试。
- # input layers
- data = paddle.layer.data("word",
- paddle.data_type.integer_value_sequence(dict_dim))
- lbl = paddle.layer.data("label", paddle.data_type.integer_value(2))
+ ```python
+ model_path = "dnn_params_pass_00000.tar.gz" # 指定模型所在的路径
+ test_dir = None # 指定测试文件所在的目录,请注意,若不指定将默认使用paddle.dataset.imdb
+ word_dict = None # 指定字典所在的路径,请注意,若不指定将默认使用paddle.dataset.imdb
+ nn_type = "dnn" # 指定测试使用的模型
+ ```
- #embedding layer
- emb = paddle.layer.embedding(input=data, size=emb_dim)
+### 使用自定义数据运行
- # convolution layers with max pooling
- 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)
+#### step1. 编写自定义的数据读取接口
- # fc and output layer
- output = paddle.layer.fc(
- input=[conv_3, conv_4], size=class_dim, act=paddle.activation.Softmax())
+例如有如下格式的数据:每一行为一条样本,以 `\t` 分隔,第一列是类别标签,第二列是输入文本的内容。文本内容中的词语以空格分隔。以下是两条示例数据:
- cost = paddle.layer.classification_cost(input=output, label=lbl)
-
- return cost, output, lbl
+```
+negative PaddlePaddle is good
+positive What a terrible weather
```
-该CNN网络的输入数据类型和前面介绍过的DNN一致。`paddle.networks.sequence_conv_pool`为PaddlePaddle中已经封装好的带有池化的文本序列卷积模块,该模块的`context_len`参数用于指定卷积核在同一时间覆盖的文本长度,即图2中的卷积核的高度;`hidden_size`用于指定该类型的卷积核的数量。可以看到,上述代码定义的结构中使用了128个大小为3的卷积核和128个大小为4的卷积核,这些卷积的结果经过最大池化和结果拼接后产生一个256维的向量,向量经过一个全连接层输出最终预测结果。
-
-## 自定义数据
-本样例中的代码通过`Paddle.dataset.imdb.train`接口使用了PaddlePaddle自带的样例数据,在第一次运行代码时,PaddlePaddle会自动下载并缓存所需的数据。如果希望使用自己的数据进行训练,需要自行编写数据读取接口。
+编写自定义的数据读取接口关键在实现一个 Python 生成器完成**从原始输入文本中解析一条训练样本的逻辑**。
-编写数据读取接口的关键在于实现一个Python生成器,生成器负责从原始输入文本中解析出一条训练样本,并组合成适当的数据形式传送给网络中的data layer。例如在本样例中,data layer需要的数据类型为`paddle.data_type.integer_value_sequence`,本质上是一个Python list。因此我们的生成器需要完成:从文件中读取数据, 以及转换成适当形式的Python list,这两件事情。
+以下代码片段实现了:读取以上格式数据返回类型为: `paddle.data_type.integer_value_sequence`(词语在字典的序号)和 `paddle.data_type.integer_value`(类别标签)的 2 个输入给网络中中定义的 2 个 `data_layer`(见 `fc_net` 或 `convolution_net`)。
-假设原始数据的格式为:
+关于 PaddlePaddle 中 `data_layer` 接受输入数据的类型,以及读取数据接口应该返回数据的格式,请参考 [input-types](http://www.paddlepaddle.org/release_doc/0.9.0/doc_cn/ui/data_provider/pydataprovider2.html#input-types) 一节。
-```
-PaddlePaddle is good 1
-What a terrible weather 0
-```
-每一行为一条样本,样本包括了原始语料和标签,语料内部单词以空格分隔,语料和标签之间用`\t`分隔。对以上格式的数据,可以使用如下自定义的数据读取接口为PaddlePaddle返回训练数据:
+- `data_dir` 测试数据所在路径
+- `word_dict` 词语的字典,用来将原始字符串表示的词语转化为字典中的序号
+- `label_dict` 类别标签的字典,用于将字符串的类别标签,转换成整数类型的序号
```python
-def encode_word(word, word_dict):
- """
- map word to id
-
- :param word: the word to be mapped
- :type word: str
- :param word_dict: word dictionary
- :type word_dict: Python dict
- """
-
- if word_dict.has_key(word):
- return word_dict[word]
- else:
- return word_dict['
']
-
-def data_reader(file_name, word_dict):
+def train_reader(data_dir, word_dict, label_dict):
"""
Reader interface for training data
- :param file_name: data file name
- :type file_name: str
- :param word_dict: word dictionary
+ :param data_dir: data directory
+ :type data_dir: str
+ :param word_dict: path of word dictionary,
+ the dictionary must has a "UNK" in it.
:type word_dict: Python dict
+ :param label_dict: path of label dictionary
+ :type label_dict: Python dict
"""
def reader():
- with open(file_name, "r") as f:
- for line in f:
- ins, label = line.strip('\n').split('\t')
- ins_data = [int(encode_word(w, word_dict)) for w in ins.split(' ')]
- yield ins_data, int(label)
+ UNK_ID = word_dict[""]
+ word_col = 0
+ lbl_col = 1
+
+ for file_name in os.listdir(data_dir):
+ with open(os.path.join(data_dir, file_name), "r") as f:
+ for line in f:
+ line_split = line.strip().split("\t")
+ word_ids = [
+ word_dict.get(w, UNK_ID)
+ for w in line_split[word_col].split()
+ ]
+ yield word_ids, label_dict[line_split[lbl_col]]
+
return reader
```
-`word_dict`是字典,用来将原始的单词字符串转化为在字典中的序号。可以用`data_reader`替换原先代码中的`Paddle.dataset.imdb.train`接口用以提供自定义的训练数据。
-
-## 运行与输出
-
-本部分以上文介绍的DNN网络为例,介绍如何利用样例中的`text_classification_dnn.py`脚本进行DNN网络的训练和对新样本的预测。
-
-`text_classification_dnn.py`中的代码分为四部分:
-
-- **fc_net函数**:定义dnn网络结构,上文已经有说明。
+本例目录下的 `reader.py` 含有读取训练和测试数据的全部代码。
-- **train\_dnn\_model函数**:模型训练函数。定义优化方式、训练输出等内容,并组织训练流程。每完成一个pass的训练,程序都会将当前的模型参数保存在硬盘上,文件名为:`dnn_params_pass***.tar.gz`,其中`***`表示pass的id,从0开始计数。本函数接受一个整数类型的参数,表示训练pass的总轮数。
+接下来,只需要将数据读取函数 `train_reader` 作为参数传递给 `train.py` 脚本中的 `paddle.batch` 接口即可使用自定义数据接口读取数据,调用方式如下:
-- **dnn_infer函数**:载入已有模型并对新样本进行预测。函数开始运行后会从当前路径下寻找并读取指定名称的参数文件,加载其中的模型参数,并对test数据集中的样本进行预测。
-
-- **main函数**:主函数
+```python
+train_reader = paddle.batch(
+ paddle.reader.shuffle(
+ reader.train_reader(train_data_dir, word_dict, lbl_dict),
+ buf_size=1000),
+ batch_size=batch_size)
+```
-要运行本样例,直接在`text_classification_dnn.py`所在路径下执行`python text_classification_dnn.py`即可,样例会自动依次执行数据集下载、数据读取、模型训练和保存、模型读取、新样本预测等步骤。
+#### step 2. 修改命令行参数
-预测的输出形式为:
+执行 `python train.py --help` 可以获取`train.py` 脚本各项启动参数的详细说明。通过修改 `train.py` 脚本的启动参数,指定自定义数据的路径。
-```
-[ 0.99892634 0.00107362] 0
-[ 0.00107638 0.9989236 ] 1
-[ 0.98185927 0.01814074] 0
-[ 0.31667888 0.68332112] 1
-[ 0.98853314 0.01146684] 0
-```
+主要参数如下:
-每一行表示一条样本的预测结果。前两列表示该样本属于0、1这两个类别的预测概率,最后一列表示样本的实际label。
+- `nn_type`:选择要使用的模型,目前支持两种:“dnn” 或者 “cnn”。
+- `train_data_dir`:指定训练数据所在的文件夹,使用自定义数据训练,必须指定此参数,否则使用`paddle.dataset.imdb`训练,同时忽略`test_data_dir`,`word_dict`,和 `label_dict` 参数。
+- `test_data_dir`:指定测试数据所在的文件夹,若不指定将不进行测试。
+- `word_dict`:字典文件所在的路径,若不指定,将从训练数据根据词频统计,自动建立字典。
+- `label_dict`:类别标签字典,用于将字符串类型的类别标签,映射为整数类型的序号。
+- `batch_size`:指定多少条样本后进行一次神经网络的前向运行及反向更新。
+- `num_passes`:指定训练多少个轮次。
-在运行CNN模型的`text_classification_cnn.py`脚本中,网络模型定义在`convolution_net`函数中,模型训练函数名为`train_cnn_model`,预测函数名为`cnn_infer`。其他用法和`text_classification_dnn.py`是一致的。
+如果将数据组织成上一节示例数据的格式,只需在 `run.sh` 脚本中指定 `train_data_dir` 参数,可以直接运行本例,无需修改数据读取接口 `reader.py`。
diff --git a/text_classification/infer.py b/text_classification/infer.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5e9833cebadd07a713c01cb7863f5fc2db2127b
--- /dev/null
+++ b/text_classification/infer.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import sys
+import os
+import gzip
+
+import paddle.v2 as paddle
+
+import network_conf
+import reader
+from utils import *
+
+
+def infer(topology, data_dir, model_path, word_dict_path, label_dict_path,
+ batch_size):
+ def _infer_a_batch(inferer, test_batch):
+ probs = inferer.infer(input=test_batch, field=['value'])
+ assert len(probs) == len(test_batch)
+ for prob in probs:
+ lab = prob.argmax()
+ print("%d\t%s\t%s" %
+ (lab, label_reverse_dict[lab],
+ "\t".join(["{:0.4f}".format(p) for p in prob])))
+
+ logger.info('begin to predict...')
+ use_default_data = (data_dir is None)
+
+ if use_default_data:
+ word_dict = paddle.dataset.imdb.word_dict()
+ label_reverse_dict = {0: "positive", 1: "negative"}
+ test_reader = paddle.dataset.imdb.test(word_dict)
+ else:
+ assert os.path.exists(
+ word_dict_path), 'the word dictionary file does not exist'
+ assert os.path.exists(
+ label_dict_path), 'the label dictionary file does not exist'
+ word_dict = load_dict(word_dict_path)
+ label_reverse_dict = load_reverse_dict(label_dict_path)
+
+ test_reader = reader.test_reader(data_dir, word_dict)()
+
+ dict_dim = len(word_dict)
+ class_num = len(label_reverse_dict)
+ prob_layer = topology(dict_dim, class_num, is_infer=True)
+
+ # initialize PaddlePaddle
+ paddle.init(use_gpu=False, trainer_count=1)
+
+ # load the trained models
+ parameters = paddle.parameters.Parameters.from_tar(
+ gzip.open(model_path, 'r'))
+ inferer = paddle.inference.Inference(
+ output_layer=prob_layer, parameters=parameters)
+
+ test_batch = []
+ for idx, item in enumerate(test_reader):
+ test_batch.append([item[0]])
+ if len(test_batch) == batch_size:
+ _infer_a_batch(inferer, test_batch)
+ test_batch = []
+
+ _infer_a_batch(inferer, test_batch)
+ test_batch = []
+
+
+if __name__ == '__main__':
+ model_path = 'dnn_params_pass_00000.tar.gz'
+ assert os.path.exists(model_path), "the trained model does not exist."
+
+ nn_type = 'dnn'
+ test_dir = None
+ word_dict = None
+ label_dict = None
+
+ if nn_type == 'dnn':
+ topology = network_conf.fc_net
+ elif nn_type == 'cnn':
+ topology = network_conf.convolution_net
+
+ infer(
+ topology=topology,
+ data_dir=test_dir,
+ word_dict_path=word_dict,
+ label_dict_path=label_dict,
+ model_path=model_path,
+ batch_size=10)
diff --git a/text_classification/network_conf.py b/text_classification/network_conf.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f1207ecfdadeabfdb8931aa196b0d7fac9abaea
--- /dev/null
+++ b/text_classification/network_conf.py
@@ -0,0 +1,102 @@
+import sys
+import math
+import gzip
+
+from paddle.v2.layer import parse_network
+import paddle.v2 as paddle
+
+__all__ = ["fc_net", "convolution_net"]
+
+
+def fc_net(dict_dim,
+ class_num,
+ emb_dim=28,
+ hidden_layer_sizes=[28, 8],
+ is_infer=False):
+ """
+ define the topology of the dnn network
+
+ :param dict_dim: size of word dictionary
+ :type input_dim: int
+ :params class_num: number of instance class
+ :type class_num: int
+ :params emb_dim: embedding vector dimension
+ :type emb_dim: int
+ """
+
+ # define the input layers
+ data = paddle.layer.data("word",
+ paddle.data_type.integer_value_sequence(dict_dim))
+ if not is_infer:
+ lbl = paddle.layer.data("label",
+ paddle.data_type.integer_value(class_num))
+
+ # define the embedding layer
+ emb = paddle.layer.embedding(input=data, size=emb_dim)
+ # max pooling to reduce the input sequence into a vector (non-sequence)
+ seq_pool = paddle.layer.pooling(
+ input=emb, pooling_type=paddle.pooling.Max())
+
+ for idx, hidden_size in enumerate(hidden_layer_sizes):
+ hidden_init_std = 1.0 / math.sqrt(hidden_size)
+ hidden = paddle.layer.fc(
+ input=hidden if idx else seq_pool,
+ size=hidden_size,
+ act=paddle.activation.Tanh(),
+ param_attr=paddle.attr.Param(initial_std=hidden_init_std))
+
+ prob = paddle.layer.fc(
+ input=hidden,
+ size=class_num,
+ act=paddle.activation.Softmax(),
+ param_attr=paddle.attr.Param(initial_std=1.0 / math.sqrt(class_num)))
+
+ if is_infer:
+ return prob
+ else:
+ return paddle.layer.classification_cost(
+ input=prob, label=lbl), prob, lbl
+
+
+def convolution_net(dict_dim,
+ class_dim=2,
+ emb_dim=28,
+ hid_dim=128,
+ is_infer=False):
+ """
+ cnn network definition
+
+ :param dict_dim: size of word dictionary
+ :type input_dim: int
+ :params class_dim: number of instance class
+ :type class_dim: int
+ :params emb_dim: embedding vector dimension
+ :type emb_dim: int
+ :params hid_dim: number of same size convolution kernels
+ :type hid_dim: int
+ """
+
+ # input layers
+ data = paddle.layer.data("word",
+ paddle.data_type.integer_value_sequence(dict_dim))
+ lbl = paddle.layer.data("label", paddle.data_type.integer_value(class_dim))
+
+ # embedding layer
+ emb = paddle.layer.embedding(input=data, size=emb_dim)
+
+ # convolution layers with max pooling
+ 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)
+
+ # fc and output layer
+ prob = paddle.layer.fc(
+ input=[conv_3, conv_4], size=class_dim, act=paddle.activation.Softmax())
+
+ if is_infer:
+ return prob
+ else:
+ cost = paddle.layer.classification_cost(input=prob, label=lbl)
+
+ return cost, prob, lbl
diff --git a/text_classification/reader.py b/text_classification/reader.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b6700313a6bf94784de73785e968193d97106f2
--- /dev/null
+++ b/text_classification/reader.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import os
+
+
+def train_reader(data_dir, word_dict, label_dict):
+ """
+ Reader interface for training data
+
+ :param data_dir: data directory
+ :type data_dir: str
+ :param word_dict: path of word dictionary,
+ the dictionary must has a "UNK" in it.
+ :type word_dict: Python dict
+ :param label_dict: path of label dictionary
+ :type label_dict: Python dict
+ """
+
+ def reader():
+ UNK_ID = word_dict["