插播一条关于Allennlp 的库的基本用法的介绍
1 安装
2 基本使用方法
3 实例教学
本来呢是亦步亦趋的跟着官网的教程走的,应该先看安装,然后教怎么使用,怎么实现自己的代码。但是看着看着看不懂啦!往后一翻,才发现这玩意才是最该先看的教程。在这里就是简单的给它翻译成中文吧。
正如前文中所提到的,Allennlp的基本使用流程是需要自定义两个文件:datareader
;model
;
同时,如果存在已经定义好的datareader
我们也是可以直接拿来就用。接下来要介绍的这个例子呢,主要起到了两个作用:①熟悉allennlp的基本框架和逻辑②学习如何自定义自己的模型。模型全部都定义完成了之后,需要写一个json文件用来完成对模型的基本配置。
在这篇博客完成之后,我们将会能够构建一个对学术论文进行分类的模型,并且会使用该模型在其他的数据集上进行测试。目的是通过这篇博客让你对如何修改代码,如何训练模型,如何进行预测等等基本操作过程有一个大致的了解。在最后我们会贴出来源代码。
3.1 安装AllenNLP
为了完成我们宏伟的目标,我们需要做的第一件事就是在我们的python
依赖库中增加allennlp
.做法很简单,我们只需要写一个requirements.txt
文件,文件中只需要包含一行代码allennlp==0.5.1
,然后在python3.6
的环境中执行这个命令pip install -r requirements.txt
。提醒一下,这个命令是安装gpu版本的allennlp,如果跟我一样不仅是码农,还是个贫下中农,那就乖乖的先把pytorch的cpu版本装好了,再安装allennlp
3.2 构建自己的代码库
正如前面所提到的,要想自己利用AllenNLP
实现一个模型,那么就需要写两部分的代码:DatasetReader/Model。所以呢,我们的下一步就是要把我们自己写的代码放在python能够找到的位置,这样我们的AllenNLP
才能够调用这些模块呀。假设我们自己的代码库基础包名叫my_library
,当然啦,你想起啥名字都行,只要这个库符合两个条件:①位于python
的查找包的路径上②这个库里包含你写的models
以及dataset_readers
3.3 编写你自己的DatasetReader
现在我们自己的代码库已经建立好啦,要开始写代码啦!当然啦,我们首先需要有一些数据。在这个教程中,我们想要预测某一篇学术论文的发表场合。具体一点说,就是给出一篇论文的标题和摘要,我们想要判断它到底是在”自然语言处理领域“,“机器学习领域”还是”人工智能领域”的论文。
我们使用的数据是从Semantic Scholar
搜索引擎上下载的,每篇文章都标注好了所属的领域。下载下来的数据文件是JSON格式的,每个文件都应该至少包含标题(title
),摘要(paperAbstract
),以及所属领域(venue
)这样三部分内容。
{
"title": "A review of Web searching studies and a framework for future research",
"paperAbstract": "Research on Web searching is at an incipient stage. ...",
"venue": "{AI|ML|ACL}"
}
在这一部分呢,我们除了会完成文件读取的代码的编写,还会对这些代码进行测试,这是一个非常好的习惯!你们也要学会哦。我们测试的时候,会从数据集中抽出来10篇文章构成一个简单的代码测试数据集,用来测试我们代码的基本功能是不是都实现啦。
当然啦,我们的测试代码是要采用AllenNLP
中提供的测试接口滴。这些接口能够很好的帮我们做好测试的准备和收尸的工作。
我们首先想一想我们需要测试的只是DatasetReader
中的read
函数,下面给出测试代码的写法
from allennlp.common.testing import AllenNlpTestCase
from my_library.dataset_readers import SemanticScholarDatasetReader
class TestSemanticScholarDatasetReader(AllenNlpTestCase):
def test_read_from_file(self):
reader = SemanticScholarDatasetReader()
dataset = reader.read('tests/fixtures/s2_papers.jsonl')
然后呢,我们就需要验证一下我们的函数输出的结果是不是符合我们的要求,我们的数据应该是这样的:
instance1 = {"title": ["Interferring", "Discourse", "Relations", "in", "Context"],
"abstract": ["We", "investigate", "various", "contextual", "effects"],
"venue": "ACL"}
instance2 = {"title": ["GRASPER", ":", "A", "Permissive", "Planning", "Robot"],
"abstract": ["Execut", "ion", "of", "classical", "plans"],
"venue": "AI"}
instance3 = {"title": ["Route", "Planning", "under", "Uncertainty", ":", "The", "Canadian",
"Traveller", "Problem"],
"abstract": ["The", "Canadian", "Traveller", "problem", "is"],
"venue": "AI"}
基于这个数据集,我们的返回的东西呢,应该是要把里面的英文都给转换成token
也就是序号才对,所以呢返回的结果应该具有如下的性质。
assert len(dataset.instances) == 10
fields = dataset.instances[0].fields
assert [t.text for t in fields["title"].tokens] == instance1["title"]
assert [t.text for t in fields["abstract"].tokens[:5]] == instance1["abstract"]
assert fields["label"].label == instance1["venue"]
fields = dataset.instances[1].fields
assert [t.text for t in fields["title"].tokens] == instance2["title"]
assert [t.text for t in fields["abstract"].tokens[:5]] == instance2["abstract"]
assert fields["label"].label == instance2["venue"]
fields = dataset.instances[2].fields
assert [t.text for t in fields["title"].tokens] == instance3["title"]
assert [t.text for t in fields["abstract"].tokens[:5]] == instance3["abstract"]
assert fields["label"].label == instance3["venue"]
测试文件写好啦,我们现在要开始正式的写DatasetReader
方法啦。首先,我们需要写重写一下_read
函数,这个函数从文本文件中获取数据,然后将数据转换成tokens
表示的instance
。
def _read(self, file_path):
with open(cached_path(file_path), "r") as data_file:
logger.info("Reading instances from lines in file at: %s", file_path)
for line_num, line in enumerate(Tqdm.tqdm(data_file.readlines())):
line = line.strip("\n")
if not line:
continue
paper_json = json.loads(line)
title = paper_json['title']
abstract = paper_json['paperAbstract']
venue = paper_json['venue']
yield self.text_to_instance(title, abstract, venue)
上面这就是_read
函数的内容了,注意到我们在这里使用open
函数来打开需要读取的文件。注意,open
函数是把我们提供的这个路径当成网络资源(url)来访问并且读取的,这一点在后面需要下载的数据文件中起到了至关重要的作用。
在打开了文件之后,_read
函数的基本逻辑呢,是遍历文件中每一行数据,然后把这一行数据转换成JSON
格式,从而提取出所需要的字段,并且利用text_to_instance
将这些字段转换成instance
类型的数据。注意,这个函数的返回值是一个instance而不是整个文本的instances,换句话说,这里只是教给程序如何去处理这些输入,具体的处理过程是透明的。还有一点需要提一下,这里用了一个很有趣的函数Tqdm
,这个函数是什么?是进度条!每次调用一下就会打印一个进度条,pretty neat
哈。下面给出text_to_instance
的基本实现:
def text_to_instance(self, title: str, abstract: str, venue: str = None) -> Instance:
tokenized_title = self._tokenizer.tokenize(title)
tokenized_abstract = self._tokenizer.tokenize(abstract)
title_field = TextField(tokenized_title, self._token_indexers)
abstract_field = TextField(tokenized_abstract, self._token_indexers)
fields = {'title': title_field, 'abstract': abstract_field}
if venue is not None:
fields['label'] = LabelField(venue)
return Instance(fields)
首先,需要记住一点:instance
类型实际上就是多个字段的集合。这些字段将会教给AllenNLP
进行处理,并且传递给你的model
。这里用到了TextField
表示转化成了序号之后(tokenized
)的文本数据;LabelField
表示类别标签;此外还有很多类型的字段我们在这里暂时用不到就先不介绍了。需要额外说一句,我们在这里能够看出,AllenNLP
在幕后做了很多工作,但是记住这些幕后工作也是可以调节的。
其次,我们可以看到我们使用类成员变量(self._tokenizer
等)来构造我们的TextField
字段。我们需要简单介绍一下这里用到的两个成员变量:self._tokenizer
/self._token_indexers
。Tokenizer
呢是把你的文本转换成单词啦,字母啦,比特对啦等等常见的你想要的形式。TokenIndexer
呢则是给这些形式编个号,并且把最终的文本转换成序号表示的形式。举个例子来说,如果你的token
是单词,那么我们强大的TokenIndexer
可以自动的为你生成单词编号,字母编号,pos_tags
的编号。你有可能意识到了一个问题,就是我们明明没有定义过这些成员变量,这些成员变量是从哪里来的呢!答:这些成员变量都是在init
函数中定义的。下面我们就来看看这个函数。
@DatasetReader.register("s2_papers")
class SemanticScholarDatasetReader(DatasetReader):
def __init__(self,
tokenizer: Tokenizer = None,
token_indexers: Dict[str, TokenIndexer] = None) -> None:
self._tokenizer = tokenizer or WordTokenizer()
self._token_indexers = token_indexers or {"tokens": SingleIdTokenIndexer()}
看一看,瞧一瞧哈,这个构造函数从参数中获取了Tokenizer
以及TokenIndexer
,这两个参数还都有默认值。我们只需要在调用这个构造函数的时候传递参数进去就行啦。但是注意!我们是不会自己去调用这个构造函数的,我们调用这个reader和model全都在config中完成,也就意味着我们所有的参数都将会配置文件中完成。当然啦,为了能够在配置文件中找到我们定义的这个model
以及dataset_reader
,我们需要给这两个类注册一个名字@DatasetReader.register("s2_papers")
,像这样。有了这个注册,我们就能够在配置文件中使用这个类。AllenNLP
为所有的注册的类都实现了一个from_params
的方法,这个方法能够非常好根据配置文件中提供的信息,对应的调用构造函数,为我们构造DatasetReader
以及model
实例。
就是这么简单!就是这个感觉!
3.4 编写Model 代码
我们现在有了可以处理数据的代码,只需要一个可以用的模型就可以跑啦。这也就是我们这一小节需要实现的东西。在这里,我们还是传统思路,先把测试文件定义好。
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
类。我们来思考一下都需要测什么呢?对于任何模型,我们都希望这个模型能够训练,能够保存,能够恢复,能够进行预测。需要说明一下,我们墙裂推荐你们使用这种测试方法先对模型进行一下简单的测试,不然等跑大数据集又慢又没谱。
当然啦,为了能够很好的使用这些测试,我们还需要做一些额外的工作:定义一个测试配置文件;构造一个更小的输入文件。
好啦,下面可以动真格的开始写模型啦。不过在开始写之前,我们首先需要确定一下模型的基本结构。我们有两个输入(标题和摘要)以及一个输出标签,注意这两个输入都应该已经是转换成序号啦。那么我们下一步自然就是需要把序号转换成对应的embeddings
啦。接下来需要考虑的事情我们如何处理这些向量呢?我们的前馈神经网络该弄成几层?神经网络该是什么结构?不用怕!我们的AllenNLP
在这里全封装,没有中间商赚差价,在这里能够让你花最少的时间完成一个基本的模型。我们先来看一看构造函数
@Model.register("paper_classifier")
class AcademicPaperClassifier(Model):
def __init__(self,
vocab: Vocabulary,
text_field_embedder: TextFieldEmbedder,
title_encoder: Seq2VecEncoder,
abstract_encoder: Seq2VecEncoder,
classifier_feedforward: FeedForward,
initializer: InitializerApplicator = InitializerApplicator(),
regularizer: Optional[RegularizerApplicator] = None) -> None:
super(AcademicPaperClassifier, self).__init__(vocab, regularizer)
self.text_field_embedder = text_field_embedder
self.num_classes = self.vocab.get_vocab_size("labels")
self.title_encoder = title_encoder
self.abstract_encoder = abstract_encoder
self.classifier_feedforward = classifier_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
以及我们输入的序号,我们就能够获得一个向量组成的序列。下一步就是对这个序列进行变化。在这里我们使用的是Seq22VecEncoder
。这个Encoder
可以有很多的变化,在这里我们使用的是最最简单的一种,就是bag of embeddings
,直接求平均。当然啦,我们也可以使用什么CNN
啦,RNN
啦等高大上的模型,我们都有的!我们都有的!
前馈神经网络呢也是一个预先定义好的Module
,我们可以修改这个网络的深度宽度激活函数。InitializerApplicator
包含着所有参数的基本初始化方法。如果你想自定义初始化,就需要时候用RegularizerApplicator
这些概念搞懂了之后,下面的操作就很简单啦。我们只需要像传统的Pytorch
中的程序一样,重写一个forward
函数就好啦。
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_embedder(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
吗?它构造的instance
就包含了这样几个字段Fields
。所以在这里,参数的名字一定要和DatasetReader
中定义的名字保持一致。AllenNLP
在这里将会自动的利用你的DatasetReader
并且把数据组织成batches
的形式,必要时还会给你padding
一下。注意,forward函数接收的参数正是一个batch的数据。
注意,我们要求必须把labels
也传递给forward
函数。这么做的主要目的是为了能够计算损失函数。在训练的时候,我们的模型会主动的去寻找这个loss
,然后自动的反向传播回去,然后更改参数。同时我们也应该注意到,这个参数是可以为空的,这主要是为了应对prediction的情况。这个将会在后面章节中进行介绍。
接下来,我们来看一看输入的类型。label
很简单,他就是一个[batch_size,1]
大小的tensor
没什么好说的。另外两个可就有点复杂啦。如果你还记得,title
和abstract
两个是TextField
类型的,然后这些TextField
又被转换成了字典类型的。这个新的字典呢可能包括了单词id,字母array或者pos标签ID什么的。但是我们的embedder
是不在乎你是什么鬼字典的,直接一股脑的扔进去就能够帮你完成转换过程。这就意味着我们TextFieldEmbedder
必须和TextField
完美的对接哇,不然不是要瞎转换啦。对接的过程又是在配置文件中完成的,在后面我们将会详细的讲解。
现在我们已经理解了模型的基本输入,来看看它的基本逻辑。模型干的第一件事就是找到title
和abstract
的embeddings
,然后对这些向量进行操作。注意我们需要利用一个叫masks
的变量来标识哪些元素仅仅是用来标识边界的,而不需要模型考虑。我们对这些向量进行了一通操作之后,生成了一个向量。然后把这个向量扔到一个前馈神经网络中就可以得到class logits
其实就是预测为各个类的概率,有了这个概率我们就可以得到最终预测的结果。最后,如果是训练过程的话,我们还需要计算损失和评价标准。我们来看一看这两部分的代码。
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()}
This method is how the model tells the training code which metrics it's computing. The second pieces of code is the decode method:
def decode(self, output_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
predictions = output_dict['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
decode
函数包括两个功能①是接收forward
函数的返回值,并且对这个返回值进行操作,比如说算出具体是那个词啊等等。②是将数字变成字符,方便阅读。好啦,至此我们的模型已经构建好啦,现在我们可以测试啦。
3.5 训练模型
为了训练模型,我们需要定义一个配置文件。首先来看对dataset_reader
的定义。
"dataset_reader": {
"type": "s2_papers"
},
这一部分配置应该包含着DatasetReader
的输入参数。type
这个字段呢,就是我们注册的自己写的DatasetReader
的名字,剩下的参数都是直接传入到了构造函数中。在这里没有其他的参数,所以我们就会使用默认的tokenizer
和tokenIndexer
。
接着看一看训练和测试的基本参数。下面这些参数定义了如何构造batch数据,如何训练模型。sorting_keys
顾名思义是定义了我们所有instance
的排序方式,排序可以提高效率(花在padding token上面的计算减少);使用AdaGrad
作为优化器,执行40个epoch,如果10个epoch没有改进的话就提前结束训练。并且这里的训练数据是从网上下载下来的,这也就是我们之前写读取文件的代码时用到了cached_path的原因。
"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",
"iterator": {
"type": "bucket",
"sorting_keys": [["abstract", "num_tokens"], ["title", "num_tokens"]],
"batch_size": 64
},
"trainer": {
"num_epochs": 40,
"patience": 10,
"cuda_device": 0,
"grad_clipping": 5.0,
"validation_metric": "+accuracy",
"optimizer": {
"type": "adagrad"
}
}
最后一段配置新鲜出炉啦。key
字段又出现啦,这是我们注册的模型名称。剩下的字段将会传递给模型的from_params
函数。我们还定义了TextFieldEmbedder
的配置,在这里用的是glove的向量作为我们的embeddings。当然我们也可以定义一些字母级别的embeddings,甚至可以定义完之后让他自动的卷积一下什么的,这都是可行的。在这里没有这种操作,也就不介绍啦。不过在crf_tagger
中是存在的。
title
和abstract
呢用的都是双向LSTM的模型作为encoders
,其实就是pytorch 里面封装了一下Seq2VecEncoder
,这样在代码中就可以给出统一的格式了。前馈神经网络可以自定义宽度深度和激活层。
"model": {
"type": "paper_classifier",
"text_field_embedder": {
"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]
}
},
完啦!到了这里我们已经把自己的模型写完啦,现在可以直接在数据集上训练啦。现在呢,唯一的问题就是我们的AllenNLP
并不认识我们的模型哇,我们必须吧它放在python
的包路径上,然后执行时加上这么一个参数--include-package my_library
。完整的执行命令如下
allennlp train \
experiments/venue_classifier.json \
-s /tmp/venue_output_dir \
--include-package my_library
3.6 实现预测功能
当我们训练好模型之后,接下来需要做的就是预测了。预测的实现是本小节的主要内容。
3.6.1 创建预测器
在我们前面写过的论文分类的模型当中,最核心的就是forward
函数,这个函数长这个样子:
def forward(self,
title: Dict[str, torch.LongTensor],
abstract: Dict[str, torch.LongTensor],
label: torch.LongTensor = None) -> Dict[str, torch.Tensor]:
上面的forward
函数已经很棒啦,其实我们以前呢都是在forward的之后,自己写个函数就叫个predict
啥的,然后用这个去预测。但是呢,在AllenNLP
里的解决方案是有所不同的。在这里把预测器定义成了一个类,重点重写一个_json_to_instance
函数,这个函数呢主要工作是根据json
格式的输入数据,生成instance
,以及标签之类的信息,然后利用forward
函数进行预测,最后把forward
的返回结果和这玩意的返回结果一起返回给你。之所以要采用这种方式,主要是为了能够方便的用于展示demo
@Predictor.register('paper-classifier')
class PaperClassifierPredictor(Predictor):
"""Predictor wrapper for the AcademicPaperClassifier"""
@overrides
def _json_to_instance(self, json_dict: JsonDict) -> Tuple[Instance, JsonDict]:
title = json_dict['title']
abstract = json_dict['paperAbstract']
instance = self._dataset_reader.text_to_instance(title=title, abstract=abstract)
# label_dict will be like {0: "ACL", 1: "AI", ...}
label_dict = self._model.vocab.get_index_to_token_vocabulary('labels')
# Convert it to list ["ACL", "AI", ...]
all_labels = [label_dict[i] for i in range(len(label_dict))]
return instance, {"all_labels": all_labels}
这段代码使用了前面写好的text_to_instance
组成了一个只包含title
和abstract
两个字段的instance
。这个函数的返回值是一个元组,包括了两个元素。第一个是返回的实例instance
,第二个是一个字典,包含了所有的标签之类的东西,总之就是我们的forward的返回值没有提供的但是我们又需要的东西都放在这里就好啦。在这里我们只是存储所有可能的标签,其他的东西暂时就不存啦。当然,如果你连标签都用不着,那你就放个空空的字典在这里占位就好啦。
3.6.2 测试预测器
到这里,就又是常规操作啦。我们想要测一测我们的预测器写的是不是对的。这里的测试和前面最大的不同之处在于需要首先初始化我们的模型,DatasetReader
以及predictor
。在这里,我在自己包下写了一个init.py的文件,这样我们就能直接引用这些包就好啦。import my_library
。
我们的这个测试将会写的非常简单,我们只需要提供一个输入,然后通过模型去执行就好啦。回想一下,我们的forward
函数中有两个输出,一个是label
一个是class_probabilities
。所以我们需要检查一下这是不是真的标签,还有这些概率值是不是真的是概率值哇。这里要结合着forward
来使用,我们给个实际的标签名就好啦。
class TestPaperClassifierPredictor(TestCase):
def test_uses_named_inputs(self):
inputs = {
"title": "Interferring Discourse Relations in Context",
"paperAbstract": (
"We investigate various contextual effects on text "
"interpretation, and account for them by providing "
"contextual constraints in a logical theory of text "
"interpretation. On the basis of the way these constraints "
"interact with the other knowledge sources, we draw some "
"general conclusions about the role of domain-specific "
"information, top-down and bottom-up discourse information "
"flow, and the usefulness of formalisation in discourse theory."
)
}
archive = load_archive('tests/fixtures/model.tar.gz')
predictor = Predictor.from_archive(archive, 'paper-classifier')
result = predictor.predict_json(inputs)
label = result.get("label")
assert label in ['AI', 'ML', 'ACL']
class_probabilities = result.get("class_probabilities")
assert class_probabilities is not None
assert all(cp > 0 for cp in class_probabilities)
assert sum(class_probabilities) == approx(1.0)
经过这些测试用例的验证,我们就可以放心大胆的使用预测的功能啦。下面给出基本的使用方法。
usage: allennlp [command] predict [-h]
[--output-file OUTPUT_FILE]
[--batch-size BATCH_SIZE]
[--silent]
[--cuda-device CUDA_DEVICE]
[-o OVERRIDES]
[--include-package INCLUDE_PACKAGE]
[--predictor PREDICTOR]
archive_file input_file
留心一下上面的代码有两个必须要有的参数,输入文件,以及训练好的模型。下面给出一个例子:
allennlp predict \
tests/fixtures/model.tar.gz \
tests/fixtures/s2_papers.jsonl \
--include-package my_library \
--predictor paper-classifier
When you run this it will print the ten test inputs and their predictions, each of which looks like:
prediction: {"all_labels": ["AI", "ACL", "ML"], "logits": [0.008737504482269287, 0.22074833512306213, -0.005263201892375946], "class_probabilities": [0.31034138798713684, 0.38363200426101685, 0.3060266375541687], "label": "ACL"}
3.7 运行在线的demo
一旦你训练好了模型,并且配置好了一个预测器,,就很容易运行一个简单的web Demo啦。只需要执行
$ python -m allennlp.service.server_simple --help
usage: server_simple.py [-h] [--archive-path ARCHIVE_PATH]
[--predictor PREDICTOR] [--static-dir STATIC_DIR]
[--title TITLE] [--field-name FIELD_NAME]
[--include-package INCLUDE_PACKAGE]
简便起见,我们先忽略STATIC_DIR
,搭建一个简单的服务器:
python -m allennlp.service.server_simple \
--archive-path tests/fixtures/model.tar.gz \
--predictor paper-classifier \
--include-package my_library \
--title "Academic Paper Classifier" \
--field-name title \
--field-name paperAbstract
执行上面的命令将会在localhost:8000
开启一个简单的服务,如图所示
想必图上的这些功能你也都知道啦,所以也就没必要额外解释啦。
z自定义DEMO
当然啦,我们这个可视化是非常简陋的,只是对预测的结果进行了简单的展示。如果你想对模型内部的数据进行展示,比如说attention heat maps
...那你就再看看其他教程吧。。哈哈哈。。。
当然啦,也不是说咱家的模型可视化就一点不能动,你可以自己写个index.html
的,放在--STATIC_DIR
参数指定的目录下就行啦。偷懒的技巧呢就是我们先跑起来基本的可视化界面,然后查看源码,保存下来,然后在这个基础上进行修改。
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.bundle.js"></script>
<div id="output" class="output">
<div class="placeholder">
<div class="placeholder__content">
<p>Run model to view results</p>
</div>
</div>
</div>
var canvas = '<canvas id="myChart" width="400" height="400"></canvas>';
document.getElementById("output").innerHTML = canvas;
We also want to parse the prediction response into JSON:
var response = JSON.parse(xhr.responseText);
And finally we just need to follow their docs for creating a pie chart:
var ctx = document.getElementById("myChart");
var pieChart = new Chart(ctx, {
type: 'pie',
data: {
labels: response['all_labels'],
datasets: [{
data: response['class_probabilities'],
backgroundColor: ['red', 'green', 'blue']
}]
}
});
运行我们自定义的demo的方法如下:
python -m allennlp.service.server_simple \
--archive-path tests/fixtures/model.tar.gz \
--predictor paper-classifier \
--include-package my_library \
--static-dir static_html
补充知识-itertools的使用
根据itertools
的官方文档的说法,这是一个受其他语言中各种各样iterator
的功能的启发构建起来的一个更高层的iterator
工具。简单来说,这个工具是利用简单的iterator
构建起更为复杂的iterator
。这个工具呢省时省力,操作简单,并且节省内存,我们经常用到其中的一些功能,比如说zip
,map
函数等。
在CrfTagger中,我们只用到了其中的一个高级功能:groupby
。这里这个功能的使用单纯的是出于效率的考虑,因为我们直接遍历然后判断就是可以解决问题的。我们来看一下这个例子,如果我们想要根据性别进行分组,实际上只需要标一下组号就行。因此,分组问题就只要一个标号函数就能完成,这也是我们这里的groupby
的基本原理:第二个参数就是标号函数,在遍历的过程中每次都会返回标号的结果。
姓名 | 年龄 | 性别 |
---|---|---|
张三 | 1 | 男 |
李四 | 32 | 女 |
王五 | 234 | 男 |
徐一 | 6 | 男 |
姓名 | 年龄 | 性别 | 组号 |
---|---|---|---|
张三 | 1 | 男 | 1 |
李四 | 32 | 女 | 2 |
王五 | 234 | 男 | 1 |
徐一 | 6 | 男 | 1 |
# groupby 本质等价于给原来的数据按照分组标号;这里_is_divider是标号函数,返回值只有两个true/false,用来标识是否是空行
for is_divider, lines in itertools.groupby(data_file, _is_divider):
# Ignore the divider chunks, so that `lines` corresponds to the words
# of a single sentence.
if not is_divider:
fields = [line.strip().split() for line in lines]
# unzipping trick returns tuples, but our Fields need lists
# 每一列是一个tuple
fields = [list(field) for field in zip(*fields)]
tokens_, pos_tags, chunk_tags, ner_tags = fields
# TextField requires ``Token`` objects
tokens = [Token(token) for token in tokens_]
yield self.text_to_instance(tokens, pos_tags, chunk_tags, ner_tags)