python学智能算法(七)|KNN邻近算法

发布于:2025-03-12 ⋅ 阅读:(23) ⋅ 点赞:(0)

【1】引言

前述学习进程中,已经了解了一些非常经典的智能算法,相关文章包括且不限于:

python学智能算法(三)|模拟退火算法:深层分析_模拟退火 动画演示-CSDN博客

python学智能算法(四)|遗传算法:原理认识和极大值分析_遗传算法和模拟退火时间复杂度-CSDN博客

python学智能算法(五)|差分进化算法:原理认识和极小值分析-CSDN博客

python学智能算法(六)|神经网络算法:BP神经网络算法入门-CSDN博客

这些算法一定程度上都非常关注自变量和因变量的内在关系,今天换一个方向,不区分自变量因变量,或者就只有因变量。因为只有一个,叫它变量也能完整描述。现在的主要关注点是:变量和变量之间的位置关系。按这种关注方式运行的智能算法,就是KNN算法。

【2】原理解释

如果一个样本在特征空间中的K个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。

除了看CSDN的博文,也可以到以下网址学习:

第2章 k-近邻算法 - 《机器学习实战(Machine Learning in Action)》 - 书栈网 · BookStack

What is k-Nearest Neighbor (kNN)? | A Comprehensive k-Nearest Neighbor Guide | Elastic

以及未来会反复用到的scikit-learn的官网学习:

KNNImputer — scikit-learn 1.6.1 documentation

【3】代码测试

理解一个算法的最简单途径就是用代码跑一次。

【3.1】准备工作

首先引入必要模块:

import numpy as np #引入numpy模块
import matplotlib.pyplot as plt #引入matplotlib模块
from sklearn.datasets import load_iris #引入sklearn模块

这里和以前不同的是,在数字处理模块numpy和画图模块matplotlib之外,新增加了一个sklearn模块。

sklearn模块是一个免费的数据库,里面存储了可以公开使用的数据。

学习sklearn模块就直接去官网:Examples — scikit-learn 1.6.1 documentationr

为了实现对sklearn模块中数据的调用,还需要处理一下:

# 加载鸢尾花数据集
# 从免费的sklearn库中存储的鸢(yuān)尾花数据集
iris = load_iris()
# X存储了iris的特征数据
# X的形状是(150行,4列)的矩阵数组
# 意味着有 150 个样本,每个样本有 4 个特征,分别是花萼长度、花萼宽度、花瓣长度和花瓣宽度
# 这些特征是用于描述每个鸢尾花样本的属性,是模型进行学习和预测的依据
X = iris.data  # 特征数据
# y中存储了150个数,相当于150个标签
y = iris.target  # 标签数据

 X取到的鸢尾花统计数据是固定的150行4列矩阵;4列代表了四个详细特征,可以理解为每一种鸢尾花样本都有4个特征,150行数据代表150种鸢尾花样本,y的值就是150。

【3.2】主函数

为了实现对整体算法的掌握,推荐的方式是先读主函数,然后再去读子函数。主函数会告知算法会调用哪些子函数,通过对主函数结构的阅读,会理解整个算法的原理。

# 设置 k 值
k = 5
# 进行预测
y_pred = knn_predict(X_train, y_train, X_test, k)

主函数先设置了k=5,代表取最邻近的5个样本量一起归类,然后预测数据y_pred直接调用了子函数knn_predict()。

【3.3】子函数

【3.3.1】knn_predict()

在主函数的指引下,直奔子函数knn_predict():

