深入淺出理解word2vec模型 (理論與原始碼分析)

語言: CN / TW / HK

深入淺出理解word2vec模型 (理論與原始碼分析)


對於演算法工程師來說, Word2Vec 可以說是大家耳熟能詳的一種詞向量計算演算法,Goole在2013年一開源該演算法就引起了工業界與學術界的廣泛關注。

一般來說,word2vec 是基於序列中隔得越近的word越相似的基礎假設來訓練的模型, 模型的損失函式也是基於該原理進行設計的。 最初word2vec是用來訓練得到詞向量,後面把它引申用於進行序列類(seq/list)資料中各個item ( 後面把word,node,item 等序列元素統稱為 item ) 的embeding生成。

Google的論文以及廣大演算法工程師們已經從大量的實踐中證明:兩個item間的向量距離能夠反映它們之間的關係(通常通過L2距離或餘弦相似度得到), 並且兩個item之間的語義關係計算可以用 他們的embeding 計算來代替。論文中提出的一個有意思的例子是:

Embedding(woman)=Embedding(man)+[Embedding(queen)-Embedding(king)]

embeding 的這個特性為可量化和可計算的item關係打開了新世界的大門,具有非常重要的參考意義。


(1) Word2Vec基礎

我們都知道,Word2Vec最初是作為語言模型提出的,實際是一種從大量文字語料中以無監督方式學習語義知識的淺層神經網路模型,包括Skip-Gram 和 Continuous Bag Of Words(簡稱CBOW)兩種網路結構。


(1.1)Skip-Gram和CBOW 結構

Skip-Gram和CBOW都可以表示成由輸入層(Input) 、對映層(Projection) 和 輸出層(Output)組成的神經網路。其中 Skip-Gram 的目標是根據當前詞來預測上下文中各詞的生成概率,而 CBOW 的目標是根據上下文出現的詞來預測當前詞的生成概率。 網路結構圖如下所示:

上圖中,w(t) 為當前所關注的詞,而w(t-2),w(t-1),w(t+1),w(t+2)為上下文出現的單詞。這裡前後滑動視窗的大小均設定為2。

輸入與輸出均是語料庫資料自身,所以底層本質上Word2Vec是無監督模型。

輸入層 每個詞由獨熱編碼方式表示,即所有詞均表示成一個N維向量,其中N為詞彙表中單詞的總數。在向量中,每個詞都將與之對應的維度置為1,其餘維度均為0。

對映層(也稱隱含層) 中,K個隱含單元的取值可以由N維輸入向量以及隱含單元之間的N*K維權重矩陣計算得到。在CBOW中,還需要將各個輸入詞所計算出的隱含單元求和,這裡是得到各個 輸入詞embeding 之後進行了sum pooling 操作(當然也可以選擇別的pooling方式,如 Average), 得到一個K維隱藏向量。Skip-Gram 這裡則是僅僅得到當前關注詞的embeding, 無需pooling操作 。

同理,輸出層 向量的值可以通過隱含層向量(K維)以及連線隱含層和輸出層之間的KN維權重矩陣計算得到。輸出層也是一個N維向量,每維與詞彙表中的每個單詞對應。這裡,CBOW和 Skip-Gram 均是用一個 1 * K維的隱含層資料(embeding)和 K * N 維度的資料計算得到 1 * N 維度的logit值, 最後對輸出層的 N維度向量(N維度logit值)應用SoftMax啟用函式,可以計算出每個單詞的生成概率。然後再和每個單詞是0或1的標籤計算損失。


(1.2) Skip-Gram和CBOW 優劣點分析

關於 Skip-Gram和 CBOW 模型的對比分析,知乎上的卡門同學分析的非常具有特色,具有很強的參考意義。

從上面圖中,我們也可以看出:

Skip-Gram 是用中心詞來預測周圍的詞,在skip-gram中,會利用周圍的詞的預測結果情況,使用GradientDecent來不斷的調整中心詞的詞向量,最終所有的文字遍歷完畢之後,也就得到了文字所有詞的詞向量。但是在skip-gram當中,每個詞都要收到周圍的詞的影響,每個詞在作為中心詞的時候,都要進行K次的預測、調整。因此, 當資料量較少,或者詞為生僻詞出現次數較少時, 這種多次的調整會使得詞向量相對的更加準確。

CBOW 是用上下文詞預測中心詞,從而得到中心詞的預測結果情況,使用GradientDesent方法不斷去優化調整週圍詞的向量。當訓練完成之後,每個詞都會作為中心詞,把周圍詞的embeding進行了調整,這樣也就獲得了整個文本里所有詞的詞向量。 要注意的是, cbow的對周圍詞的調整是統一的:求出的gradient的值會同樣的作用到每個周圍詞的詞向量當中去。儘管cbow從另外一個角度來說,某個詞也是會受到多次周圍詞的影響(多次將其包含在內的視窗移動),進行詞向量的跳幀,但是他的調整是跟周圍的詞一起調整的,grad的值會平均分到該詞上, 相當於該生僻詞沒有收到專門的訓練,它只是沾了周圍詞的光而已。

因此,從更通俗的角度來說:

在Skip-Gram裡面,每個詞在作為中心詞的時候,實際上是 1個學生 VS K個老師,K個老師(周圍詞)都會對學生(中心詞)進行“專業”的訓練,這樣學生(中心詞)的“能力”(向量結果)相對就會紮實(準確)一些,但是這樣肯定會使用更長的時間。CBOW是 1個老師 VS K個學生,K個學生(周圍詞)都會從老師(中心詞)那裡學習知識,但是老師(中心詞)是一視同仁的,教給大家的一樣的知識。

所以,一般來說 CBOW比Skip-Gram訓練速度快,訓練過程更加穩定,原因是CBOW使用上下文的方式進行訓練,每個訓練step會見到更多樣本。而在生僻字(出現頻率低的字)處理上,skip-gram比CBOW效果更好,學習的詞向量更細緻,原因就如上面分析: CBOW 是公共課,Skip-gram 是私教


(2) Skip-Gram模型詳解

書接上文, 在我們現實中的資料集中絕大部份的資料都是高維稀疏的資料集,大量的實踐證明Skip-Gram確實效果更好,所以這裡以 Skip-Gram為框架講解Word2Vec模型的細節。


(2.1) 損失函式說明

如上文圖中右邊所示,Skip-Gram的學習任務是用中間的詞與預測周圍的詞,訓練得到詞的embedding便可以用於下游任務。Skip-Gram 的形式化定義: 給定單詞序列 W1, W2, W3 ... Wt, 選取一個長度為2m+1(目標詞前後各選取m個詞)的滑動視窗,將滑動視窗從左到右華東區,每移動一次,視窗中的片語就形成了一個訓練樣本。

我們知道, 概率是用於已知模型引數,預測接下來觀測到樣本的結果; 而似然性用語已知某些觀測所得到的結果,對有關事務的性質引數進行估計。

而在 Skip-Gram中每個詞 Wt 都決定了相鄰的詞 Wt+j , 在觀測序列已定的情況下,我們可以基於極大似然估計的方法,希望所有樣本的條件概率 P( Wt+j / Wt ) 之積最大,這裡使用對數概率。因此Skip-Gram的目標函式是最大化平均的對數概率,即:

其中 m 是視窗大小,m越大則樣本數越多,預測準確性越高,但同時訓練時間也越長。當然我們還會在上述公司前面乘以 負 1/ T 以方便現有最優化方法的梯度優化。

作為一個多分類問題,Skip-Gram 定義概率 P( Wt+j / Wt ) 的最直接的方法是使用SoftMax函式。 假設我們用一個向量 Vw表示詞w, 用詞之間的內積距離VitVj表示兩詞語義的接近程度。則條件概率 P( Wt+j / Wt ) 可以用下式給出:

其中,Wo代表Wt+j , 被稱為輸出詞; Wi 代表 Wt, 被稱為輸入詞。 注意在上面的公式中,Wo和Wi 並不在一個向量空間內,Vwo 和Vwi 分別是詞W的輸出向量表達和輸入向量表達。

在上文裡我們曾經說過,從輸入層到對映層的維度為 N * K,而從對映層到輸出層的維度為 K * N。 這也就是說每個單詞均有兩套向量表達方式。實踐證明:每個單詞用兩套向量表達的方式通常效果比單套向量表達方式的效果好,因為兩套向量表達的方式應用到下游任務時可以去取兩個embeding的平均值


(2.2) 訓練過程優化

需要注意,我們上面說的輸入層和輸出層的維度 N 是詞表中單詞的總數,在現實中通常都非常大,千萬甚至上億的量級都是非常常見。但事實上,完全遵循原始的Word2Vec多分類結構的訓練方法並不可行。

假設語料庫中詞的數量為1KW,則意味著輸出層神經元有1KW個,在每次迭代更新到隱藏層神經元的權重時,都需要計算所有字典中1KW個詞的預測誤差,這在實際計算 的時候是不切實際的。

Word2vec 提出了2種方法解決該問題,一種是層次化的 Hierarchical Softmax, 另一種是 負取樣(Negative Sampling) 。

層次softmax 基本思想是將複雜的歸一化概率分解為一系列條件概率乘積的形式,每一層條件概率對應一個二分類問題,通過邏輯迴歸函式可以去擬合。對v個詞的概率歸一化問題就轉化成了對logv個詞的概率擬合問題。Hierarchical softmax通過構造一棵二叉樹將目標概率的計算複雜度從最初的V降低到了logV的量級。但是卻增加了詞與詞之間的耦合性。比如一個word出現的條件概率的變化會影響到其路徑上所有非葉子節點的概率變化。間接地對其他word出現的條件概率帶來影響, 同時層次softmax 也因為實現比較困難,效率較低且並沒有比負取樣更好的效果,所以在現實中使用的並不多。

這裡我們主要說明 負取樣 (Negative Sampling ) 的方式。相比於原來需要計算所有字典中所有詞的預測誤差,負取樣的方法只需要對取樣出的幾個負樣本計算預測誤差。再次情況下,word2vec 模型的優化目標從一個多分類問題退化成了一個近似二分類問題。

其優化目標定義為:

其中,Pn(w) 是噪聲分佈,取樣生成k個負樣本,任務變成從噪聲中區分出目標單詞 Wo, 整體的計算量與K成線性關係,K在經驗上去2~20即可,遠小於 Hierarchical Softmax 所需要的 log(W) 詞計算。

Pn(w) 的經驗取值是一元分佈的四分之三次方,效果遠超簡單的一元分佈或均勻分佈。


(3) 實踐經驗

類似於 Word2Vec根據單詞序列資料訓練Embedding,我們也可以把使用者行為點選過的item序列資料 餵給Word2Vec 演算法,訓練得到item 的Embeding , 這種方式也稱為 item2Vec


(3.1) 靈活構建序列

在使用Word2Vec學習的過程中,喂入模型的item序列的構建是非常重要的,我們可以在item ID序列中加入item的類目等屬性資訊來構建序列。我們構建的序列並不一定緊緊只有一個類別的資料,例如這個序列也可以是: userid, ip,sn, email , 但是在某些情況下,為了更好的建模使用者和各個屬性的關係,我們可以構建這樣的序列: userid, ip,userid,sn,userid,email。

構建序列的方法是非常靈活的,我們根據自己的理解和業務需要動態調整即可。

同樣,在基於隨機遊走的 Graph Embeding 演算法中,我們可以在同構圖上使用深度遊走( deepwalk ) 的方法,或則在異構圖上使用元路徑 (meta path) 的方法得到一些 item 的遊走序列,然後把這些序列喂入 skip-gram 模型中, 也可以得到不錯的效果。


(3.2) Airbnb的word2vec建模實踐

這裡要重點介紹下 Airbnb在2018年 釋出在KDD 的最佳論文 Real-time Personalization using Embeddings for Search Ranking at Airbnb

該論文中介紹了embedding在 Airbnb 愛彼迎房源搜尋排序和實時個性化推薦中的實踐。他們使用了 listing embeddings(房源嵌入)和 使用者點選行為來學習使用者的短期興趣,用user-type & listing type embeddings和使用者預定行為來學習使用者的長期興趣,並且這兩種方法都成功上線,用於實時個性化生成推薦。

