word2vec模型原理與實現


word2vec是Google在2013年開源的一款將詞表征為實數值向量的高效工具.

gensim包提供了word2vec的python接口.

word2vec采用了CBOW(Continuous Bag-Of-Words,連續詞袋模型)和Skip-Gram兩種模型.

模型原理

為了便於進行定量的分析,我們通常使用向量來代表我們研究的對象(如單詞)。常用的向量化形式有兩種:

  • one-hot編碼:一個詞用一個長度為詞典長度的向量表示。詞向量中僅一個元素為1其它均為0。
    這種方式的缺點在於向量無法反映對象之間的關系,且維度較多計算量較大。

  • 分布編碼: 該編碼將詞語映射為固定長度的向量, 即N維向量空間中的一點。
    理想狀況下,兩個對象越相似,它們詞向量的相似度也越高,空間中兩點的距離越近。

Word2Vec模型即是一種典型的分布編碼方式。

統計語言模型

N-gram模型

N-Gram模型是一種統計語言模型。簡單來講,統計語言模型是計算語料庫中某個句子出現概率的模型。

假設句子W是由T個單詞 $ w_1, w_2, w_3 … w_T $按照順序構成的,那么句子W出現的概率可以認為是T個單詞依次出現的聯合概率:

\[p(W) = p(w_1,w_2,…,w_T) = p(w_1)p(w_2 |w_1)p(w_3 |w_1^2),…p(w_T |w_1^T) \]

其中,\(w_i^j\)表示單詞 \(w_i, w_{i+1}, w_{i+2}, … w_j\) 組成的序列, \(p(w_2|w_1)\)表示在出現\(w_1\)的條件下,下一個單詞為\(w_2\)的條件概率。

那么, \(p(w_T|w_1^T)\)表示在出現序列\(w_1^T\)的條件下,下一個單詞為\(w_T\)的條件概率。

根據貝葉斯定理, 可以得到:

\[p(w_k | w_1^{k-1}) = \frac{p (w_1^k)}{p(w_1^{k-1})} \]

在句子較長的情況下,根據上面兩式計算P(W)計算量十分巨大。

根據經驗可知,一個詞出現的概率並非與前面所有詞都相關,距離越遠相關性越低。

因此,可以假設一個詞出現的概率只與前面N-1個詞有關。那么,這個問題變為了N-1階馬爾科夫模型。

對於語句\(W(w_1, w_2, w_3 … w_T)\),已知\(w_1, w_2, w_3 … w_{T-1}\),語句W出現的概率僅與\(w_T\)有關。

統計語言模型可以表示為在已知語句出現的概率的情況下,推算某個單詞w出現在語句的k位置的概率。

神經概率語言模型

神經概率語言模型沿用了N-Gram模型的核心觀點:語句出現的概率是語句中各單詞依次出現的聯合概率。

神經概率語言模型使用向量表示詞語,在已知語句W出現概率的情況下,預測最可能出現在k位置的單詞\(w_k\)

神經概率語言模型是一個三層的神經網絡模型。訓練樣本為單詞w上下文的詞向量,經過隱含層傳遞到輸出層,輸出層為單詞w的詞向量。

如圖所示:

我們可以隨機初始化詞典中所有單詞的詞向量,然后將語料庫中所有語句輸入網絡得到預測的詞向量,然后與庫中的詞向量對比修正單詞w的詞向量或者其上下文的詞向量。

經過充分迭代,最終得到可以較好地表示語義的詞向量。

Word2Vec

Word2Vec模型是Google公司在2013年開源的一種將詞語轉化為向量表示的模型。Word2Vec是由神經概率語言模型演進而來,它對神經概率語言模型做了重要改進,提高了計算效率。

Word2Vec模型有兩種主要的實現方式:連續詞袋 模型(Continuous Bag-of-Word Model, CBOW Model) 和 skip-gram_模型。

CBOW

連續詞袋模型(Continuous Bag-of-Word Model, CBOW)是一個三層神經網絡, 輸入已知上下文輸出對下個單詞的預測.

