做者:Philipp Muens算法
翻譯:老齊bash
與本文相關的圖書推薦:《數據準備和特徵工程》(電子工業出版社天貓旗艦店有售)微信
本文的代碼,均發佈到百度AI Studio的在線平臺中,關注微信公衆號「老齊教室」,並回復:#真實姓名+手機號+‘案例’#
,申請加入含有苯問案例的《機器學習案例》課程,獲得包含本案例在內的更多機器學習案例。注意: 回覆信息中(1)必須以#
開始和結尾(2)必須是真實姓名和手機號。markdown
K近鄰(簡稱K-NN或KNN)是一種簡單而優雅的機器學習算法,用於根據現有數據對不可見的數據進行分類。該算法的優勢是不須要傳統的訓練階段。若是存在分類問題和標記數據,則能夠利用現有的已分類數據,預測任何不可見的數據類別。app
讓咱們仔細看看核心思想背後相關的數學知識和將這些轉化爲代碼的過程。機器學習
想象一下,咱們邀請了100個養狗的人帶着他們的狗過來作一個咱們想作的統計實驗。每隻參與實驗的狗是咱們感興趣的4個不一樣犬種中的1個。在這些狗及其主人的配合下,咱們測量每隻狗的3種不一樣屬性:函數
測量完成後,咱們將測量值標準化,使其在000到111之間。oop
在收集了每隻狗的數據後,咱們獲得了100個測量值,每一個測量值都標有相應的狗品種。學習
下面是一個例子:測試
爲了更好地理解數據,最好把它標出來。因爲咱們收集了3種不一樣的測量數據(重量、高度和警戒性),所以能夠將全部100個數據點投影到三維空間中,並根據其標籤爲每一個數據點上色(例如,把「Podenco」的標籤塗上棕色)。
不幸的是,咱們在試圖繪製此數據時遇到問題,由於咱們忘了標註其中的一個測量數據。咱們確實有狗的重量,高度和警覺性,但因爲某種緣由,咱們忘記寫下這隻狗的品種。
既然咱們已經有其餘狗的測量數據,有沒有可能推測出這隻狗的品種呢?咱們仍然能夠將未標記的數據添加到現有三維空間中,全部其餘的彩色數據點都在這個空間裏。但咱們該怎麼給這個推測的數據點上色呢?
一個可能的解決方案是查看問題數據點周圍的5個鄰居,看看它們是什麼顏色的。若是這些數據點中的大多數標記爲「Podenco」,那麼咱們的測量數據極可能也是從Podenco中獲取的。
這正是K-NN算法(k近鄰算法)的做用。該算法根據一個不可見數據點的K近鄰和這些K近鄰的絕大多數類型,來預測該數據點的類。讓咱們從數學的角度來仔細研究一下這個問題。
爲了經過K-NN對數據進行分類,咱們只須要實現兩個概念。
如上所述,該算法經過查看K個最近鄰和它們各自的大多數類來對數據進行分類。
所以咱們須要實現兩個函數:距離函數和投票函數。前者用於計算兩點之間距離的,後者返回給定的任意標籤列表中最多見的標籤。
考慮到「最近鄰」的概念,咱們須要計算「待分類」數據點與全部其餘數據點之間的距離,以找到距離最近的點。
有幾個不一樣的距離函數。對於咱們的實現,將使用歐幾里德距離,由於它計算簡單,能夠很容易地擴展到多維。
用數學符號表示以下:
讓咱們經過一個例子來解釋這個公式。假設有兩個向量和,二者之間的歐氏距離計算以下:
將其轉化爲代碼的結果以下:
def distance(x: List[float], y: List[float]) -> float: assert len(x) == len(y) interim_res: float = 0 for i, _ in enumerate(x): interim_res += (x[i] - y[i]) ** 2 return sqrt(interim_res) assert distance([1, 2, 3, 4], [5, 6, 7, 8]) == 8 複製代碼
太好了。咱們剛剛實現了第一個構建:一個歐氏距離函數。
接下來咱們須要實現投票函數。投票函數接受一個標籤列表做爲輸入,並返回該列表的「最多見」標籤。雖然這聽起來很容易實現,但咱們應該後退一步,考慮可能遇到的潛在的極端狀況。
其中一種狀況是,咱們有兩個或多個「最多見」標籤:
# Do we return `a` or `b`? labels: List[str] = ['a', 'a', 'b', 'b', 'c'] 複製代碼
對於這些場景,咱們須要實現一個決策機制。
有幾種方法能夠解決這個問題。一種解決辦法多是隨機挑選一個標籤。然而,在咱們的例子中,咱們不該該孤立地考慮投票函數,由於咱們知道:距離函數和投票函數共同來肯定對未分類數據的預測。
咱們能夠利用這一事實。假設咱們的投票函數輸入了一個標籤列表,這個列表是按距離從近到遠排序的。有了這一條件,就很容易打破平局。咱們須要作的就是遞歸地刪除列表中的最後一個條目(也就是最遠的條目),直到只有一個標籤明顯勝出。
下面根據以上的標籤示例演示此過程:
# Do we return `a` or `b`? labels: List[str] = ['a', 'a', 'b', 'b', 'c'] # Remove one entry. We're still unsure if we should return `a` or `b` labels: List[str] = ['a', 'a', 'b', 'b'] # Remove another entry. Now it's clear that `a` is the "winner" labels: List[str] = ['a', 'a', 'b'] 複製代碼
咱們把這個算法轉換成一個函數,而且稱之爲majority_vote
:
def majority_vote(labels: List[str]) -> str: counted: Counter = Counter(labels) winner: List[str] = [] max_num: int = 0 most_common: List[Tuple[str, int]] for most_common in counted.most_common(): label: str = most_common[0] num: int = most_common[1] if num < max_num: break max_num = num winner.append(label) if len(winner) > 1: return majority_vote(labels[:-1]) return winner[0] assert majority_vote(['a', 'b', 'b', 'c']) == 'b' assert majority_vote(['a', 'b', 'b', 'a']) == 'b' assert majority_vote(['a', 'a', 'b', 'b', 'c']) == 'a' 複製代碼
測試代表,咱們的majority_vote
函數可以可靠地處理上述極端狀況(邊緣狀況)。
既然咱們已經研究並編寫了兩個函數,如今是時候把它們結合起來了。咱們即將實現的knn函數會輸入帶標籤的數據列表、一個新的度量值(咱們要分類的數據點)和一個參數k。參數k決定了:在經過majority_vote
函數投票給新標籤時,咱們要考慮多少個鄰居。
knn算法的首要任務是計算新數據點和全部其餘現有數據點之間的距離。以後,咱們須要從最近到最遠的距離排序,並提取數據點標籤。而後截斷此有序列表,使其僅包含k個最近的數據點標籤。最後一步是將此列表傳遞給投票函數,該函數用於計算預測標籤。
將所述步驟轉換爲代碼,將產生如下knn函數:
def knn(labeled_data: List[LabeledData], new_measurement, k: int = 5) -> Prediction: class Distance(NamedTuple): label: str distance: float distances: List[Distance] = [Distance(data.label, distance(new_measurement, data.measurements)) for data in labeled_data] distances = sorted(distances, key=attrgetter('distance')) labels = [distance.label for distance in distances][:k] label: str = majority_vote(labels) return Prediction(label, new_measurement) 複製代碼
就是這樣。這就是從頭開始實現的k近鄰算法!
如今是時候看看咱們的自制k-NN實現效果是否像宣傳的那樣了。爲了測試咱們編寫的代碼,咱們將使用臭名昭著的鳶尾花數據集。
該數據集由三種不一樣的鳶尾花的50個樣本組成:
對每個樣品,收集了4種不一樣的測量數據:萼片的寬度和長度以及花瓣的寬度和長度。
下面是數據集中的一個示例行,其中前4個數字是萼片長度、萼片寬度、花瓣長度、花瓣寬度,最後一個字符串表示這些測量數據的標籤。
6.9,3.1,5.1,2.3,Iris-virginica
複製代碼
探索這些數據的最好方法是可視化。不幸的是,很難繪製和檢查四維數據。然而,咱們能夠選擇兩個特徵(如花瓣長度和花瓣寬度)並繪製二維散點圖。
fig = px.scatter(x=xs, y=ys, color=text, hover_name=text, labels={'x': 'Petal Length', 'y': 'Petal Width'}) fig.show() 複製代碼
咱們能夠清楚地看到數據點的分類狀況,每一個類別的數據點有着相同的顏色,所以具備相同的標籤。
如今假設咱們有一個新的、未標記的數據點:
new_measurement: List[float] = [7, 3, 4.8, 1.5] 複製代碼
將這個數據點添加到現有的散點圖,結果以下:
fig = px.scatter(x=xs, y=ys, color=text, hover_name=text, labels={'x': 'Petal Length', 'y': 'Petal Width'}) fig.add_annotation( go.layout.Annotation( x=new_measurement[petal_length_idx], y=new_measurement[petal_width_idx], text="The measurement we want to classify") ) fig.update_annotations(dict( xref="x", yref="y", showarrow=True, arrowhead=7, ax=0, ay=-40, borderwidth=2, borderpad=4, bgcolor="#c3c3c3" )) fig.show() 複製代碼
即便咱們只是在二維中繪製花瓣的長度和寬度,新的測量值彷佛也可能來自「變色鳶尾」。
讓咱們用knn函數獲得一個明確的答案:
knn(labeled_data, new_measurement, 5)
複製代碼
果真,咱們獲得的結果代表,咱們正在處理一個「變色鳶尾」:
Prediction(label='Iris-versicolor', measurements=[7, 3, 4.8, 1.5]) 複製代碼
k近鄰分類算法是一種很是強大的分類算法,它能夠根據已有標籤的數據來標記缺失標籤的數據。k-NNs的主要思想是:利用新的「待分類」數據點的K個最近鄰來「投票」選出它應有的標籤。
所以,咱們須要兩個核心函數來實現k-NN。第一個函數計算兩個數據點之間的距離,以便找到最近的鄰居。第二個函數執行多數投票,以即可以決定哪一個標籤在給定的鄰域中最多見。
同時使用這兩個函數可使k-NN發揮積極做用,而且能夠可靠地標記未顯示的數據點。
我但願這篇文章是有幫助的,它揭開了k近鄰算法的內部工做原理的神祕面紗。
原文連接:philippmuens.com/k-nearest-n…
搜索技術問答的公衆號:老齊教室
在公衆號中回覆:老齊,可查看全部文章、書籍、課程。
以爲好看,就點贊轉發