Torchtext 詳細介紹 Part.1

Torchtext 詳細介紹 Part.1

作為一個nlp的菜鳥,一天到晚被文本的預處理搞得頭大,直到有一天我發現了torchtext這個東西, 以及發現了這篇介紹,我就順便把它翻譯出來,想看原文點這裡。


如果你曾經做過NLP的深度學習項目你就會知道預處理是有多麼的痛苦和乏味。在你訓練你的模型之前你必須做以下幾步:

  1. 從磁碟讀取數據
  2. 對文本分詞
  3. 給每一個單詞創建唯一整數標識
  4. 把文本轉化成整數序列
  5. 按照你的深度學習框架要求的形式載入數據
  6. 把所有序列pad成相同的長度,這樣你才能以batch的形式處理它們

Torchtext 這個庫可以讓上面的這些處理變得更加方便。儘管這個庫還比較新,但它使用起來非常方便——尤其在批處理和數據載入方面——這讓trochtext非常值得去學習。

在這篇文章中,我會展示如何用torchtext從頭構建和訓練一個文本分類器。為了讓這個教程更加切合實際,我會使用這個kaggle比賽的一小部分數據。數據和代碼可以在我的github上找到,所以你可以自由的clone和follow它。或者,如果你只是想看看最小的工作示例,你可以直接看github上的代碼。

你可以用以下命令來安裝torchtext

$ pip install --upgrade git+https://github.com/pytorch/text

1. 概述

Trochtext遵循以下的基本方式把數據轉化成你的神經網路可以使用的輸入形式:

Torchtext從txt文件、csv/tsv文件、json文件和某一目錄中(到現在為止有這麼多種)讀取原始數據,並將它們轉換為Datasets。Datasets僅僅是預處理的數據塊,通過各種欄位(fields)讀入內存。它們是其他數據結構可以使用的處理數據的標準形式。

然後torchtext將數據集傳遞給迭代器。 迭代器處理數字化,批處理,打包以及將數據移動到GPU。 基本上,它把將數據傳遞給神經網路的過程中所有繁重的工作都做了。

在下面的章節中,我們將看到這些過程在實際工作示例中是如何實現的。

2. 聲明欄位

Torchtext採用聲明式方法載入數據:你告訴torchtext你希望數據是什麼樣的,然後torchtext為你處理。

你要做的是聲明一個Field對象。 這個Field對象指定你想要怎麼處理某個欄位。我們來看一個例子:

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

在惡意評論分類數據集中,有兩種欄位:評論文本(TEXT)和標籤(LABEL)(惡意,嚴重惡意,淫穢,威脅,侮辱和身份仇恨)。

我們首先看一下LABEL欄位,因為它更簡單。默認情況下,所有欄位都需要一系列單詞進入,並且期望在後面建立一個從單詞到整數的映射(這個映射稱為辭彙表,我們之後會講到它是如何被創建的)。如果您傳遞的欄位是默認被數字化的,並且不是順序序列,則應該傳遞參數use_vocab = Falsesequential = False

對於評論文本,我們把想對欄位做的預處理以傳遞關鍵字參數的形式傳入。我們給欄位一個分詞器,告訴它把輸入都轉換為小寫,告訴它輸入是順序序列。

除了上面提到的關鍵字參數之外,Field類還允許用戶指定特殊標記(用於標記詞典外詞語的unk_token,用於填充的pad_token,用於句子結尾的eos_token以及用於句子開頭的可選的init_token)。設置將第一維是batch還是sequence(第一維默認是sequence),並選擇是否允許在運行時決定序列長度還是預先就決定好。幸運的是,Field類的文檔寫得相對較好,所以如果你還需要一些高級的預處理,你可以參考這個文檔以獲取更多信息。

Field類是torchtext的中心,並且使預處理變得更方便。除標準欄位類外,當前可用的其他欄位(以及它們的用例)列表如下:

3. 構建Dataset

Fields知道怎麼處理原始數據,現在我們需要告訴fields去處理哪些數據。這就是我們需要用到Dataset的地方。

