本文基于AllenNLP中文教程
有一篇帖子总结了一下学习处理NLP问题中间的坑。NLP数据预处理要比CV的麻烦很多。
- 去除停用词,建立词典,加载各种预训练词向量,Sentence -> Word ID -> Word Embedding的过程(Tobias Lee:文本预处理方法小记),其中不仅需要学习pytorch,可能还要学习spacy,NLTK,numpy,pandas,tensorboardX等常用python包。
- 用到RNN时,还要经过pad,pack,pad的过程,像这样的很多函数在使用时需要有数学基础加上简单的实践,感觉对一个新人来说,高维数据的流动有点抽象,不容易理解。
- 数据集的读取,tensorboardX的使用。。。。各种东西要学习。在运行别人的代码后打印出信息,不仅看着上档次,而且可以看到很多实用的信息。。。
AllenNLP是在pytorch基础上的封装,它的目标是处理NLP任务,可以减少很多额外的学习。
- 分词,帮你用spacy,NLTK,或者简单的按空格分词处理。
- 数据集的读取,它内置了很多数据集的读取,你可以在通过学习它的读取方式,在它的基础上对自己需要的数据集进行读取。 、
- 在Sentence -> Word ID -> Word Embedding的过程中,Glove,ELMo,BERT等常用的都可以直接使用,需要word,char粒度的都可以。
- log打印输出,在内置的输出项之外,你可以很方便地加入想要输出的信息。模型的各个组件中的参数都可以存在一个json/jsonnet文件中,修改参数进行实验很方便。
3. 完整实例,预测论文发表场合
第一部分 数据集和模型
第四步 构建Model
完成模型构建之后,就可以进行测试了,在这里先定义测试文件。
注意源文件中,测试文件被单独放在一个文件夹中,有之前DatasetReader以及模型、预测的测试文件。看了一下确实原始数据和我下载的不一样,在这里以教程的为准。
from allennlp.common.testing import ModelTestCase
class AcademicPaperClassifierTest(ModelTestCase):
def setUp(self):
super(AcademicPaperClassifierTest,self).setUp()
self.set_up_model(
'tests/fixtures/academic_paper_classifier.json',
'tests/fixtures/s2_papers.jsonl'
)
def test_model_can_train_save_and_load(self):
self.ensure_model_can_train_save_and_load(self.param_file)
这个测试文件使用了allennlp.common.testing.ModelTestCase类,测试的是模型能够训练、保存、恢复、预测。为了能够很好的使用这些测试,我们还需要定义一个测试配置文件;构造一个更小的输入文件。
- tests/fixtures/academic_paper_classifier.json
- tests/fixtures/s2_papers.jsonl
这个方法不错,之前测试都是直接加载数据运行。
接下来就是模型了,注意这两个输入都转换成序号。那么我们下一步自然就是需要把序号转换成对应的embeddings。
- inputs:title 和 abstract
- output:label
模型的结构是由AllenNLP封装好的。
模型构造函数
from typing import Dict, Optional
import numpy
from overrides import overrides
import torch
import torch.nn.functional as F
from allennlp.common.checks import ConfigurationError
from allennlp.data import Vocabulary
from allennlp.modules import FeedForward, Seq2VecEncoder, TextFieldEmbedder
from allennlp.models.model import Model
from allennlp.nn import InitializerApplicator, RegularizerApplicator
from allennlp.nn import util
from allennlp.training.metrics import CategoricalAccuracy
@Model.register("paper_classifier")
class AcademicPaperClassifier(Model):
'''
这个``Model``为学术论文执行文本分类。我们假设我们有一个标题和一个摘要,我们预测一些输出标签。
基本模型结构:我们将嵌入标题和摘要,并使用单独的Seq2VecEncoders对它们进行编码,获得表示每个内容的单个向量。然后我们将这两个向量连接起来,并通过前馈网络传递结果,我们将使用它作为每个标签的分数。
'''
def __init__(self, vocab: Vocabulary,
text_field_embedder: TextFieldEmbedder,
title_encoder: Seq2VecEncoder,
abstract_encoder: Seq2VecEncoder,
calssifier_feedforward: FeedForward,
initializer: InitializerApplicator = InitializerApplicator(),
regularizer: Optional[RegularizerApplicator] = None
) -> None:
super(AcademicPaperClassifier,self).__init__(vocab,regularizer)
self.text_field_embeeder=text_field_embedder
self.abstract_encoder=abstract_encoder
self.title_encoder=title_encoder
self.abstract_encoder=abstract_encoder
self.calssifier_feedforward=calssifier_feedforward
self.metrics = {
"accuracy": CategoricalAccuracy(),
"accuracy3": CategoricalAccuracy(top_k=3)
}
self.loss = torch.nn.CrossEntropyLoss()
initializer(self)
模型前馈神经网络
类似DatasetReader注册模型,方便配置文件的查找。注意,这里出现了一个奇怪的参数Vocabulary,这个参数顾名思义就是我们的数据字典,但是我们在哪里构造的呢?答案是不用构造!写model的时候顺手写上去就行啦,这个是Allennlp帮助我们写好的。
同时这个数据字典其实是个复合字典,包括所有TextField的字典,以及LabelField自己单独的字典。然后需要介绍的参数就是TextFieldEmbedder为所有的TextField类共同建立了一个embeddings。
利用这个embeddings以及我们输入的序号,我们就能够获得一个向量组成的序列。下一步就是对这个序列进行变化。在这里我们使用的是Seq2VecEncoder。这个Encoder可以有很多的变化,在这里我们使用的是最最简单的一种,就是bag of embeddings,直接求平均。当然啦,我们也可以使用什么CNN啦,RNN,Transformer模型。
前馈神经网络呢也是一个预先定义好的Module,我们可以修改这个网络的深度宽度激活函数。InitializerApplicator包含着所有参数的基本初始化方法。如果你想自定义初始化,就需要时候用RegularizerApplicator
def forward(self,
title: Dict[str, torch.LongTensor],
abstract:Dict[str,torch.LongTensor],
label:torch.LongTensor=None
)-> Dict[str, torch.Tensor]:
embedded_title=self.text_field_embeeder(title)
title_mask = util.get_text_field_mask(title)
encoded_title = self.title_encoder(embedded_title, title_mask)
embedded_abstract = self.text_field_embedder(abstract)
abstract_mask = util.get_text_field_mask(abstract)
encoded_abstract = self.abstract_encoder(embedded_abstract, abstract_mask)
logits = self.classifier_feedforward(torch.cat([encoded_title, encoded_abstract], dim=-1))
class_probabilities = F.softmax(logits)
output_dict = {"class_probabilities": class_probabilities}
if label is not None:
loss = self.loss(logits, label.squeeze(-1))
for metric in self.metrics.values():
metric(logits, label.squeeze(-1))
output_dict["loss"] = loss
return output_dict
我们首先注意到的应该是这个函数的参数。在这里,参数的名字一定要和DatasetReader中定义的名字保持一致。AllenNLP在这里将会自动的利用你的DatasetReader并且把数据组织成batches的形式。注意,forward函数接收的参数正是一个batch的数据。
注意,把labels也传递给forward函数用于计算损失函数。在训练的时候,我们的模型会主动的去寻找这个loss,然后自动的反向传播回去,然后更改参数。同时我们也应该注意到,这个参数是可以为空的,这主要是为了应对prediction的情况。这个将会在后面章节中进行介绍。
输入的类型。label是一个[batch_size,1]大小的tensor。title和abstract两个是TextField类型的,这些TextField转换为字典类型的。这个新的字典呢可能包括了单词id,字母array或者pos标签ID什么的。embedder直接一股脑的扔进去就能够帮你完成转换过程。这就意味着我们TextFieldEmbedder必须和TextField完全对应。对接的过程又是在配置文件中完成的。
模型的decode和metric
现在我们已经理解了模型的基本输入,来看看它的基本逻辑。
- 找到title和abstract的embeddings,然后对这些向量进行操作。注意我们需要利用一个叫masks的变量来标识哪些元素仅仅是用来标识边界的,而不需要模型考虑。
- 我们对这些向量进行了一通操作之后,生成了一个向量。将这个向量输入一个前馈神经网络中就可以得到logits(预测为各个类的概率),有了这个概率我们就可以得到最终预测的结果。
- 如果是训练过程的话,我们还需要计算损失和评价标准。
decode函数包括两个功能
- 接收forward函数的返回值,并且对这个返回值进行操作,比如说算出具体是那个词啊等等。
- 将数字变成字符,方便阅读。好啦,至此我们的模型已经构建好啦,现在我们可以测试啦。
@overrides
def decode(self, output_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
class_probabilities = F.softmax(output_dict['logits'], dim=-1)
output_dict['class_probabilities'] = class_probabilities
predictions = class_probabilities.cpu().data.numpy()
argmax_indices = numpy.argmax(predictions, axis=-1)
labels = [self.vocab.get_token_from_index(x, namespace="labels")
for x in argmax_indices]
output_dict['label'] = labels
return output_dict
@overrides
def get_metrics(self, reset: bool = False) -> Dict[str, float]:
return {metric_name: metric.get_metric(reset) for metric_name, metric in self.metrics.items()}
训练模型
在这里就是使用JSON完成一个配置文件。
在这里调整了训练迭代数,实际上这个实验我没跑完,自己手动写的老有问题,我怀疑是文件路径出错了,就把源文档的test下载下来,跑通了。
看样子以后要保存一个文件结构。
{
"dataset_reader": {
"type": "s2_papers"
},
"train_data_path": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/academic-papers-example/train.jsonl",
"validation_data_path": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/academic-papers-example/dev.jsonl",
"model": {
"type": "paper_classifier",
"text_field_embedder": {
"token_embedders": {
"tokens": {
"type": "embedding",
"pretrained_file": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/glove/glove.6B.100d.txt.gz",
"embedding_dim": 100,
"trainable": false
}
}
},
"title_encoder": {
"type": "lstm",
"bidirectional": true,
"input_size": 100,
"hidden_size": 100,
"num_layers": 1,
"dropout": 0.2
},
"abstract_encoder": {
"type": "lstm",
"bidirectional": true,
"input_size": 100,
"hidden_size": 100,
"num_layers": 1,
"dropout": 0.2
},
"classifier_feedforward": {
"input_dim": 400,
"num_layers": 2,
"hidden_dims": [200, 3],
"activations": ["relu", "linear"],
"dropout": [0.2, 0.0]
}
},
"iterator": {
"type": "bucket",
"sorting_keys": [["abstract", "num_tokens"], ["title", "num_tokens"]],
"batch_size": 64
},
"trainer": {
"num_epochs": 10,
"patience": 2,
"cuda_device": -1,
"grad_clipping": 5.0,
"validation_metric": "+accuracy",
"optimizer": {
"type": "adagrad"
}
}
}
以下为训练结果
2019-03-10 17:43:05,747 - INFO - allennlp.common.util - Metrics: {
"best_epoch": 7,
"peak_cpu_memory_MB": 0,
"training_duration": "00:24:52",
"training_start_epoch": 0,
"training_epochs": 8,
"epoch": 8,
"training_accuracy": 0.9099333333333334,
"training_accuracy3": 1.0,
"training_loss": 0.24596523192334682,
"training_cpu_memory_MB": 0.0,
"validation_accuracy": 0.8095,
"validation_accuracy3": 1.0,
"validation_loss": 0.5315047986805439,
"best_validation_accuracy": 0.814,
"best_validation_accuracy3": 1.0,
"best_validation_loss": 0.5119817899540067
}