利用 TensorFlow 入門 Word2Vec

做者:chen_h
微信號 & QQ:862251340
微信公衆號:coderpai
個人博客:請點擊這裏html

我認爲學習算法的最好方法就是嘗試去實現它,所以這個教程咱們就來學習如何利用 TensorFlow 來實現詞嵌入。node

這篇文章咱們不會去過多的介紹一些詞向量的內容,因此不少 king - man - woman - queue 的例子會被省去,直接進入編碼實踐過程。

咱們如何設計這些詞嵌入?

對於如何設計詞嵌入有不少的技術,這裏咱們討論一種很是有名的技術。與咱們往常的認知不一樣,word2vec 並非一個深層的網絡,它只是一個三層的淺層網絡。git

注意:word2vec 有不少的技術細節,可是咱們會跳過這些細節,來使得更加容易理解。github

word2vec 如何工做?

word2vec 算法的設計以下:算法

  1. 它是一個三層的網絡(一個輸入層 + 一個隱藏層 + 一個輸出層)。
  2. 模型輸入一個詞,而後去預測它周圍的詞。
  3. 移除最後一層(輸出層),保留輸入層和隱藏層。
  4. 如今,輸入一個詞庫中的詞,而後隱藏層的輸出就是輸入詞的詞向量。

就是這麼簡單,這個三層網絡就能夠獲得一個還不錯的詞向量。bash

接下來就讓咱們來實現這個模型。完整的代碼能夠點擊 Github,但我建議你先不要看完整的代碼,先一步一步學習。微信

接下來,咱們先定義咱們要處理的原始文本:網絡

import numpy as np
import tensorflow as tf
corpus_raw = 'He is the king . The king is royal . She is the royal queen '

# convert to lower case
corpus_raw = corpus_raw.lower()複製代碼

如今,咱們須要將輸入的原始文本數據轉換成一個輸入輸出對,以便咱們對輸入的詞,能夠去預測它附近的詞。好比,咱們肯定一箇中心詞, 窗口大小 app

window_size
設置爲 n ,那麼咱們就是去預測中心詞前面 n 個詞和後面 n 個詞。Chris McCormick 的 這篇博客給出了比較詳細的解釋。

A training sample generation with a window size of 2.

注意:若是中心詞是在句子的開頭或者末尾,那麼咱們就忽略窗口沒法得到的詞。

在作這個以前,咱們須要建立一個字典,用來肯定每一個單詞的索引,具體以下:dom

words = []
for word in corpus_raw.split():
    if word != '.': # because we don't want to treat . as a word
        words.append(word)
words = set(words) # so that all duplicate words are removed
word2int = {}
int2word = {}
vocab_size = len(words) # gives the total number of unique words
for i,word in enumerate(words):
    word2int[word] = i
    int2word[i] = word複製代碼

這個字典的運行結果以下:

print(word2int['queen'])
-> 42 (say)

print(int2word[42])
-> 'queen'複製代碼

接下來,咱們將咱們的句子向量轉換成單詞列表,以下:

# raw sentences is a list of sentences.
raw_sentences = corpus_raw.split('.')
sentences = []
for sentence in raw_sentences:
    sentences.append(sentence.split())複製代碼

上面代碼將幫助咱們獲得一個句子的列表,列表中的每個元素是句子的單詞列表,以下:

print(sentences)

-> [['he', 'is', 'the', 'king'], ['the', 'king', 'is', 'royal'], ['she', 'is', 'the', 'royal', 'queen']]複製代碼

接下來,咱們要產生咱們的訓練數據:

data = []

WINDOW_SIZE = 2

for sentence in sentences:
    for word_index, word in enumerate(sentence):
        for nb_word in sentence[max(word_index - WINDOW_SIZE, 0) : min(word_index + WINDOW_SIZE, len(sentence)) + 1] : 
            if nb_word != word:
                data.append([word, nb_word])複製代碼

