從勾股定理到餘弦相似度-程式設計師的數學基礎
大部分程式設計師由於理工科的背景,有一些高數、線性代數、機率論與數理統計的數學基礎。所以當機器學習的熱潮來臨的時候,都躍躍欲試,對機器學習的演算法以及背後的數學思想有比較強烈的探索慾望。
本文的作者就是其中的一位。然而實踐的過程中,又發現數學知識的理解深度有些欠缺,在理解一些公式背後的意義時,有些力不從心的感覺。因此梳理了一些數學上的知識盲點,理順自己的知識脈絡,順便分享給有需要的人。
本文主要講解餘弦相似度的相關知識點。相似度計算用途相當廣泛,是搜尋引擎、推薦引擎、分類聚類等業務場景的核心點。為了理解清楚餘弦相似度的來龍去脈,我將會從最簡單的初中數學入手,逐步推匯出餘弦公式。然後基於餘弦公式串講一些實踐的例子。
一、業務背景
通常我們日常開發中,可能會遇到如下的業務場景。
精準營銷,影像處理,搜尋引擎 這三個看似風馬牛不相及的業務場景,其實面臨一個共同的問題就是相似度的計算。例如精準營銷中的人群擴量涉及使用者相似度的計算;影像分類問題涉及影像相似度的計算,搜尋引擎涉及查詢詞和文件的相似度計算。相似度計算中,可能由於《數學之美》的影響,大家最熟悉的應該是餘弦相似度。那麼餘弦相似度是怎麼推匯出來的呢?
二、數學基礎
理解餘弦相似度,要從理解金字塔開始。我們知道金字塔的底座是一個巨大的正方形。例如吉薩大金字塔的邊長超過230m。構造這樣一個巨大的正方形,如何保證構造出來的圖形不走樣呢?比如如何確保構造的結果不是菱形或者梯形。
1、勾股定理
要保證構造出來的四邊形是正方形,需要保證兩個點:其一是四邊形的邊長相等;其二是四邊形的角是直角。四邊形的邊長相等很容易解決,在工程實踐中,取一根定長的繩子作為邊長就可以了。如何保障直角呢?古人是利用勾股定理解決的,更切確地說是勾股定理的逆定理。
構造一個三角形,保證三角形的三邊長分別是3,4,5。那麼邊長為5的邊對應的角為直角。中國有個成語“無規矩不成方圓”,其中的矩,就是直角的尺。
勾股證明是初中數學的知識,理解很容易。證明也很簡單,據說愛因斯坦11歲就發現了一種證明方法。勾股定理的證明方法據統計有超過400種, 感興趣的同學可以自行了解。另勾股定理也是費馬大定理的靈感來源,費馬大定理困擾了世間智者300多年,也誕生了很多的逸聞趣事,這裡不贅述。
2、餘弦定理
勾股定理存在著一個很大的限制,就是要求三角形必須是直角三角形。那麼對於普通的三角形,三個邊存在什麼樣的關係呢?這就引出了餘弦定理。
餘弦定理指出了任意三角形三邊的關係,也是初中就可以理解的數學知識,證明也比較簡單,這裡就略過了。
其實對於三角形,理解了勾股定理和餘弦定理。就已經掌握了三角形的很多特性和秘密了。比如根據等邊三角形,可以推匯出cos(60)=1/2。但是如果想理解幾何更多的秘密,就需要進入解析幾何的世界。這個數學知識也不算很高深,高中數學知識。
這裡我們理解最簡單就可以了,那就是三角形在直角座標系中的表示。所謂“橫看成嶺側成峰,遠近高低各不同”,我們可以理解為三角形的另一種表現形式。
比如我們可以用a,b,c三個邊描述一個三角形;在平面直角座標系中,我們可以用兩個向量表示一個三角形。
3、餘弦相似度
當我們引入了直角座標系後,三角形的表示就進入了更靈活、更強大和更抽象的境界了。幾何圖形可以用代數的方法來計算,代數可以用幾何圖形形象化表示,大大降低理解難度。比如我們用一個向量來表示三角形的一個邊,就可以從二維空間直接擴充套件到高維空間。
這裡,向量的定義跟點是一樣的;向量的乘法也只是各個維度值相乘累加;向量的長度看似是新的東西,其實繞了一個圈,本質上還是勾股定理,只是勾股定理從二維空間擴充套件到了N維空間而已。而且向量長度又是兩個相同向量乘法的特例。數學的嚴謹性在這裡體現得淋漓盡致。
結合勾股定理,餘弦定理,直角座標系,向量。我們就可以很自然地推匯出餘弦公式了,這裡唯一的理解難點就是勾股定理和餘弦定理都是用向量來表示。
得到了餘弦公式後,我們該怎麼理解餘弦公式呢?
極端情況下,兩個向量重合了,就代表兩個向量完全相似。然而這裡的完全相似,其實是指向量的方向。向量有方向和長度兩個要素,這裡只使用方向這一個要素,在實踐中就埋下了隱患。但是畢竟一個數學模型建立起來了。我們可以用這個模型解決一些實際中的問題了。
所謂數學模型,有可能並不需要高深的數學知識,對外的表現也僅僅是一個數學公式。比如餘弦定理這個數學模型,高中數學知識就足夠理解了。而且關於模型,有這樣一個很有意思的論述:“所有的數學模型都是錯的,但是有些是有用的”。這裡我們更多關注其有用的一面。
理解了向量夾角,那麼該怎麼理解向量呢?它僅僅是三角形的一條邊嗎?
人生有幾何,萬物皆向量。向量在數學上是簡單的抽象,這個抽象我們可以用太多實際的場景來使它落地。比如用向量來指代使用者標籤,用向量來指代顏色,用向量來指代搜尋引擎的邏輯...
三、業務實踐
理解了餘弦定理,理解了數學建模的方式。接下來我們就可以做一些有意思的事情了。比如前面提到的三個業務場景,我們可以看看如何用餘弦相似度來解決。當然實際問題肯定遠遠要複雜得多,但是核心的思想都是類似的。
案例1:精準營銷
假設一次運營計劃,比如我們圈定了1w的使用者,如何擴充套件到10萬人呢?
利用餘弦相似度,我們這裡其實最核心的問題就是:如何將使用者向量化?
將每個使用者視為一個向量,使用者的每個標籤值視為向量的一個維度。當然這裡實際工程中還有特徵歸一化,特徵加權等細節。我們這裡僅作為演示,不陷入到細節中。
對於人群,我們可以取人群中,所有使用者維度值的平均值,作為人群向量。這樣處理後,就可以使用餘弦公式計算使用者的相似度了。
我們透過計算大盤使用者中每個使用者跟圈定人群的相似度,取topN即可實現人群的擴量。
直接“show me the code”吧!
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 |
# -*- coding: utf-
8
-*- import
numpy as np import
numpy.linalg as linalg def cos_similarity(v1, v2):
num =
float
(np.dot(v1.T, v2)) # 若為行向量則 A.T * B
denom = linalg.norm(v1) * linalg.norm(v2)
if
denom >
0
:
cos = num / denom # 餘弦值
sim =
0.5
+
0.5
* cos # 歸一化
return
sim
return
0 if
__name__ ==
'__main__'
:
u_tag_list = [
[
"女"
,
"26"
,
"是"
,
"白領"
],
[
"女"
,
"35"
,
"是"
,
"白領"
],
[
"女"
,
"30"
,
"是"
,
"白領"
],
[
"女"
,
"22"
,
"是"
,
"白領"
],
[
"女"
,
"20"
,
"是"
,
"白領"
]
]
new_user = [
"女"
,
"20"
,
"是"
,
"白領"
]
u_tag_vector = np.array([
[
1
,
26
,
1
,
1
],
[
1
,
35
,
1
,
1
],
[
1
,
30
,
1
,
1
],
[
1
,
22
,
1
,
1
],
[
1
,
20
,
1
,
1
]
])
c1 = u_tag_vector[
0
]
c1 += u_tag_vector[
1
]
c1 += u_tag_vector[
2
]
c1 += u_tag_vector[
3
]
c1 += u_tag_vector[
4
]
c1 = c1/
5
new_user_v1 = np.array([
1
,
36
,
1
,
1
])
new_user_v2 = np.array([-
1
,
20
,
0
,
1
])
print(
"vector-u1: "
, list(map(lambda x:
'%.2f'
% x, new_user_v1.tolist()[
0
:
10
])))
print(
"vector-u2: "
, list(map(lambda x:
'%.2f'
% x, new_user_v2.tolist()[
0
:
10
])))
print(
"vector-c1: "
, list(map(lambda x:
'%.2f'
% x, c1.tolist()[
0
:
10
])))
print(
"sim<u1,c1>: "
, cos_similarity(c1, new_user_v1))
print(
"sim<u2,c1>: "
, cos_similarity(c1, new_user_v2)) |
案例2:影像分類
有兩類圖片,美食和萌寵。對於新的圖片,如何自動分類呢?
這裡我們的核心問題是:圖片如何向量化?
圖片由畫素構成,每個畫素有RGB三個通道。由於畫素粒度太細,將圖片分割成大小相對的格子,每個格子定義3個維度,維度值取格子內畫素均值。
參考部落格: 影像基礎7 影像分類——餘弦相似度
下面也是給出樣例程式碼:
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 |
# -*- coding: utf-
8
-*- import
numpy as np import
numpy.linalg as linalg import
cv2 def cos_similarity(v1, v2):
num =
float
(np.dot(v1.T, v2)) # 若為行向量則 A.T * B
denom = linalg.norm(v1) * linalg.norm(v2)
if
denom >
0
:
cos = num / denom # 餘弦值
sim =
0.5
+
0.5
* cos # 歸一化
return
sim
return
0 def build_image_vector(im):
""
"
:param im:
:
return
:
""
"
im_vector = []
im2 = cv2.resize(im, (
500
,
300
))
w = im2.shape[
1
]
h = im2.shape[
0
]
h_step =
30
w_step =
50
for
i in range(
0
, w, w_step):
for
j in range(
0
, h, h_step):
each = im2[j:j+h_step, i:i+w_step]
b, g, r = each[:, :,
0
], each[:, :,
1
], each[:, :,
2
]
im_vector.append(np.mean(b))
im_vector.append(np.mean(g))
im_vector.append(np.mean(r))
return
np.array(im_vector) def show(imm):
imm2 = cv2.resize(imm, (
510
,
300
))
print(imm2.shape)
imm3 = imm2[
0
:
50
,
0
:
30
]
cv2.imshow(
"aa"
, imm3)
cv2.waitKey()
cv2.destroyAllWindows()
imm4 = imm2[
51
:
100
,
0
:
30
]
cv2.imshow(
"bb"
, imm4)
cv2.waitKey()
cv2.destroyAllWindows()
imm2.fill(
0
) def build_image_collection_vector(p_name):
path =
"D:\\python-workspace\\cos-similarity\\images\\"
c1_vector = np.zeros(
300
)
for
pic in p_name:
imm = cv2.imread(path + pic)
each_v = build_image_vector(imm)
a=list(map(lambda x:
'%.2f'
% x, each_v.tolist()[
0
:
10
]))
print(
"p1: "
, a)
c1_vector += each_v
return
c1_vector/len(p_name) if
__name__ ==
'__main__'
:
v1 = build_image_collection_vector([
"food1.jpg"
,
"food2.jpg"
,
"food3.jpg"
])
v2 = build_image_collection_vector([
"pet1.jpg"
,
"pet2.jpg"
,
"pet3.jpg"
])
im = cv2.imread(
"D:\\python-workspace\\cos-similarity\\images\\pet4.jpg"
)
v3 = build_image_vector(im)
print(
"v1,v3:"
, cos_similarity(v1,v3))
print(
"v2,v3:"
, cos_similarity(v2,v3))
a = list(map(lambda x:
'%.2f'
% x, v3.tolist()[
0
:
10
]))
print(
"p1: "
, a)
im2 = cv2.imread(
"D:\\python-workspace\\cos-similarity\\images\\food4.jpg"
)
v4 = build_image_vector(im2)
print(
"v1,v4:"
, cos_similarity(v1, v4))
print(
"v2,v4:"
, cos_similarity(v2, v4)) |
至於程式碼中用到的圖片,使用者可以自行收集即可。筆者也是直接從搜尋引擎中擷取的。程式計算的結果也是很直觀的,V2(萌寵)跟影像D1的相似度為0.956626,比V1(美食)跟影像D1的相似度0.942010更高,所以結果也是很明確的。
案例3:文字檢索
假設有三個文件,描述的內容如下。一個是疫情背景下,蘋果公司的資訊,另外兩個是水果相關的資訊。輸入搜尋詞“蘋果是我最喜歡的水果”, 該怎麼找到最相關的文件?
這裡的核心問題也是文字和搜尋詞如何向量化?
這裡其實可以把搜尋詞也視為文件,這樣問題就簡化成:文件如何向量化?
出於簡化問題的角度,我們可以給出最簡單的答案:文件由片語成,每個詞作為一個維度;文件中詞出現的頻率作為維度值。
當然,實際操作時我們維度值的計算會更復雜一些,比如用TF-IDF。這裡用詞頻(TF)並不影響演示效果,所以我們從簡。
將文字向量化後,剩下也是依樣畫葫蘆,用餘弦公式計算相似度, 流程如下:
最後,給出程式碼:
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 |
# -*- coding: utf-
8
-*- import
numpy as np import
numpy.linalg as linalg import
jieba def cos_similarity(v1, v2):
num =
float
(np.dot(v1.T, v2)) # 若為行向量則 A.T * B
denom = linalg.norm(v1) * linalg.norm(v2)
if
denom >
0
:
cos = num / denom # 餘弦值
sim =
0.5
+
0.5
* cos # 歸一化
return
sim
return
0 def build_doc_tf_vector(doc_list):
num =
0
doc_seg_list = []
word_dic = {}
for
d in doc_list:
seg_list = jieba.cut(d, cut_all=False)
seg_filterd = filter(lambda x: len(x)>
1
, seg_list)
w_list = []
for
w in seg_filterd:
w_list.append(w)
if
w not in word_dic:
word_dic[w] = num
num+=
1
doc_seg_list.append(w_list)
print(word_dic)
doc_vec = []
for
d in doc_seg_list:
vi = [
0
] * len(word_dic)
for
w in d:
vi[word_dic[w]] +=
1
doc_vec.append(np.array(vi))
print(vi[
0
:
40
])
return
doc_vec, word_dic def build_query_tf_vector(query, word_dic):
seg_list = jieba.cut(query, cut_all=False)
vi = [
0
] * len(word_dic)
for
w in seg_list:
if
w in word_dic:
vi[word_dic[w]] +=
1
return
vi if
__name__ ==
'__main__'
:
doc_list = [
""
"
受全球疫情影響,
3
月蘋果宣佈關閉除大中華區之外數百家全球門店,其龐大的供應鏈體系也受到衝擊,
儘管目前富士康等代工廠已經開足馬力恢復生產,但相比之前產能依然受限。中國是iPhone生產的大本營,
為了轉移風險,iPhone零部件能否實現印度製造?實現印度生產的最大難點就是,相對中國,印度製造業仍然欠發達
""
",
""
"
蘋果是一種低熱量的水果,每
100
克產生大約
60
千卡左右的熱量。蘋果中營養成分可溶性大,容易被人體吸收,故有“活水”之稱。
它有利於溶解硫元素,使皮膚潤滑柔嫩。
""
",
""
"
在生活當中,香蕉是一種很常見的水果,一年四季都能吃得著,因其肉質香甜軟糯,且營養價值高,所以深受老百姓的喜愛。
那麼香蕉有什麼具體的功效,你瞭解嗎?
""
"
]
query =
"蘋果是我喜歡的水果"
doc_vector, word_dic = build_doc_tf_vector(doc_list)
query_vector = build_query_tf_vector(query, word_dic)
print(query_vector[
0
:
35
])
for
i, doc in enumerate(doc_vector):
si = cos_similarity(doc, query_vector)
print(
"doc"
, i,
":"
, si) |
我們檢索排序的結果如下:
文件D2是相似度最高的,符合我們的預期。這裡我們用最簡單的方法,實現了一個搜尋打分排序的樣例,雖然它並沒有實用價值,但是演示出了搜尋引擎的工作原理。
四、超越餘弦
前面透過簡單的3個案例,演示了餘弦定理的用法,但是沒有完全釋放出餘弦定理的洪荒之力。接下來展示一下工業級的系統中是如何使用餘弦定理的。這裡選取了開源搜尋引擎資料庫ES的核心Lucene作為研究物件。研究的問題是:Lucene是如何使用餘弦相似度進行文件相似度打分?
當然,對於Lucene的實現,它有另一個名字:向量空間模型。即許多向量化的文件集合形成了向量空間。我們首先直接看公式:
很明顯,實際公式跟理論公式長相差異很大。那麼我們怎麼理解呢?換言之,我們怎麼基於理論公式推匯出實際公式呢?
首先需要注意的是,在Lucene中,文件向量的特徵不再是我們案例3中展示的,用的詞頻,而是TF-IDF。關於TF-IDF相關的知識,比較簡單,主要的思路在於:
如何量化一個詞在文件中的關鍵程度? TF-IDF給出的答案是綜合考慮詞頻(詞在當前文件中出現的次數)以及逆文件頻率(詞出現的文件個數)兩個因素。
- 詞在當前文件中出現次數(TF)越多, 詞越重要
- 詞在其他文件出現的次數(IDF)越少,詞越獨特
感興趣的話,可以自行參考其他資料,這裡不展開說明。
回到我們的核心問題: 我們怎麼基於理論公式推匯出實際公式呢?
四步走就可以了,如下圖:
第一步:計算向量乘法
向量乘法就是套用數學公式了。這裡需要注意的是,這裡有兩個簡化的思想:
- 查詢語句中不存在的詞tf(t,q)=0
- 查詢語句基本沒有重複的詞tf(t,q)=1
所以我們比較簡單完成了第一步推導:
第二步: 計算查詢語句向量長度|V(q)|
計算向量長度,其實就是勾股定理的使用了。只不過這裡是多維空間的勾股定理。
這裡取名queryNorm, 表示這個操作是對向量的歸一化。這個其實是當向量乘以queryNorm後,就變成了單位向量。單位向量的長度為1,所以稱為歸一化,也就是取名norm。理解了這一層,看lucene原始碼的時候,就比較容易理解了。這正如琅琊榜的臺詞一樣:問題出自朝堂,答案卻在江湖。這裡是問題出自Lucene原始碼,答案卻在數學。
第三步:計算文件向量長度|V(d)|
這裡其實是不能沿用第二步的做法的。前面已經提到,向量有兩大要素:方向和長度。餘弦公式只考慮了方向因素。這樣在實際應用中,餘弦相似度就是向量長度無關的了。
這在搜尋引擎中,如果查詢語句命中了長文件和短文件,按照餘弦公式TF-IDF特徵,偏向於對短小的文件打較高的分數。對長文件不公平,所以需要最佳化一下。
這裡的最佳化思路就是採用文件詞個數累積,從而降低長文件和短文件之間的差距。當然這裡的業務訴求可能比較多樣,所以在原始碼實現的時候,開放了介面允許使用者自定義。藉以提升靈活度。
第四步:混合使用者權重和打分因子
所謂使用者權重,就是指使用者指定查詢詞的權重。例如典型地競價排名就是人為提升某些查詢詞的權重。所謂打分因子,即如果一個文件中相比其它的文件出現了更多的查詢關鍵詞,那麼其值越大。綜合考慮了多詞查詢的場景。經過4步,我們再看推匯出來的公式和實際公式,發現相似度非常高。
推導公式和官方公式基本就一致了。
五、總結
本文簡單介紹了餘弦相似度的數學背景。從埃及金字塔的建設問題出發,引出了勾股定理,進而引出了餘弦定理。並基於向量推匯出來了餘弦公式。
接下來透過三個業務場景的例子,介紹餘弦公式的應用,即數學模型如何落地到業務場景中。這三個簡單的例子程式碼不過百行,能夠幫助讀者更好地理解餘弦相似度。
最後介紹了一個工業級的樣例。基於Lucene構建的ES是當前最火熱的搜尋引擎解決方案。學習餘弦公式在Lucene中落地,有助於理解業界的真實玩法。進一步提升對餘弦公式的理解。
六、參考文獻
-
書籍《數學之美》 作者:吳軍
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69912579/viewspace-2731806/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 文字相似度計算之餘弦定理
- 推薦系統01–餘弦相似度
- 程式設計師的數學筆記2--餘數程式設計師筆記
- 餘弦相似度可能沒用?對於某些線性模型,相似度甚至不唯一模型
- java程式設計師程式設計筆試基礎學習Java程式設計師筆試
- 程式設計師的數學基礎課-黃申-極客時間程式設計師
- 程式設計師自我發展之路:從態度到方法程式設計師
- 從2012到2021,從土木到程式設計師程式設計師
- Spark/Scala實現推薦系統中的相似度演算法(歐幾里得距離、皮爾遜相關係數、餘弦相似度:附實現程式碼)Spark演算法
- 程式設計師的數學程式設計師
- 使用AVX2指令集加速推薦系統MMR層餘弦相似度計算
- 好程式設計師大資料影片教程從零基礎入門到精通程式設計師大資料
- 【工程應用十】基於十六角度量化的夾角餘弦相似度模版匹配演算法原理解析。演算法
- Python 指令碼高階程式設計:從基礎到實踐Python指令碼程式設計
- 百戰程式設計師Java基礎教學視訊程式設計師Java
- 從前端到“錢”端,前端程式設計師的出路前端程式設計師
- python程式設計從基礎到實踐第四章Python程式設計
- 從程式設計師到架構師,有捷徑嗎?程式設計師架構
- 《程式設計師的數學》思考題(一)程式設計師
- 幽默:js程式設計師的數學不好?JS程式設計師
- 餘弦距離
- 零基礎想學IT程式設計從哪入手效果好?程式設計
- 我從程式設計面試中學到的程式設計面試
- 如何從零基礎自學到獲得第一份程式設計工作?程式設計
- 《程式設計師的修煉之道:從小工到專家》程式設計師
- 好程式設計師Java培訓分享20個Java程式設計師基礎題程式設計師Java
- 從百度的PPT文化看程式設計師晉升程式設計師
- 軟體設計師:程式設計語言基礎知識程式設計
- 程式設計好學嗎?零基礎能學程式設計嗎?程式設計
- 相愛相殺:程式設計師的數學程式設計師
- 從程式設計到養生程式設計程式設計
- 從前端到“錢”端,前端程式設計師的出路在何方前端程式設計師
- 程式設計師:學校教的數學知識,程式設計根本用不到!程式設計師
- 《Python程式設計:從入門到實踐》 筆記(一)基礎知識Python程式設計筆記
- C++程式設計基礎(2)變數C++程式設計變數
- 從Java程式設計師到架構師,從工程師到技術專家,迷茫之路如何點亮Java程式設計師架構工程師
- UI設計師需要掌握的平面設計基礎!UI
- 從程式設計師到解決方案架構師的簡單指南 - Dev程式設計師架構dev