让我“康康”! Pytorch框架下,用TextCNN实现的文本分类.

TextCNN简介

CNN,全称卷积神经网络(Convolutional neural network),是计算机视觉领域(CV)最常见的一种网络之一,那么这种模型有什么用呢?
其实最早这种网络是用来对图片中所包含的大量信息进行压缩降维度和特征提取的.不难想象,如今一张图片的像素通常是800*600意味着这个图片至少含有对应的480000个点,而且如果是彩色图片,则通常还有RGB三个通道,意味着一共是3*800*600个像素点,144万个数字.这个维度对于计算机处理来说或许尚且不困难,但要是一百张,一万张,那么复杂度可想而知.于是大牛们发明了CNN卷积神经网络,通过采用卷积和池化,将图片进行降维,在尽可能保留图片信息的同时,使得计算机能够对这些数据进行处理.
而大牛们将CNN运用到文本任务时发现效果还不错,于是也就有了TextCNN的出现.

实验部分

语料数据准备

这里我采用的语料是一个DF平台上的一个比赛互联网新闻情感分析各位有兴趣的可以自行下载一下.当初在比赛的时候,有尝试着用WordVec2+LSTM去试试水,但其实最后发现人家都在用多层的BERT+GRU…留下没算力的泪水,看到有人用了TextCNN写的baseline,效果也还不错,甚至于可以媲美BERT,于是也来自己模仿一下嗷.

语料数据预处理

对于语料数据,我仅仅采用了其中的‘title’字段,一方面是因为考虑到content部分过于冗杂,title能更直接浓缩反应整篇报道的情感含义.另一方面也是为了自己的老MAC着像,毕竟老了跑不动了.
处理也是基本的分词,收集词袋,创建词表,再将词映射成对应的数字.然后用了nn.Embedding()来映射成对应的向量,目前对这个做法我尚且存疑,毕竟没有查到nn.Embedding()所采用的映射方式是什么.姑且先当作是Word2Vec的嵌入使用了.
这部分代码是删除空行,同时合并数据和对应的标签,比较坑的一点是,标签数目和数据的数目不一致,且一些号码对不上,起初坑了我好久.

def Data_Process(traindata, trainlabel):
    """处理所用的数据"""

    # 删除对应的nan行
    dellist = np.where(traindata['title'].isna())[0].tolist()
    data = traindata.drop(dellist)
    data = pd.merge(data, trainlabel, on='id')  # 数据库的连接部分
    title = data['title']

    # 收集词袋,创建对应的w2id表
    maxlength = 0
    wordofbag = set()
    for row in title:
        # 采用jieba分词,就保留
        splitrow = jieba.lcut(row)
        maxlength = max(len(splitrow), maxlength)  # 计算最长的句子长度,便于之后创建使用
        for word in splitrow:
            wordofbag.add(word)

    wordofbag = list(wordofbag)
    id = [i for i in range(len(wordofbag))]
    w2id = dict(zip(wordofbag, id))
    id2w = dict(zip(id, wordofbag))
    return w2id, id2w, wordofbag, data, maxlength

此处将每一句话的词都转换成对应的数字,注意因为分词和原本的句子长度导致的最后的向量长度会存在不一致,这里我们选取最长的一句来作为我们的统一向量的长度,是为了后面数据喂入的时候可以使得矩阵统一,对于那些不足最长长度的向量,显而易见的可以用0来填充.以避免相应的数据干扰.

def Transfer_Word(title, w2id, row, column):
    # 将文本转换成对应的数字
    transtitle = np.zeros((row, column))
    for i in range(len(title)):
        splitrow = jieba.lcut(title[i])
        transrow = np.zeros(column)
        for j in range(len(splitrow)):
            transrow[j] = w2id[splitrow[j]]
        transtitle[i] = transrow
    return transtitle

至此数据预处理全部完成,下面开始搭建模型.

模型搭建嗷

在搭建模型时候最让人厌烦的就是调整维度了,好在是有找到一张TextCNN的图,让自己一下子豁然开朗.

