FastText文本分类与tensorflow实现

1.引言

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

下面我们将具体介绍一下该模型的原理和结构,并用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在输出层中并没有采用非线性函数,因此,在训练和预测时速度非常快。

图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)

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

图2 层次softmax

2.3 调参技巧

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

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

3.FastText的tensorflow实现

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

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

  1. import os
  2. import numpy as np
  3. import tensorflow as tf
  4. from eval.evaluate import accuracy
  5. from tensorflow.contrib import slim
  6. from loss.loss import cross_entropy_loss
  7. class FastText():
  8. def __init__(self,
  9. num_classes,
  10. seq_length,
  11. vocab_size,
  12. embedding_dim,
  13. learning_rate,
  14. learning_decay_rate,
  15. learning_decay_steps,
  16. epoch,
  17. dropout_keep_prob
  18. ):
  19. self.num_classes = num_classes
  20. self.seq_length = seq_length
  21. self.vocab_size = vocab_size
  22. self.embedding_dim = embedding_dim
  23. self.learning_rate = learning_rate
  24. self.learning_decay_rate = learning_decay_rate
  25. self.learning_decay_steps = learning_decay_steps
  26. self.epoch = epoch
  27. self.dropout_keep_prob = dropout_keep_prob
  28. self.input_x = tf.placeholder(tf.int32, [None, self.seq_length], name=‘input_x’)
  29. self.input_y = tf.placeholder(tf.float32, [None, self.num_classes], name=‘input_y’)
  30. self.model()
  31. def model(self):
  32. # 词向量映射
  33. with tf.name_scope(“embedding”):
  34. self.embedding = tf.get_variable(“embedding”, [self.vocab_size, self.embedding_dim])
  35. embedding_inputs = tf.nn.embedding_lookup(self.embedding, self.input_x)
  36. with tf.name_scope(“dropout”):
  37. dropout_output = tf.nn.dropout(embedding_inputs, self.dropout_keep_prob)
  38. # 对词向量进行平均
  39. with tf.name_scope(“average”):
  40. mean_sentence = tf.reduce_mean(dropout_output, axis=1)
  41. # 输出层
  42. with tf.name_scope(“score”):
  43. self.logits = tf.layers.dense(mean_sentence, self.num_classes,name=‘dense_layer’)
  44. # 损失函数
  45. self.loss = cross_entropy_loss(logits=self.logits,labels=self.input_y)
  46. # 优化函数
  47. self.global_step = tf.train.get_or_create_global_step()
  48. learning_rate = tf.train.exponential_decay(self.learning_rate, self.global_step,
  49. self.learning_decay_steps, self.learning_decay_rate,
  50. staircase=True)
  51. optimizer= tf.train.AdamOptimizer(learning_rate)
  52. update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
  53. self.optim = slim.learning.create_train_op(total_loss=self.loss, optimizer=optimizer,update_ops=update_ops)
  54. # 准确率
  55. self.acc = accuracy(logits=self.logits,labels=self.input_y)
  56. def fit(self,train_x,train_y,val_x,val_y,batch_size):
  57. # 创建模型保存路径
  58. if not os.path.exists(‘./saves/fasttext’): os.makedirs(‘./saves/fasttext’)
  59. if not os.path.exists(‘./train_logs/fasttext’): os.makedirs(‘./train_logs/fasttext’)
  60. # 开始训练
  61. train_steps = 0
  62. best_val_acc = 0
  63. # summary
  64. tf.summary.scalar(‘val_loss’, self.loss)
  65. tf.summary.scalar(‘val_acc’, self.acc)
  66. merged = tf.summary.merge_all()
  67. # 初始化变量
  68. sess = tf.Session()
  69. writer = tf.summary.FileWriter(‘./train_logs/fasttext’, sess.graph)
  70. saver = tf.train.Saver(max_to_keep=10)
  71. sess.run(tf.global_variables_initializer())
  72. for i in range(self.epoch):
  73. batch_train = self.batch_iter(train_x, train_y, batch_size)
  74. for batch_x,batch_y in batch_train:
  75. train_steps += 1
  76. feed_dict = {self.input_x:batch_x,self.input_y:batch_y}
  77. _, train_loss, train_acc = sess.run([self.optim,self.loss,self.acc],feed_dict=feed_dict)
  78. if train_steps % 1000 == 0:
  79. feed_dict = {self.input_x:val_x,self.input_y:val_y}
  80. val_loss,val_acc = sess.run([self.loss,self.acc],feed_dict=feed_dict)
  81. summary = sess.run(merged,feed_dict=feed_dict)
  82. writer.add_summary(summary, global_step=train_steps)
  83. if val_acc>=best_val_acc:
  84. best_val_acc = val_acc
  85. saver.save(sess, “./saves/fasttext/”, global_step=train_steps)
  86. msg = ‘epoch:%d/%d,train_steps:%d,train_loss:%.4f,train_acc:%.4f,val_loss:%.4f,val_acc:%.4f’
  87. print(msg % (i,self.epoch,train_steps,train_loss,train_acc,val_loss,val_acc))
  88. def batch_iter(self, x, y, batch_size=32, shuffle=True):
  89. “””
  90. 生成batch数据
  91. :param x: 训练集特征变量
  92. :param y: 训练集标签
  93. :param batch_size: 每个batch的大小
  94. :param shuffle: 是否在每个epoch时打乱数据
  95. :return:
  96. “””
  97. data_len = len(x)
  98. num_batch = int((data_len – 1) / batch_size) + 1
  99. if shuffle:
  100. shuffle_indices = np.random.permutation(np.arange(data_len))
  101. x_shuffle = x[shuffle_indices]
  102. y_shuffle = y[shuffle_indices]
  103. else:
  104. x_shuffle = x
  105. y_shuffle = y
  106. for i in range(num_batch):
  107. start_index = i * batch_size
  108. end_index = min((i + 1) * batch_size, data_len)
  109. yield (x_shuffle[start_index:end_index], y_shuffle[start_index:end_index])
  110. def predict(self,x):
  111. sess = tf.Session()
  112. sess.run(tf.global_variables_initializer())
  113. saver = tf.train.Saver(tf.global_variables())
  114. ckpt = tf.train.get_checkpoint_state(‘./saves/fasttext/’)
  115. saver.restore(sess, ckpt.model_checkpoint_path)
  116. feed_dict = {self.input_x: x}
  117. logits = sess.run(self.logits, feed_dict=feed_dict)
  118. y_pred = np.argmax(logits, 1)
  119. return y_pred

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

图3 模型分类效果

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