KNN演算法推理與實現

Cylon 發表於 2022-06-04
演算法

Overview

K近鄰值演算法 KNN (K — Nearest Neighbors) 是一種機器學習中的分類演算法;K-NN是一種非引數惰性學習演算法。非引數意味著沒有對基礎資料分佈的假設,即模型結構是從資料集確定的。

它被稱為惰性演算法的原因是,因為它不需要任何訓練資料點來生成模型。所有訓練資料都用於測試階段,這使得訓練更快,測試階段更慢且成本更高。

如何工作

KNN 演算法是通過計算新物件與訓練資料集中所有物件之間的距離,對新例項進行分類或迴歸預測。然後選擇訓練資料集中距離最小的 K 個示例,並通過平均結果進行預測。

如圖所示:一個未分類的資料(紅色)和所有其他已分類的資料(黃色和紫色),每個資料都屬於一個類別。因此,計算未分類資料與所有其他資料的距離,以瞭解哪些距離最小,因此當K= 3 (或K= 6 )最接近的資料並檢查出現最多的類,如下圖所示,與新資料最接近的資料是在第一個圓圈內(圓圈內)的資料,在這個圓圈內還有 3 個其他資料(已經用黃色分類),我們將檢查其中的主要類別,會被歸類為紫色,因為有2個紫色球,1個黃色球。

img

KNN演算法要執行的步驟

  • 將資料分為訓練資料和測試資料
  • 選擇一個值 K
  • 確定要使用的距離演算法
  • 從需要分類的測試資料中選擇一個樣本,計算到它的 n 個訓練樣本的距離。
  • 對獲得的距離進行排序並取 k最近的資料樣本。
  • 根據 k 個鄰居的多數票將測試類分配給該類。

影響KNN演算法效能的因素

  • 用於確定最近鄰居的距離的演算法

  • 用於從 K 近鄰派生分類的決策規則

  • 用於對新示例進行分類的鄰居

如何計算距離

測量距離是KNN演算法的核心,總結了問題域中兩個物件之間的相對差異。比較常見的是,這兩個物件是描述主題(例如人、汽車或房屋)或事件(例如購買、索賠或診斷)的資料行。

漢明距離

漢明距離(Hamming Distance)計算兩個二進位制向量之間的距離,也簡稱為二進位制串 binary strings 或位串 bitstrings ;換句話說,漢明距離是將一個字串更改為另一個字串所需的最小替換次數,或將一個字串轉換為另一個字串的最小錯誤數。

示例:如一列具有類別 “紅色”、“綠色” 和 “藍色”,您可以將每個示例獨熱編碼為一個位串,每列一個位。

注:獨熱編碼 one-hot encoding:將分類資料,轉換成二進位制向量表示,這個二進位制向量用來表示一種特殊的bit(二進位制位)組合,該位元組裡,僅容許單一bit為1,其他bit都必須為0

如:

apple banana pineapple
1 0 0
0 1 0
0 0 1

100 表示蘋果,100就是蘋果的二進位制向量
010 表示香蕉,010就是香蕉的二進位制向量

red = [1, 0, 0]
green = [0, 1, 0]
blue = [0, 0, 1]

而red和green之間的距離就是兩個等長bitstrings之間bit差(對應符號不同的位置)的總和或平均數,這就是漢明距離

  • \(Hamming Distance d(a, b)\ =\ sum(xi\ !=\ yi\ for\ xi,\ yi\ in\ zip(x, y))\)

上述的實現為:

def hammingDistance(a, b):
    if len(a) != len(b):
        raise ValueError("Undefined for sequences of unequal length.")
    return sum(abs(e1 - e2) for e1, e2 in zip(a, b))

row1 = [0, 0, 0, 0, 0, 1]
row2 = [0, 0, 0, 0, 1, 0]

dist = hammingDistance(row1, row2)
print(dist)

可以看到字串之間有兩個差異,或者 6 個位位置中有 2 個不同,平均 (2/6) 約為 1/3 或 0.333。


from scipy.spatial.distance import hamming
# define data
row1 = [0, 0, 0, 0, 0, 1]
row2 = [0, 0, 0, 0, 1, 0]

