TextCNN的模型理论与应用实战

(一)说明

       本文从理论与实践入手对TextCNN进行模型讲解与模型应用实战,理论方面对TextCNN进行模型解读,14年论文《Convolutional Neural Networks for Sentence Classification》给出了模型的基本结构,16年论文《A Sensitivity Analysis of (and Practitioner Guide to) Convolutional Neural Networks for Sent》中进行了大量的调参实验,给出了TextCNN进行文本分类时的相关建议。本文在这里不对论文进行详细解读,相关内容请查看目录2.5参考文献。实战方面将利用pytorch实现中文文本分类(参考),现附论文及项目链接。
1.《Convolutional Neural Networks for Sentence Classification》
2.《A Sensitivity Analysis of (and Practitioner Guide to) Convolutional Neural Networks for Sent》
3. pytorch实现TextCNN中文文本分类

(二)模型解读

网络结构                            图1:来自《Convolutional Neural Networks for Sentence Classification》

                            
            图2:来自《A Sensitivity Analysis of (and Practitioner Guide to) Convolutional Neural Networks for Sent》

       模型结构示意图如上所示,整个模型结构可分为4层,输入层(嵌入层)、卷积层、最大池化层及全连接层,图2更为直观。

2.1输入层

       如上图所示,模型的输入是由每个句子中词的词向量构成的矩阵所组成,矩阵的shape为N*K,其中K为词向量的长度,N为句子的长度。模型根据词向量的不同分为4种,CNN-rand、CNN-static、CNN-non-static、CNN-multichannel。
       CNN-rand:所有的词向量都是随机初始化的,在训练过程中更新。
       CNN-static:词向量用word2vec得出的结果,在整个train process中所有的words保持不变,只学习其他参数。
       CNN-non-static:预训练的词向量在训练过程中要被微调。
       CNN-multichannel:初始化时两个channel都直接赋值word2vec得出的结果,每个filter也会分别applied到两个channel,但是训练过程中只有一个channel会进行BP,即一组保持词向量不变,一组进行词向量微调。
       对于没有出现在训练好的词向量表中的词(未登录词)的词向量,论文实验中采取的是使用随机初始化为0或者偏小的正数表示。

2.2卷积层

       在卷积层中,使用Filter进行卷积操作得到Feature Map。实验中使用的3种类型大小的Filter,宽度分别是3,4,5,长度为词向量的维度大小。其中每种类型大小的Filter 有100个含有不同值的Filter。每一个Filter能从输入的矩阵中抽取出一个Feature Map。
       从模型结构图中,第二层有4个Feature Map(经过卷积之后的结果),4个Feature Map是分别经过4个Filter卷积之后的结果,两个2*wordVector_dim(词向量维度)的Filter和两个3*wordVector_dim的Filter。每一个Filter分别作用于每个通道,并将结果相加进行卷积运算。

2.3池化层

       池化采用1-max pooling,即将每个Feature Map向量中最大的一个值抽取出来,所有最大值组成一个一维向量。
                                          
更多关于Max Pooling(转载自张俊林博客)

2.4全连接层

       池化操作后形成的一维向量作为全连接层的输入,再加上Dropout层防止过拟合。并在全连接层上添加l2正则化参数,经过SoftMax层作为输出层,进行分类。对于多分类问题可以使用SoftMax层,对于二分类问题可以使用一个含有sigmod激活函数的神经元作为输出层,实验中采用的是SoftMax层。

2.5参考文献

【1】《Convolutional Neural Networks for Sentence Classification》论文解读(推荐阅读:包含了作者对论文的认识并给出了论文翻译)
【2】论文笔记:A Sensitivity Analysis of Convolutional Neural Networks for Sentence Classification
【3】深度学习:TextCNN

(三)应用实战

       掌握理论后,接下来就是利用模型跑个实验,项目使用pytorch实现,项目代码完全借鉴自TextCNN Pytorch实现 中文文本分类 情感分析(作为NLP小白,现阶段要好好研读别人的代码…),所以在后文给出的代码中,我会给出略为详细的注释,方便更好的理解代码,后文截取了些主要的代码段,并不是完整代码,完整代码请看github。

3.1工具包

  • torchtext:在数据处理阶段,项目采用torchtext进行数据预处理,torchtext是一文本处理神器,可以方便的对文本进行预处理,例如截断补长、构建词表等,使用pip install torchtext可直接进行安装,关于torchtext的文档与教程附在这里。
    torchtext文档
    [TorchText]使用
    pytorch学习笔记(十九):torchtext
  • jieba:jieba是中文分词工具,需要给句子进行分词操作,pip install jieba可进行安装。
  • 预训练的中文词向量:CNN-static和CNN-multichannel需要使用预训练的中文词向量,项目使用Zhihu_QA 知乎问答(点击直接下载)训练出来的word Word2vec

3.2数据预处理

       数据集在data文件里已经给出,要用到train.tsv和dev.tsv
在这里插入图片描述
       使用torchtext对数据集进行处理:

  • 设定text_field和label_field两个field。定义以及参数含义看上面的文档或教程。
import torchtext.data as data

print('Loading data...')
//使用torchtext.data
text_field = data.Field(lower=True)
label_field = data.Field(sequential=False)
train_iter, dev_iter = load_dataset(text_field, label_field, args, device=-1, repeat=False, shuffle=True)
  • load_dataset数据加载函数,load_word_vectors词向量加载函数。
from torchtext.vocab import Vectors


//加载预训练的词向量
def load_word_vectors(model_name, model_path): 
   //使用torchtext.vocab的Vectors
   vectors = Vectors(name=model_name, cache=model_path)
   return vectors 

//加载数据:args接收命令行参数,**kwargs接收键值对: device=-1, repeat=False, shuffle=True
def load_dataset(text_field, label_field, args, **kwargs):
   //1、加载数据语料
   train_dataset, dev_dataset = dataset.get_dataset('data', text_field, label_field)
   
   // 查看dataset长什么样子
   // for i in range(0,3):
   //     print(dev_dataset[i].text)
   //     print(dev_dataset[i].label)
   //     print('=======')
   
   //2、构建词表,根据命令行参数判断是否使用预训练词向量,True的话则加载词向量    
   if args.static and args.pretrained_name and args.pretrained_path:
       vectors = load_word_vectors(args.pretrained_name, args.pretrained_path)
       text_field.build_vocab(train_dataset,dev_dataset,vectors=vectors)
       
       // 查看text_field词汇表
       // print(text_field.vocab.stoi)
       
   else:
       text_field.build_vocab(train_dataset, dev_dataset)
   label_field.build_vocab(train_dataset, dev_dataset)
   
   //3、构建迭代器
   train_iter, dev_iter = data.Iterator.splits(
       (train_dataset, dev_dataset),
       batch_sizes=(args.batch_size, len(dev_dataset)),
       sort_key=lambda x: len(x.text),
       **kwargs)
       
   // 查看batch啥样
   // batch = next(iter(train_iter))
   // print(batch)
   // print("batch_text:",batch.text)
   // print("batch_label:",batch.label)
   
   return train_iter, dev_iter

【Python:什么是*args和**kwargs】

  • get_dataset加载数据语料。
//正则匹配非中文和字母数字
regex = re.compile(r'[^\u4e00-\u9fa5aA-Za-z0-9]')


def word_cut(text):
    //将非中文和字母及数字的字符替换为空格
    text = regex.sub(' ', text)

    //对句子进行分词,空格字符不要
    return [word for word in jieba.cut(text) if word.strip()]


def get_dataset(path, text_field, label_field):
    //torchtext的用法tokenize 表示如何对文本进行分割
    text_field.tokenize = word_cut
    //通过 torchtext.data.Dataset 的类方法 splits 加载我们的数据语料
    train, dev = data.TabularDataset.splits(
        path=path, format='tsv', skip_header=True,// 如果tsv有表头, 确保这个表头不会作为数据处理
        train='train.tsv', validation='dev.tsv',
        fields=[
            ('index', None),
            ('label', label_field),
            ('text', text_field)
        ]
    )
    return train, dev

3.3模型定义

       模型构建过程中,max-pooling直接在foward中使用,注释标出了tensor流动时的shape以及一些函数的作用。

import torch
import torch.nn as nn
import torch.nn.functional as F

//模型参数:
//batch-size:128
//dropout:0.5
//embedding-dim:300
//filter-num:100
//filter-size:345

