KNN算法--手写最基础的机器学习算法

398 阅读7分钟

KNN算法介绍

        KNN(K-nearest neighbor)是机器学习里最简单的算法之一,原理也非常简单。可以用于分类或者回归。当我们要为某个新输入的实例分类的时候,我们可以抽取距离该样本最近的K个实例,如果这k个实例中的大多数属于某个类别,我们就把该实例分为该类别。

        通俗一点,物以类聚,人以群分,某个人是什么水平的,看看他身边的人,可能会知道大概。某个物品是什么种类的,看看和它相似的,就能知道大概。

        在scikit-learn中,已经有现成的KNN算法可以使用。但是为了加深理解,我们手动实现KNN算法。然后在尝试调用scikit-learn中的算法。

自己用python编写代码,实现KNN算法

假设:有10个点,位于坐标轴上,他们总共被分为2类(A类和B类),代码如下:

import numpy as np

// data是点的集合,target是其对应的分类。
data = np.array([
    [1.9,2.1],
    [2.9,3.1],
    [4.3,3.4],
    [5.5,3.5],
    [4.1,2.5],
    [4.9,5.9],
    [6.8,5.9],
    [6.5,7.0],
    [6.8,8.0],
    [7.1,9.0]
])
target = np.array(['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B'])

我们可以在坐标轴上绘制其分布:

plt.scatter(data[:, 0], data[:, 1])

结果如下图:

十个点已经显示出来了,我们按照分类A,和分类B,将每个点显示为不同的颜色,代码如下:

plt.scatter(data[:, 0][target=='A'], data[:, 1][target=='A'], color='r')
plt.scatter(data[:, 0][target=='B'], data[:, 1][target=='B'], color='g')

结果如下图:

这个时候,十个点和对应的类别已经显示在图上了。这个时候,有一个新输入的实例

new_target = np.array([3.3, 4])

我们把该点添加到之前的图中,修改代码如下:

plt.scatter(data[:, 0][target=='A'], data[:, 1][target=='A'], color='r')
plt.scatter(data[:, 0][target=='B'], data[:, 1][target=='B'], color='g')
plt.scatter(new_target[0], new_target[1], color='b')

结果如下:

可以看到蓝色的点就是我们要预测的点。(PS:肉眼可见,该点和红色的是一类的,但是我们还是要通过代码实现它) 下面要进入正题了: 我们假设KNN中的K=3,也就是说,我们根据最近的3个点的类别,去判断蓝色点属于哪一类。

k = 3

不过首先,我们还是要精确计算蓝色点和其他所有点的距离。 点D1 和点D2的距离,我们采用欧拉距离,其定义如下(还可以有其他的距离,比如曼哈顿距离,本篇再不介绍,我们初高中学过的距离公式,就是欧拉距离):

distances = np.sum(np.square(data - new_target), axis = 1)
distances

我们已经求到了蓝色点和其他所有点的距离,结果如下:

array([ 5.57,  0.97,  1.36,  5.09,  2.89,  6.17, 15.86, 19.24, 28.25, 39.44])

knn的核心思想是根据距离最近的k个点,去判断最后的结果,所以我们给结果拍个序:

nearest = np.argsort(distances)
nearest
结果如下:
array([1, 2, 4, 3, 0, 5, 6, 7, 8, 9])

可以看到,距离最近的点,数组下标为1,距离最远的点,数组下标为9 距离最近的k=3个点,序号是 1,2,4 接着求距离最近的k个点

k = 3
nearest_k = [target[item] for item in nearest[:k]]
nearest_k

结果如下:

['A', 'A', 'A']

也就是说,1,2,4 都投票给了分类A

最后一步,新实例到底属于哪个分类,就由这k个点去投票了。

from collections import Counter
votes = Counter(nearest_k)
votes

结果如下:

Counter({'A': 3})

votes.most_common(1)[0][0] 结果如下:

'A'

至此,我们已经自己编写了KNN的代码。

封装自己编写的KNN算法

我们仿scikit-learn中的算法的一般规则: 1,init方法 返回一个模型的实例 2,fit方法 将数据集用于训练模型 3,predict方法 预测我们输入的实例 4,score方法 用于评判我们预测数据的准确度

封装后的代码如下:

import numpy as np
from math import sqrt
from collections import Counter
from metrics import accuracy_score

class KNNClassifier:

    def __init__(self, k):
        """初始化kNN分类器"""
        assert k >= 1, "k must be valid"
        self.k = k
        self._X_train = None
        self._y_train = None

    def fit(self, X_train, y_train):
        """根据训练数据集X_train和y_train训练kNN分类器"""
        assert X_train.shape[0] == y_train.shape[0], \
            "the size of X_train must be equal to the size of y_train"
        assert self.k <= X_train.shape[0], \
            "the size of X_train must be at least k."

        self._X_train = X_train
        self._y_train = y_train
        return self

    def predict(self, X_predict):
        """给定待预测数据集X_predict,返回表示X_predict的结果向量"""
        assert self._X_train is not None and self._y_train is not None, \
                "must fit before predict!"
        assert X_predict.shape[1] == self._X_train.shape[1], \
                "the feature number of X_predict must be equal to X_train"

        y_predict = [self._predict(x) for x in X_predict]
        return np.array(y_predict)

    def _predict(self, x):
        """给定单个待预测数据x,返回x的预测结果值"""
        assert x.shape[0] == self._X_train.shape[1], \
            "the feature number of x must be equal to X_train"

        distances = [sqrt(np.sum((x_train - x) ** 2))
                     for x_train in self._X_train]
        nearest = np.argsort(distances)

        topK_y = [self._y_train[i] for i in nearest[:self.k]]
        votes = Counter(topK_y)

        return votes.most_common(1)[0][0]

    def score(self, X_test, y_test):
        """根据测试数据集 X_test 和 y_test 确定当前模型的准确度"""

        y_predict = self.predict(X_test)
        return accuracy_score(y_test, y_predict)

