本文詳細介紹了 word2vector 模型的模型架構,以及 TensorFlow 的實現過程,包括資料準備、建立模型、構建驗證集,並給出了執行結果示例。
GitHub 連結:https://github.com/adventuresinML/adventures-in-ml-code
Word2Vec 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 方法
從下圖考慮——在這種情況下,我們將假設「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:
正如我們所見,「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 方法將大的獨熱單詞向量減少為小得多的詞嵌入向量,這些向量保留了原始單詞的上下文和含義。這些詞嵌入向量可以作為構建自然語言模型的深度學習技術的更加高效和有效的輸入。諸如迴圈神經網路這樣的深度學習技術,將在未來佔據主要地位。
原文連結:http://adventuresinmachinelearning.com/word2vec-tutorial-tensorflow/