0%

NLP|word2vec/GloVe/fastText模型原理详解与实战

词向量是NLP中最为重要的概念。词向量的好坏直接影响到下游任务的效果。然而,即便在ELMo、BERT、ALBERT等预训练模型大行其道的当今,word2vec、GloVe、fastText仍然是目前最为流行的词向量训练方式。因此,本篇博客将具体讲解这三种词向量训练的原理,并使用gensim来展示如何使用这些词向量模型,以便得到我们想要的词向量。

注意:本篇博客尽可能讲解清楚三种词向量训练模型的原理,但是很多细节的部分,需要大家自行去了解,比如huffman树的原理、softmax的原理等等。如若不尽意,可参考我在参考文献中例举的原始论文~

word2vec模型介绍

word2vec模型其中有两种训练方式:skip-gram与CBOW,此外,还有两种加速训练的trick:hierarchical sofmtmax与negative sampling。所以,word2vec其实有4种方法。细节见细节skip-gram详细介绍

skip-gram模型

skip-gram模型是给定目标词,来预测其上下文。由于skip-gram模型是一个简单的神经网络模型。我们知道使用神经网络来训练的过程分为:

  • 确定训练数据$(X,Y)$
  • 确定网络架构
  • 确定损失函数
  • 确定优化器、迭代次数
  • 存储网络

确定训练数据

假设语料库只有一句话:The quick brown fox jumps over the lazy dog。所以总共只有8个词。在skip-gram中,训练数据的形式其实非常简单,其实就是使用n-gram的方法生成词对。譬如

上图的窗口大小是2,左右各取2个词。实际上窗口大小为5时比较好的,这里只是为了展示。在得到训练数据后,我们将其用one-hot编码后,就可以输入网络了。譬如:(the, quick)单词对就表示$[(1,0,0,0,0,0,0,0),(0,1,0,0,0,0,0,0)]$,输入网络。当然,对target单词也是需要进行抽样的。链接:对target抽样

确定网络架构

上面就是skip-gram模型的架构,输入是一个单词的vector,输出是softmax后的概率分布,每一个概率值对应词汇表中的一个单词,表示词汇表中的单词出现在输入单词的上下文的概率。中间只有一个线性的hidden layer。还是紧接着我们的例子,词汇表有8个单词,所以,输入时8维的vector,输出也应该是一个8维的vector。而hidden layer的维度则是我们想要得到的词向量的维度,比如说我们使用3个hidden units,那么,权重矩阵就是8*3维的矩阵。(实际上google推荐使用300维。)注意:在网络中,其实有两个权重矩阵,我们只需要选择其中一个就可以了,因为它们互为转置。我们最后需要的额就是权重矩阵,并不需要输出层的结果。

确定损失函数

由于在输出层使用softmax函数,那么很自然地,损失函数就是交叉熵函数。即:

我们可以发现,使用sotfmax,需要计算词汇表中所有词汇的和,而一般的词汇表数目在几十万到几百万不止,那么,这个计算代价是非常昂贵的,所以,我们需要使用一些技巧来加速训练。

Negative sampling

Negative sampling主要就是解决网络难以训练的问题。在skip-gram模型中,我们使用SGD来训练网络,这需要计算词汇表中所有单词的和,当词汇表很大的时候,这是非常难的。那么negative sampling,就是在在输出层中,我们抽取几个应该为0的维度,再加上为1的维度,来进行训练。这样就能大大加快训练速度。

譬如说,我们的词汇表为10000,hidden layer的维度300,我们nagative sampling的维度个数为5个,那么我们需要更新的维度就是6个,所以我们需要更新的权重系数为300*6=1800,这是输出层系数的0.06%。注意:在隐藏层的权重10000*300=3000000是必须需要训练的。这样一来,我们就将softmax问题转换为V个logistic二分类问题,并且我们每次只更新其中K个负样本与一个正样本的参数,从而大大降低了计算成本。

但是问题来了:我们应该怎么抽取这5个应该为0的维度呢?具体做法是:根据单词在语料库中出现的次数来决定。出现次数越多,那么约有可能被抽中。公司如下:

其中,$P(w_i)$表示单词$w_i$被抽中的概率;$f(w_i)$表示单词$w_i$在语料库中出现的次数。

hierarchical softmax

所谓的hierarchical softmax,实际上就是采用Huffman树。huffman树的叶子结点就是词汇表中的单词,从根节点到叶子结点的路径就表示单词的概率。这样一样,我们就不需要对所有单词的得分进行求和,就大大降低了计算成本。注意:在构造huffman树的时候,常用的词的深度会更小一些,即更靠近根节点;而不常用的词会更深一点。

CBOW模型

CBOW模型与skip-gram的训练方式相反。它是给定目标词上下文,然后来预测目标词。

