《tensorflow》實戰學習筆記(二)——實現Word2Vec

1 Word2Vec簡介

       2013年,Google開源了一款用於詞向量計算的工具——word2vec,引起了工業界和學術界的關注。首先,word2vec可以在百萬數量級的詞典和上億的數據集上進行高效地訓練;其次,該工具得到的訓練結果——詞向量(word embedding),可以很好地度量詞與詞之間的相似性。進而可以做其他自然語言處理任務,比如文本分類,詞性標註,機器翻譯等。
       提到Word2Vec,不得不說一下one-hot編碼,one-hot編碼是將每個單詞與一個唯一的整數索引相關聯,然後將這個整數索引i轉換爲長度爲N的二進制向量(N是詞表大小),這個向量只有第i個元素是1,其餘全是0。因此它得到的詞向量是離散稀疏且高維的。而Word2Vec是將高維詞向量嵌入到一個低維空間,語義相近的詞的向量夾角餘弦相似度更相近,可以從數據中學習得到。具體可以參考這篇文章

2 tensorflow實現Word2Vec

       下面開始用tensorflow實現Word2Vec的訓練,具體註解代碼中已經註釋的很詳細了,爲了方便copy就直接一整段都貼過來了。

# tensorflow實現Word2Vec的訓練。因爲要從網絡上下載數據,因此需要的依賴庫比較多。
import collections
import math
import os
import random
import sys
import numpy as np
import urllib
import tensorflow as tf
import zipfile36 as zipfile
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

url = 'http://mattmahoney.net/dc/'
# 定義下載文本數據的函數
def maybe_download(filename,expected_bytes):
    if not os.path.exists(filename):
        # 用urllib.request.urlretrieve下載數據的壓縮文件並覈對文件尺寸,如果已經下載則跳過
        filename,_ = urllib.request.urlretrieve(url + filename,filename)
    statinfo = os.stat(filename)
    if statinfo.st_size == expected_bytes:
        print('Found and verified', filename)
    else:
        print(statinfo.st_size)
        raise Exception('Failed to verify'+filename+'. Can you get to it with a browser?')
    return filename

filename = maybe_download('text8.zip', 31344016)

# 解壓下載的壓縮文件,並使用tf.compat.as_str將數據轉成單詞的列表。通過程序輸出,可以知道數據最後被轉爲了一個包含17005207個單詞的列表。
def read_data(filename):
    with zipfile.ZipFile(filename, 'r') as f:
        data = tf.compat.as_str(f.read(f.namelist()[0])).split()
    return data

words = read_data(filename)
print('Data size',len(words))   # Data size 17005207

# 接下來創建vocabulary詞彙表,我們使用collections.Counter統計單詞列表中單詞的頻數。
# 然後用most_common方法取top50000頻數的單詞作爲vocabulary。再創建一個dict,將vocabulary放入dictionary中,python中的dict查詢複雜度爲O(1),性能非常好。
# 對於50000以外的詞彙認定爲Unknown,將其編號爲0,
vocabulary_size = 50000
def build_dataset(words):
    count = [['UNK', -1]]
    count.extend(collections.Counter(words).most_common(vocabulary_size-1))
    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
            unk_count += 1
        data.append(index)
    count[0][1] = unk_count
    reverse_dictionary = dict(zip(dictionary.values(),dictionary.keys()))
    # 返回轉換後的編碼(data),每個單詞的頻數統計(count),詞彙表(dictionary),及其反轉的形式(reverse_dictionary)
    return data, count, dictionary, reverse_dictionary
data,count,dictionary,reverse_dictionary = build_dataset(words)
# 刪除原始單詞列表,可以節約內存
del words
print('Most common words (+UNK)',count[:5])   # Most common words (+UNK) [['UNK', 418391], ('the', 1061396), ('of', 593677), ('and', 416629), ('one', 411764)]
print('Sample data',data[:10],[reverse_dictionary[i] for i in data[:10]])  # Sample data [5234, 3081, 12, 6, 195, 2, 3134, 46, 59, 156] ['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse', 'first', 'used', 'against']

# 下面生成word2vec的訓練樣本。
data_index = 0
# generate_batch用來生成訓練用的batch數據,batch_size:batch的大小,num_skip:對每個單詞生成多少個樣本(不大於skip_window值的兩倍,並且batch_size必須是它的整數倍)
def generate_batch(batch_size,num_skips,skip_window):
    global data_index
    assert batch_size % num_skips == 0
    assert num_skips <= 2*skip_window
    batch = np.ndarray(shape=(batch_size),dtype=np.int32)
    labels = np.ndarray(shape=(batch_size,1),dtype=np.int32)
    # span爲對某個單詞創建相關樣本時會使用到的單詞數量,包括目標單詞本身和它前後的單詞
    span = 2*skip_window +1
    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 = skip_window    # 即buffer中第skip_window個變量爲目標單詞
        targets_to_avoid = [skip_window]    # 生成樣本時需要避免的單詞列表targets_to_avoid
        # 每次循環對一個語境單詞生成樣本,先產生隨機數,知道隨機數不在targets_to_avoid,代表可以使用的語境單詞,然後產生一個樣本。
        # 在對一個目標單詞生成完所有樣本後,我們再讀入下一個單詞(同時會拋掉buffer中第一個單詞),即把滑窗向後移動一位,這樣我們的目標單詞也向後移動了一個,語境單詞也整體後移了,便可以開始生成下一個目標單詞的訓練樣本。
        for j in range(num_skips):
            while target in targets_to_avoid:
                target = random.randint(0, span - 1)
            targets_to_avoid.append(target)
            batch[i * num_skips + j] = buffer[skip_window]
            labels[i*num_skips +j,0] = buffer[target]
        buffer.append(data[data_index])
        data_index = (data_index + 1)%len(data)
    # 兩層循環完成後,我們已經獲得了batch_size個訓練樣本,返回batch和labels
    return batch, labels
