TextCnn原理及实践

原理

paper地址:https://arxiv.org/pdf/1408.5882.pdf
对于文本分类问题,常见的方法无非就是抽取文本的特征,比如使用doc2evc或者LDA模型将文本转换成一个固定维度的特征向量,然后在基于抽取的特征训练一个分类器。
然而研究证明,TextCnn在文本分类问题上有着更加卓越的表现。

TextCnn的结构

1. 嵌入层(embedding layer)

textcnn使用预先训练好的词向量作embedding layer。对于数据集里的所有词,因为每个词都可以表征成一个向量,因此我们可以得到一个嵌入矩阵 M , M 里的每一行都是词向量。这个 M 可以是静态(static)的,也就是固定不变。可以是非静态(non-static)的,也就是可以根据反向传播更新

2. 卷积池化层(convolution and pooling)

卷积(convolution)

输入一个句子,首先对这个句子进行切词,假设有 s 个单词。对每个词,跟句嵌入矩阵 M , 可以得到词向量。假设词向量一共有 d 维。那么对于这个句子,便可以得到 s d 列的矩阵 A ϵ R s × d .
我们可以把矩阵 A 看成是一幅图像,使用卷积神经网络去提取特征。由于句子中相邻的单词关联性总是很高的,因此可以使用一维卷积。卷积核的宽度就是词向量的维度 d ,高度是超参数,可以设置。
现在假设有一个卷积核,是一个宽度为 d ,高度为 h 的矩阵 w ,那么 w h d 个参数需要被更新。对于一个句子,经过嵌入层之后可以得到矩阵 A ϵ R s × d . A [ i : j ] 表示 A 的第 i 行到第 j 行, 那么卷积操作可以用如下公式表示:

o i = w A [ i : i + h 1 ] , i = 1 , 2 , . . . , s h + 1

叠加上偏置 b ,在使用激活函数 f 激活, 得到所需的特征。公式如下:

c i = f ( o i + b )

对一个卷积核,可以得到特征 c ϵ R s h + 1 , 总共 s h + 1 个特征。我们可以使用更多高度不同的卷积核,得到更丰富的特征表达。

池化(pooling)

不同尺寸的卷积核得到的特征(feature map)大小也是不一样的,因此我们对每个feature map使用池化函数,使它们的维度相同。最常用的就是1-max pooling,提取出feature map照片那个的最大值。这样每一个卷积核得到特征就是一个值,对所有卷积核使用1-max pooling,再级联起来,可以得到最终的特征向量,这个特征向量再输入softmax layer做分类。这个地方可以使用drop out防止过拟合

以上过程可以用下图直观表示

这里写图片描述

  • 这里word embedding的维度是5。对于句子 i like this movie very much。可以转换成如上图所示的矩阵 A ϵ R 7 × 5
  • 有6个卷积核,尺寸为 ( 2 × 5 ) , ( 3 × 5 ) , 4 × 5 ,每个尺寸各2个.
  • A 分别与以上卷积核进行卷积操作,再用激活函数激活。每个卷积核都得到了特征向量(feature maps)
  • 使用1-max pooling提取出每个feature map的最大值,然后在级联得到最终的特征表达。
  • 将特征输入至softmax layer进行分类, 在这层可以进行正则化操作( l2-regulariation)

3. 参数选择

根据文章中的描述,总结如下。感兴趣的同学请仔细阅读原文
1. 初始化词向量
使用word2vec和golve都可以,不要使用one-hot vectors
2. 卷积核的尺寸
1-10之间,具体情况具体分析,对最终结果影响较大。一般来讲,句子长度越长,卷积核的尺寸越大。
3. 每种尺寸卷积核的数量
100-600之间,对模型性能影响较大,需要注意的是增加卷积核的数量会增加训练模型的实践。
4. 激活函数的选择
使用relu函数
5. drop out rate
0.0-0.5, 当增加卷积核的数量时,可以尝试增加drop out rate,甚至可以大于0.5
6. 池化的选择
1-max pooling
7. 正则项
正则项对最终模型性能的影响很小

基于tensorflow的代码实现

# coding: utf-8
import pickle
import logging
import tensorflow as tf
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s',level=logging.INFO)

