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)
#现在我们应该得到同样的预测。
网友评论