文章目录
- 前言
- 加载数据集
- 一维卷积
- 最大时间汇聚层
- textCNN模型
- 定义模型
- 加载预训练词向量
- 训练和评估模型
- 总结
前言
在之前的章节中,我们探讨了如何使用循环神经网络(RNN)来处理序列数据。今天,我们将探索另一种强大的模型——卷积神经网络(CNN)——并将其应用于自然语言处理中的经典任务:情感分析。
你可能会觉得奇怪,CNN不是主要用于图像处理的吗?确实,CNN在计算机视觉领域取得了巨大的成功,它通过二维卷积核捕捉图像的局部特征(如边缘、纹理)。但如果我们换个角度思考,文本序列可以被看作是一维的“图像”,其中每个词元(token)就是一个“像素”。这样,我们就可以使用一维卷积来捕捉文本中的局部模式,比如由相邻单词组成的n-gram。
本篇博客将详细介绍如何使用 textCNN 模型,这是一种专为文本分类设计的CNN架构。我们将基于IMDb电影评论数据集,训练一个能够判断评论是正面还是负面的模型。整个流程如下图所示,我们将使用预训练的GloVe词向量作为输入,将其送入textCNN模型,最终得到情感分类结果。
让我们开始吧!首先,我们需要加载所需的数据集。
完整代码:下载链接
加载数据集
我们仍然使用IMDb电影评论数据集。通过我们预先准备好的 utils_for_data.load_data_imdb
辅助函数,我们可以方便地加载训练和测试数据迭代器,以及一个根据训练数据构建好的词汇表(vocab
)。
import torch
import utils_for_data
from torch import nnbatch_size = 64
train_iter, test_iter, vocab = utils_for_data.load_data_imdb(batch_size)
一维卷积
在深入textCNN模型之前,我们先来回顾一下一维卷积是如何工作的。它本质上是二维卷积在只有一个维度(时间或序列步长)上的特例。
如下图所示,卷积窗口(或称为卷积核)在一个一维输入张量上从左到右滑动。在每个位置,输入子张量与核张量进行逐元素相乘,然后求和,得到输出张量中对应位置的一个标量值。例如,图中第一个输出值 2
是通过 0*1 + 1*2 = 2
计算得出的。
我们可以通过代码来实现这个一维互相关(corr1d
)运算,代码中详尽的注释解释了每一步的维度变化和操作目的。
import torchdef corr1d(X, K):"""实现一维互相关(卷积)运算参数:X: 输入张量,维度为 (n,) 其中 n 是输入序列的长度K: 卷积核张量,维度为 (w,) 其中 w 是卷积核的长度返回:Y: 输出张量,维度为 (n - w + 1,) 其中 n-w+1 是输出序列的长度"""# 获取卷积核的长度,维度: 标量w = K.shape[0]# 创建输出张量,长度为输入长度减去卷积核长度加1# Y的维度: (X.shape[0] - w + 1,)Y = torch.zeros((X.shape[0] - w + 1))# 遍历输出张量的每个位置for i in range(Y.shape[0]):# 在第i个位置进行卷积运算# X[i: i + w] 的维度: (w,) - 提取输入序列的一个窗口# K 的维度: (w,) - 卷积核# 两者逐元素相乘后求和得到标量结果Y[i] = (X[i: i + w] * K).sum()return Y# 测试代码
# X: 输入张量,维度 (7,) - 包含7个元素的一维张量
X = torch.tensor([0, 1, 2, 3, 4, 5, 6])# K: 卷积核张量,维度 (2,) - 包含2个元素的一维张量
K = torch.tensor([1, 2])# 调用函数进行一维卷积运算
# 输出结果的维度: (7 - 2 + 1,) = (6,)
result = corr1d(X, K)
print(result)
输出结果与预期一致:
tensor([ 2., 5., 8., 11., 14., 17.])
在NLP中,词嵌入通常是多维的,这意味着我们的输入有多个通道。一维卷积同样可以处理多通道输入。此时,卷积核也需要有相同数量的输入通道。运算时,对每个通道分别执行一维互相关,然后将所有通道的结果相加,得到一个单通道的输出。
下面是多输入通道一维互相关的实现。
import torchdef corr1d_multi_in(X, K):"""实现多输入通道的一维互相关(卷积)运算参数:X: 多通道输入张量,维度为 (c, n) 其中 c 是输入通道数,n 是每个通道的序列长度K: 多通道卷积核张量,维度为 (c, w) 其中 c 是输入通道数,w 是卷积核的长度返回:result: 输出张量,维度为 (n - w + 1,) 其中 n-w+1 是输出序列的长度"""# 遍历X和K的第0维(通道维),对每个通道分别进行一维卷积,然后求和# X的维度: (c, n) - c个通道,每个通道长度为n# K的维度: (c, w) - c个通道,每个通道的卷积核长度为w# zip(X, K) 将对应通道的输入和卷积核配对# 每次corr1d(x, k)的结果维度: (n - w + 1,)# sum()将所有通道的结果相加,最终输出维度: (n - w + 1,)return sum(corr1d(x, k) for x, k in zip(X, K))# 测试代码
# X: 多通道输入张量,维度 (3, 7) - 3个输入通道,每个通道包含7个元素
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: 多通道卷积核张量,维度 (3, 2) - 3个通道,每个通道的卷积核长度为2
K = torch.tensor([[1, 2], [3, 4], [-1, -3]])# 调用函数进行多通道一维卷积运算
# 输出结果的维度: (7 - 2 + 1,) = (6,)
result = corr1d_multi_in(X, K)
print(result)
输出结果:
tensor([ 2., 8., 14., 20., 26., 32.])
有趣的是,多输入通道的一维互相关等价于单输入通道的二维互相关,只要将二维卷积核的高度设置为与输入张量的高度相同即可,如下图所示。
最大时间汇聚层
在卷积层之后,textCNN使用了一个称为最大时间汇聚层(Max-over-time Pooling)的关键组件。卷积操作的输出长度依赖于输入序列和卷积核的宽度,导致不同卷积核产生的输出序列长度不同。最大时间汇聚层的作用是在时间步(序列长度)维度上取最大值。这相当于从每个卷积核提取的特征图中,只保留最强烈的信号。无论输入序列多长,经过这个操作后,每个通道都只会输出一个标量值,从而解决了不同卷积核输出维度不一的问题,并生成了用于分类的固定长度的特征向量。
textCNN模型
理解了一维卷积和最大时间汇聚后,我们就可以构建textCNN模型了。整个模型的架构如下图所示:
输入是一个句子,每个词元由一个多维向量表示。我们定义了多种不同宽度的卷积核(