手撕机器学习算法 | 近邻算法 | KNN

❗写在前面 ❗:

… … … … … … … … . … … …
1️⃣ 提供两种语言实现:本文提供两种语言实现,分别为Python和C++,分别依赖的核心库为Numpy和Eigen.

2️⃣ 提供关键步的公式推导:写出详尽的公式推导太花时间,所以只提供了关键步骤的公式推导。更详尽的推导,可移步其他博文或相关书籍.

3️⃣ 重在逻辑实现:文中的代码主要以实现算法的逻辑为主,在细节优化上面,并没有做更多的考虑,如果发现有不完善的地方,欢迎指出.


🌞 欢迎关注专栏,相应的内容会持续更新,祝大家变得更强!!
… … … … … … … … . … … …

1、原理推导

  
k k k近邻算法(K-Nearest Neighbors, KNN)最直观的解释:给定一个训练集,对于新的输入实例,在训练集中找到与该实例最近邻的 k k k个实例,这 k k k个实例的多数属于哪个类,则该实例就属于哪个类。

从上述对 k k k近邻的直观解释中,可以归纳出该算法的几个关键点:

(1)找到与该实例最近邻的实例,这里就涉及如何找到,即在特征向量空间中,要采取何种方式来度量距离(一般用欧氏距离)。

(2)是 k k k个实例,这个 k k k值的大小如何选择。

(3) k k k个实例的多数属于哪个类,明显是多数表决的归类规则。当然,还可能使用其他规则,所以第三个关键就是分类决策规则。

在这里插入图片描述
  
  
| 在距离选择方面 |,一般选择的是欧氏距离作为度量标准。两个样本向量 x i , x j x_i,x_j xi,xj他们之间的欧式距离可以表示为:
  
           d i j = ∑ k = 1 K ( x i k − x j k ) 2 d_{ij}=\sqrt{\sum\limits_{k=1}^{K}(x_{ik}-x_{jk})^2} dij=k=1K(xikxjk)2
  
其中, d i j d_{ij} dij表示两个样本之间的欧氏距离, k k k表示向量中第 k k k个的分量, K K K表示向量分量的总数。

| k k k值的大小对分类结果有重大影响 |, 在选择的 k k k值较小的情况下,就相当于用较小的邻域中的训练实例进行预测,只有与输入实例较近的训练实例才会对预测结果起作用。但与此同时预测结果会对实例非常敏感,分类器抗噪能力较差,因而容易产生过拟合,所以一般而言, k k k值的选择不宜过小。

但如果选择较大的 k k k值,就相当于用较大邻域中的训练实例进行预测,相应的分类误差会增大,模型整体变得简单,会产生一定程度的欠拟合。我们一般采用交叉验证的方式来选择合适的 k k k值。

| 最后是分类决策规则 | ,通常为多数表决。 k k k近邻算法的本质是基于距离和 k k k值对特征空间进行划分。当训练数据、距离度量方式、 k k k值和分类决策规则确定后,对于任一新输入的实例,其所属的类别唯一地确定。 k k k近邻算法不同于其他监督学习算法,它没有显式的学习过程

2、算法实现

🌈Python实现

import numpy as np
from collections import Counter