# 定义 KNN 分类器
def knn_predict(X_train, y_train, X_test, k):
    y_pred = []
    for test_sample in X_test:
        # 计算测试样本与所有训练样本的欧氏距离
        # test_sample 是一个一维数组(形状为 (n_features,))从数学上看是一个一行n_features的行向量
        # X_train 是一个(形状为 (n_train_samples, n_features))数组,从数学上看X_train的每一行也是一个一行n_features的行向量
        # X_train的每一行都会和test_sample作差,作差过程是同一位置元素相减
        # X_train 是一个(形状为 (n_train_samples, n_features))数组,所以会有n_train_samples行计算结果
        # 作差得到的数组形状为 (n_train_samples, n_features)
        # axis=1的意思是,在作差得到的数组内,每一个行内部求和,一共会得到n_train_samples行1列的数据
        distances = np.sqrt(np.sum((X_train - test_sample) ** 2, axis=1))
        # 获取距离最近的 k 个样本的索引
        # np.argsort会对distances进行从小到大的排序,但是排序后输出的数据原来对应的位置索引
        # 由于数据从小到大排序,输出的位置索引成为了输出矩阵数据,这时候对输出矩阵取出前k个
        # k_indices就是获得最小距离中的前k个距离对应的位置索引
        k_indices = np.argsort(distances)[:k]
        # 获取这 k 个样本的标签
        # 将k_indices 带回y_train,用位置索引的形式,找出距离test_sample最近的k行数据
        k_nearest_labels = y_train[k_indices]
        # 统计每个标签出现的次数
        # k_nearest_labels中的所有数据会被逐一统计归类
        # 输出unique_labels代表一共有unique_labels个不同的数据,每个数据出现的次数为counts
        unique_labels, counts = np.unique(k_nearest_labels, return_counts=True)
        # 选择出现次数最多的标签作为预测结果
        predicted_label = unique_labels[np.argmax(counts)]
        # y_pred是个空矩阵,往里面叠加出现次数最多的标签
        y_pred.append(predicted_label)
    return np.array(y_pred)

首先会看到除了定义一个空列表y_pred=[],剩下的是一个大的for循环。

在for循环里面:

第一步,x_train-test_sample作差后平方求和,作差过程是同一位置相减,求和是按照行的形式,把一行内部的所有列加在一起,所以最后会获得和x_train行数一样多的一个单列数组,这个数组还要再开方一次才会赋值给distance。distance是按照数学里面欧几里得距离求解的公式定义的。

第二步,调用np.argsort()函数对所有的distance元素从小到大排个序,这里通过索引的形式调用了k,意思是只选用前k个元素,也就是距离最近的k个元素。但此时输出的是前k个元素的位置索引k_indices。

第三步,通过k_indices,可以直接访问到y_train,因为x_train和y_train在行数上其实一一对应。此时获得的最近的k个元素都存储在k_nearest_labels中。

第四步,调用np.unique()函数,对存储最近的k个元素k_nearest_labels进行分类,因为k个元素可能有一些是相等的,所以会合并一些相等元素,获得独特的unique_labels和具体的数量counts。

第五步,在所有counts中,调用np.argmax()函数分析出最大的counts,找出这个counts对应的具体unique_lables赋值给predicted_lablel。这个predicted_lablel指向次轮距离计算中k个最近值出现频次最高的元素。

第六步,将predicted_lablel存储进y_pred列表。

重复上述六步,直至所有的x_train都通过计算。

虽然在这个函数里可以很直白理解knn运算过程,但几个参数的来源还需进一步追溯来加深理解。这就需要学习train_test_split()函数。

