❗写在前面 ❗:
… … … … … … … … . … … …
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=1∑K(xik−xjk)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、对于特征维度较高的数据,效果可能不佳,因为高维空间中的距离计算不够精确。