自然语言处理,我们终将还是要面对它。

这周读了 Facebook 人工智能研究院的一篇文章 Bag of Tricks for Efficient Text Classification ( arXiv:1607.01759 ),第一感觉和今年五月份读的那篇 Efficient Estimation of Word Representations in Vector Space ( arXiv:1301.3781 ) 真的很像很像,不过后来就发现了这两文章思路一致是非常合理的,这个后面说。

文章概述

这篇文章提出了一个非常简单,效果也不错的文本分类模型:fastText. 它和word2vec中的CBOW模型十分相似。CBOW模型是由前后文的几个单词的词通过一个 Projection Matrix 映射到同一个位置,同时这些词向量取到了均值,来获得中间单词的预测。而 fasttext 将一整个需要分类的句子整理成 N 个 ngram 特征,将这 N 个 ngram 特征取均值通过矩阵映射获得类型的预测。

fasttext-fastText-1-20181130

这个方法简直是简单粗暴,但是效果却出人意料地好。作者为了验证模型的效果做了两个不同的实验,一个是情感分析,另一个是文本标签预测。在做情感分析的时候,作者使用了 bigram,即每两个单词组合生成一个向量,结果就是准确率很高。

做标签预测更复杂一些,首先要解决的问题是标签数量很大的时候,只使用 softmax 函数会导致开销巨大。这里就用了一个 Hierarchical softmax 函数来替代 softmax,结合霍夫曼编码,把复杂度降到 log 级别。其次是由于使用的是 bigram,两两组合产生的参数数目很多。作者使用了 Feature Hashing for Large Scale Multitask Learning ( arXiv:0902.2206 ) 中的 hashing trick 来解决这个问题。这篇论文现在还没看,但猜测是使用链地址法来解决碰撞的哈希表中将几个类似的 bigram 映射到同一个向量,来减少开销。

最后,作者将 fastText 整个打包成一套工具,在 GitHub 上开源,这也为下面做一些实践提供了巨大便利。

文本分类

按 GitHub 上的指南安装好 fastText 以后,就可以运行它,看一下有哪些功能。

➜  fastText-0.1.0 ./fasttext
usage: fasttext <command> <args>

The commands supported by fasttext are:

  supervised              train a supervised classifier
  quantize                quantize a model to reduce the memory usage
  test                    evaluate a supervised classifier
  predict                 predict most likely labels
  predict-prob            predict most likely labels with probabilities
  skipgram                train a skipgram model
  cbow                    train a cbow model
  print-word-vectors      print word vectors given a trained model
  print-sentence-vectors  print sentence vectors given a trained model
  nn                      query for nearest neighbors
  analogies               query for analogies

获取数据。这里作者给的是一个和 cooking 有关系的文本数据集。

➜  fastText-0.1.0 wget https://s3-us-west-1.amazonaws.com/fasttext-vectors/cooking.stackexchange.tar.gz && tar xvzf cooking.stackexchange.tar.gz

完成以后我们就有了以下的文件:

2018-11-30 20:56:33 (172 KB/s) - ‘cooking.stackexchange.tar.gz.1’ saved [457609/457609]

cooking.stackexchange.id
cooking.stackexchange.txt
readme.txt

在训练之前,找惯例我们将数据集做一个划分,分为训练集和测试集。先看看总共有多少条数据。

➜  fastText-0.1.0 wc cooking.stackexchange.txt
  15404  169582 1401900 cooking.stackexchange.txt

可见一共15404条数据。这里把钱12404条作为训练集,剩下3000条作为测试集。

➜  fastText-0.1.0 head -n 12404 cooking.stackexchange.txt > cooking.train
➜  fastText-0.1.0 tail -n 3000 cooking.stackexchange.txt > cooking.valid

那就可以开始训练一个文本分类器了。将 cooking.train 作为训练输入,输出的模型是一个二进制文件。

➜  fastText-0.1.0 ./fasttext supervised -input cooking.train -output model_cooking
Read 0M words
Number of words:  14543
Number of labels: 735
Progress: 100.0%  words/sec/thread: 75880  lr: 0.000000  loss: 17.301687  eta: 0h0m 14m

训练速度确实很快..一秒多就完成了。下面立马就可以试试它的预测效果。

➜  fastText-0.1.0 ./fasttext predict model_cooking.bin -
Oops! Fire!
__label__food-safety

在我输入 Oops! Fire! 以后,它给出的预测结果是 food-safety. 这种简单的句子是很容易预测的,实际上一些复杂的句子预测效果就很差了(也可能是我的英语太蹩脚)。

下面看看测试集在这个模型上面的准确率。

➜  fastText-0.1.0 ./fasttext test model_cooking.bin cooking.valid
N       3000
P@1     0.145
R@1     0.0626
Number of examples: 3000

这里的 P 是 Precision,即「查准率」,同理 R 是「查全率(Recall)」。可见精度确实不高,那就需要优化。

