基于TextCNN(pytorch实现)的文本情感分类

  TextCNN源于2014年一篇NLP领域的论文:《Convolutional Neural Networks for Sentence Classification》
论文链接:https://arxiv.org/abs/1408.5882

  TextCNN应该算是CNN应用于文本分类最经典的模型。
  下面这幅图源于论文,通过这幅图其实就能知道TextCNN的核心思想了。
图来源于论文
  这里采取不同大小卷积核进行卷积,这里卷积核的宽度不同,起到的作用就类似于N-gram,(N-gram本身也指一个由 N N N个单词组成的集合,各单词具有先后顺序,且不要求单词之间互不相同。常用的有 Bi-gram (即认为当前词只与其前面一个词有关) 和 Tri-gram ( 即认为当前词只与其前面2个词有关))。比如上图中卷积核宽度为2时,就同时考虑到了wait和for两个单词,起到了类似于Bi-gram的作用。
下面两幅图引自谋篇博客(忘记哪篇了):
在这里插入图片描述
  可以看到输入时先要经过embedding,然后再采取不同大小卷积核进行卷积,最后将他们卷积的结果进行拼接。然后max-pooling,全连接。
在这里插入图片描述

  在这之前我先讲一下后面需要用到的一个关键的东西,torch.nn.embedding(),也就是词嵌入得到词向量。
  nn.embedding:这是一个矩阵类,里面初始化了一个随机矩阵,矩阵的长是字典的大小,宽是用来表示字典中每个元素的属性向量,向量的维度根据你想要表示的元素的复杂度而定。类实例化之后可以根据字典中元素的下标来查找元素对应的向量。

这句话引用自:https://blog.csdn.net/tommorrow12/article/details/80896331

  你也可以使用nn.Embedding.from_pretrained()加载预训练好的模型,如word2vec,glove等,在训练的过程中也可以边训练,边更新词向量,加快模型的收敛。本文用的只是简单的nn.embedding()嘿嘿~

  因为输入的句子长度不一,有的长有的短。我的做法是,长了截断,不够长补齐(我文中用’<PAD>'填充,然后在nn.embedding层将其补0,也就是用它来表示无意义的词,这样在后面的max-pooling层也就自然而然会把其过滤掉,这样就不用担心他会影响识别。)

  这里说一下它的用法:
  nn.embedding()的主要3个参数:
  第一个参数num_embeddings是指词表大小
  第二个参数embedding_dim是指你需要用多少维来表示一个符号
  第三个参数pading_idx即需要用0填充的符号在词表中的位置,如下,输出中后面两个’<PAD>'都有被填充为了0.

import torch
import torch.utils.data as Data
import torch.nn as nn

import torch.nn.functional as F

#词表
word_to_id = {
    
    'hello':0, '<PAD>':1,'world':2}
embeds = nn.Embedding(len(word_to_id), 4,padding_idx=word_to_id['<PAD>'])

text = 'hello world <PAD> <PAD>'
hello_idx = torch.LongTensor([word_to_id[i] for i in text.split()])
#词嵌入得到词向量
hello_embed = embeds(hello_idx)
print(hello_embed)

输出:
tensor([[-1.1436, 1.4588, -1.2755, 0.0077],
[-0.9600, -1.9986, -1.1087, -0.1520],
[ 0.0000, 0.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000, 0.0000]], grad_fn=)

下面基于pytorch库实现:
  显卡:菜鸡1050
  torch版本:torch==1.6.0(cuda版本)
  数据集:英文电影评论(积极、消极)二分类
  准确率:85%,一开始没分词,只是用空格分开,只有70%多,后面分了下词去停用词,就达到85%。
分词表是我自己修改了nltk路径:
C:\用户\AppData\Roaming\nltk_data\corpora\stopwords里的english文件。
  然后你把我的my_english文件放进里面就可以,或者你直接用它的english
  数据集链接和my_english分词表都在以下网盘链接:
链接:https://pan.baidu.com/s/1J3MaSoCTF04w2gy2KMBBGA
提取码:c9xj
复制这段内容后打开百度网盘手机App,操作更方便哦

  引入相关库:

import torch
import numpy as np
import pandas as pd
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data
import torch.nn.functional as F

dtype = torch.FloatTensor
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)

  读入数据集,数据集为电影评论数据(英文,一个24500条数据),分为积极和消极两类:

df = pd.read_csv('labeledTrainData.tsv',sep='\t',header=0)
print('一共有{}条数据'.format(len(df)))

  分词去停用词:

from nltk.corpus import stopwords
import nltk
def separate_sentence(text):
    disease_List = nltk.word_tokenize(text)
    #去除停用词
    filtered = [w for w in disease_List if(w not in stopwords.words('my_english'))]
    #进行词性分析,去掉动词、助词等
    Rfiltered =nltk.pos_tag(filtered)
    #以列表的形式进行返回,列表元素以(词,词性)元组的形式存在
    filter_word = [i[0] for i in Rfiltered]
    return " ".join(filter_word)
df['sep_review'] = df['review'].apply(lambda x:separate_sentence(x))

  根据需要筛选数据(这里我使用了全部数据):

#用xxx条玩玩
use_df = df[:]
use_df.head(10)

  取出评论数据和标签:

sentences = list(use_df['sep_review'])
labels = list(use_df['sentiment'])

  用PAD填充长度没有达到pad_size的文本:

PAD = ' <PAD>'  # 未知字,padding符号用来填充长短不一的句子
pad_size =  64     # 每句话处理成的长度(短填长切)