# calculate distance
dist = hamming(row1, row2)
print(dist)

歐幾里得距離

歐幾里得距離(Euclidean distance) 是計算兩個點之間的距離。在計算具體的數值(例如浮點數或整數)的兩行資料之間的距離時,您最有可能使用歐幾里得距離。

歐幾里得距離計算公式為兩個向量之間的平方差之和的平方根。

\(EuclideanDistance=\sqrt[]{\sum(a-b)^2}\)

如果要執行數千或數百萬次距離計算,通常會去除平方根運算以加快計算速度。修改後的結果分數將具有相同的相對比例,並且仍然可以在機器學習演算法中有效地用於查詢最相似的示例。

\(EuclideanDistance = sum\ for\ i\ to\ N\ (v1[i]\ –\ v2[i])^2\)

# calculating euclidean distance between vectors
from math import sqrt
from scipy.spatial.distance import euclidean

# calculate euclidean distance
def euclidean_distance(a, b):
	return sqrt(sum((e1-e2)**2 for e1, e2 in zip(a,b)))
 
# define data
row1 = [10, 20, 15, 10, 5]
row2 = [12, 24, 18, 8, 7]
# calculate distance
dist = euclidean_distance(row1, row2)
print(dist)
print(euclidean(row1, row2))

曼哈頓距離

曼哈頓距離( Manhattan distance )又被稱作計程車幾何學 Taxicab geometry;用於計算兩個向量之間的距離。

對於描述網格上的物件(如棋盤或城市街區)的向量可能更有用。計程車在城市街區之間採取的最短路徑(網格上的座標)。

粗略地說,歐幾里得幾何是中學常用的平面幾何和立體幾何 Plane geometry

曼哈頓距離可以理解為:歐幾里得空間的固定直角座標系上兩點所形成的線段對軸產生的投影的距離總和。

image

圖中: 紅、藍與黃線分別表示所有曼哈頓距離都擁有一樣長度(12),綠線表示歐幾里得距離 \(6×\sqrt2 ≈ 8.48\)

對於整數特徵空間中的兩個向量,應該計算曼哈頓距離而不是歐幾里得距離

曼哈頓距離在二維平面的計算公式是,在X軸的亮點

\(Manhattandistance\ d(x,y)=\left|x_{1}-x_{2}\right|+\left|y_{1}-y_{2}\right|\)

image

如果所示,描述格子和格子之間的距離可以用曼哈頓距離,如國王移動到右下角的距離是?

\(King=|6-8|+|6-1| = 7\)

兩個向量間的距離可以表示為 \(MD\ =\ Σ|Ai – Bi|\)

python中的公式可以表示為 :sum(abs(val1-val2) for val1, val2 in zip(a,b))

from scipy.spatial.distance import cityblock
# calculate manhattan distance
def manhattan_distance(a, b):
	return sum(abs(e1-e2) for e1, e2 in zip(a,b))
 
# define data
row1 = [10, 20, 15, 10, 5]
row2 = [12, 24, 18, 8, 7]
# calculate distance
dist = manhattan_distance(row1, row2)
print(dist)
print(cityblock(row1, row2))

閔可夫斯基距離

閔可夫斯基距離(Minkowski distance)並不是一種距離而是對是歐幾里得距離曼哈頓距離的概括,用來計算兩個向量之間的距離。

閔可夫斯基增並新增了一個引數,稱為“階數”或 p\(d(x,y) = (\sum(|x-y|)^p)^\frac{1}{p}\)

在python中的公式:

(sum for i to N (abs(v1[i] – v2[i]))^p)^(1/p)

p 是一個有序的引數,當 \(p=1\) 時,計算的是曼哈頓距離。當 \(p=2\) 時,計算的是歐幾里得距離。

在實現使用距離度量的機器學習演算法時,通常會使用閔可夫斯基距離,因為可以通過調整引數“ p ”控制用於向量的距離度量演算法的型別。

# calculating minkowski distance between vectors
from scipy.spatial import minkowski_distance
 