第一个可以优化的地方是数据集中的词语没有经过预处理,就导致一些单词由于大小写被认为是两个词,比如「Now」和「now」。这里就先把所有的词不管大写小写都转成小写。

➜  fastText-0.1.0 cat cooking.stackexchange.txt | sed -e "s/\([.\!?,'/()]\)/ \1 /g" | tr "[:upper:]" "[:lower:]" > cooking.preprocessed.txt
➜  fastText-0.1.0 head -n 12404 cooking.preprocessed.txt > cooking.train
➜  fastText-0.1.0 tail -n 3000 cooking.preprocessed.txt > cooking.valid

(正则表达式确实是一个必须掌握的东西)

然后再在这样的训练集上面跑一下模型训练。

➜  fastText-0.1.0 ./fasttext supervised -input cooking.train -output model_cooking
Read 0M words
Number of words:  8952
Number of labels: 735
Progress: 100.0%  words/sec/thread: 75000  lr: 0.000000  loss: 12.685683  eta: 0h0m 14m

和第一次训练,词的数目确实变少了。那再来看看在测试集上面跑的效果。

➜  fastText-0.1.0 ./fasttext test model_cooking.bin cooking.valid
N       3000
P@1     0.168
R@1     0.0728
Number of examples: 3000

有提高,但是还不够。下面就是增加训练次数,和调一下 learning rate.

先看看只增加训练次数。

➜  fastText-0.1.0 ./fasttext supervised -input cooking.train -output model_cooking -epoch 25
Read 0M words
Number of words:  8952
Number of labels: 735
Progress: 100.0%  words/sec/thread: 77181  lr: 0.000000  loss: 8.582131  eta: 0h0m

➜  fastText-0.1.0 ./fasttext test model_cooking.bin cooking.valid
N       3000
P@1     0.521
R@1     0.225
Number of examples: 3000

提升显著。

然后只提高 learning rate 呢?

➜  fastText-0.1.0 ./fasttext supervised -input cooking.train -output model_cooking -lr 1.0
Read 0M words
Number of words:  8952
Number of labels: 735
Progress: 100.0%  words/sec/thread: 82124  lr: 0.000000  loss: 13.937234  eta: 0h0m 14m

➜  fastText-0.1.0 ./fasttext test model_cooking.bin cooking.valid
N       3000
P@1     0.573
R@1     0.248
Number of examples: 3000

可见在 learning rate 为 1.0 的时候,也能大幅度提升模型的表现。那还等什么,都加上试试。

➜  fastText-0.1.0 ./fasttext supervised -input cooking.train -output model_cooking -lr 1.0 -epoch 25
Read 0M words
Number of words:  8952
Number of labels: 735
Progress: 100.0%  words/sec/thread: 78206  lr: 0.000000  loss: 5.915526  eta: 0h0m h-14m

➜  fastText-0.1.0 ./fasttext test model_cooking.bin cooking.valid
N       3000
P@1     0.584
R@1     0.253
Number of examples: 3000

...也是有效果的。

别忘了还有一个东西没用,就是 ngram. 在我们处理文本的时候,将连续的 n 个词当作一个词来处理,这样有了上下文的连续性我们的效果能得到很大的提升。比如下面这个句子:

今天是星期五

使用 n 取 2 的 bigram 来表示就拆解成了如下的词语:

今天 天是 是星 星期 期五

这样做以后我们在处理句子的时候也无需知道每个词之间的顺序,所以是「词袋」嘛。

那这里就用一下 bigram:

➜  fastText-0.1.0 ./fasttext supervised -input cooking.train -output model_cooking -lr 1.0 -epoch 25 -wordNgrams 2
Read 0M words
Number of words:  8952
Number of labels: 735
Progress: 100.0%  words/sec/thread: 77692  lr: 0.000000  loss: 4.762022  eta: 0h0m

➜  fastText-0.1.0 ./fasttext test model_cooking.bin cooking.valid
N       3000
P@1     0.607
R@1     0.263
Number of examples: 3000

非常好。

最后还要考虑样本量很大的情况,用的就是前面说的「哈希桶」的方式。在这里用哈希桶的方式有点浪费,但我们也可以试一下。

➜  fastText-0.1.0 ./fasttext supervised -input cooking.train -output model_cooking -lr 1.0 -epoch 25 -wordNgrams 2 -bucket 200000 -dim 50 -loss hs
Read 0M words
Number of words:  8952
Number of labels: 735
Progress: 100.0%  words/sec/thread: 2573118  lr: 0.000000  loss: 2.425255  eta: 0h0m

➜  fastText-0.1.0 ./fasttext test model_cooking.bin cooking.validN       3000
P@1     0.591
R@1     0.256
Number of examples: 3000

因为哈希桶是用来处理千万级别的数据的,我们这里总共才几千条数据,用了哈希桶的结果就是模型训练甚至不需要一秒钟。

词向量表示

fastText 工具还提供词向量表示,这就和 word2vec 几乎一致了。那我们还是来用用看。