這個程序給出了單詞輸入輸出對,咱們將窗口的大小設置爲 2。

print(data)
[['he', 'is'],
 ['he', 'the'],
 ['is', 'he'],
 ['is', 'the'],
 ['is', 'king'],
 ['the', 'he'],
 ['the', 'is'], 
.
.
.
]複製代碼

至此,咱們有了咱們的訓練數據,可是咱們須要將它轉換成計算機能夠理解的表示,即數字。也就是咱們以前設計的 word2int 字典。

咱們再進一步表示,將這些數字轉換成 0-1 向量。

i.e., 
say we have a vocabulary of 3 words : pen, pineapple, apple
where 
word2int['pen'] -> 0 -> [1 0 0]
word2int['pineapple'] -> 1 -> [0 1 0]
word2int['apple'] -> 2 -> [0 0 1]複製代碼

那麼爲何要表示成 0-1 向量呢?這個問題咱們後續討論。

# function to convert numbers to one hot vectors
def to_one_hot(data_point_index, vocab_size):
    temp = np.zeros(vocab_size)
    temp[data_point_index] = 1
    return temp
x_train = [] # input word
y_train = [] # output word
for data_word in data:
    x_train.append(to_one_hot(word2int[ data_word[0] ], vocab_size))
    y_train.append(to_one_hot(word2int[ data_word[1] ], vocab_size))
# convert them to numpy arrays
x_train = np.asarray(x_train)
y_train = np.asarray(y_train)複製代碼

如今,咱們有了 x_trainy_train 數據:

print(x_train)
->
[[ 0.  0.  0.  0.  0.  0.  1.]
 [ 0.  0.  0.  0.  0.  0.  1.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  1.  0.  0.  0.  0.]
 [ 0.  0.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  0.  1.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  0.  0.  0.  1.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 0.  1.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.]
 [ 1.  0.  0.  0.  0.  0.  0.]]複製代碼

這兩個數據的維度以下:

print(x_train.shape, y_train.shape)
->
(34, 7) (34, 7)
# meaning 34 training points, where each point has 7 dimensions複製代碼

構造 TensorFlow 模型

# making placeholders for x_train and y_train

x = tf.placeholder(tf.float32, shape=(None, vocab_size))
y_label = tf.placeholder(tf.float32, shape=(None, vocab_size))複製代碼

從上圖中能夠看出,咱們將訓練數據轉換成了另外一種向量表示。

EMBEDDING_DIM = 5 # you can choose your own number
W1 = tf.Variable(tf.random_normal([vocab_size, EMBEDDING_DIM]))
b1 = tf.Variable(tf.random_normal([EMBEDDING_DIM])) #bias
hidden_representation = tf.add(tf.matmul(x,W1), b1)複製代碼

接下來,咱們對隱藏層的數據進行處理,而且對其附近的詞進行預測。預測詞的方法咱們採用 softmax 方法。

W2 = tf.Variable(tf.random_normal([EMBEDDING_DIM, vocab_size]))
b2 = tf.Variable(tf.random_normal([vocab_size]))
prediction = tf.nn.softmax(tf.add( tf.matmul(hidden_representation, W2), b2))複製代碼

因此,完整的模型是:

input_one_hot  --->  embedded repr. ---> predicted_neighbour_prob
predicted_prob will be compared against a one hot vector to correct it.複製代碼

如今,咱們能夠訓練這個模型:

sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init) #make sure you do this!
# define the loss function:
cross_entropy_loss = tf.reduce_mean(-tf.reduce_sum(y_label * tf.log(prediction), reduction_indices=[1]))
# define the training step:
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy_loss)
n_iters = 10000
# train for n_iter iterations
for _ in range(n_iters):
    sess.run(train_step, feed_dict={x: x_train, y_label: y_train})
    print('loss is : ', sess.run(cross_entropy_loss, feed_dict={x: x_train, y_label: y_train}))複製代碼

