基于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、调调参
标签:nn,self,torch,batch,TextCNN,pytorch,test,import,文本 来源: https://blog.csdn.net/weixin_43646592/article/details/113532455