Torchtext中有各種內置Dataset,用於處理常見的數據格式。 對於csv/tsv文件,TabularDataset類很方便。 以下是我們如何使用TabularDataset從csv文件讀取數據的示例:

from torchtext.data import TabularDatasettv_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)

對於TabularDataset,我們傳入(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]

我們得到一個Example類的對象。 Example對象將單個數據的屬性放在一起。 我們也看到文本已經被切分,但還沒有被轉換為整數。因為我們還沒有構建從單詞到id的映射。我們下一步就來構建這個映射。

Torchtext將單詞映射為整數,但必須告訴它應該處理的全部單詞。 在我們的例子中,我們可能只想在訓練集上建立辭彙表,所以我們運行以下代碼:

TEXT.build_vocab(trn)

這使得torchtext遍歷訓練集中的所有元素,檢查TEXT欄位的內容,並將其添加到其辭彙表中。Torchtext有自己的Vocab類來處理辭彙。Vocab類在stoi屬性中包含從word到id的映射,並在其itos屬性中包含反向映射。 除此之外,它可以為word2vec等預訓練的embedding自動構建embedding矩陣。Vocab類還可以使用像max_sizemin_freq這樣的選項來表示辭彙表中有多少單詞或單詞出現的次數。未包含在辭彙表中的單詞將被轉換成<unk>

現在我們已經將數據格式化並讀入內存中,下一步是:創建一個迭代器將數據傳遞給我們的模型。

4. 構建迭代器

在torchvision和PyTorch中,數據的處理和批處理由DataLoaders處理。 出於某種原因,torchtext相同的東西又命名成了Iterators。 基本功能是一樣的,但我們將會看到,Iterators具有一些NLP特有的便捷功能。

以下是如何初始化列車迭代器,驗證和測試數據的代碼。

from torchtext.data import Iterator, BucketIteratortrain_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)

sort_within_batch參數設置為True時,按照sort_key按降序對每個小批次內的數據進行排序。當你想對padded序列使用pack_padded_sequence轉換為PackedSequence對象時,這是必需的。

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處理,因此我們將其作為關鍵字參數傳入。 有關其他參數的詳細信息,請參閱上面的代碼。

對於測試數據,我們不想洗牌數據,因為我們將在訓練結束時輸出預測結果。 這就是我們使用標準迭代器的原因。

以下是torchtext當前實現的iterators器列表:

5. 封裝迭代器

目前,迭代器返回一個名為torchtext.data.Batch的自定義數據類型。Batch類具有與Example類相似的API,將來自每個欄位的一批數據作為屬性。

>>> batch[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]>>> batch.__dict__.keys()dict_keys([batch_size, dataset, fields, comment_text, toxic, severe_toxic, threat, obscene, insult, identity_hate])>>> batch.comment_texttensor([[ 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]])>>> batch.toxictensor([ 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.]]))

這裡沒什麼特別的。現在,我們終於準備好開始訓練一個文本分類器了。

6. 訓練模型

我們將使用一個簡單的LSTM來演示如何根據我們構建的數據來訓練文本分類器:

import torch.nn as nnimport torch.nn.functional as Fimport torch.optim as optimfrom torch.autograd import Variableclass 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 predsem_sz = 100nh = 500model = SimpleBiLSTMBaseline(nh, emb_dim=em_sz)

現在,我們將編寫訓練循環。 多虧我們所有的預處理,讓這變得非常簡單非常簡單。我們可以使用我們包裝的Iterator進行迭代,並且數據在移動到GPU和適當數字化後將自動傳遞給我們。

import tqdmopt = optim.Adam(model.parameters(), lr=1e-2)loss_func = nn.BCEWithLogitsLoss()epochs = 2for 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 pddf = 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迭代器。


看完Part.2後再翻譯上傳。


推薦閱讀:

利用Flask搭建Pytorch深度學習服務
Pytorch源碼與運行原理淺析--網路篇(一)
【筆記】Finding Tiny Faces
Python_面向對象基礎

TAG:自然語言處理 | PyTorch | 數據挖掘 |