一、简介
1.1:fastText背景
fasttext是facebook开源的一个词向量与文本分类工具,在2016年开源,典型应用场景是“带监督的文本分类问题”。提供简单而高效的文本分类和表征学习的方法,性能比肩深度学习而且速度更快。
fastText结合了自然语言处理和机器学习中最成功的理念。这些包括了使用词袋以及n-gram袋表征语句,还有使用子字(subword)信息,并通过隐藏表征在类别间共享信息。我们另外采用了一个softmax层级(利用了类别不均衡分布的优势)来加速运算过程。
这些不同概念被用于两个不同任务:
有效文本分类 :有监督学习
学习词向量表征:无监督学习
1.2:fastText优点:
fastText是一个快速文本分类算法,与基于神经网络的分类算法相比有两大优点:
1、fastText在保持高精度的情况下加快了训练速度和测试速度
2、fastText不需要预训练好的词向量,fastText会自己训练词向量
3、fastText两个重要的优化:Hierarchical Softmax、N-gram
二、原理
2.1:fastText模型架构
fastText模型架构和word2vec中的CBOW很相似, 不同之处是fastText预测标签而CBOW预测的是中间词,即模型架构类似但是模型的任务不同。下面我们先看一下两者的架构:
![]() |
![]() |
两者不同:
· 输入层:CBOW的输入是目标单词的上下文并进行one-hot编码,fastText的输入是多个单词embedding向量,并将单词的字符级别的n-gram向量作为额外的特征;
· 从输入层到隐藏层,CBOW会将经过一次矩阵乘法并上下文单词向量叠加起来(线性变化)并应用激活函数,而fastText省略了这一过程,直接将embedding过的向量特征求和取平均;
· 输出层,一般的CBOW模型会采用Softmax作为输出,而fastText则采用了Hierarchical Softmax,大大降低了模型训练时间;
· CBOW的输出是目标词汇,fastText的输出是文档对应的类标。
2.2:Subword n-gramlnformation
在fastText的工作之前,大部分的文本向量化的工作,都是以词汇表中的独立单词作为基本单元来进行训练学习的。这种想法非常自然,但也会带来如下的问题:
· 低频词、罕见词,由于在语料中本身出现的次数就少,得不到足够的训练,效果不佳;
· 未登录词,如果出现了一些在词典中都没有出现过的词,或者带有某些拼写错误的词,传统模型更加无能为力。
fastText引入了subword n-gram的概念来解决词形变化(morphology)的问题。直观上,它将一个单词打散到字符级别,并且利用字符级别的n-gram信息来捕捉字符间的顺序关系,希望能够以此丰富单词内部更细微的语义。我们知道,西方语言文字常常通过前缀、后缀、字根来构词,汉语也有单字表义的传统,所以这样的做法听起来还是有一定的道理。
举个例子。对于一个单词“google”,为了表达单词前后边界,我们加入<>两个字符,即变形为“<google>”。假设我们希望抽取所有的tri-gram信息,可以得到如下集合:G = { <go, goo, oog,ogl, gle, le>}。在实践中,我们往往会同时提取单词的多种n-gram信息,如2/3/4/5-gram。这样,原始的一个单词google,就被一个字符级别的n-gram集合所表达。
在训练过程中,每个n-gram都会对应训练一个向量,而原来完整单词的词向量就由它对应的所有n-gram的向量求和得到。所有的单词向量以及字符级别的n-gram向量会同时相加求平均作为训练模型的输入。
从实验效果来看,subword n-gram信息的加入,不但解决了低频词未登录词的表达的问题,而且对于最终任务精度一般会有几个百分点的提升。唯一的问题就是由于需要估计的参数多,模型可能会比较膨胀。
2.3:压缩模型的建议:
· 采用hash-trick。由于n-gram原始的空间太大,可以用某种hash函数将其映射到固定大小的buckets中去,从而实现内存可控;
· 采用quantize命令,对生成的模型进行参数量化和压缩;
· 减小最终向量的维度。
需要注意的是以上几种方法都会以一定的精度损失为代价,尤其是维度的压缩,具体可以实践中再权衡。
2.4:Hierarchical Softmax
层次化的Softmax的思想实质上是将一个全局多分类的问题,转化成为了若干个二元分类问题,从而将计算复杂度从O(V)降到O(logV)。
每个二元分类问题,由一个基本的逻辑回归单元来实现。如下图所示,从根结点开始,每个中间结点(标记成灰色)都是一个逻辑回归单元,根据它的输出来选择下一步是向左走还是向右走。下图示例中实际上走了一条“左-左-右”的路线,从而找到单词w₂。而最终输出单词w₂的概率,等于中间若干逻辑回归单元输出概率的连乘积。
![]() |
![]() |
逻辑回归单元的参数
每个逻辑回归单元中,sigmoid函数所需的输入实际上由三项构成,如下公式所示:
![]() |
记号说明如下:
1. ⟦x⟧是一个特殊的函数,如果下一步需要向左走其函数值定义为1,向右则取-1。在训练时,我们知道最终输出叶子结点,并且从根结点到叶子结点的每一步的路径也是确定的。
2. v' 是每个内部结点(逻辑回归单元)对应的一个向量,这个向量可以在训练过程中学习和更新。
3. h 是网络中隐藏层的输出。
因此,我们以隐藏层的输出、中间结点对应向量以及路径取向函数为输入,相乘后再经过sigmoid函数,得到每一步逻辑回归的输出值。
三:代码实战(fastText地名识别)
import random
import fasttext
import config
import os
cate_dic = {'place': 1, 'others': 0}
def loadData():
"""
函数说明:加载数据
"""
pos_data, neg_data = [], []
if (not os.path.exists(config.path['postive_path']) or not os.path.exists(config.path['negative_path'])):
print("正样本或负样本文件有个不存在")
fr_one = open(config.path['postive_path'])
fr_two = open(config.path['negative_path'])
for line in fr_one:
ele_list = line.strip().split();
if (len(ele_list) > 0):
pos_data.append(ele_list[0])
else:
continue
for line in fr_two:
ele_list = line.strip().split();
if (len(ele_list) > 0):
neg_data.append(ele_list[0])
else:
continue
print('正样本个数是%d' % int(len(pos_data)))
print('负样本个数是%d' % int(len(neg_data)))
return pos_data, neg_data
def sprate(x):
"""
分割字,保存在list中
:param x:
:return:
"""
ele_list = []
for i in range(len(x)):
ele_list.append(x[i])
return ele_list
def preprocess_text(content_line, sentences, category):
"""
函数说明:按字进行分词,并转化为fastext的文本格式
参数:
content_line:文本数据
sentences:存储的数据
category:文本类别
"""
for line in content_line:
try:
ele_list = sprate(line)
sentences.append(
"__label__" + str(category) + " , " + " ".join(ele_list)) # 把当前的文本和对应的类别拼接起来,组合成fasttext的文本格式
except Exception as e:
print(line)
continue
def writeData(sentences, fileName):
"""
函数说明:把处理好的写入到文件中,备用
参数说明:
"""
print("writing data to fasttext format...")
out = open(fileName, 'w')
for sentence in sentences:
out.write(sentence + "\n")
print("done!")
def preprocessData(saveDataFile):
"""
生成数据集,保存训练集
:param saveDataFile:
:return:
"""
place, others = loadData()
sentences = []
preprocess_text(place, sentences, cate_dic["place"])
preprocess_text(others, sentences, cate_dic["others"])
print(sentences)
random.shuffle(sentences) # 做乱序处理,使得同类别的样本不至于扎堆
writeData(sentences, saveDataFile)
if __name__ == "__main__":
saveDataFile = config.path['save_data_path']
preprocessData(saveDataFile)
model = fasttext.train_supervised(saveDataFile, lr=0.1, dim=100, epoch=10, word_ngrams=3, loss='softmax')
result = model.test(saveDataFile)
model.save_model("./model_file.bin")
print(result)
lable_to_cate = {'__label__1': 'place', '__label__0': 'others'}
texts = ['马 家 小 学 镇']
lables = model.predict(texts)
print(lables)
print(lable_to_cate[lables[0][0][0]])