App下載

pytorch怎么實(shí)現(xiàn)textCNN?

陪你演戲 2021-08-07 11:58:28 瀏覽數(shù) (2004)
反饋

CNN算法(卷積神經(jīng)網(wǎng)絡(luò))是機(jī)器學(xué)習(xí)中最出名的算法之一,它的應(yīng)用是比較廣泛的,廣為人知的是利用CNN來進(jìn)行圖像識(shí)別處理,但是CNN也可以用在文本分類上。接下來這篇文章我們就來了解一下pytorch怎么用CNN實(shí)現(xiàn)文本分類吧。

1. 原理

2014年的一篇文章,開創(chuàng)cnn用到文本分類的先河。

Convolutional Neural Networks for Sentence Classification

原理說簡單也簡單,其實(shí)就是單層CNN加個(gè)全連接層:

CNN算法示例

不過與圖像中的cnn相比,改動(dòng)為將卷積核的寬固定為一個(gè)詞向量的維度,而長度一般取2,3,4,5這樣。

上圖中第一幅圖的每個(gè)詞對(duì)應(yīng)的一行為一個(gè)詞向量,可以使用word2vec或者glove預(yù)訓(xùn)練得到。本例中使用隨機(jī)初始化的向量。

2. 數(shù)據(jù)預(yù)處理

手中有三個(gè)文件,分別為train.txt,valid.txt,test.txt。其中每一行是一個(gè)字符串化的字典,格式為{‘type': ‘xx', ‘text':‘xxxxx'}。

2.1 轉(zhuǎn)換為csv格式

首先將每個(gè)文件轉(zhuǎn)換為csv文件,分為text和label兩列。一共有4種label,可以轉(zhuǎn)換為數(shù)字表示。代碼如下:

# 獲取文件內(nèi)容
def getData(file):
    f = open(file,'r')
    raw_data = f.readlines()
    return raw_data
# 轉(zhuǎn)換文件格式
def d2csv(raw_data,label_map,name):
    texts = []
    labels = []
    i = 0
    for line in raw_data:
        d = eval(line) #將每行字符串轉(zhuǎn)換為字典
        if len(d['type']) <= 1 or len(d['text']) <= 1: #篩掉無效數(shù)據(jù)
            continue
        y = label_map[d['type']] #根據(jù)label_map將label轉(zhuǎn)換為數(shù)字表示
        x = d['text']
        texts.append(x)
        labels.append(y)
        i+=1
        if i%1000 == 0:
            print(i)
    df = pd.DataFrame({'text':texts,'label':labels})
    df.to_csv('data/'+name+'.csv',index=False,sep='	') # 保存文件
label_map = {'執(zhí)行':0,'刑事':1,'民事':2,'行政':3}
train_data = getData('data/train.txt') #22000+行
d2csv(train_data,label_map,'train')
valid_data = getData('data/valid.txt') # 2000+行
d2csv(valid_data,label_map,'valid')
test_data = getData('data/test.txt') # 2000+行
d2csv(test_data,label_map,'test')

2.2 觀察數(shù)據(jù)分布

對(duì)于本任務(wù)來說,需要觀察每個(gè)文本分詞之后的長度。因?yàn)槊總€(gè)句子是不一樣長的,所以需要設(shè)定一個(gè)固定的長度給模型,數(shù)據(jù)中不夠長的部分填充,超出部分舍去。

訓(xùn)練的時(shí)候只有訓(xùn)練數(shù)據(jù),因此觀察訓(xùn)練數(shù)據(jù)的文本長度分布即可。分詞可以使用jieba分詞等工具。

train_text = []
for line in train_data:
    d = eval(line)
    t = jieba.cut(d['text'])
    train_text.append(t)
sentence_length = [len(x) for x in train_text] #train_text是train.csv中每一行分詞之后的數(shù)據(jù)
%matplotlib notebook
import matplotlib.pyplot as plt
plt.hist(sentence_length,1000,normed=1,cumulative=True)
plt.xlim(0,1000)
plt.show()

得到長度的分布圖:

長度分布圖

可以看到長度小于1000的文本占據(jù)所有訓(xùn)練數(shù)據(jù)的80%左右,因此訓(xùn)練時(shí)每個(gè)文本固定長度為1000個(gè)詞。

2.3 由文本得到訓(xùn)練用的mini-batch數(shù)據(jù)

目前我們手里的數(shù)據(jù)為csv形式的兩列數(shù)據(jù),一列字符串text,一列數(shù)字label。label部分不需要再處理了,不過text部分跟可訓(xùn)練的數(shù)據(jù)還差得遠(yuǎn)。

假設(shè)每個(gè)詞對(duì)應(yīng)的詞向量維度為 D i m Dim Dim,每一個(gè)樣本的分詞后的長度已知設(shè)為 W = 1000 W=1000 W=1000,每個(gè)mini-batch的大小為 N N N。那么我們希望得到的是一個(gè)個(gè)維度為 N ? W ? D i m N*W*Dim N?W?Dim的浮點(diǎn)數(shù)數(shù)據(jù)作為mini-batch輸入到模型。

