1. 程式人生 > 程式設計 >使用pytorch和torchtext進行文字分類的例項

使用pytorch和torchtext進行文字分類的例項

文字分類是NLP領域的較為容易的入門問題,本文記錄我自己在做文字分類任務以及復現相關論文時的基本流程,絕大部分操作都使用了torch和torchtext兩個庫。

1. 文字資料預處理

首先資料儲存在三個csv檔案中,分別是train.csv,valid.csv,test.csv,第一列儲存的是文字資料,例如情感分類問題經常是使用者的評論review,例如imdb或者amazon資料集。第二列是情感極性polarity,N分類問題的話就有N個值,假設值得範圍是0~N-1。

下面是很常見的文字預處理流程,英文文字的話不需要分詞,直接按空格split就行了,這裡只會主要說說第4點。

1、去除非文字部分

2、分詞

3、去除停用詞

4、對英文單詞進行詞幹提取(stemming)和詞型還原(lemmatization)

5、轉為小寫

6、特徵處理

Bag of Words

Tf-idf

N-gram

Word2vec

詞幹提取和詞型還原

from nltk.stem import SnowballStemmer
stemmer = SnowballStemmer("english") # 選擇語言
from nltk.stem import WordNetLemmatizer 
wnl = WordNetLemmatizer()

SnowballStemmer較為激進,轉換有可能出現錯誤,這裡較為推薦使用WordNetLemmatizer,它一般只在非常肯定的情況下才進行轉換,否則會返回原來的單詞。

stemmer.stem('knives')
# knive
wnl.lemmatize('knives')
# knife

因為我沒有系統學習和研究過NLTK的程式碼,所以就不多說了,有興趣的可以自己去閱讀NLTK的原始碼。

2. 使用torchtext載入文字資料

本節主要是用的模組是torchtext裡的data模組,處理的資料同上一節所描述。

首先定義一個tokenizer用來處理文字,比如分詞,小寫化,如果你已經根據上一節的詞幹提取和詞型還原的方法處理過文本里的每一個單詞後可以直接分詞就夠了。

tokenize = lambda x: x.split()

或者也可以更保險點,使用spacy庫,不過就肯定更耗費時間了。

import spacy

spacy_en = spacy.load('en')
def tokenizer(text):
 return [toke.text for toke in spacy_en.tokenizer(text)]

然後要定義Field,至於Field是啥,你可以簡單地把它理解為一個能夠載入、預處理和儲存文字資料和標籤的物件。我們可以用它根據訓練資料來建立詞表,載入預訓練的Glove詞向量等等。

def DataLoader():
 tokenize = lambda x: x.split()
 # 使用者評論,include_lengths設為True是為了方便之後使用torch的pack_padded_sequence
 REVIEW = data.Field(sequential=True,tokenize=tokenize,include_lengths=True)
 # 情感極性
 POLARITY = data.LabelField(sequential=False,use_vocab=False,dtype = torch.long)
 # 假如train.csv檔案並不是只有兩列,比如1、3列是review和polarity,2列是我們不需要的資料,
 # 那麼就要新增一個全是None的元組, fields列表儲存的Field的順序必須和csv檔案中每一列的順序對應,
 # 否則review可能就載入到polarity Field裡去了
 fields = [('review',REVIEW),(None,None),('polarity',POLARITY)]
 
 # 載入train,valid,test資料
 train_data,valid_data,test_data = data.TabularDataset.splits(
         path = 'amazon',train = 'train.csv',validation = 'valid.csv',test = 'test.csv',format = 'csv',fields = fields,skip_header = False # 是否跳過檔案的第一行
 )
 return REVIEW,POLARITY,train_data

載入完資料可以開始建詞表。如果本地沒有預訓練的詞向量檔案,在執行下面的程式碼時會自動下載到當前資料夾下的'.vector_cache'資料夾內,如果本地已經下好了,可以用Vectors指定檔名name,路徑cache,還可以使用Glove。

