自Google? 2013 年開源word2vec算法程序以后,它的簡單有梆、高效削葱、實用,很快引起業(yè)界眾人的關注和應用淳梦,為搜索引擎析砸、[廣告系統(tǒng)-谷歌的wide & deep learning][2]、[推薦系統(tǒng)][1]等互聯(lián)網服務提供新的基礎技術和思路爆袍。
何為Embedding首繁?
開篇之前首先需要明白一個概念何為Embedding?Embedding可以看作是數(shù)學上的一個空間映射(Mapping):map( lambda y: f(x) )陨囊,該映射的特點是:單射(在數(shù)學里弦疮,單射函數(shù)為一函數(shù),其將不同的引數(shù)連接至不同的值上蜘醋。更精確地說胁塞,函數(shù)f被稱為是單射時,對每一值域內的y压语,存在至多一個定義域內的x使得f(x) = y啸罢。)、映射前后結構不變胎食,對應到word embedding概念中可以理解為尋找一個函數(shù)或映射扰才,生成新的空間上的表達,把單詞one-hot所表達的X空間信息映射到Y的多維空間向量厕怜。
接下來衩匣,將以模型的角度分解embedding映射函數(shù)及新空間內表達的建模過程:
非監(jiān)督的“監(jiān)督學習”
從應用角度,新空間內映射函數(shù)的學習方法不需要大量的人工標記樣本就可以得到質量還不錯的embedding向量粥航,沒有具體的應用任務導向琅捏,從這個角度可以看作非監(jiān)督的學習過程,而從建模角度递雀,向量提取的建模過程是 個分類模型柄延,又可以看做是監(jiān)督學習,只是這個監(jiān)督沒有實際的監(jiān)督意義映之,當然后來有的應該將word2vec的前段表達方式喂給標注的過文本拦焚,形成真正意義上的監(jiān)督學習蜡坊,如Facebook的FastText杠输。
2赎败、一層隱層神經網絡
帶有一個隱層的神經網絡有以下普遍特性:理論上給定足夠該隱層節(jié)點數(shù),這個隱層可以近似擬合任意函數(shù)蠢甲,本質上僵刮,隱層是上一層的嵌入(Embedding)函數(shù)的近似表示,而且可以被用做lookup表(后面會介紹)鹦牛,word2vec也是基于該層去找到輸入word的嵌入向量表示搞糕,然后再建立下一層和當前層的連接(connections),來控制目標函數(shù)的誤差曼追∏涎觯【進一步抽象,如果從統(tǒng)計的角度礼殊,其實不同層之間的統(tǒng)計關系是一種遞歸的廣義線性關系(遞歸廣義線性模型)驹吮,每一層通過線性組合對前一層進行變換,然后以一些非線性連接函數(shù)(不同函數(shù)對應output label不同的統(tǒng)計分布晶伦,比如softmax對應多項目分布碟狞,sigmoid對應二項分布等)得到非線性結果喂給下一層,參見圖rglm】
3婚陪、Embedding函數(shù)
從前面的定義族沃,我們期望在隱層中找到一個/組嵌入函數(shù)W(這里采用lookup table的方式),使得![][3]具體的泌参,假設指定固定的向量維度脆淹,W("籃球")=(0.2, -0.4, 0.7, ...),W("蘋果")=(0.0, 0.6, -0.1, ...),W初始化時可以賦值給每個維度一個隨機數(shù)沽一,并通過與output層連接建立學習模型/任務后得到有意義的向量未辆。
4、建模
接下來來看看如何建立和訓練模型锯玛。
數(shù)據(jù)準備
為給模型準備數(shù)據(jù)咐柜,我們首先需要定義或獲取n個樣本:![][4]
假如我們有一個句子“姚明 的 籃球 打得 很不錯”。常規(guī)方式是首先由統(tǒng)計語言模型攘残,由中間詞預測周圍詞(SKIP-GRAM)拙友,或由周圍詞預測中間詞(CBOW)等方式,然后以指定的窗口向前推進歼郭,以SKIP-GRAM方式為例遗契,假設推進窗口為2,我們可以得到樣本對:("籃球","的"),("籃球","姚明"),("籃球","打得"),("籃球","很不錯")病曾,X skip至"打得"時牍蜂,得到樣本對 :("打得","籃球"),("打得","的"),("打得","很不錯")漾根,以此類推...我們可以得到用于模型的訓練樣本。
樣本表示
樣本拆解出來了鲫竞,接下來如何用數(shù)值來表達這些樣本對呢辐怕?常用的辦法是將所有的訓練數(shù)據(jù),即“word”對抽取出唯一不重復的單詞來構建詞典表(vocabulary)从绘,然后將樣本數(shù)據(jù)中的“word”表達成one-hot編碼寄疏,編碼時只對有值的位置上為1其他位置均為0,以上面例子為例僵井,“姚明 的 籃球 打得 很不錯”陕截。基于這個句子可以構建維度為5的詞典表:{"姚明":0,"":1,"的":2,"籃球":3,"打得":4,"很不錯":5},那么訓練樣本("籃球","姚明")即可表達為([0,0,1,0,0],0)批什,看起來比較像常規(guī)的多分類數(shù)據(jù)了农曲,這里為了好理解Y表示成了位置編號,后續(xù)在模型中仍以one-hot向量表達驻债。
各層條件分布
神經網絡基于這些訓練樣本將會輸出一個概率分布乳规,這個概率代表著我們的詞典中的每個詞是output word的可能性。更一般的却汉,假設隱層有K個節(jié)點(即生成word對應vector向量的維度)驯妄,對每個樣本,我們需要做兩件事情:
給定隱層后預測output word的概率合砂,即需要建個模型來估計![][5]
將觀測到的input word喂給隱層嵌入函數(shù)青扔,得到隱層的概率分布,![][6]用連接函數(shù)表達即上面提到的(常見的一般會是K個關于x線性組合的方程組翩伪,后面會講到為何不用該方式)![][3]
接下來我們需要構建整體的似然函數(shù)進行優(yōu)化:
目標函數(shù)
分別建立input層-隱層及隱層-output層的連接函數(shù)(RGLM)微猖,input層和隱層的函數(shù)上面已給出,如果假設p(y|w)為正態(tài)分布缘屹,則 log-likelihood loss便是(negative) L2 loss:![][7]凛剥,如果假設p(y|w)為多項分布,則likelihood loss便是softmax loss:![][8]從訓練樣本可以看出轻姿,output層為多分類犁珠,即隱層-output可采用softmax loss.
為了準確預測output word,該網絡需要根據(jù)上述損失函數(shù)學習參數(shù)矩陣W和R(output層)互亮,實際上犁享,對于我們來說,整個學習任務是為了學習隱層的W函數(shù)豹休,即隱層節(jié)點參數(shù)炊昆。當然對于其他任務,比如神經網絡推薦或Fasttext,網絡構造過程類似凤巨,只是學習的任務是學習輸出層的參數(shù)和結構视乐。
模型訓練
常規(guī)優(yōu)化方法會采用梯度下降和反向傳播,由上面的樣本定義敢茁,我們的訓練樣本中input和output均以one-hot表示佑淀,向量極其稀疏(通常完整字典表會是幾十萬維,假設200000)卷要,僅有一個位置的數(shù)值為1渣聚,其余均為0独榴,如果input到隱層的嵌入函數(shù)采用常見方式的話僧叉,假設節(jié)點數(shù)即嵌入向量維度為200,則隱層參數(shù)矩陣每個樣本的迭代將會是1x200000的向量和200000x200矩陣的相乘棺榔,顯然會帶來巨大計算資源的消耗瓶堕,其實每個樣本的隱層參數(shù)僅需要根據(jù)one-hot向量中數(shù)值為1的索引對應的隱層參數(shù)參數(shù)矩陣的該索引行對應的向量取出即可:
經過抽象后我們可以得到上面定義的Embedding函數(shù)/參數(shù)矩陣:
這種方式其實聯(lián)系上面提到的lookup table就容易理解了,即模型中的隱層權重矩陣便成了一個”查找表“(lookup table)症歇,進行矩陣計算時郎笆,只需要直接去查輸入的one-hot向量中提取非零位置的索引,在隱層的對應行輸出就是每個輸入單詞的“嵌入詞向量”忘晤,該過程即完成了嵌入的動作宛蚓。
對于輸出層:
經過隱層的嵌入計算,input word會被映射為1x200的dense向量设塔,再喂給輸出層經過softmax的分類器的計算凄吏,對隨機給定任意output word的嵌入向量計算其預測概率:![][8],這樣基于同一input word闰蛔,替換不同的beta(output word的嵌入向量)得到不同output word的預測概率痕钢。
至此,數(shù)據(jù)的表示及目標損失函數(shù)的定義以及模型訓練過程已拆解完畢序六。接下來任连,再看看訓練性能提升和優(yōu)化的方法。
5例诀、抽樣
基于上面的拆解随抠,我們會發(fā)現(xiàn)其實訓練過程涉及的參數(shù)數(shù)量會非常龐大,以上面的200000個單詞的字典表為例繁涂,隱層嵌入200維的詞向量拱她,那么每次迭代的輸入-隱層權重矩陣和隱層-輸出層的權重矩陣都會有 200000 x 200 = 4000萬個權重,在如此龐大的神經網絡中進行梯度下降是相當慢的爆土,而且需要大量的訓練數(shù)據(jù)來調整這些權重并且避免過擬合椭懊。所以對性能的要求仍然很高,雖然上面已經采用lookup table的方式簡化了一些計算,針對這個問題氧猬,Word2Vec的作者在論文提出了有效的方法背犯,叫“negative sampling”,每個訓練樣本的訓練只會更新一小部分的模型權重盅抚,從而降低計算負擔漠魏,甚至是詞向量的質量⊥基于對假設是柱锹,我們的數(shù)據(jù)中存在大量冗余和噪音,舉例:對于“的”這種常用高頻單詞丰包,我們會發(fā)現(xiàn)一些問題:當我們得到成對的單詞訓練樣本時禁熏,**("的", "籃球") *這樣的訓練樣本并不會給我們提供關于“籃球”更多的語義信息,因為“的”這樣的噪音詞在大部分單詞的上下文中幾乎都會出現(xiàn)邑彪。由于在語料中“的”這樣的常用詞出現(xiàn)概率很大瞧毙,因此我們將會有大量的(”的“,...)這樣的訓練樣本寄症,而這些樣本數(shù)量遠遠超過了我們學習“的”這個詞向量所需的訓練樣本數(shù)宙彪。所以在設計抽樣方法的時候可以對這樣的樣本直接排除在訓練樣本之外,對于其他樣本對隨機抽取少量的負樣本進行參數(shù)的更新有巧,而不是對one-hot向量中所有200000個位置對樣本都進行計算释漆,從而大大提高訓練效率。
上面敘述的有點繁雜篮迎,總結起來就是在對給定input word計算softmax時男图,不去更新所有詞表中word的輸出概率,而是從該樣本的output word之外隨機抽樣有限個(比如只抽樣5個word)作為負樣本計算其概率柑潦,進一步進行梯度和參數(shù)的更新享言。也就是說通過負樣本抽樣對于每次訓練只更新(5+1)個beta向量對應的參數(shù),也就是2006=1200個參數(shù)渗鬼,這樣與4000萬個相比览露,需要更新的參數(shù)占比僅為0.003%,效率提升可想而知譬胎。
6差牛、基于tensorflow的實現(xiàn)
數(shù)據(jù)加載
import os
def load_w2c_textcn_dataset(path='./data/'):
"""
Returns
--------
word_list_all : a list
a list of string (word).\n
要求:中文語料需要先分詞
"""
print("Load or Download chinese text corpus Dataset> {}".format(path))
filename = 'wiki_cn.cut'
word_list_all=[]
with open(os.path.join(path, filename)) as f:
for line in f:
word_list=line.strip().split()
for idx, word in enumerate(word_list):
word_list[idx] = word_list[idx].decode('utf-8')
#print word_list[idx]
word_list_all.append(word_list[idx])
return word_list_all
words=load_w2c_textcn_dataset(path='./data/')
print len(words)
字典構建
import collections
vocabulary_size = 200000
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? # dictionary['UNK']
unk_count = unk_count + 1
data.append(index)
count[0][1] = unk_count
reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
del words
batch數(shù)據(jù)生成器
data_index = 0
def generate_batch(batch_size, num_skips, skip_window):
global data_index
batch = np.ndarray(shape=(batch_size), dtype=np.int32)
labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
span = 2 * skip_window + 1? # [ skip_window target skip_window ]
buf = collections.deque(maxlen=span)
for _ in xrange(span):
buf.append(data[data_index])
data_index = (data_index + 1) % len(data)
for i in xrange(batch_size // num_skips):
target = skip_window? # target label at the center of the buffer
targets_to_avoid = [ skip_window ]
for j in xrange(num_skips):
while target in targets_to_avoid:
target = random.randint(0, span - 1)
targets_to_avoid.append(target)
batch[i * num_skips + j] = buf[skip_window]
labels[i * num_skips + j, 0] = buf[target]
buf.append(data[data_index])
data_index = (data_index + 1) % len(data)
return batch, labels
模型構建
import tensorflow as tf
import collections
import numpy as np
batch_size = 128
embedding_size = 128? # 生成向量維度.
skip_window = 2? ? ? # 左右窗口.
num_skips = 2? ? ? ? # 同一個keyword產生label的次數(shù).
num_sampled = 64? ? ? # 負樣本抽樣數(shù).
graph = tf.Graph()
with graph.as_default(), tf.device('/cpu:0'):
train_dataset = tf.placeholder(tf.int32, shape=[batch_size])
train_labels? = tf.placeholder(tf.int32, shape=[batch_size, 1])
embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
softmax_weights = tf.Variable(
tf.truncated_normal([vocabulary_size, embedding_size], stddev=1.0/np.sqrt(embedding_size)))
softmax_biases = tf.Variable(tf.zeros([vocabulary_size]))
embed = tf.nn.embedding_lookup(embeddings, train_dataset)
loss = tf.reduce_mean(
tf.nn.sampled_softmax_loss(weights=softmax_weights, biases=softmax_biases, inputs=embed,
labels=train_labels, num_sampled=num_sampled, num_classes=vocabulary_size))
optimizer = tf.train.AdagradOptimizer(1.0).minimize(loss)
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
normalized_embeddings = embeddings / norm
模型訓練
num_steps = 500001
import random
with tf.Session(graph=graph) as session:
tf.global_variables_initializer().run()
average_loss = 0
for step in range(num_steps):
batch_data, batch_labels = generate_batch(batch_size, num_skips, skip_window)
feed_dict = {train_dataset : batch_data, train_labels : batch_labels}
_, l = session.run([optimizer, loss], feed_dict=feed_dict)
average_loss += l
if step % 100000 == 0 and step > 0:
print('Average loss at step %d: %f' % (step, average_loss / 100000))
average_loss = 0
word2vec = normalized_embeddings.eval()
最近鄰
distances = -word2vec[dictionary[u'數(shù)據(jù)']].reshape((1, -1)).dot(word2vec.T)
inds = np.argsort(distances.ravel())[1:6]
print(' '.join([reverse_dictionary[i] for i in inds]))
----------------------------------------------
資料 統(tǒng)計 顯示 信息 證據(jù)
[1]? Peter McCullagh, John A Nelder,Generalized linear models., , 1989
[2] The seminal paper,A Neural Probabilistic Language Model(Bengio,et al.2003)has a great deal of insight about why word embeddings are powerful.
[3]:https://erikbern.com/2014/06/28/recurrent-neural-networks-for-collaborative-filtering.html
原文參考:http://www.reibang.com/p/d44ce1e3ec2f