其实图中已经写的也比较仔细了,首先将句子映射成向量矩阵.然后采用3个不同的卷积核,从上往下依次是[4,3,2].依此搭建出来的代码如下:

    def __init__(self, args):
        super(TextCNN, self).__init__()
        self.args = args

        label_num = args['label']  # 最后输出的维度
        filter_num = args['num']  # 核的数量
        filter_sizes = args['filter_sizes']  # 核的第二个维度
        vocab_size = args['vocab_size']  # 初始给定维度
        embedding_dim = args['embedding_dim']  # 嵌入后的维度
        seq_len = args['seq_len']

        self.embedding = nn.Embedding(vocab_size, embedding_dim)  # 对数字进行嵌入

        # 多个一维的卷积层
        self.conv = nn.ModuleList([
            nn.Sequential(
                nn.Conv1d(in_channels=embedding_dim, out_channels=filter_num, kernel_size=size),
                nn.ReLU(),
                nn.MaxPool1d(kernel_size=seq_len - size + 1),  # 在后面前馈的过程再进行池化-->实际不可
            ) for size in filter_sizes])
        # 防止过拟合的dropout层
        self.dropout = nn.Dropout()

        # 池化后的拼接的向量的维度,因为是最大池化,每一个分类器只提供一个数据,这里我们只用一个线性层即可.
        self.relu = nn.ReLU()
        self.Linear = nn.Linear(in_features=filter_num * len(filter_sizes), out_features=label_num)
        self.softmax = nn.LogSoftmax()  # 输出概率向量,但是损失函数需要使用NLLLoss()

这里采用nn.Sequential来搭建多个不同维度的卷积核,因为文本是不存在像图片一样的通道数的概念的,于是我们可以吧通道数来当作每个核对应的数目来使用.于是一共就有6个卷积和
搭建模型以后,还需要进行前馈操作的设定.
依旧先给出前馈过程的代码.

    def forward(self, x):
        # 前馈过程
        out = self.embedding(x)
        out = torch.transpose(out, 1, 2)
        out = [conv(out) for conv in self.conv]

        # 池化后进行连接.
        out = torch.cat(out, dim=1)
        out = torch.squeeze(out, 2)  # 从n*6*1转换为二维
        out = self.relu(out)
        out = self.Linear(out)
        # 转换成概率
        out = self.softmax(out)
        return out

代码并不复杂,大致的操作是在对原矩阵进行卷积后,在做一次对应的最大池化操作.而后将最大池化的结果进行连接.从而形成一个1*6维的向量,再输入到线性层中.最后用Softmax输出概率.

至此模型搭建基本完成,剩下的只有喂入数据撩.

数据输入

对应的我们需要在数据输入的部分给一些可以调控的TextCNN的参数.同时可以看到我用的损失函数是nn.NLLLoss(),这是因为使用交叉熵函数的话,会自动调用softmax的函数.为了可以将这个操作拆开我就使用了NLLLoss,同时采用交叉五折提高模型的性能.

    args = {
   
        'label': 3,
        'num': 2,
        'filter_sizes': [3, 4, 5],  # 一共6层
        'vocab_size': vocab_size,
        'embedding_dim': 300,
        'static': True,
        'fine_tuning': True,
        'seq_len': maxlength
    }
    model = TextCNN(args)
    Loss = nn.NLLLoss()  # 定义损失函数
    optimizer = torch.optim.Adam(model.parameters(), lr=0.02)

    label = np.array(data['label'])
    kfold = StratifiedKFold(n_splits=5, shuffle=True, random_state=2019)  # 五折交叉验证
    for train, valid in kfold.split(transtitle, label):
        for epoch in range(10):  # 不等长数据行会产生错误.处理的时候记得整理成同一种
            #训练部分
            traintitle = torch.LongTensor(transtitle[train])  # 传入的应该是Longtensor类型
            trainlabel = torch.LongTensor(label[train])
            prediction = model(traintitle)
            loss = Loss(prediction, trainlabel)
            optimizer.zero_grad()  # 自动求导
            loss.backward()
            optimizer.step()

            #验证部分
            correct = 0
            vailddata = torch.LongTensor(transtitle[valid])
            vaildlabel = torch.LongTensor(label[valid])
            vaildprediction = model(vailddata)
            # 对应的argmax中的1是指第二个维度
            correct = np.mean((torch.argmax(vaildprediction, 1) == vaildlabel).sum().numpy())
            loss = Loss(vaildprediction, vaildlabel)
            print("valid_loss:", loss.data.item(), "ACC:", correct/len(valid))

总结

最后的结果基本上可以达到接近97%以上,就线下的结果来说还是蛮不错的,而且也仅仅跑了5折10个epoch.所以效果还是可以的.而且最近在刷面经的时候,有看到BERT的性能和TextCNN相似的问题,因此其实TextCNN还是有发掘的空间的嗷.

参考文献

GitHub原码分享
Pytorch CNN搭建(NLP)
卷积神经网络超详细介绍