1. 程式人生 > >Word2Vec原理及代碼

Word2Vec原理及代碼

path 神經元 容易 num pyplot -h 改變 exist 除了

一、分布式詞表示(Distributed Represantation)

  Word2Vec 是 Google 於 2013 年開源推出的一個用於獲取 word vector 的工具包,具體思想可由Tomas Mikolov的兩篇論文一探究竟。此文是我對Word2Vec學習的總結。分布式詞表示使用低維、稠密、連續的向量來表示詞。它們通過“有指導”的方法直接學習詞向量,也稱為詞嵌入(Word Embedding)。它的發展歷程由最初的LSA、LDA、NNLM發展到後來的諸如RNNLM、Word2Vec、GloVe,以及現在最新的ELMo和BERT。

  我們知道,最簡單的一種詞表示方式是One-Hot表示,它的優點是很容易表示出不同的詞,但是缺點也是巨大的:一、所有詞向量正交,即無法找到詞與詞之間的關系。二、當有非常大的數據量時,向量的維度會因此擴大而造成維度災難。如下所示,如果用One-Hot方式來表示自然界生物,自然界的動物種類數以百萬記,基本不可能用這種方式來表示動物類別,並且也無法表示諸如鯉魚吃河蝦,老虎吃天鵝,鱷魚和天鵝都會下蛋的關系。

    鱷魚 [1,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,0,1,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,0,0,0,0,1]   分布式詞則可以用較低維度的矩陣去表示一個詞,並且“賦予”矩陣各行列有各自的意義,詞與詞之間的相似度也可以通過矩陣運算來求得。


技術分享圖片

二、CBOW與skip-gram的區別

  CBOW全稱Continuous Bag-of-Words Model,Skip-Gram全稱Continuous skip-gram Model。兩者是Word2Vec裏重要的模型,CBOW模型的訓練輸入是某一個特征詞的上下文相關的詞對應的詞向量,而輸出就是這特定的一個詞的詞向量。Skip-Gram模型和CBOW的思路是反著來的,即輸入是特定的一個詞的詞向量,而輸出是特定詞對應的上下文詞向量。這兩種模型都包含三層,輸入層、投影層、輸出層:

技術分享圖片

由上圖可見,CBOW是在已知當前詞 wt 的上下文 wt-2,wt-1,wt+1,wt+2 的前提下預測當前詞wt;而Skip-gram則是已知當前詞 wt 的前提下 ,去預測 wt-2,wt-1,wt+1,wt+2

技術分享圖片

三、Hierarchical Softmax框架與Negative Sampling框架

  Hierarchical Softmax 和 Negative Sampling是Word2Vec設計的兩種實現框架。Hierachical Softmax的基本思想是:它把訓練語料中的詞當成葉子節點,其在語料中出現的次數當做權值,通對於詞典Dictionary中的任意詞 w,Huffman 樹中必存在一條從根節點到詞 w 對應結點的路徑,由此每個詞對應一個Huffman編碼。而 Negative Sampling 不再使用復雜的 Huffman樹,而是采用隨機負采樣,可以減少訓練時間和大幅度提高性能。

  何為負采樣算法呢?訓練一個神經網絡,樣本只要有所改變(添加、修改、刪除)就需要稍微調整所有的神經網絡權重,這樣才能確保訓練結果的準確性。如果有一個巨大數據集,一個樣本的改變都會改變神經網絡的權重,代價是高昂的。而負采樣的好處是,每一個訓練樣本僅僅改變一小部分的權重而不是所有的權重,解決了這個問題。比如,當我要進行對 ”ready“ 這個單詞進行訓練時,使用負采樣,隨機選擇較少數目(小樣本一般為5~20個,大樣本為2~5個)的 ”負“ 樣本進行權重更新,並且仍然為我們的 ”正“ 單詞更新其對應的權重。詞典 Dictionary 中的詞在語料Corpus中出現的次數不同,那麽對於高頻詞而言,被選為負樣本的概率就應該比較大,而對於低頻詞,被選中的概率就應該小。

  如果我們的輸出層大約有 300 x 10,000 維度的權重矩陣,對 ”quick“ 一次進行權重更新,加上額外5個 負樣本的權重更新,一共是6個輸出神經元,和1800個權重值。這些值,僅僅只占輸出層 3,000,000 神經元中的 0.06%。

四、代碼實現

  此代碼已上傳github,點擊此處查看。網上有很多關於Word2Vec的實現,本文最初參考此代碼以及一些現成代碼,在後邊的參考文章中列舉。

import collections
import math
import os
import random
import zipfile
import numpy as np
import urllib
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

# Step 1 : 準備數據文檔

url = http://mattmahoney.net/dc/

