From 501ce212679222fe4a784dd2e5d43235b8f7c9ca Mon Sep 17 00:00:00 2001 From: caoying03 Date: Fri, 9 Jun 2017 19:41:09 +0800 Subject: [PATCH 1/2] rewrite the text classification demo. --- image_classification/reader.py | 14 - image_classification/train.py | 18 +- image_classification/vgg.py | 14 - text_classification/.gitignore | 4 + text_classification/README.md | 297 ++++++++---------- text_classification/index.html | 297 ++++++++---------- text_classification/infer.py | 86 +++++ text_classification/network_conf.py | 102 ++++++ text_classification/reader.py | 63 ++++ text_classification/run.sh | 7 + .../text_classification_cnn.py | 146 --------- .../text_classification_dnn.py | 160 ---------- text_classification/train.py | 174 ++++++++++ text_classification/utils.py | 103 ++++++ 14 files changed, 791 insertions(+), 694 deletions(-) create mode 100644 text_classification/.gitignore create mode 100644 text_classification/infer.py create mode 100644 text_classification/network_conf.py create mode 100644 text_classification/reader.py create mode 100644 text_classification/run.sh delete mode 100644 text_classification/text_classification_cnn.py delete mode 100644 text_classification/text_classification_dnn.py create mode 100644 text_classification/train.py create mode 100644 text_classification/utils.py diff --git a/image_classification/reader.py b/image_classification/reader.py index b58807e3a3..4040222728 100644 --- a/image_classification/reader.py +++ b/image_classification/reader.py @@ -1,17 +1,3 @@ -# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License - import random from paddle.v2.image import load_and_transform diff --git a/image_classification/train.py b/image_classification/train.py index d917bd8019..5a5e48a386 100644 --- a/image_classification/train.py +++ b/image_classification/train.py @@ -1,17 +1,3 @@ -# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License - import gzip import paddle.v2 as paddle @@ -51,10 +37,10 @@ def main(): learning_rate_schedule="discexp", ) train_reader = paddle.batch( - paddle.reader.shuffle(reader.test_reader("train.list"), buf_size=1000), + paddle.reader.shuffle(reader.train_reader("train.list"), buf_size=1000), batch_size=BATCH_SIZE) test_reader = paddle.batch( - reader.train_reader("test.list"), batch_size=BATCH_SIZE) + reader.test_reader("test.list"), batch_size=BATCH_SIZE) # End batch and end pass event handler def event_handler(event): diff --git a/image_classification/vgg.py b/image_classification/vgg.py index e21504ab54..fdcd351919 100644 --- a/image_classification/vgg.py +++ b/image_classification/vgg.py @@ -1,17 +1,3 @@ -# Copyright (c) 2016 PaddlePaddle Authors. All Rights Reserved -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - import paddle.v2 as paddle __all__ = ['vgg13', 'vgg16', 'vgg19'] diff --git a/text_classification/.gitignore b/text_classification/.gitignore new file mode 100644 index 0000000000..c979e15d6b --- /dev/null +++ b/text_classification/.gitignore @@ -0,0 +1,4 @@ +data +*.tar.gz +*.log +*.pyc diff --git a/text_classification/README.md b/text_classification/README.md index ff1ed7f5c0..441bb37a74 100644 --- a/text_classification/README.md +++ b/text_classification/README.md @@ -1,238 +1,191 @@ # 文本分类 -文本分类是机器学习中的一项常见任务,主要目的是根据一条文本的内容,判断该文本所属的类别。在本例子中,我们利用有标注的语料库训练二分类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 模型 -

-
-图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 +

+
+图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 模型结构如下图所示:**


