0%

TextCNN 的原理与实现

卷积神经网络的核心思想是捕捉局部特征。对于文本来说,局部特征就是由若干单词组成的滑动窗口,类似于 N-gram。卷积神经网络的优势在于能够自动地对 N-gram 特征进行组合和筛选,获得不同抽象层次的语义信息。因此在文本分类任务中,可以利用 CNN 的特性来提取句子中类似 N-gram 的关键信息。

1. TextCNN 的原理与过程

TextCNN 结构图如下所示:

从图中就可以很清楚的看到它原理了。TextCNN 包含 4 部分,分别是 Embedding 层、卷积层、池化层和分类层。

  • Embedding 层: 将文本进行 Embedding。把单词映射为一个长度为 \(k\) 的词向量(某些模型使用双通道的形式,即有两个输入矩阵,一个是用预训练好的词嵌入表达,并且在训练过程中不再变化;另一个是由同样的方式初始化,但是会作为参数,随着网络的训练发生变化)
  • 卷积层:使用 \(N\) 个卷积核(比如 3~5 个)对词向量矩阵进行卷积运算(卷积核展开后是一个 \(x\)\(y\) 列的矩阵,其中 \(y\) 与 Embedding 的维度相同)
  • 池化层:对进行卷积运算后的 \(N\) 结果进行池化操作(一般选用 max-pooling),并对池化后的 \(N\) 结果进行组合
  • 全连接层:根据分类类别数量输出各个类别的概率

2. TextCNN 可调整参数

  • 输入词向量表征:词向量可以自行训练,也可以使用 Word2Vec 或 Glove。如果数据量够大,可以尝试 one-hot
  • 卷积核大小:合理值范围在 1~10。若语料中的句子较长,可以考虑使用更大的卷积核。另外,可以在寻找到了最佳的单个 filter 的大小后,尝试在该 filter 的尺寸值附近寻找其他合适值来进行组合。实践证明这样的组合效果往往比单个最佳 filter 表现更出色
  • feature map 特征图个数:主要考虑的是当增加特征图个数时,训练时间也会加长,因此需要权衡好。这个参数会影响最终特征的维度,维度太大的话训练速度就会变慢。这里在 100-600 之间调参即可。当特征图数量增加到将性能降低时,可以加强正则化效果,如将 dropout 率提高过 0.5
  • 激活函数:\(ReLU\)\(tanh\)
  • 池化策略:1-max pooling 表现最佳,复杂任务选择 k-max
  • 正则化项:指对 CNN 参数的正则化,可以使用 dropout 或 L2,但能起的作用很小,可以试下小的 dropout 率(\(<0.5\)),L2 限制大一点

3. TextCNN 局限性

TextCNN是很适合中短文本场景的强 baseline,但不太适合长文本。因为卷积核尺寸通常不会设很大,无法捕获长距离特征。同时 max-pooling 也存在局限,会丢掉一些有用特征。TextCNN 和传统的 n-gram 词袋模型本质是一样的,它的好效果很大部分来自于词向量的引入。

4. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class TextCNN(nn.Module):
def __init__(self, vocab_size, embedding_size, output_shape):
super(TextCNN, self).__init__()

num_filters = 4
filter_sizes = [3, 4, 5]

self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=embedding_size
)
self.conv1ds = nn.ModuleList(
[nn.Conv2d(1, num_filters, (K, embedding_size)) for K in filter_sizes]
)

self.dropout = nn.Dropout(0.2)
self.fc = nn.Linear(num_filters * len(filter_sizes), output_shape)

def forward(self, x):
# (N,W,D)
x = self.embedding(x)

# (N,Ci,W,D)
x = x.unsqueeze(1)

# len(filter_sizes)*(N,num_filters,W)
x = [F.relu(conv(x)).squeeze(3) for conv in self.conv1ds]

# len(filter_sizes)*(N,num_filters)
x = [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x]

# (N,num_filters*len(filter_sizes))
x = torch.cat(x, 1)

x = self.dropout(x)
logit = self.fc(x)
return logit