def download_check(filename, expected_bytes):
    """下載數據集,如果存在就確認跳過."""
    if not os.path.exists(filename):
        print(正在下載所需數據包 …)
        filename, _ = urllib.request.urlretrieve(url + filename, filename)
    statinfo = os.stat(filename)
    if statinfo.st_size == expected_bytes:
        print(確認為目標文件 , filename)
    else:
        print(statinfo.st_size)
        raise Exception(
            文件大小不對應  + filename + 請前往 http://mattmahoney.net/dc/text8.zip 獲取數據集)
    return filename

filename = download_check(text8.zip, 31344016)

# Step 2 : 解壓文件

def read_data(filename):
    """讀取zip的第一個文件並且分割單詞為字符串數組"""
    with zipfile.ZipFile(filename) as f:
        data = tf.compat.as_str(f.read(f.namelist()[0])).split()
    return data

words = read_data(filename)
print(數據長度, len(words))

vocabulary_size = 50000

# Step 3 : 準備數據集

def build_dataset(words):
    """在字典第一個位置插入一項“UNK"代表不能識別的單詞,也就是未出現在字典的單詞統一用UNK表示"""
#  [[‘UNK‘, -1], [‘i‘, 500], [‘the‘, 498], [‘man‘, 312], ...]
    count = [[UNK, -1]]
#  dictionary {‘UNK‘:0, ‘i‘:1, ‘the‘: 2, ‘man‘:3, ...} 收集所有單詞詞頻
    count.extend(collections.Counter(words).most_common(vocabulary_size - 1))
#  python中K/V的一種數據結構"字典
    dictionary = dict()
    for word, _ in count:
        dictionary[word] = len(dictionary)
    data = list()
    unk_count = 0
    for word in words:
        if word in dictionary:
            index = dictionary[word]
        else:
            index = 0 # dictionary[‘UNK‘]
            unk_count += 1
        data.append(index)
    count[0][1] = unk_count
    reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
    return data, count, dictionary, reverse_dictionary

data, count, dictionary, reverse_dictionary = build_dataset(words)

del words 
print(詞頻最高的詞, count[:5])
print(數據樣例, data[:10], [reverse_dictionary[i] for i in data[:10]])

data_index = 0

# Step 4 : skip-gram

def generate_batch(batch_size, num_skips, skip_window):
    global data_index #global關鍵字 使data_index 可在其他函數中修改其值
    assert batch_size % num_skips == 0 #assert斷言用於判斷後者是否為true,如果返回值為假,處罰異常
    assert num_skips <= 2 * skip_window
    batch = np.ndarray(shape=(batch_size), dtype=np.int32) #ndarray對象用於存放多維數組
    labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
    span = 2 * skip_window + 1 # [ skip_window target skip_window]
    # 初始化最大長度為span的雙端隊列,超過最大長度後再添加數據,會從另一端刪除容不下的數據
    # buffer: 1, 21, 124, 438, 11
    buffer = collections.deque(maxlen=span) #創建一個隊列,模擬滑動窗口
    for _ in range(span):
        buffer.append(data[data_index])
        data_index = (data_index + 1) % len(data)
    for i in range(batch_size // num_skips): # // 是整數除
        # target : 2
        target = skip_window # target label at the center of the buffer
        # target_to_avoid : [2]
        targets_to_avoid = [ skip_window ] # 需要忽略的詞在當前的span位置
        # 更新源單詞為當前5個單詞的中間單詞
        source_word = buffer[skip_window]
        # 隨機選擇的5個span單詞中除了源單詞之外的4個單詞中的兩個
        for j in range(num_skips):
            while target in targets_to_avoid:
                target = random.randint(0, span - 1)
            targets_to_avoid.append(target) # 已經經過的target放入targets_to_avoid
            #batch中添加源單詞
            batch[i * num_skips + j] = source_word
            #labels添加目標單詞,單詞來自隨機選擇的5個span單詞中除了源單詞之外的4個單詞中的兩個
            labels[i * num_skips + j, 0] = buffer[target]
        # 往雙端隊列中添加下一個單詞,雙端隊列會自動將容不下的數據從另一端刪除
        buffer.append(data[data_index])
        data_index = (data_index + 1) % len(data)
    return batch, labels

# Step 5 : 構建一個包含隱藏層的神經網絡,隱藏層包含300節點,與我們要構造的WordEmbedding維度一致

batch, labels  = generate_batch(batch_size=8, num_skips=2, skip_window=1)
# 打印數據樣例中的skip-gram樣本
for i in range(8):
    print((,batch[i], reverse_dictionary[batch[i]],
    ,, labels[i, 0], reverse_dictionary[labels[i, 0]],))
"""
( 3081 originated , 12 as )
( 3081 originated , 5234 anarchism )
( 12 as , 6 a )
( 12 as , 3081 originated )
( 6 a , 12 as )
( 6 a , 195 term )
( 195 term , 6 a )
( 195 term , 2 of )
"""
batch_size = 128
embedding_size = 128 # Demension of the embedding vector
skip_window = 1      # How many words to consider left and right
num_skips = 2        # How many times to reuse an input to generate a label

valid_size = 16      # Random set of words to evaluate similarity on
valid_window = 100   # Only pick dev samples in the head of the distribution
valid_examples = np.random.choice(valid_window, valid_size, replace=False)
num_sampled = 64     # Number of negative examples to sample

graph = tf.Graph()
with graph.as_default():
    # 定義輸入輸出
    train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
    train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
    valid_dataset = tf.constant(valid_examples, dtype=tf.int32)

    # 當缺少GPU時,用CPU來進行訓練和操作變量
    with tf.device(/cpu:0):
        # 初始化embedding矩陣,後邊經過多次訓練後我們得到的結果就放在此embedding矩陣; 
        # tf.Variable是圖變量,tf.radom_uniform產生一個在[-1,1]間均勻分布的size為[vocabulary_size, embedding_size]的矩陣
        embeddings = tf.Variable(
            tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
        # 將輸入序列轉換成embedding表示, [batch_size, embedding_size]
        # tf.nn.embedding_lookup的作用就是找到要尋找的embedding data中的對應的行下的vector
        emded = tf.nn.embedding_lookup(embeddings, train_inputs)

        # 初始化權重,此處使用負例采樣NCE loss損失函數
        # tf.truncated_normal(shape, mean, stddev) :shape表示生成張量的維度,mean是均值,stddev是標準差。這個函數產生正太分布,
        # 均值和標準差自己設定。這是一個截斷的產生正太分布的函數,就是說產生正太分布的值如果與均值的差值大於兩倍的標準差,那就重新生成。
        nce_weights = tf.Variable(
            tf.truncated_normal([vocabulary_size, embedding_size],
                                stddev=1.0 / math.sqrt(embedding_size)))
        nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
    # Compute the average NCE loss for the batch
    # tf.nce_loss automatically draws a new sample of the negative labels each
    # time we evalute the loss
    loss =  tf.reduce_mean(
        tf.nn.nce_loss(weights = nce_weights,
                       biases = nce_biases,
                       labels = train_labels,
                       inputs = emded,
                       num_sampled = num_sampled,
                       num_classes = vocabulary_size ))          
        # 使用1.0的速率來構造SGD優化器
    optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)
        # 計算 minibatch 和 all embeddings的余弦相似度
        # tf.reduce_sum() 按照行的維度求和
    norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
    normalized_embeddings = embeddings / norm
    valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings, valid_dataset)
        # tf.matmul 矩陣相乘
    similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True) 
        # 添加變量初始化程序
    init = tf.global_variables_initializer()

