文本将介绍将卷积神经⽹络应⽤到⽂本情感分析的开创性⼯作之⼀:TextCNN [1] 。

阅读本文后你可以掌握以下技能:

  • 一维卷积、二维卷积的工作流程
  • 文本分类模型TextCNN的结构及pytorch代码实现
  • 模型如何加载预训练词向量
  • 文本情感分析(分类)任务的一般流程

正餐之前,先来两碟开胃小菜。(跳过开胃小菜,直接吃正餐完全ok)

1.卷积层的介绍(开胃菜)

  1. 1 二维卷积层

卷积神经⽹络(Convolutional Neural Network,CNN)是含有卷积层(Convolutional Layer)的神经⽹络。卷积神经⽹络最常⻅的是⼆维卷积层。它有⾼和宽两个空间维度,常⽤来处理图像数据。这里将介绍简单形式的⼆维卷积层的⼯作原理。

虽然卷积层得名于卷积(Convolution)运算,但我们通常在卷积层中使⽤更加直观的互相关(Crosscorrelation)运算。在⼆维卷积层中,⼀个⼆维输⼊数组和⼀个⼆维核(kernel)数组通过互相关运算输出⼀个⼆维数组。 我们⽤⼀个具体例⼦来解释⼆维互相关运算的含义。如图1所示,输⼊是⼀个⾼和宽均为3的⼆维数组。我们将该数组的形状记为(3,3)。核数组的⾼和宽分别为2。该数组在卷积计算中⼜称卷积核或过滤器(filter)。卷积核窗⼝(⼜称卷积窗⼝)的形状取决于卷积核的⾼和宽,即 (2,2)。图1中的阴影部分为第⼀个输出元素及其计算所使⽤的输⼊和核数组元素:0✖️0 + 1✖️1 + 2✖️3 + 4✖️3=19。

图1 二维互相关运算

在⼆维互相关运算中,卷积窗⼝从输⼊数组的最左上⽅开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。当卷积窗⼝滑动到某⼀位置时,窗⼝中的输⼊⼦数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。图1中的输出数组⾼和宽分别为2,其中的4个元素是由⼆维互相关运算得出。

下⾯我们将上述过程实现在 corr2d 函数⾥。它接受输⼊数组 X 与核数组 K ,并输出数组 Y 。

import torch
def corr2d(X, K): h, w = K.shapeY = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))  # 这里根据跟指定的X,Y计算输出结果的形状,并初始化该形状的矩阵元素全为0for i in range(Y.shape[0]):for j in range(Y.shape[1]):Y[i, j] = (X[i: i + h, j: j + w] * K).sum()return Y
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
K = torch.tensor([[0, 1], [2, 3]])
print(corr2d(X, K))
# 输出为:
tensor([[19., 25.],[37., 43.]])

多个输入通道的互相关运算。当输⼊数据含多个通道时,我们需要构造⼀个输⼊通道数与输⼊数据的通道数相同的卷积核,从⽽能够与含多通道的输⼊数据做互相关运算。图2展示了含2个输⼊通道的⼆维互相关计算的例⼦。在每个通道上,⼆维输⼊数组与⼆维核数组做互相关运算,再按通道相加即得到输出。图2中阴影部分为第⼀个输出元素及其计算所使⽤的输⼊和核数组元素:(1✖️1 + 2✖️2 + 4✖️3 + 5✖️4)+(0✖️0 + 1✖️1 + 3✖️2 + 4✖️3)=56 。

图2 含两个输入通道的互相关运算

接下来我们实现含多个输⼊通道的互相关运算。我们只需要对每个通道做互相关运算,然后通过add_n 函数来进⾏累加。

def corr2d_multi_in(X, K):# 沿着X和K的第0维(通道维)分别计算再相加res = corr2d(X[0, :, :], K[0, :, :])  # 注意哦这里调用的是本文上面的函数for i in range(1, X.shape[0]):res += corr2d(X[i, :, :], K[i, :, :])return res
X = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],[[1, 2, 3], [4, 5, 6], [7, 8, 9]]])
K = torch.tensor([[[0, 1], [2, 3]], [[1, 2], [3, 4]]])
print(corr2d_multi_in(X, K))
# 输出
tensor([[ 56., 72.],[104., 120.]])

