1 简介

Fasttext 源于 2016 年的论文《Bag of Tricks for Efficient Text Classification》,论文地址:https://arxiv.org/pdf/1607.01759.pdf。论文非常短,加上 References 不过五页,Model architecture 只有一页。

深度学习神经网络在自然语言处理中表现非常优秀,但动辄几十层,上亿参数的大型网络速度慢且需要大量算力支持,限制了使用场景。FastText 是 Facebook 开源的一款简单而高效的文本分类器,它使用浅层的神经网络实现了 word2vec 以及文本分类功能,效果与深层网络差不多,节约资源,且有百倍的速度提升,可谓高效的工业级解决方案。本篇将介绍 Fasttext 的相关概念、原理及用法。

2 相关技术

2.1 BOW

BOW BOW 是词袋 Bag of Words 的简称,BOW 是文本分类中常用的文本向量化的方法,它忽略了顺序和语法,将文本看成词汇的集合,且词汇间相互独立,如同把词放进了一个袋子。在分类任务中使用 BOW 时,就是根据各个词义综合分析文本的类型,常用于感情色彩分类等领域。

2.2 CBOW

CBOW 是连续词袋模型 Continuous Bag-of-Word Model 的简称,它常用于上下文词来预测中间词。

如图所示,使用前两个和后两个词(共 C=4 个)预测中间的词 w,其中每个词被映射成 V 维的词向量;每个词向量乘以参数矩阵 A(VxN 维矩阵),转换成 N 维数据,然后将所有词对应的 N 维的数据相加取均值,计算出 N 维的隐藏层 Hidden;再用隐藏层乘参数矩阵 B(NxV 维),计算待预测的词 w 对应的 V 维词向量;最终用预测出的 w 与真实的 w 作比较计算误差函数,然后用梯度下降调整 A,B 两个参数矩阵。由此,使用简单的神经网络就完成了预测任务。

2.3 N-gram

N-gram 是由 N 个 token(词)组成的有序集合,常用的有 Bi-gram (N=2) 和 Tri-gram (N=3),实际使用中 2 或 3-gram 就够用了。Fasttext 论文中使用 Bi-gram 将文本拆成词对。

如 I love deep learning 可拆成:

Bi-gram: {I, love}, {love, deep}, {deep, learning}

Tri-gram: {I, love, deep}, {love, deep, learning}

这样使一个词它之前的词建立联系。

以 Bi-gram 为例,Wn-1 出现时,Wn 出现的概率是:

其中 C() 表示计数,N-gram 技术在自然语言处理中广泛使用。

优势

  • 与词序无关
  • 允许模糊匹配,即使不完全匹配也能识别

Python 示例

(针对英文)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from collections import defaultdict

LANG = 'ZH'

def generate_ngrams(text, n):
'''
用于生成文本的N-gram
'''
if LANG == 'ZH':
words = [w for w in text]
else:
words = text.split()
ngrams = []
for i in range(len(words) - n + 1):
ngram = ' '.join(words[i:i + n])
ngrams.append(ngram)
return ngrams

def fuzzy_ngram_search(query, text, n, threshold):
'''
函数用于执行模糊查询
'''
query_ngrams = generate_ngrams(query, n)
text_ngrams = generate_ngrams(text, n)
match_counts = defaultdict(int)

for query_ngram in query_ngrams:
for text_ngram in text_ngrams:
# 计算Jaccard相似性系数,计算交集与并集之比,即使不完全匹配也能成功
intersection = set(query_ngram.split()) & set(text_ngram.split())
union = set(query_ngram.split()) | set(text_ngram.split())
jaccard_similarity = len(intersection) / len(union)

if jaccard_similarity >= threshold:
match_counts[text_ngram] += 1

return match_counts

# 示例用法
if LANG == 'ZH':
text = "投资者和xx公司均享有回购权,如发生下列任一情形(以较早者为准)"
query = "回购"
else:
text = "This is a sample text for demonstration purposes."
query = "sample text"
n = 2 # 使用2-gram
threshold = 0.5 # 阈值,用于确定相似性

matches = fuzzy_ngram_search(query, text, n, threshold)

if matches:
sorted_matches = sorted(matches.items(), key=lambda x: x[1], reverse=True)
print("相似的N-gram及其匹配次数:")
for match, count in sorted_matches:
print(f"{match}: {count} 次")
else:
print("未找到匹配的N-gram。")

3 FastText

3.1 原理

在文本分类问题中,早期的算法一般将词袋 BOW 作为输入,使用线性模型作为算法计算类别,这种方法在类别不均衡时效果不好,后来用将线性分类器分解为低秩矩阵或者多层网络的方法解决这一问题。

FastText 与 CBOW 结构类似,如下图所示:

图片摘自论文

其中输入是文档中的词,使用词嵌入方法,和 CBOW 一样,通过乘 A 矩阵转换到 Hidden,再乘 B 矩阵转换到输出层,与 CBOW 不同的是它的输出不是空缺的单词,而是分类的类别。

FastText 用负对数似然作为损失函数:

其中 N 是文档数,xn 是文档中的词特征。yn 是标签,A 和 B 是权重矩阵,A 用于转换到文本表示,B 用于线性变换计算类别,f 是一个 softmax 函数用于计算最终分类的概率。

当分类的类别较多时,计算的时间复杂度是 O(hk),其中 k 是类别的个数,h 是文本表示的维度。Fasttext 使用了基于霍夫曼编码树的分级 softmax,使训练的时间复杂度降为 O(hlog2(k))。每一个节点的概率大小与从树根到该节点经过的路径有关,例如某节点深度为 l+1,它的父节点分别是 n1…n2,则它的概率是:

这就意味着,节点的概率小于它父节点的概率,因此访问树时,就可以忽略概率小的分枝。如果只需预测取前 TopN,则复杂度可降至 O(log(TopN))。

下面列出了 FastText 与深度学习模型的速度比较,可以看到,其提速非常明显。

个人感觉,层次少的好处不只在于运算速度快,而且更容易归因,定位重要特征,以及估计对应的权重,不像深度网络的数据都散布在各层的节点之中。

3.2 FastText 用法

3.2.1 安装

1
$ pip install fasttext

3.2.2 训练和预测

数据格式为:label 标签, 已经分词的文档(注意:label 两边都需要又下划线)

如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import fasttext
def train(): # 训练模型
model = fasttext.train_supervised("train.txt", lr=0.1, dim=100,
epoch=5, word_ngrams=2, loss='softmax')
model.save_model("model_file.bin")

def test(): # 预测
classifier = fasttext.load_model("model_file.bin")
result = classifier.test("test.txt")
print("准确率:", result)
with open('test.txt', encoding='utf-8') as fp:
for line in fp.readlines():
line = line.strip()
if line == '':
continue
print(line, classifier.predict([line])[0][0][0])

if __name__ == '__main__':
train()
test()

4 资料

建议下载源码:

https://github.com/facebookresearch/fastText#

更详细的用法示例请见:

https://github.com/facebookresearch/fastText#example-use-cases