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个文本或文档,记为单个文本或文档的ngram特征,为隐藏层(投影层)的权重矩阵,为输出层的权重矩阵,则对于文本或文档的特征向量,每个特征会首先根据id映射到中对应的向量,然后,对这个特征向量计算平均作为整个文本或文档的向量表示,最后,将该文本或文档向量经过输出层,采用softmax函数计算得到该文本或文档在每个类别对应的概率表示。可以发现,FastText在输出层中并没有采用非线性函数,因此,在训练和预测时速度非常快。
在损失函数的选择方面,FastText选取的是负对数损失函数,其计算公式如下:
其中,表示文本的真实标签,其他符号的含义在上文都已经介绍,这里不再具体解释。模型的优化函数则采用随机梯度下降法(SGD)。
2.2 层次softmax
当文本的类别特别多时,此时,模型的计算会耗费大量的时间和资源,其计算的时间复杂度为,其中,为文本的总类别数,为文本的向量维度。为了提高速度,作者将输出层的softmax改为基于霍夫曼树的层次softmax,此时,模型的计算时间复杂度可以缩减为。层次softmax会更加各个类别出现的频率进行排序,如图2所示,其中,每个叶节点表示一个类别,当叶节点的深度越深时,则其概率将越低,假设一个叶节点的深度为,其父节点列表为,则该叶节点的概率计算公式为:
采用这种层次的结构后,在计算文本的最大类别概率时,就可以直接抛弃那些概率小的分支,从而提高模型的训练和预测速度。
2.3 调参技巧
作者将FastText与几个文本分类的主流模型进行比较,如CNN、VDCNN、TFIDF、SVM等,发现FastText在提高速度的同时,准确率也取得不错的效果,在实验中,作者发现几个技巧对模型的效果有显著的影响,分别是:
- Ngram特征的长度,适当提高特征的长度,有助于提高模型的准确率
- 隐藏层的维度可以适当增大
- 剔除数量较少的类别和特征
3.FastText的tensorflow实现
本文利用了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%左右,因此,建议不要剔除停用词。