1.2 一维卷积层

在介绍模型前我们先学习一下一维卷机的工作原理。与⼆维卷积层⼀样,⼀维卷积层使⽤⼀维的互相关运算。在⼀维互相关运算中,卷积窗⼝从输⼊数组的最左⽅开始,按从左往右的顺序,依次在输⼊数组上滑动。当卷积窗⼝滑动到某⼀位置时,窗⼝中的输⼊⼦数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。如图3所示,输⼊是⼀个宽为7的⼀维数组,核数组的宽为2。可以看到输出的宽度为7-2+1=6 ,且第⼀个元素是由输⼊的最左边的宽为2的⼦数组与核数组按元素相乘后再相加得到的:0✖️1+1✖️2=2。

图3 一维互相关运算

下面我们将一维互相关运算实现在corr1d函数里,它接受输入数组X和核数组K,并输出Y。

def corr1d(X, K):w = K.shape[0] Y = torch.zeros((X.shape[0] - w + 1))for i in range(Y.shape[0]):Y[i] = (X[i: i + w] * K).sum()return Y

这里来复现图3中一维互相关运算结果。

X, K = torch.tensor([0, 1, 2, 3, 4, 5, 6]), torch.tensor([1, 2])
print(corr1d(X, K))
# 输出
tensor([ 2., 5., 8., 11., 14., 17.])

多输⼊通道的⼀维互相关运算:在每个通道上,将核与相应的输⼊做⼀维互相关运算,并将通道之间的结果相加得到输出结果。图4展示了含3个输⼊通道的⼀维互相关运算,其中阴影部分为第⼀个输出元素及其计算所使⽤的输⼊和核数组元素:0✖️1 + 1✖️2 + 1✖️3 + 2✖️4 + 2✖️(-1)+ 3✖️(-3)= 2。

图4 含3个输入通道的一维互相关运算

我们继续复现图4中多输⼊通道的⼀维互相关运算的结果。

def corr1d_multi_in(X, K):# ⾸先沿着X和K的第0维(通道维)遍历并计算⼀维互相关结果。然后将所有结果堆叠起来沿第0维累加return torch.stack([corr1d(x, k) for x, k in zip(X, K)]).sum(dim=0)
X = torch.tensor([[0, 1, 2, 3, 4, 5, 6],[1, 2, 3, 4, 5, 6, 7],[2, 3, 4, 5, 6, 7, 8]])
K = torch.tensor([[1, 2], [3, 4], [-1, -3]])
print(corr1d_multi_in(X, K))
# 输出
tensor([ 2., 8., 14., 20., 26., 32.])

由⼆维互相关运算的定义可知,多输⼊通道的⼀维互相关运算可以看作单输⼊通道的⼆维互相关运算。如图5所示,我们也可以将图5中多输⼊通道的⼀维互相关运算以等价的单输⼊通道的⼆维互相关运算呈现。这⾥核的⾼等于输⼊的⾼。图5中的阴影部分为第⼀个输出元素及其计算所使⽤的输⼊和核数组元素: 2✖️(-1) + 3✖️(-3) + 1✖️3 + 2✖️4 + 0✖️1+ 1✖️2= 2。

图5 单输⼊通道的⼆维互相关运算

2.情感分析(正餐)

2.1 数据读取

我们使⽤斯坦福的IMDb数据集(Stanford’s Large Movie Review Dataset)作为⽂本情感分析的数据集 [2] 。这个数据集分为训练和测试⽤的两个数据集,分别包含25,000条从IMDb下载的关于电影的评论。在每个数据集中,标签为“正⾯”和“负⾯”的评论数量相等。