# Step 6 : 開始訓練
# 訓練次數
num_steps = 100001
    # tf.Session 用於運行TensorFlow操作的類
with tf.Session(graph=graph) as session:
    # 我們必須在使用之前初始化所有變量
    init.run()
    print("Initialized")
    average_loss = 0
    for step in range(num_steps):
        batch_inputs, batch_labels = generate_batch(
            batch_size, num_skips, skip_window)
        feed_dict = {train_inputs : batch_inputs, train_labels : batch_labels}
        # We perform one update step by evaluating the optimizer op( including it
        # in the list of returned values for session.run())
        _, loss_val = session.run([optimizer, loss], feed_dict=feed_dict)
        average_loss += loss_val
        if step % 2000 == 0:
            if step > 0:
                average_loss /= 2000
            #The average loss is an estimate of the loss over the last 2000 batches.
            print("Average loss at step", step, ": ", average_loss)
            average_loss = 0
        # Note that this is expensive ( ~20% slowdown if computed every 500 steps)
        if step % 10000 == 0:
            sim = similarity.eval()
            for i in range(valid_size):
                valid_word = reverse_dictionary[valid_examples[i]]
                top_k = 8 # number of nearest neighbors
                nearest = (-sim[i, : ]).argsort()[1:top_k+1]
                log_str = "與 %s 最接近的詞是:" % valid_word
                for k in range(top_k):
                    close_word = reverse_dictionary[nearest[k]]
                    log_str = "%s %s," % (log_str, close_word)
                print(log_str)
    final_embeddings = normalized_embeddings.eval()
 
# Step 7 : 繪制結果
def plot_with_labels(low_dim_embs, labels, filename=TSNE_result.png):
    assert low_dim_embs.shape[0] >= len(labels), "More labels than embeddings"
    plt.figure(figsize=(18, 18)) # in inches
    for i, label in enumerate(labels):
        x, y = low_dim_embs[i,:]
        plt.scatter(x, y)
        plt.annotate(label,
                    xy=(x, y),
                    xytext=(5, 2),
                    textcoords=offset points,
                    ha=right,
                    va=bottom)
    plt.savefig(filename)

# 使用T-SNE算法將128維降低到2維
tsne = TSNE(perplexity= 30, n_components = 2, init = pca, n_iter = 5000, random_state = 1)
# 繪制點的個數
plot_only = 100
low_dim_embs = tsne.fit_transform(final_embeddings[: plot_only, :])
labels = [reverse_dictionary[i] for i in range(plot_only)]
plot_with_labels(low_dim_embs, labels)
plt.show()

五、訓練結果

技術分享圖片

技術分享圖片

參考資料:

1、https://www.jianshu.com/p/471d9bfbd72f

2、https://www.jianshu.com/p/0bb00eed9c63

3、https://blog.csdn.net/itplus/article/details/37998797

4、https://blog.csdn.net/qq_28444159/article/details/77514563

Word2Vec原理及代碼