詞嵌入技術解析(一)

1. 詞向量介紹

在討論詞嵌入以前,先要理解詞向量的表達形式,注意,這裏的詞向量不是指Word2Vec。關於詞向量的表達,現階段採用的主要有One hot representation和Distributed representation兩種表現形式。html

1.1 One hot representation

顧名思義,採用獨熱編碼的方式對每一個詞進行表示。網絡

例如,一段描述「杭州和上海今天有雨」,經過分詞工具能夠把這段描述分爲[‘杭州’,‘和’,‘上海’,今天’,‘有’,‘雨’],所以詞表的長度爲6,那麼‘杭州’、‘上海’、'今天'的One hot representation分別爲[1 0 0 0 0 0],[0 0 1 0 0 0],[0 0 0 1 0 0]。app

能夠看到,One hot representation編碼的每一個詞都是一個維度,元素非0即1,且詞與詞之間彼此相互獨立。dom

1.2 Distributed representation

Distributed representation在One hot representation的基礎上考慮到詞與詞之間的聯繫,例如詞義、詞性等信息。每個維度元素再也不是0或1,而是連續的實數,表示不一樣的程度。Distributed representation 又包含了如下三種處理方式:分佈式

  • 基於矩陣的分佈表示。,矩陣中的一行,就成爲了對應詞的表示,這種表示描述了該詞的上下文的分佈。因爲分佈假說認爲上下文類似的詞,其語義也類似,所以在這種表示下,兩個詞的語義類似度能夠直接轉化爲兩個向量的空間距離。
  • 基於聚類的分佈表示。
  • 基於神經網絡的分佈表示。

而咱們如今常說的Distributed representation主要是基於神經網絡的分佈式表示的。例如‘杭州’、‘上海’的Distributed representation分別爲[0.3 1.2 0.8 0.7] 和 [0.5 1.2 0.6 0.8 ] 。ide

因此對於詞嵌入,咱們能夠理解爲是對詞的一種分佈式表達方式,而且是從高維稀疏向量映射到了相對低維的實數向量上。函數

2. 爲何使用詞嵌入

詞嵌入,每每和Distributed representation聯繫在一塊兒。這裏主要從計算效率、詞關係和數量這三點說明。工具

  1. 計算效率。採用One hot representation的每一個詞的向量長度是由詞彙表的數量決定,若是詞彙表數量很大,那麼每一個詞的長度會很長,同時,因爲向量元素只有一個元素爲1,其他元素爲0,因此,每一個詞的向量表達也會很是稀疏。而對於海量的詞語來說,計算效率是須要考慮的。
  2. 詞關係。和One hot representation相比,Distributed representation可以表達詞與詞之間的關係。
  3. 數量。對於把詞語做爲模型輸入的任務,對於類似的詞語,能夠經過較少樣本完成目標任務的訓練,而這是One hot representation所沒法企及的優點。

3. Language Models

因爲詞嵌入目的是爲了能更好地對NLP的輸入作預處理。因此在對詞嵌入技術做進一步討論以前,有必要對語言模型的發展作一些介紹。學習

3.1 Bag of words model

Bag of words model又稱爲詞袋模型,顧名思義,一段文本能夠用一個裝着這些詞的袋子來表示。詞袋模型一般將單詞和句子表示爲數字向量的形式,其中向量元素爲句子中此單詞在詞袋錶出現的次數。而後將數字向量輸入分類器(例如Naive Bayes),進而對輸出進行預測。這種表示方式不考慮文法以及詞的順序。測試

例如如下兩個句子:

  1. John likes to watch movies. Mary likes movies too.
  2. John also likes to watch football games.

基於以上兩個句子,能夠建構詞袋錶:"John""likes""to""watch""movies""also""football""games""Mary""too" ]

因爲詞袋錶的長度爲10,因此每一個句子的數字向量表示長度也爲10。下面是每一個句子的向量表示形式:

  1. [1, 2, 1, 1, 2, 0, 0, 0, 1, 1]
  2. [1, 1, 1, 1, 0, 1, 1, 1, 0, 0]

Bag of words model的優缺點很明顯:優勢是基於頻率統計方法,易於理解。缺點是它的假設(單詞之間徹底獨立)過於強大,沒法創建準確的模型。

3.2 N-Gram model

N-gram model的提出旨在減小傳統Bag of words model的一些強假設。

語言模型試圖預測在給定前t個單詞的前提下觀察t第 + 1個單詞w t + 1的機率:

利用機率的鏈式法則,咱們能夠計算出觀察整個句子的機率:

能夠發現,估計這些機率多是困難的。所以能夠用最大似然估計對每一個機率進行計算:

然而,即便使用最大似然估計方法進行計算,仍然很是困難:咱們一般沒法從語料庫中觀察到足夠多的數據,而且計算長度仍然很長。所以採用了馬爾可夫鏈的思想。

馬爾可夫鏈規定:系統下一時刻的狀態僅由當前狀態決定,不依賴於以往的任何狀態。即第t + 1個單詞的發生機率表示爲:

所以,一個句子的機率能夠表示爲:

一樣地,馬爾可夫假設能夠推廣到:系統下一時刻的狀態僅由當前0個、1個、2個...n個狀態決定這就是N-gram model的N的意思:對下一時刻的狀態設置當前狀態的個數。下面分別給出了unigram(一元模型)和bigram(二元模型)的第t + 1個單詞的發生機率:

能夠發現,N-Gram model 在Bag of words model的基礎上,經過採用馬爾科夫鏈的思想,減小了機率計算的複雜度,同時考慮了單詞間的相關性。

3.3 Word2Vec Model

Word2Vec模型實際上分爲了兩個部分,第一部分爲訓練數據集的構造,第二部分是經過模型獲取詞嵌入向量,即word embedding。

Word2Vec的整個建模過程實際上與自編碼器(auto-encoder)的思想很類似,即先基於訓練數據構建一個神經網絡,當這個模型訓練好之後,並不會用這個訓練好的模型處理新任務,而真正須要的是這個模型經過訓練數據所更新到的參數。

關於word embedding的發展,因爲考慮上下文關係,因此模型的輸入和輸出分別是詞彙表中的詞組成,進而產生出了兩種詞模型方法:Skip-Gram和CBOW。同時,在隱藏層-輸出層,也從softmax()方法演化到了分層softmax和negative sample方法。

因此,要拿到每一個詞的詞嵌入向量,首先須要理解Skip-Gram和CBOW。下圖展現了CBOW和Skip-Gram的網絡結構:

本文以Skip-Gram爲例,來理解詞嵌入的相關知識。Skip-Gram是給定input word來預測上下文。咱們能夠用小學英語課上的造句來幫助理解,例如:「The __________」。

關於Skip-Gram的模型結構,主要分爲幾下幾步:

  1. 從句子中定義一箇中心詞,即Skip-Gram的模型input word
  2. 定義skip_window參數,用於表示從當前input word的一側(左邊及右邊)選取詞的數量。
  3. 根據中心詞和skip_window,構建窗口列表。
  4. 定義num_skips參數,用於表示從當前窗口列表中選擇多少個不一樣的詞做爲output word。

假設有一句子"The quick brown fox jumps over the lazy dog" ,設定的窗口大小爲2(window\_size=2),也就是說僅選中心詞(input word)先後各兩個詞和中心詞(input word)進行組合。以下圖所示,以步長爲1對中心詞進行滑動,其中藍色表明input word,方框表明位於窗口列表的詞。

因此,咱們可使用Skip-Gram構建出神經網絡的訓練數據。

咱們須要明白,不能把一個詞做爲文本字符串輸入到神經網絡中,因此咱們須要一種方法把詞進行編碼進而輸入到網絡。爲了作到這一點,首先從須要訓練的文檔中構建出一個詞彙表,假設有10,000個各不相同的詞組成的詞彙表。那麼須要作的就是把每個詞作One hot representation。此外神經網絡的輸出是一個單一的向量(也有10000個份量),它包含了詞彙表中每個詞隨機選擇附近的一個詞的機率。

3.4 Skip-Gram網絡結構

下圖是須要訓練的神經網絡結構。左側的神經元Input Vector是詞彙表中進行One hot representation後的一個詞,右側的每個神經元則表明着詞彙表的每個詞。實際上,在對該神經網絡feed訓練數據進行訓練時,不只輸入詞input word(中心詞)是用One hot representation表示,輸出詞output word也是用One hot representation進行表示。但當對此網絡進行評估預測時,輸出向量其實是經過softmax()函數計算獲得的一個詞彙表全部詞的機率分佈(即一堆浮點值,而不是一個One hot representation)。

3.5 Word2Vec Model隱藏層

假設咱們正在學習具備300個特徵的詞向量。所以,隱藏層將由一個包含10,000行(每一個單詞對應一行)和300列(每一個隱藏神經元對應一列)的權重矩陣來表示。(注:谷歌在其發佈的模型中的隱藏層使用了300個輸出(特徵),這些特徵是在谷歌新聞數據集中訓練出來的(您能夠從這裏下載)。特徵的數量300則是模型進行調優選擇後的「超參數」)。

下面左右兩張圖分別從不一樣角度表明了輸入層-隱層的權重矩陣。

從左圖看,每一列表明一個One hot representation的詞和隱層單個神經元鏈接的權重向量。從右圖看,每一行實際上表明瞭每一個詞的詞向量,或者詞嵌入。

因此咱們的目標就是學習輸入層-隱藏層的權矩陣,而隱藏層-輸出層的部分,則是在模型訓練完畢後不須要保存的參數。這一點,與自編碼器的設計思想是相似的。

你可能會問本身,難道真的分別要把每個One hot representation的詞(1 x 10000)與一個10000 x 300的權矩陣相乘嗎?實際上,並非這樣。因爲One hot representation的詞具備只有一個元素這爲1,其他元素值爲0的特性,因此能夠經過查找One hot representation中元素爲1的位置索引,進而得到對應要乘以的10000 x 300的權矩陣的向量值,從而解決計算速度緩慢的問題。下圖的例子,可幫助咱們進一步理解。