CBOW模型的第一層是輸入層, 輸入已知上下文的詞向量. 中間一層稱為線性隱含層, 它將所有輸入的詞向量累加.

第三層是一棵哈夫曼樹, 樹的的葉節點與語料庫中的單詞一一對應, 而樹的每個非葉節點是一個二分類器(一般是softmax感知機等), 樹的每個非葉節點都直接與隱含層相連.

將上下文的詞向量輸入CBOW模型, 由隱含層累加得到中間向量.將中間向量輸入哈夫曼樹的根節點, 根節點會將其分到左子樹或右子樹.

每個非葉節點都會對中間向量進行分類, 直到達到某個葉節點.該葉節點對應的單詞就是對下個單詞的預測.

首先根據預料庫建立詞匯表, 詞匯表中所有單詞擁有一個隨機的詞向量.我們從語料庫選擇一段文本進行訓練.

將單詞W的上下文的詞向量輸入CBOW, 由隱含層累加, 在第三層的哈夫曼樹中沿着某個特定的路徑到達某個葉節點, 從給出對單詞W的預測.

訓練過程中我們已經知道了單詞W, 根據W的哈夫曼編碼我們可以確定從根節點到葉節點的正確路徑, 也確定了路徑上所有分類器應該作出的預測.

我們采用梯度下降法調整輸入的詞向量, 使得實際路徑向正確路徑靠攏.在訓練結束后我們可以從詞匯表中得到每個單詞對應的詞向量.

Skip-gram

Skip-gram模型同樣是一個三層神經網絡. skip-gram模型的結構與CBOW模型正好相反,skip-gram模型輸入某個單詞輸出對它上下文詞向量的預測。

輸入一個單詞, 輸出對上下文的預測.

Skip-gram的核心同樣是一個哈夫曼樹, 每一個單詞從樹根開始到達葉節點可以預測出它上下文中的一個單詞.

對每個單詞進行N-1次迭代, 得到對它上下文中所有單詞的預測, 根據訓練數據調整詞向量得到足夠精確的結果.

模型實現

繼承python內置的collections.Counter編寫詞頻統計器WordCounter

實現哈夫曼樹HuffmanTree,關於構造哈夫曼樹的算法參考這里.

定義模型類:

class Word2Vec:
    def __init__(self, vec_len=15000, learn_rate=0.025, win_len=5):
        self.cutted_text_list = None
        self.vec_len = vec_len
        self.learn_rate = learn_rate
        self.win_len = win_len
        self.word_dict = None
        self.huffman = None

詞匯表word_dict是一個字典:

word_dict = {word: {word, freq, possibility, init_vector, huffman_code}, ...}

build_word_dict方法根據WordCounter建立詞匯表:

def build_word_dict(self, word_freq):
    # word_dict = {word: {word, freq, possibility, init_vector, huffman_code}, }
    word_dict = {}
    freq_list = [x[1] for x in word_freq]
    sum_count = sum(freq_list)
    for item in word_freq:
        temp_dict = dict(
            word=item[0],
            freq=item[1],
            possibility=item[1] / sum_count,
            vector=np.random.random([1, self.vec_len]),
            Huffman=None
        )
        word_dict[item[0]] = temp_dict
    self.word_dict = word_dict

train方法控制訓練流程, 將單詞及其上下文交給CBOW方法或SkipGram方法進行具體訓練:

def train(self, word_list, model='cbow', limit=100, ignore=0):
    # build word_dict and huffman tree
    if self.huffman is None:
        if self.word_dict is None:
            counter = WordCounter(word_list)
            self.build_word_dict(counter.count_res.larger_than(ignore))
            self.cutted_text_list = counter.word_list
        self.huffman = HuffmanTree(self.word_dict, vec_len=self.vec_len)
    # get method
    if model == 'cbow':
        method = self.CBOW
    else:
        method = self.SkipGram
    # train word vector
    before = (self.win_len - 1) >> 1
    after = self.win_len - 1 - before
    total = len(self.cutted_text_list)
    count = 0
    for epoch in range(limit):
        for line in self.cutted_text_list:
            line_len = len(line)
            for i in range(line_len):
                word = line[i]
                if is_stop_word(word):
                    continue
                context = line[max(0, i - before):i] + line[i + 1:min(line_len, i + after + 1)]
                method(word, context)
            count += 1

