gensim中word2vec python原始碼理解(一)
gensim中word2vec python原始碼理解(一)使用Hierarchical Softmax方法構建單詞表
gensim中word2vec python原始碼理解(二)Skip-gram模型訓練
本文主要談一談對gensim包中封裝的word2vec python原始碼中,使用Hierarchical Softmax構建單詞表部分程式碼的理解。
由於之前閱讀的論文是對使用Hierarchical Softmax的Skip-gram模型進行拓展,因此在閱讀程式碼的時候重點閱讀了Hierarchical Softmax構建單詞表的方法,以及Skip-gram模型的訓練方法。對於negative sampling方法和CBOW模型的實現方法,則會繼續對程式碼進行研究。
init
初始化一個model(實際上是Word2Vec類的例項化物件):
model = Word2Vec(sentences, size=100, window=5, min_count=5, workers=4)
進入類的初始化方法__init__
,對裡面的屬性值進行初始化。
在傳入的訓練句子不為空的情況下,主要呼叫兩個方法:
self.build_vocab(sentences, trim_rule=trim_rule) self.train( sentences, total_examples=self.corpus_count, epochs=self.iter, start_alpha=self.alpha, end_alpha=self.min_alpha )
build_vocab
該方法是從句子序列中構建單詞表,其中每個句子都是字串組成的列表。依次呼叫了三個方法:scan_vocab
,scale_vocab
,finalize_vocab
下面依次介紹三個方法的功能:
scan_vocab
:對句子中的單詞進行初始化
程式碼內容閱讀(有省略):
sentence_no = -1 #儲存掃描完成的句子數量
total_words = 0 #儲存出現的單詞總數(不去重)
min_reduce = 1
vocab = defaultdict(int) #將單詞表初始化為一個字典
checked_string_types = 0
#掃描每個句子
for sentence_no, sentence in enumerate(sentences): #取出語料中每個句子和其在語料庫中的編號no
for word in sentence:
vocab[word] += 1 #記錄每個詞出現的次數
total_words += len(sentence) #記錄掃描過的句子裡的單詞總數
if self.max_vocab_size and len(vocab) > self.max_vocab_size: #如果對於最大單詞數有限制且當前超出限制
#將語料庫中小於min_reduce(初始值為1)的單詞都刪除
utils.prune_vocab(vocab, min_reduce, trim_rule=trim_rule)
min_reduce += 1 #不斷增大min_reduce,直到單詞表長度不大於max_vocab_size
self.corpus_count = sentence_no + 1 #儲存語料數(句子數)
self.raw_vocab = vocab #儲存單詞表
return total_words #返回單詞總數
scale_vocab
:應用min_count
的詞彙表設定(丟棄不太頻繁的單詞)和sample
(控制更頻繁單詞的取樣)。
程式碼內容閱讀(有省略):
載入新的詞彙表:
if not update: #載入一個新的詞彙表
retain_total, retain_words = 0, [] #保留總數,保留的單詞
#獲得單詞及其出現的數量,raw_vocab是scan_vocab中儲存的單詞表dict
for word, v in iteritems(self.raw_vocab):
#判斷當前單詞是否被丟棄,trim_rule為修剪規則,預設為none
if keep_vocab_item(word, v, min_count, trim_rule=trim_rule):
retain_words.append(word) #新增單詞
retain_total += v #新增詞數
if not dry_run:
#為每個單詞構建一個Vocab類,傳入詞頻、下標
self.wv.vocab[word] = Vocab(count=v, index=len(self.wv.index2word))
self.wv.index2word.append(word)
else: #不符合條件則丟棄
drop_unique += 1
drop_total += v
新增新的單詞更新模型:
else:
new_total = pre_exist_total = 0
new_words = pre_exist_words = []
for word, v in iteritems(self.raw_vocab):#遍歷更新的單詞表
if keep_vocab_item(word, v, min_count, trim_rule=trim_rule): #判斷當前單詞是否被丟棄
if word in self.wv.vocab: #如果單詞存在在之前的單詞表中
pre_exist_words.append(word) #新增至先前存在的單詞list
pre_exist_total += v#新增詞頻
if not dry_run:
self.wv.vocab[word].count += v#更新原單詞表的詞頻
else: #如果單詞不存在在之前的單詞表中(新單詞)
new_words.append(word)
new_total += v
if not dry_run:
#為單詞構建一個Vocab類
self.wv.vocab[word] = Vocab(count=v, index=len(self.wv.index2word))
self.wv.index2word.append(word)#給單詞新增下標
else:#不符合條件則丟棄
drop_unique += 1
drop_total += v
計算取樣閾值
# 預先計算每個詞彙專案的取樣閾值
if not sample:
# no words downsampled 沒有單詞被downsample,閾值等於單詞總數
threshold_count = retain_total
elif sample < 1.0:
# traditional meaning: set parameter as proportion of total
threshold_count = sample * retain_total
else:
# new shorthand: sample >= 1 means downsample all words with higher count than sample
threshold_count = int(sample * (3 + sqrt(5)) / 2)
downsample_total, downsample_unique = 0, 0
for w in retain_words:
v = self.raw_vocab[w]#v是當前單詞出現的次數
word_probability = (sqrt(v / threshold_count) + 1) * (threshold_count / v)
if word_probability < 1.0:
downsample_unique += 1
downsample_total += word_probability * v
else: #如果沒有設定sample值的話,word_probability一定>1
word_probability = 1.0
downsample_total += v
if not dry_run:
self.wv.vocab[w].sample_int = int(round(word_probability * 2**32)) #設定一個取樣值,round返回浮點數x的四捨五入值。
finalize_vocab
:根據最終詞彙表設定建立表格和模型權重。
程式碼內容閱讀(有省略):
if not self.wv.index2word:
self.scale_vocab()
if self.sorted_vocab and not update:
self.sort_vocab() #按照詞頻降序排列,使得詞頻大的詞下標更小
if self.hs:
# 新增每個單詞的Huffman編碼資訊
self.create_binary_tree()
if self.negative:
# 負取樣
self.make_cum_table()
if self.null_word:
# create null pseudo-word for padding when using concatenative L1 (run-of-words)
# this word is only ever input – never predicted – so count, huffman-point, etc doesn't matter
word, v = '\0', Vocab(count=1, sample_int=0)
v.index = len(self.wv.vocab)
self.wv.index2word.append(word)
self.wv.vocab[word] = v
# set initial input/projection and hidden weights
if not update:#如果不是新增新詞以更新,則重置權重矩陣
self.reset_weights()
else:
self.update_weights()
從程式碼中可以看出,Hierarchical Softmax方法和negative sampling方法對應兩種構建詞表的方法,分別是create_binary_tree
和make_cum_table
。
create_binary_tree
Hierarchical Softmax方法,使用儲存的詞彙單詞及其詞頻建立一個二進位制哈夫曼樹。頻繁的詞編碼更短。
# build the huffman tree
heap = list(itervalues(self.wv.vocab)) #將字典中的value以列表形式返回,其value是Vocab類的例項
heapq.heapify(heap)
for i in xrange(len(self.wv.vocab) - 1): #儲存內節點
min1, min2 = heapq.heappop(heap), heapq.heappop(heap)#取出最小的兩個
#放入兩個小值節點的父節點,下標從單詞表長度向後取,count值取兩個孩子節點的count之和,設定左右孩子
heapq.heappush(
heap, Vocab(count=min1.count + min2.count, index=i + len(self.wv.vocab), left=min1, right=min2)
)#最終只剩一個根節點在堆疊中
# recurse over the tree, assigning a binary code to each vocabulary word
#在樹上遞迴,為每個詞彙詞分配一個二進位制程式碼,儲存到達該節點的路徑上經過的內節點
if heap:
max_depth, stack = 0, [(heap[0], [], [])] #定義一個最大深度,一個堆疊,放入根節點
while stack:
node, codes, points = stack.pop()
#node節點對應一個Vocab類的例項(也就是一個節點),code對應該節點的編碼,points對應到達該節點經過的節點
if node.index < len(self.wv.vocab):
#如果取出的節點下標小於單詞表的長度,即該詞在單詞表內,取出的是葉節點
# 葉節點=>從根儲存它的路徑
node.code, node.point = codes, points
max_depth = max(len(codes), max_depth)
else: #否則,取出的是內節點=>繼續遞迴
# inner node => continue recursion
#儲存路徑經過的節點
points = array(list(points) + [node.index - len(self.wv.vocab)], dtype=uint32)
# 把左右孩子節點放入棧中
stack.append((node.left, array(list(codes) + [0], dtype=uint8), points))
stack.append((node.right, array(list(codes) + [1], dtype=uint8), points))
在構建單詞表完成後,每個單詞對應的都是類Vocab的一個例項,構建哈夫曼樹完成之後,二叉樹中每個內節點對應的也是一個Vocab類的例項,其left和right屬性分別儲存了其左右孩子,points儲存根節點到達該節點的路徑(由經過的內節點的序號構成),codes儲存該節點的二進位制編碼。
reset_weights
重置隱藏層的權重
#syn0表示詞向量矩陣
#單詞數為行,向量維數為列, empty 會建立一個沒有使用特定值來初始化的陣列
self.wv.syn0 = empty((len(self.wv.vocab), self.vector_size), dtype=REAL)
# 對於每個單詞分別為其初始化一個向量,而不是立即在RAM中實現巨大的隨機矩陣
for i in xrange(len(self.wv.vocab)): #對於單詞表中的每一個單詞
#初始化單詞向量
self.wv.syn0[i] = self.seeded_vector(self.wv.index2word[i] + str(self.seed))
if self.hs:
#syn0表示二叉樹的內節點向量矩陣,全部初始化為0向量
self.syn1 = zeros((len(self.wv.vocab), self.layer1_size), dtype=REAL)
if self.negative:
self.syn1neg = zeros((len(self.wv.vocab), self.layer1_size), dtype=REAL)
self.wv.syn0norm = None
self.syn0_lockf = ones(len(self.wv.vocab), dtype=REAL) # zeros suppress learning
至此,構建單詞表完成。