【圖解例說機器學習】K最近鄰 (KNN)

kNN (k-nearest neighbor)的定義

針對一個測試實例,在給定訓練集中,基於某種距離度量找到與之最近的k個實例點,然後基於這k個最鄰近實例點的信息,以某種決策規則來對該測試實例進行分類或迴歸。

由定義可知,kNNkNN模型包含三個基本要素:距離度量、k值選擇以及決策規則。再詳細描述這三要素之前,我們先用一個樣圖來簡單描述kNNkNN分類模型的效果。

我們以二維平面爲例,假設輸入的訓練集格式爲(x1,x2,l)(x_1,x_2,l),其中x1,x2x_1, x_2爲橫縱座標,ll爲標籤。這裏我們考慮k=1,3k=1,3的情況,決策規則爲多數投票規則,即測試實例與k個實例中的多數屬於同一類。圖1,21,2分別是k=1,3k=1,3時,二維特徵空間劃分圖。


圖1

距離度量

kNNkNN的本質是“近朱者赤近墨者黑”,即測試點的類別由其最鄰近的kk個實例點決定。這裏“最鄰近”的意義根據距離度量的不同而不同。一般來說,我們最常見的便是歐氏距離。這裏我們介紹包含歐氏距離,但比歐氏距離更普適的Minkowski距離。

假定訓練集中,每個實例包含nn個特徵,那麼實例xx可以分別表示爲x=(x1,,xn)x=(x_1,\cdots,x_n)。假定測試實例爲y=(y1,,yn)y=(y_1,\cdots,y_n),那麼x,yx, y之間的Minkowski距離可以表示爲:

L(x,y)=(i=1nxiyip)1p, L(x,y)=\left(\sum\limits_{i=1}^{n}{\lvert x_i-y_i\rvert}^p\right)^{\frac{1}{p}},