CBOW方法對輸入變量累加求和, 交由along_huffman方法進行一次預測並得到誤差, 最后根據誤差更新詞向量:

def CBOW(self, word, context):
    if not word in self.word_dict:
        return
    # get sum of all context words' vector
    word_code = self.word_dict[word]['code']
    gram_vector_sum = np.zeros([1, self.vec_len])
    for i in range(len(context))[::-1]:
        context_gram = context[i]  # a word from context
        if context_gram in self.word_dict:
            gram_vector_sum += self.word_dict[context_gram]['vector']
        else:
            context.pop(i)
    if len(context) == 0:
        return
    # update huffman
    error = self.along_huffman(word_code, gram_vector_sum, self.huffman.root)
    # modify word vector
    for context_gram in context:
        self.word_dict[context_gram]['vector'] += error
        self.word_dict[context_gram]['vector'] = preprocessing.normalize(self.word_dict[context_gram]['vector'])

SkipGram方法使用Skip-gram模型進行訓練, 它進行多次迭代:

def SkipGram(self, word, context):
    if not word in self.word_dict:
        return
    word_vector = self.word_dict[word]['vector']
    for i in range(len(context))[::-1]:
        if not context[i] in self.word_dict:
            context.pop(i)
    if len(context) == 0:
        return
    for u in context:
        u_huffman = self.word_dict[u]['code']
        error = self.along_huffman(u_huffman, word_vector, self.huffman.root)
        self.word_dict[word]['vector'] += error
        self.word_dict[word]['vector'] = preprocessing.normalize(self.word_dict[word]['vector'])

along_huffman方法進行一次預測並得到誤差:

def along_huffman(self, word_code, input_vector, root):
    node = root
    error = np.zeros([1, self.vec_len])
    for level in range(len(word_code)):
        branch = word_code[level]
        p = sigmoid(input_vector.dot(node.value.T))
        grad = self.learn_rate * (1 - int(branch) - p)
        error += grad * node.value
        node.value += grad * input_vector
        node.value = preprocessing.normalize(node.value)
        if branch == '0':
            node = node.right
        else:
            node = node.left
    return error

從詞匯表中取出詞向量:

def __getitem__(self, word):
    if not word in self.word_dict:
        return None
    return self.word_dict[word]['vector']

> wv = Word2Vec(vec_len=50)
> wv.train(data, model='cbow')
> wv['into']
array(1, 50)

完整源代碼參見Word2Vec

gensim中的word2vec封裝

gensim是著名的向量空間模型包, 使用pip安裝:

pip gensim

gensim中封裝了包括了word2vec, doc2vec等模型:

from gensim.models.word2vec import Word2Vec

profiler = Word2Vec()

首先根據語料庫構建詞匯表:

profiler.build_vocab(word_source)

數據源word_source是一個句子組成的序列:

word_source = [
    ['I', 'love', 'natural', 'language', 'processing'],
    ['word2vec', 'is', 'a', 'useful', 'model']
]

因為內存有限, 使用list作為數據源通常只能保存很少數據. word2vec也可以使用generator作為數據源.

word2vec需要再次掃描數據集進行訓練:

self.profiler.train(word_source)

word2vec支持在線訓練(resume training, 又稱繼續訓練),即進行過訓練的模型可以再次訓練, 進一步提高精度.

從訓練好的模型中獲得詞向量:

>>>profiler[word]
array([-0.00449447, -0.00310097,  0.02421786, ...], dtype=float32)

word2vec可以計算單詞相似度:

>>>profiler.similarity('woman', 'man')
0.73723527

將訓練好的模型存儲為文件:

profiler.save(filename)

讀取模型文件:

profiler = Word2Vec.load(filename)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM