TextCNN文本分类与tensorflow实现

1.引言

    我们知道,卷积神经网络(CNN)主要是在计算机视觉方面已经取得了很多很好的成就,但是,CNN在自然语言处理方面同样也可以拥有很好的应用。本文将介绍一个有关CNN的模型,用来对文本进行分类,并将它应用在文本分类的热门任务——情感分析上,模型的名称叫TextCNN,模型的论文地址如下:

  • 论文地址:《Convolutional Neural Networks for Sentence Classification》 

    下面将对该模型的原理进行具体介绍,并用tensorflow来实现它。 

2.TextCNN模型介绍

    假设对于每一个句子都进行padding,使得句子的长度都为n,对于太长的句子则进行截断,则每个句子可以表示为:

                                                                       \mathbf { x } _ { 1 : n } = \mathbf { x } _ { 1 } \oplus \mathbf { x } _ { 2 } \oplus \ldots \oplus \mathbf { x } _ { n }

其中,\mathbf { x } _ { i } \in \mathbb { R } ^ { k }表示句子中的第i个词汇,其词向量的维度为k\oplus表示将每个词向量进行拼接,\mathbf { x } _ { i : i + j }表示词汇串\mathbf { x } _ { i } , \mathbf { x } _ { i + 1 } , \dots , \mathbf { x } _ { i + j }的拼接,如图1中左侧所示,这样一来,每个句子都可以表示成一个n\times k的二维矩阵。

    接着,对于卷积操作,TextCNN的每一个卷积核的宽度都选择与词向量的维度一样大小,而高度则可以是变动的,如卷积核\mathbf { w } \in \mathbb { R } ^ { h k },其高度为h,宽度为k,该卷积核的每一次卷积操作将对h个词汇的词向量进行特征提取,记第i步提取后的特征值为c_i,则c_i的计算公式如下:

                                                                      c _ { i } = f \left( \mathbf { w } \cdot \mathbf { x } _ { i : i + h - 1 } + b \right)

其中,b表示偏置项,f则表示一个非线性函数,比如tanh等。因此,当卷积核的从上到下对句子的特征矩阵进行滑动时,如果时间步为1,则每一步卷积对应的词汇串分别为\left\{ \mathbf {x } _ { 1 : h } , \mathbf { x } _ { 2 : h + 1 } , \dots , \mathbf { x } _ { n - h + 1 : n } \right\},最终卷积结束后将得到一个长度为n-h+1的特征向量:

                                                                     \mathbf { c } = \left[ c _ { 1 } , c _ { 2 } , \dots , c _ { n - h + 1 } \right]

    接着,对卷积核得到的特征向量进行池化操作,作者选择的是最大池化操作max-pooling,因为,max-pooling从卷积后得到的特征向量中提取最大的值,其实也起点一个类似注意力的机制,使得每个卷积核可以关注于句子中一些比较重要的词汇。记池化操作得到的值为:

                                                                     \hat { c } = \max \{ \mathbf { c } \}

    通过前面的介绍我们可以发现,对于一个卷积核,不管卷积核的高度选取多少,最终对每个句子的卷积操作将得到一个特征值, 因此,TextCNN会设置多个不同高度的卷积核,每个卷积核都会对句子的特征矩阵进行卷积,最终经过池化操作得到一个特征值,然后将每个卷积核得到的特征值进行拼接,这样就可以对每个句子得到一个统一维度的向量表示,向量的长度大小即为卷积核的数量大小m,如图1中第三部分所示,此时句子向量可以表示为:

                                                                     \mathrm { z } = \left[ \hat { c } _ { 1 } , \ldots , \hat { c } _ { m } \right]

    接着,将卷积后得到的句子向量z传入一个全连接层,在该层,作者引入了dropout操作来防止过拟合,其计算公式如下:

                                                                     y = \mathbf { w } \cdot ( \mathbf { z } \circ \mathbf { r } ) + b

其中,\circ表示一个元素乘积操作,即内积,\mathbf { r }为一个长度为m的mask向量,其中,每个元素为1的概率为p,简单来讲就是一个伯努利分布。另外,作者对w也添加了一个l_2的正则化操作,最后,将全连接层计算的结果再接一个softmax层,即可得到句子在每个类别中的概率分布。