在訓練的過程當中,你在控制檯能夠獲得以下結果:

loss is :  2.73213
loss is :  2.30519
loss is :  2.11106
loss is :  1.9916
loss is :  1.90923
loss is :  1.84837
loss is :  1.80133
loss is :  1.76381
loss is :  1.73312
loss is :  1.70745
loss is :  1.68556
loss is :  1.66654
loss is :  1.64975
loss is :  1.63472
loss is :  1.62112
loss is :  1.6087
loss is :  1.59725
loss is :  1.58664
loss is :  1.57676
loss is :  1.56751
loss is :  1.55882
loss is :  1.55064
loss is :  1.54291
loss is :  1.53559
loss is :  1.52865
loss is :  1.52206
loss is :  1.51578
loss is :  1.50979
loss is :  1.50408
loss is :  1.49861
.
.
.複製代碼

隨着損失值的不斷降低,最終會達到一個穩定值。即便咱們沒法得到很精確的結果,可是咱們也不在意,由於咱們感興趣的是 W1 和 b1 的值,即隱藏層的權重。

讓咱們來看看這些權重,以下:

print(sess.run(W1))
print('----------')
print(sess.run(b1))
print('----------')

->
[[-0.85421133  1.70487809  0.481848   -0.40843448 -0.02236851]
 [-0.47163373  0.34260952 -2.06743765 -1.43854153 -0.14699034]
 [-1.06858993 -1.10739779  0.52600187  0.24079895 -0.46390489]
 [ 0.84426647  0.16476244 -0.72731972 -0.31994426 -0.33553854]
 [ 0.21508843 -1.21030915 -0.13006891 -0.24056002 -0.30445012]
 [ 0.17842589  2.08979321 -0.34172744 -1.8842833  -1.14538431]
 [ 1.61166084 -1.17404735 -0.26805425  0.74437028 -0.81183684]]
----------
[ 0.57727528 -0.83760375  0.19156453 -0.42394346  1.45631313]
----------複製代碼

爲何採用 0-1 向量?

again from Chris McCormick’s article (do read it)

當咱們將一個 0-1 向量與 W1 相乘時,咱們基本上能夠將 W1 與 0-1 向量對應的那個 1 相乘的結果就是詞向量。也就是說, W1 就是一個數據查詢表。

在咱們的程序中,咱們也添加了一個偏置項 b1 ,因此咱們也須要將它加上。

vectors = sess.run(W1 + b1)

# if you work it out, you will see that it has the same effect as running the node hidden representation
print(vectors)
->
[[-0.74829113 -0.48964909  0.54267412  2.34831429 -2.03110814]
 [-0.92472583 -1.50792813 -1.61014366 -0.88273793 -2.12359881]
 [-0.69424796 -1.67628145  3.07313657 -1.14802659 -1.2207377 ]
 [-1.7077738  -0.60641652  2.25586247  1.34536338 -0.83848488]
 [-0.10080346 -0.90931684  2.8825531  -0.58769202 -1.19922316]
 [ 1.49428082 -2.55578995  2.01545811  0.31536022  1.52662396]
 [-1.02735448  0.72176981 -0.03772151 -0.60208392  1.53156447]]複製代碼

若是咱們想獲得 queen 的向量,咱們能夠用以下表示:

print(vectors[ word2int['queen'] ])
# say here word2int['queen'] is 2
-> 
[-0.69424796 -1.67628145  3.07313657 -1.14802659 -1.2207377 ]複製代碼

那麼這些漂亮的向量有什麼用呢?

咱們寫一個如何去查找最相近向量的函數,固然這個寫法是很是簡單粗糙的。

def euclidean_dist(vec1, vec2):
    return np.sqrt(np.sum((vec1-vec2)**2))

