美文网首页
AllenNLP实例分析

AllenNLP实例分析

作者: 张小邪先森 | 来源:发表于2020-06-19 00:12 被阅读0次

    https://allennlp.org/tutorials
    给出一个句子(例如"The dog ate the apple"我们要预测每个词的词性标签。
    (例如“Det”,“NN”,“V”,“Det”,“NN”)。

    与PyTorch教程中一样,我们将把每个单词嵌入低维空间,通过LSTM传递它们以获得一系列编码,并使用前馈层将这些编码转换为一系列逻辑(对应于可能的词性标签)。

    下面是用于完成此操作的带注释的代码。您可以从头开始阅读注释,或者在需要更多解释时只需查看代码并查看注释。

    from typing import Iterator, List, Dict
    #在AllenNLP中,我们几乎所有内容都使用类型注释(type annotation)
    import torch
    import torch.optim as optim
    import numpy as np
    #AllenNLP构建在PyTorch之上,因此我们可以自由使用它的代码。
    from allennlp.data import Instance
    from allennlp.data.fields import TextField, SequenceLabelField
    #在AllenNLP中,我们将每个训练示例表示为包含各种类型的字段的实例。在这里,每个示例都有一个包含句子的TextField和一个包含相应词性标记的SequenceLabelField。
    from allennlp.data.dataset_readers import DatasetReader
    #通常,要使用AllenNLP解决这样的问题,您必须实现两个类。第一个是DatasetReader,它包含读取数据文件和生成实例流的逻辑
    from allennlp.common.file_utils import cached_path
    #我们经常希望从URL加载数据集或模型。cached_path帮助器下载此类文件,在本地缓存它们,然后返回本地路径。它还接受本地文件路径(它只是按原样返回)。
    from allennlp.data.token_indexers import TokenIndexer, SingleIdTokenIndexer
    from allennlp.data.tokenizers import Token
    #有多种方式可以将单词表示为一个或多个索引。例如,您可以维护唯一单词的词汇表,并为每个单词赋予相应的id。或者,您可能在单词中的每个字符上都有一个ID,并将每个单词表示为一系列ID。AllenNLP对此表示使用HAS TokenIndexer抽象。
    from allennlp.data.vocabulary import Vocabulary
    #TokenIndexer表示如何将标记转换为索引的规则,而词汇表包含从字符串到整数的对应映射。例如,您的TokenIndexer可能指定将一个令牌表示为一系列字符ID,在这种情况下,词汇表将包含映射{character->id}。在这个特定的示例中,我们使用SingleIdTokenIndexer为每个令牌分配一个惟一的id,因此词汇表将只包含一个映射{Token->id}(以及反向映射)。
    from allennlp.models import Model
    #除了DatasetReader之外,您通常需要实现的另一个类是Model,它是一个PyTorch模块,它接受张量输入并生成张量输出字典(包括您想要优化的训练损失)。
    from allennlp.modules.text_field_embedders import TextFieldEmbedder, BasicTextFieldEmbedder
    from allennlp.modules.token_embedders import Embedding
    from allennlp.modules.seq2seq_encoders import Seq2SeqEncoder, PytorchSeq2SeqWrapper
    from allennlp.nn.util import get_text_field_mask, sequence_cross_entropy_with_logits
    ##如上所述,我们的模型将由嵌入层、LSTM和前馈层组成。AllenNLP包括所有这些功能的抽象,这些功能可以智能地处理填充和批处理,以及各种实用函数。
    from allennlp.training.metrics import CategoricalAccuracy
    #我们想要跟踪训练和验证数据集的准确性。
    from allennlp.data.iterators import BucketIterator
    #在我们的培训中,我们需要一个可以智能地对数据进行批处理的DataIterator。
    from allennlp.training.trainer import Trainer
    #我们会用AllenNLP功能齐全的训练器。
    from allennlp.predictors import SentenceTaggerPredictor
    torch.manual_seed(1)
    #最后,我们想要对新的输入进行预测,下面将对此进行更多说明。
    class PosDatasetReader(DatasetReader):
        """
        DatasetReader for PoS tagging data, one sentence per line, like
    
            The###DET dog###NN ate###V the###DET apple###NN
        """
    #我们的首要任务是实现DatasetReader子类。
        def __init__(self, token_indexers: Dict[str, TokenIndexer] = None) -> None:
            super().__init__(lazy=False)
            self.token_indexers = token_indexers or {"tokens": SingleIdTokenIndexer()}
    #我们的DatasetReader需要的唯一参数是指定如何将标记转换为索引的TokenIndexer字典。默认情况下,我们只为每个令牌(我们称之为“令牌”)生成一个索引,即每个不同令牌的唯一id。(这只是您在大多数NLP任务中使用的标准“单词到索引”映射。)
        def text_to_instance(self, tokens: List[Token], tags: List[str] = None) -> Instance:
            sentence_field = TextField(tokens, self.token_indexers)
            fields = {"sentence": sentence_field}
    
            if tags:
                label_field = SequenceLabelField(labels=tags, sequence_field=sentence_field)
                fields["labels"] = label_field
    
            return Instance(fields)
    #DatasetReader.text_to_instance接受对应于训练示例的输入(在本例中为句子的标记和相应的词性标记),实例化相应的Fields(在本例中为句子的TextField和其标记的SequenceLabelField),并返回包含这些字段的实例。请注意,标记是可选的,因为我们希望能够从未标记的数据创建实例来对它们进行预测。
        def _read(self, file_path: str) -> Iterator[Instance]:
            with open(file_path) as f:
                for line in f:
                    pairs = line.strip().split()
                    sentence, tags = zip(*(pair.split("###") for pair in pairs))
                    yield self.text_to_instance([Token(word) for word in sentence], tags)
    #我们必须实现的另一个部分是_read,它接受一个文件名并生成一个实例流。大部分工作已经在Text_to_Instance中完成。
    class LstmTagger(Model):
    #基本上必须实现的另一个类是Model,它是torch.nn.Module的子类。它如何工作在很大程度上取决于你,它主要只需要一个向前的方法,它接受张量输入,并产生一个张量输出字典,其中包括你将用来训练模型的损失。如上所述,我们的模型将由嵌入层、序列编码器和前馈网络组成。
        def __init__(self,
    #有一件事看起来很不寻常,那就是我们将把嵌入器和序列编码器作为构造函数参数传入。这允许我们使用不同的嵌入器和编码器进行实验,而不必更改模型代码。
                     word_embeddings: TextFieldEmbedder,
    #嵌入层被指定为AllenNLP TextFieldEmbedder,它表示将令牌转换为张量的一般方式。(在这里,我们知道我们希望用学习的张量表示每个唯一的单词,但是使用通用类允许我们轻松地试验不同类型的嵌入,例如Elmo。)
                     encoder: Seq2SeqEncoder,
    #类似地,编码器被指定为通用Seq2SeqEncode,即使我们知道要使用LSTM。同样,这使得使用其他序列编码器(例如Transformer)进行实验变得很容易。
                     vocab: Vocabulary) -> None:
    #每个AllenNLP模型还需要一个词汇表,它包含标记到索引和标签到索引的命名空间映射。
            super().__init__(vocab)
            self.word_embeddings = word_embeddings
            self.encoder = encoder
    #请注意,我们必须将单词传递给基类构造函数。
            self.hidden2tag = torch.nn.Linear(in_features=encoder.get_output_dim(),
                                              out_features=vocab.get_vocab_size('labels'))
    #前馈层不是作为参数传入的,而是由我们构造的。注意,它查看编码器以查找正确的输入维度,并查看词汇表(特别是标签->索引映射)以查找正确的输出维度。
            self.accuracy = CategoricalAccuracy()
    #最后要注意的是,我们还实例化了一个CategoricalAccuracy度量,我们将使用它来跟踪每个训练和验证时期的准确性。
        def forward(self,
                    sentence: Dict[str, torch.Tensor],
                    labels: torch.Tensor = None) -> Dict[str, torch.Tensor]:
    #接下来,我们需要实现Forward,这是实际计算发生的地方。您的数据集中的每个实例都将被(与其他实例一起批处理并)反馈到Forward中。Forward方法期望张量的字典作为输入,并且期望它们的名称是实例中字段的名称。在本例中,我们有一个语句字段和(可能)一个标签字段,因此我们将相应地构造我们的Forward:
            mask = get_text_field_mask(sentence)
    #AllenNLP被设计为对批处理输入进行操作,但是不同的输入序列具有不同的长度。在幕后,AllenNLP填充较短的输入,以便批处理具有统一的形状,这意味着我们的计算需要使用掩码来排除填充。在这里,我们只使用实用函数get_text_field_ask,它返回与填充和未填充位置相对应的张量0和1。
            embeddings = self.word_embeddings(sentence)
    #我们首先将语句张量(每个语句都是一系列令牌ID)传递给word_embedding模块,该模块将每个语句转换为嵌入的张量序列。
            encoder_out = self.encoder(embeddings, mask)
    #接下来,我们将嵌入的张量(和掩码)传递给LSTM,LSTM将生成一系列编码输出。
            tag_logits = self.hidden2tag(encoder_out)
            output = {"tag_logits": tag_logits}
    #最后,我们将每个编码的输出张量传递到前馈层,以产生对应于各种标签的logit。
            if labels is not None:
                self.accuracy(tag_logits, labels, mask)
                output["loss"] = sequence_cross_entropy_with_logits(tag_logits, labels, mask)
    
            return output
    #与前面一样,标签是可选的,因为我们可能希望运行此模型来对未标记的数据进行预测。如果我们有标签,那么我们就使用它们来更新我们的精度度量,并计算输出中的“损失”。
        def get_metrics(self, reset: bool = False) -> Dict[str, float]:
            return {"accuracy": self.accuracy.get_metric(reset)}
    #我们包括了一个准确度指标,该指标在每次向前传递时都会更新。这意味着我们需要覆盖从中提取数据的get_metrics方法。在幕后,CategoricalAccuracy度量存储预测数和正确预测数,并在每次呼叫前转期间更新这些计数。每次调用get_metric都会返回计算出的精度,并(可选)重置计数,这使我们可以重新跟踪每个时期的精度。
    reader = PosDatasetReader()
    #既然我们已经实现了DatasetReader和Model,我们就可以开始培训了。我们首先需要数据集读取器的一个实例。
    train_dataset = reader.read(cached_path(
        'https://raw.githubusercontent.com/allenai/allennlp'
        '/master/tutorials/tagger/training.txt'))
    validation_dataset = reader.read(cached_path(
        'https://raw.githubusercontent.com/allenai/allennlp'
        '/master/tutorials/tagger/validation.txt'))
    #我们可以用它来读取训练数据和验证数据。这里我们从URL读取它们,但是如果您的数据是本地的,您也可以从本地文件读取它们。我们使用cached_path在本地缓存文件(并手动读取。读取本地缓存版本的路径。)
    vocab = Vocabulary.from_instances(train_dataset + validation_dataset)
    #一旦我们读取了数据集,我们就使用它们来创建我们的词汇表(即,从标记/标签到ID的映射)。
    EMBEDDING_DIM = 6
    HIDDEN_DIM = 6
    #现在我们需要构建模型。我们将为LSTM的嵌入层和隐藏层选择一个大小。
    token_embedding = Embedding(num_embeddings=vocab.get_vocab_size('tokens'),
                                embedding_dim=EMBEDDING_DIM)
    word_embeddings = BasicTextFieldEmbedder({"tokens": token_embedding})
    #对于嵌入令牌,我们将只使用BasicTextFieldEmbedder,它接受从索引名到嵌入的映射。如果返回到我们定义DatasetReader的位置,默认参数包括一个称为“tokens”的索引,因此我们的映射只需要一个对应于该索引的嵌入。我们使用词汇表来确定需要多少个嵌入,并使用embedding_dim参数来指定输出维度。也可以从预先训练的嵌入开始(例如,手套向量),但在这个小小的玩具数据集上不需要这样做。
    lstm = PytorchSeq2SeqWrapper(torch.nn.LSTM(EMBEDDING_DIM, HIDDEN_DIM, batch_first=True))
    #接下来,我们需要指定序列编码器。这里需要PytorchSeq2SeqWrapper有点令人遗憾(如果您使用配置文件,就不需要担心这个问题),但是这里需要向内置的PyTorch模块添加一些额外的功能(和更干净的界面)。在AllenNLP中,我们先成批处理所有事情,所以我们也指定了这一点。
    model = LstmTagger(word_embeddings, lstm, vocab)
    #最后,我们可以实例化模型。
    if torch.cuda.is_available():
        cuda_device = 0
    #接下来,让我们检查一下我们是否可以访问GPU。
        model = model.cuda(cuda_device)
    else:
    #既然我们这样做了,我们就把我们的模型移到GPU0上。
        cuda_device = -1
    #在本例中,我们没有这样做,所以我们指定-1以回退到CPU。(模型已经驻留的位置。)
    optimizer = optim.SGD(model.parameters(), lr=0.1)
    #现在我们准备好训练模型了。我们首先需要的是优化器。我们可以只使用PyTorch的随机梯度下降。
    iterator = BucketIterator(batch_size=2, sorting_keys=[("sentence", "num_tokens")])
    #我们还需要一个DataIterator来处理数据集的批处理。BucketIterator按指定字段对实例进行排序,以便创建具有相似序列长度的批次。在这里,我们表示要根据语句字段中的标记数量对实例进行排序。
    iterator.index_with(vocab)
    #我们还指定迭代器应该确保它的实例使用我们的词汇表进行索引;也就是说,它们的字符串已经使用我们之前创建的映射转换为整数。
    trainer = Trainer(model=model,
                      optimizer=optimizer,
                      iterator=iterator,
                      train_dataset=train_dataset,
                      validation_dataset=validation_dataset,
                      patience=10,
                      num_epochs=1000,
                      cuda_device=cuda_device)
    #现在我们实例化训练器并运行它。在这里,我们告诉它运行1000个历元,如果它花费了10个历元而验证度量没有改进,那么就提前停止训练。默认的验证度量是Lost(通过变小来改进),但是也可以指定不同的度量和方向(例如,准确性应该变得更大)。
    trainer.train()
    #当我们启动它时,它将为每个纪元打印一个进度条,其中包括“损失”和“精度”指标。如果我们的模型是好的,随着我们的训练,损失应该会下降,准确率应该会上升。
    predictor = SentenceTaggerPredictor(model, dataset_reader=reader)
    #与最初的PyTorch教程一样,我们想看看我们的模型生成的预测。AllenNLP包含一个预测器抽象,该抽象接受输入,将其转换为实例,通过您的模型馈送它们,并返回JSON可序列化的结果。通常您需要实现您自己的预测器,但是AllenNLP已经有了一个SentenceTaggerPredictor,它在这里可以完美地工作,所以我们可以使用它。它需要我们的模型(用于预测)和数据集读取器(用于创建实例)。
    tag_logits = predictor.predict("The dog ate the apple")['tag_logits']
    #它有一个只需要一句话的预测方法,并从FORWARD返回输出字典(JSON可序列化的版本)。这里,tag_logits将是Logit的(5,3)数组,对应于5个字中的每一个的3个可能的标签。
    tag_ids = np.argmax(tag_logits, axis=-1)
    #要获得实际的“预测”,我们只需取argmax即可。
    print([model.vocab.get_token_from_index(i, 'labels') for i in tag_ids])
    #然后使用我们的词汇表来查找预测的标签
    # Here's how to save the model.
    with open("/tmp/model.th", 'wb') as f:
        torch.save(model.state_dict(), f)
    #最后,我们希望能够保存模型并稍后重新加载。我们需要保存两样东西。首先是模型重量。
    vocab.save_to_files("/tmp/vocabulary")
    #第二个是词汇。
    # And here's how to reload the model.
    vocab2 = Vocabulary.from_files("/tmp/vocabulary")
    #我们只保存了模型权重,因此如果我们想要重用它们,实际上必须使用代码重新创建相同的模型结构。首先,让我们将词汇表重新加载到一个新变量中。
    model2 = LstmTagger(word_embeddings, lstm, vocab2)
    #然后让我们重新创建模型(如果我们在不同的文件中执行此操作,当然还必须重新实例化单词Embedding和LSTM)
    with open("/tmp/model.th", 'rb') as f:
        model2.load_state_dict(torch.load(f))
    #之后我们必须加载它的状态。
    if cuda_device > -1:
        model2.cuda(cuda_device)
    #在这里,我们将加载的模型移动到前面使用的GPU。这是必要的,因为我们早先将Word_Embedding和LSTM与原始模型一起移动了。模型的所有参数都需要在同一设备上。
    predictor2 = SentenceTaggerPredictor(model2, dataset_reader=reader)
    tag_logits2 = predictor2.predict("The dog ate the apple")['tag_logits']
    np.testing.assert_array_almost_equal(tag_logits2, tag_logits)
    #现在我们应该得到同样的预测。
    

    相关文章

      网友评论

          本文标题:AllenNLP实例分析

          本文链接:https://www.haomeiwen.com/subject/kqxmxktx.html