AllenNLP实例分析

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