動手實踐word2vec和doc2vec模型

騰訊DeepOcean發表於2019-03-22

我們在處理文字的時候有可能會遇到文字相似度計算或者找出文字近似表達的訴求,本篇文章是從實際操作的角度出發,手動訓練出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和doc2vec模型

從上面的座標系中我們可以看到詞語之間的距離,距離越近的詞語其相關性也就越高。

注: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()
複製程式碼

經過上面的處理以後我們就得到了簡體字的分詞檔案了。

動手實踐word2vec和doc2vec模型

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看到結果。

動手實踐word2vec和doc2vec模型

在此怕某些人疑惑,說明一下正常程式中應該存在的檔案。

四 不可不說的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檔案下面應該是這樣的。

動手實踐word2vec和doc2vec模型

我在訓練的時候一共花費了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等領域相關的原創優質技術文章:

看小編這麼辛苦,關注一個唄:)

動手實踐word2vec和doc2vec模型

相關文章