# calculate minkowski distance
def minkowski_distance(a, b, p):
	return sum(abs(e1-e2)**p for e1, e2 in zip(a,b))**(1/p)
 
# define data
row1 = [10, 20, 15, 10, 5]
row2 = [12, 24, 18, 8, 7]

# 手動實現的演算法用來使用閔可夫斯基計算距離
dist = minkowski_distance(row1, row2, 1)
# 1為曼哈頓
print(dist)
# 1為歐幾里得
dist = minkowski_distance(row1, row2, 2)
print(dist)

# 使用包 scipy.spatial來計算
print(minkowski_distance(row1, row2, 1))
print(minkowski_distance(row1, row2, 2))

KNN演算法實現

Prerequisite

首先會用示例來實現KNN演算法的每個步驟,並加以分析,然後將所有步驟關聯在在一起,形成一個適用於真實資料集的實現。

KNN在實現起來主要有三個步驟:

  • 計算距離(這裡選擇歐幾里得距離)
  • 獲得臨近鄰居
  • 做出預測

這三個步驟是KNN演算法用以解決分類和迴歸預測建模問題的基礎知識

計算距離

第一步計算資料集中兩行之間的距離。在資料集中的資料行主要由數字組成,計算兩行或數字向量之間的距離的一種簡單方法是畫一條直線。這在 2D 或 3D 平面中都是很好地選擇,並且可以很好地擴充套件到更高的維度。

這裡使用的是比較流行的計算距離的演算法,歐幾里得距離來計算兩個向量之間的直線距離。歐幾里得距離的公式是,兩個向量的平方差的平方根,\(Euclidean\ Distance=\sqrt[]{\sum(a-b)^2}\) ;在python中可以表示為:sqrt(sum i to N (x1 – x2)^2) ;其中 x1 是第一行資料,x2 是第二行資料,i 表示特定列的索引,因為可能需要對所有行進行計算。

在歐幾里得距離中,值越小,兩條記錄就越相似; 0 表示兩條記錄之間沒有差異。

那麼使用python實現一個計算歐幾里得距離的演算法

def euclidean_distance(row1, row2):
	distance = 0.0
	for i in range(len(row1)-1):
		distance += (row1[i] - row2[i])**2
	return sqrt(distance)

準備一部分測試資料,來對測試距離演算法

X1				X2					Y
2.7810836		2.550537003			0
1.465489372		2.362125076			0
3.396561688		4.400293529			0
1.38807019		1.850220317			0
3.06407232		3.005305973			0
7.627531214		2.759262235			1
5.332441248		2.088626775			1
6.922596716		1.77106367			1
8.675418651		-0.242068655		1
7.673756466		3.508563011			1

那麼來測試這些資料,需要做到的是第一行與所有行之間的距離,對於第一行與自己的距離應該為0

from math import sqrt
 
# 歐幾里得距離,計算兩個向量間距離的演算法
def euclidean_distance(row1, row2):
	distance = 0.0
	for i in range(len(row1)-1):
		distance += (row1[i] - row2[i])**2 # 平方差
	return sqrt(distance) # 平方根
 
# 測試資料集
dataset = [
    [2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]
]
row0 = dataset[0]
for row in dataset:
	distance = euclidean_distance(row0, row)
	print(distance)
    
# 0.0
# 1.3290173915275787
# 1.9494646655653247
# 1.5591439385540549
# 0.5356280721938492
# 4.850940186986411
# 2.592833759950511
# 4.214227042632867
# 6.522409988228337
# 4.985585382449795

獲取最近鄰居

資料集中新資料的鄰居是k個最接近的例項(行),這個例項由距離定義。現在誕生的問題:如何找到最近的鄰居?以及怎麼找到最近的鄰居?

  • 為了在資料集中找到 K 的鄰居,首先必須計算資料集中每條記錄與新資料之間的距離。

  • 有了距離之後,必須按照 K 的距離對訓練集中的所有例項排序。然後選擇前 k 個作為最近的鄰居。

這裡實現起來是通過將資料集中每條記錄的距離作為一個元組來跟蹤,通過對元組列表進行排序(距離降序),然後檢索最近鄰居。下面是一個實現這些步驟的函式

# 找到最近的鄰居
def get_neighbors(train, test_row, num_neighbors):
    """
    計算訓練集train中所有元素到test_row的距離
    :param train: list, 資料集,可以是訓練集
    :param test_row: list, 新的例項,也就是K
    :param num_neighbors:int,需要多少個鄰居
    :return: None
    """
    distances = list()
    for train_row in train:
        # 計算出每一行的距離,把他新增到元組中
        dist = euclidean_distance(test_row, train_row)
        distances.append((train_row, dist))
    distances.sort(key=lambda knn: knn[1]) # 根據元素哪個欄位進行排序
    neighbors = list()
    for i in range(num_neighbors):
        neighbors.append(distances[i][0])
    return neighbors

下面是完整的示例

from math import sqrt
 
# 歐幾里得距離,計算兩個向量間距離的演算法
def euclidean_distance(row1, row2):
	distance = 0.0
	for i in range(len(row1)-1):
		distance += (row1[i] - row2[i])**2 # 平方差
	return sqrt(distance) # 平方根
 
# 找到最近的鄰居
def get_neighbors(train, test_row, num_neighbors):
    """
    計算訓練集train中所有元素到test_row的距離
    :param train: list, 資料集,可以是訓練集
    :param test_row: list, 新的例項,也就是K
    :param num_neighbors:int,需要多少個鄰居
    :return: None
    """
    distances = list()
    for train_row in train:
        # 計算出每一行的距離,把他新增到元組中
        dist = euclidean_distance(test_row, train_row)
        distances.append((train_row, dist))
    distances.sort(key=lambda knn: knn[1]) # 根據元素哪個欄位進行排序
    neighbors = list()
    for i in range(num_neighbors):
        neighbors.append(distances[i][0])
    return neighbors

# 測試資料集
dataset = [
    [2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]
]

neighbors = get_neighbors(dataset, dataset[0], 3)
for neighbor in neighbors:
	print(neighbor)

# [2.7810836, 2.550537003, 0]
# [3.06407232, 3.005305973, 0]
# [1.465489372, 2.362125076, 0]

可以看到,執行後會將資料集中最相似的 3 條記錄按相似度順序列印。和預測的一樣,第一個記錄與其本身最相似,並且位於列表的頂部。

預測結果

預測結果在這裡指定是,通過分類拿到了最近的鄰居的例項,對鄰居進行分類,找到鄰居中最大類別的一類,作為預測值。這裡使用的是對鄰居值執行 max() 來實現這一點,下面是實現方式

# 預測值
def predict_classification(train, test_row, num_neighbors):
    """
    計算訓練集train中所有元素到test_row的距離
    :param train: list, 資料集,可以是訓練集
    :param test_row: list, 新的例項,也就是K
    :param num_neighbors:int,需要多少個鄰居
    :return: None
    """
    neighbors = get_neighbors(train, test_row, num_neighbors)
    output_values = [row[-1] for row in neighbors] # 拿到所屬類的真實類別
    prediction = max(set(output_values), key=output_values.count)  #算出鄰居類別最大的數量
    return prediction

下面是完整的示例

from math import sqrt
 
# 歐幾里得距離,計算兩個向量間距離的演算法
def euclidean_distance(row1, row2):
    distance = 0.0
    for i in range(len(row1)-1):
        distance += (row1[i] - row2[i])**2 # 平方差
    return sqrt(distance) # 平方根
 
# 找到最近的鄰居
def get_neighbors(train, test_row, num_neighbors):
    """
    計算訓練集train中所有元素到test_row的距離
    :param train: list, 資料集,可以是訓練集
    :param test_row: list, 新的例項,也就是K
    :param num_neighbors:int,需要多少個鄰居
    :return: None
    """
    distances = list()
    for train_row in train:
        # 計算出每一行的距離,把他新增到元組中
        dist = euclidean_distance(test_row, train_row)
        distances.append((train_row, dist))
    distances.sort(key=lambda knn: knn[1]) # 根據元素哪個欄位進行排序
    neighbors = list()
    for i in range(num_neighbors):
        neighbors.append(distances[i][0])
    return neighbors

