論文復現 | DPCNN文字分類模型實現及AG news資料集實驗復現
一 寫在前面
未經允許,不得轉載, 謝謝~~~
之前寫了一篇關於DPCNN文章解讀的筆記,所以再整理了相關的資料集處理情況和實驗情況一併分享出來,有需要的同學可以參考一下。
模型本身結構比較簡單,具體的實現是應深度學習課程邱錫鵬教授的要求,基於fastNLP這個框架來實現的, 這個框架是邱老師團隊為了簡化NLP的程式設計而釋出的,封裝了一些dataloader,常見模型等,對於文字任務還是比較友好的。
實驗選擇了原論文中使用的一個AG news資料集進行論文結果的復現,原論文用了unsupervised embedding的訓練方法能到93.13%(6.87%錯誤率),我和小組成員復現的情況在沒有使用unsupervised embedding的情況下能都達到91.49%, 相差1.64%。
這邊列了一些主要的資源資訊:
- 論文原文: ACL2017- Deep Pyramid Convolutional Neural Networks for Text Categorization
- 論文blog: 論文 | 《Deep Pyramid Convolutional Neural Networks for Text Categorization》DPCNN文字分類模型介紹
- 資料集-官網完整版: AG's corpus of news articles
- 資料集-分類任務集: AG news主題分類資料集
- fastNLP: fastNLP
二 網路模型及資料集介紹
2.1 DPCNN網路模型