from torchtext.vocab import Vectors,Glove
import torch

REVIEW,train_data = DataLoader()
# vectors = Vectors(name='glove.6B.300d.txt',cache='.vector_cache')
REVIEW.build_vocab(train_data,# 建詞表是用訓練集建,不要用驗證集和測試集
     max_size=400000,# 單詞表容量
     vectors='glove.6B.300d',# 還有'glove.840B.300d'已經很多可以選
     unk_init=torch.Tensor.normal_ # 初始化train_data中不存在預訓練詞向量詞表中的單詞
)

# print(REVIEW.vocab.freqs.most_common(20)) 資料集裡最常出現的20個單詞
# print(REVIEW.vocab.itos[:10])  列表 index to word
# print(REVIEW.vocab.stoi)    字典 word to index

接著就是把預訓練詞向量載入到model的embedding weight裡去了。

pretrained_embeddings = REVIEW.vocab.vectors
model.embedding.weight.data.copy_(pretrained_embeddings)
UNK_IDX = REVIEW.vocab.stoi[REVIEW.unk_token]
PAD_IDX = REVIEW.vocab.stoi[REVIEW.pad_token]
# 因為預訓練的權重的unk和pad的詞向量不是在我們的資料集語料上訓練得到的,所以最好置零
model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)

然後用torchtext的迭代器來批量載入資料,torchtext.data裡的BucketIterator非常好用,它可以把長度相近的文字資料儘量都放到一個batch裡,這樣最大程度地減少padding,資料就少了很多無意義的0,也減少了矩陣計算量,也許還能對最終準確度有幫助(誤)?我憑直覺猜的,沒有做實驗對比過,但是至少能加速訓練迭代應該是沒有疑問的,如果哪天我有錢了買了臺好點的伺服器做完實驗再來補充。

sort_within_batch設為True的話,一個batch內的資料就會按sort_key的排列規則降序排列,sort_key是排列的規則,這裡使用的是review的長度,即每條使用者評論所包含的單詞數量。

train_iterator,valid_iterator,test_iterator = data.BucketIterator.splits(
            (train_data,test_data),batch_size=32,sort_within_batch=True,sort_key = lambda x:len(x.review),device=torch.device('cpu'))

最後就是載入資料餵給模型了。

for batch in train_iterator:
 # 因為REVIEW Field的inclue_lengths為True,所以還會包含一個句子長度的Tensor
 review,review_len = batch.review 
 # review.size = (seq_length,batch_size),review_len.size = (batch_size,)
 polarity = batch.polarity
 # polarity.size = (batch_size,)
 predictions = model(review,review_lengths)
 loss = criterion(predictions,polarity) # criterion = nn.CrossEntropyLoss()

3. 使用pytorch寫一個LSTM情感分類器

下面是我簡略寫的一個模型,僅供參考

import torch.nn as nn
import torch.nn.functional as F
from torch.nn.utils.rnn import pack_padded_sequence
import torch


class LSTM(nn.Module):

 def __init__(self,vocab_size,embedding_dim,hidden_dim,output_dim,n_layers,bidirectional,dropout,pad_idx):
  super(LSTM,self).__init__()
  self.embedding = nn.Embedding(vocab_size,padding_idx=pad_idx)
  self.lstm = nn.LSTM(embedding_dim,num_layers=n_layers,bidirectional=bidirectional,dropout=dropout)
  self.Ws = nn.Parameter(torch.Tensor(hidden_dim,output_dim))
  self.bs = nn.Parameter(torch.zeros((output_dim,)))
  nn.init.uniform_(self.Ws,-0.1,0.1)
  nn.init.uniform_(self.bs,0.1)
  self.dropout = nn.Dropout(p=0.5)

 def forward(self,x,x_len):
  x = self.embedding(x)
  x = pack_padded_sequence(x,x_len)
  H,(h_n,c_n) = self.lstm(x)
  h_n = self.dropout(h_n)
  h_n = torch.squeeze(h_n)
  res = torch.matmul(h_n,self.Ws) + self.bs
  y = F.softmax(res,dim=1)
  # y.size(batch_size,output_dim)
  return y

