目录
- 一、系列文章
- 二、TextCNN介绍
- 2.1 一些细节
- 2.2 搭建TextCNN
- 三、训练&测试
一、系列文章
- 情感分析系列(一)——IMDb数据集及其预处理
- 情感分析系列(二)——使用BiLSTM进行情感分析
二、TextCNN介绍
TextCNN架构:
设一个序列的长度为 ,嵌入维度为 ,则该序列的嵌入矩阵的形状为 。TextCNN对该嵌入矩阵做一维卷积,视嵌入维度为通道维度,则输入通道数为 。不妨设输出通道数为 ,卷积核的大小为 ,因此卷积之后会得到形状为 的矩阵。对该矩阵应用最大时间汇聚(取每一列的最大元素)会得到一个长度为
一般地,我们会做多个一维卷积,不妨设输出通道数为 ,与之对应的卷积核的大小为 ,则卷积之后会得到 个形状为 的矩阵,分别对这 个矩阵应用最大时间汇聚可得到 个长度为 的向量,将这些向量拼在一起可得到一个长度为 的向量,最后将该向量丢进全连接层 nn.Linear(sum(c_i), 2)
即可得到分类结果。
需要注意的是,TextCNN实际上使用了两个嵌入矩阵,这两个矩阵均采用相同的初始化方式(使用预训练的词向量,word2vec或GloVe),不同之处在于,其中一个矩阵被“冻结”,不参与训练,而另一个矩阵参与训练。
TextCNN 设置了三种大小的卷积核,分别为 ,每种大小下都有两个卷积核,其中一个卷积核作用于被冻结的嵌入矩阵,另一个卷积核作用于没有被冻结的嵌入矩阵。六个卷积核的输出通道数均为 ,因此卷积之后会得到六个矩阵,形状分别为:
对它们应用最大时间汇聚然后进行拼接可得到一个长度为 的向量,所以最后的全连接层为 nn.Linear(600, 2)
。
2.1 一些细节
细节一: 既然对于同一个序列有两个嵌入矩阵,那么能否把它们当作一个通道数为 ,高度为 ,宽度为
我们先来回顾一下原始过程。设卷积核大小为 ,输出通道数为 ,则我们需要两个这种配置的卷积核以便让它们各自作用于不同的嵌入矩阵。卷积之后会得到两个不同的但形状均为 的矩阵,并且在这个过程中,两个嵌入矩阵的元素不会发生“交互”。
再来看下二维卷积的情形。此时卷积核的大小为 ,输入通道数为 ,输出通道数为 ,卷积之后只会得到一个形状为 的矩阵,并且在这个过程中,两个嵌入矩阵的元素发生了“交互”(卷积时,两个嵌入矩阵的元素会和卷积核的权重按元素相乘再相加),这显然是不对的。
细节二: 能否将两个嵌入矩阵沿列方向拼接起来形成一个大的嵌入矩阵,然后只用一个卷积核去做卷积呢?
此情形下,输入通道数为 ,输出通道数为 ,进行卷积时,两个嵌入矩阵的元素仍然发生了“交互”,这仍然是不对的。
2.2 搭建TextCNN
class TextCNN(nn.Module):
def __init__(self, vocab, embed_size=100, kernel_sizes=[3, 4, 5], num_channels=[100] * 3):
super().__init__()
self.glove = GloVe(name="6B", dim=100)
self.unfrozen_embedding = nn.Embedding.from_pretrained(self.glove.get_vecs_by_tokens(vocab.get_itos()), padding_idx=vocab['<pad>'])
self.frozen_embedding = nn.Embedding.from_pretrained(self.glove.get_vecs_by_tokens(vocab.get_itos()),
padding_idx=vocab['<pad>'],
freeze=True)
self.convs_for_unfrozen = nn.ModuleList()
self.convs_for_frozen = nn.ModuleList()
for out_channels, kernel_size in zip(num_channels, kernel_sizes):
self.convs_for_unfrozen.append(nn.Conv1d(in_channels=embed_size, out_channels=out_channels, kernel_size=kernel_size))
self.convs_for_frozen.append(nn.Conv1d(in_channels=embed_size, out_channels=out_channels, kernel_size=kernel_size))
self.pool = nn.AdaptiveMaxPool1d(1)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.5)
self.fc = nn.Linear(sum(num_channels) * 2, 2)
self.apply(self._init_weights)
def forward(self, x):
x_unfrozen = self.unfrozen_embedding(x).transpose(1, 2) # (batch_size, embed_size, seq_len)
x_frozen = self.frozen_embedding(x).transpose(1, 2) # (batch_size, embed_size, seq_len)
# 池化后得到的向量
pooled_vector_for_unfrozen = [self.pool(self.relu(conv(x_unfrozen))).squeeze()
for conv in self.convs_for_unfrozen] # shape of each element: (batch_size, 100)
pooled_vector_for_frozen = [self.pool(self.relu(conv(x_frozen))).squeeze()
for conv in self.convs_for_frozen] # shape of each element: (batch_size, 100)
# 将向量拼接起来后得到一个更长的向量
feature_vector = torch.cat(pooled_vector_for_unfrozen + pooled_vector_for_frozen, dim=-1) # (batch_size, 600)
output = self.fc(self.dropout(feature_vector)) # (batch_size, 2)
return output
def _init_weights(self, m):
# 仅对线性层和卷积层进行xavier初始化
if type(m) in (nn.Linear, nn.Conv1d):
nn.init.xavier_uniform_(m.weight)
三、训练&测试
训练&测试的代码和之前的几乎相同,但对于TextCNN,这里主要做了以下几点改动:
NUM_EPOCHS = 60
optimizer = torch.optim.Adam(model.parameters(), lr=0.0009, weight_decay=5e-4)
最终结果:
Accuracy: 0.8698
可以看出TextCNN和使用了预训练词向量的BiLSTM相差无几,但前者的训练速度要快很多。