def find_closest(word_index, vectors):
    min_dist = 10000 # to act like positive infinity
    min_index = -1
    query_vector = vectors[word_index]
    for index, vector in enumerate(vectors):
        if euclidean_dist(vector, query_vector) < min_dist and not np.array_equal(vector, query_vector):
            min_dist = euclidean_dist(vector, query_vector)
            min_index = index
    return min_index複製代碼

接下來,讓咱們來測試一下單詞 king ,queen 和 royal 這些詞。

print(int2word[find_closest(word2int['king'], vectors)])
print(int2word[find_closest(word2int['queen'], vectors)])
print(int2word[find_closest(word2int['royal'], vectors)])

->
queen
king
he複製代碼

咱們能夠獲得以下有趣的結果。

king is closest to queen
queen is closest to king
royal is closest to he複製代碼

第三個數據是咱們根據大型語料庫得出來的(看起來還不錯)。語料庫的數據更大,咱們獲得的結果會更好。(注意:因爲權重是隨機初始化的,因此咱們可能會獲得不一樣的結果,若是有須要,咱們能夠多運行幾回。)

讓咱們來畫出這個向量相關圖。

首先,咱們須要利用將爲技術將維度從 5 減少到 2,所用的技術是:tSNE(teesnee!)

from sklearn.manifold import TSNE
model = TSNE(n_components=2, random_state=0)
np.set_printoptions(suppress=True)
vectors = model.fit_transform(vectors)複製代碼

而後,咱們須要對結果進行規範化,以便咱們能夠在 matplotlib 中更好的對它進行查看。

from sklearn import preprocessing
normalizer = preprocessing.Normalizer()
vectors =  normalizer.fit_transform(vectors, 'l2')複製代碼

最後,咱們將繪製出圖。

import matplotlib.pyplot as plt
fig, ax = plt.subplots()
for word in words:
    print(word, vectors[word2int[word]][1])
    ax.annotate(word, (vectors[word2int[word]][0],vectors[word2int[word]][1] ))
plt.show()複製代碼

從圖中,咱們能夠看出。shequeen 的距離很是接近,kingroyal 的距離和 kingqueen 的距離相同。若是咱們有一個更大的語料庫,咱們能夠獲得更加複雜的關係圖。

爲何會發生這些?

咱們給神經網絡的任務是預測單詞的相鄰詞。可是咱們尚未具體的分析神經網絡是如何預測的。所以,神經網絡找出單詞的向量表示,用來幫助它預測相鄰詞這個任務。預測相鄰詞這自己不是一個有趣的任務,咱們關心的是隱藏層的向量表示。

爲了獲得這些表示,神經網絡使用了上下文信息。在咱們的語料庫中,king 和 royal 是做爲相鄰詞出現的,queen 和 royal 也是做爲相鄰詞出現的。

爲何把預測相鄰詞做爲一個任務?

其餘的任務也能夠用來訓練這個詞向量任務,好比利用 n-gram 就能夠訓練出很好的詞向量!這裏有一篇博客有詳細解釋。

那麼,咱們爲何還要使用相鄰詞預測做爲任務呢?由於有一個比較著名的模型稱爲 skip gram 模型。咱們可使用中間詞的相鄰單詞做爲輸入,並要求神經網絡去預測中間詞。這被稱爲連續詞袋模型。

總結

  • 詞向量是很是酷的一個工具。
  • 不要在實際生產環境中使用這個 TensorFlow 代碼,咱們這裏只是爲了理解才這樣寫。生產環境建議使用一些成熟的工具包,好比 gensim

我但願這個簡單教程能夠幫助到一些人,能夠更加深入的理解什麼是詞向量。


CoderPai 是一個專一於算法實戰的平臺,從基礎的算法到人工智能算法都有設計。若是你對算法實戰感興趣,請快快關注咱們吧。加入AI實戰微信羣,AI實戰QQ羣,ACM算法微信羣,ACM算法QQ羣。詳情請關注 「CoderPai」 微信號(coderpai) 。

相關文章
相關標籤/搜索