[TorchText]使用

只是教程的搬运工-.-

Field的使用

Torchtext采用声明式方法加载数据,需要先声明一个Field对象,这个Field对象指定你想要怎么处理某个数据,each Field has its own Vocab class。

  • tokenize传入一个函数,表示如何将文本str变成token
  • sequential表示是否切分数据,如果数据已经是序列化的了而且是数字类型的,则应该传递参数use_vocab = Falsesequential = False
    除了上面提到的关键字参数之外,Field类还允许用户指定特殊标记(用于标记词典外词语的unk_token,用于填充的pad_token,用于句子结尾的eos_token以及用于句子开头的可选的init_token)。设置将第一维是batch还是sequence(第一维默认是sequence),并选择是否允许在运行时决定序列长度还是预先就决定好,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)

使用spacy进行tokenizer,

import spacy
spacy_en = spacy.load('en')

def tokenizer(text): # create a tokenizer function
    return [tok.text for tok in spacy_en.tokenizer(text)]

TEXT = data.Field(sequential=True, tokenize=tokenizer, lower=True)
LABEL = data.Field(sequential=False, use_vocab=False)

构建Dataset

Fields知道怎么处理原始数据,现在我们需要告诉Fields去处理哪些数据。这就是我们需要用到Dataset的地方。Torchtext中有各种内置Dataset,用于处理常见的数据格式。 对于csv/tsv文件,TabularDataset类很方便。 以下是我们如何使用TabularDataset从csv文件读取数据的示例:

from torchtext.data import TabularDataset

tv_datafields = [("id", None), # 我们不会需要id,所以我们传入的filed是None
                 ("comment_text", TEXT), ("toxic", LABEL),
                 ("severe_toxic", LABEL), ("threat", LABEL),
                 ("obscene", LABEL), ("insult", LABEL),
                 ("identity_hate", LABEL)]
trn, vld = TabularDataset.splits(
               path="data", # 数据存放的根目录
               train='train.csv', validation="valid.csv",
               format='csv',
               skip_header=True, # 如果你的csv有表头, 确保这个表头不会作为数据处理
               fields=tv_datafields)

tst_datafields = [("id", None), # 我们不会需要id,所以我们传入的filed是None
                  ("comment_text", TEXT)]
tst = TabularDataset(
           path="data/test.csv", # 文件路径
           format='csv',
           skip_header=True, # 如果你的csv有表头, 确保这个表头不会作为数据处理
           fields=tst_datafields)
  • 我们传入(name,field)对的列表作为fields参数。我们传入的fields必须与列的顺序相同。对于我们不使用的列,我们在fields的位置传入一个None
  • splits方法通过应用相同的处理为训练数据和验证数据创建Dataset。 它也可以处理测试数据,但由于测试数据与训练数据和验证数据有不同的格式,因此我们创建了不同的Dataset。
  • 数据集大多可以和list一样去处理。 为了理解这一点,我们看看Dataset内部是怎么样的。 数据集可以像list一样进行索引和迭代,所以让我们看看第一个元素是什么样的:
>>> 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']

在一个TabularDataset里面直接指定train,test,validation,好像更为方便一些。

from torchtext import data
train, val, test = data.TabularDataset.splits(
        path='./data/', train='train.tsv',
        validation='val.tsv', test='test.tsv', format='tsv',
        fields=[('Text', TEXT), ('Label', LABEL)])

词表