class TextCNN(nn.Module):
    def __init__(self, args):
        super(TextCNN, self).__init__()
        self.args = args
		//label数量
        class_num = args.class_num
        chanel_num = 1
        filter_num = args.filter_num
        filter_sizes = args.filter_sizes

        vocabulary_size = args.vocabulary_size
        embedding_dimension = args.embedding_dim
        //输入层
        self.embedding = nn.Embedding(vocabulary_size, embedding_dimension)
        if args.static:
            // 加载预训练的词向量,freeze使词向量不更新
            self.embedding = self.embedding.from_pretrained(args.vectors, freeze=not args.non_static)
        if args.multichannel:
            //加一个通道,此通道可更新权重
            self.embedding2 = nn.Embedding(vocabulary_size, embedding_dimension).from_pretrained(args.vectors)
            chanel_num += 1
        else:
            self.embedding2 = None
        //卷积层
        self.convs = nn.ModuleList(
            [nn.Conv2d(chanel_num, filter_num, (size, embedding_dimension)) for size in filter_sizes])
        self.dropout = nn.Dropout(args.dropout)
        //全连接层
        self.fc = nn.Linear(len(filter_sizes) * filter_num, class_num)

    def forward(self, x):
        //x的shape为128*L,即batch-size*L,L为序列长度
        //embedding之后 变为128*L*300
        if self.embedding2:
            //此时x的shape为128*2*L*300,stack将两个序列合并
            x = torch.stack([self.embedding(x), self.embedding2(x)], dim=1)
        else:
            // x:128*L*300
            x = self.embedding(x)
            //x:128*1*L*300
            x = x.unsqueeze(1)
        //conv输入:[ batch_size, channels, L, Dim ]
        x = [F.relu(conv(x)).squeeze(3) for conv in self.convs]  //x:len(Ks)*(batch_size,channels,L1)   
        // (因为卷积核的维度与词向量一样所以最后一维会变成1,用squeeze将其去掉,此时L1是卷积之后向量的长度)
        x = [F.max_pool1d(item, item.size(2)).squeeze(2) for item in x] //x: len(Ks)*(batch_size,channels)
        //将不同size池化后的结果拼接在一起
        x = torch.cat(x, 1) //(batch_size,channels*len(Ks))
        x = self.dropout(x)
        logits = self.fc(x)
        return logits

【torch.stack()用法】
【torch.cat()用法】
【torch.nn.Conv2d()函数详解】

3.4训练与评估

def train(train_iter, dev_iter, model, args):
    if args.cuda:
        model.cuda()
    optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
    steps = 0
    best_acc = 0
    last_step = 0
    model.train()
    for epoch in range(1, args.epochs + 1):
        for batch in train_iter:
            //feature:len*128(L*batch)
            // target:1*128
            feature, target = batch.text, batch.label
            //feature:128*len
            feature.data.t_(), target.data.sub_(1)
            if args.cuda:
                feature, target = feature.cuda(), target.cuda()
            optimizer.zero_grad()
            logits = model(feature)
            //cross_entropy target为样本标签
            loss = F.cross_entropy(logits, target)
            loss.backward()
            optimizer.step()
            steps += 1
            if steps % args.log_interval == 0:
                corrects = (torch.max(logits, 1)[1].view(target.size()).data == target.data).sum()
                train_acc = 100.0 * corrects / batch.batch_size
                sys.stdout.write(
                    '\rBatch[{}] - loss: {:.6f}  acc: {:.4f}%({}/{})'.format(steps,
                                                                             loss.item(),
                                                                             train_acc,
                                                                             corrects,
                                                                             batch.batch_size))
            if steps % args.test_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:
                        print('Saving best model, acc: {:.4f}%\n'.format(best_acc))
                        save(model, args.save_dir, 'best', steps)
                else:
                    if steps - last_step >= args.early_stopping:
                        print('\nearly stop by {} steps, acc: {:.4f}%'.format(args.early_stopping, best_acc))
                        raise KeyboardInterrupt


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_(), target.data.sub_(1)
        if args.cuda:
            feature, target = feature.cuda(), target.cuda()
        logits = model(feature)
        loss = F.cross_entropy(logits, target)
        avg_loss += loss.item()
        corrects += (torch.max(logits, 1)
                     [1].view(target.size()).data == target.data).sum()
    size = len(data_iter.dataset)
    avg_loss /= size
    accuracy = 100.0 * corrects / size
    print('\nEvaluation - loss: {:.6f}  acc: {:.4f}%({}/{}) \n'.format(avg_loss,
                                                                       accuracy,
                                                                       corrects,
                                                                       size))
    return accuracy

模型保存:

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)

3.5结果

CNN-rand:
在这里插入图片描述
CNN-static:
在这里插入图片描述
CNN-non-static:
在这里插入图片描述
CNN-multichannel:
在这里插入图片描述

(四)总结

       博主是NLP小白,这也是第一次写博客,想通过理论+实战的方法更好的理解模型,学习模型。之前一直不喜欢写博客,觉得很浪费时间…其实现在知道这些记录和归纳是必要的,希望自己再接再厉吧。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值