# 預測值
def predict_classification(train, test_row, num_neighbors):
    """
    計算訓練集train中所有元素到test_row的距離
    :param train: list, 資料集,可以是訓練集
    :param test_row: list, 新的例項,也就是K
    :param num_neighbors:int,需要多少個鄰居
    :return: None
    """
    neighbors = get_neighbors(train, test_row, num_neighbors)
    output_values = [row[-1] for row in neighbors] # 拿到所屬類的真實類別
    prediction = max(set(output_values), key=output_values.count)  #算出鄰居類別最大的數量
    return prediction

# 測試資料集
dataset = [
    [2.7810836,2.550537003,0],
	[1.465489372,2.362125076,0],
	[3.396561688,4.400293529,0],
	[1.38807019,1.850220317,0],
	[3.06407232,3.005305973,0],
	[7.627531214,2.759262235,1],
	[5.332441248,2.088626775,1],
	[6.922596716,1.77106367,1],
	[8.675418651,-0.242068655,1],
	[7.673756466,3.508563011,1]
]

for n in range(len(dataset)):
    prediction = predict_classification(dataset, dataset[n], 5)
    print('Expected %d, Got %d.' % (dataset[n][-1], prediction))

# Expected 0, Got 0.
# Expected 0, Got 0.
# Expected 0, Got 0.
# Expected 0, Got 0.
# Expected 0, Got 0.
# Expected 1, Got 1.
# Expected 1, Got 1.
# Expected 1, Got 1.
# Expected 1, Got 1.
# Expected 1, Got 1.

執行結果列印了預期分類與從資料集中 3 個相進鄰居預測結果是一直的。

鳶尾花種例項

這裡使用的是 Iris Flower Species 資料集。

鳶尾花資料集是根據鳶尾花的測量值預測花卉種類。這是一個多類分類問題。每個類的觀察數量是平衡的。有 150 個觀測值,有 4 個輸入變數和 1 個輸出變數。變數名稱如下:

  • 萼片長度以釐米為單位。
  • 萼片寬度以釐米為單位。
  • 花瓣長度以釐米為單位。
  • 花瓣寬度以釐米為單位。
  • 真實型別

更多的關於資料集的說明可以參考:Iris-databases資料集的說明

Prerequisite

實驗的步驟大概分為如下:

  • 載入資料集並將資料轉換為可用於均值和標準差計算的數字。將屬性轉為float,將類別轉換為int。
  • 使 5折的K折較差驗證(K-Fold CV)評估該演算法。

Start

from random import seed
from random import randrange
from csv import reader
from math import sqrt

# 載入CSV
def load_csv(filename):
    dataset = list()
    with open(filename, 'r') as file:
        csv_reader = reader(file)
        for row in csv_reader:
            if not row:
                continue
            dataset.append(row)
    return dataset

# 轉換所有的值為float方便運算
def str_column_to_float(dataset, column):
    for row in dataset:
        row[column] = float(row[column].strip())

# 轉換所有的型別為int
def str_column_to_int(dataset, column):
    class_values = [row[column] for row in dataset]
    unique = set(class_values)
    lookup = dict()
    for i, value in enumerate(unique):
        lookup[value] = i
    for row in dataset:
        row[column] = lookup[row[column]]
    return lookup



# # k-folds CV函式進行劃分
def cross_validation_split(dataset, n_folds):
    dataset_split = list()
    dataset_copy = list(dataset)
    # 平均分成n_folds折數
    fold_size = int(len(dataset) / n_folds)
    for _ in range(n_folds):
        fold = list()
        while len(fold) < fold_size:
            index = randrange(len(dataset_copy))
            fold.append(dataset_copy.pop(index))
        dataset_split.append(fold)
    return dataset_split

# 計算精確度
def accuracy_metric(actual, predicted):
    correct = 0
    for i in range(len(actual)):
        if actual[i] == predicted[i]:
            correct += 1
    return correct / float(len(actual)) * 100.0

