本文主要介绍如何使用利用fastText
进行文本分类任务,包括如何准备、处理数据,训练及测试过程。
最近用到fastText
进行文本分类任务,其不用训练好的词向量,训练简单又快速,尝试了一下,效果还不错。本文旨在记录测试的过程。
本文不涉及算法原理部分,具体的原理可参考下面这篇博客:
原理参考:
fastText原理和文本分类实战,看这一篇就够了
fasttext
是facebook
开源的一个词向量与文本分类工具,于2016年开源,典型应用场景是 带监督的文本分类问题 。提供简单而高效的文本分类和表征学习的方法,性能比肩深度学习而且速度更快。
其github
地址:FastText之github
fastText
方法包含三部分,模型架构,层次Softmax和N-gram特征。
安装需求如下:
fastText
建立在Mac OS和Linux发行版上。由于它使用了一些C++ 11
特性,因此需要具有良好C++ 11支持的编译器。这些包括 :
使用Makefile
进行编译,因此您需要有一个有效的make
。如果你想使用cmake
,你至少需要2.8.9版本。
python bindings
来讲,需要满足如下条件:
编译
从
github
上获取源码,进行编译,包含使用make
编译,使用cmake
编译,使用Python
编译。
make
编译(首选)$ wget https://github.com/facebookresearch/fastText/archive/v0.9.1.zip
$ unzip v0.9.1.zip
$ cd fastText-0.9.1
$ make
目前,这不是release版本的一部分,因此您需要克隆主分支。
$ git clone https://github.com/facebookresearch/fastText.git
$ cd fastText
$ mkdir build && cd build && cmake ..
$ make && make install
这将创建fasttext二进制文件以及所有相关库(shared, static, PIC)。python
编译
目前,这不是release版本的一部分,因此您需要克隆主分支。
$ git clone https://github.com/facebookresearch/fastText.git
$ cd fastText
$ pip install .
如果想要安装最新的release
版本,可以使用pip
命令直接安装:$ pip install fasttext
下载的源代码中给出了一系列的说明文档,位置在fastText/docs
为了学习词向量,我们可以使用fasttext.train_unsupervised
函数,像下面这样:
import fasttext
# Skipgram model :
model = fasttext.train_unsupervised('data.txt', model='skipgram')
# or, cbow model :
model = fasttext.train_unsupervised('data.txt', model='cbow')
其中,data.txt
是使用utf-8
编码的用于训练的文本文件。
model.save_model("model_filename.bin")
model = fasttext.load_model('model_filename.bin')
import fasttext
model = fasttext.train_supervised('data.train.txt')
其中,data.train.txt
是一个文本文件,每行包含一个训练语句以及标签。默认情况下,我们假设标签是以字符串__label__
为前缀的词。
模型训练完成后,我们可以检索单词和标签列表:
print(model.words)
print(model.labels)
验证模型
通过在测试集上计算在P@1的准确度和召回率,(P@1 表示top1精确率,R@1表示top1召回率),使用如下测试函数
def print_results(N, p, r):
print("N\t" + str(N))
print("P@{}\t{:.3f}".format(1, p))
print("R@{}\t{:.3f}".format(1, r))
print_results(*model.test('test.txt'))
可以使用preidict
函数来预测指定的文本:
model.predict("Which baking dish is best to bake a banana bread ?", k=3)
其中,k=3
用来指定获取前3个概率最高的结果,默认k=1
。
如果想要预测多个句子,可以传入一个字符串数组,如下:
model.predict(["Which baking dish is best to bake a banana bread ?", "Why not put knives in the dishwasher?"], k=3)
当您想要保存监督模型文件时,fastText可以通过牺牲一点点性能来压缩它以获得更小的模型文件。
# with the previously trained `model` object, call :
model.quantize(input='data.train.txt', retrain=True)
# then display results and save the new model :
print_results(*model.test(valid_data))
model.save_model("model_filename.ftz")
model_filename.ftz
的大小比model_filename.bin
小得多。
如何使用Python
代码进行训练和测试呢,源代码中给出了demo
。
位置在fastText/python/doc/examples/train_supervised.py
,具体如下
import os
from fasttext import train_supervised
# 打印结果
def print_results(N, p, r):
print("N\t" + str(N))
print("P@{}\t{:.3f}".format(1, p))
print("R@{}\t{:.3f}".format(1, r))
if __name__ == "__main__":
# 数据
train_data = os.path.join(os.getenv("DATADIR", ''), 'cooking.train')
valid_data = os.path.join(os.getenv("DATADIR", ''), 'cooking.valid')
# train_supervised uses the same arguments and defaults as the fastText cli
model = train_supervised(
input=train_data, epoch=25, lr=1.0, wordNgrams=2, verbose=2, minCount=1
)
print_results(*model.test(valid_data))
model = train_supervised(
input=train_data, epoch=25, lr=1.0, wordNgrams=2, verbose=2, minCount=1,
loss="hs"
)
print_results(*model.test(valid_data))
# 保存模型
model.save_model("cooking.bin")
# 压缩模型
model.quantize(input=train_data, qnorm=True, retrain=True, cutoff=100000)
print_results(*model.test(valid_data))
model.save_model("cooking.ftz") # 保存压缩后的模型
从上图可见,训练模型的基本流程如下:
train_supervised
函数进行训练fastText
模型输入一个词的序列(一段文本或者一句话),输出这个词序列属于不同类别的概率。
输入格式为__label__class word1 word2 word3 ...
, class
表示类别标签,其前缀是__label__
,注意是前后各两个下划线。
jieba
进行中文分词对于英文来讲,空格自然断词,而对于中文,需要进行分词处理。
在这里,我使用jieba
进行中文分词,类别标签使用数字表示。将处理后的文本写入到文件中(如写入到txt文件),使用utf-8
编码格式。
处理后的数据: 每行代表一个文本,以\n
结尾,文本以空格分隔单词,如下所示,文本今天天气真的太好了
处理后为:
__label__1 今天 天气 真的 太好 了
一条文本可以有多个标签,以空格隔开即可。
def prepro_text(datas, stopwords):
sentences = []
label = 1
for category in datas:
for line in category:
try:
segs = jieba.lcut(line)
# segs = filter(lambda x:len(x)>1, segs) # 去掉长度小于等于1的词
segs = filter(lambda x:x not in stopwords, segs) #去掉停用词
sentences.append('__label__'+str(label) + " "+" ".join(segs)) #将类别和文本拼接起来
except Exception as e:
print('text: %s is error' %(line))
continue
label += 1
return sentences
# 将处理过的数据写入文件,编码格式是utf-8
def write_data(datas, file_name):
print('writing data to fastText format...')
with io.open(file_name, 'w', encoding='utf-8') as f:
for senten in datas:
# print(senten)
f.write(senten+'\n')
print('wirte done!')
训练代码如下:
model = train_supervised(input=save_data_file, epoch=10, lr=0.1, wordNgrams=2, minCount=1, loss="softmax")
函数定义在fasttext/python/fasttext_module/fasttext/FastText.py
def train_supervised(*kargs, **kwargs):
"""
Train a supervised model and return a model object.
input must be a filepath. The input text does not need to be tokenized
as per the tokenize function, but it must be preprocessed and encoded
as UTF-8. You might want to consult standard preprocessing scripts such
as tokenizer.perl mentioned here: http://www.statmt.org/wmt07/baseline.html
The input file must must contain at least one label per line. For an
example consult the example datasets which are part of the fastText
repository such as the dataset pulled by classification-example.sh.
"""
supervised_default = unsupervised_default.copy()
supervised_default.update({
'lr' : 0.1,
'minCount' : 1,
'minn' : 0,
'maxn' : 0,
'loss' : "softmax",
'model' : "supervised"
})
# 训练参数
arg_names = ['input', 'lr', 'dim', 'ws', 'epoch', 'minCount',
'minCountLabel', 'minn', 'maxn', 'neg', 'wordNgrams', 'loss', 'bucket',
'thread', 'lrUpdateRate', 't', 'label', 'verbose', 'pretrainedVectors']
params = read_args(kargs, kwargs, arg_names, supervised_default)
a = _build_args(params)
ft = _FastText(args=a)
fasttext.train(ft.f, a)
return ft
训练参数
input # 训练文件路径 (required)
lr # 学习率 [0.1]
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 # number of negatives sampled [5]
wordNgrams # max length of word ngram [1]
loss # 损失函数 {ns, hs, softmax, ova} [softmax]
bucket # number of buckets [2000000]
thread # 线程数 [number of cpus]
lrUpdateRate # 学习率更新速率 [100]
t # sampling threshold [0.0001]
label # 标签前缀 ['__label__']
verbose # verbose [2]
pretrainedVectors # pretrained word vectors (.vec file) for supervised learning []
训练完分类模型,就可以进行测试了。当然,为了以后使用方便,可以先保存一下,使用save_model()
函数。
# 保存模型
model = load_model("model_lr%.2f_epoch%d.ftz"%(lr, epoch))
测试中文文本
测试文本类别,需要将测试的文本进行中文分词,然后使用空格连接起来。
segs = jieba.lcut(test_text)
segs = filter(lambda x:x not in stop_words, segs)
test_text = " ".join(segs)
# 测试
lables, proba = model.predict(test_text)
print('%s, %.2f'%(lable_to_cate[int(lables[0][9:])], proba[0]))
调用model.predict()
函数,则返回类别类型以及概率值。
注意,此时返回的结果格式如下:
labels: ('__label__23',) # 类别
proba: [0.98677748] # 概率值
获取真实标签直接使用切片操作即可:labels[0][9:]
train_supervised,train_unsupervised和load_model函数
返回_FastText
类的实例,我们通常将其命名为模型对象。
该对象将这些训练参数公开为属性:lr, dim, ws, epoch, minCount, minCountLabel, minn, maxn, neg, wordNgrams, loss, bucket, thread, lrUpdateRate, t, label, verbose, pretrainedVectors
.
因此,model.wordNgrams
将为您提供用于训练此模型的word gram的最大长度。
此外,该对象还公开了几个函数:
get_dimension # Get the dimension (size) of a lookup vector (hidden layer).
# 等同于 `dim` 属性.
get_input_vector # Given an index, get the corresponding vector of the Input Matrix.
get_input_matrix # Get a copy of the full input matrix of a Model.
get_labels # 获取整个词典的标签列表
# 等同于 `labels` 属性.
get_line # 将一行文本分为 words 和 labels.
get_output_matrix # Get a copy of the full output matrix of a Model.
get_sentence_vector # 给定一个字符串, 获取单个向量表示. 这个函数假设给定一个单独的文本行,我们
# 通过空白(空格,newline,tab,vertical tab)来分隔单词,来控制
# characters carriage return, formfeed and the null character.
get_subword_id # Given a subword, return the index (within input matrix) it hashes to.
get_subwords # Given a word, get the subwords and their indicies.
get_word_id # Given a word, get the word id within the dictionary.
get_word_vector # Get the vector representation of word.
get_words # 获取整个词典的单词列表
# 等同于 `words` 属性.
is_quantized # 模型是否被量化
predict # Given a string, get a list of labels and a list of corresponding probabilities.
quantize # Quantize the model reducing the size of the model and it's memory footprint.
save_model # Save the model to the given path
test # Evaluate supervised model using file given by path
test_label # Return the precision and recall score for each label.
单词,标签属性返回字典中的单词和标签:
model.words # equivalent to model.get_words()
model.labels # equivalent to model.get_labels()
该对象会覆盖__getitem__
和__contains__
函数,以便返回单词的表示形式并检查单词是否在词汇表中。
model['king'] # equivalent to model.get_word_vector('king')
'king' in model # equivalent to `'king' in model.get_words()`
通过使用默认参数运行fastText
获得的模型在分类问题时效果并不太好,我们可以尝试更改默认参数来提高性能。
-epoch
来增加迭代次数;0.1-1.0
的范围内。总结一下:
-epoch
,标准范围 [5 - 50]) ;-lr
,标准范围 [0.1 - 1.0]);什么是Bigram
?
unigram
指的是单个不可分割的单元或标记,通常用做模型的输入。例如,一个unigram
可以是一个单词或字母。在fastText
中,我们作用在单词级别,因此unigrams
指的是单词。
类似的,我们用bigram
表示连续两个单词或tokens的连接,类似的,我们经常讨论n-gram
指的是n个
连续的tokens
的连接.
例如,在句子Last donut of the night,unigrams
是last, donut, of, the 以及night。bigram是
Last donut,donut of, of the 和 the night。
减小规模
对于较小的训练数据(如几千个示例)训练模型,只需要几秒钟。但是,对于较大数据集的训练模型,标签越多,开始就越慢。使训练更快的潜在解决方法是使用hierarchical softmax
,来代替常规的softmax
。
可以使用选项 -loss hs
来指定。
什么是hierarchical softmax
?
hierarchical softmax是一个损失函数,要比softmax计算的更快。
想法是通过构建一个二叉树,其叶子对应于标签,每一个黄总监节点有一个二元决策激活(例如,sigmoid),预测是应该向左还是向右。输出单元的概率由沿着从根到输出单元的路径的中间节点的概率的乘积给出。
在fastText
中,我们使用Huffman tree
,对于更频繁的输出,查找时间更快,因此输出的平均查找时间是最佳的。
多标签分类
当我们想要将文档分配给多个标签时,仍然可以使用softmax作为损失函数来预测。即要预测的标签数量和预测概率的阈值。但是使用这些参数可能很棘手且不直观,因为概率和必须为1。
处理多个标签的便捷方法是为每个标签使用独立的二元分类器。可以通过-loss one-vs-all
或者-loss ova
实现。