AllenNLP 完全通过配置文件来对数据处理、模型结果和训练过程进行设置,最简单的情况下可以一行代码不写就把一个文本分类模型训练出来。下面是一个配置文件示例:
{
"dataset_reader": {
"type": "text_classification_json",
"tokenizer": {
"type": "word",
"word_splitter": {
"type": "jieba",
}
}
},
"train_data_path": "allen.data.train",
"test_data_path": "allen.data.test",
"evaluate_on_test": true,
"model": {
"type": "basic_classifier",
"text_field_embedder": {
"tokens": {
"type": "embedding",
"embedding_dim": 100,
"trainable": true
}
},
"seq2vec_encoder": {
"type": "cnn",
"embedding_dim": 100,
"num_filters": 1,
"ngram_filter_sizes": [2, 3, 4]
}
},
"iterator": {
"type": "bucket",
"sorting_keys": [["tokens", "num_tokens"]],
"batch_size": 64
},
"trainer": {
"num_epochs": 40,
"patience": 3,
"cuda_device": -1,
"grad_clipping": 5.0,
"validation_metric": "+accuracy",
"optimizer": {
"type": "adam"
}
}
}
配置文件中的内容可以分成
数据部分: 包括 dataset_reader/train_data_path/test_data_path 这几个 key 及其 value
模型部分: 就是 model 这个 key 的内容
训练部分: 包括 evaluate_on_test/iterator/trainer 这几个 key 及其 value
由于本文不是专门介绍 AllenNLP 的文章,所以只对这些配置做简要说明,详细内容可查看文档。
数据部分
train_data_path 和 test_data_path 比较好理解,它们指定了训练数据和测试数据的文件路径;而 data_reader 则限定了数据文件的格式。
data_reader 中的配置,会被用来构建一个 DatasetReader 的子类的对象,用来读取数据并转换成一个个 Instance 对象。
内置的可用来读取分类数据的 DataReader 是 TextClassificationJsonReader ,所以配置中有
"type": "text_classification_json"
这个 type 的值是 TextClassificationJsonReader 这个类实现的时候注册上的,去看代码会看到有这样的片段
@DatasetReader.register("text_classification_json")
class TextClassificationJsonReader(DatasetReader):
这个 TextClassificationJsonReader 要求的数据文件是一行一个 json 数据,如下:
{"label": "education", "text": "名师指导托福语法技巧:名词的复数形式"}
{"label": "education", "text": "中国高考成绩海外认可是“狼来了”吗?"}
{"label": "sports, "text": "图文:法网孟菲尔斯苦战进16强孟菲尔斯怒吼"}
{"label": "sports, "text": "四川丹棱举行全国长距登山挑战赛近万人参与"}
DataReader 通过配置中 tokenizer 部分会创建一个分词器,用来将文本转换为词序列
"tokenizer": {
"type": "word",
"word_splitter": {
"type": "jieba",
}
}
type 的值设置为 word,这没什么好说的。
tokenizer 中的 word_splitter 指定的才是真正的分词器(比较绕)。
如果是英文的数据,那么 word_splitter 的配置可以不写,默认就是支持英文分词的。
但如果是用于中文处理的话,有一个 SpacyWordSplitter 可以用于中文分类,但是现有的中文 spaCy 模型仅支持 spaCy 2.0.x,和 AllenNLP 中 spaCy 要求的版本不兼容,这个是比较坑的。
好在 AllenNLP 提供了加载自定义模块的方法,按照如下方法来处理这个问题
mkdir allen_ext/
touch allen_ext/__init__.py
touch allen_ext/word_splitter.py
然后在 allen_ext/word_splitter.py 中写入如下内容
from typing import List
import jieba
from overrides import overrides
from allennlp.data.tokenizers.token import Token
from allennlp.data.tokenizers.word_splitter import WordSplitter
@WordSplitter.register('jieba')
class JiebaWordSplitter(WordSplitter):
def __init__(self):
pass
@overrides
def split_words(self, sentence: str) -> List[Token]:
offset = 0
tokens = []
for word in jieba.lcut(sentence):
word = word.strip()
if not word:
continue
start = sentence.find(word, offset)
tokens.append(Token(word, start))
offset = start + len(word)
return tokens
使用 WordSplitter.register('jieba') 后就可以在配置中 word_splitter 部分写上 "type": "jieba" 来启用。
在 allen_ext/__init__.py 中写入如下内容
from .word_splitter import JiebaWordSplitter
__all__ = ['JiebaWordSplitter']
自定义了 JiebaWordSplitter 后在训练的时候还要加载 allen_ext 这个目录才能生效,这个之后再说。
模型部分
因为是做文本分类,所以 type 设置为 basic_classifier。
这个分类器需要 text_field_embedder 和 seq2vec_encoder 两个参数:
text_field_embedder 用来定义 word embedding,这个配置应该还好理解
"text_field_embedder": {
"tokens": {
"type": "embedding",
"embedding_dim": 100,
"trainable": true
}
}
seq2vec_encoder 则用来产生句子的编码向量用于分类,这里选择了 CNN
"seq2vec_encoder": {
"type": "cnn",
"embedding_dim": 100,
"num_filters": 1,
"ngram_filter_sizes": [2, 3, 4]
}
训练部分:略
配置文件写好后,假设配置文件为 config.json,直接执行下面的命令来训练即可
allennlp train config.json -s model_save_dir --include-package allen_ext
选项 --include-package allen_ext 用来来加载自定义的模块。
最终会在 save_dir 目录下产生一个 model.tar.gz 文件,就是模型参数,然后目录下还会产生 tensorboard 能读取的 log,这个挺方便的。
评估的话,用 evaluate 命令
allennlp evaluate model_save_dir/model.tar.gz test.jsonl --include-package allen_ext
比较麻烦的是,预测需要一个 Predictor,而 AllenNLP 中内置的 TextClassifierPredictor 要求的输入是 {"sentence": "xxx"} ,这个和 TextClassificationJsonReader 的要求不一样……
如果是在代码里进行预测,那么是没有问题的,可以这样
from allen_ext import * #noqa
from allennlp.models.archival import load_archive
from allennlp.predictors.predictor import Predictor
archive = load_archive('model_save_dir/model.tar.gz')
predictor = Predictor.from_archive(archive)
inputs = {"sentence": "名师指导托福语法技巧:名词的复数形式"}
result = predictor.predict_json(inputs)
得到的 result 是这样的结构
{
'label': 'education',
'logits': [
15.88630199432373,
0.7209644317626953,
7.292031764984131,
5.195938587188721,
5.073373317718506,
-35.6490478515625,
-7.7982988357543945,
-35.44648742675781,
-18.14293098449707,
-14.513381004333496
],
'probs': [
0.999771773815155,
2.592259420453047e-07,
0.0001851213601185009,
2.2758060367777944e-05,
2.013285666180309e-05,
4.153195524896307e-23,
5.1737975015342386e-11,
5.085729773519049e-23,
1.6641527142180782e-15,
6.273159211056881e-14
],
}
这个输出结构完全是由 TextClassifierPredictor 决定的。
如果要自定义 Predictor,可以参考文档。