CNN for Sentence Classification-textcnn阅读笔记
Textcnn 论文全名是《Convolutional Neural Networks for Sentence Classification》发表于2014年 是一个最经典的模型,Yoon Kim将卷积神经网络CNN应用到文本分类任务,利用多个不同size的kernel来提取句子中的关键信息(类似于多窗口大小的ngram),从而能够更好地捕捉局部相关性。
论文地址:https://arxiv.org/abs/1408.5882
1.主要模型
textcnn模型
预处理:word embeddings 将每个单词都对应一个embeddings K 预处理的方式有很多种
Input::输入为N个单词,通过查表得到一个N*K的矩阵
卷积层:通过【3,4,5】选取卷积尺寸大小,对于一句话的词向量矩阵,用多个CNN去对矩阵做计算。论文用的是size为3,4,5的三种,每种的卷积核是100层,就类似于做了3gram,4gram,5gram来提取语义特征吧。每一个卷积核对应的特征的不同。
池化层:池化层采用的是最大池化层,对应的一个卷积核最后通过最大池化层能够得到一个数
还有一种方式是K-max pooling,利用CNN解决文本分类问题的文章还是很多的,比如这篇 A Convolutional Neural Network for Modelling Sentences 最有意思的输入是在 pooling 改成 (dynamic) k-max pooling ,pooling阶段保留 k 个最大的信息,保留了全局的序列信息。
连接层(concat):将所有的池化层进行连接
全连接层:当前节点的所有神经元都和下一个节点的所有神经元有连接
Dropout:dropout一般只使用在全连接层上,防止过拟合,在训练的时候使用,但是在测试的时候是不适用的
(1)取平均的作用:先回到标准的模型即没有dropout,我们用相同的训练数据去训练5个不同的神经网络,一般会得到5个不同的结果,此时我们可以采用 “5个结果取均值”或者“多数取胜的投票策略”去决定最终结果。例如3个网络判断结果为数字9,那么很有可能真正的结果就是数字9,其它两个网络给出了错误结果。这种“综合起来取平均”的策略通常可以有效防止过拟合问题。因为不同的网络可能产生不同的过拟合,取平均则有可能让一些“相反的”拟合互相抵消。dropout掉不同的隐藏神经元就类似在训练不同的网络,随机删掉一半隐藏神经元导致网络结构已经不同,整个dropout过程就相当于对很多个不同的神经网络取平均。而不同的网络产生不同的过拟合,一些互为“反向”的拟合相互抵消就可以达到整体上减少过拟合。
(2)减少神经元之间复杂的共适应关系:因为dropout程序导致两个神经元不一定每次都在一个dropout网络中出现。这样权值的更新不再依赖于有固定关系的隐含节点的共同作用,阻止了某些特征仅仅在其它特定特征下才有效果的情况 。迫使网络去学习更加鲁棒的特征 ,这些特征在其它的神经元的随机子集中也存在。换句话说假如我们的神经网络是在做出某种预测,它不应该对一些特定的线索片段太过敏感,即使丢失特定的线索,它也应该可以从众多其它线索中学习一些共同的特征。从这个角度看dropout就有点像L1,L2正则,减少权重使得网络对丢失特定神经元连接的鲁棒性提高。
2.例子说明
首先,这个例子的输入是“I like this movie very much!”,也就是说输入的单词个数为七个,而嵌入的维度为5.
其次,经过三个不同尺寸的卷积核进行卷积,每个尺寸的卷积核类型有两个。进行卷积后生成了特征向量,特征向量经过最大池化层后和各自的最大池化层进行拼接,形成了倒数第二层。
最后,倒数第二层这里到输出其实是加了一个全连接层,里头用到的是dropout层进行输出。
3.实验结果:
实验对七个数据集进行了实验,实验中采用了有四种不同的模型,实际上是对预训练向量的处理方式不用。
这里的特征就是词向量,有静态(static)和非静态(non-static)方式。
static方式采用比如word2vec预训练的词向量,训练过程不更新词向量,实质上属于迁移学习了,特别是数据量比较小的情况下,采用静态的词向量往往效果不错。
non-static则是在训练过程中更新词向量。推荐的方式是 non-static 中的 fine-tunning方式(与下游任务结合),它是以预训练(pre-train)的word2vec向量初始化词向量,训练过程中调整词向量,能加速收敛,当然如果有充足的训练数据和资源,直接随机初始化词向量效果也是可以的(例如bert就已经把词向量帮你设定好了,你只需要根据下游任务对词向量进行微调)。
CNN—rand: 随机生成的词向量进行训练
CNN-static: 训练过程中不再更新embeddings。实质上属于迁移学习,特别是在目标领域数据量比较小的情况下,采用静态的词向量效果也不错。(通过设置trainable=False)
CNN-non-static: 在训练过程中对embeddings进行更新和微调(fine tune),能加速收敛。(通过设置trainable=True)
CNN-multichannel: 用多种不同的词向量来代表同一个单词,也就是有多通道的概念。一个通道的词向量是不变的,一个是在训练中进行调试的,但是和单通道比较有好有坏。可以采用一个是Word2vec,一个是Glove来进行训练。
如上图所示:当使用的是静态效果的词向量的时候good的同义词可能会是bad,这就说明了可能在word2vec情况下更关注词语出现在句子中的位置,I feel so good和I feel so bad中good和bad这两个词向量可能是相等的。如果采用的是动态的词向量,那么可能对词向量进行一个调整,使得同义词相似度更加的准确。
4.总结:
尽管很少调整超参数,一个简单的CNN一层卷积的性能非常好。(一层卷积就能达到比较好的效果)
dropout有2%-4%的提升。
为每项任务调整预先培训的向量可以进一步改进(CNN-non-static)
试了另外一组预训练的词向量效果不如Word2Vec好。
Adadelta和Adagrad的效果差不多,但是速度更快。
预训练词向量中未出现的单词,初始化采用预训练矩阵的方差做正态分布,效果有提升,并且觉得初始化的方法可以有更多挖掘。
现在预处理的方式有很多种了,这篇文章用的是word2vec,但是现在ELMO,bert层出不穷,或许能给这个模型带来一些活力。
Dropout层例子
import tensorflow as tf
dropout = tf.placeholder(tf.float32)#设定一个dropout参数
x = tf.Variable(tf.ones([10, 10]))
y = tf.nn.dropout(x, dropout)
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init)
a = sess.run(y, feed_dict = {dropout: 0.5})#有0.5的神经元是未被**的
print(a)
结果:
[[0. 2. 0. 2. 2. 2. 0. 2. 2. 2.]
[2. 0. 0. 0. 2. 2. 0. 2. 0. 2.]
[0. 0. 2. 2. 2. 0. 2. 2. 2. 2.]
[0. 0. 2. 2. 0. 0. 2. 2. 0. 2.]
[0. 2. 0. 0. 2. 0. 0. 0. 0. 0.]
[2. 0. 0. 0. 0. 2. 0. 0. 0. 0.]
[0. 2. 0. 0. 2. 2. 2. 0. 2. 0.]
[0. 2. 2. 2. 0. 0. 0. 2. 0. 2.]
[0. 0. 2. 0. 2. 2. 0. 2. 0. 0.]
[0. 2. 2. 2. 2. 0. 2. 0. 2. 2.]]
Process finished with exit code 0
附件:word_cnn.py
import tensorflow as tf
class WordCNN(object):
def __init__(self, vocabulary_size, document_max_len, num_class):
self.embedding_size = 128
self.learning_rate = 1e-3
self.filter_sizes = [3, 4, 5]
self.num_filters = 100
self.x = tf.placeholder(tf.int32, [None, document_max_len], name="x")
self.y = tf.placeholder(tf.int32, [None], name="y")
self.is_training = tf.placeholder(tf.bool, [], name="is_training")
self.global_step = tf.Variable(0, trainable=False)
self.keep_prob = tf.where(self.is_training, 0.5, 1.0)
with tf.name_scope("embedding"):
init_embeddings = tf.random_uniform([vocabulary_size, self.embedding_size])
self.embeddings = tf.get_variable("embeddings", initializer=init_embeddings)
self.x_emb = tf.nn.embedding_lookup(self.embeddings, self.x)
self.x_emb = tf.expand_dims(self.x_emb, -1)
pooled_outputs = []
for filter_size in self.filter_sizes:
conv = tf.layers.conv2d(
self.x_emb,
filters=self.num_filters,
kernel_size=[filter_size, self.embedding_size],
strides=(1, 1),
padding="VALID",
activation=tf.nn.relu)
pool = tf.layers.max_pooling2d(
conv,
pool_size=[document_max_len - filter_size + 1, 1],
strides=(1, 1),
padding="VALID")
pooled_outputs.append(pool)
h_pool = tf.concat(pooled_outputs, 3)
h_pool_flat = tf.reshape(h_pool, [-1, self.num_filters * len(self.filter_sizes)])
with tf.name_scope("dropout"):
h_drop = tf.nn.dropout(h_pool_flat, self.keep_prob)
with tf.name_scope("output"):
self.logits = tf.layers.dense(h_drop, num_class, activation=None)
self.predictions = tf.argmax(self.logits, -1, output_type=tf.int32)
with tf.name_scope("loss"):
self.loss = tf.reduce_mean(
tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.logits, labels=self.y))
self.optimizer = tf.train.AdamOptimizer(self.learning_rate).minimize(self.loss, global_step=self.global_step)
with tf.name_scope("accuracy"):
correct_predictions = tf.equal(self.predictions, self.y)
self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, "float"), name="accuracy")
5.textcnn经验分享:
1、TextCNN是一个n-gram特征提取器,对于训练集中没有的n-gram不能很好的提取。对于有些n-gram,可能过于强烈,反而会干扰模型,造成误分类。
2、TextCNN对词语的顺序不敏感,在query推荐中,我把正样本分词后得到的term做随机排序,正确率并没有降低太多,当然,其中一方面的原因短query本身对term的顺序要求不敏感。隔壁组有用textcnn做博彩网页识别,正确率接近95%,在对网页内容(长文本)做随机排序后,正确率大概是85%。
3、TextCNN擅长长本文分类,在这一方面可以做到很高正确率。
4、TextCNN在模型结构方面有很多参数可调。