Skip to content

情感分析(CNN版)

一、概述

之前我们学过用循环神经网络(RNN)做情感分析,这一节我们用卷积神经网络(CNN)来完成这个任务。CNN 原本是用来处理图像的,能很好地捕捉图像的局部特征(比如相邻像素的特征),现在我们可以把文本当成一维图像来处理,这样 CNN 就能捕捉文本的局部特征,比如 n 元语法(几个连续的词组成的短语,像 “so great” 这样的组合),从而完成情感分析任务。

nlp-map-sa-cnn.svg

二、一维卷积:捕捉文本的局部特征

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 的结构就像一条流水线,一步步把文本转换成分类结果:

  1. 输入层:输入是预训练的词向量,这里用了两个嵌入层,一个是可训练的嵌入层(训练时可以调整词向量),一个是固定的预训练嵌入层(用已经训练好的词向量,比如 GloVe,训练时不修改),然后把这两个嵌入层的结果拼接起来,这样既能利用预训练的语义信息,又能让模型适应当前的情感分析任务。

  2. 卷积层:用不同宽度的卷积核(比如 3、4、5,分别对应捕捉 3 个词、4 个词、5 个词的局部特征),每个宽度的卷积核输出多个通道(比如 100 个通道,每个通道捕捉一种局部特征)。

  3. 最大时间汇聚层:对每个卷积核输出的每个通道取最大值,把每个通道转换成一个标量,然后把所有通道的标量拼接成一个固定长度的向量。

  4. 全连接层:把拼接后的向量输入到全连接层,输出两个结果(积极或消极),同时用 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'

五、小结

  1. 我们可以把文本当成一维图像,用一维卷积神经网络捕捉文本的局部特征(比如 n 元语法),就像 CNN 捕捉图像的局部特征一样。

  2. 最大时间汇聚层可以把不同长度的序列转换成固定长度的向量,方便后续的全连接层处理。

  3. textCNN 模型用不同宽度的卷积核捕捉不同长度的局部特征,然后用最大时间汇聚层提取关键特征,最后用全连接层输出分类结果,还可以用 Dropout 防止过拟合。

  4. 和 RNN 相比,CNN 处理文本时计算速度更快,因为 CNN 可以并行计算,而 RNN 是串行计算的;不过 RNN 更擅长捕捉长距离的上下文信息,CNN 更擅长捕捉局部的语义特征。

(注:文档部分内容可能由 AI 生成) 源地址

京ICP备2024093538号-1