[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.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容