当前位置:   article > 正文

史上最小白之TextCNN 中文文本分类实战_textcnn实战

textcnn实战

虽然现在已经有了异常强大的bert,效果也是非常好,但是bert啊,实在是太消耗计算资源了,本穷小子又买不起GPU服务器,只能使用colab进行学习,经常出现内存不够地情况,所以如果你也跟我一样没有比较好的GPU服务器,那么在做分类任务时,可以尝试选择TextCNN,而且目前在文本分类任务上TextCNN也取得了不错的效果。

上一篇文章:史上最小白之CNN 以及 TextCNN详解已经介绍了TextCNN的原理,这里就不再赘述了,不太明白地可以先去复习一下

为了一步一步的详细直观的解释TextCNN中文文本分类流程,以下代码均是在colab中一步一步运行,封装后的源码可以在我的github里下载:https://github.com/Stink1995/TextCNN/tree/master/TextCNN_Chinese

废话不多说,开始实战,奥利给!!!

1.数据源

1.1下载数据源

采用了清华NLP组提供的THUCNews新闻文本分类数据集的子集

数据下载链接:

THUCNews数据子集:https://pan.baidu.com/s/1NheROpFnwVJdd8GZoYfTzA 密码:i416

1.2 数据源分析

读取数据:

import pandas as pd
train_data = pd.read_csv('./drive/My Drive/TextCNN/cnews/train.tsv',sep='\t',names=['label','content'])
test_data = pd.read_csv('./drive/My Drive/TextCNN/cnews/test.tsv',sep='\t',names=['label','content'])
dev_data = pd.read_csv('./drive/My Drive/TextCNN/cnews/dev.tsv',sep='\t',names=['label','content'])
  • 1
  • 2
  • 3
  • 4

查看源数据有多少条数据、是否存在缺失值、是否存在数据分布不平衡现象

train_data.head()
  • 1

train_data.info
  • 1

(img-Wjl9tU3h-1583527921349)(/Users/liyehong/Library/Application Support/typora-user-images/image-20200227052203044.png)]

可以看到总共有50000条中文数据,不存在空值

from collections import Counter
Counter(list(train_data.label))
  • 1
  • 2

在这里插入图片描述

发现每种类别的数据都是5000条,并不存在数据分布不均衡的现象。简单查看了一下数据源的情况后,接下来就可以进行数据预处理了。

1.3 数据源预处理

首先获取停用词

stopwords_path = './drive/My Drive/TextCNN/cnews/哈工大停用词表.txt'
stopwords = open(stopwords_path).read().split('\n')
  • 1
  • 2

然后建立分词函数,分词采用jieba分词。

import jieba 
def cut(sentence):
  return [token for token in jieba.lcut(sentence) if token not in stopwords]
  • 1
  • 2
  • 3

之后就是建立词表,什么是词表?

词表就是包含数据集中的文本经过分词和去除停用词之后的所有词语的一张表,为了构建数据集中所有词语的词向量矩阵而准备。

建立两个字典,一个是{word:index},一个是{index:word},目的是为了能够把word和词向量矩阵对应起来。可以自己写,推荐使用Torchtext包,接下来介绍一下Torchtext的基本用法,更多详细的内容可以查看Torchtext的官方文档。

import torchtext
import torch
#声明一个Field对象,对象里面填的就是需要对文本进行哪些操作,比如这里lower=True英文大写转小写,tokenize=cut对于文本分词采用之前定义好的cut函数,sequence=True表示输入的是一个sequence类型的数据,还有其他更多操作可以参考文档
TEXT = torchtext.data.Field(sequential=True,lower=True,tokenize=cut)
#声明一个标签的LabelField对象,sequential=False表示标签不是sequence,dtype=torch.int64标签转化成整形
LABEL = torchtext.data.LabelField(sequential=False, dtype=torch.int64)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
#这里主要是告诉torchtext需要处理哪些数据,这些数据存放在哪里,TabularDataset是一个处理scv/tsv的常用类
train_dataset,dev_dataset,test_dataset = torchtext.data.TabularDataset.splits(
      path='./drive/My Drive/TextCNN/cnews',  #文件存放路径
      format='tsv',   #文件格式
      skip_header=False,  #是否跳过表头,我这里数据集中没有表头,所以不跳过
      train='train.tsv',  
      validation='dev.tsv',
      test='test.tsv',    
      fields=[('label',LABEL),('content',TEXT)] # 定义数据对应的表头
  )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

接下来定义预训练词向量,可以自己训练,但预训练好的词向量收敛速度更快一点,自己预训练的词向量效果也不一定好。

预训练词向量下载地址:

里面有很多类型的中文词向量,可以根据自己的需求下载

Chinese Word Vectors 中文词向量:https://github.com/Embedding/Chinese-Word-Vectors

这篇文章使用的是 Sogou News 搜狗新闻 Word2vec词向量 基于skip-gram Word + Character + Ngram

pretrained_name = 'sgns.sogou.word' # 预训练词向量文件名
pretrained_path = './drive/My Drive/TextCNN/word_embedding' #预训练词向量存放路径
vectors = torchtext.vocab.Vectors(name=pretrained_name, cache=pretrained_path)
  • 1
  • 2
  • 3

