动手实践word2vec和doc2vec模型

5,026 阅读13分钟

我们在处理文本的时候有可能会遇到文本相似度计算或者找出文本近似表达的诉求,本篇文章是从实际操作的角度出发,手动训练出word2vec和doc2vec模型,来计算文本的相似度

当我们提及word2vec的时候,可能很多人都会联想到CBOW(Continuous Bag of-Words)、Skip-gram模型以及其算法。

注:CBOW与Skip-gram模型是研究者在NNLM(Neural Network Language Model)和C&W模型的基础上保留其核心部分得到的。

提及docs2vec我们会想到DM(Distributed Memory)和DBOW(Distribute Bag of Words)模型以及其算法。

我们此篇并不会深入这些算法的内容,我们只是在应用,需要的地方我会尽可能的使用白话来说明,而这些算法的核心内容我会在后面的文章中做详细的分析。

注:我所有的代码都是在MAC的python2下面运行的,如果你使用windows或者是python3运行的时候发现报错,那么很有可能是因为编码的原因

一 我们为什么需要用到word2vec

我们知道计算机是没有识别字体的能力的,你输入“今天是什么天气”,它可能不会有任何的反应,还很有可能给你报一个错误,但是计算机的计算能力还是不错的,那么我们就可以把语言转为数字,让计算机来进行数学运算就可以了,而向量是我们表示语言的不错选项。

在以往的自然处理中经常会把字词使用独热编码(One-Hot Encoder)的方式变为向量。

举个例子来说明一下:假如现在我们有一千个各不相同的中文词,其中包括香蕉、菠萝、水果、河马等等。

而独热编码会制造出一千维度的向量,来表示这些词语:

首先将所有的词语放在一个集合中: [天气, 菠萝, 高原, 电影, 香蕉, 编码, 水果, ..........., 赤道, 太阳, 河马, 快跑, 明星]

注:这个一千维向量中词语的顺序是随机的。

那么香蕉、菠萝、水果、河马这几个词的向量就可能像下面这样表示

  • 香蕉 [0, 0, 0, 0, 1, 0, 0, ..........., 0, 0, 0, 0, 0]
  • 菠萝 [0, 1, 0, 0, 0, 0, 0, ..........., 0, 0, 0, 0, 0]
  • 水果 [0, 0, 0, 0, 0, 0, 1, ..........., 0, 0, 0, 0, 0]
  • 河马 [0, 0, 0, 0, 0, 0, 0, ..........., 0, 0, 1, 0, 0]

经过上面这种转换每一个词都使用了向量的方法进行表示。那么在理想的情况下我们只需要计算每两个向量之间的距离,就可以得到两个词语之间的关系了,距离较近的话就说明这两个词的相关性较强。

但这只是理想情况,这里存在两个难点:

  • 第一就是维度灾难:一千维的向量计算这个时间复杂度还是很高的
  • 第二就是一千维的向量顺序是随机生成的,那么词语之间可能存在的关联就看不出来。

此时就轮到word2vec出场了,word2vec会将One-Hot Encoder转化为低纬度的连续值,也就是稠密向量,并且将其中意思接近的词映射到向量中相近的位置。

注:这里具体的算法我们会在后面的文章中详细介绍。

处理以后的香蕉、菠萝、水果、河马向量:

  • 香蕉 [0.2, 0.6]
  • 菠萝 [0.3, 0.4]
  • 水果 [0.6, 0.6]
  • 河马 [0.9, 0.2]

从上面的坐标系中我们可以看到词语之间的距离,距离越近的词语其相关性也就越高。

注:word2vec处理后的词语一般不会是二维的,此处只是方便说明。

二 开始训练我们的模型

通过上面我们知道了,只要我们使用算法创建出计算机能够识别的向量,那么计算机就会帮助我们计算词语的相似度。

为了创建这样的向量,也就是word2vec模型,我们需要一些语料,在此我们使用的是维基百科中的语料,大小是1.72G。

此处是下载地址:dumps.wikimedia.org/zhwiki/late…

因为维基百科上面很多的中文网页都是繁体字的,因此我们首先要将语料中的繁体字变为简体字,同时将这些语料进行分词处理。

2.1 将获取的语料进行处理

