FastText:快速的文本分类器
参考文档《word2vec原理和gensim实现》、《深入浅出Word2Vec原理解析》
Word2Vec是轻量级的神经网络,其模型仅仅包括输入层、隐藏层和输出层,模型框架根据输入输出的不同,主要包括CBOW和Skip-gram模型。
霍夫曼树解法:
负采样:
两种解法进行一定优化,牺牲了一定的分类的准确度。比如负采样的负样本是随机选取的,所以相对已经没那么准了。
如果词汇表的大小为 V V V,那么我们就将一段长度为1的线段分成 V V V份,每份对应词汇表中的一个词。高频词对应的线段长,低频词对应的线段短(高频词数量多,分子count就大)。每个词 w w w的线段长度由下式决定: l e n ( w ) = c o u n t ( w ) ∑ u ∈ v o c a b c o u n t ( u ) len(w) = \frac{count(w)}{\sum\limits_{u \in vocab} count(u)} len(w)=u∈vocab∑count(u)count(w)
在word2vec中,分子和分母都取了3/4次幂(经验参数,提高低频词被选取的概率)如下: l e n ( w ) = c o u n t ( w ) 3 / 4 ∑ u ∈ v o c a b c o u n t ( u ) 3 / 4 len(w) = \frac{count(w)^{3/4}}{\sum\limits_{u \in vocab} count(u)^{3/4}} len(w)=u∈vocab∑count(u)3/4count(w)3/4
在采样前,我们将这段长度为1的线段划分成 M M M等份,这里 M > > V M >> V M>>V,这样可以保证每个词对应的线段都会划分成对应的小块。而M份中的每一份都会落在某一个词对应的线段上。在采样的时候,我们只需要从 M M M个位置中采样出 n e g neg neg个位置就行,此时采样到的每一个位置对应到的线段所属的词就是我们的负例词
在word2vec中, M M M取值默认为 1 0 8 10^8 108。
fasttext是facebook开源的一个词向量与文本分类工具,在2016年开源,典型应用场景是“带监督的文本分类问题”。提供简单而高效的文本分类和表征学习的方法,性能比肩深度学习而且速度更快。
fastText的核心思想:将整篇文档的词及n-gram向量叠加平均得到文档向量,然后使用文档向量做softmax多分类。这中间涉及到两个技巧:字符级n-gram特征的引入以及分层Softmax分类。叠加词向量背后的思想就是传统的词袋法,即将文档看成一个由词构成的集合。
这些不同概念被用于两个不同任务:
• 有效文本分类 :有监督学习(短文本)
• 学习词向量表征:无监督学习
fastText方法包含三部分,模型架构,层次SoftMax和N-gram特征。用词向量的叠加代表文档向量,全连接之后softmax分类。
fastText的架构和word2vec中的CBOW的架构类似,因为它们的作者都是Facebook的科学家Tomas Mikolov,而且确实fastText(2016)也算是words2vec(2014)所衍生出来的。
Continuous Bog-Of-Words:
隐藏层就是叠加后的句子(文档)向量
参考《理解文本分类利器fastText》
层次softmax的基本思想是根据类别的频率构造霍夫曼树来代替扁平化的标准softmax。通过层次softmax,获得概率分布的时间复杂度可以从O(N)降至O(logN)。(多分类转成一系列二分类)
(见速通一书162页)
n-gram解决词袋模型没有词序的问题,Hash解决n-gram膨胀问题。最大问题是有Hash冲突,但是实际中问题不大。
fastText 本身是词袋模型,为了分类的准确性,所以加入了 N-gram 特征提取词序信息。“我 爱 她”如果加入 2-Ngram,第一句话的特征还有 “我-爱” 和 “爱-她”,这两句话 “我 爱 她” 和 “她 爱 我” 就能区别开来了。当然啦,为了提高效率,我们需要过滤掉低频的 N-gram。
n-gram的问题是词表会急剧扩大,变为 ∣ V ∣ n |V|^n ∣V∣n,没有机器扛得住。所以使用散列法(Hash)对n-gram特征进行压缩。
Hash:使用Hash函数将字符串映射到某个整数。这样不管n-gram词表有多大,最后整数范围都是函数输出范围(比如4000亿词表。hash函数是对10526取余,最后输出就10526个数值,数值再转成向量)
相似处:
1.图模型结构很像,都是采用embedding向量的形式,得到word的隐向量表达。
2.都采用很多相似的优化方法,比如使用Hierarchical softmax优化训练和预测中的打分速度。
不同处:
word2vec用词预测词,而且是词袋模型,没有n-gram。fasttext用文章/句子词向量预测类别,加入了n-gram信息。所以有:
总的来说,fastText的学习速度比较快,效果还不错。
fastText是一个快速文本分类算法,与基于神经网络的分类算法相比有两大优点:
fasttext已经嵌入word2vec,可以用它做有监督和无监督(就是word2vec)。涉及到离散特征都可以用fasttext。比如招聘网站预测求职者和职位的匹配度。(求职者和职位分别提取关键词特征,然后用fasttext训练,输出录用和不录用的概率。但是求职者简历写本科就是本科学位,职位要求的本科是指本科及以上。二者还是有些不一样。需要把求职者关键字/标签加P,职位标签加J予以区分。即当数据来源不同纬度时,语义可能不同,前面加一个field予以区分)
参考文档《word2vec原理和gensim实现》
用哪种方法看需求:
1.使用时需要将多个向量相加(文本向量化) 用cbow
2.使用时都是单个词向量使用(找近义词) 用skip-gram
大原则:使用的过程和训练的过程越一致 ,效果一般越好
如果实在不知道怎么选,一般来说skip-gram+ns负采样效果好一点点。
同一批词分别进行两次训练,embedding也不在同一语义空间,不同语义空间的向量没有可比性。word2vec不能进行增量更新,有新词只能全量训练,因为语料库变了one-hot也变了,V也变了。
孤岛效应:有一堆词,明明不相关,训练出来确是显示相似的。
直接pip安装报错:“Microsoft Visual C++ 14.0 or greater is required”。在此页面下载fasttext文件,然后安装:pip install C:\Users\LS\Downloads\fasttext-0.9.2-cp38-cp38-win_amd64.whl
FastText可以快速的在CPU上进行训练,最好的实践方法就是github教程,以及官网教程。
参考官方文档《Python模块》、《FastText代码详解》
FUNCTIONS
load_model(path):加载给定文件路径的模型并返回模型对象。
read_args(arg_list, arg_dict, arg_names, default_values)
tokenize(text):给定一串文本,对其进行标记并返回一个标记列表
train_supervised(*kargs, **kwargs):监督训练,样本包含标签,即fasttext。
train_unsupervised(*kargs, **kwargs):无监督训练,样本没有标签,即word2vec。
fasttext.train_unsupervised函数:调用此函数学习词向量,即word2vec模型。
input # training file path (required)
model # unsupervised fasttext model {cbow, skipgram} [skipgram]
lr # 学习率 [0.05]
dim # 词向量维度 [100]
ws # 上下文窗口大小 [5]
epoch # 训练轮数 [5]
minCount # 最少单词词频,过滤过少的单词 [5]
minn # min length of char ngram [3]
maxn # max length of char ngram [6]
neg # 负采样个数 [5]
wordNgrams # 词ngram最大长度 [1]
loss # loss function {ns, hs, softmax, ova}[ns]
#(负采样、霍夫曼树、softmax和多分类采用多个二分类计算,即loss one-vs-all)
bucket # number of buckets,放的是subwords [2000000]
thread # cpu线程 [number of cpus]
lrUpdateRate # change the rate of updates for the learning rate,实现阶梯动态学习率 [100]
t # sampling threshold,过滤高频词,越大被保留的概率越大 [0.0001]
verbose # verbose [2]
train_supervised 参数:
input # training file path (required)
lr # 学习率 [0.05]
dim # 词向量维度 [100]
ws # 上下文窗口大小 [5]
epoch # 训练轮数 [5]
minCount # 最小词频 [1]
minCountLabel # minimal number of label occurences [1]
minn # min length of char ngram [0]
maxn # max length of char ngram [0]
neg # 负采样个数 [5]
wordNgrams # n-gram [1]
loss # loss function {ns, hs, softmax, ova} [softmax]
bucket # number of buckets [2000000]
thread # cpu线程数 [number of cpus]
lrUpdateRate # change the rate of updates for the learning rate [100]
t # sampling threshold [0.0001]
label # 标签前缀 ['__label__']
verbose # verbose [2]
pretrainedVectors # 从 (.vec file)加载预训练的词向量,用于监督训练 []
model属性
get_dimension # 获取向量(隐藏层)的维度(大小).这等价于 `dim` 属性
get_input_vector # 给定一个索引,得到输入矩阵对应的向量
get_input_matrix # 获取模型的完整输入矩阵的副本
get_labels # 获取字典的整个标签列表,这相当于 `labels` 属性。
get_line # 将一行文本拆分为单词和标签
get_output_matrix # 获取模型的完整输出矩阵的副本。
get_sentence_vector # 给定一个字符串,获得向量表示。这个函数
# assumes to be given a single line of text. We split words on
# whitespace (space, newline, tab, vertical tab) and the control
# characters carriage return, formfeed and the null character.
get_subword_id # 给定一个subword,获取字典中的词 id hashes to.
get_subwords # 给定一个词,获取子词及其索引。
get_word_id # 给定一个词,获取字典中的词 id
get_word_vector # 获取训练好的词向量。
get_words # 获取字典的整个单词列表,这相当于 `words` 属性。
is_quantized # 模型是否已经量化过
predict # 给定一个字符串,得到一个标签列表和一个对应概率列表
quantize # 量化模型,减少模型的大小和内存占用
save_model # 保存模型
test # Evaluate supervised model using file given by path
test_label # 返回每个标签的准确率和召回率。
当 fastText 运行时,进度和预计完成时间会显示在您的屏幕上。训练完成后,model变量包含有关训练模型的信息,可用于查询:
import fasttext
model = fasttext.train_unsupervised('data/fil9')#维基百科文件
model.words
[u'the', u'of', u'one', u'zero', u'and', u'in', u'two', u'a', u'nine', u'to', u'is', ...
获得词向量:(它返回词汇表中的所有单词,按频率递减排序。)
model.get_word_vector("the")
array([-0.03087516, 0.09221972, 0.17660329, 0.17308897, 0.12863874,
0.13912526, -0.09851588, 0.00739991, 0.37038437, -0.00845221,
...
-0.21184735, -0.05048715, -0.34571868, 0.23765688, 0.23726143],
dtype=float32)
保存模型(二进制),后续加载
model.save_model("result/fil9.bin")
model = fasttext.load_model("result/fil9.bin")
cobw和skipgram:
import fasttext
model = fasttext.train_unsupervised('data/fil9', "cbow")
预测结果
#读取测试集,预测模型输出
test_df=pd.read_csv('./train_set.csv',sep='\t',nrows=10000)
results=[model.predict(x) for x in test_df['text']]
results
[(('__label__2',), array([0.99827653])),
(('__label__11',), array([0.84706676])),
(('__label__3',), array([0.99988556])),
(('__label__2',), array([0.99980879])),
...
(('__label__2',), array([0.9998678])),
(('__label__1',), array([0.87650901])),
(('__label__3',), array([1.00001013])),
...]
所以输出结果是带前缀的标签和分类概率。想只得到类别,可以这样写:
result=[model.predict(x)[0][0].split('__')[-1] for x in test_df['text']]
result
['2',
'11',
'3',
'2',
'3',
'9',
'3',
'10',
'12',
'3',
'0',
...]
参考《fasttext训练的bin格式词向量转换为vec格式词向量》
#加载的fasttext预训练词向量都是vec格式的,但fasttext无监督训练后却是bin格式,因此需要进行转换
# 以下代码为fasttext官方推荐:
# 请将以下代码保存在bin_to_vec.py文件中
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division, absolute_import, print_function
from fasttext import load_model
import argparse
import errno
if __name__ == "__main__":
# 整个代码逻辑非常简单
# 以bin格式的模型为输入参数
# 按照vec格式进行文本写入
# 可通过head -5 xxx.vec进行文件查看
parser = argparse.ArgumentParser(
description=("Print fasttext .vec file to stdout from .bin file")
)
parser.add_argument(
"model",
help="Model to use",
)
args = parser.parse_args()
f = load_model(args.model)
words = f.get_words()
print(str(len(words)) + " " + str(f.get_dimension()))
for w in words:
v = f.get_word_vector(w)
vstr = ""
for vi in v:
vstr += " " + str(vi)
try:
print(w + vstr)
except IOError as e:
if e.errno == errno.EPIPE:
pass
# 打开cmd,在bin_to_vec.py路径下执行该命令,生成unsupervised_data.vec
python bin_to_vec.py word15000.bin > word15000.vec
在实践中,我们观察到 skipgram 模型在处理子词信息方面比 cbow 更好
比赛官方链接为:《零基础入门NLP - 新闻文本分类》。讨论区有《数据读取与分析》
讨论区还有大佬张帆、惊鹊和张贤等人的代码,值得大家仔细阅读。
单纯的fasttext分类,参数用讨论区默认参数,没有调整。分数0.9151。
fasttext训练很快,大概十来分钟吧。
import pandas as pd
train_df=pd.read_csv('./train_set.csv',sep='\t')
train_df['label_ft']='__label__'+train_df['label'].astype(str)
train_df[['text','label_ft']].to_csv('./train.csv',index=None,header=None,sep='\t')
import fasttext
model=fasttext.train_supervised('./train.csv',lr=1.0,wordNgrams=2,
verbose=2,minCount=1,epoch=25,loss="hs")
test_df=pd.read_csv('./test_a.csv',sep='\t')
result=[model.predict(x)[0][0].split('__')[-1] for x in test_df['text']]
result[:100]
pd.DataFrame({'label':result}).to_csv('fasttext.csv',index=None)
最终上传,得分0.9151。
调整部分参数后,最终得分0.9358。
model=fasttext.train_supervised('./train.csv',lr=0.8,wordNgrams=3,
verbose=2,minCount=1,epoch=25,loss="softmax")
首先拿15000条数据进行试验,前10000条fasttext训练,后5000条测试,代码见讨论区:《Task4 基于深度学习的文本分类1-fastText》(其实就是上面代码改了点数据集):
import pandas as pd
from sklearn.metrics import f1_score
# 转换为FastText需要的格式
train_df = pd.read_csv('../data/train_set.csv', sep='\t', nrows=15000)
train_df['label_ft'] = '__label__' + train_df['label'].astype(str)
train_df[['text','label_ft']].iloc[:-5000].to_csv('train.csv', index=None, header=None, sep='\t')
import fasttext
model = fasttext.train_supervised('train.csv', lr=1.0, wordNgrams=2,
verbose=2, minCount=1, epoch=25, loss="hs")
val_pred = [model.predict(x)[0][0].split('__')[-1] for x in train_df.iloc[-5000:]['text']]
print(f1_score(train_df['label'].values[-5000:].astype(str), val_pred, average='macro'))
#先进行word2vec训练,含全部15000条数据
train_df[['text','label_ft']].to_csv('train15000.csv', index=None, header=None, sep='\t')
model1 = fasttext.train_unsupervised('train15000.csv', lr=0.1, wordNgrams=2,
verbose=2, minCount=1, epoch=8, loss="hs")
#保存模型转为词向量
model1.save_model("word15000.bin")
#cmd命令行执行python bin_to_vec.py result1000.bin < result1000.vec,转换为vec词向量
#fasttext进行训练,词向量为前一步训练好的词向量,训练数据为10000条
model2 = fasttext.train_supervised('train.csv',pretrainedVectors='word15000.vec',lr=1.0, wordNgrams=2,
# verbose=2, minCount=1, epoch=16, loss="hs")
#预测结果
val_pred = [model2.predict(x)[0][0].split('__')[-1] for x in train_df.iloc[-5000:]['text']]
print(f1_score(train_df['label'].values[-5000:].astype(str), val_pred, average='macro'))
#首尾截断实验效果
#准备将text文本首尾截断,各取100tokens
def slipt2(x):
ls=x.split(' ')
le=len(ls)
if le<201:
return x
else:
return ' '.join(ls[:100]+ls[-100:])
trains_df['summary']=trains_df['text'].apply(lambda x:slipt2(x))
train_df[['summary','label_ft']].iloc[:-5000].to_csv('trains_summary10000.csv', index=None, header=None, sep='\t')
model3 = fasttext.train_supervised('trains_summary10000.csv',pretrainedVectors='word15000.vec',lr=1.0, wordNgrams=2,
verbose=2, minCount=1, epoch=16, loss="hs")
#预测结果
val_pred = [model3.predict(x)[0][0].split('__')[-1] for x in train_df.iloc[-5000:]['text']]
print(f1_score(train_df['label'].values[-5000:].astype(str), val_pred, average='macro'))
#读取训练测试集数据
import pandas as pd
from sklearn.metrics import f1_score
# 转换为FastText需要的格式
train_df = pd.read_csv('./train_set.csv', sep='\t')
train_df['label_ft'] = '__label__' + train_df['label'].astype(str)
train_df[['text','label_ft']].to_csv('train_20w.csv', index=None, header=None, sep='\t')
test_df = pd.read_csv('./test_a.csv', sep='\t')
df=pd.concat([train_df,test_df])
df[['text']].to_csv('train_25w.csv', index=None, header=None, sep='\t')
import fasttext
model1 = fasttext.train_unsupervised('train_25w.csv', lr=0.1, wordNgrams=2,
verbose=2, minCount=1, epoch=8, loss="hs")
model1.save_model("word_25w.bin")
#cmd下运行python bin_to_vec.py word_25w.bin > word_25w.vec
model2=fasttext.train_supervised('train_20w.csv',pretrainedVectors='word_25w.vec',lr=0.8, wordNgrams=2, verbose=2, minCount=1, epoch=18, loss="hs")
import pandas as pd
test_df = pd.read_csv('./test_a.csv', sep='\t')
test_pred = [model2.predict(x)[0][0].split('__')[-1] for x in test_df['text']]
pd.DataFrame({'label':test_pred}).to_csv('word_fast.csv',index=None)
#首尾截断进行训练
train_df = pd.read_csv('./train_set.csv', sep='\t')
train_df['label_ft'] = '__label__' + train_df['label'].astype(str)
train_df['summary']=train_df['text'].apply(lambda x:slipt2(x))
train_df[['summary','label_ft']].to_csv('train_summary_20w.csv', index=None, header=None, sep='\t')
model3 = fasttext.train_supervised('train_summary_20w.csv',pretrainedVectors='word_25w.vec',lr=0.8, wordNgrams=2,
verbose=2, minCount=1, epoch=18, loss="hs")
#预测结果
test_df['summary']=test_df['text'].apply(lambda x:slipt2(x))
test_pred = [model3.predict(x)[0][0].split('__')[-1] for x in test_df['summary']]
pd.DataFrame({'label':test_pred}).to_csv('word_fast_cut.csv',index=None)
最终得分0.9203,至少证明了长文本分类,数据集够多的时候,进行部分截断比较好。
数据量 | fasttext | word2vec+fasttext | word2vec+fasttext+首尾截断 |
---|---|---|---|
10000+5000 | 0.8272 | 0.8426 | 0.8304 |
20w+5w | 0.9151(没调参) | 0.9162(没调参) | 0.9203(没调参) |
20w+5w | 0.9358(已调参) | 0.9421(已调参) | |
截断比不截断高0.4-0.6个点。 |
首尾截断 | f1 | loss | n-gram |
---|---|---|---|
各30,同时epoch=18,lr=0.8,下同 | 0.9190 | hs | 2 |
各30 | 0.9352 | softmax | 2 |
各30 | 0.9388 | softmax | 3 |
各30 | 0.9382 | softmax | 4 |
各30 | softmax | 5 | |
各30,同时epoch=18,lr=0.5 | softmax | 4 | |
各30,同时epoch=27,lr=0.5 | softmax | 4 | |
----- | ----- | ----- | ----- |
各50 | 0.9192 | hs | 2 |
各80 | 0.9170 | hs | 2 |
各100 | 0.9200/0.9184 | hs | 2 |
各150 | 0.9226 | hs | 2 |
各150 | 0.9371 | softmax | 2 |
各150 | 0.9436 | softmax | 3 |
各150 | 0.9417 | softmax | 4 |
各200 | 0.9212 | hs | 2 |
不截断 | 0.9158 | hs | 2 |
不截断,加和平均的词向量太多,无用信息冲淡了关键信息。 | |||
fasttext分类的loss必须选择softmex,不需要hs和ng,因为类别少。 | |||
n-gram中,n增大可以表示一部分词序,有利于文本表征。但是太大的话,词向量和n-gram向量太多,分类效果也不好(参数过多学不好或者是无用信息过多)。 |
初步选择以下参数:
#首尾截断各150个词
model3=fasttext.train_supervised('train_summary_20w.csv',pretrainedVectors='word_25w.vec',
lr=0.8,wordNgrams=3,verbose=2,minCount=1,epoch=18,loss="softmax")
最终分数f1=0.9421。