Convolutional Neural Networks for Sentence Classification
卷积神经网络用于文本分类
整理学习笔记的过程即是将零散的知识点进行归纳和总结的过程。 最近致力于研究Yoon Kim的一篇经典之作《Convolutional Neural Networks for Sentence Classification》,这篇文章可被视为卷积神经网络在文本分类任务中的开创性工作(虽然第一篇相关工作并非由他完成,但Kim提出了多个变体并进行了详细调参)。
2 模型
整个模型的构造如下图所示:

在自然语言处理中,在句子中第i个词项的位置上存在一个k维的连续实数向量 x_i\in R^k。为了简化描述,在必要时进行填充处理后一个句子的长度是n并表示如下:
卷积操作中的滤波器尺寸设定为w\in R^{hk}, 这一参数配置会被应用于滑动窗口处理h个连续的输入词素, 从而生成一个新的特征表示。具体而言, 每个输出特征c_i将基于当前窗口内的词素序列X_{i:i+h-1}通过公式(2)进行计算得到:
该滤波装置会工作于每个句子中的所有可能区间内的词汇集合,并通过滑动窗口的方式提取特征信息从而生成一个特征图:
在该处建立特征映射关系:C \in R^{n-h+1}。在随后的处理过程中进行最大池化操作,在此过程中仅提取出具有最大响应的图像区域。
2.1 正则化
为了解决模型过拟合的问题,在倒数第二层引入了Dropout机制以增强模型的正则化能力,并采用L2范数作为权重约束手段。具体而言,在反向传播过程中会暂时关闭p比例的隐藏单元以防止过依赖特定特征:即,在训练过程中我们会动态地丢弃部分神经元信息。已知,在倒数第二层中我们的特征向量z=[C₁,…,Cₘ](其中表示我们拥有m个滤波器)。通常情况下神经元输出结果可表示为:
是在前向传播过程中,使用了dropout的时候输出单元变成:
此处\circ符号代表按元素逐个相乘运算。此处r\in R^m是一个mask向量,并且该mask是由概率p的伯努利随机变量生成的。
伯努利分布又名两点分布或0-1分布,是一个离散型的概率分布。
当伯努利试验获得成功结果时,则伯努利随机变量取值为一;而当伯努利试验获得失败结果时,则伯努利随机变量取值为零。设其成功的概率范围在0到1之间,并以p表示。相应的失败概率则为q等于1减去p。其概率质量函数定义如下:
梯度仅仅在unmasked上的单元进行反向传播,所谓的被masked的就是在倒数第二层隐藏单元中在dropout过程中被置为0的单元。在测试的时候,在前面训练的时候学习到的权重向量已经按p的比例改变了W的大小,新的 \hat{w}=pw , \hat{w} (这里不在使用dropout)用来对未曾使用过的句子进行评分。我们额外通过L2范数来限制权重向量,通过重新衡量w的大小,通过 \begin{Vmatrix} w \end{Vmatrix}_2=s ,无论什么时候 \begin{Vmatrix} w \end{Vmatrix}_2>s在一个梯度下降步骤之后。
Dropout注意事项:正则是解决过拟合的问题,在最后一层softmax的时候是full-connected layer,因此容易产生过拟合。
策略就是:
- 在训练阶段,对max-pooling layer的输出实行一些dropout,以概率p激活,激活的部分传递给softmax层。
- 在测试阶段,w已经学好了,但是不能直接用于unseen sentences,要乘以p之后再用,这个阶段没有dropout了全部输出给softmax层。
反向传播
由神经元i连接到神经元j的突触权值的校正值 \Delta w_{ji}(n)定义如下:
- 如果神经元 j是一个输出层节点,则 \delta_j(n)=e_j(n)\cdot\varphi'(v_j(n))。
输出神经元j仍然采用了非线性激活函数。否则就没有导数可求? - 神经元 被识别为一个隐藏层节点时,则有\delta_j(n)=\varphi'(v_j(n))\cdot\sum_{k}\delta_k^{(n)}w_{kj}(n)。
3.1超参数和训练
关于model
- 对于所有数据集我们采用了修正线性单元Rectified linear units.
- 滤波器窗口大小h设定为3、4、5,并且每种滤波器数量均为100.
- dropout率设置为 dropout rate 5.
- l2正则化被设置用于限制权值大小.
- 随机选取了训练数据量10%作为开发集.
- 将Google News中的word2vec模型作为初始输入并设定了维度参数 dim=300 .
- 每个句子长度设定为n并在必要时进行了填充处理.
- 目标类别数量
- 数据集规模
- 词汇表规模
关于training
- 设置 mini-batch 大小为 50。
- 随机打乱的 mini-batch。
- Adadelta 的更新规则表明其性能与 Adagrad 接近,并且所需的 epoch 数较少。
- 测试方法采用标准的 train-test 划分方法。
3.2预训练词向量
我们使用可通过公开获取的word2vec词向量;该词向量是基于训练了10亿来自Google News中的单词所得出的。
3.3模型变种
我们在几个变种模型上进行实验。
- CNN-RAND: 我们的模型采用了所有单词均被随机初始化并在后续训练过程中进行优化的方法。
- CNN-static: 该方法基于word2vec预训练的模型,在这种情况下所有单词(包括未知词)均被随机初始化并固定不变,仅学习其他参数配置。
- CNN-non-static: 此方法仅调整了预训练词向量,在其他参数配置上与CNN-static方法保持一致。
- CNN-multichannel: 该架构由两个不同的词向量集合构成 每个集合被视为一个独立渠道 类似图像中的channel维度 每个滤波器能够同时处理这两个渠道的信息 然而,在反向传播过程中梯度仅影响其中一个渠道 这种设计使得该方法能够更加精细地调节各个渠道对应的词嵌入 同时保证另一个渠道不受影响 初始时两个渠道都将使用word2vec生成的结果作为基础
各种模型在各种数据集里的表现如下图:

4.1多通道vs单通道
最初的目标是设计多通道结构以抑制过拟合现象(通过确保学习到的向量与真实向量保持高度一致),并且实验结果表明,在小数据集条件下采用多通道模型的效果显著优于单通道模型
5 tf代码解释
positive_example_lines = 5,331行
negative_example_lines = 5,331行
x_text等于正样本加负样本等于10,662行数。
max_document_length等于56表示最长的文档包含54个token。
x等于10,734行数将每一个词编码成一个唯一的整数索引序列编号其中首次出现的是从1开始分配给该词后续相同词项同样采用此规则若某词从未在训练数据集中出现过则将其编码为零索引。
5.1 在研究代码的过程中常常会问自己一些问题;从问题出发在阅读过程中寻找答案这种方法我个人认为是最highly efficient的
了解该类的核心功能是什么?TextCNN类构建了一个基础版本的卷积神经网络架构,在文本分类任务中展现出良好的性能特征。该模型通过一系列层次化的特征提取模块实现对输入文本的理解与分类任务目标的达成。具体而言,在输入层阶段,默认采用词嵌入技术将离散化的文本数据转化为连续向量表示形式;随后通过卷积操作提取局部语义特征;为了增强模型对长距离依赖关系捕捉的能力,在池化操作后引入多长度滤波器机制;最终通过全连接层完成类别预测任务所需的特征组合与分类决策过程。值得注意的是,在词嵌入维度设计上,默认采用统一大小(如200维)以简化计算过程;而在多长度滤波器设计方面,则通过设定不同窗口尺寸(如3/4/5个词)来覆盖不同粒度的语言信息组织模式;此外,在多滤波器输出特征向量拼接过程中,默认采用简单的拼接方式进行特征融合运算;最后通过全连接层将所有中间特征融合成一个最终预测相关的全局表征向量作为分类决策的基础依据
各参数的大小如下:
sequecce_length:56
num_classes:2
vocab_size:18758
embedding_size:128
filter_sizes:[3,4,5]
num_filters:128
l2_reg_labbda:0
5.2 Embedding Layer
W = tf.Variable(tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0),name="W")
self.embedded_chars = tf.nn.embedding_lookup(W, self.input_x)
self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)
W存储了一个包含所有单词向量的矩阵,W初始化时是随机生成出来的,即论文中提出的第一个模型CNN-rand。在训练过程中,并非每次都使用整个词库。每个批次由多个句子组成,在每个句子中标记了出现哪些单词(最大长度不超过sequence_length),因此整个批次相当于一个二维列表。其中这个批次被定义为input_x。
self.input_x = tf.placeholder(tf.int32,[None,sequence_length],name="input_x")
tf.nn.embedding_lookup:查找input_x中所有word的ids,获取它们的word vector。batch中的每个sentence的每个word都要查找。所以得到的embedded_chars的shape=[None, sequence_length, embedding_size] (1)
但是,输入的word vectors得到之后,下一步就是输入到卷积层,用到tf.nn.conv2d函数,再看看conv2d的参数列表:
input: [batch, in_height, in_width, in_channels](2)
filter: [filter_height, filter_width, in_channels, out_channels](3)
对比(1)(2)可以发现,就差一个in_channels了,而最simple的版本也就只有1通道(Yoon的第四个模型用到了multichannel)
因此需要expand dim来适应conv2d的input要求,万能的tensorflow已经提供了这样的功能:
因此只需要:
self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)
就能在embedded_chars后面加一个in_channels=1
5.3 Conv and Max-pooling
conv-maxpool-3
embedded_chars_expanded = [?,56,128,1]
w = [3,128,1,128]
b= [128]
conv = tf.nn.conv2d(embedded_chars_expanded,w,padding='VALID')=[?,54,1,128]
h = tf.nn.relu(tf.nn.bias_add(conv, b))=[?,54,1,128]
pooled = tf.nn.max_pool(
h,
ksize=[1, 56 - 3 + 1, 1, 1],
strides=[1, 1, 1, 1],
padding='VALID')=[?,1,1,128]
对于其余卷积滤波器大小情形类似。
5.4 output
W = tf.get_variable(
"W",
shape=[num_filters_total, num_classes],
initializer=tf.contrib.layers.xavier_initializer()) tf.get_variable通过所给的名字创建或是返回一个变量。
b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name="b")
l2_loss += tf.nn.l2_loss(W) l2正则,我也不知道到底有啥作用,有多大作用,这里对权重和偏置都进行l2正则,奇怪?
l2_loss += tf.nn.l2_loss(b)
self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name="scores")
self.predictions = tf.argmax(self.scores, 1, name="predictions")
前文介绍过文章中的输出: y=w \cdot (z\circ r)+b
6 training
6.1 Keep track of gradient values and sparsity (optional)
这部分的作用不太懂
7最终结果


图中蓝色代表训练集,红色代表验证集! train/dev:9662/1000