class KNN:

    def __init__(self, k=None):
        """
        初始化 KNN 分类器
        
        - k: 选择最近邻的数目
        """
        self.k = k
        self.X_train = None
        self.y_train = None

    def calc_euclidean_distance(self, X):
        """
        计算待预测样本 X 与训练样本之间的欧氏距离矩阵

        Parameters:
        - X: 待预测样本的特征矩阵,每行是一个待预测样本的特征向量

        Returns:
        - euclidean_distance: 欧氏距离矩阵,其中每个元素表示一个待预测样本与一个训练样本之间的欧氏距离
        """
        X1X2 = np.dot(X, self.X_train.T)  # 计算 X 与 X_train 的点积
        X1_square = np.sum(np.square(X), axis=1, keepdims=True)  # 计算 X 中每个样本的平方和,并保持二维形状
        X2_square = np.sum(np.square(self.X_train), axis=1, keepdims=True)  # 计算 X_train 中每个样本的平方和,并保持二维形状

        # 计算欧氏距离矩阵的平方
        dist_square = X1_square - 2 * X1X2 + X2_square.T

        # 取平方根得到欧氏距离矩阵
        euclidean_distance = np.sqrt(dist_square)

        return euclidean_distance
    
    def fit(self, X, y):
        """
        假装训练,其实只是将训练数据进行保存一下
        :param X:  待训练样本的特征矩阵
        :param y:  待训练样本的目标值
        :return: 
        """
        self.X_train = X
        self.y_train = y

    def predict(self, X):
        """
        对待预测样本 X 进行预测

        Parameters:
        - X: 待预测样本的特征矩阵,每行是一个待预测样本的特征向量
        
        Returns:
        - y_pred: 预测结果向量,每个元素是对应待预测样本的预测类别
        """
        euclidean_distance = self.calc_euclidean_distance(X)  # 计算 X 与 X_train 的欧氏距离矩阵
        m_pred = euclidean_distance.shape[0]  # 待预测样本的数量

        y_pred = np.zeros(m_pred)  # 初始化预测结果向量

        for i in range(m_pred):
            # 对每个待预测样本,找到其与所有训练样本的距离,并按距离排序
            labels = self.y_train[np.argsort(euclidean_distance[i, :])].flatten()

            closest_y = labels[:self.k]  # 取最近的 k 个样本的标签

            c = Counter(closest_y)  # 统计最近 k 个样本中每个类别的出现次数

            y_pred[i] = c.most_common(1)[0][0]  # 将出现次数最多的类别作为预测结果

        return y_pred

模拟测试:

import numpy as np
import matplotlib.pyplot as plt

# 模拟数据集
np.random.seed(0)

# 类别1数据,均值为[2, 2],协方差矩阵为[[1, 0], [0, 1]]
class1_mean = [2, 2]
class1_cov = [[1, 0], [0, 1]]
class1_samples = np.random.multivariate_normal(class1_mean, class1_cov, 50)

# 类别2数据,均值为[4, 4],协方差矩阵为[[1, 0], [0, 1]]
class2_mean = [4, 4]
class2_cov = [[1, 0], [0, 1]]
class2_samples = np.random.multivariate_normal(class2_mean, class2_cov, 50)

# 合并数据集
X_train = np.concatenate((class1_samples, class2_samples), axis=0)
y_train = np.concatenate((np.zeros(50), np.ones(50)))  # 标签,0表示类别1,1表示类别2


plt.figure(figsize=(8, 6))
plt.scatter(class1_samples[:, 0], class1_samples[:, 1], color='blue', label='Class 1')
plt.scatter(class2_samples[:, 0], class2_samples[:, 1], color='red', label='Class 2')
plt.title('Simulated Dataset for KNN')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend()
plt.grid(True)
plt.show()

生成的数据分布如图:

在这里插入图片描述

测试结果:

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split

# numpy手搓实现的knn
knn = KNN(k=5)
X1, X2, y1, y2 = train_test_split(X_train, y_train, train_size=0.8)
knn.fit(X1, y1)
print("numpy的knn实现, 当k=5的时候knn的在训练集上面的正确率:", (knn.predict(X2) == y2).sum() / len(y2))

# sklearn实现的knn
knn_model = KNeighborsClassifier(5)
knn_model.fit(X1, y1)
print("sklearn的knn实现, 当k=5的时候knn的在训练集上面的正确率:", (knn_model.predict(X2) == y2).sum() / len(y2))

# 结果:
# numpy的knn实现, 当k=5的时候knn的在训练集上面的正确率: 0.9
# sklearn的knn实现, 当k=5的时候knn的在训练集上面的正确率: 0.9

no problem!

⚡C++实现

  
 太简单了…略…有人需要再加…

3、总结

KNN算法主要在推荐系统以及图像识别领域运用得较多。综合来说KNN具有以下特点。

优点:
1、简单直观,易于理解和实现。
2、适用于多分类问题和数据量不大的情况。
3、对异常值不敏感,因为它只依赖最近的K个邻居。

缺点:
1、预测速度较慢,特别是在大数据集上。
2、需要保存全部训练数据,空间复杂度高。
3、对于特征维度较高的数据,效果可能不佳,因为高维空间中的距离计算不够精确。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值