【3.3.2】train_test_split()
# 划分数据集为训练集和测试集
def train_test_split(X, y, test_size=0.3, random_state=42):
    # 设置随机数的种子目的是保证每次生成的随机数一致
    np.random.seed(random_state)
    # len(X)=150
    # indices是[0,len(X)-1]中的每一个整数组成的数组
    indices = np.arange(len(X))
    # 调用np.random.shuffle()函数将indices顺序随机打乱
    np.random.shuffle(indices)
    # test_size在函数的输入参数中提前给出,此处和len(X)相乘后取整
    # test_size在经int()函数取正时,采用的是向下1取正方式
    # test_size本身要比indices小
    test_size = int(len(X) * test_size)
    # test_indices按照索引的形式,只取出indices数组中索引位置从0到test_size-1的部分
    test_indices = indices[:test_size]
    # train_indices按照索引的形式,取出indices数组中索引位置从test_size开始到末尾所有的数据
    train_indices = indices[test_size:]
    # X_train按照索引的形式,取出X数组中train_indices所包含数据对应的行数
    # train_indices是indices数组中索引位置从test_size开始到末尾所有的数据
    # X_train取出X数组中行数=【从test_size开始到末尾所有的数据】的行
    X_train = X[train_indices]
    # y_train按照索引的形式,取出y数组中train_indices所包含数据对应的行数
    # train_indices是indices数组中索引位置从test_size开始到末尾所有的数据
    # y_train取出X数组中行数=【从test_size开始到末尾所有的数据】的行
    y_train = y[train_indices]
    # X_test按照索引的形式,取出X数组中test_indices所包含数据对应的行数
    # test_indices是indices数组中索引位置从0到test_size所有的数据
    # X_train取出X数组中行数=【从0到test_size所有的数据】的行
    X_test = X[test_indices]
    # y_test按照索引的形式,取出y数组中test_indices所包含数据对应的行数
    # test_indices是indices数组中索引位置从0到test_size所有的数据
    # y_test取出X数组中行数=【从0到test_size所有的数据】的行
    y_test = y[test_indices]
    return X_train, X_test, y_train, y_test

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

train_test_split()函数直接读取了X,y,还增加了参数test_size=0.3以及random_state=42。test_size是用来划定X和y的测试集大小;random_state=42则是在机器学习中随机数种子的一个习惯性定义,传说和电影有关,大家其实可以随意改动这个数字。

在生成随机数之后,用indices直接取得了X的所有行数索引。

然后调用np.random.shuffle()函数对indices进行了随机换序,这样做的目的其实是让用于训练的数据集对整体样本量具有代表性。

之后定义的test_size调用了test_size=0.3来设定一个较小的位置索引范围,test_indices则提取了前test_size行数据。由于之前已经打乱了indices的顺序,所以前test_size行数据对整体样本量具有代表性。

剩余的数据则由train_indices存储。

具体的:

X_test存储了X的前test_size行数据;

y_test存储了y的前test_size行数据;

X_train存储了X的从第test_size行开始到结束的所有数据;

y_train存储了y的从第test_size行开始到结束的所有数据。

train_test_split()将X和y分解为测试部分和训练部分。

【3.4】预测

在主函数的运行过程中,因为x_test是参考位置点,所以经过计算获得的训练数据y_pred数量和x_test一样多,自然也和y_test一样多。

所以,直接调用np.mean()函数计算y_pred == y_test,就可以获得计算的准确率。

【3.5】可视化

在预测部分,实际上所有计算过程已经结束。运行代码在控制台也会获得输出:

NN 模型的准确率: 1.00

可见KNN算法准确识别出了鸢尾花的类型。

为了进一步图片展示,选取了鸢尾花的两个特征来画散点图,实际上这话总选择是随机的,只要选择两个特征可以画散点图即可。同一类别是根据欧氏距离来的,所以用欧氏距离来控制颜色,就可以获得鸢尾花的分类效果:

# 可视化预测结果(这里只选取前两个特征进行可视化)
plt.figure(figsize=(10, 6))
plt.scatter(X_test[:, 0], X_test[:, 1], c=y_pred, cmap='viridis', edgecolor='k', s=50)
plt.title('KNN Classification Results')
plt.xlabel('Sepal length')
plt.ylabel('Sepal width')
plt.show()

运行代码获得的图像为:

图1 KNN运行效果

【4】细节说明

tes-size和k都是随机定义的数据,可以自由修改测试KNN算法运行效果。

【5】总结

学习了KNN算法的基本原理,使用python通过KNN算法实现了对鸢尾花的分类。