# 這裏簡單測試一下generate_batch函數,
# 結果以第一個樣本爲例"3084 originated -> 5235 anarchism",3084是目標單詞originated的編號,這個單詞對應的語境單詞是anarchism,其編號爲5235.
# batch,labels = generate_batch(batch_size=8, num_skips=2,skip_window=1)
# for i in range(8):
#     print(batch[i],reverse_dictionary[batch[i]], '->',labels[i,0],reverse_dictionary[labels[i,0]])


# 定義訓練參數
batch_size = 128
embedding_size = 128    # 單詞轉爲稠密向量的維度,一般是50~1000
skip_window = 1       # 單詞間最遠可以聯繫的距離
num_skips = 2       # 對每個目標單詞提取的樣本數

valid_size = 16      # 用來抽取的驗證單詞數
valid_window = 100      # 驗證單詞只從頻數最高的100個單詞中抽取
valid_examples = np.random.choice(valid_window, valid_size, replace=False)      # 驗證數據
num_sampled = 64        # 訓練時用來做負樣本的噪聲單詞的數量

# 下面開始定義skip_gram word2vec模型的網絡結構
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)
    # 限定所有計算再CPU上執行,因爲接下去的一些計算操作再GPU上可能還沒有實現。
    with tf.device('/cpu:0'):
        # 隨機生成所有單詞的詞向量embeddings,單詞表大小爲50000,向量爲都爲128
        embeddings = tf.Variable(
            tf.random_uniform([vocabulary_size,embedding_size], -1.0, 1.0)
        )
        # 查找輸入train_inputs對應的向量embed
        embed = tf.nn.embedding_lookup(embeddings, train_inputs)
        # NCE Loss爲訓練的優化目標
        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]))

    loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weights,
                                         biases=nce_biases,
                                         labels=train_labels,
                                         inputs=embed,
                                         num_sampled=num_sampled,
                                         num_classes=vocabulary_size))

    # 定義優化器爲SGD,學習速率爲1.0
    optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)
    # 計算嵌入向量embeddings的L2範數norm
    norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings),1,keep_dims=True))
    # 再將embeddings除以L2範數得到標準化後的normalized_embeddings
    normalized_embeddings = embeddings/norm
    # tf.nn.embedding_lookup用來查詢單詞的嵌入向量,並計算驗證單詞的嵌入向量與詞彙表中所有單詞的相似性。
    valid_embeddings = tf.nn.embedding_lookup(
        normalized_embeddings, valid_dataset)
    similarity = tf.matmul(
        valid_embeddings,normalized_embeddings,transpose_b=True
    )
    # 初始化所有模型參數
    init = tf.global_variables_initializer()

num_steps = 100001
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}

        _, loss_val = session.run([optimizer,loss],feed_dict=feed_dict)
        average_loss += loss_val
        # 每兩千次循環計算一下平均loss並顯示出來。
        if step % 2000 == 0:
            if step > 0:
                average_loss /= 2000
            print("Average loss at step ", step, ": ", average_loss)
            average_loss = 0
        # 每10000次循環計算一次驗證單詞與全部單詞的相似度並將每個驗證單詞最相似的8個單詞展示出來。
        if step%10000 == 0:
            sim = similarity.eval()
            for i in range(valid_size):
                valid_word = reverse_dictionary[valid_examples[i]]
                top_k = 8
                nearest = (-sim[i, :]).argsort()[1:top_k+1]
                log_str = "Nearest to %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()

# 可視化效果
# 這裏的low_dim_embs是降到2維的單詞的空間向量,將在圖中展示每個單詞的位置。
def plot_with_labels(low_dim_embs,labels,filename='tsne.png'):
    assert low_dim_embs.shape[0] >= len(labels), "More labels than embeddings"
    plt.figure(figsize=(18, 18))
    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)  # 保存到本地

# 利用sklearn.manifold.TSNE實現降維,將128維降到2維
tsne = TSNE(perplexity=30,n_components=2,init='pca',n_iter=5000)
plot_only = 100    # 這裏只顯示詞頻最高的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)

       如圖所示,距離相近的單詞在語義上具有很高的相似性,例如most和more,zero~nine等。
降維後的Word2Vec可視化圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章