本篇文章来源于http://mlexplained.com/2018/02/15/language-modeling-tutorial-in-torchtext-practical-torchtext-part-2/

 

上一篇文章中,以文本分类为例,编写了torchtext入门教程。

在本文中,将概述如何使用torchtext训练语言模型。我们还将介绍在训练自己的实际模型时可能要使用的torchtext的一些更实际的功能。具体来说,我们将介绍

  • 使用内置数据集
  • 使用自定义标记器
  • 使用预训练的词嵌入

完整代码可在此处获得请注意,如果您正在运行本教程中的代码,则我假设您出于训练速度的考虑可以访问GPU。如果没有GPU,您仍然可以继续学习,但是培训会非常缓慢。

1.什么是语言建模?

语言建模是一项任务,我们要构建一个模型,该模型可以将单词序列作为输入,并确定该序列成为实际人类语言的可能性。例如,我们希望我们的模型预测“This is a sentence”可能是一个序列,而“cold his book her”则不太可能。

尽管语言模型本身似乎并不有趣,但是它们可以用作无监督的预训练方法或其他任务(如聊天生成)的基础。无论如何,语言建模是NLP深度学习中最基本的任务之一,因此,将语言建模作为其他更复杂的任务(例如机器翻译)的基础是一个好主意。

我们通常训练语言模型的方式是通过训练它们以给定一个句子或多个句子中的所有先前单词来预测下一个单词。因此,我们需要做的语言建模就是大量的语言数据。在本教程中,我们将使用著名的WikiText2数据集,该数据集是torchtext提供的内置数据集。

2.准备数据

要使用WikiText2数据集,我们需要准备用于处理文本的标记化和数字化的字段。这次,我们将尝试使用自己的自定义令牌生成器:spacy令牌生成器。 Spacy是一个处理许多自然语言处理任务的框架,而torchtext旨在与其紧密合作。使用torchtext可以轻松使用令牌生成器:我们要做的就是传递令牌生成器函数

import torchtext
from torchtext import data
import spacy
 
from spacy.symbols import ORTH
my_tok = spacy.load('en')
 
def spacy_tok(x):
    return [tok.text for tok in my_tok.tokenizer(x)]
 
TEXT = data.Field(lower=True, tokenize=spacy_tok)

add_special_case只是告诉令牌生成器以某种方式解析某个字符串。特殊情况字符串后面的列表表示我们希望如何对字符串进行标记。

如果我们想将"don't" into "do" and "'nt",那么我们将写

my_tok.tokenizer.add_special_case("don't", [{ORTH: "do"}, {ORTH: "n't"}])

现在,我们准备加载WikiText2数据集。使用这些数据集的有效方法有两种:一种是将数据集加载为训练集,验证集和测试集,另一种是作为迭代器加载。数据集提供了更大的灵活性,因此我们将在此处使用该方法。

from torchtext.datasets import WikiText2
 
train, valid, test = WikiText2.splits(TEXT) # loading custom datasets requires passing in the field, but nothing else.

让我们快速浏览一下内部。请记住,数据集的行为在很大程度上类似于普通列表,因此我们可以使用len函数来测量长度

>>> len(train)
1

仅一个训练示例?我们做错了吗?事实并非如此。只是数据集的整个语料库包含在一个示例中。我们将在以后看到该示例的批处理方式。

现在我们有了数据,让我们来建立词汇表。这次,让我们尝试使用预先计算的词嵌入。这次我们将使用200维的GloVe向量。在torchtext中还有各种其他预先计算的词嵌入(包括尺寸为100和300的GloVe向量),也可以以几乎相同的方式加载。

TEXT.build_vocab(train, vectors="glove.6B.200d")

我们仅用3行代码(不包括导入和令牌生成器)准备了数据集。现在,我们继续构建Iterator,该迭代器将为我们处理批处理并将数据移至GPU。

这是本教程的精彩部分,并说明了为什么torchtext对于语言建模如此方便。事实证明,torchtext有一个非常方便的迭代器,可以为我们完成大部分繁重的工作。称为BPTTIteratorBPTTIterator做以下为我们:

  • 将语料库分为序列长度 bptt 的批次 

例如,假设我们具有以下语料库:

"Machine learning is a field of computer science that gives computers the ability to learn without being explicitly programmed."

 

尽管这句话很短,但是实际的语料库却长了数千个单词,因此我们不可能一次全部输入。我们将要把语料库分成较短长度的序列。在上面的示例中,如果我们想将语料库划分为序列长度为5的批次,则将获得以下序列:

["Machine", "learning", "is", "a", "field"],

["of", "computer", "science", "that", "gives"],

["computers", "the", "ability", "to", "learn"],

["without", "being", "explicitly", "programmed", EOS]

  • 生成作为输入序列偏移一的批次

在语言建模中,监督数据是单词序列中的下一个单词。因此,我们要生成序列,这些序列是输入序列的偏移量之一。 在上面的示例中,我们将获得以下序列,我们可以训练模型进行预测

["learning", "is", "a", "field", "of"],

["computer", "science", "that", "gives", "computers"],

["the", "ability", "to", "learn", "without"],

["being", "explicitly", "programmed", EOS, EOS]

  这是用于创建迭代器的代码:

train_iter, valid_iter, test_iter = data.BPTTIterator.splits(
    (train, valid, test),
    batch_size=32,
    bptt_len=30, # this is where we specify the sequence length
    device=0,
    repeat=False)

与往常一样,最好查看实际发生的情况。

>>> b = next(iter(train_iter)); vars(b).keys()
dict_keys(['batch_size', 'dataset', 'train', 'text', 'target'])

我们看到我们有一个从未明确要求的属性:target。我们希望它是目标序列

>>> b.text[:5, :3]
Variable containing:
     9    953      0
    10    324   5909
     9     11  20014
    12   5906     27
  3872  10434      2
 
>>> b.target[:5, :3]
Variable containing:
    10    324   5909
     9     11  20014
    12   5906     27
  3872  10434      2
  3892      3  10780

注意,文本和目标的第一个维度是序列,第二个是批处理。我们看到目标确实是原始文本偏移了1(向下移动了1)。这意味着我们拥有开始训练语言模型所需的一切!

3.训练语言模型

使用上述迭代器,可以轻松地训练语言模型。

首先,我们需要准备模型。我们将从PyTorch中示例借用并自定义模型

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable as V
 
class RNNModel(nn.Module):
    def __init__(self, ntoken, ninp,
                 nhid, nlayers, bsz,
                 dropout=0.5, tie_weights=True):
        super(RNNModel, self).__init__()
        self.nhid, self.nlayers, self.bsz = nhid, nlayers, bsz
        self.drop = nn.Dropout(dropout)
        self.encoder = nn.Embedding(ntoken, ninp)
        self.rnn = nn.LSTM(ninp, nhid, nlayers, dropout=dropout)
        self.decoder = nn.Linear(nhid, ntoken)
        self.init_weights()
        self.hidden = self.init_hidden(bsz) # the input is a batched consecutive corpus
                                            # therefore, we retain the hidden state across batches
 
    def init_weights(self):
        initrange = 0.1
        self.encoder.weight.data.uniform_(-initrange, initrange)
        self.decoder.bias.data.fill_(0)
        self.decoder.weight.data.uniform_(-initrange, initrange)
 
    def forward(self, input):
        emb = self.drop(self.encoder(input))
        output, self.hidden = self.rnn(emb, self.hidden)
        output = self.drop(output)
        decoded = self.decoder(output.view(output.size(0)*output.size(1), output.size(2)))
        return decoded.view(output.size(0), output.size(1), decoded.size(1))
 
    def init_hidden(self, bsz):
        weight = next(self.parameters()).data
        return (V(weight.new(self.nlayers, bsz, self.nhid).zero_().cuda()),
                V(weight.new(self.nlayers, bsz, self.nhid).zero_()).cuda())
  
    def reset_history(self):
        self.hidden = tuple(V(v.data) for v in self.hidden)

 

语言模型本身很简单:它采用一系列单词标记,将它们嵌入,通过LSTM进行处理,然后针对每个输入单词在下一个单词上发出概率分布。我们做了一些细微的修改,例如将隐藏状态保存在模型对象中,并添加了重置历史记录方法。我们需要保留历史记录的原因是因为整个数据集都是连续的语料库,这意味着我们要保留一批中序列之间的隐藏状态。当然,我们不可能保留整个历史记录(这太昂贵了),因此我们将在训练期间定期重置历史记录。

