使用pytorch和torchtext進行文本分類

使用pytorch和torchtext進行文本分類

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

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, POLARITY, 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, valid_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, embedding_dim, padding_idx=pad_idx)
        self.lstm = nn.LSTM(embedding_dim, hidden_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, 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, optimizer, criterion)
    print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {acc* 100:.2f}%')

注意事項和遇到的一些坑

  1. 文本情感分類需不需要去除停用詞?
  • 應該是不用的,否則acc有可能下降。
  1. data.TabularDataset.splits雖然好用,但是如果你只想加載訓練集,這時候如果直接不給validation和test參數賦值,那麼其他代碼和原來一樣,比如這樣

    train_data = data.TabularDataset.splits(
                                        path = '',
                                        train = 'train.csv',
                                        format = 'csv',
                                        fields = fields,
                                        skip_header = False # 是否跳過文件的第一行
    )
    

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

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

    train_data = data.TabularDataset.splits(
                                        path = '',
                                        train = 'train.csv',
                                        format = 'csv',
                                        fields = fields,
                                        skip_header = False # 是否跳過文件的第一行
    )[0] # 注意這裏的切片,選擇元組的第一個也是唯一一個元素賦給train_data
    
  2. 同理data.BucketIterator.splits也有相同的問題,它不但返回的是元組,它的參數datasets要求也是以元組形式,即(train_data, valid_data, test_data)進行賦值,否則在下面的運行中也會出現各種各樣奇怪的問題。

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

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

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

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平分秋色,甚至更勝一籌呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章