# 評估演算法
def evaluate_algorithm(dataset, algorithm, n_folds, *args):
    """
    評估演算法,計算演算法的精確度
    :param dataset: list, 資料集
    :param algorithm: function, 演算法名
    :param n_folds: int,折數
    :param args: 用於algorithm的引數
    :return: None
    """
    folds = cross_validation_split(dataset, n_folds) # 分成5折
    scores = list()
    for fold in folds:
        train_set = list(folds)
        train_set.remove(fold) # 訓練集不包含本身
        train_set = sum(train_set, [])
        test_set = list() # 測試集
        for row in fold:
            row_copy = list(row)
            test_set.append(row_copy)
            row_copy[-1] = None
        predicted = algorithm(train_set, test_set, *args)
        actual = [row[-1] for row in fold]
        accuracy = accuracy_metric(actual, predicted)
        scores.append(accuracy)
    return scores

# 歐幾里得距離,計算兩個向量間距離的演算法
def euclidean_distance(row1, row2):
    distance = 0.0
    for i in range(len(row1)-1):
        distance += (row1[i] - row2[i])**2
    return sqrt(distance)

# 確定最鄰近的鄰居
def get_neighbors(train, test_row, num_neighbors):
    """
    計算訓練集train中所有元素到test_row的距離
    :param train: list, 資料集,可以是訓練集
    :param test_row: list, 新的例項,也就是K
    :param num_neighbors:int,需要多少個鄰居
    :return: None
    """
    distances = list()
    for train_row in train:
        dist = euclidean_distance(test_row, train_row)
        distances.append((train_row, dist))
    distances.sort(key=lambda tup: tup[1])
    neighbors = list()
    for i in range(num_neighbors):
        neighbors.append(distances[i][0])
    return neighbors

# 與臨近值進行比較並預測
def predict_classification(train, test_row, num_neighbors):
    """
    計算訓練集train中所有元素到test_row的距離
    :param train: list, 資料集,可以是訓練集
    :param test_row: list, 新的例項,也就是K
    :param num_neighbors:int,需要多少個鄰居
    :return: None
    """
    neighbors = get_neighbors(train, test_row, num_neighbors)
    output_values = [row[-1] for row in neighbors]
    prediction = max(set(output_values), key=output_values.count)
    return prediction

# kNN Algorithm
def k_nearest_neighbors(train, test, num_neighbors):
    predictions = list()
    for row in test:
        output = predict_classification(train, row, num_neighbors)
        predictions.append(output)
    return(predictions)

# 使用KNN演算法計算鳶尾花資料集
seed(1)
filename = 'iris.csv'
dataset = load_csv(filename)
for i in range(len(dataset[0])-1):
    str_column_to_float(dataset, i)
# 轉換型別為int
str_column_to_int(dataset, len(dataset[0])-1)
# 評估演算法
n_folds = 5 # 5折
num_neighbors = 5 #取5個鄰居
scores = evaluate_algorithm(dataset, k_nearest_neighbors, n_folds, num_neighbors)
print('Scores: %s' % scores)
print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores))))

# Scores: [96.66666666666667, 96.66666666666667, 100.0, 90.0, 100.0]
# Mean Accuracy: 96.667%

上述是對整個資料集的預測百分比,也可以對對應的類的資訊進行輸出

首先在類別轉換函式 str_column_to_int 中增加列印方法

for i, value in enumerate(unique):
    lookup[value] = i
    print('[%s] => %d' % (value, i))

然後在定義一個新的例項,這個例項是用於預測的資訊 row = [5.7,2.9,4.2,1.3] ; 然後修改需要預測的資料,進行預測

# 原來的整個資料集打分不需要了
# scores = evaluate_algorithm(dataset, k_nearest_neighbors, n_folds, num_neighbors)
# print('Scores: %s' % scores)
# print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores))))

# 定義一個新資料
row = [5.7,2.9,4.2,1.3]

label = predict_classification(dataset, row, num_neighbors)
print('Data=%s, Predicted: %s' % (row, label))

# Data=[5.7, 2.9, 4.2, 1.3], Predicted: 1

通過預測,可以看出預測結果屬於第 1 類,就知道該花為 Iris-setosa

Reference

distance measures

k nearest neighbors implement

相關文章