要使用预先计算的词嵌入,我们需要显式传递嵌入矩阵的初始权重。权重包含在词汇表的vectors属性中。

weight_matrix = TEXT.vocab.vectors
model = RNNModel(weight_matrix.size(0),
        weight_matrix.size(1), 200, 1, BATCH_SIZE)
 
model.encoder.weight.data.copy_(weight_matrix)
model.cuda()

 

 现在我们可以开始训练语言模型了。我们将在此处使用Adam优化器。对于损失,我们将使用该nn.CrossEntropyLoss函数。这种损失将正确类别的索引作为基本事实,而不是一个热点。不幸的是,它仅需要尺寸为2或4的张量,因此我们需要做一些重塑。

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3, betas=(0.7, 0.99))
n_tokens = weight_matrix.size(0)

 我们将编写训练循环

from tqdm import tqdm 
def train_epoch(epoch):
"""One epoch of a training loop"""
    epoch_loss = 0
    for batch in tqdm(train_iter):
    # reset the hidden state or else the model will try to backpropagate to the
    # beginning of the dataset, requiring lots of time and a lot of memory
         model.reset_history()
 
    optimizer.zero_grad()
 
    text, targets = batch.text, batch.target
    prediction = model(text)
    # pytorch currently only supports cross entropy loss for inputs of 2 or 4 dimensions.
    # we therefore flatten the predictions out across the batch axis so that it becomes
    # shape (batch_size * sequence_length, n_tokens)
    # in accordance to this, we reshape the targets to be
    # shape (batch_size * sequence_length)
    loss = criterion(prediction.view(-1, n_tokens), targets.view(-1))
    loss.backward()
 
    optimizer.step()
 
    epoch_loss += loss.data[0] * prediction.size(0) * prediction.size(1)
 
    epoch_loss /= len(train.examples[0].text)
 
    # monitor the loss
    val_loss = 0
    model.eval()
    for batch in valid_iter:
        model.reset_history()
        text, targets = batch.text, batch.target
        prediction = model(text)
        loss = criterion(prediction.view(-1, n_tokens), targets.view(-1))
        val_loss += loss.data[0] * text.size(0)
    val_loss /= len(valid.examples[0].text)
 
    print('Epoch: {}, Training Loss: {:.4f}, Validation Loss: {:.4f}'.format(epoch, epoch_loss, val_loss))

 run

n_epochs = 2
for epoch in range(1, n_epochs + 1):
    train_epoch(epoch)

了解语言模型的损失与质量之间的对应关系非常困难,因此,最好定期检查语言模型的输出。这可以通过编写一些自定义代码以根据vocab将整数映射回单词来完成:

def word_ids_to_sentence(id_tensor, vocab, join=None):
    """Converts a sequence of word ids to a sentence"""
    if isinstance(id_tensor, torch.LongTensor):
        ids = id_tensor.transpose(0, 1).contiguous().view(-1)
    elif isinstance(id_tensor, np.ndarray):
        ids = id_tensor.transpose().reshape(-1)
    batch = [vocab.itos[ind] for ind in ids] # denumericalize
    if join is None:
        return batch
    else:
        return join.join(batch)

可以这样运行:

rrs = model(b.text).cpu().data.numpy()
word_ids_to_sentence(np.argmax(arrs, axis=2), TEXT.vocab, join=' ')

将结果限制为前几个单词,我们得到如下结果:

'<unk>   <eos> = = ( <eos>   <eos>   = = ( <unk> as the <unk> @-@ ( <unk> species , <unk> a <unk> of the <unk> ( the <eos> was <unk> <unk> <unk> to the the a of the first " , the , <eos>   <eos> reviewers were t'
很难评估质量,但是很明显,我们将需要做更多的工作或培训才能使语言模型正常工作。

4。结论

希望本教程提供了有关如何使用torchtext进行语言建模的基本见解,以及torchtext的一些更高级的功能,例如内置数据集,自定义标记器和预训练的单词嵌入。

在本教程中,我们使用了非常基本的语言模型,但是有许多最佳实践可以显着提高性能。