其論文中有很多值得參考的點,這裡簡單列舉下:

(1) Airbnb 利用session內點選資料構建了序列建模使用者的短期興趣。

(2) 將預定房源引入目標函式,即不管這個被預定房源在不在word2vec的滑動視窗中都架設這個被預定房源與滑動視窗的中心房源相關,即相當於引入了一個全域性上下文到目標函式中。

(3) 基於某些屬性規則做相似使用者和相似房源的聚合,使用者和房源被定義在同一個向量空間中,用word2vec負取樣的方法訓練,可以同時得到使用者和房源的Embedding,二者之間的Cos相似度代表了使用者對某些房源的長期興趣偏好。

(4) 把搜尋詞和房源置於同一向量空間進行Embeding。

(5) 與中心房源同一市場的房源集合中進行隨機取樣作為負樣本,可以更好發現同一市場內部房源的差異性。

才開始作者在看論文的時候想自己去復現一下論文,結果發現網路上也搜不到論文中介紹的如何自定義損失函式的方法,知乎上聯絡Airbnb的官方賬號也沒有收到回覆,悲催。


(3.3) 一種可行的實踐

最後,作者經過苦苦摸索,終於找到了自己的解決方法,下面進行簡單的介紹:

首先要想清楚的一個點是:我們喂入模型中的序列其實就是我們訓練Word2Vec模型的樣本。觀察上面的損失函式公式我們發覺,損失函式其實就是基於我們開篇所介紹的假設:序列中隔得越近的word越相似 。 然後,我們用設計好的相應的資料去訓練模型,是不是意味著我們就修改了模型的訓練目標呢。

例如:上面第5點,論文中介紹說,在目標函式中引入了同一地區的負樣本集合加入到損失函式,那我們在構建樣本pair的時候,是不是可以讓負樣本的選擇符合論文中說的方式,然後構建成 (pos sample, same place neg sample, -1 ) 這樣的形式的樣本新增到原來的樣本集中呢。

這裡我想說明的一點就是:我們選擇什麼樣的樣本輸入模型,就等價於在模型訓練的損失函式中加入了什麼型別的損失。明著看起來是新增負樣本,沒有修改模型,但是本質上就是修改了損失函式。

基於此,對於一切的序列資料,我們都可以選擇Word2Vec(推薦Skip-Gram)進行序列item的embeding學習,只是我們要重點關注訓練樣本序列的構建,因為這涉及到我們模型最終的訓練目標。

我不知道這一點我說清楚了沒有,但是希望你能理解我大概要表示的意思,如有任何問題,歡迎聯絡我進行討論哈 ~


(4)程式碼時刻

talk is cheap , show me the code !!!

下面的程式碼是使用tensorflow2.0,採用了tf.keras 中階API來構建模型結構,其中包括瞭如何構建word2vec需要的pair資料,如何在一元表上完成負取樣,如何匯出word對應的embeding 以及如何提高模型的訓練速度。本程式碼具有詳細的註釋以及實現的思路說明,具有極高的參考價值與可重用性,有問題歡迎討論~

該工程完整程式碼,可以去 演算法全棧之路 公眾號回覆 word2vec原始碼 下載。