在此我们是使用gensim库来帮助我们。

    def my_function():
    space = ' '
    i = 0
    l = []
    zhwiki_name = './data/zhwiki-latest-pages-articles.xml.bz2'
    f = open('./data/reduce_zhiwiki.txt', 'w')
    # 读取xml文件中的语料
    wiki = WikiCorpus(zhwiki_name, lemmatize=False, dictionary={})
    for text in wiki.get_texts():
        for temp_sentence in text:
            # 将语料中的繁体字转化为中文
            temp_sentence = Converter('zh-hans').convert(temp_sentence)
            # 使用jieba进行分词
            seg_list = list(jieba.cut(temp_sentence))
            for temp_term in seg_list:
                l.append(temp_term)
        f.write(space.join(l) + '\n')
        l = []
        i = i + 1

        if (i %200 == 0):
            print('Saved ' + str(i) + ' articles')
    f.close()

经过上面的处理以后我们就得到了简体字的分词文件了。

2.2 向量化训练

经过上面的操作我们就得到了处理好的语料,接下来我们使用gensim中的Word2Vec帮助我们完成词到向量的转化。

# -*- coding: utf-8 -*-
from gensim.models import Word2Vec
from gensim.models.word2vec import LineSentence
import logging

logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

def my_function():
    wiki_news = open('./data/reduce_zhiwiki.txt', 'r')
    # Word2Vec第一个参数代表要训练的语料
    # sg=0 表示使用CBOW模型进行训练
    # size 表示特征向量的维度,默认为100。大的size需要更多的训练数据,但是效果会更好. 推荐值为几十到几百。
    # window 表示当前词与预测词在一个句子中的最大距离是多少
    # min_count 可以对字典做截断. 词频少于min_count次数的单词会被丢弃掉, 默认值为5
    # workers 表示训练的并行数
    model = Word2Vec(LineSentence(wiki_news), sg=0,size=192, window=5, min_count=5, workers=9)
    model.save('zhiwiki_news.word2vec')

if __name__ == '__main__':
    my_function()

2.3 测试最终的效果

现在我们得到了一个向量的模型,这个时候我们可以看一下实际效果。

#coding=utf-8
import gensim
import sys

reload(sys) 
sys.setdefaultencoding('utf8')

def my_function():

    model = gensim.models.Word2Vec.load('./data/zhiwiki_news.word2vec')
    print(model.similarity(u'香蕉',u'菠萝'))  # 相似度为0.52
    print(model.similarity(u'香蕉',u'水果'))  # 相似度为0.53

    word = '河马'
    if word in model.wv.index2word:
        for item in model.most_similar(unicode(word, "utf-8")):
            print item[0]   # 长颈鹿 狒狒 犰狳 斑马 亚洲象 猫科 黑猩猩 驯鹿 仓鼠 豹猫
        

if __name__ == '__main__':
    my_function()

从上面最后的结果我们可以看到,‘香蕉’和‘菠萝’的相似度是0.52,‘香蕉’和‘水果’的相似度是0.53,可能这个最终的显示效果并不符合你的预期,在你的认知中你可能会认为前者的相似度应该大于后者才对,而最终是这样一个结果的可能原因在于,我们使用的维基百科语料中‘香蕉’和‘水果’相邻的情况比较多。

那么现在的你是否早已经迫不及待的想要亲自动手实践一下了呢?

gitee.com/wangtao_it_…

这个是前面做处理训练以及测试所用到的代码,因为码云的上传文件大小限制,所以你需要手动下载维基百科的语料,并且将下载好的语料放在data文件夹下面。

你可以将码云上面的代码下载到本地。

第一步运行data_pre_process.py文件

第二步运行training.py文件

第三步运行test.py文件

其中第一步和第二步和花费一些时间。

三 word2vec应用于文章

通过上面的操作你现在已经可以获得词与词之间的相似关系了,但是在平常的需求中我们可能还会遇到整篇文档的相似度问题。那么这个时候我们可以先抽取整篇文章的关键词,接着将关键词向量化,然后将得到的各个词向量相加,最后得到的一个词向量总和代表文章的向量化表示,利用这个总的向量计算文章相似度。

3.1 文章关键词提取

关键词提取我们使用jieba分词进行提取

# -*- coding: utf-8 -*-
import jieba.posseg as pseg
from jieba import analyse

def keyword_extract(data, file_name):
   tfidf = analyse.extract_tags
   keywords = tfidf(data)
   return keywords