Torchtext将单词映射为整数,但必须告诉它应该处理的全部单词。 在我们的例子中,我们可能只想在训练集上建立词汇表,所以我们运行代码:TEXT.build_vocab(trn)。这使得torchtext遍历训练集中的所有元素,检查TEXT字段的内容,并将其添加到其词汇表中。Torchtext有自己的Vocab类来处理词汇。Vocab类在stoi属性中包含从word到id的映射,并在其itos属性中包含反向映射。 除此之外,它可以为word2vec等预训练的embedding自动构建embedding矩阵。Vocab类还可以使用像max_size和min_freq这样的选项来表示词汇表中有多少单词或单词出现的次数。未包含在词汇表中的单词将被转换成<unk>。
TEXT.build_vocab(train, vectors="glove.6B.100d"),可以给vectors直接传入一个字符串类型的,那么会自动下载你需要的词向量,存放的位置是./.vector_cache 的文件夹下,或者可以使用类vocab.Vectors指定你自己的词向量。

Note you can directly pass in a string and it will download pre-trained word vectors and load them for you. You can also use your own vectors by using this class vocab.Vectors. The downloaded word embeddings will stay at ./.vector_cache folder. I have not yet discovered a way to specify a custom location to store the downloaded vectors (there should be a way right?).

将预训练的词向量加载到模型中。

from torchtext import data
TEXT.build_vocab(train, vectors="glove.6B.100d")
vocab = TEXT.vocab
self.embed = nn.Embedding(len(vocab), emb_dim)
self.embed.weight.data.copy_(vocab.vectors)

从词表变回单词

先安装工具,先从 GitHub上git clone代码然后进入目录安装,因为pip安装的不是最新版本

cd revtok/
python setup.py install

使用时候只需要使用可翻转的Field代替原来的Field就可以创建双向的转换的Vocb了。

from torchtext import data
TEXT = data.ReversibleField(sequential=True, lower=True, include_lengths=True)
...
for data in valid_iter:
        (x, x_lengths), y = data.Text, data.Description
        orig_text = TEXT.reverse(x.data)

对于预训练单词表中没有的单词我们可以随机进行初始化。

  • 在构建Field的时候设置include_lengths字段为True可以在返回minibatch的时候同时返回一个表示每个句子长度的list。

include_lengths: Whether to return a tuple of a padded minibatch and
a list containing the lengths of each examples, or just a padded
minibatch. Default: False.

随机初始化未知单词。

def init_emb(vocab, init="randn", num_special_toks=2):
    emb_vectors = vocab.vectors
    sweep_range = len(vocab)
    running_norm = 0.
    num_non_zero = 0
    total_words = 0
    for i in range(num_special_toks, sweep_range):
        if len(emb_vectors[i, :].nonzero()) == 0:
            # std = 0.05 is based on the norm of average GloVE 100-dim word vectors
            if init == "randn":
                torch.nn.init.normal(emb_vectors[i], mean=0, std=0.05)
        else:
            num_non_zero += 1
            running_norm += torch.norm(emb_vectors[i])
        total_words += 1
    logger.info("average GloVE norm is {}, number of known words are {}, total number of words are {}".format(
        running_norm / num_non_zero, num_non_zero, total_words))

构建迭代器

在torchvision和PyTorch中,数据的处理和批处理由DataLoaders处理。 出于某种原因,torchtext相同的东西又命名成了Iterators。 基本功能是一样的,但我们将会看到,Iterators具有一些NLP特有的便捷功能。

  • 对于验证集和训练集合使用BucketIterator.splits(),目的是自动进行shuffle和padding,并且为了训练效率期间,尽量把句子长度相似的shuffle在一起。
  • 对于测试集用Iterator,因为不用sort
  • sort 是对全体数据按照升序顺序进行排序,而sort_within_batch仅仅对一个batch内部的数据进行排序。
  • sort_within_batch参数设置为True时,按照sort_key按降序对每个小批次内的数据进行降序排序。当你想对padded序列使用pack_padded_sequence转换为PackedSequence对象时,这是必需的。
  • 注意sortshuffle默认只是对train=True字段进行的,但是train字段默认是True。所以测试集合可以这么写testIter = Iterator(tst, batch_size = 64, device =-1, train=False)写法等价于下面的一长串写法。
  • repeat 是否连续的训练无数个batch ,默认是False
  • device 可以是torch.device
