本節課主要通過以下幾個方面講解了word2vec:
- 詞向量出現的原因
- one-hot representation VS Dristributed representation
- 兩種word2vec的算法:
- Skip-grams (SG):預測上下文
- Continuous Bag of Words (CBOW):預測目標單詞 - 兩種高效的訓練方法:(以後補充)
- Hierarchical softmax
- Negative sampling - Skip-grams的tensorflow源碼實現過程
- Skip-grams的純手寫實現過程
1. 詞向量出現的原因
在標準的語言學中,單詞就像一種語言學符號,它指代了世界上某些具體的事物。但是這樣的解釋應用在計算機系統去處理語言是困難的。用分類資源來處理詞義的WordNet擁有很多詞彙分類信息。但是他很難具體的判斷兩個單詞的相似性,或者說不能把相似性量化。因此詞向量產生了。
2. one-hot representation VS Dristributed representation
- one-hot representation
根據語料庫的大小確定詞向量的維度,當單詞出現在某個位置,這個維度數值爲1,剩餘維度爲0. 這樣產生的詞向量面臨着兩個問題:當語料庫比較大時,詞向量面臨着維度災難;由於one-hot向量之間是互相正交的,所以無論兩個單詞如何相似,one-hot向量都不能刻畫其相似度。例如課程中講到的motel和hotel:
- Dristributed representation
分佈式的表示可以解決one-hot向量的問題。它通過模型的訓練,將每個單詞映射到一個低維的向量空間,而且他是一個dense的向量。完美的解決了one-hot向量表示的問題。唯一的缺點就是我們不能解釋道具體每一個維度代表的什麼。具體的效果可以通過後面的編碼部分看到。
3. 兩種word2vec的算法
word2vec的方法主要有Skip-grams (SG)和Continuous Bag of Words (CBOW)和兩種模型。而Skip-Gram是從中心詞推測出上下文;而CBOW是從上下文推測推測中心詞。下面是兩種模型的結構圖:
下面主要圍繞Skip-grams (SG)模型展開
3.1 Skip-grams (SG)的框架細節如下圖中所示
上圖中V代表語料字典的大小,d代表了單詞embedding的維度。
w_t是一箇中心單詞的one-hot形式。W是中心詞語embedding的矩陣。這兩個相乘,相當於一個查表操作,在W中得到中心詞語的embedding vector表示。中間的W’是存儲上下文單詞的embedding的矩陣。中心詞語的embedding與上下文單詞的embedding的矩陣相乘得到每個上下文備選詞語與已經選定的中心詞語的相似度,就是上文中提到的vc*uo。最相似的一個或幾個我們就認爲它們是中心詞語的上下文。使用softmax得到的概率與真實上下文單詞的one-hot形式作比較。還可以看出目前的預測並不準確。
3.2 Skip-grams (SG)所涉及到的公式以及推導過程
對於一個長度爲T的語料庫,假設我們爲每一個詞選取的上下文窗口的大小是m(指的是上下文各m個詞),則我們的目標函數是最大化訓練語料的對數似然概率:
其中t爲中心詞,θ是模型參數
對於p(o|c):
其中c代表中心詞,o代表上下文某個詞.
v_c和u_o分別是中心詞和輸出詞的向量,也就是我們模型中的參數。
觀察可以發現p(o|c)是一個softmax形式,softmax函數可以得到數字到概率分佈的映射,也就是說可以講數字轉化爲概率。因此這裏使用softmax將一堆相似度轉化爲概率。
優化這個目標函數的算法是隨機梯度下降法(SGD)
首先對"center"向量v_c進行求導,得到以下結果:
首先對"output"向量u_o進行求導,得到以下結果:
通過不斷地迭代優化,得到作爲中心詞的向量表示V和作爲上下文的向量表示U。課程中最後將兩個向量拼接起來得到我們想要的詞向量,也可以對應位置相加求和得到詞向量。
5. Skip-grams的tensorflow源碼實現過程
將TensorFlow實現word2vec的basic版過了一遍,寫了詳細的備註,並實現了一個小例子(Github)。
語料採用的金庸大師的《倚天屠龍記》,《停用詞表》採用的哈工大發布的語料。
- 展示詞向量具體效果的函數還沒寫,接下來會補充
- 後面nce loss具體細節會細看。
效果和代碼如下:
由於文本有限,效果肯定也就有限啦~
from numpy import random
import numpy as np
import collections
import math
import tensorflow as tf
import jieba
from collections import Counter
# 此函數作用是對初始語料進行分詞處理
def cut_txt(old_file):
cut_file = old_file + '_cut.txt' # 分詞之後保存的文件名
fi = open(old_file, 'r', encoding='utf-8') #注意操作前要把文件轉化成utf-8文件
text = fi.read() # 獲取文本內容
new_text = jieba.cut(text, cut_all=False) # 採用精確模式切詞
str_out = ' '.join(new_text)
#去除停用詞
stopwords = [line.strip() for line in open('DataSet/中文停用詞.txt', 'r',encoding='utf-8').readlines()]
for stopword in stopwords:
str_out=str_out.replace(' '+stopword+' ',' ')
fo = open(cut_file, 'w', encoding='utf-8')
fo.write(str_out)
ret_list=str_out.split()#訓練語料
ret_set=list(set(ret_list))#字典
list_len=len(ret_list)
set_len=len(set(ret_list))
print('總字數 總詞數 :')
print(list_len,set_len) #總字數 總詞數
print('詞頻字典 :')
print(dict(Counter(ret_list)))# 詞頻字典
return ret_list,ret_set
# 預處理切詞後的數據
def build_dataset(words, n_words):
count = [['UNK', -1]] #存放詞頻做大的n_words個單詞,第一個元素爲單詞,第二個元素爲詞頻。UNK爲其他單詞
count.extend(collections.Counter(words).most_common(n_words - 1))#獲取詞頻做大的n_words-1個單詞(因爲有了UNK,所以少一個)
dictionary = dict() #建立單詞的索引字典
for word, _ in count:
dictionary[word] = len(dictionary) #建立單詞的索引字典,key爲單詞,value爲索引值
data = list()# 建立存放訓練語料對應索引號的list,也就是說將訓練的語料換成每個單詞對應的索引
unk_count = 0 #計數UNK的個數
for word in words:
if word in dictionary:
index = dictionary[word]#獲取單詞在字典中的索引
else:
index = 0 #字典中不存在的單詞(UNK),在字典中的索引是0
unk_count += 1 #每次遇到字典中不存在的單詞 UNK計數器加1
data.append(index)#將訓練語料對應的索引號存入data中
count[0][1] = unk_count
reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))#將單詞的索引字典反轉,即將key和value對換
return data, count, dictionary, reversed_dictionary#訓練語料的索引;top大的單詞詞頻;字典{單詞:索引值};字典{索引值:單詞}
# 爲 skip-gram model 產生bathch訓練樣本.
#從文本總體的第skip_window+1個單詞開始,每個單詞依次作爲輸入,它的輸出可以是上下文範圍內的單詞中的任何一個單詞。一般不是取全部而是隨機取其中的幾組,以增加隨機性。
def generate_batch(batch_size, num_skips, skip_window):#batch_size 就是每次訓練用多少數據,skip_window是確定取一個詞周邊多遠的詞來訓練,num_skips是對於一個輸入數據,隨機取其窗口內幾個單詞作爲上下文(即輸出標籤)。
global data_index
assert batch_size % num_skips == 0#保證batch_size是 num_skips的整倍數,控制下面的循環次數
assert num_skips <= 2 * skip_window #保證num_skips不會超過當前輸入的的上下文的總個數
batch = np.ndarray(shape=(batch_size), dtype=np.int32) #存儲訓練語料中心詞的索引
labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)#存儲訓練語料中心詞對應的上下文的索引
span = 2 * skip_window + 1 # [ skip_window target skip_window ]
#這個很重要,最大長度是span,後面如果數據超過這個長度,前面的會被擠掉,這樣使得buffer裏面永遠是data_index周圍的span歌數據,
#而buffer的第skip_window個數據永遠是當前處理循環裏的輸入數據
buffer = collections.deque(maxlen=span)#一個完整的窗口存儲器
if data_index + span > len(data):
data_index = 0
buffer.extend(data[data_index:data_index + span])
data_index += span #獲取下一個要進入隊列的訓練數據的索引
for i in range(batch_size // num_skips):#一個batch一共需要batch個訓練單詞對,每個span中隨機選取num_skips個單詞對,所以要循環batch_size // num_skips次
target = skip_window # 中心詞索引在buffer中的位置
targets_to_avoid = [skip_window] #自己肯定要排除掉,不能自己作爲自己的上下文
for j in range(num_skips):#採樣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] #這裏保存的是訓練的輸入序列
labels[i * num_skips + j, 0] = buffer[target] #這裏保存的是訓練時的輸出序列,也就是標籤
if data_index == len(data): #超長時回到開始
buffer.extend(data[0:span])
data_index = span
else:
buffer.append(data[data_index]) #append時會把queue的開始的一個擠掉
data_index += 1 #此處是控制模型窗口是一步步往後移動的
data_index = (data_index + len(data) - span) % len(data)# 倒回一個span,防止遺漏最後的一些單詞
return batch, labels#返回 輸入序列 輸出序列
############ 第一步:對初始語料進行分詞處理 ############
train_data,dict_data=cut_txt('DataSet/倚天屠龍記.txt')#切詞
vocabulary_size =10000#字典的大小,只取詞頻top10000的單詞
############ 第二步:預處理切詞後的數據 ############
data, count, dictionary, reverse_dictionary = build_dataset(train_data,vocabulary_size)#預處理數據
print()
print(data)
print()
print(count)
print()
print(dictionary)
print()
print(reverse_dictionary)
print('Most common words (+UNK)', count[:5])#詞頻最高的前5個單詞
print('Sample data', data[:10], [reverse_dictionary[i] for i in data[:10]])#前10個訓練數據索引及其具體單詞
############ 第三步:爲 skip-gram model 產生bathch訓練樣本. ############
data_index = 0#控制窗口滑動的
batch, labels = generate_batch(batch_size=128, num_skips=8, skip_window=5)#產生一個batch的訓練數據。batch大小128;從上下文中隨機抽取8個單詞作爲輸出標籤;窗口大小5(即一個窗口下11個單詞,1個人中心詞,10個上下文單詞);
for i in range(10):#輸出一下一個batch中的前10個訓練數據對(即10個訓練樣本)
print(batch[i], reverse_dictionary[batch[i]],'->', labels[i, 0], reverse_dictionary[labels[i, 0]])
############ 第四步: 構造一個訓練skip-gram 的模型 ############
batch_size = 128 #一次更新參數所需的單詞對
embedding_size = 128 # 訓練後詞向量的維度
skip_window = 5 #窗口的大小
num_skips = 8 # 一個完整的窗口(span)下,隨機取num_skips個單詞對(訓練樣本)
# 構造驗證集的超參數
valid_size = 16 # 隨機選取valid_size個單詞,並計算與其最相似的單詞
valid_window = 100 # 從詞頻最大的valid_window個單詞中選取valid_size個單詞
valid_examples = np.random.choice(valid_window, valid_size, replace=False)#選取驗證集的單詞索引
num_sampled = 64 #負採樣的數目
graph = tf.Graph()
with graph.as_default():
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) #驗證集
embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))#定義單詞的embedding
embed = tf.nn.embedding_lookup(embeddings, train_inputs)#窗口查詢中心詞對應的embedding
# 爲 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]))#偏差
# 對於一個batch,計算其平均的 NEC loss
# 採用負採樣優化訓練過程
loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weights,biases=nce_biases,labels=train_labels,inputs=embed,num_sampled=num_sampled,num_classes=vocabulary_size))
#採用隨機梯度下降優化損失函數,學習率採用1.0
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)
# 從字典中所有的單詞計算一次與驗證集最相似(餘弦相似度判斷標準)的單詞
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)#驗證集的單位向量,乘以所有單詞的單位向量。得到餘弦相似度
# 變量初始化
init = tf.global_variables_initializer()
############ 第五步:開始訓練 ############
num_steps = 100001 #迭代次數
with tf.Session(graph=graph) as session:
init.run()
print('開始訓練')
average_loss = 0
for step in range(num_steps):
batch_inputs, batch_labels = generate_batch(batch_size, num_skips, skip_window)#產生一個batch
feed_dict = {train_inputs: batch_inputs, train_labels: batch_labels}#tensor的輸入
_, loss_val = session.run([optimizer, loss], feed_dict=feed_dict)#得到一個batch的損失值
average_loss += loss_val #損失值累加
if step % 2000 == 0:#每迭代2000次,就計算一次平均損失,並輸出
if step > 0:
average_loss /= 2000
print('Average loss at step ', step, ': ', average_loss)
average_loss = 0 #每2000次迭代後,將累加的損失值歸零
if step % 10000 == 0:#每迭代10000次 就計算一次與驗證集最相似的單詞,由於計算量很大,所以儘量少計算相似度
sim = similarity.eval()
for i in range(valid_size):
valid_word = reverse_dictionary[valid_examples[i]]#得到需驗證的單詞
top_k = 10 # 和驗證集最相似的top_k個單詞
nearest = (-sim[i, :]).argsort()[1:top_k + 1]#最鄰近的單詞的索引,[1:top_k + 1]從1開始,是跳過了本身
log_str = 'Nearest to %s:' % valid_word
for k in range(top_k):
close_word = reverse_dictionary[nearest[k]]#獲得第k個最近的單詞
log_str = '%s %s,' % (log_str, close_word) #拼接要輸出的字符串
print(log_str)
final_embeddings = normalized_embeddings.eval()
6. Skip-grams的純手寫實現過程(還有問題,需要修改)
# 此函數作用是對初始語料進行分詞處理後,作爲訓練模型的語料
def cut_txt(old_file):
import jieba
from collections import Counter
global cut_file # 分詞之後保存的文件名
cut_file = old_file + '_cut.txt'
fi = open(old_file, 'r', encoding='utf-8')
text = fi.read() # 獲取文本內容
new_text = jieba.cut(text, cut_all=False) # 精確模式
str_out = ' '.join(new_text)
str_out =str_out.replace(',', '').replace('。', '').replace('?', '').replace('!', '') \
.replace('“', '').replace('”', '').replace(':', '').replace('…', '').replace('(', '').replace(')', '') \
.replace('—', '').replace('《', '').replace('》', '').replace('、', '').replace('‘', '') \
.replace('’', '') # 去掉標點符號
#去除停用詞
# stopwords = [line.strip() for line in open('DataSet/中文停用詞.txt', 'r',encoding='utf-8').readlines()]
# for stopword in stopwords:
# str_out=str_out.replace(' '+stopword+' ',' ')
fo = open(cut_file, 'w', encoding='utf-8')
fo.write(str_out)
ret_list=str_out.split()#訓練語料
ret_set=list(set(ret_list))#字典
list_len=len(ret_list)
set_len=len(set(ret_list))
print('總字數 總詞數 :')
print(list_len,set_len) #總字數 總詞數
print('詞頻字典 :')
print(dict(Counter(ret_list)))# 詞頻字典
return ret_list,ret_set
def derivate_v(t,j,X,Y):
v_c=Y[dict_data.index(train_data[t])] #中心詞向量 1×d維
u_o=X[dict_data.index(train_data[t - j])] # 上下文的詞向量 1×d維
exp_below=np.sum(np.exp(X.dot(np.transpose(v_c)))) #v_c 1×d維
temp_top=np.reshape(np.exp(X.dot(np.transpose(v_c))),(VOCA_LEN,1))
exp_top=np.sum((np.tile(temp_top,(1,DIMENSION_VECTOR)))*X,axis=0)
return u_o-exp_top/exp_below
def derivate_u(t,j,X,Y):
v_c = Y[dict_data.index(train_data[t])] # 中心詞向量 1×d維
u_o = X[dict_data.index(train_data[t - j])] # 上下文的詞向量 1×d維
exp_below = np.sum(np.exp(X.dot(np.transpose(v_c)))) # v_c 1×d維
exp_top=np.exp(u_o.dot(np.transpose(u_o)))*v_c
return v_c-exp_top/exp_below
from numpy import random
import numpy as np
import heapq
from sklearn.metrics.pairwise import cosine_similarity
train_data,dict_data=cut_txt('DataSet/倚天屠龍記.txt')
DIMENSION_VECTOR=50
WINDOWS=5
ALPHA=0.01
VOCA_LEN=len(dict_data)
V=np.transpose(random.random(size=(DIMENSION_VECTOR,VOCA_LEN))) # W矩陣 v×d維
U=np.copy(V) # U矩陣 v×d維
for t in range(WINDOWS,VOCA_LEN-WINDOWS):
for j in range(WINDOWS,-(WINDOWS+1),-1):
if j == 0: continue
V_new=V[dict_data.index(train_data[t])]-ALPHA*derivate_v(t,j,U,V)
U_new=U[dict_data.index(train_data[t-j])] - ALPHA * derivate_u(t,j,U,V)
# print(V[dict_data.index(train_data[t])])
# print(V_new)
V[dict_data.index(train_data[t])]=V_new
U[dict_data.index(train_data[t - j])]=U_new
print(t)
V_U=V+U
V1 = np.reshape(V_U[dict_data.index('張無忌')],(1,-1))
similarity=cosine_similarity(V1,V_U).tolist()[0]
max_num_index_list = list(map(similarity.index, heapq.nlargest(11, similarity)))
for k in range(len(max_num_index_list)):
print(dict_data[max_num_index_list[k]])
print('-------------------------------------')