文本分类是NLP很多任务中都作为一项基本的任务,也是一项非常重要的任务,比如文本检索、情感分析、对话系统中的意图分析、文章归类等等,随着深度学习的发展,神经网络在NLP文本分类中的模型越来越多,也取得了比较不错的效果,但是,当语料和类别比较大时,很多深度学习方法不管在训练还是预测时,速度都非常慢,因此,为了克服该缺点,facebook在16年提出了一个轻量级的模型——FastText,该方法在标准的多核CPU上训练10亿级的词汇只需要10分钟,并且准确率可以与很多深度学习方法相媲美,论文的下载地址如下:
下面我们将具体介绍一下该模型的原理和结构,并用tensorflow来实现它。
2.1 模型结构
FastText的网络结构与word2vec中的CBOW很相似,如图1所示。模型主要包含一个投影层和一个输出层,与CBOW的结构一样,其中,与CBOW主要有两点不同:第一个是CBOW的输入是每个单词的上下文单词,而FastText的输入的一个文本中的特征,可以是Ngram语言模型的特征;第二个是CBOW的输出的目标词汇,而FastText的输出则是文本的类别。
假设对于N个文本或文档,记为单个文本或文档的ngram特征,为隐藏层(投影层)的权重矩阵,为输出层的权重矩阵,则对于文本或文档的特征向量,每个特征会首先根据id映射到中对应的向量,然后,对这个特征向量计算平均作为整个文本或文档的向量表示,最后,将该文本或文档向量经过输出层,采用softmax函数计算得到该文本或文档在每个类别对应的概率表示。可以发现,FastText在输出层中并没有采用非线性函数,因此,在训练和预测时速度非常快。
在损失函数的选择方面,FastText选取的是负对数损失函数,其计算公式如下:
其中,表示文本的真实标签,其他符号的含义在上文都已经介绍,这里不再具体解释。模型的优化函数则采用随机梯度下降法(SGD)。
2.2 层次softmax
当文本的类别特别多时,此时,模型的计算会耗费大量的时间和资源,其计算的时间复杂度为,其中,为文本的总类别数,为文本的向量维度。为了提高速度,作者将输出层的softmax改为基于霍夫曼树的层次softmax,此时,模型的计算时间复杂度可以缩减为。层次softmax会更加各个类别出现的频率进行排序,如图2所示,其中,每个叶节点表示一个类别,当叶节点的深度越深时,则其概率将越低,假设一个叶节点的深度为,其父节点列表为,则该叶节点的概率计算公式为:
采用这种层次的结构后,在计算文本的最大类别概率时,就可以直接抛弃那些概率小的分支,从而提高模型的训练和预测速度。
2.3 调参技巧
作者将FastText与几个文本分类的主流模型进行比较,如CNN、VDCNN、TFIDF、SVM等,发现FastText在提高速度的同时,准确率也取得不错的效果,在实验中,作者发现几个技巧对模型的效果有显著的影响,分别是:
本文利用了github上公开的一些情感分析数据集作为本次实验的数据,数据下载地址如下:
由于大部分数据集都是只有正负两种类别,因此,本文只选择了二分类的数据集,其中,微博数据因为标注质量比较差,因此,本文没有选择微博的评论数据,最终,选择了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次参数更新后,模型基本达到稳定,模型的效果如下:
在3000个测试集上的准确率基本达到95%,模型效果还是蛮不错的,笔者通过实验发现,适当提高embedding的维度,对embedding添加dropout可以提高模型的效果,另外,不要轻易剔除停用词,因为情感分析中很多意思其实可以通过一些连接词、转折词表达出来,笔者在剔除停用词的情况下,在测试集的准确率只有77%左右,因此,建议不要剔除停用词。
最后,大概总结一下FastText模型的优缺点: