Appearance
情感分析(CNN版)
一、概述
之前我们学过用循环神经网络(RNN)做情感分析,这一节我们用卷积神经网络(CNN)来完成这个任务。CNN 原本是用来处理图像的,能很好地捕捉图像的局部特征(比如相邻像素的特征),现在我们可以把文本当成一维图像来处理,这样 CNN 就能捕捉文本的局部特征,比如 n 元语法(几个连续的词组成的短语,像 “so great” 这样的组合),从而完成情感分析任务。
二、一维卷积:捕捉文本的局部特征
1. 一维卷积的基本原理
一维卷积的工作方式和二维卷积类似,就像一个 “小滑块”(卷积核)在文本序列上从左到右滑动,每滑动到一个位置,就计算这个位置的输入和卷积核的元素乘积之和,得到一个输出值。
举个简单的例子:假设输入的文本序列是 [0,1,2,3],卷积核是 [1,2],那么第一个输出值是0*1 + 1*2 = 2;然后卷积核滑动到下一个位置,输入变成 [1,2],输出是1*1 + 2*2 = 5;再滑动到 [2,3],输出是2*1 + 3*2 = 8,最终输出序列就是 [2,5,8]。这个过程就是一维卷积的计算,它能捕捉到相邻两个词之间的局部关联。
2. 多通道的一维卷积
在文本里,每个词会用一个 d 维的向量来表示(也就是词嵌入),这时候输入就可以看成是 d 个通道的一维序列(每个通道对应词向量的一个维度)。多通道的一维卷积就是让卷积核也有 d 个通道,每个通道的卷积核和输入的对应通道做互相关计算,然后把所有通道的结果加起来,得到一个输出值,这样就能同时捕捉词向量不同维度的局部特征。
三、最大时间汇聚层:提取最关键的特征
最大时间汇聚层的作用很简单:对每个通道的所有时间步(也就是文本的每个位置)取最大值,这样不管每个通道的序列长度是多少,都能把它转换成一个固定长度的标量值。
比如某个通道的输出序列是 [2,5,8],取最大值就是 8,这个值就代表了这个通道里最关键的特征。这个层的好处是,不管不同卷积核输出的序列长度不一样,都能把它们转换成固定长度的向量,方便后续的全连接层处理。
四、textCNN 模型:用 CNN 做情感分析
1. 模型结构
textCNN 的结构就像一条流水线,一步步把文本转换成分类结果:
输入层:输入是预训练的词向量,这里用了两个嵌入层,一个是可训练的嵌入层(训练时可以调整词向量),一个是固定的预训练嵌入层(用已经训练好的词向量,比如 GloVe,训练时不修改),然后把这两个嵌入层的结果拼接起来,这样既能利用预训练的语义信息,又能让模型适应当前的情感分析任务。
卷积层:用不同宽度的卷积核(比如 3、4、5,分别对应捕捉 3 个词、4 个词、5 个词的局部特征),每个宽度的卷积核输出多个通道(比如 100 个通道,每个通道捕捉一种局部特征)。
最大时间汇聚层:对每个卷积核输出的每个通道取最大值,把每个通道转换成一个标量,然后把所有通道的标量拼接成一个固定长度的向量。
全连接层:把拼接后的向量输入到全连接层,输出两个结果(积极或消极),同时用 Dropout 防止模型过拟合。
2. 代码实现(PyTorch 版)
首先定义 textCNN 模型:
python
import torch
from torch import nn
from d2l import torch as d2l
class TextCNN(nn.Module):
def __init__(self, vocab_size, embed_size, kernel_sizes, num_channels, **kwargs):
super(TextCNN, self).__init__(**kwargs)
# 可训练的嵌入层,训练时会更新参数
self.embedding = nn.Embedding(vocab_size, embed_size)
# 固定的预训练嵌入层,训练时不更新参数
self.constant_embedding = nn.Embedding(vocab_size, embed_size)
# Dropout层,防止过拟合
self.dropout = nn.Dropout(0.5)
# 全连接层,输出2类(积极/消极)
self.decoder = nn.Linear(sum(num_channels), 2)
# 最大时间汇聚层,把每个通道的序列转换成1个标量
self.pool = nn.AdaptiveAvgPool1d(1)
self.relu = nn.ReLU()
# 创建多个一维卷积层,不同宽度的卷积核捕捉不同长度的局部特征
self.convs = nn.ModuleList()
for c, k in zip(num_channels, kernel_sizes):
# 输入通道数是2*embed_size,因为拼接了两个嵌入层
self.convs.append(nn.Conv1d(2 * embed_size, c, k))
def forward(self, inputs):
# 输入形状是(批量大小,词元数量)
# 两个嵌入层的输出形状都是(批量大小,词元数量,词元向量维度),拼接后变成(批量大小,词元数量,2*词元向量维度)
embeddings = torch.cat((self.embedding(inputs), self.constant_embedding(inputs)), dim=2)
# 一维卷积层的输入格式是(批量大小,通道数,序列长度),所以调整维度
embeddings = embeddings.permute(0, 2, 1)
# 对每个卷积层的输出做ReLU激活,然后用最大时间汇聚层,最后去掉最后一个维度
encoding = torch.cat([
torch.squeeze(self.relu(self.pool(conv(embeddings))), dim=-1)
for conv in self.convs], dim=1)
# 经过Dropout后输入全连接层得到输出结果
outputs = self.decoder(self.dropout(encoding))
return outputs然后创建模型实例,初始化权重:
python
# 词向量维度设为100,卷积核宽度为3、4、5,每个卷积核输出100个通道
embed_size, kernel_sizes, nums_channels = 100, [3, 4, 5], [100, 100, 100]
# 获取所有可用的GPU
devices = d2l.try_all_gpus()
# 创建模型,词表大小是之前处理好的vocab的长度
net = TextCNN(len(vocab), embed_size, kernel_sizes, nums_channels)
# 初始化权重,用Xavier初始化让参数分布更合理
def init_weights(m):
if type(m) in (nn.Linear, nn.Conv1d):
nn.init.xavier_uniform_(m.weight)
net.apply(init_weights);3. 训练和评估模型
我们用 Adam 优化器和交叉熵损失函数来训练模型:
python
lr, num_epochs = 0.001, 5
# 定义Adam优化器
trainer = torch.optim.Adam(net.parameters(), lr=lr)
# 交叉熵损失函数
loss = nn.CrossEntropyLoss(reduction="none")
# 训练模型
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)训练完成后,大概能得到这样的结果:训练准确率 0.978,测试准确率 0.869 左右,说明模型在训练集上拟合得很好,在测试集上也能取得不错的准确率。
4. 预测示例
训练好模型后,我们可以用它来预测文本的情感:
python
# 预测积极的句子
d2l.predict_sentiment(net, vocab, 'this movie is so great')
# 输出:'positive'
# 预测消极的句子
d2l.predict_sentiment(net, vocab, 'this movie is so bad')
# 输出:'negative'五、小结
我们可以把文本当成一维图像,用一维卷积神经网络捕捉文本的局部特征(比如 n 元语法),就像 CNN 捕捉图像的局部特征一样。
最大时间汇聚层可以把不同长度的序列转换成固定长度的向量,方便后续的全连接层处理。
textCNN 模型用不同宽度的卷积核捕捉不同长度的局部特征,然后用最大时间汇聚层提取关键特征,最后用全连接层输出分类结果,还可以用 Dropout 防止过拟合。
和 RNN 相比,CNN 处理文本时计算速度更快,因为 CNN 可以并行计算,而 RNN 是串行计算的;不过 RNN 更擅长捕捉长距离的上下文信息,CNN 更擅长捕捉局部的语义特征。
(注:文档部分内容可能由 AI 生成) 源地址