Processing math: 100%
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函数,那么很自然地,损失函数就是交叉熵函数。即:

L=Vi=1yilog(ˆyi),ˆyi=e(wThi)Vi=1e(wThi),V

我们可以发现,使用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(wi)=f(wi)34Vi=0f(wi)34

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

hierarchical softmax

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

CBOW模型

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

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

h=1CWT(x1+x2+...+xC)

其中,x1,x2,..,xC是输入的vector,h是隐藏层的输出,C是输入的单词的数目。

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

GloVe模型

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

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

统计共现矩阵

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

Xi=Nj=1Xij,Pij=XijXi,ratioi,j,k=PikPjk

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

  • ik相关,jk不相关的时候,那么ratioi,j,k的值会非常的大;
  • ik相关,jk相关的时候,那么ratioi,j,k的值会趋于1;
  • ik不相关,jk相关的时候,那么ratioi,j,k的值会非常的小;
  • ik不相关,jk不相关的时候,那么ratioi.j.k的值会趋于1。

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

学习词向量

假设我们的函数为:g(vi,vj,vk),那么,我们的目标就是:g(vi,vj,vk)=PikPjk。所以,很自然地,我们可以想到:

mini,j,k(g(vi,vj,vk)PikPjk)2

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

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

g(vi,vj,vk)=exp((vivj)Tvk)

由于我们的目标是:g(vi,vj,vk)=PikPjk,将g函数带入,得到:

PikPjk=exp(vTivk)exp(vTjvk)

所以,我们只要令:Pik=exp(vTivk)Pjk=exp(vTjvk),即可。我们再将这两个式子统一一下,如下:

Pij=exp(vTivj),XijXi=exp(vTivj),logXijlogXi=vTivj

我们可以看看最后的化简的式子,右边是对称的:vTivj=vTjvi;而左边是不对称的。所以,我们需要再化简一下:

logXij=vTivj+bi+bj

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

mini,j(vTivj+bi+bjlogXij)2

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

mini,jf(Xij)(vTivj+bi+bjlogXij)2

其中,权重项函数为:

f(x)={(xxmax)0.75x<xmax1x>=xmax

以上就是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个词,w1,w2,w3表示3个词的向量,w12,w23表示bigram特征的向量。那么hidden layer的输出是:h=15WT(w1+w2+w3+w12+w23)

这里有一个问题需要注意:在实际中,我们往往会使用多种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☕️~