新聞中心
本文詳細介紹了 word2vector 模型的模型架構(gòu),以及 TensorFlow 的實現(xiàn)過程,包括數(shù)據(jù)準備、建立模型、構(gòu)建驗證集,并給出了運行結(jié)果示例。

GitHub 鏈接:https://github.com/adventuresinML/adventures-in-ml-code
Word2Vec softmax 訓練器
在接下來的教程中,我將解決的問題是該如何建立一個深度學習模型預測文本序列。然而,在建立模型之前,我們必須理解一些關(guān)鍵的自然語言處理(NLP)的思想。NLP 的關(guān)鍵思想之一是如何有效地將單詞轉(zhuǎn)換為數(shù)字向量,然后將這些數(shù)字向量「饋送」到機器學習模型中進行預測。本教程將對現(xiàn)在使用的主要技術(shù),即「Word2Vec」進行介紹。在討論了相關(guān)的背景材料之后,我們將使用 TensorFlow 實現(xiàn) Word2Vec 嵌入。要快速了解 TensorFlow,請查看我的 TensorFlow 教程:
http://adventuresinmachinelearning.com/python-tensorflow-tutorial/
我們?yōu)槭裁葱枰?Word2Vec
如果我們想把單詞輸入機器學習模型,除非使用基于樹的方法,否則需要把單詞轉(zhuǎn)換成一些數(shù)字向量。一種直接的方法是使用「獨熱編碼」方法將單詞轉(zhuǎn)換為稀疏表示,向量中只有一個元素設(shè)置為 1,其余為 0。我們構(gòu)建分類任務也采用了相同的方法——詳情請參考該教程:
http://adventuresinmachinelearning.com/neural-networks-tutorial/#setting-up-output
所以,我們可以使用如下的向量表示句子「The cat sat on the mat」:
我們在此將一個六個字的句子轉(zhuǎn)換為一個 6*5 的矩陣,其中 5 是詞匯量(「the」有重復)。然而,在實際應用中,我們希望深度學習模型能夠在詞匯量很大(10,000 字以上)的情況下進行學習。從這里能看到使用「獨熱碼」表示單詞的效率問題——對這些詞匯建模的任何神經(jīng)網(wǎng)絡(luò)的輸入層至少都有 10,000 個節(jié)點。不僅如此,這種方法剝離了單詞的所有局部語境——也就是說它會去掉句子中(或句子之間)緊密相連的單詞的信息。
例如,我們可能想看到「United」和「States」靠得很近,或者是「Soviet」和「Union」,或者「食物」和「吃」等等。如果我們試圖以這種方法對自然語言建模,會丟失所有此類信息,這將是一個很大的疏漏。因此,我們需要使用更高效的方法表示文本數(shù)據(jù),而這種方法可以保存單詞的上下文的信息。這是 Word2Vec 方法發(fā)明的初衷。
Word2Vec 方法
如上文所述,Word2Vec 方法由兩部分組成。首先是將高維獨熱形式表示的單詞映射成低維向量。例如將 10,000 列的矩陣轉(zhuǎn)換為 300 列的矩陣。這個過程被稱為詞嵌入。第二個目標是在保留單詞上下文的同時,從一定程度上保留其意義。在 Word2Vec 方法中實現(xiàn)這兩個目標的方法之一是,輸入一個詞,然后試著估計其他詞出現(xiàn)在該詞附近的概率,稱為 skip-gram 方法。還有一種與此相反的被稱為連續(xù)詞袋模型(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」?!竤kip」指一個輸入詞在不同的上下文詞的情況下,在數(shù)據(jù)集中重復的次數(shù)(這點會在稍后陳述)。這些 gram 被輸入 Word2Vec 上下文預測系統(tǒng)。舉個例子,假設(shè)輸入詞是「cat」——Word2Vec 試圖從提供的輸入字中預測上下文(「the」,「sat」)。Word2Vec 系統(tǒng)將遍歷所有給出的 gram 和輸入的單詞,并嘗試學習適當?shù)挠成湎蛄?嵌入),這些映射向量保證了在給定輸入單詞的情況下,正確的上下文單詞能得到更高概率。
什么是 Word2Vec 預測系統(tǒng)?不過是一種神經(jīng)網(wǎng)絡(luò)。
softmax Word2Vec 方法
從下圖考慮——在這種情況下,我們將假設(shè)「The cat sat on the mat」這個句子是一個文本數(shù)據(jù)庫的一部分,而這個文本數(shù)據(jù)庫的詞匯量非常大——有 10,000 個字。我們想將其減少到長度為 300 的嵌入。
Word2Vec softmax 訓練器
如上表所示,如果我們?nèi)〕觥竎at」這個詞,它將成為 10,000 個詞匯中的一個單詞。因此我們可以將它表示成一個長度為 10,000 的獨熱向量。然后將這個輸入向量連接到一個具有 300 個節(jié)點的隱藏層。連接這個圖層的權(quán)重將成為新的詞向量。該隱藏層中的節(jié)點的激活是加權(quán)輸入的線性總和(不會使用如 sigmoid 或 tanh 這樣的非線性激活函數(shù))。此后這些節(jié)點會饋送到 softmax 輸出層。在訓練過程中,我們想要改變這個神經(jīng)網(wǎng)絡(luò)的權(quán)重,使「cat」周圍的單詞在 softmax 輸出層中輸出的概率更高。例如,如果我們的文本數(shù)據(jù)集有許多蘇斯博士(Dr.Seuss)的書籍,我們希望通過神經(jīng)網(wǎng)絡(luò),像「the」,「sat」和「on」這樣的詞能得到更高概率(給出很多諸如「the cat sat on the mat」這樣的句子)。
通過訓練這個網(wǎng)絡(luò),我們將創(chuàng)建一個 10,000*300 的權(quán)重矩陣,該矩陣使用有 300 個節(jié)點的隱藏層與長度為 10,000 的輸入相連接。該矩陣中的每一行都與有 10,000 詞匯的詞匯表的一個單詞相對應——我們通過這種方式有效地將表示單詞的獨熱向量的長度由 10,000 減少至 300。實際上,該權(quán)重矩陣可以當做查找或編碼單詞的總表。不僅如此,由于我們采用這種方式訓練網(wǎng)絡(luò),這些權(quán)值還包含了上下文信息。一旦我們訓練了網(wǎng)絡(luò),就意味著我們放棄了 softmax 層并使用 10,000 x 300 的權(quán)重矩陣作為我們的嵌入式查找表。
如何用代碼實現(xiàn)上述想法?
在 TensorFlow 中實現(xiàn) softmax Word2Vec 方法
與其他機器學習模型一樣,該網(wǎng)絡(luò)也有兩個組件——一個用于將所有數(shù)據(jù)轉(zhuǎn)換為可用格式,另一個則用于對數(shù)據(jù)進行訓練、驗證和測試。在本教程中,我首先會介紹如何將數(shù)據(jù)收集成可用的格式,然后對模型的 TensorFlow 圖進行討論。請注意,在 Github 中可找到本教程的完整代碼。在本例中,大部分代碼都是以這里的 TensorFlow Word2Vec 教程
(https://github.com/tensorflow/tensorflow/blob/r1.2/tensorflow/examples/tutorials/word2vec/word2vec_basic.py)為基礎(chǔ),并對其進行了一些個人修改。
準備文本數(shù)據(jù)
前面提到的 TensorFlow 教程有幾個函數(shù),這些函數(shù)可用于提取文本數(shù)據(jù)庫并對其進行轉(zhuǎn)換,在此基礎(chǔ)上我們可以小批量(mini-batch)提取輸入詞及其相關(guān) gram,進而用于訓練 Word2Vec 系統(tǒng)。下面的內(nèi)容會依次介紹這些函數(shù):
- 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
該函數(shù)用于檢查是否已經(jīng)從提供的 URL 下載了文件(代碼中的 filename)。如果沒有,使用 urllib.request Python 模塊(該模塊可從給定的 url 中檢索文件),并將該文件下載到本地代碼目錄中。如果文件已經(jīng)存在(即 os.path.exists(filename)返回結(jié)果為真),那么函數(shù)不會再下載文件。接下來,expected_bytes 函數(shù)會對文件大小進行檢查,以確保下載文件與預期的文件大小一致。如果一切正常,將返回至用于提取數(shù)據(jù)的文件對象。為了在本例所用數(shù)據(jù)集中調(diào)用該函數(shù),我們執(zhí)行了下面的代碼:
- url = 'http://mattmahoney.net/dc/'
- filename = maybe_download('text8.zip', url, 31344016)
接下來我們要做的是取用指向已下載文件的文件對象,并使用 Python zipfile 模塊提取數(shù)據(jù)。
- # 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()函數(shù)檢索該檔案中的所有成員——在本例中只有一個成員,所以我們可以使用 0 索引對其進行訪問。然后,我們使用 read()函數(shù)讀取文件中的所有文本,并傳遞給 TensorFlow 的 as_str 函數(shù),以確保文本保存為字符串數(shù)據(jù)類型。***,我們使用 split()函數(shù)創(chuàng)建一個列表,該列表包含文本文件中所有的單詞,并用空格字符分隔。我們可以在這里看到一些輸出:
- vocabulary = read_data(filename)print(vocabulary[:7])['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse']
如我們所見,返回的詞匯數(shù)據(jù)包含一個清晰的單詞列表,將其按照原始文本文件的句子排序。現(xiàn)在我們已經(jīng)提取了所有的單詞并置入列表,需要對其進行進一步的處理以創(chuàng)建 skip-gram 批量數(shù)據(jù)。處理步驟如下:
- 提取前 10000 個最常用的單詞,置入嵌入向量;
- 匯集所有單獨的單詞,并用唯一的整數(shù)對它們進行索引——這一步等同于為單詞創(chuàng)建獨熱碼。我們將使用一個字典來完成這一步;
- 循環(huán)遍歷數(shù)據(jù)集中的每個單詞(詞匯變量),并將其分配給在步驟 2 中創(chuàng)建的***的整數(shù)。這使在單詞數(shù)據(jù)流中進行查找或處理操作變得更加容易。
實現(xiàn)上述行為的代碼如下所示:
- 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))
- dictdictionary = 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
***步是設(shè)置一個「計數(shù)器」列表,該列表中存儲在數(shù)據(jù)集中找到一個單詞的次數(shù)。由于我們的詞匯量僅限于 10,000 個單詞,因此,不包括在前 10,000 個最常用單詞中的任何單詞都將標記為「UNK」,表示「未知」。然后使用 Python 集合模塊和 Counter()類以及關(guān)聯(lián)的 most_common()函數(shù)對已初始化的計數(shù)列表進行擴展。這些設(shè)置用于計算給定參數(shù)(單詞)中的單詞數(shù)量,然后以列表格式返回 n 個最常見的單詞。
該函數(shù)的下一部分創(chuàng)建了一個字典,名為 dictionary,該字典由關(guān)鍵詞進行填充,而這些關(guān)鍵詞與每個***的詞相對應。分配給每個***的關(guān)鍵詞的值只是簡單地將字典的大小以整數(shù)形式進行遞增。例如,將 1 賦值給***常用的單詞,2 賦值給第二常用的詞,3 賦值給第三常用的詞,依此類推(整數(shù) 0 被分配給「UNK」詞)。這一步給詞匯表中的每個單詞賦予了唯一的整數(shù)值——完成上述過程的第二步。
接下來,該函數(shù)將對數(shù)據(jù)集中的每個單詞進行循環(huán)遍歷-——該數(shù)據(jù)集是由 read_data()函數(shù)輸出的。經(jīng)過這一步,我們創(chuàng)建了一個叫做「data」的列表,該列表長度與單詞量相同。但該列表不是由獨立單詞組成的單詞列表,而是個整數(shù)列表——在字典里由分配給該單詞的唯一整數(shù)表示每一個單詞。因此,對于數(shù)據(jù)集的***個句子 [『anarchism』, 『originated』, 『as』, 『a』, 『term』, 『of』, 『abuse』],現(xiàn)在在數(shù)據(jù)變量中是這樣的:[5242,3083,12,6,195,2,3136]。這解決了上述第三步。
***,該函數(shù)創(chuàng)建了一個名為 reverse_dictionary 的字典,它允許我們根據(jù)其唯一的整數(shù)標識符來查找單詞,而非根據(jù)單詞查找標識符。
建立數(shù)據(jù)的***一點在于,現(xiàn)在要創(chuàng)建一個包含輸入詞和相關(guān) gram 的數(shù)據(jù)集,這可用于訓練 Word2Vec 嵌入系統(tǒng)。執(zhí)行這一步操作的代碼如下:
- 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
該函數(shù)會生成小批量數(shù)據(jù)用于我們的訓練中(可在此了解小批量訓練:http://adventuresinmachinelearning.com/stochastic-gradient-descent/)。這些小批量包括輸入詞(存儲在批量中)和 gram 中隨機關(guān)聯(lián)的上下文單詞,這些批量將作為標簽對結(jié)果進行預測(存儲在上下文中)。例如,在 gram 為 5 的「the cat sat on the」中,輸入詞即中心詞,也就是「sat」,并且將被預測的上下文將從這一 gram 的剩余詞中隨機抽?。篬『the 』,『cat』,『on』,『the』]。在該函數(shù)中,通過 num_skips 定義從上下文中隨機抽取的單詞數(shù)量。該函數(shù)會使用 skip_window 定義輸入詞周圍抽取的上下文單詞的窗口大小——在上述例子(「the cat sat on the」)中,輸入詞「sat」周圍的 skip_window 的寬度為 2。
在上述函數(shù)中,我們首先將批次和輸出標簽定義為 batch_size 的變量。然后定義其廣度的大小(span size),這基本上就是我們要提取輸入詞和上下文的單詞列表的大小。在上述例子的子句「the cat on the」中,廣度是 5 = 2 * skip window + 1。此后還需創(chuàng)建一個緩沖區(qū):
- buffer = collections.deque(maxlen=span)for _ in range(span):
- buffer.append(data[data_index])
- data_index = (data_index + 1) % len(data)
這個緩沖區(qū)將會***程度地保留 span 元素,還是一種用于采樣的移動窗口。每當有新的單詞索引添加至緩沖區(qū)時,最左方的元素將從緩沖區(qū)中排出,以便為新的單詞索引騰出空間。輸入文本流中的緩沖器被存儲在全局變量 data_index 中,每當緩沖器中有新的單詞進入時,data_index 遞增。如果到達文本流的末尾,索引更新的「%len(data)」組件會將計數(shù)重置為 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 變量——現(xiàn)在我們有了從數(shù)據(jù)集中分出批量數(shù)據(jù)的方法。我們現(xiàn)在可以在 TensorFlow 中寫訓練 Word2Vec 的代碼了。然而,在此之前,我們要先建立一個用于測試模型表現(xiàn)的驗證集。我們通過測量向量空間中最接近的向量來建立驗證集,并使用英語知識以確保這些詞確實是相似的。這將在下一節(jié)中進行具體討論。不過我們可以先暫時使用另一種方法,從詞匯表最常用的詞中隨機提取驗證單詞,代碼如下所示:
- # 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 個整數(shù)——這些整數(shù)與文本數(shù)據(jù)中最常用的 100 個單詞的整數(shù)索引相對應。我們將通過考察這些詞語來評估相關(guān)單詞與向量空間相關(guān)聯(lián)的過程在我們的學習模型中進行得如何。到現(xiàn)在為止,我們可以建立 TensorFlow 模型了。
建立 TensorFlow 模型
接下來我將介紹在 TensorFlow 中建立 Word2Vec 詞嵌入器的過程。這涉及到什么內(nèi)容呢?簡單地說,我們需要建立我之前提出的神經(jīng)網(wǎng)絡(luò),該網(wǎng)絡(luò)在 TensorFlow 中使用詞嵌入矩陣作為隱藏層,還包括一個輸出 softmax 層。通過訓練該模型,我們將通過學習得到***的詞嵌入矩陣,因此我們將通過學習得到一個簡化的、保留了上下文的單詞到向量的映射。
首先要做的是設(shè)置一些稍后要用的變量——設(shè)置這些變量的目的稍后會變得清楚:
- 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.
接下來,我們設(shè)置一些 TensorFlow 占位符,這些占位符會保存輸入詞(的整數(shù)索引)和我們準備預測的上下文單詞。我們還需要創(chuàng)建一個常量來保存 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)
接下來,我們需要設(shè)置嵌入矩陣變量或張量——這是使用 TensorFlow 中 embedding_lookup()函數(shù)最直接的方法,我會在下文對其進行簡短地解釋:
- # 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)
上述代碼的***步是創(chuàng)建嵌入變量,這實際上是線性隱藏層連接的權(quán)重。我們用 -1.0 到 1 的隨機均勻分布對變量進行初始化。變量大小包括 vocabulary_size 和 embedding_size。vocabulary_size 是上一節(jié)中用來設(shè)置數(shù)據(jù)的 10,000 個單詞。這是我們輸入的獨熱向量,在向量中僅有一個值為「1」的元素是當前的輸入詞,其他值都為「0」。embedding_size 是隱藏層的大小,也是新的更小的單詞表示的長度。我們也考慮了可以把這個張量看作一個大的查找表——行是詞匯表中的每個詞,列是每個詞的新的向量表示。以下一個簡化的例子(使用虛擬值),其中 vocabulary_size = 7,embedding_size = 3:
正如我們所見,「anarchism」(實際上由一個整數(shù)或獨熱向量表示)現(xiàn)在表示為 [0.5,0.1,-0.1]。我們可以通過查找其整數(shù)索引、搜索嵌入行查找嵌入向量的方法「查找」anarchism:[0.5,0.1,-0.1]。
下面的代碼涉及到 tf.nn.embedding_lookup()函數(shù),在 TensorFlow 的此類任務中該函數(shù)是一個很有用的輔助函數(shù):它取一個整數(shù)索引向量作為輸入——在本例中是訓練輸入詞的張量 train_input,并在已給的嵌入張量中「查找」這些索引。
因此,該命令將返回訓練批次中每個給定輸入詞的當前嵌入向量。完整的嵌入張量將在訓練過程中進行優(yōu)化。
接下來,我們必須創(chuàng)建一些權(quán)重和偏差值來連接輸出 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
因為權(quán)重變量連接著隱藏層和輸出層,因此其大小 size(out_layer_size,hidden_layer_size)=(vocabulary_size,embedding_size)。一如以往,偏差值是一維的,且大小與輸出層一致。然后,我們將嵌入變量與權(quán)重相乘(嵌入),再與偏差值相加。接下來可以做 softmax 運算,并通過交叉熵損失函數(shù)來優(yōu)化模型的權(quán)值、偏差值和嵌入。我們將使用 TensorFlow 中的 softmax_cross_entropy_with_logits()函數(shù)簡化這個過程。然而,如果要使用該函數(shù)的話,我們首先要將上下文單詞和整數(shù)索引轉(zhuǎn)換成獨熱向量。下面的代碼不僅執(zhí)行了這兩步操作,還對梯度下降進行了優(yōu)化:
- # 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)
接下來,我們需要執(zhí)行相似性評估以檢查模型訓練時的表現(xiàn)。為了確定哪些詞彼此相似,我們需要執(zhí)行某種操作來測量不同詞的詞嵌入向量間的「距離」。在本例中,我們計算了余弦相似度以度量不同向量間的距離。定義如下:
公式中粗體字母**A**和**B**是需要測量距離的兩個向量。具有 2 個下標(|| A || 2)的雙平行線是指向量的 L2 范數(shù)。為了得到向量的 L2 范數(shù),可以將向量的每個維數(shù)(在這種情況下,n = 300,我們的嵌入向量的寬度)平方對其求和后再取平方根:
在 TensorFlow 中計算余弦相似度的***方法是對每個向量進行歸一化,如下所示:
然后,我們可以將這些歸一化向量相乘得到余弦相似度。我們將之前提過的驗證向量或驗證詞與嵌入向量中所有的單詞相乘,然后我們可以將之按降序進行排列,以得到與驗證詞最相似的單詞。
首先,我們分別使用 tf.square(),tf.reduce_sum()和 tf.sqrt()函數(shù)分別計算每個向量的 L2 范數(shù)的平方、和以及平方根:
- # 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)
然后我們就可以使用 tf.nn.embedding_lookup()函數(shù)查找之前提到的驗證向量或驗證詞:
- valid_embeddings = tf.nn.embedding_lookup(
- normalized_embeddings, valid_dataset)
我們向 embedding_lookup()函數(shù)提供了一個整數(shù)列表(該列表與我們的驗證詞匯表相關(guān)聯(lián)),該函數(shù)對 normalized_embedding 張量按行進行查找,返回一個歸一化嵌入的驗證集的子集?,F(xiàn)在我們有了歸一化的驗證集張量 valid_embeddings,可將其嵌入完全歸一化的詞匯表(normalized_embedding)以完成相似性計算:
- similarity = tf.matmul(
- valid_embeddings, normalized_embeddings, transpose_b=True)
該操作將返回一個(validation_size, vocabulary_size)大小的張量,該張量的每一行指代一個驗證詞,列則指驗證詞和詞匯表中其他詞的相似度。
運行 TensorFlow 模型
下面的代碼對變量進行了初始化并在訓練循環(huán)中將初始化的變量饋送入每個數(shù)據(jù)批次中,每迭代 2,000 次后輸出一次平均損失值。如果在這段代碼中有不能理解的地方,請查看我的 TensorFlow 教程。
- with tf.Session(graphgraph=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_dictfeed_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
接下來,我們想要輸出與驗證詞相似程度***的單詞——這一步需要通過調(diào)用上面定義的相似性運算以及對結(jié)果進行排序來達成(注意,由于計算量大,因此每迭代 10,000 次執(zhí)行一次該操作):
- # 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)
該函數(shù)首先計算相似性,即給每個驗證詞返回一組余弦相似度的值。然后我們遍歷驗證集中的每一個詞,使用 argsort()函數(shù)輸入相似度的負值,取前 8 個最接近的詞并按降序進行排列。打印出這 8 個詞的代碼,我們就可以看到嵌入過程是如何執(zhí)行的了。
***,在完成所有的訓練過程的所有迭代之后,我們可以將最終的嵌入結(jié)果定為一個單獨的張量供以后使用(比如其他深度學習或機器學習過程):
- final_embeddings = normalized_embeddings.eval()
現(xiàn)在我們完成了——真的完成了嗎?Word2Vec 的這個 softmax 方法的代碼被放在了 Github 上——你可以試著運行它,但我并不推薦。為什么?因為它真的很慢。
提速——「真正的」Word2Vec 方法
事實上,使用 softmax 進行評估和更新一個有 10,000 詞的輸出或詞匯表的權(quán)值是非常慢的。我們從 softmax 的定義考慮:
在我們正在處理的內(nèi)容中,softmax 函數(shù)將預測哪些詞在輸入詞的上下文中具有***的可能性。為了確定這個概率,softmax 函數(shù)的分母必須評估詞匯表中所有可能的上下文單詞。因此,我們需要 300 * 10,000 = 3M 的權(quán)重,所有這些權(quán)重都需要針對 softmax 輸出進行訓練。這會降低速度。
NCE(Noise Contrastive Estimation,噪聲對比估計,
http://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf)的速度更快,可以作為替代方案。這個方法不是用上下文單詞相對于詞匯表中所有可能的上下文單詞的概率,而是隨機抽樣 2-20 個可能的上下文單詞,并僅從這些單詞中評估概率。在此不對細節(jié)進行描述,但可以肯定的是,該方法可用于訓練模型,且可大大加快訓練進程。
TensorFlow 已經(jīng)在此幫助過我們,并為我們提供了 NCE 損失函數(shù),即 tf.nn.nce_loss()。我們可以將權(quán)重和偏差變量輸入 tf.nn.nce_loss()。使用該函數(shù)和 NCE,迭代 100 次的時間從 softmax 的 25 秒減少到不到 1 秒。用以下內(nèi)容替換 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_samplednum_sampled=num_sampled,
- num_classes=vocabulary_size))
- optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(nce_loss)
現(xiàn)在我們可以運行代碼了。如上所述,每迭代 10,000 次代碼輸出驗證詞和 Word2Vec 系統(tǒng)得出的相似詞。您可以在下面看到隨機初始化和 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 次迭代后的最終結(jié)果:
- 最接近 nine 的詞:eight, one, zero, seven, six, two, five, three
- 最接近 this 的詞:that, the, a, UNK, one, it, he, an
通過查看上面的輸出,我們可以首先看到「nine」這個詞與其他數(shù)字的關(guān)聯(lián)性越來越強(「eight」,「one」,「seven」等)這是有一定道理的。隨著迭代次數(shù)的增加,「this」這個詞在句子中起到代詞和定冠詞的作用,與其他代詞(「he」,「it」)和其他定冠詞(「the」,「that」等)關(guān)聯(lián)在一起。
總而言之,我們已經(jīng)學會了如何使用 Word2Vec 方法將大的獨熱單詞向量減少為小得多的詞嵌入向量,這些向量保留了原始單詞的上下文和含義。這些詞嵌入向量可以作為構(gòu)建自然語言模型的深度學習技術(shù)的更加高效和有效的輸入。諸如循環(huán)神經(jīng)網(wǎng)絡(luò)這樣的深度學習技術(shù),將在未來占據(jù)主要地位。
原文:http://adventuresinmachinelearning.com/word2vec-tutorial-tensorflow/
【本文是專欄機構(gòu)“機器之心”的原創(chuàng)譯文,微信公眾號“機器之心( id: almosthuman2014)”】
戳這里,看該作者更多好文
網(wǎng)站標題:在Python和TensorFlow上構(gòu)建Word2Vec詞嵌入模型
文章位置:http://m.fisionsoft.com.cn/article/dpeihid.html


咨詢
建站咨詢
