AllenNLP 使用教程

插播一条关于Allennlp 的库的基本用法的介绍

细胞

1 安装

2 基本使用方法

3 实例教学

本来呢是亦步亦趋的跟着官网的教程走的,应该先看安装,然后教怎么使用,怎么实现自己的代码。但是看着看着看不懂啦!往后一翻,才发现这玩意才是最该先看的教程。在这里就是简单的给它翻译成中文吧。

  正如前文中所提到的,Allennlp的基本使用流程是需要自定义两个文件:datareadermodel;
同时,如果存在已经定义好的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_indexersTokenizer呢是把你的文本转换成单词啦,字母啦,比特对啦等等常见的你想要的形式。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没什么好说的。另外两个可就有点复杂啦。如果你还记得,titleabstract两个是TextField类型的,然后这些TextField又被转换成了字典类型的。这个新的字典呢可能包括了单词id,字母array或者pos标签ID什么的。但是我们的embedder是不在乎你是什么鬼字典的,直接一股脑的扔进去就能够帮你完成转换过程。这就意味着我们TextFieldEmbedder必须和TextField完美的对接哇,不然不是要瞎转换啦。对接的过程又是在配置文件中完成的,在后面我们将会详细的讲解。

  现在我们已经理解了模型的基本输入,来看看它的基本逻辑。模型干的第一件事就是找到titleabstractembeddings,然后对这些向量进行操作。注意我们需要利用一个叫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的名字,剩下的参数都是直接传入到了构造函数中。在这里没有其他的参数,所以我们就会使用默认的tokenizertokenIndexer

  接着看一看训练和测试的基本参数。下面这些参数定义了如何构造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中是存在的。

  titleabstract呢用的都是双向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组成了一个只包含titleabstract两个字段的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开启一个简单的服务,如图所示

demo

  想必图上的这些功能你也都知道啦,所以也就没必要额外解释啦。

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。这个工具呢省时省力,操作简单,并且节省内存,我们经常用到其中的一些功能,比如说zipmap函数等。

  在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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,848评论 25 707
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,969评论 3 119
  • 2017年2月16日 星期四 农历正月二十日 “育心丽谦·时间管理100天挑战营"第32天 【早起】4:50 【学...
    归零2017阅读 246评论 0 2
  • 这些日子,情绪依然时好时坏,尽管表面上是在安静地看书,或者随意地聊天,但一个人时内心时常翻江倒海,静不下来,以至于...
    绽蕊向阳阅读 656评论 0 1