其中,p>0p>0是一個可變參數:
L(x,y)={i=1nxiyi,p=1(曼哈頓距離)(i=1nxiyi2)12,p=2(歐氏距離)maxi=1nxiyi,p,(切比雪夫距離) L(x,y)= \begin{cases} \sum\limits_{i=1}^{n}{\lvert x_i-y_i\rvert},\quad p=1\quad\text{(曼哈頓距離)}\\ \left(\sum\limits_{i=1}^{n}{\lvert x_i-y_i\rvert}^2\right)^{\frac{1}{2}},\quad p=2\quad\text{(歐氏距離)}\\ \max\limits_{i=1}^{n}{\lvert x_i-y_i\rvert},\quad p\to\infty,\quad\text{(切比雪夫距離)} \end{cases}
當然pp也可以取小於11的值,如p=12p=\frac{1}{2}。圖33給出了當pp取不同值時,與原點距離爲11的圖形:


圖3
Note: 這裏只是介紹了較常用的Minkowski距離,

kk值的選擇

調參是機器學習算法的重要一環。在kNNkNN算法中,kk值的選取對結果的影響較大。下面以圖44來距離說明:

(a)當kk取值較小時,此時是根據測試實例周圍較少的訓練樣例信息來進行分類。由於訓練樣例離測試樣例比較近,因此訓練誤差比較小。當這些鄰近的訓練樣例是噪聲時,會嚴重影響分類結果,即泛化誤差變大。
(b)當kk取值較大時,此時是根據測試實例周圍較多的訓練樣例信息來進行分類。這時與測試實例相距較遠(相關性較小)的訓練樣例也對分類結果有影響,使得泛化誤差較大。一個極端的例子就是以考慮所有的訓練樣例,這時測試樣例被歸爲訓練樣例數最大的一類。

Note: 模型複雜度的理解:對於有參模型來說(例如線性擬合),模型複雜度一般可以用參數的多少來判斷。對於無參模型來說(例如這裏的kNNkNN),這裏還需思考。可能的情況?考慮極端情況,當kk取值爲整個訓練樣例數時,這時的模型最簡單,即測試樣例被歸爲訓練樣例數最大的一類。當kk取值爲11時,每個測試樣例都需要根據其最鄰近節點來進行分類,這時模型變得很複雜。

通常來說,我們可以通過交叉驗證來選取kk值。同時,我們也可以對這kk個訓練樣例進行距離加權,來克服(b)的影響。


決策規則

kNNkNN既可進行分類,也可用於迴歸。

  • 對於分類問題來說,一般採用的是投票法,即測試樣例被歸爲最鄰近kk個訓練樣例數中最大的一類。
  • 對於迴歸問題來說,一般採用的是平均法。顧名思義,測試樣例的取值爲這kk個訓練樣例取值的平均值。
  • 最後,我們可以對這兩種方法進行改進,即對這kk個訓練樣例進行加權,離測試樣例較近的訓練樣例一般具有更大的影響權重。

kNNkNN優缺點:

優點:

最近更新於20-01-20,明早就回家過年了,年後再更新。
未完待續。。。
最近更新於20-03-30,優缺點還需用圖示說明。


算法實踐

下面我們給出兩種方式實現KNN分類算法:一、自己編程實現KNN算法;二、使用更加簡單的scikit-learn庫。
注意:數據集爲iris數據集,有150個訓練集,4個feature, 總共分3類。在方法一中,我們考慮了所有4個feature,將所有150個訓練數據作爲訓練(即在程序中設置split=1),讀者可以通過設置split的值來獲取測試集用於交叉檢驗得到最佳的k值。在方法二中,我們只考慮了前2個feature,這麼做是爲了在二維圖中展示分類結果。

自寫KNN算法

  • 算法思路:
  1. 計算已知數據集中的點與當前點之間的距離
  2. 按照距離遞增次序進行排序
  3. 選取與當前點距離最小的K個點
  4. 確定這K個點所在類別的出現次數
  5. 返回這K個點出現次數最多的類別作爲當前點的預測分類
  • 代碼實現
from sklearn import datasets, neighbors
import random
import math
import numpy as np

# Divide the original dataset into training data and test data
def LoadDataSet(irisData, split, trainData, testData, trainLabel, testLabel):
    allData = irisData.data
    allLabel = irisData.target
    for i in range(len(allData)):
        if random.random() < split:  #
            trainData.append(allData[i])
            trainLabel.append(allLabel[i])
        else:
            testData.append(allData[i])
            testLabel.append(allLabel[i])

# Calculate the distance between two instance
def CalDist(instance1, instance2):
    dist = 0
    length = len(instance1)
    for i in range(length):
        dist += pow((instance1[i] - instance2[i]), 2)
    return math.sqrt(dist)

# The KNN algorithm
def knn(instance, k, trainData, trainLabel):
    allDist = []
    # Calculate distances from all training data
    for i in range(len(trainData)):
        allDist.append([CalDist(instance, trainData[i]), i])
    allDist.sort()
    # Determine the neighbors
    neighbors = []
    for j in range(k):
        neighbors.append(allDist[j][1])
    numLabels = len(np.unique(trainLabel))
    vote = [0] * numLabels
    # Vote to decide the resultant label
    for kk in range(k):
        vote[trainLabel[neighbors[kk]]] += 1
    # print the result
    print(vote.index(max(vote)))

# load dataset of iris
irisData = datasets.load_iris()

# All data are used for training
split = 1
# Number of neighbors
k = 3
trainData = []
trainLabel = []
testData = []
testLabel = []
LoadDataSet(irisData, split, trainData, testData, trainLabel, testLabel)

predictPoint=[7.6, 3., 6.6, 2.1]

knn(predictPoint, k, trainData, trainLabel)


使用scikit-learn庫

  • 代碼實現
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn import neighbors, datasets

# The number of neighbors
k = 3

# import dataset of iris
iris = datasets.load_iris()

# The first two-dim feature for simplicity
X = iris.data[:, :2]
# The labels
y = iris.target

h = .02  # step size in the mesh

# Create color maps for three types of labels
cmap_light = ListedColormap(['tomato', 'limegreen', 'cornflowerblue'])

# we create an instance of Neighbours Classifier and fit the data.
clf = neighbors.KNeighborsClassifier(k, 'uniform')
clf.fit(X, y)

# Plot the decision boundary. Assign a color to each point in the mesh.
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                     np.arange(y_min, y_max, h))
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])

# Z is a matrix (values) for the two-dim space
Z = Z.reshape(xx.shape)


# Plot the training points: different
def PlotTrainPoint():
    for i in range(0, len(X)):
        if y[i] == 0:
            plt.plot(X[i][0], X[i][1], 'rs', markersize=6, markerfacecolor="r")
        elif y[i] == 1:
            plt.plot(X[i][0], X[i][1], 'gs', markersize=6, markerfacecolor="g")
        else:
            plt.plot(X[i][0], X[i][1], 'bs', markersize=6, markerfacecolor="b")


# Set the format of labels
def LabelFormat(plt):
    ax = plt.gca()
    plt.tick_params(labelsize=14)
    labels = ax.get_xticklabels() + ax.get_yticklabels()
    [label.set_fontname('Times New Roman') for label in labels]
    font1 = {'family': 'Times New Roman',
             'weight': 'normal',
             'size': 16,
             }


# Plot the boundary lines (contour figure)
fig = plt.figure()
plt.contour(xx, yy, Z, 3, colors='black', linewidths=1, linestyles='solid')
PlotTrainPoint()
plt.title("3-Class classification (k = %i, weights = '%s')" % (k, 'uniform'), LabelFormat(plt))
plt.show()

# Plot the boundary maps (mesh figure)
plt.figure()
plt.pcolormesh(xx, yy, Z, cmap=cmap_light)
PlotTrainPoint()
plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
plt.title("3-Class classification (k = %i, weights = '%s')" % (k, 'uniform'), LabelFormat(plt))
plt.show()

  • 仿真結果
    圖4和圖5沒有本質區別,不同之處在於圖4只畫了分類的輪廓,圖5是將整個空間的點進行了分類。從圖中可以看出,kNN適合於非線性分類。