for i in range(len(sentences)):
    sen2list = sentences[i].split()
    sentence_len = len(sen2list)
    if sentence_len<pad_size:
        sentences[i] += PAD*(pad_size-sentence_len)
    else:
        sentences[i] = " ".join(sen2list[:pad_size])

  设置一些参数和制作词表(后面用来给单词编号):

# TextCNN Parameter
num_classes = len(set(labels))  # num_classes=2
batch_size = 64
word_list = " ".join(sentences).split()
vocab = list(set(word_list))
word2idx = {
    
    w: i for i, w in enumerate(vocab)}
vocab_size = len(vocab)

  给单词编号(编完号后续还要embeding将其转成词向量):

def make_data(sentences, labels):
    inputs = []
    for sen in sentences:
        inputs.append([word2idx[n] for n in sen.split()])

    targets = []
    for out in labels:
        targets.append(out) # To using Torch Softmax Loss function
    return inputs, targets
input_batch, target_batch = make_data(sentences, labels)
input_batch, target_batch = torch.LongTensor(input_batch), torch.LongTensor(target_batch)

  用Data.TensorDataset(torch.utils.data)对给定的tensor数据(样本和标签),将它们包装成dataset:

from sklearn.model_selection import train_test_split
# 划分训练集,测试集
x_train,x_test,y_train,y_test = train_test_split(input_batch,target_batch,test_size=0.2,random_state = 0)

train_dataset = Data.TensorDataset(torch.tensor(x_train), torch.tensor(y_train))
test_dataset = Data.TensorDataset(torch.tensor(x_test), torch.tensor(y_test))
dataset = Data.TensorDataset(input_batch, target_batch)

  然后用Data.DataLoader(torch.utils.data)数据加载器,组合数据集和采样器,并在数据集上提供单进程或多进程迭代器。它可以对我们上面所说的数据集dataset作进一步的设置(比如可以设置打乱,对数据裁剪,设置batch_size等操作,很方便):

train_loader = Data.DataLoader(
    dataset=train_dataset,      # 数据,封装进Data.TensorDataset()类的数据
    batch_size=batch_size,      # 每块的大小
    shuffle=True,               # 要不要打乱数据 (打乱比较好)
    num_workers=2,              # 多进程(multiprocess)来读数据
)
test_loader = Data.DataLoader(
    dataset=test_dataset,      # 数据,封装进Data.TensorDataset()类的数据
    batch_size=batch_size,      # 每块的大小
    shuffle=True,               # 要不要打乱数据 (打乱比较好)
    num_workers=2,              # 多进程(multiprocess)来读数据
)

TextCNN结构:
  卷积层处采用了3种不同大小卷积核(2,300),(3,300),(4,300),其中300是每个单词的embeding后的维度。这样有利于提取多种语义信息。卷积完后利用torch.cat将3中卷积结果拼接。

class TextCNN(nn.Module):
    def __init__(self):
        super(TextCNN, self).__init__()
        self.filter_sizes = (2, 3, 4)
        self.embed = 300
        self.num_filters = 256
        self.dropout = 0.5
        self.num_classes = num_classes
        self.n_vocab = vocab_size
        #通过padding_idx将<PAD>字符填充为0,因为他没意义哦,后面max-pooling自然而然会把他过滤掉哦
        self.embedding = nn.Embedding(self.n_vocab, self.embed, padding_idx=word2idx['<PAD>'])
        self.convs = nn.ModuleList(
            [nn.Conv2d(1, self.num_filters, (k, self.embed)) for k in self.filter_sizes])
        
        self.dropout = nn.Dropout(self.dropout)
        self.fc = nn.Linear(self.num_filters * len(self.filter_sizes), self.num_classes)
        
    def conv_and_pool(self, x, conv):
        x = F.relu(conv(x)).squeeze(3)
        x = F.max_pool1d(x, x.size(2)).squeeze(2)
        return x
        
    def forward(self, x):
        out = self.embedding(x)
        out = out.unsqueeze(1)
        out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1)
        out = self.dropout(out)
        out = self.fc(out)
        return out

模型训练过程:

model = TextCNN().to(device)
criterion = nn.CrossEntropyLoss().to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)

# Training
for epoch in range(30):
    for batch_x, batch_y in train_loader:
        batch_x, batch_y = batch_x.to(device), batch_y.to(device)
        
        pred = model(batch_x)
        loss = criterion(pred, batch_y)
        if (epoch + 1) % 10 == 0:
            print('Epoch:', '%04d' % (epoch + 1), 'loss =', '{:.6f}'.format(loss))

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

模型测试过程:这里只测试准确率

test_acc_list = []
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
    for data, target in test_loader:
        data, target = data.to(device), target.to(device)
        output = model(data)
#         loss = criterion(output, target)
#         test_loss += F.nll_loss(output, target, reduction='sum').item() # 将一批的损失相加

        pred = output.max(1, keepdim=True)[1]                           # 找到概率最大的下标
        correct += pred.eq(target.view_as(pred)).sum().item()

# test_loss /= len(test_loader.dataset)
# test_loss_list.append(test_loss)
test_acc_list.append(100. * correct / len(test_loader.dataset))
print('Accuracy: {}/{} ({:.0f}%)\n'.format(correct, len(test_loader.dataset),100. * correct / len(test_loader.dataset)))

输出:
Accuracy: 4023/4900 (85%)

  之后用SVM去试了试,核函数是高斯核,词向量用的是word2vec,最后准确率只有60%左右哈哈哈哈哈哈哈哈~
CNN还是好用的~~~

  TextCNN还可以继续提高准确率的地方(准确率达到90多不是问题):
  1、文本预处理部分
  2、模型结构上
  3、embedding上
  4、调调参

猜你喜欢

转载自blog.csdn.net/weixin_43646592/article/details/113532455