其中网络架构与skip-gram模型非常的类似。但是需要注意输入。CBOW模型的输入是多个单词的one-hot vector的和,而隐藏层需要对其求平均。如下:

其中,$x_1,x_2,..,x_C$是输入的vector,$h$是隐藏层的输出,$C$是输入的单词的数目。

其余与skip-gram模型一样,输出层也是使用了softmax。此外,negative sampling与hierarchical softmax这两种trick也都可以用在CBOW模型中。

GloVe模型

GloVe模型出自于另一篇论文《Global Vectors for Word Representation》。强力推荐大家去读一读,这是一篇非常好懂的论文!🤩

GloVe模型的目标是:得到单词的词向量,让其尽可能的包含语义与语法信息。输入是语料库(没错,不需要去构建训练集),输出是词向量。GloVe模型的思路是:从语料库中统计共现矩阵,然后根据共现矩阵与GloVe模型来学习词向量。

统计共现矩阵

我们记共现矩阵为:$X=[X_{ij}]_{N\times N}$。其中,$X_{ij}$表示单词$j$在单词$i$的上下文中出现的次数,$N$表示词汇表中的单词数目。至于如何构建的,其实就是根据统计窗口,遍历整个语料库,来进行统计。接下来,我们再引入一些符号:

其中,$P_{ij}$表示单词$j$在单词$i$的上下文出现的概率,$ratio_{i,j,k}$表示单词$i$与单词$k$的相关度,以及单词$j$与单词$k$的相关度的比值。为什么要有这个东西?因为,如果我们直接看$P_{ij}$来表示单词$i$与单词$j$的之间的相关度的话,其实是不够的,因为这很有可能受语料库的影响。所以,需要需要它们之间的比值。我们可以发现$ratio_{i,j,k}$有如下性质:

  • 当$i$与$k$相关,$j$与$k$不相关的时候,那么$ratio_{i,j,k}$的值会非常的大;
  • 当$i$与$k$相关,$j$与$k$相关的时候,那么$ratio_{i,j,k}$的值会趋于1;
  • 当$i$与$k$不相关,$j$与$k$相关的时候,那么$ratio_{i,j,k}$的值会非常的小;
  • 当$i$与$k$不相关,$j$与$k$不相关的时候,那么$ratio_{i.j.k}$的值会趋于1。

那么,GloVe模型的想法是:假设我们现在以及得到了单词的词向量$v_i,v_j,v_k$,那么,如果说我们通过某个函数,能够求得$ratio_{i,j,k}$的话,那么就说我们的词向量与共现矩阵是一致的,也就是说我们的的词向量包含了共现矩阵中的信息。

学习词向量

假设我们的函数为:$g(v_i,v_j,v_k)$,那么,我们的目标就是:$g(v_i,v_j,v_k)=\frac {P_{ik}}{P_{jk}}$。所以,很自然地,我们可以想到:

只要让这个式子最小,我们就能求出所有的词向量。但是,这个式子存在一个问题:那就是这个计算复杂度太高了,为:$O(N\times N\times N)$。所以,我们必须想办法减小复杂度。

由于我们想衡量$v_i$与$v_j$的相似度,那么,我们引进$v_i-v_j$这个是很自然的;此外,由于$ratio_{i,j,k}$是一个标量,那么我们引入内积也是很自然的,$(v_i-v_j)^Tv_k$,最后我们在外面加一层$exp$,从而能够简化运算。所以我们的函数可以变为如下式子:

由于我们的目标是:$g(v_i,v_j,v_k)=\frac {P_{ik}}{P_{jk}}$,将g函数带入,得到:

所以,我们只要令:$P_{ik}=exp(v_i^Tv_k),P_{jk}=exp(v_j^Tv_k)$,即可。我们再将这两个式子统一一下,如下:

我们可以看看最后的化简的式子,右边是对称的:$v_i^Tv_j=v_j^Tv_i$;而左边是不对称的。所以,我们需要再化简一下:

所以,我们的最优化式子可以化简如下:

此外,我们还想让出现频率高的词对有更高的权重,所以,我们需要再加一个权重项,最终如下:

其中,权重项函数为:

以上就是GloVe模型的全部内容~

GloVe模型与word2vec的对比:

  • GloVe模型最大的优点是利用了全局的信息,而word2vec中,尤其是skip-gram,只利用了目标词周边的一小部分上下文。尤其当引入negative sampling训练的时候,丧失了词与词之间的关系信息。
  • GloVe模型能够加快模型训练速度,并且能够控制词的相对权重。

fastText模型

fastText是Facebook在2016年所提出的方法。其实,整个模型架构并没有特别创新的地方,和CBOW模型非常地像。其创新的地方在于:子词嵌入的引入。(论文写的太简洁了,导致看了很久😫)

模型架构

