文本分類是NLP領(lǐng)域的較為容易的入門問題,本文記錄文本分類任務(wù)的基本流程,大部分操作使用了torch和torchtext兩個(gè)庫。
1. 文本數(shù)據(jù)預(yù)處理
首先數(shù)據(jù)存儲(chǔ)在三個(gè)csv文件中,分別是train.csv,valid.csv,test.csv,第一列存儲(chǔ)的是文本數(shù)據(jù),例如情感分類問題經(jīng)常是用戶的評(píng)論 review ,例如imdb或者amazon數(shù)據(jù)集。第二列是情感極性 polarity ,N分類問題的話就有N個(gè)值,假設(shè)值的范圍是0~N-1。
下面是很常見的文本預(yù)處理流程,英文文本的話不需要分詞,直接按空格split就行了,這里只會(huì)主要說說第4點(diǎn)。
1.去除非文本部分
2.分詞
3.去除停用詞
4.對(duì)英文單詞進(jìn)行 詞干提取 (stemming)和 詞型還原 (lemmatization)
5.轉(zhuǎn)為小寫
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
較為激進(jìn),轉(zhuǎn)換有可能出現(xiàn)錯(cuò)誤,這里較為推薦使用WordNetLemmatizer
,它一般只在非常肯定的情況下才進(jìn)行轉(zhuǎn)換,否則會(huì)返回原來的單詞。
stemmer.stem('knives')
# knive
wnl.lemmatize('knives')
# knife
因?yàn)槲覜]有系統(tǒng)學(xué)習(xí)和研究過NLTK的代碼,所以就不在這里展開說了,有興趣的可以自己去看NLTK源代碼。
2. 使用torchtext加載文本數(shù)據(jù)
本節(jié)主要是用的模塊是torchtext里的data模塊,處理的數(shù)據(jù)同上一節(jié)所描述。
首先定義一個(gè)tokenizer用來處理文本,比如分詞,小寫化,如果你已經(jīng)根據(jù)上一節(jié)的詞干提取和詞型還原的方法處理過文本里的每一個(gè)單詞后可以直接分詞就夠了。
tokenize = lambda x: x.split()
或者也可以更保險(xiǎn)點(diǎn),使用spacy庫,不過就肯定更耗費(fèi)時(shí)間了。
import spacy
spacy_en = spacy.load('en')
def tokenizer(text):
return [toke.text for toke in spacy_en.tokenizer(text)]
然后要定義 Field ,至于Field是啥,你可以簡單地把它理解為一個(gè)能夠加載、預(yù)處理和存儲(chǔ)文本數(shù)據(jù)和標(biāo)簽的對(duì)象。我們可以用它根據(jù)訓(xùn)練數(shù)據(jù)來建立詞表,加載預(yù)訓(xùn)練的Glove詞向量等等。
def DataLoader():
tokenize = lambda x: x.split()
# 用戶評(píng)論,include_lengths設(shè)為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列是我們不需要的數(shù)據(jù),
# 那么就要添加一個(gè)全是None的元組, fields列表存儲(chǔ)的Field的順序必須和csv文件中每一列的順序?qū)?yīng),
# 否則review可能就加載到polarity Field里去了
fields = [('review', REVIEW), (None, None), ('polarity', POLARITY)]
# 加載train,valid,test數(shù)據(jù)
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
加載完數(shù)據(jù)可以開始建詞表。如果本地沒有預(yù)訓(xùn)練的詞向量文件,在運(yùn)行下面的代碼時(shí)會(huì)自動(dòng)下載到當(dāng)前文件夾下的'.vector_cache'文件夾內(nèi),如果本地已經(jīng)下好了,可以用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, # 建詞表是用訓(xùn)練集建,不要用驗(yàn)證集和測(cè)試集
max_size=400000, # 單詞表容量
vectors='glove.6B.300d', # 還有'glove.840B.300d'已經(jīng)很多可以選
unk_init=torch.Tensor.normal_ # 初始化train_data中不存在預(yù)訓(xùn)練詞向量詞表中的單詞
)
# print(REVIEW.vocab.freqs.most_common(20)) 數(shù)據(jù)集里最常出現(xiàn)的20個(gè)單詞
# print(REVIEW.vocab.itos[:10]) 列表 index to word
# print(REVIEW.vocab.stoi) 字典 word to index
接著就是把預(yù)訓(xùn)練詞向量加載到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]
# 因?yàn)轭A(yù)訓(xùn)練的權(quán)重的unk和pad的詞向量不是在我們的數(shù)據(jù)集語料上訓(xùn)練得到的,所以最好置零
model.embedding.weight.data[UNK_IDX] = torch.zeros(EMBEDDING_DIM)
model.embedding.weight.data[PAD_IDX] = torch.zeros(EMBEDDING_DIM)
然后用torchtext的迭代器來批量加載數(shù)據(jù),torchtext.data里的BucketIterator非常好用,它可以把長度相近的文本數(shù)據(jù)盡量都放到一個(gè)batch里,這樣最大程度地減少padding,數(shù)據(jù)就少了很多無意義的0,也減少了矩陣計(jì)算量,也許還能對(duì)最終準(zhǔn)確度有幫助(誤)?
sort_within_batch設(shè)為True的話,一個(gè)batch內(nèi)的數(shù)據(jù)就會(huì)按sort_key的排列規(guī)則降序排列,sort_key是排列的規(guī)則,這里使用的是review的長度,即每條用戶評(píng)論所包含的單詞數(shù)量。
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'))
最后就是加載數(shù)據(jù)喂給模型了。
for batch in train_iterator:
# 因?yàn)镽EVIEW Field的inclue_lengths為True,所以還會(huì)包含一個(gè)句子長度的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寫一個(gè)LSTM情感分類器
下面是我簡略寫的一個(gè)模型,僅供參考
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
訓(xùn)練函數(shù)
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}%')
4. 注意事項(xiàng)和遇到的一些坑
1.文本情感分類需不需要去除停用詞?
?應(yīng)該是不用的,否則acc有可能下降。
2.data.TabularDataset.splits
雖然好用,但是如果你只想加載訓(xùn)練集,這時(shí)候如果直接不給validation和test參數(shù)賦值,那么其他代碼和原來一樣,比如這樣
train_data = data.TabularDataset.splits(
path = '',
train = 'train.csv',
format = 'csv',
fields = fields,
skip_header = False # 是否跳過文件的第一行
)
那么底下你一定會(huì)報(bào)錯(cuò),因?yàn)?code>data.TabularDataset.splits返回的是一個(gè)元組,也就是如果是訓(xùn)練驗(yàn)證測(cè)試三個(gè)文件都給了函數(shù),就返回(train_data, valid_data, test_data)
,這時(shí)候你用三個(gè)變量去接受函數(shù)返回值當(dāng)然沒問題,元組會(huì)自動(dòng)拆包。
當(dāng)只給函數(shù)一個(gè)文件train.csv時(shí),函數(shù)返回的是(train_data)
而非train_data
,因此正確的寫法應(yīng)該如下
train_data = data.TabularDataset.splits(
path = '',
train = 'train.csv',
format = 'csv',
fields = fields,
skip_header = False # 是否跳過文件的第一行
)[0] # 注意這里的切片,選擇元組的第一個(gè)也是唯一一個(gè)元素賦給train_data
3.同理data.BucketIterator.splits
也有相同的問題,它不但返回的是元組,它的參數(shù)datasets要求也是以元組形式,即(train_data, valid_data, test_data)
進(jìn)行賦值,否則在下面的運(yùn)行中也會(huì)出現(xiàn)各種各樣奇怪的問題。
如果你要生成兩個(gè)及以上的迭代器,那么沒問題,直接照上面寫就完事了。
如果你只要生成train_iterator
,那么正確的寫法應(yīng)該是下面這樣
train_iter = data.BucketIterator(
train_data,
batch_size=32,
sort_key=lambda x:len(x.review),
sort_within_batch=True,
shuffle=True # 訓(xùn)練集需要shuffle,但因?yàn)轵?yàn)證測(cè)試集不需要
# 可以生成驗(yàn)證和測(cè)試集的迭代器直接用data.iterator.Iterator類就足夠了
)
4.出現(xiàn)的問題 x = pack_padded_sequence(x, x_len)
當(dāng)數(shù)據(jù)集有長度為0的句子時(shí), 就會(huì)后面報(bào)錯(cuò)
5.當(dāng)vocab size較大而訓(xùn)練數(shù)據(jù)不多的情況下,我在實(shí)驗(yàn)時(shí)發(fā)現(xiàn)Adagrad效果比Adam好,如果數(shù)據(jù)較多,可以嘗試使用RMSProp和Adam
5. 總結(jié)
不僅僅是NLP領(lǐng)域,在各大頂會(huì)中,越來越多的學(xué)者選擇使用Pytorch而非TensorFlow,主要原因就是因?yàn)樗囊子眯裕瑃orchtext和pytorch搭配起來是非常方便的NLP工具,可以大大縮短文本預(yù)處理,加載數(shù)據(jù)的時(shí)間。
我本人之前用過tf 1.x以及keras,最終擁抱了Pytorch,也是因?yàn)樗cNumpy極其類似的用法,更Pythonic的代碼,清晰的源碼讓我在遇到bug時(shí)能一步一步找到問題所在,動(dòng)態(tài)圖讓人能隨時(shí)看到輸出的Tensor的全部信息,這些都是Pytorch的優(yōu)勢(shì)。
現(xiàn)在tf 2.0也在不斷改進(jìn),有人稱tf越來越像pytorch了,其實(shí)pytorch也在不斷向tf學(xué)習(xí),在工業(yè)界,tf仍然處于王者地位,不知道未來pytorch能不能在工業(yè)界也與tf平分秋色,甚至更勝一籌呢?
-
數(shù)據(jù)預(yù)處理
+關(guān)注
關(guān)注
1文章
20瀏覽量
2756 -
nlp
+關(guān)注
關(guān)注
1文章
488瀏覽量
22033 -
pytorch
+關(guān)注
關(guān)注
2文章
807瀏覽量
13199
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論