1. 詞向量介紹
在討論詞嵌入之前,先要理解詞向量的表達形式,注意,這里的詞向量不是指Word2Vec。關於詞向量的表達,現階段采用的主要有One hot representation和Distributed representation兩種表現形式。
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]。
可以看到,One hot representation編碼的每個詞都是一個維度,元素非0即1,且詞與詞之間彼此相互獨立。
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 ] 。
所以對於詞嵌入,我們可以理解為是對詞的一種分布式表達方式,並且是從高維稀疏向量映射到了相對低維的實數向量上。
2. 為什么使用詞嵌入
詞嵌入,往往和Distributed representation聯系在一起。這里主要從計算效率、詞關系和數量這三點說明。
- 計算效率。采用One hot representation的每個詞的向量長度是由詞匯表的數量決定,如果詞匯表數量很大,那么每個詞的長度會很長,同時,由於向量元素只有一個元素為1,其余元素為0,所以,每個詞的向量表達也會非常稀疏。而對於海量的詞語來講,計算效率是需要考慮的。
- 詞關系。和One hot representation相比,Distributed representation能夠表達詞與詞之間的關系。
- 數量。對於把詞語作為模型輸入的任務,對於相似的詞語,可以通過較少樣本完成目標任務的訓練,而這是One hot representation所無法企及的優勢。
3. Language Models
由於詞嵌入目的是為了能更好地對NLP的輸入做預處理。所以在對詞嵌入技術作進一步討論之前,有必要對語言模型的發展做一些介紹。
3.1 Bag of words model
Bag of words model又稱為詞袋模型,顧名思義,一段文本可以用一個裝着這些詞的袋子來表示。詞袋模型通常將單詞和句子表示為數字向量的形式,其中向量元素為句子中此單詞在詞袋表出現的次數。然后將數字向量輸入分類器(例如Naive Bayes),進而對輸出進行預測。這種表示方式不考慮文法以及詞的順序。
例如以下兩個句子:
- John likes to watch movies. Mary likes movies too.
- John also likes to watch football games.
基於以上兩個句子,可以建構詞袋表:[ "John", "likes", "to", "watch", "movies", "also", "football", "games", "Mary", "too" ]
由於詞袋表的長度為10,所以每個句子的數字向量表示長度也為10。下面是每個句子的向量表示形式:
- [1, 2, 1, 1, 2, 0, 0, 0, 1, 1]
- [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的模型結構,主要分為幾下幾步:
- 從句子中定義一個中心詞,即Skip-Gram的模型input word
- 定義skip_window參數,用於表示從當前input word的一側(左邊及右邊)選取詞的數量。
- 根據中心詞和skip_window,構建窗口列表。
- 定義num_skips參數,用於表示從當前窗口列表中選擇多少個不同的詞作為output word。
假設有一句子"The quick brown fox jumps over the lazy dog" ,設定的窗口大小為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. 總結
- 詞嵌入是一種把詞從高維稀疏向量映射到了相對低維的實數向量上的表達方式。
- Skip-Gram和CBOW的作用是構造神經網絡的訓練數據。
- 目前設計的網絡結構實際上是由DNN+softmax()組成。
- 由於每個輸入向量有且僅有一個元素為1,其余元素為0,所以計算詞嵌入向量實際上就是在計算隱藏層的權矩陣。
- 對於單位矩陣的每一維(行)與實矩陣相乘,可以簡化為查找元素1的位置索引從而快速完成計算。
6. 結束了嗎?
仔細閱讀代碼,我們發現prediction時,使用的是softmax()。即輸入詞在輸出層分別對詞匯表的每一個詞進行概率計算,如果在海量詞匯表的前提下,計算效率是否需要考慮在內?有沒有更快的計算方式呢?
此外,本文第3節提到的分層softmax是什么?negative samples又是什么?Huffman code又是怎樣使用的?關於這些問題的思考,請關注:詞嵌入的那些事兒(二)
7. 參考資料
[1] Word2Vec Tutorial - The Skip-Gram Model