1.KNN算法

191 阅读7分钟

1、概述

俗话说得好:“物以类聚,人以群分”。在工作生活中,人们通常和身边的人具有一些相同的特质,KNN就是利用了这一原理实现数据的归类。KNN是一种监督学习算法,常用于处理分类和回归任务,适用于数值型和标称型数据。

2、基本原理

KNN是一种惰性学习算法,因为它不需要从训练集中学习任何的特征,只需要计算两个点之间的距离。将一组带标签的数据作为样本集,在输入一组没有标签的数据后,统计这些数据与样本集中数据的相似度,当距离越大时说明相似度更小,反之则越大,按照距离进行排序,选出前K个数据点的标签汇总成待分类数据点的标签。 如下图所示:

Pasted image 20250803142839.png

圆形和正方形代表两个不同的类别,现在我们需要辨别出处于中心的❌属于哪一类,按照KNN的思想:

  • 当K = 3时,绿色样本的3个近邻中有两个橙色正方形的样本,一个蓝色圆形的样本,此时应当把绿色样本点归类为正方形那一类。
  • 当K=5时,绿色样本的5个近邻中有两个橙色正方形的样本,三个蓝色圆形的样本,此时应当把绿色样本点归类为圆形那一类。 从上例可以看出,KNN在确定了K值后,按照一定的度量(这里是两个点的距离)选了离自己近的K个点,然后根据一定的规则(这里是少数服从多数)来决定最终的结果。其基本流程如下:

image.png

3、K值的选取

3.1K值选取的影响

当K值过小时,说明参考的邻居样本个数过少,存在一定的偶然性,从而导致算法的预测变得很不稳定。此外,若单个样本的影响越大,需要模型花更多的节点去拟合,最终导致模型的复杂度变高,并且模型与样本数据会十分契合,从而导致过拟合。 K值过大时,说明参考的邻居样本个数太多,可以减少噪声数据的影响,但模型会变得简单,不能很好地覆盖训练数据,导致出现欠拟合。

3.2K值选取的方法

那么应该如何选取恰当的K值才能获得比较好的效果呢?通常用交叉验证方法进行求解。: 将训练集分成K个子集,将其中一个子集作为测试集,其余作为训练集;然后换另一个子集作为测试集,其余作为训练集,如此重复K次,将表现最好的k作为最终的k值。

KNN的K值和K折交叉验证的K值不一样

  • KNN的K值是算法需要参考的邻居个数;
  • K折交叉验证的K值是数据要分割的份数以及需要验证的轮数。
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV

# 定义k值的范围,选取1-30中的奇数
param_grid = {'n_neighbors': list(range(1, 31,2))}
# 5折交叉验证
grid = GridSearchCV(
	KNeighborsClassifier(),
	param_grid,
	cv=5,
	scoring='accuracy')

grid.fit(X_train, y_train)

print("最佳K值:", grid.best_params_['n_neighbors'])

print("验证集准确率:", grid.best_score_)'

4、距离度量

距离度量是用来衡量两个数据点之间的相似度的,通常有如下三种距离计算公式:欧式距离闵可夫斯基距离曼哈顿距离,三者表达式如下:

类别公式
欧式距离d(x,y)=i=1n(xiyi)2\large d(x, y) = \sqrt{\sum_{i=1}^{n}(x_{i} - y_{i})^{2}}
曼哈顿距离d(x,y)=i=1nxiyi\large d(x,y) = \sum_{i=1}^{n}\|x_{i} - y_{i}\|
闵可夫斯基距离d(x,y)=(i=1nxiyiP)1p\large d(x,y) = (\sum_{i=1}^{n}\|x_{i} - y_{i}\|^{P})^{\frac{1}{p}}

设训练样本X={x(1),x(2),,x(n)}X = \{x^{(1)}, x^{(2)}, \cdots, x^{(n)}\},其中每个xx具有mm个特征,且x(i)={x1(i),x2(i),,xm(i)}Rmx^{(i)} = \{x^{(i)}_1,x^{(i)}_2,\cdots,x^{(i)}_m\} \in \mathcal{R}^m,则闵可夫斯基距离LpL_p的定义为:

Lp(x(i),x(j))=(i=1mxk(i)xk(j)p)1p,p1\large L_p(x^{(i)}, x^{(j)}) = (\sum_{i=1}^m |x_k^{(i)} - x_k^{(j)}|^p)^{\frac{1}{p}}, p \ge 1

  • 当p = 1时,LpL_p为曼哈顿距离,即