網路模型
網路模型如上所示,具體的分析都寫在上一篇部落格裡了, 這裡的細節就不贅述了~
2.2 AG news資料集
- 我們採用的資料集是原論文使用的資料集之一 AG news。
- AG news是由 ComeToMyHead 超過一年的努力,從 2000 多不同的新聞來源蒐集的超過 1 百萬的新聞文章。
- 官網下載到完整的資料集的
newsSpace.bz2
的格式為{ source,url,title,image,category,description,rank,pubdate,video }; 完整的資料集可以用於做資料探勘(聚類,分類等、資訊檢索(排名,搜尋等)等多種任務; - 本次實驗採用其中的 主題分類資料集 由XiangZhang( [email protected] )從以上資料集中構建;
- 主題分類資料集檔案從以上原始語料庫中選擇4個最大的類,每個類包含30,000訓練樣本和1900測試樣本,因此總的訓練樣本是120,000,總的測試樣本是7600。
- 主要資料檔案說明如下:
-
classes.txt
包含包含類名稱,即:World、Sports、Business、Sci/Tec; -
train.csv
和test.csv
包含了逗號分隔的3欄,格式為 { label, title,description },分別是類索引 (1-4), 標題和描述; - 標題和描述都有雙引號""包含,其中的內部引號由雙重引號標出,新行由\n分隔;
-
三 具體實現過程
1 全域性路徑和全域性變數: util.py
- 主要定義了資料集的路徑和一些全域性變數;
- 這些引數直接在這個檔案裡修改即可。
# some global varible dataset_path = 'dataset/' classes_txt = dataset_path + 'classes.txt' train_csv = dataset_path + 'train.csv' test_csv = dataset_path + 'test.csv' pickle_path = 'result/' # some global variable word_embedding_dimension = 300 num_classes = 4
2 資料載入檔案: dataloader.py
- 主要是用AG news資料集進行處理生成
dataset_train
和dataset_test
; - 具體實現主要基於fastNLP
- 主要的處理流程包括:
max_se_len
- 可以按照以上的處理思路自己用PyTorch寫一個數據載入的程式碼,也可以按照fastNLP的需求配置建立一個虛擬環境(我自己是建立了一個名為
fastnlp
的虛擬環境的) - 這部分的程式碼檔案比較長:
from fastNLP import DataSet from fastNLP import Instance from fastNLP import Vocabulary from utils import * # read csv data to DataSet dataset_train = DataSet.read_csv(train_csv,headers=('label','title','description'),sep='","') dataset_test = DataSet.read_csv(test_csv,headers=('label','title','description'),sep='","') # preprocess data dataset_train.apply(lambda x: int(x['label'][1])-1,new_field_name='label') dataset_train.apply(lambda x: x['title'].lower(), new_field_name='title') dataset_train.apply(lambda x: x['description'][:-2].lower()+' .', new_field_name='description') dataset_test.apply(lambda x: int(x['label'][1])-1,new_field_name='label') dataset_test.apply(lambda x: x['title'].lower(), new_field_name='title') dataset_test.apply(lambda x: x['description'][:-2].lower()+ ' .', new_field_name='description') # split sentence with space def split_sent(instance): return instance['description'].split() dataset_train.apply(split_sent,new_field_name='description_words') dataset_test.apply(split_sent,new_field_name='description_words') # add item of length of words dataset_train.apply(lambda x: len(x['description_words']),new_field_name='description_seq_len') dataset_test.apply(lambda x: len(x['description_words']),new_field_name='description_seq_len') # get max_sentence_length max_seq_len_train=0 max_seq_len_test=0 for i in range (len(dataset_train)): if(dataset_train[i]['description_seq_len'] > max_seq_len_train): max_seq_len_train = dataset_train[i]['description_seq_len'] else: pass for i in range (len(dataset_test)): if(dataset_test[i]['description_seq_len'] > max_seq_len_test): max_seq_len_test = dataset_test[i]['description_seq_len'] else: pass max_sentence_length = max_seq_len_train if (max_seq_len_test > max_sentence_length): max_sentence_length = max_seq_len_test print ('max_sentence_length:',max_sentence_length) # set input,which will be used in forward dataset_train.set_input("description_words") dataset_test.set_input("description_words") # set target,which will be used in evaluation dataset_train.set_target("label") dataset_test.set_target("label") # build vocabulary vocab = Vocabulary(min_freq=2) dataset_train.apply(lambda x:[vocab.add(word) for word in x['description_words']]) vocab.build_vocab() # index sentence by Vocabulary dataset_train.apply(lambda x: [vocab.to_index(word) for word in x['description_words']],new_field_name='description_words') dataset_test.apply(lambda x: [vocab.to_index(word) for word in x['description_words']],new_field_name='description_words') # pad title_words to max_sentence_length def padding_words(data): for i in range(len(data)): if data[i]['description_seq_len'] <= max_sentence_length: padding = [0] * (max_sentence_length - data[i]['description_seq_len']) data[i]['description_words'] += padding else: pass return data dataset_train= padding_words(dataset_train) dataset_test = padding_words(dataset_test) dataset_train.apply(lambda x: len(x['description_words']), new_field_name='description_seq_len') dataset_test.apply(lambda x: len(x['description_words']), new_field_name='description_seq_len') dataset_train.rename_field("description_words","description_word_seq") dataset_train.rename_field("label","label_seq") dataset_test.rename_field("description_words","description_word_seq") dataset_test.rename_field("label","label_seq") print("dataset processed successfully!")
3 網路模型實現: model.py
- 這個在github上也能找到一些程式碼;
- 基於PyTorch實現的;
- 實現的時候參考了一個 原始碼 , 但是修改了他的一點小問題(添加了漏掉的一處shortcut);
- 貼一下修改之後的吧:
import torch import torch.nn as nn class ResnetBlock(nn.Module): def __init__(self, channel_size): super(ResnetBlock, self).__init__() self.channel_size = channel_size self.maxpool = nn.Sequential( nn.ConstantPad1d(padding=(0, 1), value=0), nn.MaxPool1d(kernel_size=3, stride=2) ) self.conv = nn.Sequential( nn.BatchNorm1d(num_features=self.channel_size), nn.ReLU(), nn.Conv1d(self.channel_size, self.channel_size, kernel_size=3, padding=1), nn.BatchNorm1d(num_features=self.channel_size), nn.ReLU(), nn.Conv1d(self.channel_size, self.channel_size, kernel_size=3, padding=1), ) def forward(self, x): x_shortcut = self.maxpool(x) x = self.conv(x_shortcut) x = x + x_shortcut return x class DPCNN(nn.Module): def __init__(self,max_features,word_embedding_dimension,max_sentence_length,num_classes): super(DPCNN, self).__init__() self.max_features = max_features self.embed_size = word_embedding_dimension self.maxlen = max_sentence_length self.num_classes=num_classes self.channel_size = 250 self.embedding = nn.Embedding(self.max_features, self.embed_size) torch.nn.init.normal_(self.embedding.weight.data,mean=0,std=0.01) self.embedding.weight.requires_grad = True # region embedding self.region_embedding = nn.Sequential( nn.Conv1d(self.embed_size, self.channel_size, kernel_size=3, padding=1), nn.BatchNorm1d(num_features=self.channel_size), nn.ReLU(), nn.Dropout(0.2) ) self.conv_block = nn.Sequential( nn.BatchNorm1d(num_features=self.channel_size), nn.ReLU(), nn.Conv1d(self.channel_size, self.channel_size, kernel_size=3, padding=1), nn.BatchNorm1d(num_features=self.channel_size), nn.ReLU(), nn.Conv1d(self.channel_size, self.channel_size, kernel_size=3, padding=1), )
4 訓練網路: train.py
- 這部分也是用了fastNLP的藉口,總共就沒幾行程式碼:
from utils import * from model import * from dataloader import * from fastNLP import Trainer from copy import deepcopy from fastNLP.core.losses import CrossEntropyLoss from fastNLP.core.metrics import AccuracyMetric from fastNLP.core.optimizer import Adam from fastNLP.core.utils import save_pickle # load model model=DPCNN(max_features=len(vocab),word_embedding_dimension=word_embedding_dimension,max_sentence_length = max_sentence_length,num_classes=num_classes) # define loss and metric loss = CrossEntropyLoss(pred="output",target="label_seq") metric = AccuracyMetric(pred="predict", target="label_seq") # train model with train_data,and val model with test_data # embedding=300 gaussian init,weight_decay=0.0001, lr=0.001,epoch=5 trainer=Trainer(model=model,train_data=dataset_train,dev_data=dataset_test,loss=loss,metrics=metric,save_path=None,batch_size=64,n_epochs=5,optimizer=Adam(lr=0.001, weight_decay=0.0001)) trainer.train() # save pickle save_pickle(model,pickle_path=pickle_path,file_name='new_model.pkl')
5 測試網路: test.py
trained_model.pkl
from utils import * from model import * from dataloader import * from fastNLP import Tester from fastNLP.core.metrics import AccuracyMetric from fastNLP.core.utils import load_pickle # define model model=DPCNN(max_features=len(vocab),word_embedding_dimension=word_embedding_dimension,max_sentence_length = max_sentence_length,num_classes=num_classes) # load checkpoint to model load_model = load_pickle(pickle_path=pickle_path, file_name='trained_model.pkl') # use Tester to evaluate tester=Tester(data=dataset_test,model=load_model,metrics=AccuracyMetric(pred="predict",target="label_seq"),batch_size=4) acc=tester.test() print(acc)
四 實驗結果
1 中間過程
訓練階段一共做了以下幾組對比實驗來確定比較好的訓練條件,大家也可以參考一下:
-
關於語料選擇
AG文字分類語料庫由{label,title,description}三者構成,DPCNN論文中沒有明確指出分類任務時使用的是 title 還是 description,因此我們在其他條件不變的情況下進行了分別使用 title 和 description 的對比實驗。
具體實驗設定情況如下所示:
- 實驗 1:input=title, word_embedding=300, embedding_init = random, batch_size=32, lr=0.01, epoch=5;
- 實驗 2:input=description, word_embedding=300, embedding_init = random, batch_size =32, lr=0.01, epoch=5;
- 實驗2優於實驗1;
-
關於batch_size選擇
原論文中選用了batch_size=100,但考慮到裝置的限制,在實驗復現的過程中我們 選擇了較為常見的32和64進行對比實驗。在原來實驗2的基礎上新增加一組batch_size=64, 其他條件保持不變的實驗 3,具體實驗設定情況如下所示:
- 實驗2:input=description,word_embedding=300,embedding_init=random,batch_size= 32, lr=0.01, epoch=5;
- 實驗3:input=description,word_embedding=300,embedding_init=random,batch_size= 64, lr=0.01, epoch=5;
- 實驗3優於實驗2;
-
關於word_embedding的選擇
在復現的過程中我們選擇了三個不同緯度的 embedding,分別為 300、500 和 100 進行網路模型的訓練和測試。並基於以上兩組實驗的結果,選用 descrition 語料集並將 batch_size 設定為 64. 具體的實驗設定如下所示:
- 實驗4:input=description,word_embedding=300,embedding_init=random,batch_size= 64, lr=0.01, epoch=5;
- 實驗5:input=description,word_embedding=500,embedding_init=random,batch_size= 64, lr=0.01, epoch=5;
- 實驗6:input=description,word_embedding=100,embedding_init=random,batch_size= 64, lr=0.01, epoch=5;
- 符合引數越多,網路能表示資訊越多,但是越難訓練的規律;
-
learning_rate調整
以上實驗都採用的是 0.01 的 learning_rate,但是從實驗情況看到幾乎每個實驗隨著訓練的進行,其在測試集上的表現都是動盪的。因此考慮到使用的資料集比較小,所以在接下 來的實驗中將學習率由原來的 0.01 調整為 0.001,希望網路模型能夠更加穩定。
注:關於每個具體的實驗結果情況和實驗分析我就不再詳細描述了,也是一些非常基礎的內容.....
2 最終實驗
綜合考慮以上幾組對比實驗的結果,以及考慮 learning_rate 的調整選擇最優的實 驗條件重新進行網路模型的訓練過程。另外還在原來的實驗基礎上參考原論文設定加入了 weight_decay=0.0001的正則化項。最終保留word_embedding=300和word_embedding的100 兩項展開實驗。
具體的實驗設定情況如下所示:
- 實驗8:input=description,word_embedding=300,embedding_init=gaussian,batch_size= 64, lr=0.001, epoch=5;
- 實驗9:input=description,word_embedding=100,embedding_init=gaussian,batch_size= 64, lr=0.001, epoch=5;
實驗結果

綜合最優條件進行訓練的實驗結果:accuracy(%)

實驗8執行情況
在沒有用unsupervised embeding的情況下,最佳訓練條件下得到的實驗結果如圖能夠在測試集上達到 91.49% 的準確度,與DPCNN 原論文展示的 93.13% 僅相差 1.64%。
五 寫在最後
暫時就先寫這麼多吧~~~~
寫完一個報告又整理一篇部落格的肌肉痠痛感,啊啊啊~如果覺得還可以的話,點個喜歡可以不可以呢(-o⌒) ☆
後面等我想整理的時候再傳到github上好了。