# 导入要使用的相关包
import collections
import os
import random
import tarfile
import torch
from torch import nn
import torchtext.vocab as Vocab
import torch.utils.data as Data
from tqdm import tqdm
import time
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
DATA_ROOT = "Datasets/"  # 指定数据集所在的位置
fname = os.path.join(DATA_ROOT, "aclImdb_v1.tar.gz")
if not os.path.exists(os.path.join(DATA_ROOT, "aclImdb")):print("从压缩包解压...")with tarfile.open(fname, 'r') as f: f.extractall(DATA_ROOT)
# 接下来,读取训练数据集和测试数据集。每个样本是⼀条评论及其对应的标签:1表示“正⾯”,0表 示“负⾯”。def read_imdb(folder='train', data_root="datasets/aclImdb"):data = []for label in ['pos', 'neg']:folder_name = os.path.join(data_root, folder, label)for file in tqdm(os.listdir(folder_name)):with open(os.path.join(folder_name, file), 'rb') as f:review = f.read().decode('utf-8').replace('\n','').lower()data.append([review, 1 if label == 'pos' else 0])random.shuffle(data)return data
train_data, test_data = read_imdb('train'), read_imdb('test')

2.2 预处理数据

我们需要对每条评论做分词,从⽽得到分好词的评论。这⾥定义的 get_tokenized_imdb 函数使⽤最简单的⽅法:基于空格进⾏分词。函数的定义如下:

def get_tokenized_imdb(data):"""data: list of [string, label],"""def tokenizer(text):return [tok.lower() for tok in text.split(' ')]return [tokenizer(review) for review, _ in data]

现在,我们可以根据分好词的训练数据集来创建词典了。我们在这⾥过滤掉了出现次数少于5的词。

def get_vocab_imdb(data):tokenized_data = get_tokenized_imdb(data)counter = collections.Counter([tk for st in tokenized_data for tk in st])return Vocab.Vocab(counter, min_freq=5)
vocab = get_vocab_imdb(train_data)

因为每条评论⻓度不⼀致所以不能直接组合成⼩批量,我们定义 preprocess_imdb 函数对每条评论进⾏分词,并通过词典转换成词索引,然后通过截断或者补0来将每条评论⻓度固定成500。

def preprocess_imdb(data, vocab):max_l = 500 # 将每条评论通过截断或者补0,使得⻓度变成500,当然这个长度自己也可以做调整def pad(x):return x[:max_l] if len(x) > max_l else x + [0] * (max_l - len(x))tokenized_data = get_tokenized_imdb(data)features = torch.tensor([pad([vocab.stoi[word] for word in words]) for words in tokenized_data])labels = torch.tensor([score for _, score in data])return features, labels

2.3 创建迭代器

现在,我们创建数据迭代器。每次迭代将返回⼀个⼩批量的数据。

batch_size = 64
train_set = Data.TensorDataset(*preprocess_imdb(train_data, vocab))
test_set = Data.TensorDataset(*preprocess_imdb(test_data, vocab))
train_iter = Data.DataLoader(train_set, batch_size, shuffle=True)
test_iter = Data.DataLoader(test_set, batch_size)

打印第⼀个⼩批量数据的形状以及训练集中⼩批量的个数。

for X, y in train_iter:print('X', X.shape, 'y', y.shape)break
'#batches:', len(train_iter)
# 输出
# X torch.Size([64, 500]) y torch.Size([64])
# ('#batches:', 391)

2.4 TextCNN模型介绍

TextCNN模型主要使⽤了⼀维卷积层和时序最⼤池化层。假设输⼊的⽂本序列由n个词组成,每个词⽤d维的词向量表示。那么输⼊样本的宽为n,⾼为1,输⼊通道数为d。textCNN的计算主要分为以下⼏步。

  1. 定义多个⼀维卷积核,并使⽤这些卷积核对输⼊分别做卷积计算。宽度不同的卷积核可能会捕捉到不同个数的相邻词的相关性。

  2. 对输出的所有通道分别做时序最⼤池化,再将这些通道的池化输出值连结为向量。

  3. 通过全连接层将连结后的向量变换为有关各类别的输出。这⼀步可以使⽤丢弃层应对过拟合。