```python @ 歡迎關注作者公眾號 演算法全棧之路

!/usr/bin/env python

-- coding: utf-8 --

from tensorflow.python.ops import array_ops from tensorflow.python.util import nest import tensorflow as tf from tensorflow.keras.layers import * from tensorflow.keras.models import Model from tensorflow.keras.optimizers import Adam

import tensorflow.keras.backend as backend from tensorflow.python.ops import control_flow_ops

from collections import defaultdict import numpy as np import tqdm import math import random

class SaveEmbPerEpoch(tf.keras.callbacks.Callback):

def set_emb(self, emb, vocab, inverse_vocab, word_embedding_file):
    self.word_embedding_file = word_embedding_file
    self.words_embedding_in = emb
    self.vocabulary = vocab
    self.inverse_vocab = inverse_vocab

def on_epoch_end(self, epoch, logs=None):
    with open(self.word_embedding_file + '.{}'.format(epoch), 'w') as f:
        weights = self.words_embedding_in.get_weights()[0]
        for i in range(len(self.vocabulary)):
            emb = weights[i, :]
            line = '{} {}\n'.format(
                self.inverse_vocab[i],
                ' '.join([str(x) for x in emb])
            )
            f.write(line)

wget http://mattmahoney.net/dc/text8.zip -O text8.gz

gzip -d text8.gz -f

train_file = './train.txt'

class Word2Vec(object): def init(self, train_file, sample=1e-4, embedding_dim=200):

    # 訓練檔案
    self.train_file = train_file
    # 最大句子長度
    self.MAX_SENTENCE_LENGTH = 1024
    # 過濾掉詞語頻率低於count 的詞語在count中
    self.min_count = 5
    # 子取樣權重
    self.subsampling_power = 0.75
    # 取樣率
    self.sample = sample
    # 根據訓練資料產生的詞典 word freq 字典儲存起來
    self.save_vocab = './vocab.txt'
    # 視窗大熊啊
    self.window = 5
    # 每個中心詞,選擇一個上下文詞構建一個正樣本,就隨機取樣選擇幾個負樣本.
    self.negative = 5
    # 從原始碼中還是從論文中的構建樣本方式 原始碼[context_word, word] or 論文[word, context_word]
    self.skip_gram_by_src = True
    # 維度
    self.embedding_dim = embedding_dim
    # 儲存embeding 的檔案
    self.word_embedding_file = 'word_embedding.txt'
    self.tfrecord_file = 'word_pairs.tfrecord'
    self.train_file = "./train.txt"

    self.vocabulary = None
    self.next_random = 1
    # 詞表最大尺寸,一元模型表大小
    self.table_size = 10 ** 8
    self.batch_size = 256
    self.epochs = 1

    # 是否生成tf_redocd資料參與訓練
    self.gen_tfrecord = True

    # build vocabulary
    print('build vocabulary ...')
    self.build_vocabulary()

    # build dataset
    print('transfer data to tfrecord ...')
    # 是否生成 tf_record格式的資料
    if self.gen_tfrecord:
        self.data_to_tfrecord()

    # 使用from_generator,速度非常慢,遍歷100個句子需要50s
    # self.dataset = tf.data.Dataset.from_generator(
    #     self.train_data_generator,
    #     output_types=(tf.int32, tf.int32),
    #     output_shapes=((2,), (),)
    # ).batch(self.batch_size).prefetch(1)

    print('parse tfrecord data to dataset ...')
    # 使用tfrecord後,100個句子需要6s
    self.dataset = self.make_dataset()

    # build model
    print('build model ...')
    self.build_model()

# 構建訓練資料集合,也就是解析tfrecord資料
def make_dataset(self):
    # 解析單個樣本
    def parse_tfrecord(record):
        features = tf.io.parse_single_example(
            record,
            features={
                'pair': tf.io.FixedLenFeature([2], tf.int64),
                'label': tf.io.FixedLenFeature([1], tf.float32)
            })
        label = features['label']
        pair = features['pair']
        return pair, label

    # 讀入tfrecord file
    dataset = tf.data.TFRecordDataset(self.tfrecord_file) \
        .map(parse_tfrecord, num_parallel_calls=8) \
        .batch(self.batch_size).prefetch(self.batch_size)
    return dataset

# 輸入 word ,取樣率
# 構建一元模型表,進行高效的負取樣
# 概率大小和陣列寬度保持了一致性,在陣列上隨機取樣,就是按照概率分層抽樣
def build_unigram_table(self, word_prob):
    # 構建unigram 表,一元表
    self.table = [0] * self.table_size
    word_index = 1
    # 用當前詞語index的取樣概率當前詞語的長度
    # 初始化當前長度
    cur_length = word_prob[word_index]
    for a in range(len(self.table)):
        # 對錶中每一個元素,找到該下表對應詞語的index,也就是該詞語
        # 每個詞語對應一個下標,不滿足下面那個判斷條件的時候,當前下標放的元素依然是word_index
        self.table[a] = word_index
        # 當滿足這個條件的時候,就需要進一步更新下標對應的值了。
        # 保持下標占比a 和概率佔比cur_length處於一致的空間,不一致的時候就修改放的元素。
        # 佔比比較
        if a / len(self.table) > cur_length:
            # 下一位放word_index+1
            word_index += 1
            # cur_len 構建了累計分佈函式
            cur_length += word_prob[word_index]
        # Misra-Gries演算法
        # 使用Misra-Gries演算法,當詞彙字典的大小到達極限值時,訪問詞典的每一個key,將其value值的大小-1,
        # 當某個key的value為0時將其移除字典,直到能夠加入新的key.
        if word_index >= len(self.vocabulary):
            word_index -= 1

def build_vocabulary(self):

    # 構建詞頻字典
    word_freqs = defaultdict(int)
    # 迴圈讀取訓練資料,得到某一行的各個單詞
    for tokens in self.data_gen():
        # tokens 裡得到某一行的各個單詞
        for token in tokens:
            word_freqs[token] += 1
    # 低頻過濾
    word_freqs = {word: freq for word, freq in word_freqs.items() \
                  if freq >= self.min_count}

    # 按照詞語頻率降序構建字典 {word :index },index 從1開始
    self.vocabulary = {word: index + 1 for (index, (word, freq)) in enumerate(
        sorted(word_freqs.items(), key=lambda x: x[1], reverse=True))}

    # index 0 特殊處理
    self.vocabulary['</s>'] = 0
    # 倒排表 index,word
    self.inverse_vocab = {index: token for token, index in self.vocabulary.items()}

    # save vocab
    with open(self.save_vocab, 'w') as f:
        for i in range(len(self.vocabulary)):
            word = self.inverse_vocab[i]
            if i > 0:
                freq = word_freqs[word]
            else:
                freq = 0
            f.write(f"{word} {freq}\n")

    # 負取樣的取樣概率,f(w)^(3/4)/Z
    # 取樣率計算的分母, 歸一化求和,頻率分佈的 3/4
    train_words_ns = sum([freq ** (self.subsampling_power) for freq in word_freqs.values()])

    # 得到每一個單詞index對應的取樣頻率
    self.ns_word_prob = {self.vocabulary[word]: (freq ** self.subsampling_power) / train_words_ns for word, freq in
                         word_freqs.items()}

    # 構建一元模型,在上面隨機取樣就可以做到word分佈上的分層抽樣
    self.build_unigram_table(self.ns_word_prob)

    #         self.unigrams_prob = [0]
    #         for i in range(1, len(self.vocabulary)):
    #             # print(inverse_vocab[i])
    #             self.unigrams_prob.append(self.ns_word_prob[i])

    # (sqrt(vocab[word].cn / (sample * train_words)) + 1) * (sample * train_words) / vocab[word].cn;
    # subsampling
    # 如果取樣率大於0
    if self.sample > 0:
        # 所有頻率的和
        train_words = sum([freq for freq in word_freqs.values()])
        # 根據每個詞語的頻率得到drop ratio
        self.subsampling_drop_ratio = {
            word: (math.sqrt(freq / (self.sample * train_words)) + 1) * (self.sample * train_words) / freq \
            for word, freq in word_freqs.items()
        }

# 構建 word2vec 模型
def build_model(self):
    vocab_size = len(self.vocabulary)
    # embedding_dim = 100
    inputs = Input(shape=(2,))
    target = inputs[:, 0:1]
    context = inputs[:, 1:2]
    self.words_embedding_in = tf.keras.layers.Embedding(
        vocab_size,
        self.embedding_dim,
        input_length=1,
        name="word_embedding_in"
    )
    self.words_embedding_out = tf.keras.layers.Embedding(
        vocab_size,
        self.embedding_dim,
        input_length=1,
        name="word_embedding_out"
    )
    word_emb = self.words_embedding_in(target)  # batch_size,1,embeing_size
    context_emb = self.words_embedding_out(context)
    dots = tf.keras.layers.Dot(axes=(2, 2))([word_emb, context_emb])
    outputs = tf.keras.layers.Flatten()(dots)
    self.model = Model(inputs, outputs)

    self.model.compile(
        optimizer='adam',
        # loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
        # loss=tf.keras.losses.binary_crossentropy(from_logits=True),
        loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
        metrics=['accuracy'])

# 模型訓練
def train(self):
    # tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="logs")
    my_callback = SaveEmbPerEpoch()
    my_callback.set_emb(self.words_embedding_in,
                        self.vocabulary,
                        self.inverse_vocab,
                        self.word_embedding_file)
    self.model.fit(word2vec.dataset, epochs=self.epochs, callbacks=[my_callback])

def save_word_embeddings(self):
    with open(self.word_embedding_file, 'w') as f:
        f.write('{} {}\n'.format(len(self.vocabulary), self.embedding_dim))
        weights = self.words_embedding_in.get_weights()[0]
        for i in range(len(self.vocabulary)):
            emb = weights[i, :]
            line = '{} {}\n'.format(
                self.inverse_vocab[i],
                ','.join([str(x) for x in emb])
            )
            f.write(line)

def data_to_tfrecord(self):
    # 寫入到 tf_record file
    with tf.io.TFRecordWriter(self.tfrecord_file) as writer:
        # 得到進度條
        for item in tqdm.tqdm(self.train_data_gen()):
            # list  [context_word,word], 1.0/0.0
            # [word, context_word]
            pair, label = item
            feature = {
                'pair': tf.train.Feature(int64_list=tf.train.Int64List(value=pair)),
                'label': tf.train.Feature(float_list=tf.train.FloatList(value=[label]))
            }
            example = tf.train.Example(features=tf.train.Features(feature=feature))
            writer.write(example.SerializeToString())

# 生成訓練資料,生成完成之後傳入下一個方法生成tf_record 資料
def train_data_gen(self):
    cnt = 0
    sample_list = []
    # 得到上面取樣過的所有的多行訓練的單詞 token_index
    for tokens_index in self.tokens_gen():
        # print(len(tokens_index), cnt)

        # 當前行的token 列表
        for i in range(len(tokens_index)):
            # print('cnt={}, i={}'.format(cnt, i))
            # 總共已經處理多少個單詞了
            cnt += 1
            # 當前序列已經處理了當前第 i 個了。
            # 當前單詞的 index, 中心詞
            word = tokens_index[i]
            # 生成一個視窗大小之內的隨機數
            b = random.randint(0, self.window - 1)
            # 中心詞前取幾個,後取幾個的分界線
            window_t = self.window - b

            # c為上下文座標
            #                 context_ = [tokens_index[c] for c in range(i - window_t, i + window_t + 1) \
            #                                if c >=0 and c <=len(tokens_index) and c != i]
            #                 print('window_t = {}, contexts words={}'.format(window_t, context_))

            # 中心詞為i ,i前取 i - window_t個,i 後取 i + window_t + 1個。
            for c in range(i - window_t, i + window_t + 1):
                # 越界的和中心詞跳過。
                if c < 0 or c >= len(tokens_index) or c == i:
                    continue
                # 當前列表中的當前中心詞的上下文
                #
                context_word = tokens_index[c]
                # print('c={}, context_word={}'.format(c, context_word))

                # 構造副樣本
                # 採用np.random.choice的方法,10句話要5分鐘。
                # 採用tf.random.fixed_unigram_candidate_sampler,10句話要7分鐘。
                # 所以最後還得用hash的方法搞。10句話基本不需要時間
                # 但是改成dataset後,仍然需要5s
                #                     neg_indexs = [np.random.choice(
                #                         list(self.ns_word_prob.keys()),
                #                         p=list(self.ns_word_prob.values())) for _ in range(self.negative)]
                #

                # 做self.negative 次負取樣
                # 每個中心詞,選擇一個上下文詞構建一個正樣本,就隨機取樣選擇幾個負樣本.
                neg_indexs = [self.table[random.randint(0, len(self.table) - 1)] \
                              for _ in range(self.negative)]

                # 呼叫當前函式就返回一個迭代值,下次迭代時,程式碼從 yield 當前的下一條語句繼續執行,
                # 而函式的本地變數看起來和上次中斷執行前是完全一樣的,於是函式繼續執行,直到再次遇到 yield。
                if self.skip_gram_by_src:
                    # 從原始碼中還是從論文中的構建樣本方式 原始碼[context_word, word] or 論文[word, context_word]
                    # 返回正樣本
                    sample_list.append(([context_word, word], 1.0))

                    # 遍歷負取樣樣本
                    for negative_word in neg_indexs:
                        # 如果負取樣的詞不等於當前詞
                        if negative_word != word:
                            # 返回一組負樣本,替換掉中心詞語
                            sample_list.append(([context_word, negative_word], 0.0))
                else:
                    # 和上面的唯一性區別就是
                    sample_list.append(([context_word, word], 1.0))
                    for negative_word in neg_indexs:
                        if negative_word != word:
                            sample_list.append(([context_word, negative_word], 0.0))

    return sample_list

# 返回 token_index list ,訓練的單詞
def tokens_gen(self):
    cnt = 0
    lines_tokens_list = []
    all_tokens_count = 0
    # 讀入原始訓練資料,得到所有行的資料
    for tokens in self.data_gen():
        # 當前行
        tokens_index = []
        for token in tokens:
            if token not in self.vocabulary:
                continue
            if self.sample > 0:
                # 如果需要進行取樣
                # 得到word,drop_ratio概率,大於這個概率就丟棄
                if np.random.uniform(0, 1) > self.subsampling_drop_ratio[token]:
                    # if self.subsampling_drop_ratio[token] < self.w2v_random():
                    continue
            # 新增該訓練詞語的索引
            tokens_index.append(self.vocabulary[token])
            all_tokens_count += 1
        # if cnt == 10:
        #     return None
        cnt += 1
        lines_tokens_list.append(tokens_index)

    print("lines_tokens_list line len :" + str(cnt))
    print("lines_tokens_list all tokens  :" + str(all_tokens_count))

    return lines_tokens_list

def data_generator_from_memery(self):
    data = open(train_file).readlines()[0].split(' ')
    cur_tokens = []
    index = 0
    while index + 100 < len(data):
        yield data[index: index + 100]
        index += 100
    yield data[index:]

    # for i in range(len(data)):
    #     cur_tokens.append(data[i])
    #     if i % 100 == 0:
    #         yield cur_tokens
    #         cur_tokens = []

# 資料生成方法
def data_gen(self):
    raw_data_list = []
    prev = ''
    # 讀取訓練資料檔案
    with open(train_file) as f:
        # 死迴圈去讀
        while True:
            # 單詞讀取最大句子長度
            buffer = f.read(self.MAX_SENTENCE_LENGTH)
            if not buffer:
                break
            # print('|{}|'.format(buffer))
            # 把句子分割成各行
            lines = (prev + buffer).split('\n')
            # print(len(lines))
            for idx, line in enumerate(lines):
                # 處理當前行
                # 分成一個個詞
                tokens = line.split(' ')
                if idx == len(lines) - 1:
                    # 最後一行
                    cur_tokens = [x for x in tokens[:-1] if x]
                    # 把當前 MAX_SENTENCE_LENGTH 最後一個詞儲存起來, 和下一次讀取的時候進行拼接
                    prev = tokens[-1]
                else:
                    # 返回當前行的各個詞語
                    cur_tokens = [x for x in tokens if x]

                raw_data_list.append(cur_tokens)

    print("raw_data_list length:" + str(len(raw_data_list)))
    return raw_data_list

if name == "main": print(tf.version) word2vec = Word2Vec(train_file, sample=1e-4) word2vec.train()

```

到這裡,深入淺出理解word2vec模型理論與原始碼分析就完成了,歡迎留言交流 ~


碼字不易,覺得有收穫就點贊、分享、再看三連吧~

歡迎掃碼關注作者的公眾號: 演算法全棧之路