什麼是Word2Vec模型?它如何實現?
作者詳細介紹了 word2vector 模型的模型架構,以及 TensorFlow 的實現過程,包括數據準備、建立模型、構建驗證集,並給出了運行結果示例。
詞嵌入 softmax 訓練器
在接下來的教程中,我將解決的問題是該如何建立一個深度學習模型預測文本序列。然而,在建立模型之前,我們必須理解一些關鍵的自然語言處理(NLP)的思想。NLP 的關鍵思想之一是如何有效地將單詞轉換為數字向量,然後將這些數字向量「饋送」到機器學習模型中進行預測。本教程將對現在使用的主要技術,即「Word2Vec」進行介紹。在討論了相關的背景材料之後,我們將使用 TensorFlow 實現 Word2Vec 嵌入。要快速了解 TensorFlow,請查看我的 TensorFlow 教程:http://adventuresinmachinelearning.com/python-tensorflow-tutorial/
我們為什麼需要 Word2Vec如果我們想把單詞輸入機器學習模型,除非使用基於樹的方法,否則需要把單詞轉換成一些數字向量。一種直接的方法是使用「獨熱編碼」方法將單詞轉換為稀疏表示,向量中只有一個元素設置為 1,其餘為 0。我們構建分類任務也採用了相同的方法——詳情請參考該教程:http://adventuresinmachinelearning.com/neural-networks-tutorial/#setting-up-output
所以,我們可以使用如下的向量表示句子「The cat sat on the mat」:
我們在此將一個六個字的句子轉換為一個 6*5 的矩陣,其中 5 是辭彙量(「the」有重複)。然而,在實際應用中,我們希望深度學習模型能夠在辭彙量很大(10,000 字以上)的情況下進行學習。從這裡能看到使用「獨熱碼」表示單詞的效率問題——對這些辭彙建模的任何神經網路的輸入層至少都有 10,000 個節點。不僅如此,這種方法剝離了單詞的所有局部語境——也就是說它會去掉句子中(或句子之間)緊密相連的單詞的信息。
例如,我們可能想看到「United」和「States」靠得很近,或者是「Soviet」和「Union」,或者「食物」和「吃」等等。如果我們試圖以這種方法對自然語言建模,會丟失所有此類信息,這將是一個很大的疏漏。因此,我們需要使用更高效的方法表示文本數據,而這種方法可以保存單詞的上下文的信息。這是 Word2Vec 方法發明的初衷。
Word2Vec 方法論如上文所述,Word2Vec 方法由兩部分組成。首先是將高維獨熱形式表示的單詞映射成低維向量。例如將 10,000 列的矩陣轉換為 300 列的矩陣。這個過程被稱為詞嵌入。第二個目標是在保留單詞上下文的同時,從一定程度上保留其意義。在 Word2Vec 方法中實現這兩個目標的方法之一是,輸入一個詞,然後試著估計其他詞出現在該詞附近的概率,稱為 skip-gram 方法。還有一種與此相反的被稱為連續詞袋模型(Continuous Bag Of Words,CBOW)的方法——CBOW 將一些上下文詞語作為輸入,並通過評估概率找出最適合(概率最大)該上下文的詞。在本教程中,我們將重點介紹 skip-gram 方法。
什麼是 gram?gram 是一個有 n 個單詞的組(group),其中 n 是 gram 的窗口大小(window size)。因此,對「The cat sat on the mat」這句話來說,這句話用 3 個 gram 表示的話,是「The cat sat」、「cat sat on」、「sat on the」、「on the mat」。「skip」指一個輸入詞在不同的上下文詞的情況下,在數據集中重複的次數(這點會在稍後陳述)。這些 gram 被輸入 Word2Vec 上下文預測系統。舉個例子,假設輸入詞是「cat」——Word2Vec 試圖從提供的輸入字中預測上下文(「the」,「sat」)。Word2Vec 系統將遍歷所有給出的 gram 和輸入的單詞,並嘗試學習適當的映射向量(嵌入),這些映射向量保證了在給定輸入單詞的情況下,正確的上下文單詞能得到更高概率。
什麼是 Word2Vec 預測系統?不過是一種神經網路。
softmax Word2Vec 方法
Consider the diagram below – in this case we』ll assume the sentence「The cat sat on the mat」is part of a much larger text database, with a very large vocabulary – say 10,000 words in length. We want to reduce this to a 300 length embedding.
從下圖考慮——在這種情況下,我們將假設「The cat sat on the mat」這個句子是一個文本資料庫的一部分,而這個文本資料庫的辭彙量非常大——有 10,000 個字。我們想將其減少到長度為 300 的嵌入。
Word2Vec softmax 訓練器
如上表所示,如果我們取出「cat」這個詞,它將成為 10,000 個辭彙中的一個單詞。因此我們可以將它表示成一個長度為 10,000 的獨熱向量。然後將這個輸入向量連接到一個具有 300 個節點的隱藏層。連接這個圖層的權重將成為新的詞向量。該隱藏層中的節點的激活是加權輸入的線性總和(不會使用如 sigmoid 或 tanh 這樣的非線性激活函數)。此後這些節點會饋送到 softmax 輸出層。在訓練過程中,我們想要改變這個神經網路的權重,使「cat」周圍的單詞在 softmax 輸出層中輸出的概率更高。例如,如果我們的文本數據集有許多蘇斯博士(Dr.Seuss)的書籍,我們希望通過神經網路,像「the」,「sat」和「on」這樣的詞能得到更高概率(給出很多諸如「the cat sat on the mat」這樣的句子)。
通過訓練這個網路,我們將創建一個 10,000*300 的權重矩陣,該矩陣使用有 300 個節點的隱藏層與長度為 10,000 的輸入相連接。該矩陣中的每一行都與有 10,000 辭彙的辭彙表的一個單詞相對應——我們通過這種方式有效地將表示單詞的獨熱向量的長度由 10,000 減少至 300。實際上,該權重矩陣可以當做查找或編碼單詞的總表。不僅如此,由於我們採用這種方式訓練網路,這些權值還包含了上下文信息。一旦我們訓練了網路,就意味著我們放棄了 softmax 層並使用 10,000 x 300 的權重矩陣作為我們的嵌入式查找表。
如何用代碼實現上述想法?
在 TensorFlow 中實現 softmax Word2Vec 方法
與其他機器學習模型一樣,該網路也有兩個組件——一個用於將所有數據轉換為可用格式,另一個則用於對數據進行訓練、驗證和測試。在本教程中,我首先會介紹如何將數據收集成可用的格式,然後對模型的 TensorFlow 圖進行討論。請注意,在 Github 中可找到本教程的完整代碼。在本例中,大部分代碼都是以這裡的 TensorFlow Word2Vec 教程(https://github.com/tensorflow/tensorflow/blob/r1.2/tensorflow/examples/tutorials/word2vec/word2vec_basic.py)為基礎,並對其進行了一些個人修改。
準備文本數據
前面提到的 TensorFlow 教程有幾個函數,這些函數可用於提取文本資料庫並對其進行轉換,在此基礎上我們可以小批量(mini-batch)提取輸入詞及其相關 gram,進而用於訓練 Word2Vec 系統。下面的內容會依次介紹這些函數:
def maybe_download(filename, url, expected_bytes):"""Download a file if not present, and make sure it"s the right size."""if not os.path.exists(filename):
filename, _ = urllib.request.urlretrieve(url + filename, filename)
statinfo = os.stat(filename)if statinfo.st_size == expected_bytes:print("Found and verified", filename)else:print(statinfo.st_size)raise Exception("Failed to verify " + filename + ". Can you get to it with a browser?")return filename
該函數用於檢查是否已經從提供的 URL 下載了文件(代碼中的 filename)。如果沒有,使用 urllib.request Python 模塊(該模塊可從給定的 url 中檢索文件),並將該文件下載到本地代碼目錄中。如果文件已經存在(即 os.path.exists(filename)返回結果為真),那麼函數不會再下載文件。接下來,expected_bytes 函數會對文件大小進行檢查,以確保下載文件與預期的文件大小一致。如果一切正常,將返回至用於提取數據的文件對象。為了在本例所用數據集中調用該函數,我們執行了下面的代碼:
url = "http://mattmahoney.net/dc/"
filename = maybe_download("text8.zip", url, 31344016)
接下來我們要做的是取用指向已下載文件的文件對象,並使用 Python zipfile 模塊提取數據。
# 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
使用 zipfile.ZipFile()來提取壓縮文件,然後我們可以使用 zipfile 模塊中的讀取器功能。首先,namelist()函數檢索該檔案中的所有成員——在本例中只有一個成員,所以我們可以使用 0 索引對其進行訪問。然後,我們使用 read()函數讀取文件中的所有文本,並傳遞給 TensorFlow 的 as_str 函數,以確保文本保存為字元串數據類型。最後,我們使用 split()函數創建一個列表,該列表包含文本文件中所有的單詞,並用空格字元分隔。我們可以在這裡看到一些輸出:
vocabulary = read_data(filename)print(vocabulary[:7])["anarchism", "originated", "as", "a", "term", "of", "abuse"]
如我們所見,返回的辭彙數據包含一個清晰的單詞列表,將其按照原始文本文件的句子排序。現在我們已經提取了所有的單詞並置入列表,需要對其進行進一步的處理以創建 skip-gram 批量數據。處理步驟如下:
1. 提取前 10000 個最常用的單詞,置入嵌入向量;
2. 彙集所有單獨的單詞,並用唯一的整數對它們進行索引——這一步等同於為單詞創建獨熱碼。我們將使用一個字典來完成這一步;
3. 循環遍曆數據集中的每個單詞(辭彙變數),並將其分配給在步驟 2 中創建的獨一無二的整數。這使在單詞數據流中進行查找或處理操作變得更加容易。
實現上述行為的代碼如下所示:
def build_dataset(words, n_words):"""Process raw inputs into a dataset."""
count = [["UNK", -1]]
count.extend(collections.Counter(words).most_common(n_words - 1))
dictionary = dict()for word, _ in count:
dictionary[word] = len(dictionary)
data = list()
unk_count = 0for 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()))return data, count, dictionary, reversed_dictionary
第一步是設置一個「計數器」列表,該列表中存儲在數據集中找到一個單詞的次數。由於我們的辭彙量僅限於 10,000 個單詞,因此,不包括在前 10,000 個最常用單詞中的任何單詞都將標記為「UNK」,表示「未知」。然後使用 Python 集合模塊和 Counter()類以及關聯的 most_common()函數對已初始化的計數列表進行擴展。這些設置用於計算給定參數(單詞)中的單詞數量,然後以列表格式返回 n 個最常見的單詞。
該函數的下一部分創建了一個字典,名為 dictionary,該字典由關鍵詞進行填充,而這些關鍵詞與每個獨一無二的詞相對應。分配給每個獨一無二的關鍵詞的值只是簡單地將字典的大小以整數形式進行遞增。例如,將 1 賦值給第一常用的單詞,2 賦值給第二常用的詞,3 賦值給第三常用的詞,依此類推(整數 0 被分配給「UNK」詞)。這一步給辭彙表中的每個單詞賦予了唯一的整數值——完成上述過程的第二步。
接下來,該函數將對數據集中的每個單詞進行循環遍歷-——該數據集是由 read_data()函數輸出的。經過這一步,我們創建了一個叫做「data」的列表,該列表長度與單詞量相同。但該列表不是由獨立單片語成的單詞列表,而是個整數列表——在字典里由分配給該單詞的唯一整數表示每一個單詞。因此,對於數據集的第一個句子 [『anarchism』, 『originated』, 『as』, 『a』, 『term』, 『of』, 『abuse』],現在在數據變數中是這樣的:[5242,3083,12,6,195,2,3136]。這解決了上述第三步。
最後,該函數創建了一個名為 reverse_dictionary 的字典,它允許我們根據其唯一的整數標識符來查找單詞,而非根據單詞查找標識符。
建立數據的最後一點在於,現在要創建一個包含輸入詞和相關 gram 的數據集,這可用於訓練 Word2Vec 嵌入系統。執行這一步操作的代碼如下:
data_index = 0# generate batch datadef generate_batch(data, batch_size, num_skips, skip_window):global data_index
assert batch_size % num_skips == 0assert 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 # [ skip_window input_word skip_window ]
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):
target = skip_window # input word at the center of the buffer
targets_to_avoid = [skip_window]for j in range(num_skips):while target in targets_to_avoid:
target = random.randint(0, span - 1)
targets_to_avoid.append(target)
batch[i * num_skips + j] = buffer[skip_window] # this is the input word
context[i * num_skips + j, 0] = buffer[target] # these are the context words
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
該函數會生成小批量數據用於我們的訓練中(可在此了解小批量訓練:http://adventuresinmachinelearning.com/stochastic-gradient-descent/)。這些小批量包括輸入詞(存儲在批量中)和 gram 中隨機關聯的上下文單詞,這些批量將作為標籤對結果進行預測(存儲在上下文中)。例如,在 gram 為 5 的「the cat sat on the」中,輸入詞即中心詞,也就是「sat」,並且將被預測的上下文將從這一 gram 的剩餘詞中隨機抽取:[『the 』,『cat』,『on』,『the』]。在該函數中,通過 num_skips 定義從上下文中隨機抽取的單詞數量。該函數會使用 skip_window 定義輸入詞周圍抽取的上下文單詞的窗口大小——在上述例子(「the cat sat on the」)中,輸入詞「sat」周圍的 skip_window 的寬度為 2。
在上述函數中,我們首先將批次和輸出標籤定義為 batch_size 的變數。然後定義其廣度的大小(span size),這基本上就是我們要提取輸入詞和上下文的單詞列表的大小。在上述例子的子句「the cat on the」中,廣度是 5 = 2 * skip window + 1。此後還需創建一個緩衝區:
buffer = collections.deque(maxlen=span)for _ in range(span):
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
這個緩衝區將會最大程度地保留 span 元素,還是一種用於採樣的移動窗口。每當有新的單詞索引添加至緩衝區時,最左方的元素將從緩衝區中排出,以便為新的單詞索引騰出空間。輸入文本流中的緩衝器被存儲在全局變數 data_index 中,每當緩衝器中有新的單詞進入時,data_index 遞增。如果到達文本流的末尾,索引更新的「%len(data)」組件會將計數重置為 0。
填寫批量處理和上下文變數的代碼如下所示:
for i in range(batch_size // num_skips):
target = skip_window # input word at the center of the buffer
targets_to_avoid = [skip_window]for j in range(num_skips):while target in targets_to_avoid:
target = random.randint(0, span - 1)
targets_to_avoid.append(target)
batch[i * num_skips + j] = buffer[skip_window] # this is the input word
context[i * num_skips + j, 0] = buffer[target] # these are the context words
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
選擇的第一個詞「target」是單詞表最中間的詞,因此這是輸入詞。然後從單詞的 span 範圍中隨機選擇其他單詞,確保上下文中不包含輸入詞且每個上下文單詞都是唯一的。batch 變數會反映出重複的輸入詞(buffer [skip_window]),這些輸入詞會與 context 中的每個上下文單詞進行匹配。
然後返回 batch 變數和 context 變數——現在我們有了從數據集中分出批量數據的方法。我們現在可以在 TensorFlow 中寫訓練 Word2Vec 的代碼了。然而,在此之前,我們要先建立一個用於測試模型表現的驗證集。我們通過測量向量空間中最接近的向量來建立驗證集,並使用英語知識以確保這些詞確實是相似的。這將在下一節中進行具體討論。不過我們可以先暫時使用另一種方法,從辭彙表最常用的詞中隨機提取驗證單詞,代碼如下所示:
# We pick a random validation set to sample nearest neighbors. Here we limit the# validation samples to the words that have a low numeric ID, which by# construction are also the most frequent.
valid_size = 16 # Random set of words to evaluate similarity on.
valid_window = 100 # Only pick dev samples in the head of the distribution.
valid_examples = np.random.choice(valid_window, valid_size, replace=False)
上面的代碼從 0 到 100 中隨機選擇了 16 個整數——這些整數與文本數據中最常用的 100 個單詞的整數索引相對應。我們將通過考察這些詞語來評估相關單詞與向量空間相關聯的過程在我們的學習模型中進行得如何。到現在為止,我們可以建立 TensorFlow 模型了。
建立 TensorFlow 模型
接下來我將介紹在 TensorFlow 中建立 Word2Vec 詞嵌入器的過程。這涉及到什麼內容呢?簡單地說,我們需要建立我之前提出的神經網路,該網路在 TensorFlow 中使用詞嵌入矩陣作為隱藏層,還包括一個輸出 softmax 層。通過訓練該模型,我們將通過學習得到最好的詞嵌入矩陣,因此我們將通過學習得到一個簡化的、保留了上下文的單詞到向量的映射。
首先要做的是設置一些稍後要用的變數——設置這些變數的目的稍後會變得清楚:
batch_size = 128
embedding_size = 128 # Dimension of the embedding vector.
skip_window = 1 # How many words to consider left and right.
num_skips = 2 # How many times to reuse an input to generate a context.
接下來,我們設置一些 TensorFlow 佔位符,這些佔位符會保存輸入詞(的整數索引)和我們準備預測的上下文單詞。我們還需要創建一個常量來保存 TensorFlow 中的驗證集索引:
train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
valid_dataset = tf.constant(valid_examples, dtype=tf.int32)
接下來,我們需要設置嵌入矩陣變數或張量——這是使用 TensorFlow 中 embedding_lookup()函數最直接的方法,我會在下文對其進行簡短地解釋:
# Look up embeddings for inputs.
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
embed = tf.nn.embedding_lookup(embeddings, train_inputs)
上述代碼的第一步是創建嵌入變數,這實際上是線性隱藏層連接的權重。我們用 -1.0 到 1 的隨機均勻分布對變數進行初始化。變數大小包括 vocabulary_size 和 embedding_size。vocabulary_size 是上一節中用來設置數據的 10,000 個單詞。這是我們輸入的獨熱向量,在向量中僅有一個值為「1」的元素是當前的輸入詞,其他值都為「0」。embedding_size 是隱藏層的大小,也是新的更小的單詞表示的長度。我們也考慮了可以把這個張量看作一個大的查找表——行是辭彙表中的每個詞,列是每個詞的新的向量表示。以下一個簡化的例子(使用虛擬值),其中 vocabulary_size = 7,embedding_size = 3:
[Image: file:///-/blob/NBDAAAnwZv8/Hn1HwOhB1t-GxPmzonZBSw]
正如我們所見,「anarchism」(實際上由一個整數或獨熱向量表示)現在表示為 [0.5,0.1,-0.1]。我們可以通過查找其整數索引、搜索嵌入行查找嵌入向量的方法「查找」anarchism:[0.5,0.1,-0.1]。
下面的代碼涉及到 tf.nn.embedding_lookup()函數,在 TensorFlow 的此類任務中該函數是一個很有用的輔助函數:它取一個整數索引向量作為輸入——在本例中是訓練輸入詞的張量 train_input,並在已給的嵌入張量中「查找」這些索引。
因此,該命令將返回訓練批次中每個給定輸入詞的當前嵌入向量。完整的嵌入張量將在訓練過程中進行優化。
接下來,我們必須創建一些權重和偏差值來連接輸出 softmax 層,並對其進行運算。如下所示:
# Construct the variables for the softmax
weights = tf.Variable(tf.truncated_normal([vocabulary_size, embedding_size],
stddev=1.0 / math.sqrt(embedding_size)))
biases = tf.Variable(tf.zeros([vocabulary_size]))
hidden_out = tf.matmul(embed, tf.transpose(weights)) + biases
因為權重變數連接著隱藏層和輸出層,因此其大小 size(out_layer_size,hidden_layer_size)=(vocabulary_size,embedding_size)。一如以往,偏差值是一維的,且大小與輸出層一致。然後,我們將嵌入變數與權重相乘(嵌入),再與偏差值相加。接下來可以做 softmax 運算,並通過交叉熵損失函數來優化模型的權值、偏差值和嵌入。我們將使用 TensorFlow 中的 softmax_cross_entropy_with_logits()函數簡化這個過程。然而,如果要使用該函數的話,我們首先要將上下文單詞和整數索引轉換成獨熱向量。下面的代碼不僅執行了這兩步操作,還對梯度下降進行了優化:
# convert train_context to a one-hot format
train_one_hot = tf.one_hot(train_context, vocabulary_size)
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=hidden_out,
labels=train_one_hot))# Construct the SGD optimizer using a learning rate of 1.0.
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(cross_entropy)
接下來,我們需要執行相似性評估以檢查模型訓練時的表現。為了確定哪些詞彼此相似,我們需要執行某種操作來測量不同詞的詞嵌入向量間的「距離」。在本例中,我們計算了餘弦相似度以度量不同向量間的距離。定義如下:
公式中粗體字母**A**和**B**是需要測量距離的兩個向量。具有 2 個下標(|| A || 2)的雙平行線是指向量的 L2 範數。為了得到向量的 L2 範數,可以將向量的每個維數(在這種情況下,n = 300,我們的嵌入向量的寬度)平方對其求和後再取平方根:
在 TensorFlow 中計算餘弦相似度的最好方法是對每個向量進行歸一化,如下所示:
然後,我們可以將這些歸一化向量相乘得到餘弦相似度。我們將之前提過的驗證向量或驗證詞與嵌入向量中所有的單詞相乘,然後我們可以將之按降序進行排列,以得到與驗證詞最相似的單詞。
首先,我們分別使用 tf.square(),tf.reduce_sum()和 tf.sqrt()函數分別計算每個向量的 L2 範數的平方、和以及平方根:
# Compute the cosine similarity between minibatch examples and all embeddings.
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
normalized_embeddings = embeddings / norm
然後我們就可以使用 tf.nn.embedding_lookup()函數查找之前提到的驗證向量或驗證詞:
valid_embeddings = tf.nn.embedding_lookup(
normalized_embeddings, valid_dataset)
我們向 embedding_lookup()函數提供了一個整數列表(該列表與我們的驗證辭彙表相關聯),該函數對 normalized_embedding 張量按行進行查找,返回一個歸一化嵌入的驗證集的子集。現在我們有了歸一化的驗證集張量 valid_embeddings,可將其嵌入完全歸一化的辭彙表(normalized_embedding)以完成相似性計算:
similarity = tf.matmul(
valid_embeddings, normalized_embeddings, transpose_b=True)
該操作將返回一個(validation_size, vocabulary_size)大小的張量,該張量的每一行指代一個驗證詞,列則指驗證詞和辭彙表中其他詞的相似度。
運行 TensorFlow 模型
下面的代碼對變數進行了初始化並在訓練循環中將初始化的變數饋送入每個數據批次中,每迭代 2,000 次後輸出一次平均損失值。如果在這段代碼中有不能理解的地方,請查看我的 TensorFlow 教程。
with tf.Session(graph=graph) as session:# We must initialize all variables before we use them.
init.run()print("Initialized")
average_loss = 0for step in range(num_steps):
batch_inputs, batch_context = generate_batch(data,
batch_size, num_skips, skip_window)
feed_dict = {train_inputs: batch_inputs, train_context: batch_context}# We perform one update step by evaluating the optimizer op (including it# in the list of returned values for session.run()
_, loss_val = session.run([optimizer, cross_entropy], feed_dict=feed_dict)
average_loss += loss_val
if step % 2000 == 0:if step > 0:
average_loss /= 2000# The average loss is an estimate of the loss over the last 2000 batches.print("Average loss at step ", step, ": ", average_loss)
average_loss = 0
接下來,我們想要輸出與驗證詞相似程度最高的單詞——這一步需要通過調用上面定義的相似性運算以及對結果進行排序來達成(注意,由於計算量大,因此每迭代 10,000 次執行一次該操作):
# Note that this is expensive (~20% slowdown if computed every 500 steps)if step % 10000 == 0:
sim = similarity.eval()for i in range(valid_size):
valid_word = reverse_dictionary[valid_examples[i]]
top_k = 8 # number of nearest neighbors
nearest = (-sim[i, :]).argsort()[1:top_k + 1]
log_str = "Nearest to %s:" % valid_word
for k in range(top_k):
close_word = reverse_dictionary[nearest[k]]
log_str = "%s %s," % (log_str, close_word)print(log_str)
該函數首先計算相似性,即給每個驗證詞返回一組餘弦相似度的值。然後我們遍歷驗證集中的每一個詞,使用 argsort()函數輸入相似度的負值,取前 8 個最接近的詞並按降序進行排列。列印出這 8 個詞的代碼,我們就可以看到嵌入過程是如何執行的了。
最後,在完成所有的訓練過程的所有迭代之後,我們可以將最終的嵌入結果定為一個單獨的張量供以後使用(比如其他深度學習或機器學習過程):
final_embeddings = normalized_embeddings.eval()
現在我們完成了——真的完成了嗎?Word2Vec 的這個 softmax 方法的代碼被放在了 Github 上——你可以試著運行它,但我並不推薦。為什麼?因為它真的很慢。
提速——「真正的」Word2Vec 方法
事實上,使用 softmax 進行評估和更新一個有 10,000 詞的輸出或辭彙表的權值是非常慢的。我們從 softmax 的定義考慮:
在我們正在處理的內容中,softmax 函數將預測哪些詞在輸入詞的上下文中具有最高的可能性。為了確定這個概率,softmax 函數的分母必須評估辭彙表中所有可能的上下文單詞。因此,我們需要 300 * 10,000 = 3M 的權重,所有這些權重都需要針對 softmax 輸出進行訓練。這會降低速度。
NCE(Noise Contrastive Estimation,_,_雜訊對比估計,http://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf)的速度更快,可以作為替代方案。這個方法不是用上下文單詞相對於辭彙表中所有可能的上下文單詞的概率,而是隨機抽樣 2-20 個可能的上下文單詞,並僅從這些單詞中評估概率。在此不對細節進行描述,但可以肯定的是,該方法可用於訓練模型,且可大大加快訓練進程。
TensorFlow 已經在此幫助過我們,並為我們提供了 NCE 損失函數,即 tf.nn.nce_loss()。我們可以將權重和偏差變數輸入 tf.nn.nce_loss()。使用該函數和 NCE,迭代 100 次的時間從 softmax 的 25 秒減少到不到 1 秒。用以下內容替換 softmax:
# Construct the variables for the NCE loss
nce_weights = tf.Variable(
tf.truncated_normal([vocabulary_size, embedding_size],
stddev=1.0 / math.sqrt(embedding_size)))
nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
nce_loss = tf.reduce_mean(
tf.nn.nce_loss(weights=nce_weights,
biases=nce_biases,
labels=train_context,
inputs=embed,
num_sampled=num_sampled,
num_classes=vocabulary_size))
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(nce_loss)
現在我們可以運行代碼了。如上所述,每迭代 10,000 次代碼輸出驗證詞和 Word2Vec 系統得出的相似詞。您可以在下面看到隨機初始化和 50,000 次迭代標記之間的某些選定驗證詞的改進:
開始:
最接近 nine 的詞:heterosexual, scholarly, scandal, serves, humor, realized, cave, himself
最接近 this 的詞:contains, alter, numerous, harmonica, nickname, ghana, bogart, Marxist
迭代 10,000 次後:
最接近 nine 的詞:zero, one, and, coke, in, UNK, the, jpg
最接近 this 的詞:the, a, UNK, killing, meter, afghanistan, ada, Indiana
50,000 次迭代後的最終結果:
最接近 nine 的詞:eight, one, zero, seven, six, two, five, three
最接近 this 的詞:that, the, a, UNK, one, it, he, an
通過查看上面的輸出,我們可以首先看到「nine」這個詞與其他數字的關聯性越來越強(「eight」,「one」,「seven」等)這是有一定道理的。隨著迭代次數的增加,「this」這個詞在句子中起到代詞和定冠詞的作用,與其他代詞(「he」,「it」)和其他定冠詞(「the」,「that」等)關聯在一起。
總而言之,我們已經學會了如何使用 Word2Vec 方法將大的獨熱單詞向量減少為小得多的詞嵌入向量,這些向量保留了原始單詞的上下文和含義。這些詞嵌入向量可以作為構建自然語言模型的深度學習技術的更加高效和有效的輸入。諸如循環神經網路這樣的深度學習技術,將在未來佔據主要地位。
推薦閱讀:
※在不考慮語音輸入的前提下,訊飛輸入法和搜狗輸入法哪個更好?
※古詩為什麼能自動生成?
※為什麼計算機學科的論文不給出具體的參數、代碼、數據集來方便實驗重現呢?
※如何評價「谷歌用神經機器系統把漢語翻譯成英語,錯誤率最高下降85%」?
※如何向文科同學科普自然語言處理(NLP)?