def getKeywords(docpath, savepath):

   with open(docpath, 'r') as docf, open(savepath, 'w') as outf:
      for data in docf:
         data = data[:len(data)-1]
         keywords = keyword_extract(data, savepath)
         for word in keywords:
            outf.write(word + ' ')
         outf.write('\n')

上面两个函数的作用就是进行关键词的提取。

提取完关键词以后我们需要将关键词向量化。

3.2 关键词向量化

# -*- coding: utf-8 -*-
import codecs
import numpy
import gensim
import numpy as np
from keyword_extract import *
import sys

reload(sys) 
sys.setdefaultencoding('utf8')

wordvec_size=192
def get_char_pos(string,char):
    chPos=[]
    try:
        chPos=list(((pos) for pos,val in enumerate(string) if(val == char)))
    except:
        pass
    return chPos

def word2vec(file_name,model):
    with codecs.open(file_name, 'r') as f:
        # 初始化一个192维的向量
        word_vec_all = numpy.zeros(wordvec_size)
        for data in f:
            # 判断模型是否包含词语
            space_pos = get_char_pos(data, ' ')
            # 获取关键词每行的第一个词
            try:
                first_word=data[0:space_pos[0]]
            except:
                pass
            # 判断模型中是否存在first_word,为真时将其添加到word_vec_all
            if model.__contains__(unicode(first_word, "utf-8")):
                word_vec_all= word_vec_all+model[unicode(first_word, "utf-8")]
            # 遍历space_pos
            for i in range(len(space_pos) - 1):
                word = data[space_pos[i]:space_pos[i + 1]]
                if model.__contains__(unicode(word, "utf-8")):
                    word_vec_all = word_vec_all+model[unicode(word, "utf-8")]
        return word_vec_all

上面两个函数的作用就是将得到的关键词进行向量化,值得注意的是,因为我们的语料比较小,因此我们在向量化的过程中会判断关键词是否存在于语料中。

因为我使用的是python2,因此你在上面的代码中可以看到unicode函数,如果你使用的python3那么unicode函数的地方可能需要修改一下。

3.3 相似度计算

通过上面的两步操作我们已经获得了一个能够代表文章的词向量,接下来就是使用这个向量来计算文本的相似度了。

def simlarityCalu(vector1,vector2):
    vector1Mod=np.sqrt(vector1.dot(vector1))
    vector2Mod=np.sqrt(vector2.dot(vector2))
    if vector2Mod!=0 and vector1Mod!=0:
        simlarity=(vector1.dot(vector2))/(vector1Mod*vector2Mod)
    else:
        simlarity=0
    return simlarity
    
if __name__ == '__main__':
    model = gensim.models.Word2Vec.load('data/zhiwiki_news.word2vec')
    p1 = './data/P1.txt'
    p2 = './data/P2.txt'
    p1_keywords = './data/P1_keywords.txt'
    p2_keywords = './data/P2_keywords.txt'
    getKeywords(p1, p1_keywords)
    getKeywords(p2, p2_keywords)
    p1_vec=word2vec(p1_keywords,model)
    p2_vec=word2vec(p2_keywords,model)
    print(simlarityCalu(p1_vec,p2_vec))     # 0.9880877789981191

这个函数就是计算相似度的一个函数。 从上面的结果来看,两个文章的相似度还是很高的,你可以自己动手尝试一下。

为了你更方便的动手实践,我也将刚刚提到的这些代码放在了码云上面,因为都是使用的同一个语料进行的训练,因此你可以将你在2.1到2.3训练出的模型放在data下面,这样你就不需要重复进行训练。

gitee.com/wangtao_it_…

注:此地址下面的代码也包含了2.1到2.3中的代码

如果你已经将训练好的模型放在了data下面,那么你直接可以运行word2vec_sim.py看到结果。

在此怕某些人疑惑,说明一下正常程序中应该存在的文件。

四 不可不说的doc2vec

你通过上面动手实践以后就可以计算单词于单词,文章与文章之间的相似度,但是你以为只有word2vec一种方式计算文章与文章之间的相似度吗?

不是的,doc2vec也可以计算文章与文章之间的相似度,并且doc2vec会关注于文章词语之间的顺序而且还会综合上下文的语序信息。

举个例子:武松打死了老虎,这句话在分词的时候会被分成“武松”、“打死”、“老虎”(了是停用词,被去掉),word2vec在计算的时候会按照这三个词的向量求平均,但是这个语意信息没有保留下来,你不知道是武松打死的老虎,还是老虎打死的武松。