from torchtext.data import Iterator, BucketIterator

train_iter, val_iter = BucketIterator.splits((trn, vld), 
                                             # 我们把Iterator希望抽取的Dataset传递进去
                                             batch_sizes=(25, 25),
                                             device=-1, 
                                             # 如果要用GPU,这里指定GPU的编号
                                             sort_key=lambda x: len(x.comment_text), 
                                             # BucketIterator 依据什么对数据分组
                                             sort_within_batch=False,
                                             repeat=False)
                                             # repeat设置为False,因为我们想要包装这个迭代器层。
test_iter = Iterator(tst, batch_size=64, 
                     device=-1, 
                     sort=False, 
                     sort_within_batch=False, 
                     repeat=False)

BucketIterator是torchtext最强大的功能之一。它会自动将输入序列进行shuffle并做bucket。这个功能强大的原因是——正如我前面提到的——我们需要填充输入序列使得长度相同才能批处理。 例如,序列

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

会需要pad成

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

填充量由batch中最长的序列决定。因此,当序列长度相似时,填充效率最高。BucketIterator会在在后台执行这些操作。需要注意的是,你需要告诉BucketIterator你想在哪个数据属性上做bucket。在我们的例子中,我们希望根据comment_text字段的长度进行bucket处理,因此我们将其作为关键字参数传入sort_key = lambda x: len(x.comment_text)

train_iter, val_iter, test_iter = data.BucketIterator.splits(
        (train, val, test), sort_key=lambda x: len(x.Text),
        batch_sizes=(32, 256, 1), device=-1)

BucketIteratorIterator的区别是,BucketIterator尽可能的把长度相似的句子放在一个batch里面。

Defines an iterator that batches examples of similar lengths together

封装迭代器

目前,迭代器返回一个名为torchtext.data.Batch的自定义数据类型。Batch类具有与Example类相似的API,将来自每个字段的一批数据作为属性。

>>> train_iter
[torchtext.data.batch.Batch of size 25]
    [.comment_text]:[torch.LongTensor of size 494x25]
    [.toxic]:[torch.LongTensor of size 25]
    [.severe_toxic]:[torch.LongTensor of size 25]
    [.threat]:[torch.LongTensor of size 25]
    [.obscene]:[torch.LongTensor of size 25]
    [.insult]:[torch.LongTensor of size 25]
    [.identity_hate]:[torch.LongTensor of size 25]
>>> train_iter.__dict__.keys()
dict_keys(['batch_size', 'dataset', 'fields', 'comment_text', 'toxic', 'severe_toxic', 'threat', 'obscene', 'insult', 'identity_hate'])
>>> train_iter.comment_text
tensor([[  15,  606,  280,  ...,   15,   63,   15],
        [ 360,  693,   18,  ...,   29,    4,    2],
        [  45,  584,   14,  ...,   21,  664,  645],
        ...,
        [   1,    1,    1,  ...,   84,    1,    1],
        [   1,    1,    1,  ...,  118,    1,    1],
        [   1,    1,    1,  ...,   15,    1,    1]])
>>> train_iter.toxic
tensor([ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
         0,  0,  1,  0,  1,  0,  1,  0,  0,  0,  0])

不幸的是,这种自定义数据类型使得代码重用变得困难(因为每次列名发生变化时,我们都需要修改代码),并且使torchtext在某些情况(如torchsample和fastai)下很难与其他库一起使用。
我希望这可以在未来得到优化(我正在考虑提交PR,如果我可以决定API应该是什么样的话),但同时,我们使用简单的封装来使batch易于使用。

具体来说,我们将把batch转换为形式为(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 # 传入自变量x列表和因变量y列表

    def __iter__(self):
        for batch in self.dl:
            x = getattr(batch, self.x_var) # 在这个封装中只有一个自变量

            if self.y_vars is not None: # 把所有因变量cat成一个向量
                temp = [getattr(batch, feat).unsqueeze(1) for feat in self.y_vars]
                y = torch.cat(temp, 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)

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

>>> next(train_dl.__iter__())
(tensor([[  15,   15,   15,  ...,  375,  354,   44],
         [ 601,  657,  360,  ...,   27,   63,  739],
         [ 242,   22,   45,  ...,  526,    4,    3],
         ...,
         [   1,    1,    1,  ...,    1,    1,    1],
         [   1,    1,    1,  ...,    1,    1,    1],
         [   1,    1,    1,  ...,    1,    1,    1]]),
 tensor([[ 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.],
         [ 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.],
         [ 1.,  1.,  0.,  1.,  1.,  0.],
         [ 0.,  0.,  0.,  0.,  0.,  0.]]))

训练模型

我们将使用一个简单的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__() 
        # 词汇量是 len(TEXT.vocab)
        self.embedding = nn.Embedding(len(TEXT.vocab), emb_dim)
        self.encoder = nn.LSTM(emb_dim, hidden_dim, num_layers=1)
        self.linear_layers = []
        # 中间fc层
        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, :, :]  # 选择最后一个output
        for layer in self.linear_layers:
          feature = layer(feature)
        preds = self.predictor(feature)
        return preds

em_sz = 100
nh = 500
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() # 训练模式
    for x, y in tqdm.tqdm(train_dl): # 由于我们的封装,我们可以直接对数据进行迭代
        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)

    # 计算验证数据的误差
    val_loss = 0.0
    model.eval() # 评估模式
    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()
    # 模型的实际输出是logit,所以再经过一个sigmoid函数
    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)

对于mask的数据的支持

RNN需要将PyTorch 变量打包成一个padded 序列。

  • sort_within_batch=True ,如果想使用PyTorch里面的pack_padded sequence时候必须要设置为TRUE
  • sort_key 是对数据集合在一个batch的的排序函数。
  • repeat 是否使用多个epoch。

repeat – Whether to repeat the iterator for multiple epochs. Default: False.

TEXT = data.ReversibleField(sequential=True, lower=True, include_lengths=True)
train, val, test = data.TabularDataset.splits(
        path='./data/', train='train.tsv',
        validation='val.tsv', test='test.tsv', format='tsv',
        fields=[('Text', TEXT), ('Label', LABEL)])
        
train_iter, val_iter, test_iter = data.Iterator.splits(
        (train, val, test), sort_key=lambda x: len(x.Text), 
        batch_sizes=(32, 256, 256), device=args.gpu, 
        sort_within_batch=True, repeat=False)

在模型中这么使用

def forward(self, input, lengths=None):
        embed_input = self.embed(input)

        packed_emb = embed_input
        if lengths is not None:
            lengths = lengths.view(-1).tolist()
            packed_emb = nn.utils.rnn.pack_padded_sequence(embed_input, lengths)

        output, hidden = self.encoder(packed_emb)  # embed_input

        if lengths is not None:
            output,_=nn.utils.rnn.pad_packed_sequence(output)

在主循环中可以以这样的方式传递数据,未打包之前

# Note this loop will go on FOREVER
for val_i, data in enumerate(train_iter):
  (x, x_lengths), y = data.Text, data.Description
  output = model(x, x_lengths)
  
  # terminate condition, when loss converges or it reaches 50000 iterations
  if loss converges or val_i == 50000:
    break

如果我们想训练一定的epoch,我们可以设置,repeat=False,这样我们的数据会训练10个epoch然后停止下来。

# Note this loop will stop when training data is traversed once
epochs = 10

for epoch in range(epochs):
  for data in train_iter:
    (x, x_lengths), y = data.Text, data.Description
    # model running...

如果我们提前不知道应该在多少个epoch的时候停止,比如当精度达到某个值的时候停止,我们就可以设置repeat = True,然后在代码里面break.

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

推荐阅读更多精彩内容