TextCNN文本分类与tensorflow实现_第1张图片 图1 TextCNN模型结构

     以上就是TextCNN的模型结构,我们可以发现其实模型并没有什么新奇之处,主要是对卷积核的宽度进行了限制,保证与词向量的维度一致,另外,采用多种高度的卷积核同时进行卷积操作,获得多通道的输出,其他的基本没什么变动。CNN由于通过调整卷积核的高度,可以使得每个特征是对h个词汇的抽象,就类似与ngram一样,因此,在一定程度上保留了词汇之间的时序信息。

3.TextCNN的tensorflow实现

    接下来,本文通过tensorflow框架来实现TextCNN模型,并将其应用在情感分析任务上,有关实验的数据集可以参考前面的文章《FastText文本分类与tensorflow实现》,TextCNN的模型代码如下:

import os
import numpy as np
import tensorflow as tf
from eval.evaluate import accuracy
from tensorflow.contrib import slim
from loss.loss import cross_entropy_loss

class TextCNN(object):
    def __init__(self,
                 num_classes,
                 seq_length,
                 vocab_size,
                 embedding_dim,
                 learning_rate,
                 learning_decay_rate,
                 learning_decay_steps,
                 epoch,
                 filter_sizes,
                 num_filters,
                 dropout_keep_prob,
                 l2_lambda
                 ):
        self.num_classes = num_classes
        self.seq_length = seq_length
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.learning_rate = learning_rate
        self.learning_decay_rate = learning_decay_rate
        self.learning_decay_steps = learning_decay_steps
        self.epoch = epoch
        self.filter_sizes = filter_sizes
        self.num_filters = num_filters
        self.dropout_keep_prob = dropout_keep_prob
        self.l2_lambda = l2_lambda
        self.input_x = tf.placeholder(tf.int32, [None, self.seq_length], name='input_x')
        self.input_y = tf.placeholder(tf.float32, [None, self.num_classes], name='input_y')
        self.l2_loss = tf.constant(0.0)
        self.model()

    def model(self):
        # embedding层
        with tf.name_scope("embedding"):
            self.embedding= tf.Variable(tf.random_uniform([self.vocab_size, self.embedding_dim], -1.0, 1.0),
                                        name="embedding")
            self.embedding_inputs = tf.nn.embedding_lookup(self.embedding,self.input_x)
            self.embedding_inputs = tf.expand_dims(self.embedding_inputs,-1)

        # 卷积层 + 池化层
        pooled_outputs = []
        for i, filter_size in enumerate(self.filter_sizes):
            with tf.name_scope("conv_{0}".format(filter_size)):
                filter_shape = [filter_size, self.embedding_dim, 1, self.num_filters]
                W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W")
                b = tf.Variable(tf.constant(0.1, shape=[self.num_filters]), name="b")
                conv = tf.nn.conv2d(
                    self.embedding_inputs,
                    W,
                    strides=[1, 1, 1, 1],
                    padding="VALID",
                    name="conv"
                )
                h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")
                pooled = tf.nn.max_pool(
                    h,
                    ksize=[1, self.seq_length - filter_size + 1, 1, 1],
                    strides=[1, 1, 1, 1],
                    padding='VALID',
                    name="pool"
                )
                pooled_outputs.append(pooled)

        # 将每种尺寸的卷积核得到的特征向量进行拼接
        num_filters_total = self.num_filters * len(self.filter_sizes)
        h_pool = tf.concat(pooled_outputs, 3)
        h_pool_flat = tf.reshape(h_pool, [-1, num_filters_total])

        # 对最终得到的句子向量进行dropout
        with tf.name_scope("dropout"):
            h_drop = tf.nn.dropout(h_pool_flat, self.dropout_keep_prob)

        # 全连接层
        with tf.name_scope("output"):
            W = tf.get_variable("W",shape=[num_filters_total, self.num_classes],
                                initializer=tf.contrib.layers.xavier_initializer())
            b = tf.Variable(tf.constant(0.1, shape=[self.num_classes]), name="b")
            self.l2_loss += tf.nn.l2_loss(W)
            self.l2_loss += tf.nn.l2_loss(b)
            self.logits = tf.nn.xw_plus_b(h_drop, W, b, name="scores")
            self.pred = tf.argmax(self.logits, 1, name="predictions")

        # 损失函数
        self.loss = cross_entropy_loss(logits=self.logits, labels=self.input_y) + self.l2_lambda*self.l2_loss

        # 优化函数
        self.global_step = tf.train.get_or_create_global_step()
        learning_rate = tf.train.exponential_decay(self.learning_rate, self.global_step,
                                                   self.learning_decay_steps, self.learning_decay_rate,
                                                   staircase=True)

        optimizer = tf.train.AdamOptimizer(learning_rate)
        update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
        self.optim = slim.learning.create_train_op(total_loss=self.loss, optimizer=optimizer, update_ops=update_ops)

        # 准确率
        self.acc = accuracy(logits=self.logits, labels=self.input_y)

    def fit(self,train_x,train_y,val_x,val_y,batch_size):
        # 创建模型保存路径
        if not os.path.exists('./saves/textcnn'): os.makedirs('./saves/textcnn')
        if not os.path.exists('./train_logs/textcnn'): os.makedirs('./train_logs/textcnn')

        # 开始训练
        train_steps = 0
        best_val_acc = 0
        # summary
        tf.summary.scalar('val_loss', self.loss)
        tf.summary.scalar('val_acc', self.acc)
        merged = tf.summary.merge_all()

        # 初始化变量
        sess = tf.Session()
        writer = tf.summary.FileWriter('./train_logs/textcnn', sess.graph)
        saver = tf.train.Saver(max_to_keep=10)
        sess.run(tf.global_variables_initializer())

        for i in range(self.epoch):
            batch_train = self.batch_iter(train_x, train_y, batch_size)
            for batch_x,batch_y in batch_train:
                train_steps += 1
                feed_dict = {self.input_x:batch_x,self.input_y:batch_y}
                _, train_loss, train_acc = sess.run([self.optim,self.loss,self.acc],feed_dict=feed_dict)

                if train_steps % 1000 == 0:
                    feed_dict = {self.input_x:val_x,self.input_y:val_y}
                    val_loss,val_acc = sess.run([self.loss,self.acc],feed_dict=feed_dict)

                    summary = sess.run(merged,feed_dict=feed_dict)
                    writer.add_summary(summary, global_step=train_steps)

                    if val_acc>=best_val_acc:
                        best_val_acc = val_acc
                        saver.save(sess, "./saves/textcnn/", global_step=train_steps)

                    msg = 'epoch:%d/%d,train_steps:%d,train_loss:%.4f,train_acc:%.4f,val_loss:%.4f,val_acc:%.4f'
                    print(msg % (i,self.epoch,train_steps,train_loss,train_acc,val_loss,val_acc))

        sess.close()

    def batch_iter(self, x, y, batch_size=32, shuffle=True):
        """
        生成batch数据
        :param x: 训练集特征变量
        :param y: 训练集标签
        :param batch_size: 每个batch的大小
        :param shuffle: 是否在每个epoch时打乱数据
        :return:
        """
        data_len = len(x)
        num_batch = int((data_len - 1) / batch_size) + 1

        if shuffle:
            shuffle_indices = np.random.permutation(np.arange(data_len))
            x_shuffle = x[shuffle_indices]
            y_shuffle = y[shuffle_indices]
        else:
            x_shuffle = x
            y_shuffle = y
        for i in range(num_batch):
            start_index = i * batch_size
            end_index = min((i + 1) * batch_size, data_len)
            yield (x_shuffle[start_index:end_index], y_shuffle[start_index:end_index])

    def predict(self,x):
        sess = tf.Session()
        sess.run(tf.global_variables_initializer())
        saver = tf.train.Saver(tf.global_variables())
        ckpt = tf.train.get_checkpoint_state('./saves/textcnn/')
        saver.restore(sess, ckpt.model_checkpoint_path)

        feed_dict = {self.input_x: x}
        logits = sess.run(self.logits, feed_dict=feed_dict)
        y_pred = np.argmax(logits, 1)
        return y_pred

    数据集方面的参数设置与 《FastText文本分类与tensorflow实现》中的一样,然后TextCNN的卷积核高度设置三种尺度,分别为3,4,5,每种尺度的卷积核数量均为100,另外,L2惩罚系数设置为3,最终模型的训练效果如图2所示,在3000个测试集上的预测准确率达到了97.56%,比之前的FastText大约高了2个百分点。

TextCNN文本分类与tensorflow实现_第2张图片 图2 TextCNN模型效果

4.总结

    最后,大致总结一下TextCNN的优缺点:

  • 模型采用CNN的结构,可以充分发挥CNN的并行计算能力,训练速度快
  • CNN通过调整卷积核的高度,可以灵活综合词汇的多种时序信息,这点比FastText要方便得多
  • CNN虽然可以捕捉到句子中词汇的局部时序信息,但是还是没法捕捉整个句子的时序信息,这一点是不如RNN的

你可能感兴趣的:(文本分类)