然后建立词表,直接调用torchtext的build_vocab函数

TEXT.build_vocab(train_dataset, dev_dataset,test_dataset,
                 vectors=vectors)
LABEL.build_vocab(train_dataset, dev_dataset,test_dataset)
  • 1
  • 2
  • 3

我们来看一下建立好了之后的效果:

查看词表的size,也就是有多少个词
在这里插入图片描述
查看词向量:
在这里插入图片描述
词表中词向量的维度:
在这里插入图片描述

查看word对应的索引index:
在这里插入图片描述
查看索引index对应的word:
在这里插入图片描述
接下来最后一步,生成迭代器:

# torchtext.data.BucketIterator.splits 使用BucketIterator生成迭代器的主要是因为BucketIterator能够将样本长度接近的句子尽量放在同一个batch里面,这样假如这里我们每128个样本为一个batch,句子长度差距过大,就会给短句加入过多的无意义的<pad>,但是句子长度相近的在一个batch里面的话,就能够避免这个问题
train_iter, dev_iter,test_iter = torchtext.data.BucketIterator.splits(
        (train_dataset, dev_dataset,test_dataset), #需要生成迭代器的数据集
        batch_sizes=(128, 128,128), # 每个迭代器分别以多少样本为一个batch
        sort_key=lambda x: len(x.content) #按什么顺序来排列batch,这里是以句子的长度,就是上面说的把句子长度相近的放在同一个batch里面
        )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

截止以上步骤,数据预处理的部分就全部做完了,接下来就开始咱们的建模啦!!!

2.基于Pytorch TextCNN 建模

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

class TextCNN(nn.Module):
    def __init__(self, 
                 class_num, # 最后输出的种类数 
                 filter_sizes, # 卷积核的长也就是滑动窗口的长 
                 filter_num,   # 卷积核的数量 
                 vocabulary_size, # 词表的大小
                 embedding_dimension, # 词向量的维度
                 vectors, # 词向量
                 dropout): # dropout率
        super(TextCNN, self).__init__() # 继承nn.Module

        chanel_num = 1  # 通道数,也就是一篇文章一个样本只相当于一个feature map

        self.embedding = nn.Embedding(vocabulary_size, embedding_dimension) # 嵌入层 
        self.embedding = self.embedding.from_pretrained(vectors) #嵌入层加载预训练词向量

        self.convs = nn.ModuleList(
            [nn.Conv2d(chanel_num, filter_num, (fsz, embedding_dimension)) for fsz in filter_sizes])  # 卷积层
        self.dropout = nn.Dropout(dropout) # dropout
        self.fc = nn.Linear(len(filter_sizes) * filter_num, class_num) #全连接层

    def forward(self, x):
        # x维度[句子长度,一个batch中所包含的样本数] 例:[3451,128]
        x = self.embedding(x) # #经过嵌入层之后x的维度,[句子长度,一个batch中所包含的样本数,词向量维度] 例:[3451,128,300]
        x = x.permute(1,0,2) # permute函数将样本数和句子长度换一下位置,[一个batch中所包含的样本数,句子长度,词向量维度] 例:[128,3451,300]
        x = x.unsqueeze(1) # # conv2d需要输入的是一个四维数据,所以新增一维feature map数 unsqueeze(1)表示在第一维处新增一维,[一个batch中所包含的样本数,一个样本中的feature map数,句子长度,词向量维度] 例:[128,1,3451,300]
        x = [conv(x) for conv in self.convs] # 与卷积核进行卷积,输出是[一个batch中所包含的样本数,卷积核数,句子长度-卷积核size+1,1]维数据,因为有[3,4,5]三张size类型的卷积核所以用列表表达式 例:[[128,16,3459,1],[128,16,3458,1],[128,16,3457,1]]
        x = [sub_x.squeeze(3) for sub_x in x]#squeeze(3)判断第三维是否是1,如果是则压缩,如不是则保持原样 例:[[128,16,3459],[128,16,3458],[128,16,3457]]
        x = [F.relu(sub_x) for sub_x in x] # ReLU激活函数激活,不改变x维度 
        x = [F.max_pool1d(sub_x,sub_x.size(2)) for sub_x in x] # 池化层,根据之前说的原理,max_pool1d要取出每一个滑动窗口生成的矩阵的最大值,因此在第二维上取最大值 例:[[128,16,1],[128,16,1],[128,16,1]]
        x = [sub_x.squeeze(2) for sub_x in x] # 判断第二维是否为1,若是则压缩 例:[[128,16],[128,16],[128,16]]
        x = torch.cat(x, 1) # 进行拼接,例:[128,48]
        x = self.dropout(x) # 去除掉一些神经元防止过拟合,注意dropout之后x的维度依旧是[128,48],并不是说我dropout的概率是0.5,去除了一半的神经元维度就变成了[128,24],而是把x中的一些神经元的数据根据概率全部变成了0,维度依旧是[128,48]
        logits = self.fc(x) # 全接连层 例:输入x是[128,48] 输出logits是[128,10]
        return logits
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

