目录
分布表示(Distributional Representation)
分布式表示(Distributed Representation)
CBOW模型(Continuous Bag-of-Words Model)
Skip-gram模型(Continuous Skip-gram Model)
Glove(Global Vectors for Word Representation)
PV-DM模型(Paragraph Vector Distributed Memory Model)
PV-DBOW模型(Paragraph Vector Distributed Bag-of-Words Model)
这两天将word2vec和doc2vec的一些相关模型捋了一遍,觉得有必要将它们放在一起做个总结,一方面它们做的事情类似,放在一起比较合适;另一方面,加深对各个模型思想的理解,方便以后随时查看。对本文参考的诸多大牛的文献一并感谢,图中使用了文献中的部分图片,具体文献名称如下:
《A Neural Probabilistic Language Model》
《Effificient Estimation of Word Representations in Vector Space 》
《Distributed Representations of Sentences and Documents》
《Word2Vec (Part 1): NLP With Deep Learning with Tensorflow (Skip-gram)》
《Word2Vec Tutorial - The Skip-Gram Model》
《Efficient Estimation of Word Representations in Vector Space》
《A Unified Architecture for Natural Language Processing: Deep Neural Networks with Multitask Learning》
词袋模型(Bag of Word Model)
词袋模型主要对句子(sentences)、段落(paragraph)和文本(text)进行表示的一种模型。它是将所有训练的句子、段落或者文本中的词语装进一个袋子里,作为字典,不考虑其词法和语序的问题,即每个词语都是独立的。例如,给定如下2个句子:
Jane wants to go to Shenzhen.
Bob wants to go to Shanghai.
将句子中的词装进词袋中,袋子里包括{ Jane,wants,to,go,Shenzhen,Bob,Shanghai }。为袋子中的词添加索引,如下:
{"Jane":1,"wants":2,"to":3,"go":4,"Shenzhen":5,"Bob":6,"Shanghai":7}
这样每个词可以通过索引来表示或者获取,比如"wants"这个词可以使用索引2表示等等。接下来,我们就可以为每个句子构造一个对应的向量,该向量的维数和字典(也就是词袋)中词的个数相同,向量的每个分量表示词袋中的词在句子中出现的频率,例如上面两个句子对应的向量分别为:
(1,1,2,1,1,0,0)
(0,1,2,1,0,1,1)
比如第一个向量的第三个分量“2”就表示词袋中索引为3的“to”这个词在该句子中出现的次数。上面是以句子为例,换成段落或者文本也可以使用该模型向量化。词袋模型会存在如下几个问题:
(1)维度灾难
通常情况下,词袋中的词非常多,可能有几十万甚至上百万,对应的向量就会达到几十万维甚至百万维,而且除了句子(段落或者文本)中出现的词语对应的分量不为0,其余大部分分量为0,也就是说向量是高维稀疏的。
(2)无法保留词序信息
句子(段落或者文本)通过词袋模型表示成向量后,词与词之间的先后顺序(order)并没有表示出来,也就是说丢失了词序信息。例如将上面的第一个句子打乱形成一个新的句子,如下:
Jane wants to go to Shenzhen.
Shenzhen Jane to go wants to .
上面两个句子通过词袋模型对应同一个向量(1,1,2,1,1,0,0),但显然这两个句子语义完全不同。
(3)存在语义鸿沟
由语言的句法到语义的理解之间存在着一个语义鸿沟。这和无法保留语义信息有很大关系。
独热表示(One-hot Representation)
段落或者文本是有句子构成的,而句子是由一个个词构成的,可以将词看成是表达文本处理的最基本单元。那我们就可以将注意力放在词的向量表示上了。独特表示就是这样的方式,它和词袋模型有几分相似之处。也是根据大量的文本或者句子构造一个字典。依然以上面两个句子为例,它们构成的字典如下:
{"Jane":1,"wants":2,"to":3,"go":4,"Shenzhen":5,"Bob":6,"Shanghai":7}
现在我们就可以对单个词“Jane”进行向量表示了,第i个分量表示词典中第i个词是否是“Jane”,所以表示后的向量维数和词典大小相同,只有一个分量为1,其余为0,如下:
(1,0,0,0,0,0,0)
这种方式会存在如下一些问题:
(1)维度灾难
这和词袋模型类似。
(2)表示的词很有限
(3)无法表示词的语义信息
分布表示(Distributional Representation)
词是承载语义的最基本的单元,而词袋模型和独特表示仅仅是将句子或者词符号化,不包含任何的语义信息。如何将语义信息融入到词表示中,Harris 在 1954 年提出的分布假说(Distributional Hypothesis)为这一设想提供了理论基础:上下文相似的词,其语义也相似。Firth 在 1957 年对分布假说进 行了进一步阐述和明确:词的语义由其上下文决定(a word is characterized by the company it keeps)[29]。二十世纪 90 年代初期,统计方法在自然语言处理中逐渐 成为主流,分布假说也再次被人关注。Dagan 和 Schütze 等人总结完善了利用上 下文分布表示词义的方法,并将这种表示用于词义消歧等任务 [20, 21, 100, 101], 这类方法在当时被成为词空间模型(word space model)。在此后的发展中,这 类方法逐渐演化成为基于矩阵的分布表示方法,期间的十多年时间里,这类方 法得到的词表示都被直接称为分布表示(distributional representation)。所以说分布表示基于分布假设理论,利用共生矩阵来获取词的语义表示,可以看成是一类获取词表示的方法。这里我们举个例子说明下“上下文相似的词,其语义也相似”,看如下两个句子:
MYJ520宣誓就职。
CYW520宣誓就职。
句子中的“MYJ”和“CYW”这两个词,它们的上下文非常相似,都是“520宣誓就职”,我们认为“MYJ”和“CYW”的语义也相似,这和实际是相符的,因为他们都是领导人。
分布式表示(Distributed Representation)
基于神经网络的分布表示一般称为词向量、词嵌入(word embedding)或分布式表示(distributed representation)。首先我们需要区分该方法与分布表示(Distributional Representation)的区别和联系,分布表示基于分布假说,主要利用共生矩阵来获取词的语义表示,是一类获取词表示的方法,而分布式表示是基于神经网络的的分布表示,它们既有联系又有区别。分布式表示通常是神经网络训练的中间产物,当然也有以训练词向量为目标的神经网络。通常我们先确定向量的维数,比如百维或者千维,然后向量的每个分量是实数,向量表示了词的语义信息。举个例子,比如词“Jane”,我们先确定向量维数,比如7维,如下:
(1.2, 0.5, 0.4, 0.2, 0.1, 0, 0)
和之前说的词袋模型或者独热表示都不同。分布式表示后的向量不再是稀疏的。这里“分布式”取分散之意,它将语义表示到向量的各个分量中去。再来说下“Word Embedding”,暂且翻译成“词嵌入”,它是将所有词看成一个空间,每个词对应的向量假设是d维,这些向量构成d维空间。这样Embedding就可以看成是词空间到d维向量空间的映射,如下:
.
被称为嵌入空间(embedding space),中的向量称之为嵌入向量(embedding vector)。现在大家对“Word Embedding”应该理解了,其实就是将词嵌入到。非常有趣的是类似“漂亮”和“美丽”这些语义相近的词嵌入到中,它们靠的很近,如图:
语言模型(Language Model)
语言模型是基于概率统计理论的。它在信息检索、机器翻译和语音识别中承担着重要的任务。它是为长度为m的字符串(或者句子)确定联合概率分布。其中表示词,比如对于下面这个句子:
MYJ520宣誓就职
这句话分词后为:“MYJ”,“520”,“宣誓”,“就职”,这里MYJ,520,宣誓,就职。估算它们联合概率指的是这四个词像句子这样一起出现的概率。我们知道
这样联合概率可以链式计算:
计算的问题就可以转化为计算等式右边各项的问题。实际上等式右边的每一项表示的是当句子的前i-1个词是,第i个词是的概率。这表示第i个位置上出现什么样的词和前i-1个位置上的词是相关的,这样正好保留词之间的顺序。另外一方面,如果句子很长的话,m值很大,这会导致等式右边从某项开始计算非常困难,比如。为了简化计算,而且句子很长时,最后的词与最前面的词关系变得很弱,可忽略不计。比如第n个词只与前n-1个词相关,与之前的词无关。这样
这就是n元模型(n-gram model)。
(1)当n=1时,称为一元模型,此时整个句子中的每个词都是相互独立的,那么联合概率就可以如下计算:
(2)当n=1时,忽略了词与词之间的关联,所以一元模型的效果并不理想,从而我们考虑n=2的二元模型,也就是每个词只与它前面的一个词相关。此时条件概率变为:
联合概率变为:
(3)当n=3时,称为3元模型
n越大。保留的词序信息越丰富,但是计算成本也相应增长。根据计算力,选择一个合适的n比较重要。通常n在2~5比较合适。
当选定了n之后,接下来就是估算每个条件概率
这可以通过统计语料中句子出现的频率与句子出现的频率之比,也就是
count表示频率,统计频率的时候,可能会出现分子或者分母为0的情况,这可以通过一些平滑算法解决,不再累述。
语言模型是基于统计概率的,所以有时也称为统计语言模型。
神经网络语言模型(NNLM)
事实上,上面的语言模型存在维数灾难(curse of dimensionality),举个例子,如果要计算10个词组成的句子的联合概率分布,假设词典V中有10万个词,这需要计算。而NNLM模型改变了这个现状。下面我们来详细介绍NNML思想。NNML的神经网络结构如图:
上图神经网络输入输入层(绿色方块)、投影层、隐藏层和输出层。下面我们逐个来介绍。假设训练数据集为,,每个都是词典(词典中的词是唯一不重复的)中的一个词,词典的大小记为。数据集可以看成是将训练文本分词后得到的,指的是词的数量,所以这些词中是可以重复的。比如下面一小段训练文本:
分词后对应如下数据集:
猫/ 属于/ 猫科/ 动物/ 分/ 家猫/ 野猫/ 许多/ 家庭/ 都/ 喜欢/ 猫/
对应的T=12,其中猫。
下面我们这里同样使用语言模型中的n元模型,假设n个词之间相关。每次从训练集中取出长度为n的文本序列,比如:
可以取.
比如t=1时,文本序列是(因为该词之前没有词了),时,文本序列为。等等。总共有T个。
现在将长度为n的文本序列作为神经网络的训练数据,但并不是将这n个词全部输入,而是输入前n-1个词输入,第n个词作为神经网络输出,所以图中使用n-1个绿色方块表示每个词的输入。另外注意,这里输入的是每个词的独热表示。也就是一个维向量,向量中只有一个分量为1,其余为0.输入向量与矩阵运算后得到投影层。是一个阶矩阵,如下:
现在假设输的词的独热表示为,其中第i个位置为1。向量经过与矩阵运算后得到:
实际是将矩阵C的第行提取了出来。因为一个词的独热表示是不变的,而且矩阵在训练后也不变,这样就可以看成词的词向量,它是一个m维向量。可根据情况选择m=30或者60等。所以投影层得到的结果就是神经网络的中间产物。它将每个词都表示成了唯一的一个向量。得到的n-1个词的特征向量将作为隐藏层的输入。这里隐藏层使用的tanh函数。如图:
tanh实际做的是规范化,将函数的任何输入都规范到(-1,1)之间。假设投影层到隐藏层的权重矩阵为,偏置向量为d,隐藏层输出,如下计算:
其中,也就是前n-1个词的特征向量通过按顺序连接(concatenation)后构成的向量,大小为。为了方便我们将计算z的维数都标识出来:
假设隐藏层到输出层的权重矩阵为,偏置向量为,如上图,如果投影层与输出层有连接,就假设权重矩阵为,那么输出层最后输出为
,如果投影层与隐藏层没有连接,。为了清楚,将计算y的维数都标识出来:
比如对于向量,经过softmax函数计算后,每个分量如下:
容易发现,实际就是将任何向量的每个分量转换成了一个概率。这样最终的输出是一个维向量,分量i对应的是词典中索引为i的词的概率。如图中所示:
context指的是上下文,也就是输入。表示在输入是情况下,接下来出现词(这里使用的是词的索引)的概率。这不就是上面说的语言模型中得条件概率吗?
语言模型中通过统计方法计算,NNML模型通过神经网络训练获得。模型已经介绍完了,我们现在通过NNML模型可以得到两个结果:
(1)每个词的特征向量,也就是词向量,它是NNML的中间产物;
(2)联合概率,NNML的最终结果会得到条件概率,从他们可以计算出文本序列的联合概率。
模型中需要计算的参数如下:
总共有个参数。 下面我们定义模型的目标函数,训练的目标是最大化每一个条件概率也就是:
取对数,然后添加上正则项,如下:
含有模型参数,如下表示:
(1)
模型训练的目标就是最大化(1)式。
模型采用随机梯度下降法更新,注意这里是最大化(1)式,所以用随机梯度上升法,即:
关于随机梯度下降法,参见《机器学习之梯度下降法(GD)、随机梯度下降法(SGD)和随机平均梯度下降法(SAGD)》。
NNML模型训练时的计算量要超过语言模型,主要计算瓶颈在输出层需要计算词典中每个词的概率。我们采用并行计算,比如共享内存处理或者具有快速网络的Linux集群。在共享内存处理的情况下,选择异步实现,每个处理器处理一部分数据集,更新的参数存储在共享内存中,有时候一个处理器更新的参数可能会被另一个处理器更新的参数覆盖,会产生“噪音”,这种噪音很小,并没有明显地减慢训练。另一方面,可以使用快速网络的Linux集群,因为NNML计算的瓶颈在输出层计算所有词的概率,我们将这些任务并行,每个CPU计算一部分输出,对应的更新与输出相关的参数。
C&W模型
CBOW模型(Continuous Bag-of-Words Model)
CBOW模型和NNLM有一些类似,不同的是:
(1)去掉了NNML中的隐藏层,也就是去掉了比较复杂的非线性处理;
(2)对所有词共享投射层(projection layer),而在NNML中每个词单独有一个投影层。在CBOW的投影层只是对输入的所有词的向量做了平均。
(3)在NNML中,模型通过前n-1个词,预测第n个词,而在CBOW中,使用上下文的词,预测中间位置的词。
这样得到的CBOW模型比NNML简单了很多。CBOW模型的网络结果示意图,如下:
如图:这里仍然使用n元模型,如下:
其中是的上下文(context),是模型的输入,是模型的输出。假设输入层到投影层的投影矩阵为,是一个阶矩阵,如下:
输入的是上下文对应的独特表示后的向量,通过与矩阵C运算后,实际会提取矩阵C的n-1行向量,为了方便,比如提取的就是前n-1行向量,
这个矩阵将作为投影层的输入,然后投影层会对向量求平均(图中标识的是SUM,可能标识有问题),得到
avg作为投影层输出。假设投影层到输出的权重矩阵为
后面使用softmax函数即可,不过模型中使用了Huffman tree对计算进行了优化。CBOW模型的目标函数与NNML类似,不再累述。
Skip-gram模型(Continuous Skip-gram Model)
Skip-gram模型与CBOW模型类似,不过在Skip-gram模型中,输入是,输出是的上下文,如图:
举个例子来说明该模型,假设训练数据为下面这样一个句子:
The dog barked at the mailman
并且假设输入dog,输出The barked at the mailman,这样一个输入训练多个输出。这在实现时比较困难,实际解决的时候,我们将输入和输出分为几组,如下:
(input=dog,output=The ),……,(input=dog,output=mailman)。
Glove(Global Vectors for Word Representation)
略。
PV-DM模型(Paragraph Vector Distributed Memory Model)
PV-DM模型和CBOW模型对应,不过它不是训练词向量,而是用来训练段向量(Paragraph Vector),当然它同时也会得到词向量,如图:
假设每个段落对应一个m维向量,每个词也是对应m维向量。如图,矩阵W的和前面提到的模型中C一样,假设是,它的每一行是一个词的词向量,同理D是段对应的矩阵,,P是所有段落构成的集合,它的每一行表示一个段落的段向量(PV)。注意的是,在训练过程中,对所有的词或者段落,矩阵W是共享的,都一样,但是矩阵是变化的,如果输入的词都来源于同一个段落,共享,如果输入的词变为另一个段落中的词,则变为相应段落的段向量。
对于新的段落,如何预测呢?在预测时,需要执行一个推理步骤来计算新段落的段落向量。这也是通过梯度下降得到的。在这一步中,模型其余部分的参数,即单词向量W和softmax权值是固定的。
PV-DBOW模型(Paragraph Vector Distributed Bag-of-Words Model)
PV-DBOW模型和Skip-gram模型对应。但名字容易让人混淆,和CBOW模型名字类似。它的主要思想是使用段落,来预测出现这个段落中词的概率,如图:
简单应用
上面提到的算法可以用来计算文章相似度,对于word2vec,做法如下:
(1)利用word2vec训练语料,得到词向量化模型,这样就可以将词向量化;
(2)比如计算文章text1和text2的相似度,先从文本提取关键词(比如使用tf-idf),然后使用(1)中的模型将关键词向量化,这些向量相加得到文章的向量化表示
(3)利用向量的相似度算法计算文章相似度,通常有编辑距离,余弦相似度等算法。
对于doc2vec,做法如下:
(1)利用doc2vec训练语料,得到词段落向量化模型,这样就可以将段落向量化;
(2)利用训练好的模型,推测文章的段落向量,这里需要固定词向量的矩阵和softmax,然后使用随机梯度下降法计算。
(3)同word2vec的(3)。
语料下载地址:https://dumps.wikimedia.org/zhwiki/latest/
zhwiki-latest-pages-articles.xml.bz2
语料2G不到,下载时间很长。
代码目录结构如图:
pre_process.py模块:
from gensim.corpora import WikiCorpus
import jieba
from word2vec.langconv import *
# 原始语料库
zhwiki_origin = 'data/zhwiki-latest-pages-articles.xml.bz2'
# 处理后的语料库
zhwiki_new = 'data/reduce_zhwiki.txt'
def pre_process():
"""
数据预处理
:return:
"""
word_term = []
text_count = 0 # 记录处理的文章数
zhwiki = WikiCorpus(zhwiki_origin, lemmatize=False, dictionary={})
# 写数据到文件中需指定编码encoding='utf-8'
# 遍历原始语料text,遍历text中的sentence
with open(zhwiki_new, 'w', encoding='utf-8') as f:
for text in zhwiki.get_texts():
for sentence in text:
# 繁体字处理
sentence = Converter('zh-hans').convert(sentence)
# 分词
seg_list = list(jieba.cut(sentence))
for term in seg_list:
word_term.append(term)
# 分词后以空格连接写入txt文件中
f.write(" ".join(word_term) + "\n")
word_term = []
# 打印已经处理的文章数
if text_count % 200 == 0:
print("Saved " + str(text_count) + "articles")
text_count = text_count + 1
if __name__ == '__main__':
pre_process()
处理后数据保存在reduce_zhwiki.txt中,如图:
因为语料库很大,单机CPU运行时间消耗很长。
word2vec_train.py代码如下:
from gensim.models import Word2Vec
from gensim.models.word2vec import LineSentence
# 处理后的语料库
wiki_new_file = 'data/reduce_zhwiki.txt'
def train():
"""
训练词向量模型
:return:
"""
wiki = open(wiki_new_file,encoding='utf-8')
# 参数说明:
# sg训练的算法, 1表示skip-gram; 其他表示CBOW.
# size是词向量的维数,
# window窗口,当前词与预测词的最大距离,可以和n元模型中的n对应
# min_count表示忽略词频率小于该值的所有词
# workers工作线程数,加快训练速度
model = Word2Vec(LineSentence(wiki), sg=0, size=192, window=5, min_count=5, workers=9)
# 一定要将训练好的模型保存(否则每次都要重新训练,耗时)
model.save('zhwiki.word2vec')
if __name__ == '__main__':
train()
该模块以处理后的数据reduce_zhwiki.txt开始训练模型,训练完成后将模型保存为zhwiki.word2vec文件,方便后面调用。单机CPU训练时间也很长。下面就可以使用训练好的模型,计算词之间的相似度了。
similarity_test.py代码如下:
from gensim.models import Word2Vec
# 加载训练好的模型
model = Word2Vec.load('zhwiki.word2vec')
sim1 = model.similarity('漂亮', '美丽')
sim2 = model.similarity('漂亮', '丑陋')
print('sim1=', sim1, 'sim2=', sim2)
word = '漂亮'
if word in model.wv.index2word:
sim_word = model.most_similar(word)
print(sim_word)
该模块使用训练好的模型计算词的相似度。
文档相似度:
doc2v2c_demo.py代码如下:
import jieba
import codecs
import gensim.models as g
import numpy as np
model_path = 'model/zhiwiki_news.doc2vec'
start_alpha = 0.01
infer_epoch = 1000
docvec_size = 192
p1 = 'data/P1.txt'
p2 = 'data/P2.txt'
def simlarity(vector1, vector2):
vector1_mod = np.sqrt(vector1.dot(vector1))
vector2_mod = np.sqrt(vector2.dot(vector2))
sim = 0
if vector1_mod * vector2_mod != 0:
sim = (vector1.dot(vector2)) / (vector1_mod * vector2_mod)
return sim
def doc2vec(file_name, model):
# 对段落分词
doc_words = [w for x in codecs.open(file_name, 'r', 'utf-8').readlines()
for w in jieba.cut(x.strip())]
# alpha学习率
# epochs训练文档的轮次,值越大,时间越长,但是推测出的值更稳定
doc_vec_all = model.infer_vector(doc_words=doc_words, alpha=start_alpha, epochs=infer_epoch)
return doc_vec_all
if __name__ == '__main__':
model = g.Doc2Vec.load(model_path)
p1_vec = doc2vec(p1, model)
p2_vec = doc2vec(p2, model)
print(simlarity(p1_vec, p2_vec))
输出如下: