在NLP領域,詞向量是一個非常基礎的知識點,計算機是不能識別文字,所以要讓計算機記住文字只能通過數字的形式,在最初所采用的是one-hot(獨熱)編碼,簡單回顧一下這種編碼方式
例如:我很討厭下雨
分詞之后:我 很 討厭 下雨
可知詞表大小為4,采用one-hot編碼方式則為
我:[1,0,0,0]
很:[0,1,0,0]
討厭:[0,0,1,0]
下雨:[0,0,0,1]
這種方式可以很明顯的看出one-hot編碼的缺點,主要是體現在兩方面:詞表過大的時候,會生成非常大的系數矩陣;無法記錄詞與詞之間的關系
因而在這個基礎上,word2vec應運而生!本文將結合模型的實現代碼詳細解讀Word2Vec之一的Skip-Gram模型。本文主要由以下幾個部分:
一、網絡模型圖
二、代碼實現
數據准備
數據與處理
模型搭建
訓練&測試
網絡模型圖

Skip-Gram的網絡模型如上,其原理就是根據一個詞去生成周圍的詞。以“我 很 討厭 下雨”為例,(若生成范圍為1:即前后的一個詞)則是:‘’討厭‘’ 生成 “很” 和 “下雨”。上述圖片所展示的模型的生成范圍為2:即前后的兩個詞
在這里,可能大家還是不好理解,為什么要這樣做?其實很簡單,當我們在學習這個知識點的時候,之前一定學習過了邏輯回歸模型(經典的手寫體識別),這個模型的本質上也是一個回歸模型。以“我 很 討厭 下雨”為例,根據“討厭” 生成 “很”和“下雨” 實際可以理解為:
樣本 “討厭” 對應着標簽“很”和“下雨” 這樣,對於這個模型而言,當我們輸入樣本“討厭”的時候,對應模型的輸出則應該是“很”和“下雨”
但是,我們在如下代碼的實現過程當中,並不是一次就輸出其所對應的標簽,而是多次。以“我 很 討厭 下雨”為例,不是通過一次輸入“討厭”的時候,直接就輸出了“很”和“下雨”而是兩次,第一次輸入“討厭”,輸出“很”,第二次輸入“討厭”,輸出“下雨”。因而代碼實現的具體網絡模型如下:

代碼實現
數據准備
1 #1.導入所依賴的庫 2 import time 3 import collections 4 import math 5 import os 6 import random 7 import zipfile 8 import numpy as np 9 import urllib 10 import pprint 11 import tensorflow as tf 12 import matplotlib.pyplot as plt 13 os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" 14 15 #2.准備數據集 16 17 url = "http://mattmahoney.net/dc/" 18 19 def maybe_download(filename,expected_bytes): 20 """ 21 判斷文件是否已經下載,如果沒有,則下載數據集 22 """ 23 if not os.path.exists(filename): 24 #數據集不存在,開始下載 25 filename,_ = urllib.request.urlretrieve(url + filename,filename) 26 #有關函數urllib.request.urlretrieve的詳解博客https://blog.csdn.net/pursuit_zhangyu/article/details/80556275 27 #核對文件尺寸 28 stateinfo = os.stat(filename) 29 #有關系統stat調用的詳細解釋鏈接https://www.cnblogs.com/fmgao-technology/p/9056425.html 30 if stateinfo.st_size == expected_bytes: 31 print("數據集已存在,且文件尺寸合格!",filename) 32 else : 33 print(stateinfo.st_size) 34 raise Exception( 35 "文件尺寸不對 !請重新下載,下載地址為:"+url 36 ) 37 return filename 38 """ 39 測試文件是否存在 40 """ 41 filename = maybe_download("text8.zip",31344016) 42 43 #3.解壓文件 44 def read_data(filename): 45 #有關zipfile模塊的詳細使用介紹博客https://www.cnblogs.com/ManyQian/p/9193199.html 46 with zipfile.ZipFile(filename) as f: 47 #有關python中split()函數的詳細解釋博客https://www.runoob.com/python/att-string-split.html 48 data = tf.compat.as_str(f.read(f.namelist()[0])).split() 49 ''' 50 使用 zipfile.ZipFile()來提取壓縮文件,然后我們可以使用 51 zipfile 模塊中的讀取器功能。首先,namelist()函數檢索該 52 檔案中的所有成員——在本例中只有一個成員,所以我們可以使 53 用 0 索引對其進行訪問。然后,我們使用 read()函數讀取文 54 件中的所有文本,並傳遞給 TensorFlow 的 as_str 函數,以確 55 保文本保存為字符串數據類型。最后,我們使用 split()函數 56 創建一個列表,該列表包含文本文件中所有的單詞,並用空格字 57 符分隔''' 58 return data 59 words = read_data(filename) 60 print("總的單詞個數:",len(words))
數據預處理
1 #4.構建詞匯表,並統計每個單詞出現的頻數,同時用字典的形式進行存儲,取頻數排名前50000的單詞 2 vocabulary_size = 50000 3 def build_dataset(words): 4 count = [["unkown",-1]] 5 #collections.Counter()返回的是形如[["unkown",-1],("the",4),("physics",2)] 6 count.extend(collections.Counter(words).most_common(vocabulary_size - 1)) 7 #有關collection模塊中counter類的詳細解釋鏈接http://www.pythoner.com/205.html 8 #most_common()函數用來實現Top n 功能 即截取counter結果的前多少個子項 9 #對於列表的一些常見基本操作詳細鏈接https://blog.csdn.net/ywx1832990/article/details/78928238 10 dictionary = {} 11 #將全部單詞轉為編號(以頻數排序的編號),我們只關注top50000的單詞,以外的認為是unknown的,編號為0,同時統計一下這類詞匯的數量 12 for word,_ in count: 13 dictionary[word] = len(dictionary) 14 #形如:{"the":1,"UNK":0,"a":12} 15 data = [] 16 unk_count = 0 #准備統計top50000以外的單詞的個數 17 for word in words: 18 #對於其中每一個單詞,首先判斷是否出現在字典當中 19 if word in dictionary: 20 #如果已經出現在字典中,則轉為其編號 21 index = dictionary[word] 22 else: 23 #如果不在字典,則轉為編號0 24 index = 0 25 unk_count += 1 26 data.append(index)#此時單詞已經轉變成對應的編號 27 """ 28 print(data[:10]) 29 [5234, 3081, 12, 6, 195, 2, 3134, 46, 59, 156] 30 """ 31 count[0][1] = unk_count #將統計好的unknown的單詞數,填入count中 32 #將字典進行翻轉,形如:{3:"the,4:"an"} 33 reverse_dictionary = dict(zip(dictionary.values(),dictionary.keys())) 34 return data,count,dictionary,reverse_dictionary 35 #為了節省內存,將原始單詞列表進行刪除 36 data,count,dictionary,reverse_dictionary = build_dataset(words) 37 del words 38 #將部分結果展示出來 39 #print("出現頻率最高的單詞(包括未知類別的):",count[:10]) 40 #將已經轉換為編號的數據進行輸出,從data中輸出頻數,從翻轉字典中輸出編號對應的單詞 41 #print("樣本數據(排名):",data[:10],"\n對應的單詞",[reverse_dictionary[i] for i in data[:10]]) 42 43 #5.生成Word2Vec的訓練樣本,使用skip-gram模式 44 data_index = 0 45 46 def generate_batch(batch_size,num_skips,skip_window): 47 """ 48 49 :param batch_size: 每個訓練批次的數據量 50 :param num_skips: 每個單詞生成的樣本數量,不能超過skip_window的兩倍,並且必須是batch_size的整數倍 51 :param skip_window: 單詞最遠可以聯系的距離,設置為1則表示當前單詞只考慮前后兩個單詞之間的關系,也稱為滑窗的大小 52 :return:返回每個批次的樣本以及對應的標簽 53 """ 54 global data_index #聲明為全局變量,方便后期多次使用 55 #使用Python中的斷言函數,提前對輸入的參數進行判別,防止后期出bug而難以尋找原因 56 assert batch_size % num_skips == 0 57 assert num_skips <= skip_window * 2 58 59 batch = np.ndarray(shape=(batch_size),dtype=np.int32) #創建一個batch_size大小的數組,數據類型為int32類型,數值隨機 60 labels = np.ndarray(shape=(batch_size,1),dtype=np.int32) #數據維度為[batch_size,1] 61 span = 2 * skip_window + 1 #入隊的長度 62 buffer = collections.deque(maxlen=span) #創建雙向隊列。最大長度為span 63 """ 64 print(batch,"\n",labels) 65 batch :[0 ,-805306368 ,405222565 ,1610614781 ,-2106392574 ,2721-2106373584 ,163793] 66 labels: [[ 0] 67 [-805306368] 68 [ 407791039] 69 [ 536872957] 70 [ 2] 71 [ 0] 72 [ 0] 73 [ 131072]] 74 """ 75 #對雙向隊列填入初始值 76 for _ in range(span): 77 buffer.append(data[data_index]) 78 data_index = (data_index+1) % len(data) 79 """ 80 print(buffer,"\n",data_index) 輸出: 81 deque([5234, 3081, 12], maxlen=3) 82 3 83 """ 84 #進入第一層循環,i表示第幾次入雙向隊列 85 for i in range(batch_size // num_skips): 86 target = skip_window #定義buffer中第skip_window個單詞是目標 87 targets_avoid = [skip_window] #定義生成樣本時需要避免的單詞,因為我們要預測的是語境單詞,不包括目標單詞本身,因此列表開始包括第skip_window個單詞 88 for j in range(num_skips): 89 """第二層循環,每次循環對一個語境單詞生成樣本,先產生隨機數,直到不在需要避免的單詞中,也即需要找到可以使用的語境詞語""" 90 while target in targets_avoid: 91 target = random.randint(0,span-1) 92 targets_avoid.append(target) #因為該語境單詞已經被使用過了,因此將其添加到需要避免的單詞庫中 93 batch[i * num_skips + j] = buffer[skip_window] #目標詞匯 94 labels[i * num_skips +j,0] = buffer[target] #語境詞匯 95 #此時buffer已經填滿,后續的數據會覆蓋掉前面的數據 96 #print(batch,labels) 97 buffer.append(data[data_index]) 98 data_index = (data_index + 1) % len(data) 99 return batch,labels 100 batch,labels = generate_batch(8,2,1) 101 """ 102 for i in range(8): 103 print("目標單詞:"+reverse_dictionary[batch[i]]+"對應編號為:".center(20)+str(batch[i])+" 對應的語境單詞為: ".ljust(20)+reverse_dictionary[labels[i,0]]+" 編號為",labels[i,0]) 104 測試結果: 105 目標單詞:originated 對應編號為: 3081 對應的語境單詞為: as 編號為 12 106 目標單詞:originated 對應編號為: 3081 對應的語境單詞為: anarchism 編號為 5234 107 目標單詞:as 對應編號為: 12 對應的語境單詞為: originated 編號為 3081 108 目標單詞:as 對應編號為: 12 對應的語境單詞為: a 編號為 6 109 目標單詞:a 對應編號為: 6 對應的語境單詞為: as 編號為 12 110 目標單詞:a 對應編號為: 6 對應的語境單詞為: term 編號為 195 111 目標單詞:term 對應編號為: 195 對應的語境單詞為: of 編號為 2 112 目標單詞:term 對應編號為: 95 對應的語境單詞為: a 編號為 6 113 """
在這一個部分,對於理解生成mini-batch數據的那一塊兒非常重要,因為這對我們進行其他任務的數據處理時非常有幫助。大家只要知道這個代碼實現的模型是上面的第二張圖片所對應的模型,就能知道,為什么每次只生成一個標簽,而不是多個。因為我們是一個輸入,一個輸出。也可以看到注釋當中的內容:每兩行的目標單詞都是一樣的,但對應語境單詞不一樣。代碼的第84行到第98行!建議以“我 很 討厭 下雨“為例,手寫一下這個過程。
模型搭建
1 #6.定義訓練數據的一些參數 2 batch_size = 128 #訓練樣本的批次大小 3 embedding_size = 128 #單詞轉化為稠密詞向量的維度 4 skip_window = 1 #單詞可以聯系到的最遠距離 5 num_skips = 1 #每個目標單詞提取的樣本數 6 7 #7.定義驗證數據的一些參數 8 valid_size = 16 #驗證的單詞數 9 valid_window = 100 #指驗證單詞只從頻數最高的前100個單詞中進行抽取 10 valid_examples = np.random.choice(valid_window,valid_size,replace=False) #進行隨機抽取 11 num_sampled = 64 #訓練時用來做負樣本的噪聲單詞的數量 12 13 #8.開始定義Skip-Gram Word2Vec模型的網絡結構 14 #8.1創建一個graph作為默認的計算圖,同時為輸入數據和標簽申請占位符,並將驗證樣例的隨機數保存成TensorFlow的常數 15 graph = tf.Graph() 16 with graph.as_default(): 17 train_inputs = tf.placeholder(tf.int32,[batch_size]) 18 train_labels = tf.placeholder(tf.int32,[batch_size,1]) 19 valid_dataset = tf.constant(valid_examples,tf.int32) 20 21 #選擇運行的device為CPU 22 with tf.device("/cpu:0"): 23 #單詞大小為50000,向量維度為128,隨機采樣在(-1,1)之間的浮點數 24 embeddings = tf.Variable(tf.random_uniform([vocabulary_size,embedding_size],-1.0,1.0)) 25 #使用tf.nn.embedding_lookup()函數查找train_inputs對應的向量embed 26 embed = tf.nn.embedding_lookup(embeddings,train_inputs) 27 #使用截斷正太函數初始化權重,偏重初始化為0 28 weights = tf.Variable(tf.truncated_normal([vocabulary_size,embedding_size],stddev= 1.0 /math.sqrt(embedding_size))) 29 biases = tf.Variable(tf.zeros([vocabulary_size])) 30 #隱藏層實現 31 hidden_out = tf.matmul(embed, tf.transpose(weights)) + biases 32 #將標簽使用one-hot方式表示,便於在softmax的時候進行判斷生成是否准確 33 train_one_hot = tf.one_hot(train_labels, vocabulary_size) 34 cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=hidden_out, labels=train_one_hot)) 35 #優化選擇隨機梯度下降 36 optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(cross_entropy) 37 #為了方便進行驗證,采用余弦定理驗證相似性 鏈接https://blog.csdn.net/u012160689/article/details/15341303 38 #歸一化 39 norm = tf.sqrt(tf.reduce_sum(tf.square(weights),1,keep_dims=True)) 40 normalized_embeddings = weights / norm 41 valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings,valid_dataset) #查詢驗證單詞的嵌入向量 42 #計算驗證單詞的嵌入向量與詞匯表中所有單詞的相似性 43 similarity = tf.matmul( 44 valid_embeddings,normalized_embeddings,transpose_b=True 45 ) 46 init = tf.global_variables_initializer() #定義參數的初始化
訓練&驗證
1 ##9.啟動訓練 2 num_steps = 150001 #進行15W次的迭代計算 3 t0 = time.time() 4 #創建一個回話並設置為默認 5 with tf.Session(graph=graph) as session: 6 init.run() #啟動參數的初始化 7 print("初始化完成!") 8 average_loss = 0 #計算誤差 9 10 #開始迭代訓練 11 for step in range(num_steps): 12 batch_inputs,batch_labels = generate_batch(batch_size,num_skips,skip_window) #調用生成訓練數據函數生成一組batch和label 13 feed_dict = {train_inputs:batch_inputs,train_labels:batch_labels} #待填充的數據 14 #啟動回話,運行優化器optimizer和損失計算函數,並填充數據 15 optimizer_trained,loss_val = session.run([optimizer,cross_entropy],feed_dict=feed_dict) 16 average_loss += loss_val #統計NCE損失 17 18 #為了方便,每2000次計算一下損失並顯示出來 19 if step % 2000 == 0: 20 if step > 0: 21 average_loss /= 2000 22 print('第%d輪迭代用時:%s'% (step, time.time()- t0)) 23 t0 = time.time() 24 print("第{}輪迭代后的損失為:{}".format(step,average_loss)) 25 average_loss = 0 26 27 #每10000次迭代,計算一次驗證單詞與全部單詞的相似度,並將於驗證單詞最相似的前8個單詞呈現出來 28 if step % 10000 == 0: 29 sim = similarity.eval() #計算向量 30 for i in range(valid_size): 31 valid_word = reverse_dictionary[valid_examples[i]] #得到對應的驗證單詞 32 top_k = 8 33 nearest = (-sim[i,:]).argsort()[1:top_k+1] #計算每一個驗證單詞相似度最接近的前8個單詞 34 log_str = "與單詞 {} 最相似的: ".format(str(valid_word)) 35 36 for k in range(top_k): 37 close_word = reverse_dictionary[nearest[k]] #相似度高的單詞 38 log_str = "%s %s, " %(log_str,close_word) 39 print(log_str) 40 final_embeddings = normalized_embeddings.eval() 41 42 #10.可視化Word2Vec效果 43 def plot_with_labels(low_dim_embs,labels,filename = "tsne.png"): 44 assert low_dim_embs.shape[0] >= len(labels),"標簽數超過了嵌入向量的個數!!" 45 46 plt.figure(figsize=(20,20)) 47 for i,label in enumerate(labels): 48 x,y = low_dim_embs[i,:] 49 plt.scatter(x,y) 50 plt.annotate( 51 label, 52 xy = (x,y), 53 xytext=(5,2), 54 textcoords="offset points", 55 ha="right", 56 va="bottom" 57 ) 58 plt.savefig(filename) 59 from sklearn.manifold import TSNE 60 tsne = TSNE(perplexity=30,n_components=2,init="pca",n_iter=5000) 61 plot_only = 100 62 low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only,:]) 63 Labels = [reverse_dictionary[i] for i in range(plot_only)] 64 plot_with_labels(low_dim_embs,Labels) 65 """ 66 第142000輪迭代后的損失為:4.46674475479126 67 第144000輪迭代后的損失為:4.460033647537231 68 第146000輪迭代后的損失為:4.479593712329865 69 第148000輪迭代后的損失為:4.463101862192154 70 第150000輪迭代后的損失為:4.3655951328277585 71 與單詞 can 最相似的: may, will, would, could, should, must, might, cannot, 72 與單詞 were 最相似的: are, was, have, had, been, be, those, including, 73 與單詞 is 最相似的: was, has, are, callithrix, landesverband, cegep, contains, became, 74 與單詞 been 最相似的: be, become, were, was, acuity, already, banded, had, 75 與單詞 new 最相似的: repertory, rium, real, ursus, proclaiming, cegep, mesoplodon, bolster, 76 與單詞 their 最相似的: its, his, her, the, our, some, these, landesverband, 77 與單詞 when 最相似的: while, if, where, before, after, although, was, during, 78 與單詞 of 最相似的: vah, in, neutronic, widehat, abet, including, nine, cegep, 79 與單詞 first 最相似的: second, last, biggest, cardiomyopathy, next, cegep, third, burnt, 80 與單詞 other 最相似的: different, some, various, many, thames, including, several, bearings, 81 與單詞 its 最相似的: their, his, her, the, simplistic, dativus, landesverband, any, 82 與單詞 from 最相似的: into, through, within, in, akita, bde, during, lawless, 83 與單詞 would 最相似的: will, can, could, may, should, might, must, shall, 84 與單詞 people 最相似的: those, men, pisa, lep, arctocephalus, protectors, saguinus, builders, 85 與單詞 had 最相似的: has, have, was, were, having, ascribed, wrote, nitrile, 86 與單詞 all 最相似的: auditum, some, scratch, both, several, many, katydids, two, 87 """
以上就是實現代碼的全部,大家可以粘貼過去,則能運行!但是!但是!但是!會非常慢,因為我們在最后進行softmax的時候,當詞表大小過大的時候,這個計算時間會非常的復雜。因此,word2vec的講點就在於,采用了負采樣的方法,大大提高了模型的訓練速度。有關負采樣,我會單獨寫一篇文檔詳解。對於上述代碼,要改用負采樣的方法進行訓練,則只是需要將模型搭建中的28行到40行替換為如下代碼,即可
1 #優化目標選擇NCE loss 2 #使用截斷正太函數初始化NCE損失的權重,偏重初始化為0 3 nce_weights = tf.Variable(tf.truncated_normal([vocabulary_size,embedding_size],stddev= 1.0 /math.sqrt(embedding_size))) 4 nce_biases = tf.Variable(tf.zeros([vocabulary_size])) 5 6 #計算學習出的embedding在訓練數據集上的loss,並使用tf.reduce_mean()函數進行匯總 7 loss = tf.reduce_mean(tf.nn.nce_loss( 8 weights=nce_weights, 9 biases=nce_biases, 10 labels =train_labels, 11 inputs=embed, 12 num_sampled=num_sampled, 13 num_classes=vocabulary_size 14 )) 15 16 #定義優化器為SGD,且學習率設置為1.0.然后計算嵌入向量embeddings的L2范數norm,並計算出標准化后的normalized_embeddings 17 optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss) 18 norm = tf.sqrt(tf.reduce_sum(tf.square(nce_weights),1,keep_dims=True)) #嵌入向量的L2范數 19 normalized_embeddings = nce_weights / norm #標准哈embeddings
祝大家一切順利!代碼中的個別部分,我在注釋中附上了相應的博客鏈接,感謝這些博主!也感謝大家!如果有神額不正確的地方,歡迎大家指正。