能夠看到,One hot representation中元素爲1的位置索引爲3,因此只須要乘以10000 x 300的權矩陣中位置索引一樣爲3的向量值便可獲得相應的輸出。

3.6 Word2Vec Model輸出層

下面是計算「car」這個單詞的輸出神經元的輸出的例子:

4. 基於Tensorflow的Skip-Gram極簡實現

網上找了一些Tensorflow版本的skip-gram實現,但都有一個問題,輸入單詞並無按照論文的要求作One hot representation,不知道是否是出於計算速度方面的考慮。所以,本小節的代碼仍是遵循原論文的描述,對輸入單詞及輸出單詞首先作了One hot representation。

首先,是訓練數據的構造,包括skip_window上下文參數、詞的One hot representation以及中心詞、輸出詞對的構造。

import numpy as np

corpus_raw = 'He is the king . The king is royal . She is the royal  queen '

# 大小寫轉換
corpus_raw = corpus_raw.lower()
words = []
for word in corpus_raw.split():
    if word != '.':
        words.append(word)


# 建立一個字典,將單詞轉換爲整數,並將整數轉換爲單詞。
words = set(words)
word2int = {}
int2word = {}
vocab_size = len(words)
for i, word in enumerate(words):
    word2int[word] = i
    int2word[i] = word

raw_sentences = corpus_raw.split('.')
sentences = []
for sentence in raw_sentences:
    sentences.append(sentence.split())

# 構造訓練數據

WINDOW_SIZE = 2
data = []
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])


# one-hot編碼
def to_one_hot(data_point_index, vocab_size):
    """
    對單詞進行one-hot representation
    :param data_point_index: 單詞在詞彙表的位置索引
    :param vocab_size: 詞彙表大小
    :return: 1 x vocab_size 的one-hot representatio
    """
    temp = np.zeros(vocab_size)
    temp[data_point_index] = 1
    return temp


# 輸入單詞和輸出單詞
x_train = [] 
y_train = []  

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))

其次,是Tensorflow計算圖的構造,包括輸入輸出的定義、輸入層-隱藏層,隱藏層-輸出層的構造以及損失函數、優化器的構造。最後輸出每一個詞的word embedding。具體代碼以下所示:

import tensorflow as tf

# 定義輸入、輸出佔位符
x = tf.placeholder(tf.float32, shape=(None, vocab_size))
y_label = tf.placeholder(tf.float32, shape=(None, vocab_size))

# 定義word embedding向量長度
EMBEDDING_DIM = 5

# 隱藏層構造
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)

# 輸出層構造
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))

# 構建會話並初始化全部參數
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)

# 定義損失,這裏只是採用常規DNN+softmax,未使用分層softmax和negative sample
cross_entropy_loss = tf.reduce_mean(-tf.reduce_sum(y_label * tf.log(prediction), reduction_indices=[1]))

# 優化器
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}))

# 詞嵌入 word embedding
vectors = sess.run(W1 + b1)
print('word embedding:')
print(vectors)

上述代碼的計算圖能夠簡單表示爲如下形式:

最後,打印出每一個單詞的詞嵌入向量以下所示:

 當詞嵌入向量訓練完成後,咱們能夠進行一個簡單的測試,這裏經過計算詞嵌入向量間的歐氏距離尋找相近的詞:

# 測試

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


print('與 king 最接近的詞是:', int2word[find_closest(word2int['king'], vectors)])
print('與 queen 最接近的詞是:', int2word[find_closest(word2int['queen'], vectors)])
print('與 royal 最接近的詞是:', int2word[find_closest(word2int['royal'], vectors)])

下面是輸出的測試結果:

5. 總結

  1. 詞嵌入是一種把詞從高維稀疏向量映射到了相對低維的實數向量上的表達方式。
  2. Skip-Gram和CBOW的做用是構造神經網絡的訓練數據。
  3. 目前設計的網絡結構其實是由DNN+softmax()組成。
  4. 因爲每一個輸入向量有且僅有一個元素爲1,其他元素爲0,因此計算詞嵌入向量實際上就是在計算隱藏層的權矩陣。
  5. 對於單位矩陣的每一維(行)與實矩陣相乘,能夠簡化爲查找元素1的位置索引從而快速完成計算。

6. 結束了嗎?

仔細閱讀代碼,咱們發現prediction時,使用的是softmax()。即輸入詞在輸出層分別對詞彙表的每個詞進行機率計算,若是在海量詞彙表的前提下,計算效率是否須要考慮在內?有沒有更快的計算方式呢?

此外,本文第3節提到的分層softmax是什麼?negative samples又是什麼?Huffman code又是怎樣使用的?關於這些問題的思考,請關注:詞嵌入的那些事兒(二)

7. 參考資料

[1] Word2Vec Tutorial - The Skip-Gram Model

相關文章
相關標籤/搜索