提交 436f480d 编写于 作者: C Cao Ying 提交者: GitHub

Merge pull request #102 from lcy-seso/refine_ner

refine NER.
# 命名实体识别 # 命名实体识别
以下是本例的简要目录结构及说明:
```text
.
├── data # 存储运行本例所依赖的数据
│   ├── download.sh
├── images # README 文档中的图片
├── index.html
├── infer.py # 测试脚本
├── network_conf.py # 模型定义
├── reader.py # 数据读取接口
├── README.md # 文档
├── train.py # 训练脚本
└── utils.py # 定义同样的函数
```
## 简介
命名实体识别(Named Entity Recognition,NER)又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等,是自然语言处理研究的一个基础问题。NER任务通常包括实体边界识别、确定实体类别两部分,可以将其作为序列标注问题解决。 命名实体识别(Named Entity Recognition,NER)又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等,是自然语言处理研究的一个基础问题。NER任务通常包括实体边界识别、确定实体类别两部分,可以将其作为序列标注问题解决。
序列标注可以分为Sequence Classification、Segment Classification和Temporal Classification三类[[1](#参考文献)],本例只考虑Segment Classification,即对输入序列中的每个元素在输出序列中给出对应的标签。对于NER任务,由于需要标识边界,一般采用[BIO方式](http://book.paddlepaddle.org/07.label_semantic_roles/)定义的标签集,如下是一个NER的标注结果示例: 序列标注可以分为Sequence Classification、Segment Classification和Temporal Classification三类[[1](#参考文献)],本例只考虑Segment Classification,即对输入序列中的每个元素在输出序列中给出对应的标签。对于NER任务,由于需要标识边界,一般采用[BIO标注方法](http://book.paddlepaddle.org/07.label_semantic_roles/)定义的标签集,如下是一个NER的标注结果示例:
<div align="center"> <div align="center">
<img src="images/ner_label_ins.png" width = "80%" align=center /><br> <img src="images/ner_label_ins.png" width = "80%" align=center /><br>
图1. BIO标注方法示例 图1. BIO标注方法示例
</div> </div>
根据序列标注结果可以直接得到实体边界和实体类别。类似的,分词、词性标注、语块识别、[语义角色标注](http://book.paddlepaddle.org/07.label_semantic_roles/index.cn.html)等任务同样可通过序列标注来解决。 根据序列标注结果可以直接得到实体边界和实体类别。类似的,分词、词性标注、语块识别、[语义角色标注](http://book.paddlepaddle.org/07.label_semantic_roles/index.cn.html)等任务都可通过序列标注来解决。使用神经网络模型解决问题的思路通常是:前层网络学习输入的特征表示,网络的最后一层在特征基础上完成最终的任务;对于序列标注问题,通常:使用基于RNN的网络结构学习特征,将学习到的特征接入CRF完成序列标注。实际上是将传统CRF中的线性模型换成了非线性神经网络。沿用CRF的出发点是:CRF使用句子级别的似然概率,能够更好的解决标记偏置问题[[2](#参考文献)]。本例也将基于此思路建立模型。虽然,这里以NER任务作为示例,但所给出的模型可以应用到其他各种序列标注任务中。
由于序列标注问题的广泛性,产生了[CRF](http://book.paddlepaddle.org/07.label_semantic_roles/index.cn.html)等经典的序列模型,这些模型大多只能使用局部信息或需要人工设计特征。随着深度学习研究的发展,循环神经网络(Recurrent Neural Network,RNN等序列模型能够处理序列元素之间前后关联问题,能够从原始输入文本中学习特征表示,而更加适合序列标注任务,更多相关知识可参考PaddleBook中[语义角色标注](https://github.com/PaddlePaddle/book/blob/develop/07.label_semantic_roles/README.cn.md)一课。
使用神经网络模型解决问题的思路通常是:前层网络学习输入的特征表示,网络的最后一层在特征基础上完成最终的任务;对于序列标注问题,通常:使用基于RNN的网络结构学习特征,将学习到的特征接入CRF完成序列标注。实际上是将传统CRF中的线性模型换成了非线性神经网络。沿用CRF的出发点是:CRF使用句子级别的似然概率,能够更好的解决标记偏置问题[[2](#参考文献)]。本例也将基于此思路建立模型。虽然,这里以NER任务作为示例,但所给出的模型可以应用到其他各种序列标注任务中 由于序列标注问题的广泛性,产生了[CRF](http://book.paddlepaddle.org/07.label_semantic_roles/index.cn.html)等经典的序列模型,这些模型大多只能使用局部信息或需要人工设计特征。随着深度学习研究的发展,循环神经网络(Recurrent Neural Network,RNN等 序列模型能够处理序列元素之间前后关联问题,能够从原始输入文本中学习特征表示,而更加适合序列标注任务,更多相关知识可参考PaddleBook中[语义角色标注](https://github.com/PaddlePaddle/book/blob/develop/07.label_semantic_roles/README.cn.md)一课
## 模型说明 ## 模型详解
NER任务的输入是"一句话",目标是识别句子中的实体边界及类别,我们参照论文\[[2](#参考文献)\]仅对原始句子进行了一些预处理工作:将每个词转换为小写,并将原词是否大写另作为一个特征,共同作为模型的输入。按照上述处理序列标注问题的思路,可构造如下结构的模型(图2是模型结构示意图) NER任务的输入是"一句话",目标是识别句子中的实体边界及类别,我们参照论文\[[2](#参考文献)\]仅对原始句子进行了一些简单的预处理工作:将每个词转换为小写,并将原词是否大写另作为一个特征,共同作为模型的输入。模型如图2所示,工作流程如下
1. 构造输入 1. 构造输入
- 输入1是句子序列,采用one-hot方式表示 - 输入1是句子序列,采用one-hot方式表示
...@@ -28,51 +45,47 @@ NER任务的输入是"一句话",目标是识别句子中的实体边界及类 ...@@ -28,51 +45,47 @@ NER任务的输入是"一句话",目标是识别句子中的实体边界及类
<div align="center"> <div align="center">
<img src="images/ner_network.png" width = "40%" align=center /><br> <img src="images/ner_network.png" width = "40%" align=center /><br>
图2. NER模型的网络结构图 图2. NER 模型网络结构图
</div> </div>
## 数据说明 ## 数据说明
在本例中,我们使用CoNLL 2003 NER任务中开放出的数据集。该任务(见[此页面](http://www.clips.uantwerpen.be/conll2003/ner/))只提供了标注工具的下载,原始Reuters数据由于版权原因需另外申请免费下载。在获取原始数据后可参照标注工具中README生成所需数据文件,完成后将包括如下三个数据文件: 在本例中,我们[CoNLL 2003 NER任务](http://www.clips.uantwerpen.be/conll2003/ner/)为例,原始Reuters数据由于版权原因需另外申请免费下载,请大家按照原网站说明获取。
| 文件名 | 描述 | + 我们仅在`data`目录下的`train``test`文件中放置少数样本用以示例输入数据格式。
|---|---| + 本例依赖数据还包括
| eng.train | 训练数据 | 1. 输入文本的词典
| eng.testa | 验证数据,可用来进行参数调优 | 2. 为词典中的词语提供预训练好的词向量
| eng.testb | 评估数据,用来进行最终效果评估 | 2. 标记标签的词典
标记标签词典已附在`data`目录中,对应于`data/target.txt`文件。输入文本的词典以及词典中词语的预训练的词向量来自:[Stanford CS224d](http://cs224d.stanford.edu/)课程作业。**为运行本例,请首先在`data`目录下运行`download.sh`脚本下载输入文本的词典和预训练的词向量。** 完成后会将这两个文件一并放入`data`目录下,输入文本的词典和预训练的词向量分别对应:`data/vocab.txt``data/wordVectors.txt`这两个文件。
为保证本例的完整性,我们从中抽取少量样本放在`data/train``data/test`文件中,作为示例使用;由于版权原因,完整数据还请大家自行获取。这三个文件数据格式如下: CoNLL 2003原始数据格式如下:
``` ```
U.N. NNP I-NP I-ORG U.N. NNP I-NP I-ORG
official NN I-NP O official NN I-NP O
Ekeus NNP I-NP I-PER Ekeus NNP I-NP I-PER
heads VBZ I-VP O heads VBZ I-VP O
for IN I-PP O for IN I-PP O
Baghdad NNP I-NP I-LOC Baghdad NNP I-NP I-LOC
. . O O . . O O
``` ```
其中第一列为原始句子序列(第二、三列分别为词性标签和句法分析中的语块标签,这里暂时不用),第四列为采用了I-TYPE方式表示的NER标签(I-TYPE和BIO方式的主要区别在于语块开始标记的使用上,I-TYPE只有在出现相邻的同类别实体时对后者使用B标记,其他均使用I标记),句子之间以空行分隔。 - 第一列为原始句子序列
- 第二、三列分别为词性标签和句法分析中的语块标签,本例不使用
- 第四列为采用了 I-TYPE 方式表示的NER标签
- I-TYPE 和 BIO 方式的主要区别在于语块开始标记的使用上,I-TYPE只有在出现相邻的同类别实体时对后者使用B标记,其他均使用I标记),句子之间以空行分隔。
原始数据需要进行数据预处理才能被PaddlePaddle处理,预处理主要包括下面几个步骤: 我们在`reader.py`脚本中完成对原始数据的处理以及读取,主要包括下面几个步骤:
1. 从原始数据文件中抽取出句子和标签,构造句子序列和标签序列; 1. 从原始数据文件中抽取出句子和标签,构造句子序列和标签序列;
2.I-TYPE表示的标签转换为BIO方式表示的标签; 2. I-TYPE 表示的标签转换为 BIO 方式表示的标签;
3. 将句子序列中的单词转换为小写,并构造大写标记序列; 3. 将句子序列中的单词转换为小写,并构造大写标记序列;
4. 依据词典获取词对应的整数索引。 4. 依据词典获取词对应的整数索引。
我们将在`conll03.py`中完成以上预处理工作(使用方法将在后文给出):
```python 预处理完成后,一条训练样本包含3个部分作为神经网络的输入信息用于训练:(1)句子序列;(2)首字母大写标记序列;(3)标注序列,下表是一条训练样本的示例:
# import conll03
# conll03.corpus_reader函数完成上面第1步和第2步.
# conll03.reader_creator函数完成上面第3步和第4步.
# conll03.train和conll03.test函数可以获取处理之后的每条样本来供PaddlePaddle训练和测试.
```
预处理完成后,一条训练样本包含3个部分:句子序列、首字母大写标记序列、标注序列。下表是一条训练样本的示例。
| 句子序列 | 大写标记序列 | 标注序列 | | 句子序列 | 大写标记序列 | 标注序列 |
|---|---|---| |---|---|---|
...@@ -84,165 +97,65 @@ NER任务的输入是"一句话",目标是识别句子中的实体边界及类 ...@@ -84,165 +97,65 @@ NER任务的输入是"一句话",目标是识别句子中的实体边界及类
| baghdad | 1 | B-LOC | | baghdad | 1 | B-LOC |
| . | 0 | O | | . | 0 | O |
另外,本例依赖的数据还包括:word词典、label词典和预训练的词向量三个文件。label词典已附在`data`目录中,对应于`data/target.txt`;word词典和预训练的词向量来源于[Stanford CS224d](http://cs224d.stanford.edu/)课程作业,请先在该示例所在目录下运行`data/download.sh`脚本进行下载,完成后会将这两个文件一并放入`data`目录下,分别对应`data/vocab.txt``data/wordVectors.txt` ## 运行
### 编写数据读取接口
## 使用说明
本示例给出的`conll03.py``ner.py`两个Python脚本分别提供了数据相关和模型相关接口。
### 数据接口使用
`conll03.py`提供了使用CoNLL 2003数据的接口,各主要函数的功能已在数据说明部分进行说明。结合我们提供的接口和文件,可以按照如下步骤使用CoNLL 2003数据:
1. 定义各数据文件、词典文件和词向量文件路径; 自定义数据读取接口只需编写一个 Python 生成器实现从原始输入文本中解析一条训练样本的逻辑。[reader.py](./reader.py) 中的`data_reader`函数实现了读取原始数据返回类型为: `paddle.data_type.integer_value_sequence`的 3 个输入(分别对应:词语在字典的序号、是否为大写、标注结果在字典中的序号)给`network_conf.ner_net`中定义的 3 个 `data_layer` 的功能。
2. 调用`conll03.train``conll03.test`接口。
对应如下代码: ### 训练
```python 1. 运行 `sh data/download.sh`
import conll03 2. 修改 `train.py``main` 函数,指定数据路径
# 修改以下变量为对应文件路径 ```python
train_data_file = 'data/train' # 训练数据文件的路径 main(
test_data_file = 'data/test' # 测试数据文件的路径 train_data_file='data/train',
vocab_file = 'data/vocab.txt' # 输入句子对应的字典文件的路径 test_data_file='data/test',
target_file = 'data/target.txt' # 标签对应的字典文件的路径 vocab_file='data/vocab.txt',
emb_file = 'data/wordVectors.txt' # 预训练的词向量参数的路径 target_file='data/target.txt',
emb_file='data/wordVectors.txt')
# 返回训练数据的生成器
train_data_reader = conll03.train(train_data_file, vocab_file, target_file)
# 返回测试数据的生成器
test_data_reader = conll03.test(test_data_file, vocab_file, target_file)
```
### 模型接口使用
`ner.py`提供了以下两个接口分别进行模型训练和预测:
1. `ner_net_train(data_reader, num_passes)`函数实现了模型训练功能,参数`data_reader`表示训练数据的迭代器、`num_passes`表示训练pass的轮数。训练过程中每100个iteration会打印模型训练信息。我们同时在模型配置中加入了chunk evaluator,会输出当前模型对语块识别的Precision、Recall和F1值。chunk evaluator 的详细使用说明请参照[文档](http://www.paddlepaddle.org/develop/doc/api/v2/config/evaluators.html#chunk)。每个pass后会将模型保存为`params_pass_***.tar.gz`的文件(`***`表示pass的id)。
2. `ner_net_infer(data_reader, model_file)`函数实现了预测功能,参数`data_reader`表示测试数据的迭代器、`model_file`表示保存在本地的模型文件,预测过程会按如下格式打印预测结果:
```
U.N. B-ORG
official O
Ekeus B-PER
heads O
for O
Baghdad B-LOC
. O
``` ```
其中第一列为原始句子序列,第二列为BIO方式表示的NER标签。
### 运行程序
本例另在`ner.py`中提供了完整的运行流程,包括数据接口的使用和模型训练、预测。根据上文所述的接口使用方法,使用时需要将`ner.py`中如下的数据设置部分中的各变量修改为对应文件路径:
```python 3. 运行命令 `python train.py`**需要注意:直接运行使用的是示例数据,请替换真实的标记数据。**
# 修改以下变量为对应文件路径
train_data_file = 'data/train' # 训练数据文件的路径
test_data_file = 'data/test' # 测试数据文件的路径
vocab_file = 'data/vocab.txt' # 输入句子对应的字典文件的路径
target_file = 'data/target.txt' # 标签对应的字典文件的路径
emb_file = 'data/wordVectors.txt' # 预训练的词向量参数的路径
```
各接口的调用已在`ner.py`中提供:
```python
# 训练数据的生成器
train_data_reader = conll03.train(train_data_file, vocab_file, target_file)
# 测试数据的生成器
test_data_reader = conll03.test(test_data_file, vocab_file, target_file)
# 模型训练
ner_net_train(data_reader=train_data_reader, num_passes=1)
# 预测
ner_net_infer(data_reader=test_data_reader, model_file='params_pass_0.tar.gz')
```
为运行序列标注模型除适当调整`num_passes``model_file`两参数值外,无需再做其它修改(也可根据需要自行调用各接口,如只使用预测功能)。完成修改后,运行本示例只需在`ner.py`所在路径下执行`python ner.py`即可。该示例程序会执行数据读取、模型训练和保存、模型读取及新样本预测等步骤。
### 自定义数据和任务
前文提到本例中的模型可以应用到其他序列标注任务中,这里以词性标注任务为例,给出使用其他数据,并应用到其他任务的操作方法。
假定有如下格式的原始数据:
```
U.N. NNP
official NN
Ekeus NNP
heads VBZ
for IN
Baghdad NNP
. .
```
第一列为原始句子序列,第二列为词性标签序列,两列之间以“\t”分隔,句子之间以空行分隔。
为使用PaddlePaddle和本示例提供的模型,可参照`conll03.py`并根据需要自定义数据接口,如下:
1. 参照`conll03.py`中的`corpus_reader`函数,定义接口返回句子序列和标签序列生成器; ```text
commandline: --use_gpu=False --trainer_count=1
```python Initing parameters..
# 实现句子和对应标签的抽取,传入数据文件路径,返回句子和标签序列生成器。 Init parameters done.
def corpus_reader(filename): Pass 0, Batch 0, Cost 41.430110, {'ner_chunk.precision': 0.01587301678955555, 'ner_chunk.F1-score': 0.028368793427944183, 'ner_chunk.recall': 0.13333334028720856, 'error': 0.939393937587738}
def reader(): Test with Pass 0, Batch 0, {'ner_chunk.precision': 0.0, 'ner_chunk.F1-score': 0.0, 'ner_chunk.recall': 0.0, 'error': 0.16260161995887756}
sentence = []
labels = []
with open(filename) as f:
for line in f:
if len(line.strip()) == 0:
if len(sentence) > 0:
yield sentence, labels
sentence = []
labels = []
else:
segs = line.strip().split()
sentence.append(segs[0])
labels.append(segs[-1])
f.close()
return reader
``` ```
2. 参照`conll03.py`中的`reader_creator`函数,定义接口返回id化的句子和标签序列生成器。 ### 预测
1. 修改 [infer.py](./infer.py)`main` 函数,指定:需要测试的模型的路径、测试数据、字典文件,预测标记文件的路径,默认参数如下:
```python ```python
# 传入corpus_reader返回的生成器、dict类型的word词典和label词典,返回id化的句子和标签序列生成器。 infer(
def reader_creator(corpus_reader, word_dict, label_dict): model_path="models/params_pass_0.tar.gz",
def reader(): batch_size=2,
for sentence, labels in corpus_reader(): test_data_file="data/test",
word_idx = [ vocab_file="data/vocab.txt",
word_dict.get(w, UNK_IDX) # 若使用小写单词,请使用w.lower() target_file="data/target.txt")
for w in sentence
]
# 若使用首字母大写标记,请去掉以下注释符号,并在yield语句的word_idx后加上mark
# mark = [
# 1 if w[0].isupper() else 0
# for w in sentence
# ]
label_idx = [label_dict.get(w) for w in labels]
yield word_idx, label_idx, sentence # 加上sentence方便预测时打印
return reader
``` ```
自定义了数据接口后,要使用本示例中的模型,只需在调用模型训练和预测接口`ner_net_train``ner_net_infer`时传入调用`reader_creator`返回的生成器即可。另外需要注意,这里给出的数据接口定义去掉了`conll03.py`一些预处理(使用原始句子,而非转换成小写单词加上大写标记),`ner.py`中的模型相关接口也需要进行一些调整: 2. 在终端运行 `python infer.py`,开始测试,会看到如下预测结果:
1. 修改网络结构定义接口`ner_net`中大写标记相关内容: ```text
cricket O
- O
leicestershire O
take O
over O
at O
top O
after O
innings O
victory O
. O
删去`mark`和`mark_embedding`两个变量; ```
输出分为两列,以“\t” 分隔,第一列是输入的词语,第二列是标记结果。多条输入序列之间以空行分隔。
2. 修改模型训练接口`ner_net_train`中大写标记相关内容:
将变量`feeding`定义改为`feeding = {'word': 0, 'target': 1}`;
3. 修改预测接口`ner_net_infer`中大写标记相关内容:
将`test_data.append([item[0], item[1]])`改为`test_data.append([item[0]])`。
如果要继续使用NER中的特征预处理(小写单词、大写标记),请参照上文`reader_creator`代码段给出的注释进行修改,此时`ner.py`中的模型相关接口不必进行修改。
## 参考文献 ## 参考文献
......
wget http://cs224d.stanford.edu/assignment2/assignment2.zip wget http://cs224d.stanford.edu/assignment2/assignment2.zip
unzip assignment2.zip
cp assignment2_release/data/ner/wordVectors.txt data/ if [ $? -eq 0 ];then
cp assignment2_release/data/ner/vocab.txt data/ unzip assignment2.zip
rm -rf assignment2.zip assignment2_release cp assignment2_release/data/ner/wordVectors.txt ./data
cp assignment2_release/data/ner/vocab.txt ./data
rm -rf assignment2.zip assignment2_release
else
echo "download data error!" >> /dev/stderr
exit 1
fi
-DOCSTART- -X- O O
CRICKET NNP I-NP O CRICKET NNP I-NP O
- : O O - : O O
LEICESTERSHIRE NNP I-NP I-ORG LEICESTERSHIRE NNP I-NP I-ORG
......
-DOCSTART- -X- O O
EU NNP I-NP I-ORG EU NNP I-NP I-ORG
rejects VBZ I-VP O rejects VBZ I-VP O
German JJ I-NP I-MISC German JJ I-NP I-MISC
......
因为 它太大了无法显示 source diff 。你可以改为 查看blob
...@@ -42,24 +42,41 @@ ...@@ -42,24 +42,41 @@
<div id="markdown" style='display:none'> <div id="markdown" style='display:none'>
# 命名实体识别 # 命名实体识别
以下是本例的简要目录结构及说明:
```text
.
├── data # 存储运行本例所依赖的数据
│   ├── download.sh
├── images # README 文档中的图片
├── index.html
├── infer.py # 测试脚本
├── network_conf.py # 模型定义
├── reader.py # 数据读取接口
├── README.md # 文档
├── train.py # 训练脚本
└── utils.py # 定义同样的函数
```
## 简介
命名实体识别(Named Entity Recognition,NER)又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等,是自然语言处理研究的一个基础问题。NER任务通常包括实体边界识别、确定实体类别两部分,可以将其作为序列标注问题解决。 命名实体识别(Named Entity Recognition,NER)又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括人名、地名、机构名、专有名词等,是自然语言处理研究的一个基础问题。NER任务通常包括实体边界识别、确定实体类别两部分,可以将其作为序列标注问题解决。
序列标注可以分为Sequence Classification、Segment Classification和Temporal Classification三类[[1](#参考文献)],本例只考虑Segment Classification,即对输入序列中的每个元素在输出序列中给出对应的标签。对于NER任务,由于需要标识边界,一般采用[BIO方式](http://book.paddlepaddle.org/07.label_semantic_roles/)定义的标签集,如下是一个NER的标注结果示例: 序列标注可以分为Sequence Classification、Segment Classification和Temporal Classification三类[[1](#参考文献)],本例只考虑Segment Classification,即对输入序列中的每个元素在输出序列中给出对应的标签。对于NER任务,由于需要标识边界,一般采用[BIO标注方法](http://book.paddlepaddle.org/07.label_semantic_roles/)定义的标签集,如下是一个NER的标注结果示例:
<div align="center"> <div align="center">
<img src="images/ner_label_ins.png" width = "80%" align=center /><br> <img src="images/ner_label_ins.png" width = "80%" align=center /><br>
图1. BIO标注方法示例 图1. BIO标注方法示例
</div> </div>
根据序列标注结果可以直接得到实体边界和实体类别。类似的,分词、词性标注、语块识别、[语义角色标注](http://book.paddlepaddle.org/07.label_semantic_roles/index.cn.html)等任务同样可通过序列标注来解决。 根据序列标注结果可以直接得到实体边界和实体类别。类似的,分词、词性标注、语块识别、[语义角色标注](http://book.paddlepaddle.org/07.label_semantic_roles/index.cn.html)等任务都可通过序列标注来解决。使用神经网络模型解决问题的思路通常是:前层网络学习输入的特征表示,网络的最后一层在特征基础上完成最终的任务;对于序列标注问题,通常:使用基于RNN的网络结构学习特征,将学习到的特征接入CRF完成序列标注。实际上是将传统CRF中的线性模型换成了非线性神经网络。沿用CRF的出发点是:CRF使用句子级别的似然概率,能够更好的解决标记偏置问题[[2](#参考文献)]。本例也将基于此思路建立模型。虽然,这里以NER任务作为示例,但所给出的模型可以应用到其他各种序列标注任务中。
由于序列标注问题的广泛性,产生了[CRF](http://book.paddlepaddle.org/07.label_semantic_roles/index.cn.html)等经典的序列模型,这些模型大多只能使用局部信息或需要人工设计特征。随着深度学习研究的发展,循环神经网络(Recurrent Neural Network,RNN等序列模型能够处理序列元素之间前后关联问题,能够从原始输入文本中学习特征表示,而更加适合序列标注任务,更多相关知识可参考PaddleBook中[语义角色标注](https://github.com/PaddlePaddle/book/blob/develop/07.label_semantic_roles/README.cn.md)一课。
使用神经网络模型解决问题的思路通常是:前层网络学习输入的特征表示,网络的最后一层在特征基础上完成最终的任务;对于序列标注问题,通常:使用基于RNN的网络结构学习特征,将学习到的特征接入CRF完成序列标注。实际上是将传统CRF中的线性模型换成了非线性神经网络。沿用CRF的出发点是:CRF使用句子级别的似然概率,能够更好的解决标记偏置问题[[2](#参考文献)]。本例也将基于此思路建立模型。虽然,这里以NER任务作为示例,但所给出的模型可以应用到其他各种序列标注任务中 由于序列标注问题的广泛性,产生了[CRF](http://book.paddlepaddle.org/07.label_semantic_roles/index.cn.html)等经典的序列模型,这些模型大多只能使用局部信息或需要人工设计特征。随着深度学习研究的发展,循环神经网络(Recurrent Neural Network,RNN等 序列模型能够处理序列元素之间前后关联问题,能够从原始输入文本中学习特征表示,而更加适合序列标注任务,更多相关知识可参考PaddleBook中[语义角色标注](https://github.com/PaddlePaddle/book/blob/develop/07.label_semantic_roles/README.cn.md)一课
## 模型说明 ## 模型详解
NER任务的输入是"一句话",目标是识别句子中的实体边界及类别,我们参照论文\[[2](#参考文献)\]仅对原始句子进行了一些预处理工作:将每个词转换为小写,并将原词是否大写另作为一个特征,共同作为模型的输入。按照上述处理序列标注问题的思路,可构造如下结构的模型(图2是模型结构示意图) NER任务的输入是"一句话",目标是识别句子中的实体边界及类别,我们参照论文\[[2](#参考文献)\]仅对原始句子进行了一些简单的预处理工作:将每个词转换为小写,并将原词是否大写另作为一个特征,共同作为模型的输入。模型如图2所示,工作流程如下
1. 构造输入 1. 构造输入
- 输入1是句子序列,采用one-hot方式表示 - 输入1是句子序列,采用one-hot方式表示
...@@ -70,51 +87,47 @@ NER任务的输入是"一句话",目标是识别句子中的实体边界及类 ...@@ -70,51 +87,47 @@ NER任务的输入是"一句话",目标是识别句子中的实体边界及类
<div align="center"> <div align="center">
<img src="images/ner_network.png" width = "40%" align=center /><br> <img src="images/ner_network.png" width = "40%" align=center /><br>
图2. NER模型的网络结构图 图2. NER 模型网络结构图
</div> </div>
## 数据说明 ## 数据说明
在本例中,我们使用CoNLL 2003 NER任务中开放出的数据集。该任务(见[此页面](http://www.clips.uantwerpen.be/conll2003/ner/))只提供了标注工具的下载,原始Reuters数据由于版权原因需另外申请免费下载。在获取原始数据后可参照标注工具中README生成所需数据文件,完成后将包括如下三个数据文件: 在本例中,我们以 [CoNLL 2003 NER任务](http://www.clips.uantwerpen.be/conll2003/ner/)为例,原始Reuters数据由于版权原因需另外申请免费下载,请大家按照原网站说明获取。
| 文件名 | 描述 | + 我们仅在`data`目录下的`train`和`test`文件中放置少数样本用以示例输入数据格式。
|---|---| + 本例依赖数据还包括
| eng.train | 训练数据 | 1. 输入文本的词典
| eng.testa | 验证数据,可用来进行参数调优 | 2. 为词典中的词语提供预训练好的词向量
| eng.testb | 评估数据,用来进行最终效果评估 | 2. 标记标签的词典
标记标签词典已附在`data`目录中,对应于`data/target.txt`文件。输入文本的词典以及词典中词语的预训练的词向量来自:[Stanford CS224d](http://cs224d.stanford.edu/)课程作业。**为运行本例,请首先在`data`目录下运行`download.sh`脚本下载输入文本的词典和预训练的词向量。** 完成后会将这两个文件一并放入`data`目录下,输入文本的词典和预训练的词向量分别对应:`data/vocab.txt`和`data/wordVectors.txt`这两个文件。
为保证本例的完整性,我们从中抽取少量样本放在`data/train`和`data/test`文件中,作为示例使用;由于版权原因,完整数据还请大家自行获取。这三个文件数据格式如下: CoNLL 2003原始数据格式如下:
``` ```
U.N. NNP I-NP I-ORG U.N. NNP I-NP I-ORG
official NN I-NP O official NN I-NP O
Ekeus NNP I-NP I-PER Ekeus NNP I-NP I-PER
heads VBZ I-VP O heads VBZ I-VP O
for IN I-PP O for IN I-PP O
Baghdad NNP I-NP I-LOC Baghdad NNP I-NP I-LOC
. . O O . . O O
``` ```
其中第一列为原始句子序列(第二、三列分别为词性标签和句法分析中的语块标签,这里暂时不用),第四列为采用了I-TYPE方式表示的NER标签(I-TYPE和BIO方式的主要区别在于语块开始标记的使用上,I-TYPE只有在出现相邻的同类别实体时对后者使用B标记,其他均使用I标记),句子之间以空行分隔。 - 第一列为原始句子序列
- 第二、三列分别为词性标签和句法分析中的语块标签,本例不使用
- 第四列为采用了 I-TYPE 方式表示的NER标签
- I-TYPE 和 BIO 方式的主要区别在于语块开始标记的使用上,I-TYPE只有在出现相邻的同类别实体时对后者使用B标记,其他均使用I标记),句子之间以空行分隔。
原始数据需要进行数据预处理才能被PaddlePaddle处理,预处理主要包括下面几个步骤: 我们在`reader.py`脚本中完成对原始数据的处理以及读取,主要包括下面几个步骤:
1. 从原始数据文件中抽取出句子和标签,构造句子序列和标签序列; 1. 从原始数据文件中抽取出句子和标签,构造句子序列和标签序列;
2. 将I-TYPE表示的标签转换为BIO方式表示的标签; 2. 将 I-TYPE 表示的标签转换为 BIO 方式表示的标签;
3. 将句子序列中的单词转换为小写,并构造大写标记序列; 3. 将句子序列中的单词转换为小写,并构造大写标记序列;
4. 依据词典获取词对应的整数索引。 4. 依据词典获取词对应的整数索引。
我们将在`conll03.py`中完成以上预处理工作(使用方法将在后文给出):
```python 预处理完成后,一条训练样本包含3个部分作为神经网络的输入信息用于训练:(1)句子序列;(2)首字母大写标记序列;(3)标注序列,下表是一条训练样本的示例:
# import conll03
# conll03.corpus_reader函数完成上面第1步和第2步.
# conll03.reader_creator函数完成上面第3步和第4步.
# conll03.train和conll03.test函数可以获取处理之后的每条样本来供PaddlePaddle训练和测试.
```
预处理完成后,一条训练样本包含3个部分:句子序列、首字母大写标记序列、标注序列。下表是一条训练样本的示例。
| 句子序列 | 大写标记序列 | 标注序列 | | 句子序列 | 大写标记序列 | 标注序列 |
|---|---|---| |---|---|---|
...@@ -126,165 +139,65 @@ NER任务的输入是"一句话",目标是识别句子中的实体边界及类 ...@@ -126,165 +139,65 @@ NER任务的输入是"一句话",目标是识别句子中的实体边界及类
| baghdad | 1 | B-LOC | | baghdad | 1 | B-LOC |
| . | 0 | O | | . | 0 | O |
另外,本例依赖的数据还包括:word词典、label词典和预训练的词向量三个文件。label词典已附在`data`目录中,对应于`data/target.txt`;word词典和预训练的词向量来源于[Stanford CS224d](http://cs224d.stanford.edu/)课程作业,请先在该示例所在目录下运行`data/download.sh`脚本进行下载,完成后会将这两个文件一并放入`data`目录下,分别对应`data/vocab.txt`和`data/wordVectors.txt`。 ## 运行
### 编写数据读取接口
## 使用说明
本示例给出的`conll03.py`和`ner.py`两个Python脚本分别提供了数据相关和模型相关接口。
### 数据接口使用
`conll03.py`提供了使用CoNLL 2003数据的接口,各主要函数的功能已在数据说明部分进行说明。结合我们提供的接口和文件,可以按照如下步骤使用CoNLL 2003数据:
1. 定义各数据文件、词典文件和词向量文件路径; 自定义数据读取接口只需编写一个 Python 生成器实现从原始输入文本中解析一条训练样本的逻辑。[reader.py](./reader.py) 中的`data_reader`函数实现了读取原始数据返回类型为: `paddle.data_type.integer_value_sequence`的 3 个输入(分别对应:词语在字典的序号、是否为大写、标注结果在字典中的序号)给`network_conf.ner_net`中定义的 3 个 `data_layer` 的功能。
2. 调用`conll03.train`和`conll03.test`接口。
对应如下代码: ### 训练
```python 1. 运行 `sh data/download.sh`
import conll03 2. 修改 `train.py` 的 `main` 函数,指定数据路径
# 修改以下变量为对应文件路径 ```python
train_data_file = 'data/train' # 训练数据文件的路径 main(
test_data_file = 'data/test' # 测试数据文件的路径 train_data_file='data/train',
vocab_file = 'data/vocab.txt' # 输入句子对应的字典文件的路径 test_data_file='data/test',
target_file = 'data/target.txt' # 标签对应的字典文件的路径 vocab_file='data/vocab.txt',
emb_file = 'data/wordVectors.txt' # 预训练的词向量参数的路径 target_file='data/target.txt',
emb_file='data/wordVectors.txt')
# 返回训练数据的生成器
train_data_reader = conll03.train(train_data_file, vocab_file, target_file)
# 返回测试数据的生成器
test_data_reader = conll03.test(test_data_file, vocab_file, target_file)
```
### 模型接口使用
`ner.py`提供了以下两个接口分别进行模型训练和预测:
1. `ner_net_train(data_reader, num_passes)`函数实现了模型训练功能,参数`data_reader`表示训练数据的迭代器、`num_passes`表示训练pass的轮数。训练过程中每100个iteration会打印模型训练信息。我们同时在模型配置中加入了chunk evaluator,会输出当前模型对语块识别的Precision、Recall和F1值。chunk evaluator 的详细使用说明请参照[文档](http://www.paddlepaddle.org/develop/doc/api/v2/config/evaluators.html#chunk)。每个pass后会将模型保存为`params_pass_***.tar.gz`的文件(`***`表示pass的id)。
2. `ner_net_infer(data_reader, model_file)`函数实现了预测功能,参数`data_reader`表示测试数据的迭代器、`model_file`表示保存在本地的模型文件,预测过程会按如下格式打印预测结果:
```
U.N. B-ORG
official O
Ekeus B-PER
heads O
for O
Baghdad B-LOC
. O
``` ```
其中第一列为原始句子序列,第二列为BIO方式表示的NER标签。
### 运行程序
本例另在`ner.py`中提供了完整的运行流程,包括数据接口的使用和模型训练、预测。根据上文所述的接口使用方法,使用时需要将`ner.py`中如下的数据设置部分中的各变量修改为对应文件路径:
```python 3. 运行命令 `python train.py` ,**需要注意:直接运行使用的是示例数据,请替换真实的标记数据。**
# 修改以下变量为对应文件路径
train_data_file = 'data/train' # 训练数据文件的路径
test_data_file = 'data/test' # 测试数据文件的路径
vocab_file = 'data/vocab.txt' # 输入句子对应的字典文件的路径
target_file = 'data/target.txt' # 标签对应的字典文件的路径
emb_file = 'data/wordVectors.txt' # 预训练的词向量参数的路径
```
各接口的调用已在`ner.py`中提供:
```python
# 训练数据的生成器
train_data_reader = conll03.train(train_data_file, vocab_file, target_file)
# 测试数据的生成器
test_data_reader = conll03.test(test_data_file, vocab_file, target_file)
# 模型训练
ner_net_train(data_reader=train_data_reader, num_passes=1)
# 预测
ner_net_infer(data_reader=test_data_reader, model_file='params_pass_0.tar.gz')
```
为运行序列标注模型除适当调整`num_passes`和`model_file`两参数值外,无需再做其它修改(也可根据需要自行调用各接口,如只使用预测功能)。完成修改后,运行本示例只需在`ner.py`所在路径下执行`python ner.py`即可。该示例程序会执行数据读取、模型训练和保存、模型读取及新样本预测等步骤。
### 自定义数据和任务
前文提到本例中的模型可以应用到其他序列标注任务中,这里以词性标注任务为例,给出使用其他数据,并应用到其他任务的操作方法。
假定有如下格式的原始数据:
```
U.N. NNP
official NN
Ekeus NNP
heads VBZ
for IN
Baghdad NNP
. .
```
第一列为原始句子序列,第二列为词性标签序列,两列之间以“\t”分隔,句子之间以空行分隔。
为使用PaddlePaddle和本示例提供的模型,可参照`conll03.py`并根据需要自定义数据接口,如下:
1. 参照`conll03.py`中的`corpus_reader`函数,定义接口返回句子序列和标签序列生成器; ```text
commandline: --use_gpu=False --trainer_count=1
```python Initing parameters..
# 实现句子和对应标签的抽取,传入数据文件路径,返回句子和标签序列生成器。 Init parameters done.
def corpus_reader(filename): Pass 0, Batch 0, Cost 41.430110, {'ner_chunk.precision': 0.01587301678955555, 'ner_chunk.F1-score': 0.028368793427944183, 'ner_chunk.recall': 0.13333334028720856, 'error': 0.939393937587738}
def reader(): Test with Pass 0, Batch 0, {'ner_chunk.precision': 0.0, 'ner_chunk.F1-score': 0.0, 'ner_chunk.recall': 0.0, 'error': 0.16260161995887756}
sentence = []
labels = []
with open(filename) as f:
for line in f:
if len(line.strip()) == 0:
if len(sentence) > 0:
yield sentence, labels
sentence = []
labels = []
else:
segs = line.strip().split()
sentence.append(segs[0])
labels.append(segs[-1])
f.close()
return reader
``` ```
2. 参照`conll03.py`中的`reader_creator`函数,定义接口返回id化的句子和标签序列生成器。 ### 预测
1. 修改 [infer.py](./infer.py) 的 `main` 函数,指定:需要测试的模型的路径、测试数据、字典文件,预测标记文件的路径,默认参数如下:
```python ```python
# 传入corpus_reader返回的生成器、dict类型的word词典和label词典,返回id化的句子和标签序列生成器。 infer(
def reader_creator(corpus_reader, word_dict, label_dict): model_path="models/params_pass_0.tar.gz",
def reader(): batch_size=2,
for sentence, labels in corpus_reader(): test_data_file="data/test",
word_idx = [ vocab_file="data/vocab.txt",
word_dict.get(w, UNK_IDX) # 若使用小写单词,请使用w.lower() target_file="data/target.txt")
for w in sentence
]
# 若使用首字母大写标记,请去掉以下注释符号,并在yield语句的word_idx后加上mark
# mark = [
# 1 if w[0].isupper() else 0
# for w in sentence
# ]
label_idx = [label_dict.get(w) for w in labels]
yield word_idx, label_idx, sentence # 加上sentence方便预测时打印
return reader
``` ```
自定义了数据接口后,要使用本示例中的模型,只需在调用模型训练和预测接口`ner_net_train`和`ner_net_infer`时传入调用`reader_creator`返回的生成器即可。另外需要注意,这里给出的数据接口定义去掉了`conll03.py`一些预处理(使用原始句子,而非转换成小写单词加上大写标记),`ner.py`中的模型相关接口也需要进行一些调整: 2. 在终端运行 `python infer.py`,开始测试,会看到如下预测结果:
1. 修改网络结构定义接口`ner_net`中大写标记相关内容: ```text
cricket O
- O
leicestershire O
take O
over O
at O
top O
after O
innings O
victory O
. O
删去`mark`和`mark_embedding`两个变量; ```
输出分为两列,以“\t” 分隔,第一列是输入的词语,第二列是标记结果。多条输入序列之间以空行分隔。
2. 修改模型训练接口`ner_net_train`中大写标记相关内容:
将变量`feeding`定义改为`feeding = {'word': 0, 'target': 1}`;
3. 修改预测接口`ner_net_infer`中大写标记相关内容:
将`test_data.append([item[0], item[1]])`改为`test_data.append([item[0]])`。
如果要继续使用NER中的特征预处理(小写单词、大写标记),请参照上文`reader_creator`代码段给出的注释进行修改,此时`ner.py`中的模型相关接口不必进行修改。
## 参考文献 ## 参考文献
......
import gzip
import reader
from network_conf import *
from utils import *
def infer(model_path, batch_size, test_data_file, vocab_file, target_file):
def _infer_a_batch(inferer, test_data, id_2_word, id_2_label):
probs = inferer.infer(input=test_data, field=["id"])
assert len(probs) == sum(len(x[0]) for x in test_data)
for idx, test_sample in enumerate(test_data):
start_id = 0
for w, tag in zip(test_sample[0],
probs[start_id:start_id + len(test_sample[0])]):
print("%s\t%s" % (id_2_word[w], id_2_label[tag]))
print("\n")
start_id += len(test_sample[0])
word_dict = load_dict(vocab_file)
word_dict_len = len(word_dict)
word_reverse_dict = load_reverse_dict(vocab_file)
label_dict = load_dict(target_file)
label_reverse_dict = load_reverse_dict(target_file)
label_dict_len = len(label_dict)
# initialize PaddlePaddle
paddle.init(use_gpu=False, trainer_count=1)
parameters = paddle.parameters.Parameters.from_tar(
gzip.open(model_path, "r"))
predict = ner_net(
word_dict_len=word_dict_len,
label_dict_len=label_dict_len,
is_train=False)
inferer = paddle.inference.Inference(
output_layer=predict, parameters=parameters)
test_data = []
for i, item in enumerate(
reader.data_reader(test_data_file, word_dict, label_dict)()):
test_data.append([item[0], item[1]])
if len(test_data) == batch_size:
_infer_a_batch(inferer, test_data, word_reverse_dict,
label_reverse_dict)
test_data = []
_infer_a_batch(inferer, test_data, word_reverse_dict, label_reverse_dict)
test_data = []
if __name__ == "__main__":
infer(
model_path="models/params_pass_0.tar.gz",
batch_size=2,
test_data_file="data/test",
vocab_file="data/vocab.txt",
target_file="data/target.txt")
import math
import gzip
import paddle.v2 as paddle
import paddle.v2.evaluator as evaluator
import conll03
import itertools
# init dataset
train_data_file = 'data/train'
test_data_file = 'data/test'
vocab_file = 'data/vocab.txt'
target_file = 'data/target.txt'
emb_file = 'data/wordVectors.txt'
train_data_reader = conll03.train(train_data_file, vocab_file, target_file)
test_data_reader = conll03.test(test_data_file, vocab_file, target_file)
word_dict, label_dict = conll03.get_dict(vocab_file, target_file)
word_vector_values = conll03.get_embedding(emb_file)
# init hyper-params
word_dict_len = len(word_dict)
label_dict_len = len(label_dict)
mark_dict_len = 2
word_dim = 50
mark_dim = 5
hidden_dim = 300
mix_hidden_lr = 1e-3
default_std = 1 / math.sqrt(hidden_dim) / 3.0
emb_para = paddle.attr.Param(
name='emb', initial_std=math.sqrt(1. / word_dim), is_static=True)
std_0 = paddle.attr.Param(initial_std=0.)
std_default = paddle.attr.Param(initial_std=default_std)
def d_type(size):
return paddle.data_type.integer_value_sequence(size)
def ner_net(is_train):
word = paddle.layer.data(name='word', type=d_type(word_dict_len))
mark = paddle.layer.data(name='mark', type=d_type(mark_dict_len))
word_embedding = paddle.layer.mixed(
name='word_embedding',
size=word_dim,
input=paddle.layer.table_projection(input=word, param_attr=emb_para))
mark_embedding = paddle.layer.mixed(
name='mark_embedding',
size=mark_dim,
input=paddle.layer.table_projection(input=mark, param_attr=std_0))
emb_layers = [word_embedding, mark_embedding]
word_caps_vector = paddle.layer.concat(
name='word_caps_vector', input=emb_layers)
hidden_1 = paddle.layer.mixed(
name='hidden1',
size=hidden_dim,
act=paddle.activation.Tanh(),
bias_attr=std_default,
input=[
paddle.layer.full_matrix_projection(
input=word_caps_vector, param_attr=std_default)
])
rnn_para_attr = paddle.attr.Param(initial_std=0.0, learning_rate=0.1)
hidden_para_attr = paddle.attr.Param(
initial_std=default_std, learning_rate=mix_hidden_lr)
rnn_1_1 = paddle.layer.recurrent(
name='rnn1-1',
input=hidden_1,
act=paddle.activation.Relu(),
bias_attr=std_0,
param_attr=rnn_para_attr)
rnn_1_2 = paddle.layer.recurrent(
name='rnn1-2',
input=hidden_1,
act=paddle.activation.Relu(),
reverse=1,
bias_attr=std_0,
param_attr=rnn_para_attr)
hidden_2_1 = paddle.layer.mixed(
name='hidden2-1',
size=hidden_dim,
bias_attr=std_default,
act=paddle.activation.STanh(),
input=[
paddle.layer.full_matrix_projection(
input=hidden_1, param_attr=hidden_para_attr),
paddle.layer.full_matrix_projection(
input=rnn_1_1, param_attr=rnn_para_attr)
])
hidden_2_2 = paddle.layer.mixed(
name='hidden2-2',
size=hidden_dim,
bias_attr=std_default,
act=paddle.activation.STanh(),
input=[
paddle.layer.full_matrix_projection(
input=hidden_1, param_attr=hidden_para_attr),
paddle.layer.full_matrix_projection(
input=rnn_1_2, param_attr=rnn_para_attr)
])
rnn_2_1 = paddle.layer.recurrent(
name='rnn2-1',
input=hidden_2_1,
act=paddle.activation.Relu(),
reverse=1,
bias_attr=std_0,
param_attr=rnn_para_attr)
rnn_2_2 = paddle.layer.recurrent(
name='rnn2-2',
input=hidden_2_2,
act=paddle.activation.Relu(),
bias_attr=std_0,
param_attr=rnn_para_attr)
hidden_3 = paddle.layer.mixed(
name='hidden3',
size=hidden_dim,
bias_attr=std_default,
act=paddle.activation.STanh(),
input=[
paddle.layer.full_matrix_projection(
input=hidden_2_1, param_attr=hidden_para_attr),
paddle.layer.full_matrix_projection(
input=rnn_2_1,
param_attr=rnn_para_attr), paddle.layer.full_matrix_projection(
input=hidden_2_2, param_attr=hidden_para_attr),
paddle.layer.full_matrix_projection(
input=rnn_2_2, param_attr=rnn_para_attr)
])
output = paddle.layer.mixed(
name='output',
size=label_dict_len,
bias_attr=False,
input=[
paddle.layer.full_matrix_projection(
input=hidden_3, param_attr=std_default)
])
if is_train:
target = paddle.layer.data(name='target', type=d_type(label_dict_len))
crf_cost = paddle.layer.crf(
size=label_dict_len,
input=output,
label=target,
param_attr=paddle.attr.Param(
name='crfw',
initial_std=default_std,
learning_rate=mix_hidden_lr))
crf_dec = paddle.layer.crf_decoding(
size=label_dict_len,
input=output,
label=target,
param_attr=paddle.attr.Param(name='crfw'))
return crf_cost, crf_dec, target
else:
predict = paddle.layer.crf_decoding(
size=label_dict_len,
input=output,
param_attr=paddle.attr.Param(name='crfw'))
return predict
def ner_net_train(data_reader=train_data_reader, num_passes=1):
# define network topology
crf_cost, crf_dec, target = ner_net(is_train=True)
evaluator.sum(name='error', input=crf_dec)
evaluator.chunk(
name='ner_chunk',
input=crf_dec,
label=target,
chunk_scheme='IOB',
num_chunk_types=(label_dict_len - 1) / 2)
# create parameters
parameters = paddle.parameters.create(crf_cost)
parameters.set('emb', word_vector_values)
# create optimizer
optimizer = paddle.optimizer.Momentum(
momentum=0,
learning_rate=2e-4,
regularization=paddle.optimizer.L2Regularization(rate=8e-4),
gradient_clipping_threshold=25,
model_average=paddle.optimizer.ModelAverage(
average_window=0.5, max_average_window=10000), )
trainer = paddle.trainer.SGD(
cost=crf_cost,
parameters=parameters,
update_equation=optimizer,
extra_layers=crf_dec)
reader = paddle.batch(
paddle.reader.shuffle(data_reader, buf_size=8192), batch_size=64)
feeding = {'word': 0, 'mark': 1, 'target': 2}
def event_handler(event):
if isinstance(event, paddle.event.EndIteration):
if event.batch_id % 100 == 0:
print "Pass %d, Batch %d, Cost %f, %s" % (
event.pass_id, event.batch_id, event.cost, event.metrics)
if event.batch_id % 1000 == 0:
result = trainer.test(reader=reader, feeding=feeding)
print "\nTest with Pass %d, Batch %d, %s" % (
event.pass_id, event.batch_id, result.metrics)
if isinstance(event, paddle.event.EndPass):
# save parameters
with gzip.open('params_pass_%d.tar.gz' % event.pass_id, 'w') as f:
parameters.to_tar(f)
result = trainer.test(reader=reader, feeding=feeding)
print "\nTest with Pass %d, %s" % (event.pass_id, result.metrics)
trainer.train(
reader=reader,
event_handler=event_handler,
num_passes=num_passes,
feeding=feeding)
return parameters
def ner_net_infer(data_reader=test_data_reader, model_file='ner_model.tar.gz'):
test_data = []
test_sentences = []
for item in data_reader():
test_data.append([item[0], item[1]])
test_sentences.append(item[-1])
if len(test_data) == 10:
break
predict = ner_net(is_train=False)
lab_ids = paddle.infer(
output_layer=predict,
parameters=paddle.parameters.Parameters.from_tar(gzip.open(model_file)),
input=test_data,
field='id')
flat_data = [word for word in itertools.chain.from_iterable(test_sentences)]
labels_reverse = {}
for (k, v) in label_dict.items():
labels_reverse[v] = k
pre_lab = [labels_reverse[lab_id] for lab_id in lab_ids]
for word, label in zip(flat_data, pre_lab):
print word, label
if __name__ == '__main__':
paddle.init(use_gpu=False, trainer_count=1)
ner_net_train(data_reader=train_data_reader, num_passes=1)
ner_net_infer(
data_reader=test_data_reader, model_file='params_pass_0.tar.gz')
import math
import paddle.v2 as paddle
import paddle.v2.evaluator as evaluator
def ner_net(word_dict_len, label_dict_len, stack_num=2, is_train=True):
mark_dict_len = 2
word_dim = 50
mark_dim = 5
hidden_dim = 128
word = paddle.layer.data(
name='word',
type=paddle.data_type.integer_value_sequence(word_dict_len))
word_embedding = paddle.layer.embedding(
input=word,
size=word_dim,
param_attr=paddle.attr.Param(
name='emb', initial_std=math.sqrt(1. / word_dim), is_static=True))
mark = paddle.layer.data(
name='mark',
type=paddle.data_type.integer_value_sequence(mark_dict_len))
mark_embedding = paddle.layer.embedding(
input=mark,
size=mark_dim,
param_attr=paddle.attr.Param(initial_std=math.sqrt(1. / word_dim)))
word_caps_vector = paddle.layer.concat(
input=[word_embedding, mark_embedding])
mix_hidden_lr = 1e-3
rnn_para_attr = paddle.attr.Param(initial_std=0.0, learning_rate=0.1)
hidden_para_attr = paddle.attr.Param(
initial_std=1 / math.sqrt(hidden_dim), learning_rate=mix_hidden_lr)
# the first rnn layer shares the input-to-hidden mappings.
hidden = paddle.layer.fc(
name="__hidden00__",
size=hidden_dim,
act=paddle.activation.Tanh(),
bias_attr=paddle.attr.Param(initial_std=1.),
input=word_caps_vector,
param_attr=hidden_para_attr)
fea = []
for direction in ["fwd", "bwd"]:
for i in range(stack_num):
if i:
hidden = paddle.layer.fc(
name="__hidden%02d_%s__" % (i, direction),
size=hidden_dim,
act=paddle.activation.STanh(),
bias_attr=paddle.attr.Param(initial_std=1.),
input=[hidden, rnn],
param_attr=[hidden_para_attr, rnn_para_attr])
rnn = paddle.layer.recurrent(
name="__rnn%02d_%s__" % (i, direction),
input=hidden,
act=paddle.activation.Relu(),
bias_attr=paddle.attr.Param(initial_std=1.),
reverse=i % 2 if direction == "fwd" else not i % 2,
param_attr=rnn_para_attr)
fea += [hidden, rnn]
rnn_fea = paddle.layer.fc(
size=hidden_dim,
bias_attr=paddle.attr.Param(initial_std=1.),
act=paddle.activation.STanh(),
input=fea,
param_attr=[hidden_para_attr, rnn_para_attr] * 2)
emission = paddle.layer.fc(
size=label_dict_len,
bias_attr=False,
input=rnn_fea,
param_attr=rnn_para_attr)
if is_train:
target = paddle.layer.data(
name='target',
type=paddle.data_type.integer_value_sequence(label_dict_len))
crf = paddle.layer.crf(
size=label_dict_len,
input=emission,
label=target,
param_attr=paddle.attr.Param(name='crfw', initial_std=1e-3))
crf_dec = paddle.layer.crf_decoding(
size=label_dict_len,
input=emission,
label=target,
param_attr=paddle.attr.Param(name='crfw'))
return crf, crf_dec, target
else:
predict = paddle.layer.crf_decoding(
size=label_dict_len,
input=emission,
param_attr=paddle.attr.Param(name='crfw'))
return predict
...@@ -2,16 +2,9 @@ ...@@ -2,16 +2,9 @@
Conll03 dataset. Conll03 dataset.
""" """
import tarfile from utils import *
import gzip
import itertools
import collections
import re
import numpy as np
__all__ = ['train', 'test', 'get_dict', 'get_embedding'] __all__ = ["data_reader"]
UNK_IDX = 0
def canonicalize_digits(word): def canonicalize_digits(word):
...@@ -28,96 +21,46 @@ def canonicalize_word(word, wordset=None, digits=True): ...@@ -28,96 +21,46 @@ def canonicalize_word(word, wordset=None, digits=True):
if (wordset != None) and (word in wordset): return word if (wordset != None) and (word in wordset): return word
word = canonicalize_digits(word) # try to canonicalize numbers word = canonicalize_digits(word) # try to canonicalize numbers
if (wordset == None) or (word in wordset): return word if (wordset == None) or (word in wordset): return word
else: return "UUUNKKK" # unknown token else: return "<UNK>" # unknown token
def load_dict(filename):
d = dict()
with open(filename, 'r') as f:
for i, line in enumerate(f):
d[line.strip()] = i
return d
def data_reader(data_file, word_dict, label_dict):
def get_dict(vocab_file='data/vocab.txt', target_file='data/target.txt'):
"""
Get the word and label dictionary.
""" """
word_dict = load_dict(vocab_file) The dataset can be obtained according to http://www.clips.uantwerpen.be/conll2003/ner/.
label_dict = load_dict(target_file) It returns a reader creator, each sample in the reader includes:
return word_dict, label_dict word id sequence, label id sequence and raw sentence.
def get_embedding(emb_file='data/wordVectors.txt'): :return: reader creator
""" :rtype: callable
Get the trained word vector.
""" """
return np.loadtxt(emb_file, dtype=float)
def corpus_reader(filename='data/train'):
def reader(): def reader():
UNK_IDX = word_dict["<UNK>"]
sentence = [] sentence = []
labels = [] labels = []
with open(filename) as f: with open(data_file, "r") as f:
for line in f: for line in f:
if re.match(r"-DOCSTART-.+", line) or (len(line.strip()) == 0): if len(line.strip()) == 0:
if len(sentence) > 0: if len(sentence) > 0:
yield sentence, labels word_idx = [
word_dict.get(
canonicalize_word(w, word_dict), UNK_IDX)
for w in sentence
]
mark = [1 if w[0].isupper() else 0 for w in sentence]
label_idx = [label_dict[l] for l in labels]
yield word_idx, mark, label_idx
sentence = [] sentence = []
labels = [] labels = []
else: else:
segs = line.strip().split() segs = line.strip().split()
sentence.append(segs[0]) sentence.append(segs[0])
# transform from I-TYPE to BIO schema # transform I-TYPE to BIO schema
if segs[-1] != 'O' and (len(labels) == 0 or if segs[-1] != "O" and (len(labels) == 0 or
labels[-1][1:] != segs[-1][1:]): labels[-1][1:] != segs[-1][1:]):
labels.append('B' + segs[-1][1:]) labels.append("B" + segs[-1][1:])
else: else:
labels.append(segs[-1]) labels.append(segs[-1])
f.close()
return reader return reader
def reader_creator(corpus_reader, word_dict, label_dict):
"""
Conll03 train set creator.
The dataset can be obtained according to http://www.clips.uantwerpen.be/conll2003/ner/.
It returns a reader creator, each sample in the reader includes word id sequence, label id sequence and raw sentence for purpose of print.
:return: Training reader creator
:rtype: callable
"""
def reader():
for sentence, labels in corpus_reader():
word_idx = [
word_dict.get(canonicalize_word(w, word_dict), UNK_IDX)
for w in sentence
]
mark = [1 if w[0].isupper() else 0 for w in sentence]
label_idx = [label_dict.get(w) for w in labels]
yield word_idx, mark, label_idx, sentence
return reader
def train(data_file='data/train',
vocab_file='data/vocab.txt',
target_file='data/target.txt'):
return reader_creator(
corpus_reader(data_file),
word_dict=load_dict(vocab_file),
label_dict=load_dict(target_file))
def test(data_file='data/test',
vocab_file='data/vocab.txt',
target_file='data/target.txt'):
return reader_creator(
corpus_reader(data_file),
word_dict=load_dict(vocab_file),
label_dict=load_dict(target_file))
import gzip
import numpy as np
import reader
from utils import *
from network_conf import *
def main(train_data_file,
test_data_file,
vocab_file,
target_file,
emb_file,
num_passes=10,
batch_size=32):
word_dict = load_dict(vocab_file)
label_dict = load_dict(target_file)
word_vector_values = get_embedding(emb_file)
word_dict_len = len(word_dict)
label_dict_len = len(label_dict)
paddle.init(use_gpu=False, trainer_count=1)
# define network topology
crf_cost, crf_dec, target = ner_net(word_dict_len, label_dict_len)
evaluator.sum(name="error", input=crf_dec)
evaluator.chunk(
name="ner_chunk",
input=crf_dec,
label=target,
chunk_scheme="IOB",
num_chunk_types=(label_dict_len - 1) / 2)
# create parameters
parameters = paddle.parameters.create(crf_cost)
parameters.set("emb", word_vector_values)
# create optimizer
optimizer = paddle.optimizer.Momentum(
momentum=0,
learning_rate=2e-4,
regularization=paddle.optimizer.L2Regularization(rate=8e-4),
gradient_clipping_threshold=25,
model_average=paddle.optimizer.ModelAverage(
average_window=0.5, max_average_window=10000), )
trainer = paddle.trainer.SGD(
cost=crf_cost,
parameters=parameters,
update_equation=optimizer,
extra_layers=crf_dec)
train_reader = paddle.batch(
paddle.reader.shuffle(
reader.data_reader(train_data_file, word_dict, label_dict),
buf_size=1000),
batch_size=batch_size)
test_reader = paddle.batch(
paddle.reader.shuffle(
reader.data_reader(test_data_file, word_dict, label_dict),
buf_size=1000),
batch_size=batch_size)
feeding = {"word": 0, "mark": 1, "target": 2}
def event_handler(event):
if isinstance(event, paddle.event.EndIteration):
if event.batch_id % 1 == 0:
logger.info("Pass %d, Batch %d, Cost %f, %s" % (
event.pass_id, event.batch_id, event.cost, event.metrics))
if event.batch_id % 1 == 0:
result = trainer.test(reader=test_reader, feeding=feeding)
logger.info("\nTest with Pass %d, Batch %d, %s" %
(event.pass_id, event.batch_id, result.metrics))
if isinstance(event, paddle.event.EndPass):
# save parameters
with gzip.open("models/params_pass_%d.tar.gz" % event.pass_id,
"w") as f:
parameters.to_tar(f)
result = trainer.test(reader=test_reader, feeding=feeding)
logger.info("\nTest with Pass %d, %s" % (event.pass_id,
result.metrics))
trainer.train(
reader=train_reader,
event_handler=event_handler,
num_passes=num_passes,
feeding=feeding)
if __name__ == "__main__":
main(
train_data_file='data/train',
test_data_file='data/test',
vocab_file='data/vocab.txt',
target_file='data/target.txt',
emb_file='data/wordVectors.txt')
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import os
import re
import argparse
import numpy as np
from collections import defaultdict
logger = logging.getLogger("logger")
logger.setLevel(logging.INFO)
def get_embedding(emb_file='data/wordVectors.txt'):
"""
Get the trained word vector.
"""
return np.loadtxt(emb_file, dtype=float)
def load_dict(dict_path):
"""
Load the word dictionary from the given file.
Each line of the given file is a word, which can include multiple columns
seperated by tab.
This function takes the first column (columns in a line are seperated by
tab) as key and takes line number of a line as the key (index of the word
in the dictionary).
"""
return dict((line.strip().split("\t")[0], idx)
for idx, line in enumerate(open(dict_path, "r").readlines()))
def load_reverse_dict(dict_path):
"""
Load the word dictionary from the given file.
Each line of the given file is a word, which can include multiple columns
seperated by tab.
This function takes line number of a line as the key (index of the word in
the dictionary) and the first column (columns in a line are seperated by
tab) as the value.
"""
return dict((idx, line.strip().split("\t")[0])
for idx, line in enumerate(open(dict_path, "r").readlines()))
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册