1、 前言
本篇主要介紹關(guān)鍵詞的向量表示,也就是大家熟悉的word embedding。自Google 2013 年開(kāi)源word2vec算法程序以后,它的簡(jiǎn)單、高效、實(shí)用,很快引起業(yè)界眾人的關(guān)注和應(yīng)用,為搜索引擎、[廣告系統(tǒng)-谷歌的wide & deep learning][2]、[推薦系統(tǒng)][1]等互聯(lián)網(wǎng)服務(wù)提供新的基礎(chǔ)技術(shù)和思路。
何為Embedding?
開(kāi)篇之前首先需要明白一個(gè)概念何為Embedding?Embedding可以看作是數(shù)學(xué)上的一個(gè)空間映射(Mapping):map( lambda y: f(x) ),該映射的特點(diǎn)是:?jiǎn)紊洌ㄔ跀?shù)學(xué)里,單射函數(shù)為一函數(shù),其將不同的引數(shù)連接至不同的值上。更精確地說(shuō),函數(shù)f被稱(chēng)為是單射時(shí),對(duì)每一值域內(nèi)的y,存在至多一個(gè)定義域內(nèi)的x使得f(x) = y。)、映射前后結(jié)構(gòu)不變,對(duì)應(yīng)到word embedding概念中可以理解為尋找一個(gè)函數(shù)或映射,生成新的空間上的表達(dá),把單詞one-hot所表達(dá)的X空間信息映射到Y(jié)的多維空間向量。
接下來(lái),將以模型的角度分解embedding映射函數(shù)及新空間內(nèi)表達(dá)的建模過(guò)程:
非監(jiān)督的“監(jiān)督學(xué)習(xí)”
從應(yīng)用角度,新空間內(nèi)映射函數(shù)的學(xué)習(xí)方法不需要大量的人工標(biāo)記樣本就可以得到質(zhì)量還不錯(cuò)的embedding向量,沒(méi)有具體的應(yīng)用任務(wù)導(dǎo)向,從這個(gè)角度可以看作非監(jiān)督的學(xué)習(xí)過(guò)程,而從建模角度,向量提取的建模過(guò)程是 個(gè)分類(lèi)模型,又可以看做是監(jiān)督學(xué)習(xí),只是這個(gè)監(jiān)督?jīng)]有實(shí)際的監(jiān)督意義,當(dāng)然后來(lái)有的應(yīng)該將word2vec的前段表達(dá)方式喂給標(biāo)注的過(guò)文本,形成真正意義上的監(jiān)督學(xué)習(xí),如Facebook的FastText。
2、一層隱層神經(jīng)網(wǎng)絡(luò)
帶有一個(gè)隱層的神經(jīng)網(wǎng)絡(luò)有以下普遍特性:理論上給定足夠該隱層節(jié)點(diǎn)數(shù),這個(gè)隱層可以近似擬合任意函數(shù),本質(zhì)上,隱層是上一層的嵌入(Embedding)函數(shù)的近似表示,而且可以被用做lookup表(后面會(huì)介紹),word2vec也是基于該層去找到輸入word的嵌入向量表示,然后再建立下一層和當(dāng)前層的連接(connections),來(lái)控制目標(biāo)函數(shù)的誤差。【進(jìn)一步抽象,如果從統(tǒng)計(jì)的角度,其實(shí)不同層之間的統(tǒng)計(jì)關(guān)系是一種遞歸的廣義線(xiàn)性關(guān)系(遞歸廣義線(xiàn)性模型),每一層通過(guò)線(xiàn)性組合對(duì)前一層進(jìn)行變換,然后以一些非線(xiàn)性連接函數(shù)(不同函數(shù)對(duì)應(yīng)output label不同的統(tǒng)計(jì)分布,比如softmax對(duì)應(yīng)多項(xiàng)目分布,sigmoid對(duì)應(yīng)二項(xiàng)分布等)得到非線(xiàn)性結(jié)果喂給下一層,參見(jiàn)圖rglm】
3、Embedding函數(shù)
從前面的定義,我們期望在隱層中找到一個(gè)/組嵌入函數(shù)W(這里采用lookup table的方式),使得![][3]具體的,假設(shè)指定固定的向量維度,W("籃球")=(0.2, -0.4, 0.7, ...),W("蘋(píng)果")=(0.0, 0.6, -0.1, ...),W初始化時(shí)可以賦值給每個(gè)維度一個(gè)隨機(jī)數(shù),并通過(guò)與output層連接建立學(xué)習(xí)模型/任務(wù)后得到有意義的向量。
4、建模
接下來(lái)來(lái)看看如何建立和訓(xùn)練模型。
數(shù)據(jù)準(zhǔn)備
為給模型準(zhǔn)備數(shù)據(jù),我們首先需要定義或獲取n個(gè)樣本:![][4]
假如我們有一個(gè)句子“姚明 的 籃球 打得 很不錯(cuò)”。常規(guī)方式是首先由統(tǒng)計(jì)語(yǔ)言模型,由中間詞預(yù)測(cè)周?chē)~(SKIP-GRAM),或由周?chē)~預(yù)測(cè)中間詞(CBOW)等方式,然后以指定的窗口向前推進(jìn),以SKIP-GRAM方式為例,假設(shè)推進(jìn)窗口為2,我們可以得到樣本對(duì):("籃球","的"),("籃球","姚明"),("籃球","打得"),("籃球","很不錯(cuò)"),X skip至"打得"時(shí),得到樣本對(duì) :("打得","籃球"),("打得","的"),("打得","很不錯(cuò)"),以此類(lèi)推...我們可以得到用于模型的訓(xùn)練樣本。樣本表示
樣本拆解出來(lái)了,接下來(lái)如何用數(shù)值來(lái)表達(dá)這些樣本對(duì)呢?常用的辦法是將所有的訓(xùn)練數(shù)據(jù),即“word”對(duì)抽取出唯一不重復(fù)的單詞來(lái)構(gòu)建詞典表(vocabulary),然后將樣本數(shù)據(jù)中的“word”表達(dá)成one-hot編碼,編碼時(shí)只對(duì)有值的位置上為1其他位置均為0,以上面例子為例,“姚明 的 籃球 打得 很不錯(cuò)”。基于這個(gè)句子可以構(gòu)建維度為5的詞典表:{"姚明":0,"":1,"的":2,"籃球":3,"打得":4,"很不錯(cuò)":5},那么訓(xùn)練樣本("籃球","姚明")即可表達(dá)為([0,0,1,0,0],0),看起來(lái)比較像常規(guī)的多分類(lèi)數(shù)據(jù)了,這里為了好理解Y表示成了位置編號(hào),后續(xù)在模型中仍以one-hot向量表達(dá)。-
各層條件分布
神經(jīng)網(wǎng)絡(luò)基于這些訓(xùn)練樣本將會(huì)輸出一個(gè)概率分布,這個(gè)概率代表著我們的詞典中的每個(gè)詞是output word的可能性。更一般的,假設(shè)隱層有K個(gè)節(jié)點(diǎn)(即生成word對(duì)應(yīng)vector向量的維度),對(duì)每個(gè)樣本,我們需要做兩件事情:- 給定隱層后預(yù)測(cè)output word的概率,即需要建個(gè)模型來(lái)估計(jì)![][5]
- 將觀測(cè)到的input word喂給隱層嵌入函數(shù),得到隱層的概率分布,![][6]用連接函數(shù)表達(dá)即上面提到的(常見(jiàn)的一般會(huì)是K個(gè)關(guān)于x線(xiàn)性組合的方程組,后面會(huì)講到為何不用該方式)![][3]
接下來(lái)我們需要構(gòu)建整體的似然函數(shù)進(jìn)行優(yōu)化:
目標(biāo)函數(shù)
分別建立input層-隱層及隱層-output層的連接函數(shù)(RGLM),input層和隱層的函數(shù)上面已給出,如果假設(shè)p(y|w)為正態(tài)分布,則 log-likelihood loss便是(negative) L2 loss:![][7],如果假設(shè)p(y|w)為多項(xiàng)分布,則likelihood loss便是softmax loss:![][8]從訓(xùn)練樣本可以看出,output層為多分類(lèi),即隱層-output可采用softmax loss.
為了準(zhǔn)確預(yù)測(cè)output word,該網(wǎng)絡(luò)需要根據(jù)上述損失函數(shù)學(xué)習(xí)參數(shù)矩陣W和R(output層),實(shí)際上,對(duì)于我們來(lái)說(shuō),整個(gè)學(xué)習(xí)任務(wù)是為了學(xué)習(xí)隱層的W函數(shù),即隱層節(jié)點(diǎn)參數(shù)。當(dāng)然對(duì)于其他任務(wù),比如神經(jīng)網(wǎng)絡(luò)推薦或Fasttext,網(wǎng)絡(luò)構(gòu)造過(guò)程類(lèi)似,只是學(xué)習(xí)的任務(wù)是學(xué)習(xí)輸出層的參數(shù)和結(jié)構(gòu)。-
模型訓(xùn)練
常規(guī)優(yōu)化方法會(huì)采用梯度下降和反向傳播,由上面的樣本定義,我們的訓(xùn)練樣本中input和output均以one-hot表示,向量極其稀疏(通常完整字典表會(huì)是幾十萬(wàn)維,假設(shè)200000),僅有一個(gè)位置的數(shù)值為1,其余均為0,如果input到隱層的嵌入函數(shù)采用常見(jiàn)方式的話(huà),假設(shè)節(jié)點(diǎn)數(shù)即嵌入向量維度為200,則隱層參數(shù)矩陣每個(gè)樣本的迭代將會(huì)是1x200000的向量和200000x200矩陣的相乘,顯然會(huì)帶來(lái)巨大計(jì)算資源的消耗,其實(shí)每個(gè)樣本的隱層參數(shù)僅需要根據(jù)one-hot向量中數(shù)值為1的索引對(duì)應(yīng)的隱層參數(shù)參數(shù)矩陣的該索引行對(duì)應(yīng)的向量取出即可:
embedding.png
經(jīng)過(guò)抽象后我們可以得到上面定義的Embedding函數(shù)/參數(shù)矩陣:
embedding-abstract.png
這種方式其實(shí)聯(lián)系上面提到的lookup table就容易理解了,即模型中的隱層權(quán)重矩陣便成了一個(gè)”查找表“(lookup table),進(jìn)行矩陣計(jì)算時(shí),只需要直接去查輸入的one-hot向量中提取非零位置的索引,在隱層的對(duì)應(yīng)行輸出就是每個(gè)輸入單詞的“嵌入詞向量”,該過(guò)程即完成了嵌入的動(dòng)作。
對(duì)于輸出層:
經(jīng)過(guò)隱層的嵌入計(jì)算,input word會(huì)被映射為1x200的dense向量,再喂給輸出層經(jīng)過(guò)softmax的分類(lèi)器的計(jì)算,對(duì)隨機(jī)給定任意output word的嵌入向量計(jì)算其預(yù)測(cè)概率:![][8],這樣基于同一input word,替換不同的beta(output word的嵌入向量)得到不同output word的預(yù)測(cè)概率。
至此,數(shù)據(jù)的表示及目標(biāo)損失函數(shù)的定義以及模型訓(xùn)練過(guò)程已拆解完畢。接下來(lái),再看看訓(xùn)練性能提升和優(yōu)化的方法。
5、抽樣
基于上面的拆解,我們會(huì)發(fā)現(xiàn)其實(shí)訓(xùn)練過(guò)程涉及的參數(shù)數(shù)量會(huì)非常龐大,以上面的200000個(gè)單詞的字典表為例,隱層嵌入200維的詞向量,那么每次迭代的輸入-隱層權(quán)重矩陣和隱層-輸出層的權(quán)重矩陣都會(huì)有 200000 x 200 = 4000萬(wàn)個(gè)權(quán)重,在如此龐大的神經(jīng)網(wǎng)絡(luò)中進(jìn)行梯度下降是相當(dāng)慢的,而且需要大量的訓(xùn)練數(shù)據(jù)來(lái)調(diào)整這些權(quán)重并且避免過(guò)擬合。所以對(duì)性能的要求仍然很高,雖然上面已經(jīng)采用lookup table的方式簡(jiǎn)化了一些計(jì)算,針對(duì)這個(gè)問(wèn)題,Word2Vec的作者在論文提出了有效的方法,叫“negative sampling”,每個(gè)訓(xùn)練樣本的訓(xùn)練只會(huì)更新一小部分的模型權(quán)重,從而降低計(jì)算負(fù)擔(dān),甚至是詞向量的質(zhì)量。基于對(duì)假設(shè)是,我們的數(shù)據(jù)中存在大量冗余和噪音,舉例:對(duì)于“的”這種常用高頻單詞,我們會(huì)發(fā)現(xiàn)一些問(wèn)題:當(dāng)我們得到成對(duì)的單詞訓(xùn)練樣本時(shí),**("的", "籃球") *這樣的訓(xùn)練樣本并不會(huì)給我們提供關(guān)于“籃球”更多的語(yǔ)義信息,因?yàn)椤暗摹边@樣的噪音詞在大部分單詞的上下文中幾乎都會(huì)出現(xiàn)。由于在語(yǔ)料中“的”這樣的常用詞出現(xiàn)概率很大,因此我們將會(huì)有大量的(”的“,...)這樣的訓(xùn)練樣本,而這些樣本數(shù)量遠(yuǎn)遠(yuǎn)超過(guò)了我們學(xué)習(xí)“的”這個(gè)詞向量所需的訓(xùn)練樣本數(shù)。所以在設(shè)計(jì)抽樣方法的時(shí)候可以對(duì)這樣的樣本直接排除在訓(xùn)練樣本之外,對(duì)于其他樣本對(duì)隨機(jī)抽取少量的負(fù)樣本進(jìn)行參數(shù)的更新,而不是對(duì)one-hot向量中所有200000個(gè)位置對(duì)樣本都進(jìn)行計(jì)算,從而大大提高訓(xùn)練效率。
上面敘述的有點(diǎn)繁雜,總結(jié)起來(lái)就是在對(duì)給定input word計(jì)算softmax時(shí),不去更新所有詞表中word的輸出概率,而是從該樣本的output word之外隨機(jī)抽樣有限個(gè)(比如只抽樣5個(gè)word)作為負(fù)樣本計(jì)算其概率,進(jìn)一步進(jìn)行梯度和參數(shù)的更新。也就是說(shuō)通過(guò)負(fù)樣本抽樣對(duì)于每次訓(xùn)練只更新(5+1)個(gè)beta向量對(duì)應(yīng)的參數(shù),也就是2006=1200個(gè)參數(shù),這樣與4000萬(wàn)個(gè)相比,需要更新的參數(shù)占比僅為0.003%,效率提升可想而知。
6、基于tensorflow的實(shí)現(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
要求:中文語(yǔ)料需要先分詞
"""
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)
- 字典構(gòu)建
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
- 模型構(gòu)建
import tensorflow as tf
import collections
import numpy as np
batch_size = 128
embedding_size = 128 # 生成向量維度.
skip_window = 2 # 左右窗口.
num_skips = 2 # 同一個(gè)keyword產(chǎn)生label的次數(shù).
num_sampled = 64 # 負(fù)樣本抽樣數(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
- 模型訓(xùn)練
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ì) 顯示 信息 證據(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
[4]:https://research.googleblog.com/2016/06/wide-deep-learning-better-together-with.html?utm_source=tuicool&utm_medium=referral