圖4

圖5

附錄

圖1的python 源代碼:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from scipy.spatial import Voronoi, voronoi_plot_2d, cKDTree
import pickle

# The number of test points and train points
Num_test = 3
Num_train = 50
# Generate the two-dimension feature (x,y), Here x,y are coordinates
Loc_x_train = 1000 * np.random.rand(Num_train, 1)
Loc_y_train = 1000 * np.random.rand(Num_train, 1)
Label_train = np.round(np.random.rand(Num_train, 1))
Loc_train = 1000 * np.random.rand(Num_train, 2)
filename = 'Loc_x_train'

# Generate the test points
Loc_x_test = 1000 * np.random.rand(Num_test, 1)
Loc_y_test = 1000 * np.random.rand(Num_test, 1)
Loc_test = 1000 * np.random.rand(Num_test, 2)

for i in range(0, len(Loc_x_train)):
    Loc_train[i] = [Loc_x_train[i], Loc_y_train[i]]
for i in range(0, len(Loc_x_test)):
    Loc_test[i] = [Loc_x_test[i], Loc_y_test[i]]

# Use the scipy.spatial packets to form voronoi
vor = Voronoi(Loc_train)
fig = voronoi_plot_2d(vor, show_points=False, show_vertices=False,
                      line_colors='black', line_width=2, line_alpha=1,
                      point_size=15)
# Plot the train pints
for i in range(0, Num_train):
    if Label_train[i]:
        plt.plot(Loc_x_train[i], Loc_y_train[i], 'rs', markersize=6, markerfacecolor="w")
    else:
        plt.plot(Loc_x_train[i], Loc_y_train[i], 'bs', markersize=6, markerfacecolor="w")

# Use the kdtree to find the nearest train point for each test point
voronoi_kdtree = cKDTree(Loc_train)
test_point_dist, test_point_regions = voronoi_kdtree.query(Loc_test)

# Classify the test points, the same color as the nearest train point
for i in range(0, Num_test):
    if Label_train[test_point_regions[i]]:
        plt.plot(Loc_x_test[i], Loc_y_test[i], 'ro', markersize=6)
    else:
        plt.plot(Loc_x_test[i], Loc_y_test[i], 'bo', markersize=6)

# The following are typical settings for plotting figures
plt.axis([0, 1001, 0, 1001])
ax = plt.gca()
plt.tick_params(labelsize=14)
labels = ax.get_xticklabels() + ax.get_yticklabels()
[label.set_fontname('Times New Roman') for label in labels]
font1 = {'family': 'Times New Roman',
         'weight': 'normal',
         'size': 16,
         }

plt.xlabel('X-axis (m)', font1)
plt.ylabel('Y-axis (m)', font1)
plt.title('k=1', font1)
plt.savefig('f2.png')
plt.show()

圖3的python源代碼:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from scipy.spatial import Voronoi, voronoi_plot_2d, cKDTree
import pickle
import math


original_point = [0, 0]

x = np.linspace(-1, 1, 10000)

# p=0.5
y1 = 1 + np.abs(x) - 2 * np.power(np.abs(x), 0.5)
y2 = - y1
plt.plot(x, y1, 'g')
plt.plot(x, y2, 'g')
# p=1
y1 = 1 - np.abs(x)
y2 = np.abs(x) - 1
plt.plot(x, y1, 'r')
plt.plot(x, y2, 'r')
# p=2
y1 = np.power(1 - np.power(x, 2), 1 / 2)
y2 = -y1
plt.plot(x, y1, 'b-')
plt.plot(x, y2, 'b-')

# p-> infty
for i in range(0, len(x)):
    if np.abs(x[i]) == 1:
        y1[i] = 0
    else:
        y1[i] = 1
y2 = -y1
plt.plot(x, y1, 'k-')
plt.plot(x, y2, 'k-')


# To plot figures
plt.axis('equal')
ax=plt.gca()

plt.annotate('$p=0.5$', xy=(0.25, 0.25), xycoords='data',
             xytext=(-25, -25), textcoords='offset points', color='g', fontsize=12, arrowprops=dict(arrowstyle="->",
             connectionstyle="arc,rad=0", color='g'))
plt.annotate('$p=1$', xy=(0.5, 0.5), xycoords='data',
             xytext=(-25, -25), textcoords='offset points', color='r', fontsize=12, arrowprops=dict(arrowstyle="->",
             connectionstyle="arc,rad=0", color='r'))
plt.annotate('$p=2$', xy=(0.7, 0.7), xycoords='data',
             xytext=(-25, -25), textcoords='offset points', color='b', fontsize=12, arrowprops=dict(arrowstyle="->",
             connectionstyle="arc,rad=0", color='b'))
plt.annotate(r'$p\to\infty$', xy=(1, 1), xycoords='data',
             xytext=(-35, -25), textcoords='offset points', color='k', fontsize=12, arrowprops=dict(arrowstyle="->",
             connectionstyle="arc,rad=0", color='k'))

plt.savefig('f3.png')
plt.show()



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章