Introduction
本文接續 word2vec (part2) ,介紹如何根據推導出來的 backward propagation 公式,從頭到尾實作一個簡易版的 word2vec 。
本例的 input layer 採用 skip-gram , output layer 採用 negative sampling
本例用唐詩語料庫:https://github.com/ckmarkoh/coscup_nndl/blob/master/poem.txt
首先,載入所需的模組
1
2
3
4
5
| import json
from collections import Counter, OrderedDict
import numpy as np
import random
import math
|
Build Dictionray
再來是建立字典,即將每個字給一個id來對應。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| def LearnVocabFromTrainFile():
# 開啟唐詩語料庫
f = open("poem.txt")
# 統計唐詩語料庫中每個字出現的頻率
vcount = Counter()
for line in f.readlines():
for w in line.decode("utf-8").strip().split():
vcount.update(w)
# 僅保留出現次數大於五的字,並按照出現次數排序
vcount_list = sorted(filter(lambda x: x[1] >= 5, vcount.items())
, reverse=True, key=lambda x: x[1])
# 建立字典,將每個字給一個id ,字為 key, id 為 value
vocab_dict = OrderedDict(map(lambda x: (x[1][0], x[0]), enumerate(vcount_list)))
# 建立詞頻統計用的字典,給定某字,可查到其出現頻率
vocab_freq_dict = OrderedDict(map(lambda x: (x[0], x[1]), vcount_list))
return vocab_dict, vocab_freq_dict
vocab_dict, vocab_freq_dict = LearnVocabFromTrainFile()
|
印出字典檔,每個字對應到一個id(編號)
1
2
| for w,wid in vocab_dict.items():
print "%s : %s"%(w,wid)
|
結果如下:
1
2
3
4
5
6
7
8
9
10
| 不 : 0
人 : 1
山 : 2
無 : 3
風 : 4
......
謏 : 5496
笮 : 5497
躠 : 5498
噆 : 5499
|
印出詞頻統計用的字典,給定某字,可查詢到其出現頻率:
1
2
| for w,wfreq in vocab_freq_dict.items():
print "%s : %s"%(w,wfreq)
|
結果如下:
1
2
3
4
5
6
7
8
9
10
| 不 : 26426
人 : 20966
山 : 16056
無 : 15795
風 : 15618
...
謏 : 5
笮 : 5
躠 : 5
噆 : 5
|
Build Unigram Table
本例採用 negative sampling ,需要先建立 unigram table 以便進行 negative sampling 。
所謂的 Unigram Table 即是一個 array ,其中每個元素為某字的id,而某字的頻率,即為此id在此 table 中出現的次數的 0.75次方。
例如,id 為 5496 的字,詞頻為 5 ,則在此 Unigram Table 中,5496 的次數為:
由於 array 中的元素個數必須是整數,所以 5496 在 Unigram Table 中出現三次。
建立 Unigram Table 的程式碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
| def InitUnigramTable(vocab_freq_dict):
table_freq_list = map(lambda x: (x[0], int(x[1][1] ** 0.75)), enumerate(vocab_freq_dict.items()))
table_size = sum([x[1] for x in table_freq_list])
table = np.zeros(table_size).astype(int)
offset = 0
for item in table_freq_list:
table[offset:offset + item[1]] = item[0]
offset += item[1]
return table
table = InitUnigramTable(vocab_freq_dict)
|
得出的 Unigram Table 如下:
1
2
3
| [ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 ... , 5495 5495 5495 5496 5496 5496 5497 5497 5497 5498
5498 5498 5499 5499 5499]
|
Training word2vec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
| def train(vocab_dict, vocab_freq_dict, table):
total_words = sum([x[1] for x in vocab_freq_dict.items()])
vocab_size = len(vocab_dict)
# 參數設定
layer1_size = 30 # hidden layer 的大小,即向量大小
window = 2 # 上下文寬度的上限
alpha_init = 0.025 # learning rate
sample = 0.001 # 用來隨機丟棄高頻字用
negative = 10 # negative sampling 的數量
ite = 2 # iteration 次數
# Weights 初始化
# syn0 : input layer 到 hidden layer 之間的 weights ,用隨機值初始化
# syn1 : hidden layer 到 output layer 之間的 weights ,用0初始化
syn0 = (0.5 - np.random.rand(vocab_size, layer1_size)) / layer1_size
syn1 = np.zeros((layer1_size, vocab_size))
# 印出進度用
train_words = 0 # 總共訓練了幾個字
p_count = 0
avg_err = 0.
err_count = 0
for local_iter in range(ite):
print "local_iter", local_iter
f = open("poem.txt")
for line in f.readlines():
#用來暫存要訓練的字,一次訓練一個句子
sen = []
# 取出要被訓練的字
for word_raw in line.decode("utf-8").strip().split():
last_word = vocab_dict.get(word_raw, -1)
# 丟棄字典中沒有的字(頻率太低)
if last_word == -1:
continue
cn = vocab_freq_dict.get(word_raw)
ran = (math.sqrt(cn / float(sample * total_words + 1))) * (sample * total_words) / cn
# 根據字的頻率,隨機丟棄,頻率越高的字,越有機會被丟棄
if ran < random.random():
continue
train_words += 1
# 將要被訓練的字加到 sen
sen.append(last_word)
# 根據訓練過的字數,調整 learning rate
alpha = alpha_init * (1 - train_words / float(ite * total_words + 1))
if alpha < alpha_init * 0.0001:
alpha = alpha_init * 0.0001
# 逐一訓練 sen 中的字
for a, word in enumerate(sen):
# 隨機調整 window 大小
b = random.randint(1, window)
for c in range(a - b, a + b + 1):
# input 為 window 範圍中,上下文的某一字
if c < 0 or c == a or c >= len(sen):
continue
last_word = sen[c]
# h_err 暫存 hidden layer 的 error 用
h_err = np.zeros((layer1_size))
# 進行 negative sampling
for negcount in range(negative):
# positive example,從 sen 中取得,模型要輸出 1
if negcount == 0:
target_word = word
label = 1
# negative example,從 table 中抽樣,模型要輸出 0
else:
while True:
target_word = table[random.randint(0, len(table) - 1)]
if target_word not in sen:
break
label = 0
# 模型預測結果
o_pred = 1 / (1 + np.exp(- np.dot(syn0[last_word, :], syn1[:, target_word])))
# 預測結果和標準答案的差距
o_err = o_pred - label
# backward propagation
# 此部分請參照 word2vec part2 的公式推導結果
# 1.將 error 傳遞到 hidden layer
h_err += o_err * syn1[:, target_word]
# 2.更新 syn1
syn1[:, target_word] -= alpha * o_err * syn0[last_word]
avg_err += abs(o_err)
err_count += 1
# 3.更新 syn0
syn0[last_word, :] -= alpha * h_err
# 印出目前結果
p_count += 1
if p_count % 10000 == 0:
print "Iter: %s, Alpha %s, Train Words %s, Average Error: %s" \
% (local_iter, alpha, 100 * train_words, avg_err / float(err_count))
avg_err = 0.
err_count == 0.
# 每一個 iteration 儲存一次訓練完的模型
model_name = "w2v_model_blog_%s.json" % (local_iter)
print "save model: %s" % (model_name)
fm = open(model_name, "w")
fm.write(json.dumps(syn0.tolist(), indent=4))
fm.close()
|
開始訓練:
1
| train(vocab_dict, vocab_freq_dict, table)
|
輸出結果如下,可以看到,當訓練過的字數增加時, Error 也跟著降低
大概要花幾十分鐘左右訓練完
1
2
3
4
5
6
7
8
9
| Iter: 0, Alpha 0.0249923666923, Train Words 475200, Average Error: 0.499999254842
Iter: 0, Alpha 0.0249846739501, Train Words 954100, Average Error: 0.249998343836
Iter: 0, Alpha 0.0249771900316, Train Words 1420000, Average Error: 0.166660116256
Iter: 0, Alpha 0.0249693430813, Train Words 1908500, Average Error: 0.124949913475
Iter: 0, Alpha 0.024961329072, Train Words 2407400, Average Error: 0.0993522008349
Iter: 0, Alpha 0.0249531817368, Train Words 2914600, Average Error: 0.0787704454331
Iter: 0, Alpha 0.0249453540624, Train Words 3401900, Average Error: 0.06351951221
Iter: 0, Alpha 0.0249377801891, Train Words 3873400, Average Error: 0.0495117808015
..........
|
Show Result
檢視 word2vec 訓練結果的方法,即是看使用 cosine similarity 計算,是否能得出與某字語意相近的字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # 讀取訓練好的模型
f2 = open("w2v_model_1.json", "r")
w2v_model = np.array(json.loads("".join(f2.readlines())))
f2.close()
vocab_dict_reversed = OrderedDict([(x[1], x[0]) for x in vocab_dict.items()])
# 計算 cosine similarity 最高的前五字
def get_top(word):
wid = vocab_dict.get(word)
# 將某字與模型中所有的字向量做內積
dot_result = np.dot(w2v_model, np.expand_dims(w2v_model[wid], axis=1))
norm = np.sqrt(np.sum(np.power(w2v_model.T, 2), axis=0))
# 計算 cosine similarity
cosine_result = np.divide(dot_result[:, 0], norm*norm[wid])
# 根據 cosine similarity 的值排序
final_result = sorted(filter(lambda x:x[0] != wid,
[(x[0], x[1]) for x in enumerate(cosine_result)]),
key=lambda x: x[1], reverse=True)
print word
# 印出語意最接近的前五字,以及其 cosine similarity
for x in final_result[:5]:
print vocab_dict_reversed.get(x[0]), x[1]
|
分別計算「山、峰、河、日」這四字語意最相近的字
1
2
3
4
| get_top(u"山")
get_top(u"峰")
get_top(u"河")
get_top(u"日")
|
結果如下,可看出,計算所得出語意最相近的字,實際上,語意也相近,例如,山和峰、嶺的語意都很接近。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| 山
嶺 0.854901128361
嵩 0.846620438864
峰 0.842831270385
岡 0.838129842909
嶂 0.834701215189
峰
山 0.842831270385
嶽 0.83917452917
嶺 0.8219837161
頂 0.821088331571
嶂 0.809565794884
河
湟 0.787726187693
涇 0.770652269018
淮 0.751135710239
川 0.742243126005
汾 0.740643816278
日
旦 0.869047480855
又 0.842383624714
曛 0.830549707539
夕 0.826327222048
暉 0.82616774597
|
向量加減運算後的 cosine similarity ,例如: 女 + 父 - 男 = 母
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| def get_calculated_top(w1, w2, w3):
wid1, wid2, wid3 = vocab_dict.get(w1), vocab_dict.get(w2), vocab_dict.get(w3)
v1, v2, v3 = w2v_model[wid1], w2v_model[wid2], w2v_model[wid3]
# 得出加減運算後的向量
combined_vec = v1 + (v2 - v3)
dot_result = np.dot(w2v_model, np.expand_dims(combined_vec, axis=1))
norm = np.sqrt(np.sum(np.power(w2v_model.T, 2), axis=0))
cvec_norm = np.sqrt(np.sum(np.power(combined_vec, 2)))
cosine_result = np.divide(dot_result[:, 0], norm * cvec_norm)
final_result = sorted(filter(lambda x: x[0] not in [wid1, wid2, wid3],
[(x[0], x[1]) for x in enumerate(cosine_result)]),
key=lambda x: x[1], reverse=True)
print "%s + %s - %s" % (w1, w2, w3)
for x in final_result[:5]:
print vocab_dict_reversed.get(x[0]), x[1]
|
1
| get_calculated_top(u"女", u"父", u"男")
|
結果如下,如預期,運算結果的語意接近「母」:
1
2
3
4
5
6
| 女 + 父 - 男
母 0.731002049447
娥 0.707469857054
客 0.69027387716
娃 0.687831493041
侶 0.681667240226
|