FastText文本分类与tensorflow实现

1.引言

    文本分类是NLP很多任务中都作为一项基本的任务,也是一项非常重要的任务,比如文本检索、情感分析、对话系统中的意图分析、文章归类等等,随着深度学习的发展,神经网络在NLP文本分类中的模型越来越多,也取得了比较不错的效果,但是,当语料和类别比较大时,很多深度学习方法不管在训练还是预测时,速度都非常慢,因此,为了克服该缺点,facebook在16年提出了一个轻量级的模型——FastText,该方法在标准的多核CPU上训练10亿级的词汇只需要10分钟,并且准确率可以与很多深度学习方法相媲美,论文的下载地址如下:

  • 论文地址:《Bag of Tricks for Efficient Text Classification》

    下面我们将具体介绍一下该模型的原理和结构,并用tensorflow来实现它。

2.FastText原理介绍

2.1 模型结构

    FastText的网络结构与word2vec中的CBOW很相似,如图1所示。模型主要包含一个投影层和一个输出层,与CBOW的结构一样,其中,与CBOW主要有两点不同:第一个是CBOW的输入是每个单词的上下文单词,而FastText的输入的一个文本中的特征,可以是Ngram语言模型的特征;第二个是CBOW的输出的目标词汇,而FastText的输出则是文本的类别。

    假设对于N个文本或文档,记x _ { n }为单个文本或文档的ngram特征,A为隐藏层(投影层)的权重矩阵,B为输出层的权重矩阵,则对于文本或文档的特征向量,每个特征会首先根据id映射到A中对应的向量,然后,对这n个特征向量计算平均作为整个文本或文档的向量表示,最后,将该文本或文档向量经过输出层,采用softmax函数f计算得到该文本或文档在每个类别对应的概率表示。可以发现,FastText在输出层中并没有采用非线性函数,因此,在训练和预测时速度非常快。

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

     在损失函数的选择方面,FastText选取的是负对数损失函数,其计算公式如下:

                                                                 - \frac { 1 } { N } \sum _ { n = 1 } ^ { N } y _ { n } \log \left( f \left( B A x _ { n } \right) \right)

其中,y_n表示文本的真实标签,其他符号的含义在上文都已经介绍,这里不再具体解释。模型的优化函数则采用随机梯度下降法(SGD)。 

2.2 层次softmax

    当文本的类别特别多时,此时,模型的计算会耗费大量的时间和资源,其计算的时间复杂度为O ( k h ),其中,k为文本的总类别数,h为文本的向量维度。为了提高速度,作者将输出层的softmax改为基于霍夫曼树的层次softmax,此时,模型的计算时间复杂度可以缩减为O \left( h \log _ { 2 } ( k ) \right)。层次softmax会更加各个类别出现的频率进行排序,如图2所示,其中,每个叶节点表示一个类别,当叶节点的深度越深时,则其概率将越低,假设一个叶节点的深度为l+1,其父节点列表为n _ { 1 } , \ldots , n _ { l },则该叶节点的概率计算公式为:

                                                                 P \left( n _ { l + 1 } \right) = \prod _ { i = 1 } ^ { l } P \left( n _ { i } \right)

采用这种层次的结构后,在计算文本的最大类别概率时,就可以直接抛弃那些概率小的分支,从而提高模型的训练和预测速度。

FastText文本分类与tensorflow实现_第2张图片 图2 层次softmax

2.3 调参技巧

     作者将FastText与几个文本分类的主流模型进行比较,如CNN、VDCNN、TFIDF、SVM等,发现FastText在提高速度的同时,准确率也取得不错的效果,在实验中,作者发现几个技巧对模型的效果有显著的影响,分别是:

  • Ngram特征的长度,适当提高特征的长度,有助于提高模型的准确率
  • 隐藏层的维度可以适当增大
  • 剔除数量较少的类别和特征

3.FastText的tensorflow实现

    本文利用了github上公开的一些情感分析数据集作为本次实验的数据,数据下载地址如下:

  • 数据地址:https://github.com/SophonPlus/ChineseNlpCorpus

    由于大部分数据集都是只有正负两种类别,因此,本文只选择了二分类的数据集,其中,微博数据因为标注质量比较差,因此,本文没有选择微博的评论数据,最终,选择了130万的语料作为训练数据,其中0.1%作为验证集,模型的主要代码如下:

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 FastText():
    def __init__(self,
                 num_classes,
                 seq_length,
                 vocab_size,
                 embedding_dim,
                 learning_rate,
                 learning_decay_rate,
                 learning_decay_steps,
                 epoch,
                 dropout_keep_prob
                 ):
        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.dropout_keep_prob = dropout_keep_prob
        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.model()

    def model(self):
        # 词向量映射
        with tf.name_scope("embedding"):
            self.embedding = tf.get_variable("embedding", [self.vocab_size, self.embedding_dim])
            embedding_inputs = tf.nn.embedding_lookup(self.embedding, self.input_x)

        with tf.name_scope("dropout"):
            dropout_output = tf.nn.dropout(embedding_inputs, self.dropout_keep_prob)

        # 对词向量进行平均
        with tf.name_scope("average"):
            mean_sentence = tf.reduce_mean(dropout_output, axis=1)

        # 输出层
        with tf.name_scope("score"):
            self.logits = tf.layers.dense(mean_sentence, self.num_classes,name='dense_layer')

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

        # 优化函数
        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/fasttext'): os.makedirs('./saves/fasttext')
        if not os.path.exists('./train_logs/fasttext'): os.makedirs('./train_logs/fasttext')

        # 开始训练
        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/fasttext', 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/fasttext/", 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))

    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/fasttext/')
        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

     在参数设置方面,隐藏层的维度选择的是200,词汇数量是100000,句子长度是166,最终,在经过31000次参数更新后,模型基本达到稳定,模型的效果如下:

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

    在3000个测试集上的准确率基本达到95%,模型效果还是蛮不错的,笔者通过实验发现,适当提高embedding的维度,对embedding添加dropout可以提高模型的效果,另外,不要轻易剔除停用词,因为情感分析中很多意思其实可以通过一些连接词、转折词表达出来,笔者在剔除停用词的情况下,在测试集的准确率只有77%左右,因此,建议不要剔除停用词。

4.总结

    最后,大概总结一下FastText模型的优缺点:

  • 训练和预测的速度快,在标准的多核CPU上训练10亿级的词汇只需要10分钟。
  • 准确率高,可以与很多深度学习方法相媲美。
  • 模型简单,参数量少,收敛速度快。
  • 模型没法捕捉句子的时序信息,对于一些包含多种情感的句子可能效果就不是很好。

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