以上就是基于Pytorch 的TextCNN的建模全过程了,基本上每一步都做了非常详细的注释,要是还有不懂地,可以在评论里面留言哟。模型建好后,接下来就是训练了。

3.TextCNN模型的训练

class_num = len(LABEL.vocab) # 类别数目
filter_size = [3,4,5]  # 卷积核种类数 
filter_num=16   # 卷积核数量
vocab_size = len(TEXT.vocab) # 词表大小
embedding_dim = TEXT.vocab.vectors.size()[-1] # 词向量维度
vectors = TEXT.vocab.vectors # 词向量
dropout=0.5 
learning_rate = 0.001  # 学习率
epochs = 5   # 迭代次数
save_dir = './drive/My Drive/TextCNN/model' # 模型保存路径
steps_show = 10   # 每10步查看一次训练集loss和mini batch里的准确率
steps_eval = 100  # 每100步测试一下验证集的准确率
early_stopping = 1000  # 若发现当前验证集的准确率在1000步训练之后不再提高 一直小于best_acc,则提前停止训练

textcnn_model = TextCNN(class_num=class_num,
        filter_sizes=filter_size,
        filter_num=filter_num,
        vocabulary_size=vocab_size,
        embedding_dimension=embedding_dim,
        vectors=vectors,
        dropout=dropout)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

定义train函数:

基本上Pytorch的train函数是一个固定的框架,自己根据需要稍微调整一下就行了。

def train(train_iter, dev_iter, model):

    if torch.cuda.is_available(): # 判断是否有GPU,如果有把模型放在GPU上训练,速度质的飞跃
      model.cuda()

    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) # 梯度下降优化器,采用Adam
    steps = 0
    best_acc = 0
    last_step = 0
    model.train()
    for epoch in range(1, epochs + 1): 
        for batch in train_iter:
            feature, target = batch.content, batch.label
            if torch.cuda.is_available(): # 如果有GPU将特征更新放在GPU上
              feature,target = feature.cuda(),target.cuda() 
            optimizer.zero_grad() # 将梯度初始化为0,每个batch都是独立训练地,因为每训练一个batch都需要将梯度归零
            logits = model(feature)
            loss = F.cross_entropy(logits, target) # 计算损失函数 采用交叉熵损失函数
            loss.backward()  # 反向传播
            optimizer.step() # 放在loss.backward()后进行参数的更新
            steps += 1 
            if steps % steps_show == 0: # 每训练多少步计算一次准确率,我这边是1,可以自己修改
                corrects = (torch.max(logits, 1)[1].view(target.size()).data == target.data).sum() # logits是[128,10],torch.max(logits, 1)也就是选出第一维中概率最大的值,输出为[128,1],torch.max(logits, 1)[1]相当于把每一个样本的预测输出取出来,然后通过view(target.size())平铺成和target一样的size (128,),然后把与target中相同的求和,统计预测正确的数量
                train_acc = 100.0 * corrects / batch.batch_size # 计算每个mini batch中的准确率
                print('steps:{} - loss: {:.6f}  acc:{:.4f}'.format(
                  steps,
                  loss.item(),
                  train_acc))
                
            if steps % steps_eval == 0: # 每训练100步进行一次验证
              dev_acc = dev_eval(dev_iter,model)
              if dev_acc > best_acc:
                best_acc = dev_acc
                last_step = steps
                print('Saving best model, acc: {:.4f}%\n'.format(best_acc))
                save(model,save_dir, steps)
              else:
                if steps - last_step >= early_stopping:
                  print('\n提前停止于 {} steps, acc: {:.4f}%'.format(last_step, best_acc))
                  raise KeyboardInterrupt
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
def dev_eval(dev_iter,model):
  model.eval()
  corrects, avg_loss = 0, 0
  for batch in dev_iter:
      feature, target = batch.content, batch.label
      if torch.cuda.is_available():
          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(dev_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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
# 定义模型保存函数
def save(model, save_dir, steps):
    if not os.path.isdir(save_dir):
        os.makedirs(save_dir)
    save_path = 'bestmodel_steps{}.pt'.format(steps)
    save_bestmodel_path = os.path.join(save_dir, save_path)
    torch.save(model.state_dict(), save_bestmodel_path)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
train(train_iter,dev_iter,textcnn_model)
  • 1

最后结果:
在这里插入图片描述
迭代了10次,卷积核也只选了16个,还有很多其他可以优化的地方,这里也就只写一个初步的流程,效果也还可以了,验证集上准确率96.2%,可以自己再对超参数进行调参,效果应该还会提升。

我们再看看测试集准确率:
在这里插入图片描述
测试集准确率为96.55%,也还行。

4.结语

以上就是本期TextCNN中文文本分类的全部内容了,不知道你学会了没?
生命不息,学习不止。
大家一起加油吧!!!

5.参考

https://github.com/bigboNed3/chinese_text_cnn

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/353333
推荐阅读
相关标签
  

闽ICP备14008679号