L1(x(i),x(j))=i=1mxk(i)xk(j)\large L_1(x^{(i)}, x^{(j)}) = \sum_{i=1}^m |x_k^{(i)} - x_k^{(j)}|

  • 当p = 2时,LpL_p为欧式距离,也是最常用的距离,即

L2(x(i),x(j))=(i=1m(xk(i)xk(j))2\large L_2(x^{(i)}, x^{(j)}) = \sqrt{(\sum_{i=1}^m (x_k^{(i)} - x_k^{(j)})^2}

5、决策规则

相较于回归算法,KNN更常用与分类算法。在分类算法中,在决定最后属于哪种类别时有如下几种策略:

  • 多数表决法:分类问题中最常用的分类决策方法,将待分类样本归类为邻居中出现次数最多的类别
  • 加权表决法:在多数表决法的基础上,对特定的邻居样本进行加权,从而提高对距离较近的样本的分类准确性。
  • 贝叶斯决策法:利用贝叶斯公式,根据邻居样本的类别和先验概率,计算后验概率。

6、示例

下面以Mnist数据集为例使用KNN分类算法,MNIST 是手写数字数据集,其中包含了很多手写数字0~9 的黑白图像,每张图像都由 28×2828 \times 28 个像素点组成。数据读入后,每个像素点用 1 或 0 表示,1 代表黑色像素,属于图像背景;0 代表白色像素,属于手写数字.

6.1 手动实现的KNN

import matplotlib.pyplot as plt
import numpy as np
import os

m_x = np.loadtxt('mnist_x.txt', delimiter = ' ')
m_y = np.loadtxt('mnist_y.txt')

data = np.reshape(np.array(m_x[0], dtype=int), [28, 28])
plt.figure()
plt.imshow(data, cmap='gray')

# 训练集和测试集 八二分
ratio = 0.8
split = int(len(m_x) * 0.8)
np.random.seed(0)
idx = np.random.permutation(np.arange(len(m_x))) # 下标随机打乱成一个新的顺序,存到 idx 里
m_x = m_x[idx]
m_y = m_y[idx]
x_train, x_test = m_x[:split], m_x[split:]
y_train, y_test = m_y[:split], m_y[split:]

# 距离度量选择欧式距离
def distance(a, b):
    return np.sqrt(np.sum(np.square(a - b)))

class KNN:
    def __init__(self, k, label_num):
        self.k = k
        self.label_num = label_num

    def fit(self, x_train, y_train):
        self.x_train = x_train
        self.y_train = y_train

    def get_knn_indics(self, x):
        dis = list(map(lambda a: distance(a, x), self.x_train)) #把训练集里每个样本与当前样本 x 的距离算出来
        knn_indics = np.argsort(dis)
        knn_indics = knn_indics[:self.k]
        return knn_indics
    
    def get_label(self, x):
        knn_indices = self.get_knn_indics(x)
        label_statistic = np.zeros(shape=[self.label_num])
        for index in knn_indices:
            label = int(self.y_train[index])
            label_statistic[label] += 1
        return np.argmax(label_statistic)
    
    def predict(self, x_test):
        predicted_test_labels = np.zeros(shape=[len(x_test)], dtype=int)
        for i, x in enumerate(x_test):
            predicted_test_labels[i] = self.get_label(x)
        return predicted_test_labels

for k in range(1, 10):
    knn = KNN(k, label_num=10)
    knn.fit(x_train, y_train)
    predicted_labels = knn.predict(x_test)

    accuracy = np.mean(predicted_labels == y_test)
    print(f'k的取值为{k}, 预测值为{accuracy * 100:.1f}%')

6.2 sklearn实现KNN分类算法

from sklearn.neighbors import KNeighborsClassifier
from matplotlib.colors import ListedColormap

data = np.loadtxt('gauss.csv', delimiter=',')
x_train = data[:, :2]
y_train = data[:, 2]
print(f'数据集大小为:{len(x_train)}')

plt.figure()
plt.scatter(x_train[y_train==0, 0], x_train[y_train==0, 1], c='blue', marker='o')
plt.scatter(x_train[y_train==1, 0], x_train[y_train==1, 1], c='red', marker='x')
plt.xlabel('x-axis')
plt.ylabel('y-axis')
plt.show()

step = 0.02

x_min, x_max = np.min(x_train[:, 0]) - 1, np.max(x_train[:, 0]) + 1
y_min, y_max = np.min(x_train[:, 1]) - 1, np.max(x_train[:, 1]) + 1

xx, yy = np.meshgrid(np.arange(x_min, x_max, step), np.arange(y_min, y_max, step))
grid_data = np.concatenate([xx.reshape(-1, 1), yy.reshape(-1, 1)], axis = 1)

fig = plt.figure(figsize=(16, 4.5))
ks = [1, 3, 10]
cmap_light = ListedColormap(['royalblue', 'lightcoral'])

for i, k in enumerate(ks):
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(x_train, y_train)
    z = knn.predict(grid_data)

    ax = fig.add_subplot(1, 3, i+1)
    ax.pcolormesh(xx, yy, z.reshape(xx.shape), cmap=cmap_light, alpha=0.7)
    ax.scatter(x_train[y_train==0, 0], x_train[y_train == 0, 1], c='blue', marker='o')
    ax.scatter(x_train[y_train==1, 0], x_train[y_train == 1, 1], c='red', marker='x')
    ax.set_xlabel('x-axis')
    ax.set_ylabel('y-axis')
    ax.set_title(f'k = {k}')

plt.show()

6.3 sklearn实现KNN回归算法

现在需要把一张黑白RGB图片颜色空间的值映射到LAB区间,其基本原理如下:

Pasted image 20250803210536.png

```python
from skimage import io
from skimage.color import rgb2lab, lab2rgb
from sklearn.neighbors import KNeighborsRegressor
import os
path = 'style_transfer'

data_dir = os.path.join(path, 'vangogh')
fig = plt.figure(figsize=(16,5))
for i, file in enumerate(np.sort(os.listdir(data_dir))[:3]):
    img = io.imread(os.path.join(data_dir, file))
    ax = fig.add_subplot(1, 3, i+1)
    ax.imshow(img)
    ax.set_xlabel('X axis')
    ax.set_ylabel('Y axis')
    ax.set_title(file)
plt.show()

block_size = 1

def read_style_image(file_name, size=block_size):
    img = io.imread(file_name)
    fig = plt.figure()
    plt.imshow(img)
    plt.xlabel('X axis')
    plt.ylabel('Y axis')
    plt.show()

    img = rgb2lab(img)
    w, h = img.shape[:2]

    X = []
    Y = []
    for x in range(size, w - size):
        for y in range(size, h - size):
            X.append(img[x - size: x+size+1, y-size:y+size+1, 0].flatten())
            Y.append(img[x, y, 1:])
    return X, Y
X, Y = read_style_image(os.path.join(path, 'style.jpg'))
knn = KNeighborsRegressor(n_neighbors=4, weights='distance')
knn.fit(X, Y)

def rebuild(img, size=block_size):
    # 打印内容图像
    fig = plt.figure()
    plt.imshow(img)
    plt.xlabel('X axis')
    plt.ylabel('Y axis')
    plt.show()

    # 将内容图像转为LAB表示
    img = rgb2lab(img)
    w, h = img.shape[:2]

    # 初始化输出图像对应的矩阵
    photo = np.zeros([w, h, 3])
    # 枚举内容图像的中心点,保存所有窗口
    print('Constructing window...')
    X = []
    for x in range(size, w - size):
        for y in range(size, h - size):
            # 得到中心点对应的窗口
            window = img[x - size: x + size + 1, \
                y - size: y + size + 1, 0].flatten()
            X.append(window)
    X = np.array(X)

    # 用KNN回归器预测颜色
    print('Predicting...')
    pred_ab = knn.predict(X).reshape(w - 2 * size, h - 2 * size, -1)
    # 设置输出图像
    photo[:, :, 0] = img[:, :, 0]
    photo[size: w - size, size: h - size, 1:] = pred_ab

    # 由于最外面size层无法构造窗口,简单起见,我们直接把这些像素裁剪掉
    photo = photo[size: w - size, size: h - size, :]
    return photo

content = io.imread(os.path.join(path, 'input.jpg'))
new_photo = rebuild(content)
# 为了展示图像,我们将其再转换为RGB表示
new_photo = lab2rgb(new_photo)

fig = plt.figure()
plt.imshow(new_photo)
plt.xlabel('X axis')
plt.ylabel('Y axis')
plt.show()

参考

  1. 《跟我一起学机器学习》清华大学出版社
  2. 动手学机器学习