詞嵌入向量(WordEmbedding)是NLP里面一個(gè)重要的概念惦蚊,我們可以利用WordEmbedding將一個(gè)單詞轉(zhuǎn)換成固定長(zhǎng)度的向量表示器虾,從而便于進(jìn)行數(shù)學(xué)處理。本文將介紹WordEmbedding的使用方式蹦锋,并講解如何通過神經(jīng)網(wǎng)絡(luò)生成WordEmbedding兆沙。
WordEmbedding的使用
使用數(shù)學(xué)模型處理文本語料的第一步就是把文本轉(zhuǎn)換成數(shù)學(xué)表示,有兩種方法莉掂,第一種方法可以通過one-hot矩陣表示一個(gè)單詞挤悉,one-hot矩陣是指每一行有且只有一個(gè)元素為1,其他元素都是0的矩陣巫湘。針對(duì)字典中的每個(gè)單詞装悲,我們分配一個(gè)編號(hào),對(duì)某句話進(jìn)行編碼時(shí)尚氛,將里面的每個(gè)單詞轉(zhuǎn)換成字典里面這個(gè)單詞編號(hào)對(duì)應(yīng)的位置為1的one-hot矩陣就可以了诀诊。比如我們要表達(dá)“the cat sat on the mat”,可以使用如下的矩陣表示阅嘶。
one-hot表示方式很直觀属瓣,但是有兩個(gè)缺點(diǎn),第一讯柔,矩陣的每一維長(zhǎng)度都是字典的長(zhǎng)度抡蛙,比如字典包含10000個(gè)單詞,那么每個(gè)單詞對(duì)應(yīng)的one-hot向量就是1X10000的向量魂迄,而這個(gè)向量只有一個(gè)位置為1粗截,其余都是0,浪費(fèi)空間捣炬,不利于計(jì)算熊昌。第二绽榛,one-hot矩陣相當(dāng)于簡(jiǎn)單的給每個(gè)單詞編了個(gè)號(hào),但是單詞和單詞之間的關(guān)系則完全體現(xiàn)不出來婿屹。比如“cat”和“mouse”的關(guān)聯(lián)性要高于“cat”和“cellphone”灭美,這種關(guān)系在one-hot表示法中就沒有體現(xiàn)出來。
WordEmbedding解決了這兩個(gè)問題昂利。WordEmbedding矩陣給每個(gè)單詞分配一個(gè)固定長(zhǎng)度的向量表示届腐,這個(gè)長(zhǎng)度可以自行設(shè)定,比如300蜂奸,實(shí)際上會(huì)遠(yuǎn)遠(yuǎn)小于字典長(zhǎng)度(比如10000)犁苏。而且兩個(gè)單詞向量之間的夾角值可以作為他們之間關(guān)系的一個(gè)衡量。如下表示:
通過簡(jiǎn)單的余弦函數(shù)窝撵,我們就可以計(jì)算兩個(gè)單詞之間的相關(guān)性傀顾,簡(jiǎn)單高效:
因?yàn)閃ordEmbedding節(jié)省空間和便于計(jì)算的特點(diǎn),使得它廣泛應(yīng)用于NLP領(lǐng)域碌奉。接下來我們講解如何通過神經(jīng)網(wǎng)絡(luò)生成WordEmbedding短曾。
WordEmbedding的生成
WordEmbedding的生成我們使用tensorflow,通過構(gòu)造一個(gè)包含了一個(gè)隱藏層的神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)赐劣。
下面是下載數(shù)據(jù)和加載數(shù)據(jù)的代碼嫉拐,一看就懂。訓(xùn)練數(shù)據(jù)我們使用的是http://mattmahoney.net/dc/enwik8.zip數(shù)據(jù)魁兼,里面是維基百科的數(shù)據(jù)婉徘。
def maybe_download(filename, url):
"""Download a file if not present, and make sure it's the right size."""
if not os.path.exists(filename):
filename, _ = urllib.urlretrieve(url + filename, filename)
return filename
# Read the data into a list of strings.
def read_data(filename):
"""Extract the first file enclosed in a zip file as a list of words."""
with zipfile.ZipFile(filename) as f:
data = tf.compat.as_str(f.read(f.namelist()[0])).split()
return data
def collect_data(vocabulary_size=10000):
url = 'http://mattmahoney.net/dc/'
filename = maybe_download('enwik8.zip', url)
vocabulary = read_data(filename)
print(vocabulary[:7])
data, count, dictionary, reverse_dictionary = build_dataset(vocabulary, vocabulary_size)
del vocabulary # Hint to reduce memory.
return data, count, dictionary, reverse_dictionary
接下來是如何構(gòu)建訓(xùn)練數(shù)據(jù)。構(gòu)建訓(xùn)練數(shù)據(jù)主要包括統(tǒng)計(jì)詞頻咐汞,生成字典文件盖呼,并且根據(jù)字典文件給訓(xùn)練源數(shù)據(jù)中的單詞進(jìn)行編號(hào)等工作。我們生成的字典不可能包含所有的單詞化撕,一般我們按照單詞頻率由高到低排序几晤,選擇覆蓋率大于95%的單詞加入詞典就可以了,因?yàn)樵~典越大植阴,覆蓋的場(chǎng)景越大蟹瘾,同時(shí)計(jì)算開銷越大,這是一個(gè)均衡掠手。下面的代碼展示了這個(gè)過程憾朴,首先統(tǒng)計(jì)所有輸入語料的詞頻,選出頻率最高的10000個(gè)單詞加入字典喷鸽。同時(shí)在字典第一個(gè)位置插入一項(xiàng)“UNK"代表不能識(shí)別的單詞众雷,也就是未出現(xiàn)在字典的單詞統(tǒng)一用UNK表示。然后給字典里每個(gè)詞編號(hào),并把源句子里每個(gè)詞表示成在字典中的編號(hào)报腔。我們可以根據(jù)每個(gè)詞的編號(hào)查找WordEmbedding中的向量表示株搔。
def build_dataset(words, n_words):
"""Process raw inputs into a dataset."""
count = [['UNK', -1]]
# [['UNK', -1], ['i', 500], ['the', 498], ['man', 312], ...]
count.extend(collections.Counter(words).most_common(n_words - 1))
# dictionary {'UNK':0, 'i':1, 'the': 2, 'man':3, ...}
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 += 1
data.append(index)
count[0][1] = unk_count
reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
# data: "I like cat" -> [1, 21, 124]
# count: [['UNK', 349], ['i', 500], ['the', 498], ['man', 312], ...]
# dictionary {'UNK':0, 'i':1, 'the': 2, 'man':3, ...}
# reversed_dictionary: {0:'UNK', 1:'i', 2:'the', 3:'man', ...}
return data, count, dictionary, reversed_dictionary
接下來我們看一下如何將源句子轉(zhuǎn)換成訓(xùn)練過程的輸入和輸出剖淀,這一步是比較關(guān)鍵的纯蛾。有兩種業(yè)界常用的WordEmbedding生成方式,Continuous Bag Of Words (CBOW)方法和skip-gram方法纵隔,我們采用skip-gram方法翻诉。訓(xùn)練的目的是獲得能夠反映任意兩個(gè)單詞之間關(guān)系的單詞向量表示,所以我們的輸入到輸出的映射也要翻譯兩個(gè)單詞之間的關(guān)聯(lián)捌刮。skip-gram的思路是將所有的源句子按固定長(zhǎng)度(比如128個(gè)單詞)分割成很多batch碰煌。對(duì)于每個(gè)batch,從前往后每次選取長(zhǎng)度為skip_window的窗口(我們?cè)O(shè)定skip_window=5)绅作。對(duì)于窗口中的5個(gè)單詞芦圾,我們生成兩個(gè)source-target數(shù)據(jù)對(duì),這兩個(gè)source-target對(duì)的source都是窗口中間的單詞俄认,也就是第三個(gè)單詞个少,然后從另外四個(gè)單詞中隨機(jī)選取兩個(gè)作為兩個(gè)target單詞。然后窗口向后移動(dòng)一個(gè)單詞眯杏,每次向后移動(dòng)一個(gè)位置獲取下5個(gè)單詞夜焦,一共循環(huán)64次,獲取到64X2=128個(gè)source-target對(duì)岂贩,作為一個(gè)batch的訓(xùn)練數(shù)據(jù)茫经。總的思路就是把某個(gè)單詞和附近的單詞組對(duì)萎津,作為輸入和輸出卸伞。這里同一個(gè)source單詞,會(huì)被映射到不同的target單詞锉屈,這樣理論上可以獲取任意兩個(gè)單詞之間的關(guān)系荤傲。
比如對(duì)于句子"cat and dog play balls on the floor",第一個(gè)窗口就是“cat and dog play balls"部念,生成的兩個(gè)source-target對(duì)可能是下面中的任意兩個(gè):
dog -> cat
dog -> and
dog -> balls
dog -> play
第二個(gè)窗口是"and dog play balls on"弃酌,生成的兩個(gè)source-target對(duì)可能是下面中的任意兩個(gè):
play -> and
play -> balls
play -> dog
play -> on
下面是代碼實(shí)現(xiàn):
def generate_batch(data, batch_size, num_skips, skip_window):
global data_index
assert batch_size % num_skips == 0
assert num_skips <= 2 * skip_window
batch = np.ndarray(shape=(batch_size), dtype=np.int32)
context = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
span = 2 * skip_window + 1 # span含義 -> [ skip_window input_word skip_window ]
# 初始化最大長(zhǎng)度為span的雙端隊(duì)列,超過最大長(zhǎng)度后再添加數(shù)據(jù)儡炼,會(huì)從另一端刪除容不下的數(shù)據(jù)
# buffer: 1, 21, 124, 438, 11
buffer = collections.deque(maxlen=span)
for _ in range(span):
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
for i in range(batch_size // num_skips): # 128 / 2
# target: 2
target = skip_window # input word at the center of the buffer
# targets_to_avoid: [2]
targets_to_avoid = [skip_window] # 需要忽略的詞在當(dāng)前span的位置
# 更新源單詞為當(dāng)前5個(gè)單詞的中間單詞
source_word = buffer[skip_window]
# 隨機(jī)選擇的5個(gè)span單詞中除了源單詞之外的4個(gè)單詞中的兩個(gè)
for j in range(num_skips):
while target in targets_to_avoid: # 隨機(jī)重新從5個(gè)詞中選擇一個(gè)尚未選擇過的詞
target = random.randint(0, span - 1)
targets_to_avoid.append(target)
# batch添加源單詞
batch[i * num_skips + j] = source_word
# context添加目標(biāo)單詞妓湘,單詞來自隨機(jī)選擇的5個(gè)span單詞中除了源單詞之外的4個(gè)單詞中的兩個(gè)
context[i * num_skips + j, 0] = buffer[target]
# 往雙端隊(duì)列中添加下一個(gè)單詞,雙端隊(duì)列會(huì)自動(dòng)將容不下的數(shù)據(jù)從另一端刪除
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
# Backtrack a little bit to avoid skipping words in the end of a batch
data_index = (data_index + len(data) - span) % len(data)
return batch, context
接下來是構(gòu)建神經(jīng)網(wǎng)絡(luò)的過程乌询,我們構(gòu)建了一個(gè)包含一個(gè)隱藏層的神經(jīng)網(wǎng)絡(luò)榜贴,該隱藏層包含300個(gè)節(jié)點(diǎn),這個(gè)數(shù)量和我們要構(gòu)造的WordEmbedding維度一致。
with graph.as_default():
# 定義輸入輸出
train_sources = tf.placeholder(tf.int32, shape=[batch_size])
train_targets = tf.placeholder(tf.int32, shape=[batch_size, 1])
valid_dataset = tf.constant(valid_examples, dtype=tf.int32)
# 初始化embeddings矩陣,這個(gè)就是經(jīng)過多步訓(xùn)練后最終我們需要的embedding
embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
# 將輸入序列轉(zhuǎn)換成embedding表示, [batch_size, embedding_size]
embed = tf.nn.embedding_lookup(embeddings, train_sources)
# 初始化權(quán)重
weights = tf.Variable(tf.truncated_normal([embedding_size, vocabulary_size], stddev=1.0 / math.sqrt(embedding_size)))
biases = tf.Variable(tf.zeros([vocabulary_size]))
# 隱藏層輸出結(jié)果的計(jì)算, [batch_size, vocabulary_size]
hidden_out = tf.transpose(tf.matmul(tf.transpose(weights), tf.transpose(embed))) + biases
# 將label結(jié)果轉(zhuǎn)換成one-hot表示, [batch_size, 1] -> [batch_size, vocabulary_size]
train_one_hot = tf.one_hot(train_targets, vocabulary_size)
# 根據(jù)隱藏層輸出結(jié)果和標(biāo)記結(jié)果唬党,計(jì)算交叉熵
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=hidden_out, labels=train_one_hot))
# 隨機(jī)梯度下降進(jìn)行一步反向傳遞
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(cross_entropy)
# 計(jì)算驗(yàn)證數(shù)據(jù)集中的單詞和字典表里所有單詞的相似度鹃共,并在validate過程輸出相似度最高的幾個(gè)單詞
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
normalized_embeddings = embeddings / norm
valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings, valid_dataset)
similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)
# 參數(shù)初始化賦值
init = tf.global_variables_initializer()
我們首先隨機(jī)初始化embeddings矩陣,通過tf.nn.embedding_lookup函數(shù)將輸入序列轉(zhuǎn)換成WordEmbedding表示作為隱藏層的輸入驶拱。初始化weights和biases霜浴,計(jì)算隱藏層的輸出。然后計(jì)算輸出和target結(jié)果的交叉熵蓝纲,使用GradientDescentOptimizer完成一次反向傳遞阴孟,更新可訓(xùn)練的參數(shù),包括embeddings變量税迷。在Validate過程中永丝,對(duì)測(cè)試數(shù)據(jù)集中的單詞,利用embeddings矩陣計(jì)算測(cè)試單詞和所有其他單詞的相似度箭养,輸出相似度最高的幾個(gè)單詞慕嚷,看看它們相關(guān)性如何,作為一種驗(yàn)證方式毕泌。
通過這個(gè)神經(jīng)網(wǎng)絡(luò)喝检,就可以完成WordEmbedding的訓(xùn)練,繼而應(yīng)用于其他NLP的任務(wù)懈词。
完整代碼可以參考Git Demo Code
參考:
https://www.tensorflow.org/tutorials/word2vec
http://mccormickml.com/2016/04/19/word2vec-tutorial-the-skip-gram-model/
http://adventuresinmachinelearning.com/word2vec-tutorial-tensorflow/