class TextCNN(object):
    """
    A CNN for text classification.
    Uses an embedding layer, followed by a convolution, max-pooling and soft-max layer.
    """
    def __init__(self, config):
        self.lr = config['lr']
        self.batch_size = config['batch_size']
        # 词典的大小
        self.vocab_size = config['vocab_size']
        self.num_classes = config['num_classes']
        self.keep_prob = config['keep_prob']
        # length of word embedding
        self.embedding_size = config['embedding_size']
        # seting filter sizes, type of list
        self.filter_sizes = config['filter_sizes']
        # max length of sentence
        self.sentence_length = config['sentence_length']
        # number of filters
        self.num_filters = config['num_filters']

    def add_placeholders(self):
        self.X = tf.placeholder('int32', [None, self.sentence_length])
        self.y = tf.placeholder('int32', [None, ])

    def inference(self):
        with tf.variable_scope('embedding_layer'):
            # loading embedding weights
            with open('Text_cnn/embedding_matrix.pkl','rb') as f:
                embedding_weights = pickle.load(f)
            # non-static 
            self.W = tf.Variable(embedding_weights, trainable=True, name='embedding_weights',dtype='float32')
            # shape of embedding chars is (None, sentence_length, embedding_size)
            self.embedding_chars = tf.nn.embedding_lookup(self.W, self.X)
            # shape of embedding char expanded is (None, sentence_length, embedding_size, 1)
            self.embedding_chars_expanded = tf.expand_dims(self.embedding_chars, -1)
        with tf.variable_scope('convolution_pooling_layer'):
            pooled_outputs = []
            for i, filter_size in enumerate(self.filter_sizes):
                filter_shape = [filter_size, self.embedding_size, 1, self.num_filters]
                W = tf.get_variable('W'+str(i), shape=filter_shape,
                                    initializer=tf.truncated_normal_initializer(stddev=0.1))
                b = tf.get_variable('b'+str(i), shape=[self.num_filters],
                                    initializer=tf.zeros_initializer())
                conv = tf.nn.conv2d(self.embedding_chars_expanded, W, strides=[1,1,1,1],
                                    padding='VALID', name='conv'+str(i))
                # apply nonlinearity
                h = tf.nn.relu(tf.add(conv, b))
                # max pooling
                pooled = tf.nn.max_pool(h, ksize=[1, self.sentence_length - filter_size + 1, 1, 1],
                    strides=[1, 1, 1, 1], padding='VALID', name="pool")
                # shape of pooled is (?,1,1,300)
                pooled_outputs.append(pooled)
            # combine all the pooled features
            self.feature_length = self.num_filters * len(self.filter_sizes)
            self.h_pool = tf.concat(pooled_outputs,3)
            # shape of (?, 900)
            self.h_pool_flat = tf.reshape(self.h_pool, [-1, self.feature_length])
        # add dropout before softmax layer
        with tf.variable_scope('dropout_layer'):
            # shape of [None, feature_length]
            self.features = tf.nn.dropout(self.h_pool_flat, self.keep_prob)
        # fully-connection layer
        with tf.variable_scope('fully_connection_layer'):
            W = tf.get_variable('W', shape=[self.feature_length, self.num_classes],
                                initializer=tf.contrib.layers.xavier_initializer())
            b = tf.get_variable('b', shape=[self.num_classes],
                                initializer=tf.constant_initializer(0.1))
            # shape of [None, 2]
            self.y_out = tf.matmul(self.features, W) + b
            self.y_prob = tf.nn.softmax(self.y_out)

    def add_loss(self):
        loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=self.y, logits=self.y_out)
        self.loss = tf.reduce_mean(loss)
        tf.summary.scalar('loss',self.loss)

    def add_metric(self):
        self.y_pred = self.y_prob[:,1] > 0.5
        self.precision, self.precision_op = tf.metrics.precision(self.y, self.y_pred)
        self.recall, self.recall_op = tf.metrics.recall(self.y, self.y_pred)
        # add precision and recall to summary
        tf.summary.scalar('precision', self.precision)
        tf.summary.scalar('recall', self.recall)

    def train(self):
        # Applies exponential decay to learning rate
        self.global_step = tf.Variable(0, trainable=False)
        # define optimizer
        optimizer = tf.train.AdamOptimizer(self.lr)
        extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
        with tf.control_dependencies(extra_update_ops):
            self.train_op = optimizer.minimize(self.loss, global_step=self.global_step)

    def build_graph(self):
        """build graph for model"""
        self.add_placeholders()
        self.inference()
        self.add_loss()
        self.add_metric()
        self.train()

猜你喜欢

转载自blog.csdn.net/john_xyz/article/details/79210088