在討論詞嵌入以前,先要理解詞向量的表達形式,注意,這裏的詞向量不是指Word2Vec。關於詞向量的表達,現階段採用的主要有One hot representation和Distributed representation兩種表現形式。html
顧名思義,採用獨熱編碼的方式對每一個詞進行表示。網絡
例如,一段描述「杭州和上海今天有雨」,經過分詞工具能夠把這段描述分爲[‘杭州’,‘和’,‘上海’,今天’,‘有’,‘雨’],所以詞表的長度爲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
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
因此對於詞嵌入,咱們能夠理解爲是對詞的一種分佈式表達方式,而且是從高維稀疏向量映射到了相對低維的實數向量上。函數
詞嵌入,每每和Distributed representation聯繫在一塊兒。這裏主要從計算效率、詞關係和數量這三點說明。工具
因爲詞嵌入目的是爲了能更好地對NLP的輸入作預處理。因此在對詞嵌入技術做進一步討論以前,有必要對語言模型的發展作一些介紹。學習
Bag of words model又稱爲詞袋模型,顧名思義,一段文本能夠用一個裝着這些詞的袋子來表示。詞袋模型一般將單詞和句子表示爲數字向量的形式,其中向量元素爲句子中此單詞在詞袋錶出現的次數。而後將數字向量輸入分類器(例如Naive Bayes),進而對輸出進行預測。這種表示方式不考慮文法以及詞的順序。測試
例如如下兩個句子:
基於以上兩個句子,能夠建構詞袋錶:[ "John", "likes", "to", "watch", "movies", "also", "football", "games", "Mary", "too" ]
因爲詞袋錶的長度爲10,因此每一個句子的數字向量表示長度也爲10。下面是每一個句子的向量表示形式:
Bag of words 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的基礎上,經過採用馬爾科夫鏈的思想,減小了機率計算的複雜度,同時考慮了單詞間的相關性。
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的模型結構,主要分爲幾下幾步:
假設有一句子"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個份量),它包含了詞彙表中每個詞隨機選擇附近的一個詞的機率。
下圖是須要訓練的神經網絡結構。左側的神經元Input Vector是詞彙表中進行One hot representation後的一個詞,右側的每個神經元則表明着詞彙表的每個詞。實際上,在對該神經網絡feed訓練數據進行訓練時,不只輸入詞input word(中心詞)是用One hot representation表示,輸出詞output word也是用One hot representation進行表示。但當對此網絡進行評估預測時,輸出向量其實是經過softmax()函數計算獲得的一個詞彙表全部詞的機率分佈(即一堆浮點值,而不是一個One hot representation)。
假設咱們正在學習具備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的向量值便可獲得相應的輸出。
下面是計算「car」這個單詞的輸出神經元的輸出的例子:
網上找了一些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)])
下面是輸出的測試結果:
仔細閱讀代碼,咱們發現prediction時,使用的是softmax()。即輸入詞在輸出層分別對詞彙表的每個詞進行機率計算,若是在海量詞彙表的前提下,計算效率是否須要考慮在內?有沒有更快的計算方式呢?
此外,本文第3節提到的分層softmax是什麼?negative samples又是什麼?Huffman code又是怎樣使用的?關於這些問題的思考,請關注:詞嵌入的那些事兒(二)