訓練函式

def train(model,iterator,optimizer,criterion):
 epoch_loss = 0
 num_sample = 0
 correct = 0

 model.train()
 for batch in iterator:
  optimizer.zero_grad()
  review,review_lengths = batch.review
  polarity = batch.polarity
  predictions = model(review,review_lengths)
  correct += torch.sum(torch.argmax(preds,dim=1) == polarity)
  loss = criterion(predictions,polarity)
  loss.backward()
  epoch_loss += loss.item()
  num_sample += len(batch)
  optimizer.step()

 return epoch_loss / num_sample,correct.float() / num_sample

if __name__ == '__main__':
 for epoch in range(N_EPOCHS):
 train_loss,acc = train(model,train_iter,criterion)
 print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {acc* 100:.2f}%')

注意事項和遇到的一些坑

文字情感分類需不需要去除停用詞?

應該是不用的,否則acc有可能下降。

data.TabularDataset.splits雖然好用,但是如果你只想載入訓練集,這時候如果直接不給validation和test引數賦值,那麼其他程式碼和原來一樣,比如這樣

train_data = data.TabularDataset.splits(
         path = '',skip_header = False # 是否跳過檔案的第一行
)

那麼底下你一定會報錯,因為data.TabularDataset.splits返回的是一個元組,也就是如果是訓練驗證測試三個檔案都給了函式,就返回(train_data,test_data),這時候你用三個變數去接受函式返回值當然沒問題,元組會自動拆包。

當只給函式一個檔案train.csv時,函式返回的是(train_data)而非train_data,因此正確的寫法應該如下

train_data = data.TabularDataset.splits(
         path = '',skip_header = False # 是否跳過檔案的第一行
)[0] # 注意這裡的切片,選擇元組的第一個也是唯一一個元素賦給train_data


同理data.BucketIterator.splits也有相同的問題,它不但返回的是元組,它的引數datasets要求也是以元組形式,即(train_data,test_data)進行賦值,否則在下面的執行中也會出現各種各樣奇怪的問題。

如果你要生成兩個及以上的迭代器,那麼沒問題,直接照上面寫就完事了。

如果你只要生成train_iterator,那麼正確的寫法應該是下面這樣

train_iter = data.BucketIterator(
   train_data,sort_key=lambda x:len(x.review),shuffle=True # 訓練集需要shuffle,但因為驗證測試集不需要
    # 可以生成驗證和測試集的迭代器直接用data.iterator.Iterator類就足夠了
)

出現的問題 x = pack_padded_sequence(x,x_len) 當資料集有長度為0的句子時,就會後面報錯

Adagrad效果比Adam好的多

4. 總結

不僅僅是NLP領域,在各大頂會中,越來越多的學者選擇使用Pytorch而非TensorFlow,主要原因就是因為它的易用性,torchtext和pytorch搭配起來是非常方便的NLP工具,可以大大縮短文字預處理,載入資料的時間。

我本人之前用過tf 1.x以及keras,最終擁抱了Pytorch,也是因為它與Numpy極其類似的用法,更Pythonic的程式碼,清晰的原始碼讓我在遇到bug時能一步一步找到問題所在,動態圖讓人能隨時看到輸出的Tensor的全部資訊,這些都是Pytorch的優勢。

現在tf 2.0也在不斷改進,有人笑稱tf越來越像pytorch了,其實pytorch也在不斷向tf學習,在工業界,tf仍然處於王者地位,不知道未來pytorch能不能在工業界也與tf平分秋色,甚至更勝一籌呢?

以上這篇使用pytorch和torchtext進行文字分類的例項就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。