[語料庫模型] 07-程式碼: 餘弦相似性

嗨,昨天語料庫模型建好了,下一步要如何使用呢? 我們要如何比對輸入的句子與語料庫中的哪一句最相似呢?

相似度計算方式

計算兩個點之間存在的差異大小,主要有兩種方式,歐式距離與餘弦相似度。
詳細的介紹參考: 歐氏距離與餘弦相似度的比較

歐式距離

主要計算空間中點與點之間的距離。
距離越大,相似性越低,反之亦然。

歐式距離

餘弦相似度

主要計算向量間的夾角大小。
夾角越大,相似性越低,反之亦然。

餘弦相似度

而餘弦相似度的公式由「內積公式」推導而來。

formula

餘弦相似度無關乎向量大小,重點是向量之間的方向。

  • 方向一致時餘弦值為 1
  • 直角時餘弦相似度的值為 0
  • 方向相反時餘弦值為 -1

詞向量

此段參考 机器是这样理解语言 - 词向量 | 莫煩Python

比較

大部分時候,我們注重的不一定是點與點之間的距離,更重要的是兩個向量的方向是否接近。這時候,餘弦相似度就比歐式距離適合。

程式碼

莫煩 Python 的程式碼: https://github.com/MorvanZhou/NLP-Tutorials
我修改過的版本: https://gitlab.com/graduate_lab415/nlp/-/blob/master/main.py

執行

1
2
3
4
5
6
7
8
9
10
11
12
"""test"""
q = sys.argv[1]
# q = "拐杖去哪裡申請"
# q = "I like kitty"
if CUT_METHOD == "ckip":
tmp_q = [q]
q = tmp_q

scores = docs_score(q)
d_ids = scores.argsort()[-3:][::-1]
if LOG:
print("\n[{}] top 3 docs for '{}':\n{}".format(CUT_METHOD, (q if CUT_METHOD != "ckip" else q[0]), [docs[i] for i in d_ids]))

第一句 q 是讀入要比較的句子,sys.argv[1] 就是用來讀取執行程式碼時輸入的參數,過幾天的文章會寫到。我如果還記得,再回來補連結…
第一個 if 那邊是因為 CKIP 和 Jieba 轉換完的格式不一樣,需要做個轉換,統一一下。
docs_score(q) 計算 q 和其他所有句子的餘弦相似性。

Python List 操作方式

1
2
3
4
5
a = [1, 2, 3, 4, 5]
a[::-1] #[::-1] 是倒置,輸出: [5, 4, 3, 2, 1]
a[-3] #[-3] 是倒數第 3 個,輸出: 3
a[-3:] #[-3:] 是從倒數第三個到最後一個,輸出: [3, 4, 5]
a[-3:][::-1] #輸出: [5, 4, 3]

計算分數(餘弦相似度)

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
def docs_score(query, len_norm=False):
"""斷詞"""
if CUT_METHOD == "jieba":
q_words = CutSentence.cut_jeiba(re.sub('[??()()「」,,、。::_“”→]', ' ', query), log=LOG) # use jieba
elif CUT_METHOD == "ckip":
q_words = CutSentence.cut_ckip(query, log=LOG) # use ckip
temp_arr = np.array(q_words)
temp_arr = temp_arr.reshape(len(q_words[0]))
q_words = temp_arr
else:
q_words = query.replace(",", "").split(" ")

"""label"""
if LABEL:
q_words = add_label(q_words)

"""add unknown words"""
unknown_v = 0
for v in set(q_words):
if v not in v2i:
v2i[v] = len(v2i)
i2v[len(v2i) - 1] = v
unknown_v += 1
if unknown_v > 0:
_idf = np.concatenate((idf, np.zeros((unknown_v, 1), dtype=np.float)), axis=0)
_tf_idf = np.concatenate((tf_idf, np.zeros((unknown_v, tf_idf.shape[1]), dtype=np.float)), axis=0)
else:
_idf, _tf_idf = idf, tf_idf
counter = Counter(q_words)
q_tf = np.zeros((len(_idf), 1), dtype=np.float) # [n_vocab, 1]
for v in counter.keys():
q_tf[v2i[v], 0] = counter[v]
q_vec = q_tf * _idf # [n_vocab, 1]

q_scores = cosine_similarity(q_vec, _tf_idf) # 拿q的向量和所有向量比對
if len_norm:
len_docs = [len(d) for d in docs_words]
q_scores = q_scores / np.array(len_docs)
return q_scores

一個新拿到的 q (這個 function 中叫 query),進行比較前,也要經過段詞、加 label,再檢查 query 有沒有新的詞,有的話就要加到 v2ii2v 裡面。將 query 轉換成向量 q_vec 後才能算相似性唷。

最後 q_scores = cosine_similarity(q_vec, _tf_idf) 計算 query 與每個句子的相似性。

結語

終於寫完了,這篇應該本系列是最難的一篇了,我早上還先複習了數學呢。

參考資料