对比sklearn中的KNN

import numpy as np
from sklearn.neighbors import KNeighborsClassifier

KNN_classfier = KNeighborsClassifier(n_neighbors=3)
KNN_classfier.fit(data, target)

new_target = np.array([[3.3, 4]])
KNN_classfier.predict(new_target)

结果如下:

array(['A'], dtype='<U1')

和我们自己编写的KNN算法的结果一直,预测的都是A。

为了方便我们理解,我们编写的算法,只是预测了单个实例。下面,我们采用正式的数据集来运行自己写的算法。

用sklearn的KNN预测鸢尾花

鸢尾花数据集是很好的用于KNN算法的数据集。我们从0开始。 该数据集,提供了花瓣的4个属性值,然后会告诉我们每个案例对应的分类。

为了方便查看更多细节,首先导入鸢尾花数据集:

import numpy as np
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_iris

KNN_classfier = KNeighborsClassifier(n_neighbors=3)

data = load_iris()
X = data.data
y = data.target

然后我们查看数据集的一些信息吧。

data.feature_names

4个特征值分别是(花萼长度、花萼宽度、花瓣长度、花瓣宽度),如下:

['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']

然后看看数据集大小:

data.data.shape, data.target.shape

结果如下:

((150, 4), (150,))

共150条数据,每条数据4个特征值,target是150x1的数组,代表了150条数据对应的分类结果。

我们先将数据集切分为训练数据和测试数据2个部分吧,我们将30%的数据用作测试集,70%的部分用作训练集:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,random_state=666)

X_train.shape, y_train.shape, X_test.shape, y_test.shape

返回结果如下:

((105, 4), (105,), (45, 4), (45,))

结果显示,数据集已经按照我们的想法,切分为 训练集:测试机 = 105:45的2个部分了。

话不多书,开始训练吧

KNN_classfier_iris = KNeighborsClassifier(n_neighbors=3)

KNN_classfier_iris.fit(X_train, y_train)

在预测之前,我们看看它们本来的分类结果吧:

y_test

结果如下:

array([1, 2, 1, 2, 0, 1, 1, 2, 1, 1, 1, 0, 0, 0, 2, 1, 0, 2, 2, 2, 1, 0,
       2, 0, 1, 1, 0, 1, 2, 2, 0, 0, 1, 2, 1, 1, 2, 2, 0, 1, 2, 2, 1, 1,
       0])

接下来我们预测一下

y_predict = KNN_classfier_iris.predict(X_test)
y_predict

返回的是45个测试样本的预测结果,如下:

array([1, 2, 1, 2, 0, 1, 1, 2, 1, 1, 1, 0, 0, 0, 2, 1, 0, 2, 2, 2, 1, 0,
       2, 0, 1, 1, 0, 1, 2, 2, 0, 0, 1, 2, 1, 1, 2, 2, 0, 1, 2, 2, 1, 1,
       0])

惊讶的发现,预测结果竟然和我们的原本的分类结果完全一样。这么说本次预测的准确度应该是100%了? 为了验证这个猜想,我们调用方法查看一下准确度吧:

KNN_classfier_iris.score(X_test, y_test)

结果如下:

1.0

哦耶,本次预测的结果是1.0,也就是100%。

用自己的KNN,预测鸢尾花

除了导入模型这一步有一点差距,其他的步骤,和调用sklearn中的算法,几乎一样,我们快速实现吧。

# 导入的是自己实现的KNNClassfier
from KNN import KNNClassifier

KNN_classfier = KNNClassifier(k=3)
KNN_classfier.fit(X_train, y_train)
KNN_classfier.predict(X_test)

预测结果如下:

array([1, 2, 1, 2, 0, 1, 1, 2, 1, 1, 1, 0, 0, 0, 2, 1, 0, 2, 2, 2, 1, 0,
       2, 0, 1, 1, 0, 1, 2, 2, 0, 0, 1, 2, 1, 1, 2, 2, 0, 1, 2, 2, 1, 1,
       0])

看看准确度吧:

KNN_classfier.score(X_test, y_test)

结果如下:

1.0

我们自己实现的KNN算法,和sklearn的KNN算法,结果一致。

总结

        KNN算法的思路简单,但是也有一些明显的优缺点:

        第一点:k的取值很关键,如果k的取值过大,假设这里的k=10,那么就相当于用全部的点,去投票,最后的结果显而易见,就是5:5的结果。那么实际结果没有参考意义。如果k=1,那么用最近的一个点去投票,假如最近的这个点,刚好是一个被错误分类的实例,或者偏差很大的实例。那么其可靠性就不强。总而言之,k要适当大小。

        第二点:计算量非常巨大,只适合小规模数据集,因为每次都要计算该点和所有点的距离,样本数量大的时候,耗时严重。