整个模型架构与CBOW模型非常的类似。输入是一段序列,输出是这段序列的标签类别。中间层仍然是线性的。输出层仍然是softmax。并且在训练的时候,使用hierarchical softmax来加速训练。

子词嵌入

这个才是fasttext模型真正创新的地方。在word2vec或者其他的模型中,相似形态的单词由不同的向量来表示。譬如,老师们,老师。但是,老师们、老师这两个单词,意思其实非常的相近,不应该被编码成不同的向量。所以,在fasttext中,引入了子词嵌入。具体做法是:我们将Apples,使用trigram,得到:<ap,app,ppl,ple,les,es>,在训练过程中,每一个n-gram都会由一个向量来表示,我们可以用这6个trigram的叠加来表示Apples这个单词。序列中所有单词的向量以及n-gram向量同时相加平均,作为训练的输入。譬如:一段序列中3个词,$w_1,w_2,w_3$表示3个词的向量,$w_{12},w_{23}$表示bigram特征的向量。那么hidden layer的输出是:$h=\frac 15W^T(w_1+w_2+w_3+w_{12}+w_{23})$。

这里有一个问题需要注意:在实际中,我们往往会使用多种n-gram,所以得到的n-gram结果比单词数目多得多。所以来存储这么多的n-gram是不太现实的。所以,我们的做法是:将所有的n-gram映射到一张hash桶中,这样的话,就能够实现n-gram的vector共享。

子词嵌入的好处:对低频词汇的词向量训练效果会更好,因为它可以和其他词共享n-gram;此外,对于UNK,我们仍然可以构建它们的字符级n-gram向量。

理论部分就到这里,我们再回顾一下:fastText到底快在哪里?

  • 使用了hierarchical softmax,这本来就是对普通的softmax很大的加速了;
  • 在hierarchical softmax中,使用Huffman树来构建,对于文本分类而言,其分类树远远小于skip-gram模型中的词表数目,所以,相比于skip-gram模型,更加快速;
  • 一些trick,譬如使用提前算好exp的值。(在使用logisitic进行二分类的时候使用。)

实战

在实战部分,我主要是使用gensim。当然,如果有兴趣的小伙伴们,可以去看它们的源码,这个在github上都能够搜的到。

fasttext

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
from gensim.models import FastText
sentences=[["你", "是", "谁"], ["我", "是", "中国人"]]
#训练主函数

#sentences表示str类型的list的集合
#size表示词向量的维度
#window表示当前词与预测词之间的距离
#min_count表示低于这个数字的单词的频数,那么将被删除掉
#iter表示epoch
#min_n表示字符级的n-gram的最小的n
#max_n表示字符级的n-gram的最大的n
#word_gram:当为1的时候,会用到字符级的n-gram;为0,则不会用到字符级的n-gram,等同于word2vec。
model = FastText(sentences, size=4, window=3, min_count=1,
iter=10,min_n = 3 , max_n = 6,word_ngrams =1)

#获得词向量
model.wv["你"]

#模型的保存与加载
model.save("fname")
model=FastText.load("fname")

#注意,这个只能单向保存为word2vec,无法通过load加载
model.wv.save_word2vec_format('test_fasttext.txt', binary=False)
model.wv.save_word2vec_format('test_fasttext.bin', binary=True)

#在线更新语料库
# 在线更新训练 fasttext
from gensim.models import FastText
sentences_1 = [["cat", "say", "meow"], ["dog", "say", "woof"],["dude", "say", "wazzup!"]]
sentences_2 = []

model = FastText(min_count=1)
#build.vocab表示建立字典,其中需要corpus作为参数,当update参数为true的时候,那么就更新字典
model.build_vocab(sentences_1)
#train表示训练fasttext模型
#参数:
#total_examples表示字典中的使用的corpus中的句子数目一致
#epochs表示迭代次数
#total_examples=model.corpus_count, epochs=model.epochs是标准写法
model.train(sentences_1, total_examples=model.corpus_count, epochs=model.epochs)

#model.build_vocab(sentences_2,update=True)
#model.train(sentences_2, total_examples=model.corpus_count, epochs=model.iter)

#获取词向量字典,两种方式
model.wv.vocab
model.wv.index2word

#获取与目标词相似的词
model.wv.most_similar(positive=['你', '是'], negative=['中国人'])
#model.wv.most_similar_cosmul(positive=['你', '是'], negative=['中国人'])



参考文献

1 《word2vec Parameter Learning Explained》,Xin Rong(强推, 比原始论文详细的多!)

2 《GloVe: Global Vectors for Word Representation》

3 《Bag of Tricks for Efficient Text Classification》

4 https://www.yuque.com/liwenju/kadtqt/ensofi

5 http://albertxiebnu.github.io/fasttext/

Would you like to buy me a cup of coffee☕️~