论文复现 | DPCNN文本分类模型实现及AG news数据集实验复现

一 写在前面

未经允许,不得转载, 谢谢~~~

之前写了一篇关于DPCNN文章解读的笔记,所以再整理了相关的数据集处理情况和实验情况一并分享出来,有需要的同学可以参考一下。

模型本身结构比较简单,具体的实现是应深度学习课程邱锡鹏教授的要求,基于fastNLP这个框架来实现的, 这个框架是邱老师团队为了简化NLP的编程而发布的,封装了一些dataloader,常见模型等,对于文本任务还是比较友好的。

实验选择了原论文中使用的一个AG news数据集进行论文结果的复现,原论文用了unsupervised embedding的训练方法能到93.13%(6.87%错误率),我和小组成员复现的情况在没有使用unsupervised embedding的情况下能都达到91.49%, 相差1.64%。

这边列了一些主要的资源信息:

二 网络模型及数据集介绍

2.1 DPCNN网络模型

9933353-e9e0aca854d43225.png
网络模型

网络模型如上所示,具体的分析都写在上一篇博客里了, 这里的细节就不赘述了~

2.2 AG news数据集

  1. 我们采用的数据集是原论文使用的数据集之一 AG news。
  2. AG news是由 ComeToMyHead 超过一年的努力,从 2000 多不同的新闻来源搜集的超过 1 百万的新闻文章。
  3. 官网下载到完整的数据集的newsSpace.bz2的格式为{ source,url,title,image,category,description,rank,pubdate,video }; 完整的数据集可以用于做数据挖掘(聚类,分类等、信息检索(排名,搜索等)等多种任务;
  4. 本次实验采用其中的主题分类数据集由XiangZhang([email protected])从以上数据集中构建;
  5. 主题分类数据集文件从以上原始语料库中选择4个最大的类,每个类包含30,000训练样本和1900测试样本,因此总的训练样本是120,000,总的测试样本是7600。
  6. 主要数据文件说明如下:
    • classes.txt包含包含类名称,即:World、Sports、Business、Sci/Tec;
    • train.csvtest.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

  1. 主要是用AG news数据集进行处理生成dataset_traindataset_test ;
  2. 具体实现主要基于fastNLP
  3. 主要的处理流程包括:
    • 读入数据;
    • label转为int类型;
    • title和description转小写;
    • 用空格分割description得到单词序列;
    • 计算单词序列的长度并得到文本中最大的句子包含的句子长度max_se_len
    • 根据训练数据构建词汇表;
    • 根据词汇表将每个单词序列中的单词提换出呢个对应的单词id;
    • 对短的单词序列用0做padding,全部变成等长的序列(DPCNN模型需要);
  4. 可以按照以上的处理思路自己用PyTorch写一个数据加载的代码,也可以按照fastNLP的需求配置创建一个虚拟环境(我自己是创建了一个名为fastnlp的虚拟环境的)
  5. 这部分的代码文件比较长:
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

  1. 这个在github上也能找到一些代码;
  2. 基于PyTorch实现的;
  3. 实现的时候参考了一个源码, 但是修改了他的一点小问题(添加了漏掉的一处shortcut);
  4. 贴一下修改之后的吧:
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 中间过程

训练阶段一共做了以下几组对比实验来确定比较好的训练条件,大家也可以参考一下:

  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;
  2. 关于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;
  3. 关于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;
    • 符合参数越多,网络能表示信息越多,但是越难训练的规律;
  4. 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;

实验结果

9933353-51fd5f14e6db084c.png
综合最优条件进行训练的实验结果:accuracy(%)

9933353-ec864d104cd55035.png
实验8运行情况

在没有用unsupervised embeding的情况下,最佳训练条件下得到的实验结果如图能够在测试集上达到 91.49% 的准确度,与DPCNN 原论文展示的 93.13% 仅相差 1.64%。

五 写在最后

暂时就先写这么多吧~~~~

写完一个报告又整理一篇博客的肌肉酸痛感,啊啊啊~如果觉得还可以的话,点个喜欢可以不可以呢(-o⌒) ☆

后面等我想整理的时候再传到github上好了。

猜你喜欢

转载自blog.csdn.net/weixin_33871366/article/details/87137231