-图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/index.html b/text_classification/index.html index 3ee660d847..9cc1a050de 100644 --- a/text_classification/index.html +++ b/text_classification/index.html @@ -41,243 +41,196 @@ diff --git a/text_classification/infer.py b/text_classification/infer.py new file mode 100644 index 0000000000..d5e9833ceb --- /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 0000000000..8f1207ecfd --- /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 0000000000..7b6700313a --- /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[""] + word_col = 1 + lbl_col = 0 + + 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 + + +def test_reader(data_dir, word_dict): + """ + Reader interface for testing 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 + """ + + def reader(): + UNK_ID = word_dict[""] + word_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") + if len(line_split) < word_col: continue + word_ids = [ + word_dict.get(w, UNK_ID) + for w in line_split[word_col].split() + ] + yield word_ids, line_split[word_col] + + return reader diff --git a/text_classification/run.sh b/text_classification/run.sh new file mode 100644 index 0000000000..39dfe461cf --- /dev/null +++ b/text_classification/run.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +python train.py \ +--nn_type="dnn" \ +--batch_size=64 \ +--num_passes=10 \ +2>&1 | tee train.log diff --git a/text_classification/text_classification_cnn.py b/text_classification/text_classification_cnn.py deleted file mode 100644 index c101f1c2d0..0000000000 --- a/text_classification/text_classification_cnn.py +++ /dev/null @@ -1,146 +0,0 @@ -import sys -import paddle.v2 as paddle -import gzip - - -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 - """ - - # 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)) - - #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 - output = paddle.layer.fc( - input=[conv_3, conv_4], size=class_dim, act=paddle.activation.Softmax()) - - cost = paddle.layer.classification_cost(input=output, label=lbl) - - return cost, output, lbl - - -def train_cnn_model(num_pass): - """ - train cnn model - - :params num_pass: train pass number - :type num_pass: int - """ - - # load word dictionary - print 'load dictionary...' - word_dict = paddle.dataset.imdb.word_dict() - - dict_dim = len(word_dict) - class_dim = 2 - # define data reader - train_reader = paddle.batch( - paddle.reader.shuffle( - lambda: paddle.dataset.imdb.train(word_dict), buf_size=1000), - batch_size=100) - test_reader = paddle.batch( - lambda: paddle.dataset.imdb.test(word_dict), batch_size=100) - - # network config - [cost, output, label] = convolution_net(dict_dim, class_dim=class_dim) - # create parameters - parameters = paddle.parameters.create(cost) - # create optimizer - adam_optimizer = paddle.optimizer.Adam( - learning_rate=1e-3, - regularization=paddle.optimizer.L2Regularization(rate=1e-3), - model_average=paddle.optimizer.ModelAverage(average_window=0.5)) - - # add auc evaluator - paddle.evaluator.auc(input=output, label=label) - - # create trainer - trainer = paddle.trainer.SGD( - cost=cost, parameters=parameters, update_equation=adam_optimizer) - - # Define end batch and end pass event handler - def event_handler(event): - if isinstance(event, paddle.event.EndIteration): - if event.batch_id % 100 == 0: - print "\nPass %d, Batch %d, Cost %f, %s" % ( - event.pass_id, event.batch_id, event.cost, event.metrics) - else: - sys.stdout.write('.') - sys.stdout.flush() - if isinstance(event, paddle.event.EndPass): - result = trainer.test(reader=test_reader, feeding=feeding) - print "\nTest with Pass %d, %s" % (event.pass_id, result.metrics) - with gzip.open("cnn_params_pass" + str(event.pass_id) + ".tar.gz", - 'w') as f: - parameters.to_tar(f) - - # begin training network - feeding = {'word': 0, 'label': 1} - trainer.train( - reader=train_reader, - event_handler=event_handler, - feeding=feeding, - num_passes=num_pass) - - print("Training finished.") - - -def cnn_infer(file_name): - """ - predict instance labels by cnn network - - :params file_name: network parameter file - :type file_name: str - """ - - print("Begin to predict...") - - word_dict = paddle.dataset.imdb.word_dict() - dict_dim = len(word_dict) - class_dim = 2 - - [_, output, _] = convolution_net(dict_dim, class_dim=class_dim) - parameters = paddle.parameters.Parameters.from_tar(gzip.open(file_name)) - - infer_data = [] - infer_data_label = [] - for item in paddle.dataset.imdb.test(word_dict): - infer_data.append([item[0]]) - infer_data_label.append(item[1]) - - predictions = paddle.infer( - output_layer=output, - parameters=parameters, - input=infer_data, - field=['value']) - for i, prob in enumerate(predictions): - print prob, infer_data_label[i] - - -if __name__ == "__main__": - paddle.init(use_gpu=False, trainer_count=1) - num_pass = 5 - train_cnn_model(num_pass=num_pass) - param_file_name = "cnn_params_pass" + str(num_pass - 1) + ".tar.gz" - cnn_infer(file_name=param_file_name) diff --git a/text_classification/text_classification_dnn.py b/text_classification/text_classification_dnn.py deleted file mode 100644 index 5a45a75a27..0000000000 --- a/text_classification/text_classification_dnn.py +++ /dev/null @@ -1,160 +0,0 @@ -import sys -import math -import paddle.v2 as paddle -import gzip - - -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 - """ - - # 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 - - -def train_dnn_model(num_pass): - """ - train dnn model - - :params num_pass: train pass number - :type num_pass: int - """ - - # load word dictionary - print 'load dictionary...' - word_dict = paddle.dataset.imdb.word_dict() - - dict_dim = len(word_dict) - class_dim = 2 - # define data reader - train_reader = paddle.batch( - paddle.reader.shuffle( - lambda: paddle.dataset.imdb.train(word_dict), buf_size=1000), - batch_size=100) - test_reader = paddle.batch( - lambda: paddle.dataset.imdb.test(word_dict), batch_size=100) - - # network config - [cost, output, label] = fc_net(dict_dim, class_dim=class_dim) - - # create parameters - parameters = paddle.parameters.create(cost) - # create optimizer - adam_optimizer = paddle.optimizer.Adam( - learning_rate=1e-3, - regularization=paddle.optimizer.L2Regularization(rate=1e-3), - model_average=paddle.optimizer.ModelAverage(average_window=0.5)) - - # add auc evaluator - paddle.evaluator.auc(input=output, label=label) - - # create trainer - trainer = paddle.trainer.SGD( - cost=cost, parameters=parameters, update_equation=adam_optimizer) - - # Define end batch and end pass event handler - def event_handler(event): - if isinstance(event, paddle.event.EndIteration): - if event.batch_id % 100 == 0: - print "\nPass %d, Batch %d, Cost %f, %s" % ( - event.pass_id, event.batch_id, event.cost, event.metrics) - else: - sys.stdout.write('.') - sys.stdout.flush() - if isinstance(event, paddle.event.EndPass): - result = trainer.test(reader=test_reader, feeding=feeding) - print "\nTest with Pass %d, %s" % (event.pass_id, result.metrics) - with gzip.open("dnn_params_pass" + str(event.pass_id) + ".tar.gz", - 'w') as f: - parameters.to_tar(f) - - # begin training network - feeding = {'word': 0, 'label': 1} - trainer.train( - reader=train_reader, - event_handler=event_handler, - feeding=feeding, - num_passes=num_pass) - - print("Training finished.") - - -def dnn_infer(file_name): - """ - predict instance labels by dnn network - - :params file_name: network parameter file - :type file_name: str - """ - - print("Begin to predict...") - - word_dict = paddle.dataset.imdb.word_dict() - dict_dim = len(word_dict) - class_dim = 2 - - [_, output, _] = fc_net(dict_dim, class_dim=class_dim) - parameters = paddle.parameters.Parameters.from_tar(gzip.open(file_name)) - - infer_data = [] - infer_data_label = [] - for item in paddle.dataset.imdb.test(word_dict): - infer_data.append([item[0]]) - infer_data_label.append(item[1]) - - predictions = paddle.infer( - output_layer=output, - parameters=parameters, - input=infer_data, - field=['value']) - for i, prob in enumerate(predictions): - print prob, infer_data_label[i] - - -if __name__ == "__main__": - paddle.init(use_gpu=False, trainer_count=1) - num_pass = 5 - train_dnn_model(num_pass=num_pass) - param_file_name = "dnn_params_pass" + str(num_pass - 1) + ".tar.gz" - dnn_infer(file_name=param_file_name) diff --git a/text_classification/train.py b/text_classification/train.py new file mode 100644 index 0000000000..f635fdf29f --- /dev/null +++ b/text_classification/train.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys +import gzip + +import paddle.v2 as paddle + +import network_conf +import reader +from utils import * + + +def train(topology, + train_data_dir=None, + test_data_dir=None, + word_dict_path=None, + label_dict_path=None, + batch_size=32, + num_passes=10): + """ + train dnn model + + + :params train_data_path: path of training data, if this parameter + is not specified, paddle.dataset.imdb will be used to run this example + :type train_data_path: str + :params test_data_path: path of testing data, if this parameter + is not specified, paddle.dataset.imdb will be used to run this example + :type test_data_path: str + :params word_dict_path: path of training data, if this parameter + is not specified, paddle.dataset.imdb will be used to run this example + :type word_dict_path: str + :params num_pass: train pass number + :type num_pass: int + """ + + use_default_data = (train_data_dir is None) + + if use_default_data: + logger.info(("No training data are porivided, " + "use paddle.dataset.imdb to train the model.")) + logger.info("please wait to build the word dictionary ...") + + word_dict = paddle.dataset.imdb.word_dict() + train_reader = paddle.batch( + paddle.reader.shuffle( + lambda: paddle.dataset.imdb.train(word_dict), buf_size=1000), + batch_size=100) + test_reader = paddle.batch( + lambda: paddle.dataset.imdb.test(word_dict), batch_size=100) + + class_num = 2 + else: + if word_dict_path is None or not os.path.exists(word_dict_path): + logger.info(("word dictionary is not given, the dictionary " + "is automatically built from the training data.")) + + # build the word dictionary to map the original string-typed + # words into integer-typed index + build_dict( + data_dir=train_data_dir, + save_path=word_dict_path, + use_col=1, + cutoff_fre=5, + insert_extra_words=[""]) + + if not os.path.exists(label_dict_path): + logger.info(("label dictionary is not given, the dictionary " + "is automatically built from the training data.")) + # build the label dictionary to map the original string-typed + # label into integer-typed index + build_dict( + data_dir=train_data_dir, save_path=label_dict_path, use_col=0) + + word_dict = load_dict(word_dict_path) + + lbl_dict = load_dict(label_dict_path) + class_num = len(lbl_dict) + logger.info("class number is : %d." % (len(lbl_dict))) + + train_reader = paddle.batch( + paddle.reader.shuffle( + reader.train_reader(train_data_dir, word_dict, lbl_dict), + buf_size=1000), + batch_size=batch_size) + + if test_data_dir is not None: + # here, because training and testing data share a same format, + # we still use the reader.train_reader to read the testing data. + test_reader = paddle.batch( + paddle.reader.shuffle( + reader.train_reader(test_data_dir, word_dict, lbl_dict), + buf_size=1000), + batch_size=batch_size) + else: + test_reader = None + + dict_dim = len(word_dict) + logger.info("length of word dictionary is : %d." % (dict_dim)) + + paddle.init(use_gpu=False, trainer_count=1) + + # network config + cost, prob, label = topology(dict_dim, class_num) + + # create parameters + parameters = paddle.parameters.create(cost) + + # create optimizer + adam_optimizer = paddle.optimizer.Adam( + learning_rate=1e-3, + regularization=paddle.optimizer.L2Regularization(rate=1e-3), + model_average=paddle.optimizer.ModelAverage(average_window=0.5)) + + # create trainer + trainer = paddle.trainer.SGD( + cost=cost, + extra_layers=paddle.evaluator.auc(input=prob, label=label), + parameters=parameters, + update_equation=adam_optimizer) + + # begin training network + feeding = {"word": 0, "label": 1} + + def _event_handler(event): + """ + Define end batch and end pass event handler + """ + if isinstance(event, paddle.event.EndIteration): + if event.batch_id % 100 == 0: + logger.info("Pass %d, Batch %d, Cost %f, %s\n" % ( + event.pass_id, event.batch_id, event.cost, event.metrics)) + + if isinstance(event, paddle.event.EndPass): + if test_reader is not None: + result = trainer.test(reader=test_reader, feeding=feeding) + logger.info("Test at Pass %d, %s \n" % (event.pass_id, + result.metrics)) + with gzip.open("dnn_params_pass_%05d.tar.gz" % event.pass_id, + "w") as f: + parameters.to_tar(f) + + trainer.train( + reader=train_reader, + event_handler=_event_handler, + feeding=feeding, + num_passes=num_passes) + + logger.info("Training has finished.") + + +def main(args): + if args.nn_type == "dnn": + topology = network_conf.fc_net + elif args.nn_type == "cnn": + topology = network_conf.convolution_net + + train( + topology=topology, + train_data_dir=args.train_data_dir, + test_data_dir=args.test_data_dir, + word_dict_path=args.word_dict, + label_dict_path=args.label_dict, + batch_size=args.batch_size, + num_passes=args.num_passes) + + +if __name__ == "__main__": + args = parse_train_cmd() + if args.train_data_dir is not None: + assert args.word_dict and args.label_dict, ( + "the parameter train_data_dir, word_dict_path, and label_dict_path " + "should be set at the same time.") + main(args) diff --git a/text_classification/utils.py b/text_classification/utils.py new file mode 100644 index 0000000000..7364add220 --- /dev/null +++ b/text_classification/utils.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import logging +import os +import argparse +from collections import defaultdict + +logger = logging.getLogger("logger") +logger.setLevel(logging.INFO) + + +def parse_train_cmd(): + parser = argparse.ArgumentParser( + description="PaddlePaddle text classification demo") + parser.add_argument( + "--nn_type", + type=str, + help="define which type of network to use, available: [dnn, cnn]", + default="dnn") + parser.add_argument( + "--train_data_dir", + type=str, + required=False, + help=("path of training dataset (default: None). " + "if this parameter is not set, " + "paddle.dataset.imdb will be used."), + default=None) + parser.add_argument( + "--test_data_dir", + type=str, + required=False, + help=("path of testing dataset (default: None). " + "if this parameter is not set, " + "paddle.dataset.imdb will be used."), + default=None) + parser.add_argument( + "--word_dict", + type=str, + required=False, + help=("path of word dictionary (default: None)." + "if this parameter is not set, paddle.dataset.imdb will be used." + "if this parameter is set, but the file does not exist, " + "word dictionay will be built from " + "the training data automatically."), + default=None) + parser.add_argument( + "--label_dict", + type=str, + required=False, + help=("path of label dictionay (default: None)." + "if this parameter is not set, paddle.dataset.imdb will be used." + "if this parameter is set, but the file does not exist, " + "word dictionay will be built from " + "the training data automatically."), + default=None) + parser.add_argument( + "--batch_size", + type=int, + default=32, + help="the number of training examples in one forward/backward pass") + parser.add_argument( + "--num_passes", type=int, default=10, help="number of passes to train") + + return parser.parse_args() + + +def build_dict(data_dir, + save_path, + use_col=0, + cutoff_fre=0, + insert_extra_words=[]): + values = defaultdict(int) + + for file_name in os.listdir(data_dir): + file_path = os.path.join(data_dir, file_name) + if not os.path.isfile(file_path): + continue + with open(file_path, "r") as fdata: + for line in fdata: + line_splits = line.strip().split("\t") + if len(line_splits) < use_col: continue + for w in line_splits[use_col].split(): + values[w] += 1 + + with open(save_path, "w") as f: + for w in insert_extra_words: + f.write("%s\t-1\n" % (w)) + + for v, count in sorted( + values.iteritems(), key=lambda x: x[1], reverse=True): + if count < cutoff_fre: + break + f.write("%s\t%d\n" % (v, count)) + + +def load_dict(dict_path): + return dict((line.strip().split("\t")[0], idx) + for idx, line in enumerate(open(dict_path, "r").readlines())) + + +def load_reverse_dict(dict_path): + return dict((idx, line.strip().split("\t")[0]) + for idx, line in enumerate(open(dict_path, "r").readlines())) From 136a60d7873c63da2758bceeee6ff7e11cf5dd37 Mon Sep 17 00:00:00 2001 From: caoying03 Date: Tue, 13 Jun 2017 17:59:43 +0800 Subject: [PATCH 2/2] update readme. --- text_classification/README.md | 197 +++++++++++++++++---------------- text_classification/index.html | 197 +++++++++++++++++---------------- text_classification/infer.py | 25 +++-- 3 files changed, 220 insertions(+), 199 deletions(-) diff --git a/text_classification/README.md b/text_classification/README.md index 441bb37a74..191ab20f2e 100644 --- a/text_classification/README.md +++ b/text_classification/README.md @@ -1,24 +1,23 @@ # 文本分类 -以下是本例目录包含的文件以及对应说明(`images` 文件夹以及 `index.html` 与使用无关可不关心): +以下是本例目录包含的文件以及对应说明: ```text . -├── images +├── images # 文档中的图片 │   ├── cnn_net.png │   └── dnn_net.png -├── index.html -├── infer.py # 预测任务脚本 -├── network_conf.py # 本例中涉及的各种网络结构均定义在此文件中,希望进一步修改模型结构,请修改此文件 -├── reader.py # 读取数据接口,若使用自定义格式的数据,可直接修改此文件 +├── index.html # 文档 +├── infer.py # 预测脚本 +├── network_conf.py # 本例中涉及的各种网络结构均定义在此文件中,若进一步修改模型结构,请查看此文件 +├── reader.py # 读取数据接口,若使用自定义格式的数据,请查看此文件 ├── README.md # 文档 -├── run.sh # 运行此脚本,可以以默认参数直接开始训练任务 -├── train.py # 训练任务脚本 +├── run.sh # 训练任务运行脚本,直接运行此脚本,将以默认参数开始训练任务 +├── train.py # 训练脚本 └── utils.py # 定义通用的函数,例如:打印日志、解析命令行参数、构建字典、加载字典等 ``` ## 简介 - 文本分类任务根据给定一条文本的内容,判断该文本所属的类别,是自然语言处理领域的一项重要的基础任务。[PaddleBook](https://github.com/PaddlePaddle/book) 中的[情感分类](https://github.com/PaddlePaddle/book/blob/develop/06.understand_sentiment/README.cn.md)一课,正是一个典型的文本分类任务,任务流程如下: 1. 收集电影评论网站的用户评论数据。 @@ -35,7 +34,7 @@ "No Free Lunch (NFL)" 是机器学习任务基本原则之一:没有任何一种模型是天生优于其他模型的。模型的设计和选择建立在了解不同模型特性的基础之上,但同时也是一个多次实验评估的过程。在本例中,我们继续向大家介绍几种最常用的文本分类模型,它们的能力和复杂程度不同,帮助大家对比学习这些模型学习效果之间的差异,针对不同的场景选择使用。 -### DNN 模型与 CNN 模型 +## 模型详解 `network_conf.py` 中包括以下模型: @@ -53,7 +52,6 @@ 2. DNN 刻画的往往是频繁词特征,潜在会受到分词错误的影响,但对一些依赖关键词特征也能做的不错的任务:如 Spam 短信检测,依然是一个有效的模型。 3. 在大多数需要一定语义理解(例如,借助上下文消除语义中的歧义)的文本分类任务上,以 CNN / RNN 为代表的序列模型的效果往往好于 DNN 模型。 -## 模型详解 ### 1. DNN 模型 **DNN 模型结构入下图所示:** @@ -75,7 +73,7 @@ 该 DNN 模型默认对输入的语料进行二分类(`class_dim=2`),embedding(词向量)维度默认为28(`emd_dim=28`),两个隐层均使用Tanh激活函数(`act=paddle.activation.Tanh()`)。需要注意的是,该模型的输入数据为整数序列,而不是原始的单词序列。事实上,为了处理方便,我们一般会事先将单词根据词频顺序进行 id 化,即将词语转化成在字典中的序号。 -## 2. CNN 模型 +### 2. CNN 模型 **CNN 模型结构如下图所示:** @@ -96,96 +94,105 @@ CNN 网络的输入数据类型和 DNN 一致。PaddlePaddle 中已经封装好的带有池化的文本序列卷积模块:`paddle.networks.sequence_conv_pool`,可直接调用。该模块的 `context_len` 参数用于指定卷积核在同一时间覆盖的文本长度,即图 2 中的卷积核的高度。`hidden_size` 用于指定该类型的卷积核的数量。本例代码默认使用了 128 个大小为 3 的卷积核和 128 个大小为 4 的卷积核,这些卷积的结果经过最大池化和结果拼接后产生一个 256 维的向量,向量经过一个全连接层输出最终的预测结果。 -## 运行 -### 使用 PaddlePaddle 内置的情感分类数据 - -- 运行`sh run.sh` 将以 PaddlePaddle 内置的情感分类数据集:`paddle.dataset.imdb` 运行本例 -- 运行 `python infer.py` 脚本加载训练好的模型进行预测。通过修改 `infer.py` 脚本中 `__main__` 函数中以下变量修改使用的模型和指定测试数据。脚本默认对 `paddle.dataset.imdb` 数据集中的测试数据进行测试。 +## 使用 PaddlePaddle 内置数据运行 - ```python - model_path = "dnn_params_pass_00000.tar.gz" # 指定模型所在的路径 - test_dir = None # 指定测试文件所在的目录,请注意,若不指定将默认使用paddle.dataset.imdb - word_dict = None # 指定字典所在的路径,请注意,若不指定将默认使用paddle.dataset.imdb - nn_type = "dnn" # 指定测试使用的模型 - ``` +### 如何训练 -### 使用自定义数据运行 +在终端中执行 `sh run.sh` 以下命令, 将以 PaddlePaddle 内置的情感分类数据集:`paddle.dataset.imdb` 直接运行本例,会看到如下输入: -#### step1. 编写自定义的数据读取接口 - -例如有如下格式的数据:每一行为一条样本,以 `\t` 分隔,第一列是类别标签,第二列是输入文本的内容。文本内容中的词语以空格分隔。以下是两条示例数据: - -``` -negative PaddlePaddle is good -positive What a terrible weather -``` - -编写自定义的数据读取接口关键在实现一个 Python 生成器完成**从原始输入文本中解析一条训练样本的逻辑**。 - -以下代码片段实现了:读取以上格式数据返回类型为: `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) 一节。 - -- `data_dir` 测试数据所在路径 -- `word_dict` 词语的字典,用来将原始字符串表示的词语转化为字典中的序号 -- `label_dict` 类别标签的字典,用于将字符串的类别标签,转换成整数类型的序号 - -```python -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[""] - 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 +```text +Pass 0, Batch 0, Cost 0.696031, {'__auc_evaluator_0__': 0.47360000014305115, 'classification_error_evaluator': 0.5} +Pass 0, Batch 100, Cost 0.544438, {'__auc_evaluator_0__': 0.839249312877655, 'classification_error_evaluator': 0.30000001192092896} +Pass 0, Batch 200, Cost 0.406581, {'__auc_evaluator_0__': 0.9030032753944397, 'classification_error_evaluator': 0.2199999988079071} +Test at Pass 0, {'__auc_evaluator_0__': 0.9289745092391968, 'classification_error_evaluator': 0.14927999675273895} ``` +日志每隔 100 个 batch 输出一次,输出信息包括:(1)Pass 序号;(2)Batch 序号;(3)依次输出当前 Batch 上评估指标的评估结果。评估指标在配置网络拓扑结构时指定,在上面的输出中,输出了训练样本集之的 AUC 以及错误率指标。 -本例目录下的 `reader.py` 含有读取训练和测试数据的全部代码。 +### 如何预测 -接下来,只需要将数据读取函数 `train_reader` 作为参数传递给 `train.py` 脚本中的 `paddle.batch` 接口即可使用自定义数据接口读取数据,调用方式如下: - -```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) -``` +训练结束后模型默认存储在当前工作目录下,在终端中执行 `python infer.py` ,预测脚本会加载训练好的模型进行预测。 -#### step 2. 修改命令行参数 +- 默认加载使用 `paddle.data.imdb.train` 训练一个 Pass 产出的 DNN 模型对 `paddle.dataset.imdb.test` 进行测试 -执行 `python train.py --help` 可以获取`train.py` 脚本各项启动参数的详细说明。通过修改 `train.py` 脚本的启动参数,指定自定义数据的路径。 +会看到如下输出: -主要参数如下: - -- `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`:指定训练多少个轮次。 +```text +positive 0.9275 0.0725 previous reviewer gave a much better of the films plot details than i could what i recall mostly is that it was just so beautiful in every sense emotionally visually just br if you like movies that are wonderful to look at and also have emotional content to which that beauty is relevant i think you will be glad to have seen this extraordinary and unusual work of br on a scale of 1 to 10 id give it about an the only reason i shy away from 9 is that it is a mood piece if you are in the mood for a really artistic very romantic film then its a 10 i definitely think its a mustsee but none of us can be in that mood all the time so overall +negative 0.0300 0.9700 i love scifi and am willing to put up with a lot scifi are usually and i tried to like this i really did but it is to good tv scifi as 5 is to star trek the original silly cheap cardboard sets stilted dialogues cg that doesnt match the background and painfully onedimensional characters cannot be overcome with a scifi setting im sure there are those of you out there who think 5 is good scifi tv its not its clichéd and while us viewers might like emotion and character development scifi is a genre that does not take itself seriously star trek it may treat important issues yet not as a serious philosophy its really difficult to care about the characters here as they are not simply just missing a of life their actions and reactions are wooden and predictable often painful to watch the makers of earth know its rubbish as they have to always say gene earth otherwise people would not continue watching must be turning in their as this dull cheap poorly edited watching it without breaks really brings this home of a show into space spoiler so kill off a main character and then bring him back as another actor all over again +``` -如果将数据组织成上一节示例数据的格式,只需在 `run.sh` 脚本中指定 `train_data_dir` 参数,可以直接运行本例,无需修改数据读取接口 `reader.py`。 +输出日志每一行是对一条样本预测的结果,以 `\t` 分隔,共 3 列,分别是:(1)预测类别标签;(2)样本分别属于每一类的概率,内部以空格分隔;(3)输入文本。 + +## 使用自定义数据训练和预测 + +### 如何训练 + +1. 数据组织 + + 假设有如下格式的训练数据:每一行为一条样本,以 `\t` 分隔,第一列是类别标签,第二列是输入文本的内容,文本内容中的词语以空格分隔。以下是两条示例数据: + + ``` + positive PaddlePaddle is good + negative What a terrible weather + ``` + +2. 编写数据读取接口 + + 自定义数据读取接口只需编写一个 Python 生成器实现**从原始输入文本中解析一条训练样本**的逻辑。以下代码片段实现了读取原始数据返回类型为: `paddle.data_type.integer_value_sequence`(词语在字典的序号)和 `paddle.data_type.integer_value`(类别标签)的 2 个输入给网络中定义的 2 个 `data_layer` 的功能。 + ```python + def train_reader(data_dir, word_dict, label_dict): + def reader(): + 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 + ``` + + - 关于 PaddlePaddle 中 `data_layer` 接受输入数据的类型,以及数据读取接口对应该返回数据的格式,请参考 [input-types](http://www.paddlepaddle.org/release_doc/0.9.0/doc_cn/ui/data_provider/pydataprovider2.html#input-types) 一节。 + - 以上代码片段详见本例目录下的 `reader.py` 脚本,`reader.py` 同时提供了读取测试数据的全部代码。 + + 接下来,只需要将数据读取函数 `train_reader` 作为参数传递给 `train.py` 脚本中的 `paddle.batch` 接口即可使用自定义数据接口读取数据,调用方式如下: + + ```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) + ``` + +3. 修改命令行参数 + + - 如果将数据组织成示例数据的同样的格式,只需在 `run.sh` 脚本中修改 `train.py` 启动参数,指定 `train_data_dir` 参数,可以直接运行本例,无需修改数据读取接口 `reader.py`。 + - 执行 `python train.py --help` 可以获取`train.py` 脚本各项启动参数的详细说明,主要参数如下: + - `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`:指定训练多少个轮次。 + +### 如何预测 + +1. 修改 `infer.py` 中以下变量,指定使用的模型、指定测试数据。 + + ```python + model_path = "dnn_params_pass_00000.tar.gz" # 指定模型所在的路径 + nn_type = "dnn" # 指定测试使用的模型 + test_dir = "./data/test" # 指定测试文件所在的目录 + word_dict = "./data/dict/word_dict.txt" # 指定字典所在的路径 + label_dict = "./data/dict/label_dict.txt" # 指定类别标签字典的路径 + ``` +2. 在终端中执行 `python infer.py`。 diff --git a/text_classification/index.html b/text_classification/index.html index 9cc1a050de..21e4e14dac 100644 --- a/text_classification/index.html +++ b/text_classification/index.html @@ -42,25 +42,24 @@ diff --git a/text_classification/infer.py b/text_classification/infer.py index d5e9833ceb..a7ac4e1d5a 100644 --- a/text_classification/infer.py +++ b/text_classification/infer.py @@ -13,20 +13,22 @@ def infer(topology, data_dir, model_path, word_dict_path, label_dict_path, batch_size): - def _infer_a_batch(inferer, test_batch): + def _infer_a_batch(inferer, test_batch, ids_2_word, ids_2_label): 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]))) + for word_ids, prob in zip(test_batch, probs): + word_text = " ".join([ids_2_word[id] for id in word_ids[0]]) + print("%s\t%s\t%s" % (ids_2_label[prob.argmax()], + " ".join(["{:0.4f}".format(p) + for p in prob]), word_text)) logger.info('begin to predict...') use_default_data = (data_dir is None) if use_default_data: word_dict = paddle.dataset.imdb.word_dict() + word_reverse_dict = dict((value, key) + for key, value in word_dict.iteritems()) label_reverse_dict = {0: "positive", 1: "negative"} test_reader = paddle.dataset.imdb.test(word_dict) else: @@ -34,7 +36,9 @@ def _infer_a_batch(inferer, test_batch): 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) + word_reverse_dict = load_reverse_dict(word_dict_path) label_reverse_dict = load_reverse_dict(label_dict_path) test_reader = reader.test_reader(data_dir, word_dict)() @@ -56,11 +60,14 @@ def _infer_a_batch(inferer, 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) + _infer_a_batch(inferer, test_batch, word_reverse_dict, + label_reverse_dict) test_batch = [] - _infer_a_batch(inferer, test_batch) - test_batch = [] + if len(test_batch): + _infer_a_batch(inferer, test_batch, word_reverse_dict, + label_reverse_dict) + test_batch = [] if __name__ == '__main__':