值得你注意的是,在分析文章方面并不是doc2vec一定优于word2vec,这个需要结合你具体的业务场景来使用。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import gensim.models as g
from gensim.corpora import WikiCorpus
import logging
from langconv import *

#enable logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

docvec_size=192
class TaggedWikiDocument(object):
    def __init__(self, wiki):
        self.wiki = wiki
        self.wiki.metadata = True
    def __iter__(self):
        import jieba
        for content, (page_id, title) in self.wiki.get_texts():
            yield g.doc2vec.LabeledSentence(words=[w for c in content for w in jieba.cut(Converter('zh-hans').convert(c))], tags=[title])

def my_function():
    zhwiki_name = './data/zhwiki-latest-pages-articles.xml.bz2'
    wiki = WikiCorpus(zhwiki_name, lemmatize=False, dictionary={})
    documents = TaggedWikiDocument(wiki)

    model = g.Doc2Vec(documents, dm=0, dbow_words=1, size=docvec_size, window=8, min_count=19, iter=5, workers=8)
    model.save('data/zhiwiki_news.doc2vec')

if __name__ == '__main__':
    my_function()

上面的代码作用是用来训练doc2vec模型的,与word2vec类似,该训练主要分为数据预处理和段落向量训练两个步骤,这里我们使用TaggedWikiDocument函数预处理我们的维基百科预料,所不同的是这里不再是将每个维基百科语料的文档进行分词,而是直接将转换后的简体文本保留,当你训练完成以后你的data文件下面应该是这样的。

我在训练的时候一共花费了15个小时,如果你不想等待这么久的话,可以在下面的链接中下载我已经训练好的模型文件。

链接:share.weiyun.com/5HekNcq 密码:pd9i3n

同样的我也将上面的代码放在了码云上。

gitee.com/wangtao_it_…

如果你想要自己动手训练模型的话,只需要运行train_model.py文件,然后运行doc2vec_sim.py就可以看到结果了。

如果你已经在微云下载了我提供的模型文件,你只需要将模型文件放在data文件夹下面,直接运行doc2vec_sim.py就可以看到结果了。

五 你需要新模型

上面所使用的模型是使用维基百科进行训练得到的,你可能感觉不是特别好,不要慌张,我这里给你提供一个由新闻,百度百科,小说训练得到的64维模型,这个模型是1.5G,我放在了微云上,方便你下载使用。

链接:share.weiyun.com/507zMyF 密码:sk5g4y

#!/usr/bin/python
#-*-coding:utf-8 -*-
import gensim
import jieba
import numpy as np
from scipy.linalg import norm
import re
import sys

reload(sys) 
sys.setdefaultencoding('utf8')

model_file = './data/model.bin'
model = gensim.models.KeyedVectors.load_word2vec_format(model_file, binary=True)


if __name__ == '__main__':
    print(model.similarity(u'香蕉',u'菠萝'))  # 相似度为0.90
    print(model.similarity(u'香蕉',u'水果'))  # 相似度为0.79

    word = '河马'
    if word in model.wv.index2word:
        for item in model.most_similar(unicode(word, "utf-8")):
            print item[0]   # 猩猩 海象 袋鼠 狒狒 浣熊 长颈鹿 大猩猩 乌贼 鲸鱼 松鼠

你需要将微云上面的模型文件下载下来,然后放在data下面就可以了。

因为这个的代码比较简单因此就不将代码放在码云了。

上面的代码就是使用新的模型得到的结果,我们可以看到这个结果相比于维基百科还是有一定的提升的,这个就是预料大带来的直观好处。

六 这只是开始

在上面你可能已经学会了使用语料生成word2vec和doc2vec模型,并且使用一些词语和文章验证过你的生成结果,但这只是刚刚开始,在以后的文章中我们会一起学习word2vec和doc2vec背后所使用的数学算法和思想,以及NLP其余方面的知识。

由于本人的认知能力以及表达能力有限,如果文章中有哪些说明不到位或者解释有误的情况,请你及时指出,期待与你的共同进步。

欢迎关注"腾讯DeepOcean"微信公众号,每周为你推送前端、人工智能、SEO/ASO等领域相关的原创优质技术文章:

看小编这么辛苦,关注一个呗:)