于是還需要以下幾個(gè)步驟:

分詞去除停用詞建立詞匯表(詞匯表是詞語到index的映射,index從0到M,M為已知詞匯的個(gè)數(shù),形如{'可愛‘:0, ‘美好':1,…})將分詞且去除停用詞之后的數(shù)據(jù)轉(zhuǎn)換為下標(biāo)數(shù)據(jù),維度應(yīng)該為 N a l l ? W N_{all}*W Nall??W, N a l l N_{all} Nall?是所有樣本的數(shù)量。其中長度不足W的樣本在后面補(bǔ)特定字符,長度超過W的樣本截?cái)?。將?shù)據(jù)分割為一個(gè)個(gè) N ? W N*W N?W大小的mini-batch作為模型的輸入。根據(jù)mini-batch數(shù)據(jù)向詞向量中映射得到 N ? W ? D i m N*W*Dim N?W?Dim大小的最終輸入。(這步在模型中)

看起來復(fù)雜哭了,手動(dòng)處理起來確實(shí)有些麻煩。不過后來發(fā)現(xiàn)跟pytorch很相關(guān)的有個(gè)包torchtext能夠很方便的做到這幾步,所以直接來介紹用這個(gè)包的做法。

在貼代碼之前先貼兩個(gè)torchtext的教程。torchtext入門教程 還是不懂的話看torchtext文檔。 還還是不懂請(qǐng)直接看源碼。對(duì)照教程看以下代碼。

首先是分詞函數(shù),寫為有一個(gè)參數(shù)的函數(shù):

def tokenizer(x):
    res = [w for w in jieba.cut(x)]
    return res

接著是停用詞表,在網(wǎng)上找的一個(gè)停用詞資源(也可以跳過這步):

stop_words = []
print('build stop words set')
with open('data/stopwords.dat') as f:
    for l in f.readlines():
        stop_words.append(l.strip())

然后設(shè)定TEXT和LABEL兩個(gè)field。定義以及參數(shù)含義看上面的文檔或教程。

TEXT = data.Field(sequential=True, tokenize=tokenizer,fix_length=1000,stop_words=stop_words)
LABEL = data.Field(sequential=False,use_vocab=False)

讀取文件,分詞,去掉停用詞等等。直接一波帶走:

train,valid,test = data.TabularDataset.splits(path='data',train='train.csv',
                                              validation='valid.csv',test='test.csv',
                                              format='csv',
                                              skip_header=True,csv_reader_params={'delimiter':'	'},
                                              fields=[('text',TEXT),('label',LABEL)])

建立詞匯表:

TEXT.build_vocab(train)

生成iterator形式的mini-batch數(shù)據(jù):

train_iter, val_iter, test_iter = data.Iterator.splits((train,valid,test),
                                                             batch_sizes=(args.batch_size,args.batch_size,args.batch_size),
                                                             device=args.device,
                                                             sort_key=lambda x:len(x.text),
                                                             sort_within_batch=False,
                                                             repeat=False)

That's all! 簡單得令人發(fā)指!雖然為了搞懂這幾個(gè)函數(shù)整了大半天。最終的這幾個(gè)xxx_iter就會(huì)生成我們需要的維度為N ? W N*WN?W的數(shù)據(jù)。

3. 模型

模型其實(shí)相對(duì)很簡單,只有一個(gè)embedding映射,加一層cnn加一個(gè)激活函數(shù)以及一個(gè)全連接。

不過需要注意使用不同大小的卷積核的寫法。

可以選擇使用多個(gè)nn.Conv2d然后手動(dòng)拼起來,這里使用nn.ModuleList模塊。其實(shí)本質(zhì)上還是使用多個(gè)Conv2d然后拼起來。

import torch
import torch.nn as nn
import torch.nn.functional as F
class textCNN(nn.Module):
    def __init__(self, args):
        super(textCNN, self).__init__()
        self.args = args
        
        Vocab = args.embed_num ## 已知詞的數(shù)量
        Dim = args.embed_dim ##每個(gè)詞向量長度
        Cla = args.class_num ##類別數(shù)
        Ci = 1 ##輸入的channel數(shù)
        Knum = args.kernel_num ## 每種卷積核的數(shù)量
        Ks = args.kernel_sizes ## 卷積核list,形如[2,3,4]
        
        self.embed = nn.Embedding(Vocab,Dim) ## 詞向量,這里直接隨機(jī)
        
        self.convs = nn.ModuleList([nn.Conv2d(Ci,Knum,(K,Dim)) for K in Ks]) ## 卷積層
        self.dropout = nn.Dropout(args.dropout) 
        self.fc = nn.Linear(len(Ks)*Knum,Cla) ##全連接層
        
    def forward(self,x):
        x = self.embed(x) #(N,W,D)
        
        x = x.unsqueeze(1) #(N,Ci,W,D)
        x = [F.relu(conv(x)).squeeze(3) for conv in self.convs] # len(Ks)*(N,Knum,W)
        x = [F.max_pool1d(line,line.size(2)).squeeze(2) for line in x]  # len(Ks)*(N,Knum)
        
        x = torch.cat(x,1) #(N,Knum*len(Ks))
        
        x = self.dropout(x)
        logit = self.fc(x)
        return logit

4. 訓(xùn)練腳本

import os
import sys
import torch
import torch.autograd as autograd
import torch.nn.functional as F
def train(train_iter, dev_iter, model, args):
    if args.cuda:
        model.cuda(args.device)    
    optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
    
    steps = 0
    best_acc = 0
    last_step = 0
    model.train()
    print('training...')
    for epoch in range(1, args.epochs+1):
        for batch in train_iter:
            feature, target = batch.text, batch.label #(W,N) (N)
            feature.data.t_()
            
            if args.cuda:
                feature, target = feature.cuda(), target.cuda()
            
            optimizer.zero_grad()
            logit = model(feature)
            loss = F.cross_entropy(logit, target)
            loss.backward()
            optimizer.step()
            
            steps += 1
            if steps % args.log_interval == 0:
                result = torch.max(logit,1)[1].view(target.size())
                corrects = (result.data == target.data).sum()
                accuracy = corrects*100.0/batch.batch_size
                sys.stdout.write('
Batch[{}] - loss: {:.6f} acc: {:.4f}$({}/{})'.format(steps,
                                                                                        loss.data.item(),
                                                                                        accuracy,
                                                                                        corrects,
                                                                                        batch.batch_size))
            if steps % args.dev_interval == 0:
                dev_acc = eval(dev_iter, model, args)
                if dev_acc > best_acc:
                    best_acc = dev_acc
                    last_step = steps
                    if args.save_best:
                        save(model,args.save_dir,'best',steps)
                else:
                    if steps - last_step >= args.early_stop:
                        print('early stop by {} steps.'.format(args.early_stop))
            elif steps % args.save_interval == 0:
                save(model,args.save_dir,'snapshot',steps)

訓(xùn)練腳本中還有設(shè)置optimizer以及l(fā)oss的部分。其余部分比較trivial。

模型的保存:

def save(model, save_dir, save_prefix, steps):
    if not os.path.isdir(save_dir):
        os.makedirs(save_dir)
    save_prefix = os.path.join(save_dir,save_prefix)
    save_path = '{}_steps_{}.pt'.format(save_prefix,steps)
    torch.save(model.state_dict(),save_path)

eval函數(shù),用來評(píng)估驗(yàn)證集與測試集合上的準(zhǔn)確率acc。

def eval(data_iter, model, args):
    model.eval()
    corrects, avg_loss = 0,0
    for batch in data_iter:
        feature, target = batch.text, batch.label
        feature.data.t_()
        
        if args.cuda:
            feature, target = feature.cuda(), target.cuda()
        
        logit = model(feature)
        loss = F.cross_entropy(logit,target)
        
        avg_loss += loss.data[0]
        result = torch.max(logit,1)[1]
        corrects += (result.view(target.size()).data == target.data).sum()
    
    size = len(data_iter.dataset)
    avg_loss /= size 
    accuracy = 100.0 * corrects/size
    print('
Evaluation - loss: {:.6f} acc: {:.4f}%({}/{}) 
'.format(avg_loss,accuracy,corrects,size))
    return accuracy

5. main函數(shù)

這暫時(shí)就不貼了??梢詤⒖枷乱徊糠纸o出的github。

最終在測試集合上accuracy為97%(畢竟只是四分類)。

但是遇到個(gè)問題就是隨著accuracy上升,loss也在迅速增大。

測試結(jié)果

在一番探究之后大致得出結(jié)論就是,這樣是沒問題的。比如在本例中是個(gè)四分類,加入全連接層輸出的結(jié)果是[-10000,0,0,10000],而正確分類是0。

那么這就是個(gè)錯(cuò)誤的結(jié)果。計(jì)算一下這個(gè)單個(gè)樣例的loss。先算softmax,約等于[ e ? 20000 , e ? 10000 , e ? 10000 , 1 e^{-20000},e^{-10000},e^{-10000},1 e?20000,e?10000,e?10000,1]。真實(shí)的label為[1,0,0,0],因此交叉熵為20000。

所以我們發(fā)現(xiàn)這一個(gè)錯(cuò)誤樣例的loss就會(huì)這么大。最終的loss大一些也是正常的。

不過為什么隨著accuracy接近100%而導(dǎo)致loss迅速增加這個(gè)問題還需要進(jìn)一步研究。大概是因?yàn)殡S著accuracy升高導(dǎo)致結(jié)果接近訓(xùn)練集的分布,這樣與驗(yàn)證集或測試集的分布產(chǎn)生比較極端差別的個(gè)例會(huì)增加。

6.引用

代碼部分參考了很多這位老哥的github,在此感謝。跟他不一樣的地方主要是數(shù)據(jù)處理部分。

以上就是pytorch怎么用CNN實(shí)現(xiàn)文本分類的全部內(nèi)容,希望能給大家一個(gè)參考,也希望大家多多支持W3Cschool。


0 人點(diǎn)贊