资料来源于http://anie.me/On-Torchtext/,通过这个链接可以直接看到教程资料

另一个教程写的也还可以,http://mlexplained.com/2018/02/08/a-comprehensive-tutorial-to-torchtext/

这位大佬把教程提炼了一下,感觉非常简洁:https://blog.csdn.net/u012436149/article/details/79310176

这篇文章主要是针对http://mlexplained.com/2018/02/08/a-comprehensive-tutorial-to-torchtext/这个教程进行翻译

 

在开始训练模型之前,您必须:

  1. 从磁盘读取数据
  2. 标记文本
  3. 创建从单词到唯一整数的映射
  4. 将文本转换为整数列表
  5. 以您的深度学习框架所需的任何格式加载数据
  6. 填充文本,以使所有序列具有相同的长度,因此您可以批量处理它们

Torchtext  是一个使上述所有处理变得更加容易的库。尽管它仍相对较新,但其便利的功能(尤其是在批处理和加载方面)使它成为值得学习和使用的库。

在本文中,我将演示如何使用torchtext从头开始构建和训练文本分类器。为了使本教程切合实际,我将使用来自Kaggle竞赛的一小部分数据数据和代码可在GitHub存储库找到,因此可以随时对其进行克隆和后续操作。

1.概述

Torchtext遵循以下基本公式将数据转换为神经网络的有效输入:

Torchtext以文本文件,csv / tsv文件,json文件和目录的形式(截至目前)接收原始数据,并将其转换为数据集。数据集是经过预处理的数据块,具有各种字段,可以读取到内存中。它们是其他数据结构可以使用的处理数据的规范形式。

 

然后,Torchtext将数据集传递给迭代器。迭代器处理数字化,批处理,打包并将数据移至GPU。基本上,它完成了将数据传递到神经网络所需的所有繁重工作。

2.声明字段

Torchtext采用声明性的方式加载其数据:您告诉torchtext您希望数据看起来如何,然后torchtext会为您处理数据

您这样做的方法是声明一个 Field。该 Field 指定您希望如何处理某个(您猜到的)字段。让我们看一个例子.

from torchtext.data import Field
tokenize = lambda x: x.split()
TEXT = Field(sequential=True, tokenize=tokenize, lower=True)

LABEL = Field(sequential=False, use_vocab=False)

在toxic comment 分类数据集中,有两种字段:comment text 和 labels (toxic, severe toxic, obscene, threat, insult, and identity hate)。

让我们先来看一下LABEL字段,因为它很简单。默认情况下,所有字段都希望输入一个单词序列,并且它们期望稍后再建立一个从单词到整数的映射(此映射称为vocab,稍后我们将了解如何创建它)。如果要传递的字段在默认情况下已经数字化并且不是顺序的,那么应该传入use_vocab=False and sequential=False。

对于 comment text, 我们传入希望字段用作关键字参数的预处理。我们给它提供我们希望该字段使用的tokenizer ,告诉它将输入转换为小写,还告诉它输入是顺序的。(For the comment text, we pass in the preprocessing we want the field to do as keyword arguments. We give it the tokenizer we want the field to use, tell it to convert the input to lowercase, and also tell it the input is sequential.)

除了上面提到的关键字参数外,Field类还允许用户指定特殊标记(unk_token用于词汇外单词,pad_token用于填充,eos_token句子结尾以及init_token用于开头的可选标记),选择是将第一维设为批处理还是序列(默认情况下,第一维为序列),然后选择是允许序列长度在运行时确定还是预先确定。幸运的是,Field类的文档字符串编写得相对好,因此,如果需要一些高级预处理,则应参考它们以获取更多信息。

字段类位于torchtext的中心,这使得预处理如此容易。除了标准字段类之外,这是当前可用字段的列表(及其用例):

名称 描述 用例
Field 定义预处理和后处理的常规字段 非文本字段和不需要将整数映射回单词的文本字段
ReversibleField 字段的扩展,允许单词ID反向映射到单词 如果要将整数映射回自然语言,则为文本字段(例如在语言建模的情况下)
NestedField 一个将未标记的文本处理为一组较小字段的字段 一个将未标记的文本处理为一组较小字段的字段

LabelField

(New!)

带有sequential=False and no <unk> token的常规字段。新添加在torchtext github存储库的master分支上,尚未发布。 文本分类中的标签字段。

 

 

 

 

 

 

 

 

 

 

 

3.构造数据集

字段知道给定原始数据时的处理方式。现在,我们需要告诉字段应该使用哪些数据。这是我们使用数据集的地方。

Torchtext中有各种内置的数据集,用于处理常见的数据格式。对于csv / tsv文件,TabularDataset类很方便。这是我们使用TabularDataset从csv文件读取数据的方式:

from torchtext.data import TabularDataset

tv_datafields = [("id", None), # we won't be needing the id, so we pass in None as the field
                 ("comment_text", TEXT), ("toxic", LABEL),
                 ("severe_toxic", LABEL), ("threat", LABEL),
                 ("obscene", LABEL), ("insult", LABEL),
                 ("identity_hate", LABEL)]
# train set, valid set
trn, vld
= TabularDataset.splits( path="data", # the root directory where the data lies train='train.csv', validation="valid.csv", format='csv', skip_header=True, # if your csv header has a header, make sure to pass this to ensure it doesn't get proceesed as data! fields=tv_datafields) # test set tst_datafields = [("id", None), # we won't be needing the id, so we pass in None as the field ("comment_text", TEXT)] tst = TabularDataset( path="data/test.csv", # the file path format='csv', skip_header=True, # if your csv header has a header, make sure to pass this to ensure it doesn't get proceesed as data! fields=tst_datafields)

对于TabularDataset,我们传入(name, field)对的列表作为fields参数,我们传入的字段必须与列相同。对于我们不使用的列,我们传入一个在其中field元素为None的元组。

splits方法通过应用相同的处理为训练和验证数据创建数据集。它还可以处理测试数据,但是由于输出的测试数据与训练和验证数据的格式不同,因此我们创建了不同的数据集。

数据集的处理方式与列表相同。要了解这一点,请深入了解我们的数据集是有益的。数据集可以像普通列表一样被索引和迭代,因此让我们看一下第一个元素是什么样的:

 

>>> trn[0]
torchtext.data.example.Example at 0x10d3ed3c8
>>> trn[0].__dict__.keys()
dict_keys(['comment_text', 'toxic', 'severe_toxic', 'threat', 'obscene', 'insult', 'identity_hate'])
>>> trn[0].comment_text[:3]
['explanation', 'why', 'the']

我们得到一个Example对象。Example对象将单个数据点的属性捆绑在一起。我们还看到文本已经为我们标记了,但尚未转换为整数。这是有道理的,因为我们尚未构建从单词到id的映射。构造此映射是我们的下一步。

Torchtext处理将单词映射到整数,但是必须告知它应该处理的所有单词。在我们的情况下,我们可能只想在训练集上构建词汇表,因此我们运行以下代码:

 

TEXT.build_vocab(trn)

这样,torchtext就可以遍历训练集中的所有元素,检查与该TEXT字段相对应的内容,并将单词注册在其词汇表中。Torchtext具有自己的称为Vocab的类,用于处理词汇表。Vocab类在其stoi属性中包含从单词到id的映射,在其属性中包含反向映射itos除此之外,它还可以使用各种预先训练的嵌入(例如word2vec)为您自动构建一个嵌入矩阵(有关详细信息,请参见另一篇教程)。Vocab类还可以使用诸如max_sizemin_freq决定词汇中有多少个单词,或单词必须在词汇中出现多少次。未包含在词汇将被转换成<UNK>,令牌代表“unknown”。

以下是当前可用的一组数据集及其接收的数据格式的列表:

Name Description Use Case
TabularDataset 将csv / tsv文件和json文件或Python字典的路径作为输入 任何涉及每个文本的标签的问题
 LanguageModelingDataset 将文本文件的路径作为输入 语言建模
TranslationDataset 采用每种语言的路径和文件扩展名。 

例如,如果文件是英语:“ hoge.en”,法语:“ hoge.fr”,path =“ hoge”,exts =(“ en”,“ fr”)

翻译
SequenceTaggingDataset 采取文件路径,输入序列和输出序列用制表符分隔 序列标记

 

 

 

 

 

 

 

 

 

 

 

 

 

 

现在,我们已经格式化了数据并将其读入内存,我们转到下一步:创建一个Iterator将数据传递给我们的模型。

4.构造迭代器

在torchvision和PyTorch中,数据的处理和批处理由DataLoaders处理。由于某种原因,torchtext已将执行完全相同操作的对象重命名为Iterators。基本功能是相同的,但是正如我们将看到的,迭代器具有一些NLP特有的便捷功能

以下是有关如何初始化训练,验证和测试数据的迭代器的代码。

from torchtext.data import Iterator, BucketIterator

train_iter, val_iter = BucketIterator.splits(
 (trn, vld), # we pass in the datasets we want the iterator to draw data from
 batch_sizes=(64, 64),
 device=-1, # if you want to use the GPU, specify the GPU number here
 sort_key=lambda x: len(x.comment_text), # the BucketIterator needs to be told what function it should use to group the data.
 sort_within_batch=False,
 repeat=False # we pass repeat=False because we want to wrap this Iterator layer.
)
test_iter = Iterator(tst, batch_size=64, device=-1, sort=False, sort_within_batch=False, repeat=False)

更新:该sort_within_batch参数设置为True时,将根据降序对每个小型批处理中的数据进行排序sort_key当您要使用pack_padded_sequence填充序列数据并将填充序列张量转换为PackedSequence对象时,这是必需的

BucketIterator是Torchtext最强大的功能之一。它会自动将输入序列洗牌并存储到类似长度的序列中。

 

正如我前面提到的那样,此功能强大的原因是-我们需要填充输入序列的长度相同,以实现批处理。例如,序列

[ [3, 15, 2, 7],
  [4, 1],
  [5, 5, 6, 8, 1] ]

需要填充才能成为

[[3,15,2,7,0],
  [4,1,0,0,0],
  [5,5,6,8,1]]

如您所见,所需的填充量由批处理中的最长序列决定。因此,当序列长度相似时,填充是最有效的。BucketIterator在后台执行所有这些操作。提醒您,您需要告诉BucketIterator您要存储数据的属性是什么。在我们的例子中,我们要基于comment_text字段的长度进行存储,因此我们将其作为关键字参数传入。有关其他参数的详细信息,请参见上面的代码。

对于测试数据,我们不希望对数据进行混洗,因为我们将在训练结束时输出预测。这就是为什么我们使用标准迭代器。

 

这是torchtext当前实现的迭代器的列表:

名称 描述 用例
Iterator 按照数据集的顺序遍历数据 测试数据或顺序重要的其他任何数据
 BucketIterator 将相似长度的序列合并在一起 文本分类,序列标记等(输入长度​​可变的用例)
 BPTTIterator 专为语言建模而构建的迭代器,它还会生成延迟一个时间步的输入序列。它还会改变BPTT(时间反向传播)的长度。该迭代器应有其自己的文章,因此在此我将省略其详细信息 语言建模

 

 

 

 

 

 

 

 

 

5.包装迭代器

当前,迭代器返回一个称为torchtext.data.Batch的自定义数据类型。Batch类具有与Example类型类似的API,每个字段中的一批数据作为属性。不幸的是,这种自定义数据类型使代码重用变得困难(因为每次列名更改时,我们都需要修改代码),并且在某些用例(例如torchsample和fastai)中,torchtext难以与其他库一起使用。

我希望以后会解决此问题(如果我可以决定API的外观,我正在考虑提交PR),但与此同时,我们将使用一个简单的包装程序来简化批次的使用。

具体而言,我们将批次转换为(x,y)形式的元组,其中x是自变量(模型的输入),而y是因变量(监管数据)。这是代码:

class BatchWrapper:
      def __init__(self, dl, x_var, y_vars):
            self.dl, self.x_var, self.y_vars = dl, x_var, y_vars # we pass in the list of attributes for x 

      def __iter__(self):
            for batch in self.dl:
                  x = getattr(batch, self.x_var) # we assume only one input in this wrapper

                  if self.y_vars is None: # we will concatenate y into a single tensor
                        y = torch.cat([getattr(batch, feat).unsqueeze(1) for feat in self.y_vars], dim=1).float()
                  else:
                        y = torch.zeros((1))

                  yield (x, y)

      def __len__(self):
            return len(self.dl)

train_dl = BatchWrapper(train_iter, "comment_text", ["toxic", "severe_toxic", "obscene", "threat", "insult", "identity_hate"])
valid_dl = BatchWrapper(val_iter, "comment_text", ["toxic", "severe_toxic", "obscene", "threat", "insult", "identity_hate"])
test_dl = BatchWrapper(test_iter, "comment_text", None)

我们在这里所做的就是将批处理对象转换为输入和输出的元组。

&amp;gt;&amp;gt;&amp;gt; next(train_dl.__iter__())
(Variable containing:
   606   354   334  ...     63    15    15
   693    63    55  ...      4   601    29
   584     4   520  ...    664   242    21
        ...          ⋱          ...
     1     1     1  ...      1     1    84
     1     1     1  ...      1     1   118
     1     1     1  ...      1     1    15
 [torch.LongTensor of size 494x25], Variable containing:
     0     0     0     0     0     0
     1     1     0     1     1     0
     1     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     1     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
     0     0     0     0     0     0
 [torch.FloatTensor of size 25x6])

这里没什么好看的。现在,我们终于准备好开始训练文本分类器了。

6.训练模型

我们将使用一个简单的LSTM来演示如何在我们构建的数据上训练文本分类器:

import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable

class SimpleLSTMBaseline(nn.Module):
    def __init__(self, hidden_dim, emb_dim=300, num_linear=1):
        super().__init__() # don't forget to call this!
        self.embedding = nn.Embedding(len(TEXT.vocab), emb_dim)
        self.encoder = nn.LSTM(emb_dim, hidden_dim, num_layers=1)
        self.linear_layers = []
        for _ in range(num_linear - 1):
            self.linear_layers.append(nn.Linear(hidden_dim, hidden_dim))
            self.linear_layers = nn.ModuleList(self.linear_layers)
        self.predictor = nn.Linear(hidden_dim, 6)

    def forward(self, seq):
        hdn, _ = self.encoder(self.embedding(seq))
        feature = hdn[-1, :, :]
        for layer in self.linear_layers:
          feature = layer(feature)
        preds = self.predictor(feature)
        return preds

em_sz = 100
nh = 500
nl = 3
model = SimpleBiLSTMBaseline(nh, emb_dim=em_sz)

现在,我们将编写训练循环。感谢我们所有的预处理,这非常简单。我们可以使用包装的Iterator进行迭代,将数据移至GPU并进行适当的数字化处理后,数据会自动传递给我们

import tqdm

opt = optim.Adam(model.parameters(), lr=1e-2)
loss_func = nn.BCEWithLogitsLoss()

epochs = 2

for epoch in range(1, epochs + 1):
    running_loss = 0.0
    running_corrects = 0
    model.train() # turn on training mode
    for x, y in tqdm.tqdm(train_dl): # thanks to our wrapper, we can intuitively iterate over our data!
        opt.zero_grad()

        preds = model(x)
        loss = loss_func(y, preds)
        loss.backward()
        opt.step()

        running_loss += loss.data[0] * x.size(0)

    epoch_loss = running_loss / len(trn)

    # calculate the validation loss for this epoch
    val_loss = 0.0
    model.eval() # turn on evaluation mode
    for x, y in valid_dl:
        preds = model(x)
        loss = loss_func(y, preds)
        val_loss += loss.data[0] * x.size(0)

    val_loss /= len(vld)
    print('Epoch: {}, Training Loss: {:.4f}, Validation Loss: {:.4f}'.format(epoch, epoch_loss, val_loss))

这里没有什么要解释的:这只是一个标准的训练循环。现在,让我们生成我们的预测

test_preds = []
for x, y in tqdm.tqdm(test_dl):
    preds = model(x)
    preds = preds.data.numpy()
    # the actual outputs of the model are logits, so we need to pass these values to the sigmoid function
    preds = 1 / (1 + np.exp(-preds))
    test_preds.append(preds)
    test_preds = np.hstack(test_preds)

最后,我们可以将预测写入到csv文件中。

import pandas as pd
df = pd.read_csv("data/test.csv")
for i, col in enumerate(["toxic", "severe_toxic", "obscene", "threat", "insult", "identity_hate"]):
    df[col] = test_preds[:, i]

df.drop("comment_text", axis=1).to_csv("submission.csv", index=False)

我们完成了!我们可以将该文件提交给Kaggle,尝试完善我们的模型,更改令牌生成器或任何我们喜欢的方式,并且只需对上面的代码进行一些更改即可

7.结论和进一步阅读

我希望本教程能够深入了解torchtext的用法和实用性。尽管该库仍然很新,并且存在许多粗糙的地方,但我相信torchtext是朝着标准化文本预处理迈出的重要一步,它将提高全世界NLP工作人员的工作效率。

如果您想查看torchtext用于语言建模的信息,已经上传了另一个教程,其中详细介绍了语言建模和BPTT迭代器。