只是教程的搬運工-.-
Field的使用
Torchtext采用聲明式方法加載數據,需要先聲明一個Field對象,這個Field對象指定你想要怎么處理某個數據,each Field has its own Vocab class。
-
tokenize
傳入一個函數,表示如何將文本str變成token -
sequential
表示是否切分數據,如果數據已經是序列化的了而且是數字類型的,則應該傳遞參數use_vocab = False
和sequential = 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對象時,這是必需的。 - 注意
sort
和shuffle
默認只是對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)
BucketIterator
和Iterator
的區別是,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
.