首先是获取数据集。作者给了一个 英文维基 的数据集,可以说非常巨大..一共 14.5GB. 在艰难的下载以后我终于让这个数据集躺在我的电脑里了。但是我们只是用一下 fastText 的词向量功能,可以先弄一个小一点的数据集试试。

➜  fastText-0.1.0 mkdir data
➜  fastText-0.1.0 wget -c http://mattmahoney.net/dc/enwik9.zip -P data
➜  fastText-0.1.0 unzip data/enwik9.zip -d data

这里下载来的是HTML/XML标记语言文件,我们要让它变成我们需要的东西还需要一些处理。好在作者已经在工具包里放了一个脚本用来处理它。

➜  fastText-0.1.0 perl wikifil.pl data/enwik9 > data/fil9

可以用 head 命令来看下文件里的情况,确定是我们想要的以后,就可以开始模型的训练了。

➜  fastText-0.1.0 mkdir result
➜  fastText-0.1.0 ./fasttext skipgram -input data/fil9 -output result/fil9
Read 124M words
Number of words:  218316
Number of labels: 0
Progress: 100.0%  words/sec/thread: 100702  lr: 0.000000  loss: 1.742439  eta: 0h0m

训练时间挺久的,自己电脑跑的话十分钟左右吧。
然后再 result 文件夹里就可以看到 fil9.binfil9.vec 两个文件了。后者就是用向量描述的词语,可以看下里面的内容:

➜  fastText-0.1.0 head -n 4 result/fil9.vec

输出结果很长,每个单词使用 100 维的向量表示,当然这里的维度也是可以调的。

前面词向量是使用 skipgram 模型生成的,也可以使用 CBOW 模型来生成词向量。

➜  fastText-0.1.0 ./fasttext cbow -input data/fil9 -output result/fil9

Skip-gram 和 CBOW 两个模型的细节还是看 word2vec 的那篇文章。

fasttext-fastText-2-20181130

训练模型的时候也可以调整一下比如训练次数,learning rate,线程数这样的参数。

➜  fastText-0.1.0 ./fasttext skipgram -input data/fil9 -output result/fil9 -epoch 1 -lr 0.5 -thread 4

这里就是使用 Skip-gram 模型,训练次数为 1,learning rate为 0.5,4 线程训练。

下面可以输入特定词来看看它的词向量的值。

echo "asparagus pidgey yellow" | ./fasttext print-word-vectors result/fil9.bin

输出的是这三个单词,每个 100 维的向量。同时作者也指出,对于训练集里没有出现过的单词,同样可以给出词向量。

➜  fastText-0.1.0 echo "enviroment" | ./fasttext print-word-vectors result/fil9.bin

下面做一些对词向量的测试。首先是寻找一个词的近义词。由于每个单词都被向量表示,那么寻找一个向量最近的向量也不过是中学数学问题。

➜  fastText-0.1.0 ./fasttext nn result/fil9.bin
Pre-computing word vectors... done.
Query word? sony
nintendo 0.811914
panasonic 0.801393
famicom 0.791413
walkman 0.788833
videocassette 0.787339
samcor 0.783624
jvc 0.781695
betamax 0.778924
playstation 0.778051
playstationjapan 0.775387

在我输入 「sony」 以后,fastText 给出了一些近义词,比如「任天堂」、「松下」等,准确度还是可以的。

也可以通过词运算来感受下它的准确度,词运算就是假定两组拥有相同关系的词组可以通过其中三个词的运算来获得第四个词,比如「微软」-「盖茨」==「苹果」-「乔布斯」。 但是实际上这里词运算效果不是很好,要考虑到同义词的影响。后来查了一下,斯坦福有篇文章 GloVe Global Vectors for Word Representation 解决了这个问题。在这里为了表示效果,还是看一下作者放的样例。

➜  fastText-0.1.0 ./fasttext analogies result/fil9.bin
Pre-computing word vectors... done.
Query triplet (A - B + C)? berlin germany france
paris 0.893872
marseille 0.761099
dubourg 0.754856
louveciennes 0.749326
valenciennes 0.748037
pompignan 0.741549
strasbourg 0.735633
bordeaux 0.734896
maubourg 0.734029
pigneaux 0.731205

猜测是因为数据集不够大的缘故导致的效果不好,如果拿那个 14.5GB 的维基英文语料库训练可能会有提升。

后面作者通过比较来提了一下 ngrams 的加入能让模型效果得到显著提升,在此不表。

写在后面

我也是很惊奇这么简单的模型在实验上能达到这么好的效果。后来发现 fastText 所用的模型和 word2vec 如此相似是因为... word2vec 的作者 Tomas Mikolov 同时也是 fastText 的四作。这位在提出 word2vec 的时候还在谷歌,然后就被 Facebook 挖走了。

在 Facebook Research 上有他的 个人介绍页面 ,这么强就算了,长得还那么帅。

扑通。

支付宝扫码打赏 微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章

Yzstr Andy's Picture
Yzstr Andy

School of Data and Computer Science, SUN YAT-SEN UNIVERSITY