图6⽤⼀个例⼦解释了TextCNN的设计。这⾥的输⼊是⼀个有11个词的句⼦,每个词⽤6维词向量表示。因此输⼊序列的宽为11,输⼊通道数为6。给定2个⼀维卷积核,核宽分别为2和4,输出通道数分别设为4和5。因此,⼀维卷积计算后,4个输出通道的宽为11-2+1=10 ,⽽其他5个通道的宽为11-4+1=8。尽管每个通道的宽不同,我们依然可以对各个通道做时序最⼤池化,并将9个通道的池化输出连结成⼀个9维向量。最终,使⽤全连接将9维向量变换为2维输出,即正⾯情感和负⾯情感的预测。

图6 TextCNN的设计

2.5 时序最大池化层

在正式实现模型之前,我们先来弄清楚一个概念时序最大池化层,时序最⼤池化的主要⽬的是抓取时序中最重要的特征,它通常能使模型不受⼈为添加字符的影响。我们可以通过普通的池化来实现全局池化。代码实现如下:

class GlobalMaxPool1d(nn.Module):def __init__(self):super(GlobalMaxPool1d, self).__init__()def forward(self, x):# x shape: (batch_size, channel, seq_len)# return shape: (batch_size, channel, 1)return F.max_pool1d(x, kernel_size=x.shape[2])

其实,上面的代码对应的是图7的过程,注意图7是图6中的一个子环节。

图7 时序最大池化层过程

在卷积层我们设置了不同尺寸的卷积核,对于输入的同一长度句子,经过不同尺寸卷积核的卷积计算,那么输出张量的大小也会不同,如图7中第一个矩阵大小是(4,10),第二矩阵大小(5,8),所以为了动态的从时间序列上(也就是句子长度)提取最大值,这里通过设置kernel_size=x.shape[2],动态地调整最大池化窗口的宽度。

下面我们使用pytorch来实现TextCNN:

class TextCNN(nn.Module):def __init__(self, vocab, embed_size, kernel_sizes, num_channels):super(TextCNN, self).__init__()self.embedding = nn.Embedding(len(vocab), embed_size)# 不参与训练的嵌⼊层self.constant_embedding = nn.Embedding(len(vocab), embed_size)self.dropout = nn.Dropout(0.5)self.decoder = nn.Linear(sum(num_channels), 2)# 时序最⼤池化层没有权᯿,所以可以共⽤⼀个实例self.pool = GlobalMaxPool1d()self.convs = nn.ModuleList() # 创建多个⼀维卷积层for c, k in zip(num_channels, kernel_sizes):self.convs.append(nn.Conv1d(in_channels = 2*embed_size, out_channels = c, kernel_size = k))def forward(self, inputs):# 将两个形状是(批量⼤⼩, 词数, 词向量维度)的嵌⼊层的输出按词向量连结embeddings = torch.cat((self.embedding(inputs),self.constant_embedding(inputs)), dim=2) # (batch, seq_len, 2*embed_size)# 根据Conv1D要求的输⼊格式,将词向量维,即⼀维卷积层的通道维(即词向量那⼀维),变换到前⼀维embeddings = embeddings.permute(0, 2, 1)# 对于每个⼀维卷积层,在时序最⼤池化后会得到⼀个形状为(批量⼤⼩, 通道⼤⼩, 1)的# Tensor。使⽤flatten函数去掉最后⼀维,然后在通道维上连结encoding = torch.cat([self.pool(F.relu(conv(embeddings))).squeeze(-1) for conv in self.convs], dim=1)# 应⽤丢弃法后使⽤全连接层得到输出outputs = self.decoder(self.dropout(encoding))return outputs

下面我们创建⼀个 TextCNN 实例。它有3个卷积层,它们的核宽分别为3、4和5,输出通道数均为100。

embed_size,  kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]
net = TextCNN(vocab, embed_size, kernel_sizes, nums_channels)

这里我们需要加载预训练词向量。这里加载的是glove训练的100维的词向量 [3],下面给出加载预训练词向量的代码:

def load_pretrained_embedding(words, pretrained_vocab):"""从预训练好的vocab中提取出words对应的词向量"""embed = torch.zeros(len(words), pretrained_vocab.vectors[0].shape[0]) # 初始化为0oov_count = 0 # out of vocabularyfor i, word in enumerate(words):try:idx = pretrained_vocab.stoi[word]embed[i, :] = pretrained_vocab.vectors[idx]except KeyError:oov_count += 0if oov_count > 0:print("There are %d oov words.")return embed
# 如果没有提前下载,运行该代码会自动下载,但是下载速度极慢,建议使用迅雷下载(非会员也很快),地址:https://nlp.stanford.edu/data/glove.6B.zip
glove_vocab = Vocab.GloVe(name='6B', dim=100, cache=os.path.join(DATA_ROOT, "glove"))
net.embedding.weight.data.copy_(load_pretrained_embedding(vocab.itos, glove_vocab))
net.constant_embedding.weight.data.copy_(load_pretrained_embedding(vocab.itos, glove_vocab))
net.constant_embedding.weight.requires_grad = False

在给出训练过程之前,我们先定义好评价函数:

# 评价函数
def evaluate_accuracy(data_iter, net, device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')):acc_sum, n = 0.0, 0with torch.no_grad():for X, y in data_iter:if isinstance(net, torch.nn.Module):net.eval() # 评估模式, 会关闭dropoutacc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()net.train() # 改回训练模式else:if ('is_training' in net.__code__.co_varnames):# 如果有is_training这个参数将is_training设置成Falseacc_sum += (net(X, is_training = False).argmax(dim=1) == y).float().sum().item()else:acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()n += y.shape[0]return acc_sum / n

万事俱备,就差训练了!

def train(train_iter, test_iter, net, loss, optimizer, device, num_epochs):for epoch in range(num_epochs):train_acc_sum, sum_l, start, n, batch_count = 0.0, 0.0, time.time(), 0, 0for X, y in train_iter:out = net(X)l = loss(out,y)optimizer.zero_grad()l.backward()optimizer.step()sum_l += l.cpu().item()n += y.shape[0]batch_count += 1train_acc_sum += (out.argmax(dim=1) == y).float().sum().cpu().item()test_acc = evaluate_accuracy(test_iter, net)print('epoch %d, loss %.4f, train_acc %.3f, test acc %.3f, time %.1f sec'% (epoch + 1, sum_l / batch_count, train_acc_sum / n, test_acc, time.time() - start ))
lr, num, num_epochs = 0.01, 5, 5
optimizer = torch.optim.Adam(filter(lambda p:p.requires_grad, net.parameters()), lr=lr)
loss = nn.CrossEntropyLoss()
train(train_iter, test_iter, net, loss, optimizer, device, num_epochs)

到此,就大功告成了!如果你想开箱即用,就百度云下载打包好的代码、语料、词向量 提取码: 2ja6 [4]。

参考

  1. ^[1] https://arxiv.org/abs/1408.5882
  2. ^[2] http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
  3. ^[3] https://nlp.stanford.edu/data/glove.6B.zip
  4. ^[4] https://pan.baidu.com/s/1mL_0aqnOUdtqaAIyb1sPmg

TextCNN-文本情感分析项目实战相关推荐

  1. Hugging Face 中文预训练模型使用介绍及情感分析项目实战

    Hugging Face 中文预训练模型使用介绍及情感分析项目实战 Hugging Face 一直致力于自然语言处理NLP技术的平民化(democratize),希望每个人都能用上最先进(SOTA, ...

  2. 基于bert的股票用户评论的情感分析项目实战+数据+代码可直运行

  3. NLP 实战:手把手带你搞定文本情感分析

    随着移动互联网的普及,很多人已经习惯于在网络上表达意见和建议.比如电商网站上对商品的评价.社交媒体中对品牌.产品.政策的评价等等.这些评价中都蕴含着巨大的商业价值.而对这种评价的分析就是情感分析的主要 ...

  4. Python文本情感分析实战【源码】

    Python文本情感分析 引言: 情感分析:又称为倾向性分析和意见挖掘,它是对带有情感色彩的主观性文本进行分析.处理.归纳和推理的过程,其中情感分析还可以细分为情感极性(倾向)分析,情感程度分析,主客 ...

  5. 基于微博评论的文本情感分析与关键词提取的实战案例~

    点击上方"Python爬虫与数据挖掘",进行关注 回复"书籍"即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 宣室求贤访逐臣,贾生才调更无伦. ...

  6. NLP学习(十三)-NLP实战之LSTM三分类文本情感分析-tensorflow2+Python3

    背景介绍 文本情感分析作为NLP的常见任务,具有很高的实际应用价值.本文将采用LSTM模型,训练一个能够识别文本postive, neutral, negative三种情感的分类器. 本文的目的是快速 ...

  7. [深度学习TF2][RNN-LSTM]文本情感分析包含(数据预处理-训练-预测)

    基于LSTM的文本情感分析 0. 前言 1. 数据下载 2. 训练数据介绍 3. 用到Word2Vector介绍 wordsList.npy介绍 wordVectors.npy介绍 4 数据预处理 4 ...

  8. python snownlp情感分析_白杨数说 | 不会做文本情感分析?试试这两个Python包

    情感分析是自然语言处理(NLP)领域的一类任务,又称倾向性分析,意见抽取,意见挖掘,情感挖掘,主观分析等,它是对带有情感色彩的主观性文本进行分析.处理.归纳和推理的过程.具体到数据新闻领域,文本情感分 ...

  9. python 文本分析库_Python有趣|中文文本情感分析

    前言 前文给大家说了python机器学习的路径,这光说不练假把式,这次,罗罗攀就带大家完成一个中文文本情感分析的机器学习项目,今天的流程如下: 数据情况和处理 数据情况 这里的数据为大众点评上的评论数 ...

最新文章

  1. HaoZip(好压) 去广告纯净版 4.4
  2. lua去掉字符串中的UTF-8的BOM三个字节
  3. Dapr项目应用探索
  4. [蓝桥杯2015决赛]密文搜索
  5. 建表and新增删除数据A
  6. IIS6应用程序池中间的 Web 园
  7. CentOS 快速安装ftp
  8. 【C语言】浅谈C语言数组%c%s打印逻辑及数组打印单个汉字
  9. jmeter beanshell 之常用的代码
  10. python计算矩阵的散度_Python Sympy计算梯度、散度和旋度的实例
  11. 微信公众号使用:在微信公众号文章中嵌入小程序的方法
  12. 拼多多参谋:拼多多隐私号是什么意思?拼多多隐私号怎么查看真实号码?
  13. JS实现快递单打印功能
  14. 二进制十进制小数转换
  15. Spring Cache key生成策略, 不要想当然认为是全类名+方法+参数
  16. 使用服务器备份还原Linux系统
  17. 题目:猴子吃桃问题:猴子第一天摘下若干个桃子,当即吃了一半,还不瘾,又多吃了一个
第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下
的一半零一个。到第10天早上想再吃时,见
  18. 什么是本地O2O 本地O2O有哪些细分领域?
  19. HDU2066:一个人的旅行(spfa)
  20. PHP实现对小程序微信支付v2订单的结果查询

热门文章

  1. 2014年创业致富的佛香公司-武汉吉香缘gs 公司
  2. 【倍增算法】CF379F New Year Tree 题解
  3. jQuery副本单刷(一) 速清小怪
  4. 一文详解python中的数据库操作
  5. Springboot毕设项目剧本杀桌游收银系统l6288(java+VUE+Mybatis+Maven+Mysql)
  6. taotao shopping mall---【积跬步 1】
  7. 书摘 - 重新定义公司:谷歌是如何运营的
  8. 缺页异常处理-do_page_fault
  9. delta动态对冲的python代码实现(1)
  10. 车道线识别 tusimple 数据集介绍