Python 深度实战:如何用 Sklearn 构建一个完美的 K 近邻(KNN)模型

引言

你是否想过,如果我们想预测一种水果的类别,最直观的方法是什么?也许是看看周围类似的水果是什么。这就是 K 近邻算法的核心思想——一种非常直观且强大的机器学习算法。简单来说,它通过测量不同特征值之间的距离来进行分类。

在这篇文章中,我们将深入探讨 KNN 算法的原理,并使用 Python 的 Scikit-Learn 库从零开始构建一个完整的 KNN 分类模型。我们不仅会涉及到基础代码的实现,还会深入讨论数据预处理、超参数调优、K 值的选择以及如何优化模型性能。无论你是刚入门机器学习,还是希望巩固 Sklearn 使用技巧,这篇文章都会为你提供实用的见解。

1. 理解 KNN 的核心逻辑

在开始写代码之前,让我们先用简单的语言理解一下 KNN 是如何工作的。

KNN 是一种基于实例的学习(Instance-Based Learning),也被称为懒惰学习(Lazy Learning)。这意味着它不会在训练阶段显式地学习一个模型函数,而是直接记忆训练数据。当我们要进行预测时,它会在训练集中寻找与待预测数据点距离最近的 K 个邻居,并根据这 K 个邻居的标签来决定预测结果。

  • 对于分类问题:通常采用“多数投票”原则,即 K 个邻居中哪个类别最多,预测结果就属于哪个类别。
  • 对于回归问题:通常计算 K 个邻居的目标值的平均值。

这个过程听起来很简单,但其中蕴含着几个关键的技术细节,比如如何定义“距离”,以及如何选择合适的“K 值”。我们将在后续章节中详细解答。

2. 准备工作:生成与可视化非线性数据

为了展示 KNN 处理复杂边界的能力,我们首先需要生成一些非线性可分的数据。现实世界的数据往往不是简单的线性分布,因此使用 Sklearn 的 make_moons 函数生成“月牙形”数据是一个非常好的选择。这种数据集包含两个交错的半圆,非常适合测试分类器的非线性拟合能力。

2.1 导入必要的库

在开始之前,我们需要导入一些核心的数据处理和可视化库。你可以把它们看作是我们手中的工具箱:

  • pandas:用于数据处理和分析,我们可以用它来创建整洁的数据表格。
  • INLINECODE45ce6520 和 INLINECODEd7459e56:Python 中最强大的可视化库,帮我们“看见”数据的形状。
  • sklearn(Scikit-Learn):我们将使用的核心机器学习库。
  • numpy:用于数值计算。
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_moons
import numpy as np

# 设置绘图风格,让图表看起来更专业
sns.set(style="whitegrid")

2.2 生成合成数据

接下来,我们使用 make_moons 生成 300 个样本点,并加入少量的噪声。噪声的加入模拟了现实世界数据中的干扰,使问题更接近真实场景。

# 生成合成数据:n_samples=300 表示生成300个点,noise=0.3 添加噪声
# random_state=42 确保每次运行代码时生成的数据是一样的,方便复现
X, y = make_moons(n_samples=300, noise=0.3, random_state=42)

# 为了方便可视化,我们将数据转换为 DataFrame
# X 包含特征坐标,y 包含类别标签 (0 或 1)
df = pd.DataFrame(X, columns=["Feature 1", "Feature 2"])
df[‘Target‘] = y

# 打印前5行数据,快速预览
print("数据预览:")
print(df.head())

2.3 数据可视化

俗话说“一图胜千言”。让我们画出这些数据点,看看我们面临的挑战是什么。

plt.figure(figsize=(10, 6))
# 使用 seaborn 绘制散点图,根据 ‘Target‘ 列着色
sns.scatterplot(data=df, x="Feature 1", y="Feature 2", hue="Target", palette="Set1", s=60, edgecolor=‘k‘)
plt.title("2D 分类数据集可视化", fontsize=15)
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.legend(title=‘Class‘)
plt.show()

从图中你可以看到,两个类别交织在一起,形成两个半月形。这种形状无法用一条直线分割,因此线性模型(如逻辑回归)可能会遇到困难,而这正是 KNN 大显身手的时候。

3. 数据预处理:归一化的艺术

在训练模型之前,有一个绝对不能忽略的步骤:数据归一化

为什么 KNN 必须归一化?

KNN 算法主要依赖距离度量(最常用的是欧几里得距离)来判断相似度。如果数据中的特征具有不同的量纲或数量级,距离计算就会被数值范围大的特征所主导。

举个简单的例子:假设我们要预测房价,特征有“面积(平方米)”和“房间数”。

  • 面积范围:50 ~ 200
  • 房间数范围:1 ~ 5

如果不进行归一化,“面积”的微小变化(比如10米)在距离计算中产生的数值,可能会远大于“房间数”的巨大变化(比如从1变到5)。这会导致模型错误地认为“房间数”对预测结果几乎没有影响。

我们使用 StandardScaler 将所有特征缩放到均值为 0,方差为 1 的范围内,确保每个特征在距离计算中拥有平等的“话语权”。

3.1 实现数据拆分与归一化

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 1. 实例化缩放器
scaler = StandardScaler()

# 2. 计算训练数据的均值和标准差,并进行转换
# 注意:我们只对 X 进行缩放,不需要对目标标签 y 做处理
X_scaled = scaler.fit_transform(X)

# 3. 拆分训练集和测试集
# test_size=0.3 意味着 30% 的数据用于测试,70% 用于训练
# stratify=y 确保训练集和测试集中正负样本的比例与原数据集一致(分层抽样)
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.3, random_state=42, stratify=y
)

print(f"训练集大小: {X_train.shape[0]}")
print(f"测试集大小: {X_test.shape[0]}")

4. 构建与训练初始 KNN 模型

现在数据已经准备好了,让我们构建第一个 KNN 模型。我们将从最常用的默认值开始,即 n_neighbors=5。这意味着在预测一个新数据点时,模型会寻找距离它最近的 5 个邻居。

from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report

# 1. 初始化 KNN 分类器,设置邻居数量 k=5
knn = KNeighborsClassifier(n_neighbors=5)

# 2. 拟合模型(在 KNN 中,这一步主要是存储数据)
print("正在训练 KNN 模型...")
knn.fit(X_train, y_train)

# 3. 在测试集上进行预测
y_pred = knn.predict(X_test)

# 4. 计算并打印准确率
accuracy = accuracy_score(y_test, y_pred)
print(f"
测试集准确率 (k=5): {accuracy:.2f}")

# 5. 打印详细的分类报告(精确率、召回率、F1分数)
print("
分类报告:")
print(classification_report(y_test, y_pred))

在这里,你应该会看到不错的准确率(通常在 0.85 到 0.90 之间)。但是,你可能会问:“5 真的是最好的 K 值吗?” 让我们深入探讨这个问题。

5. 深入探讨:如何选择最佳的 K 值?

选择合适的 K 值是 KNN 算法中最重要的超参数调优步骤。K 值的选择直接影响模型的偏差和方差。

  • K 值过小(例如 K=1):模型变得非常复杂,会紧紧跟随训练数据的波动。这被称为过拟合。模型会对训练集中的噪声非常敏感,导致在测试集上表现不佳。边界会非常扭曲。
  • K 值过大(例如 K=100):模型变得过于简单,会忽略数据中的局部结构。这被称为欠拟合。决策边界会变得过于平滑,甚至可能将所有点都预测为多数类。

5.1 使用交叉验证寻找最优 K

为了找到最佳的 K 值,我们不能仅仅在测试集上尝试,因为这样会导致数据泄露(即我们为了调优而“训练”了测试集)。正确的方法是在训练集上进行 K 折交叉验证

让我们编写代码来测试 K 值从 1 到 20 的情况,并可视化结果。

from sklearn.model_selection import cross_val_score

# 设置 K 值的测试范围:1 到 20
k_range = range(1, 21)
cv_scores = []

# 进行 5 折交叉验证
for k in k_range:
    # 初始化 KNN
    knn = KNeighborsClassifier(n_neighbors=k)
    
    # 计算 5 折交叉验证的得分,返回的是一个包含 5 个准确率的数组
    scores = cross_val_score(knn, X_train, y_train, cv=5, scoring=‘accuracy‘)
    
    # 取平均分并存储
    cv_scores.append(scores.mean())

# 找到产生最高准确率的 K 值
# np.argmax 返回最大值的索引,我们需要在 k_range 中找到对应的值
best_k = list(k_range)[np.argmax(cv_scores)]
max_score = np.max(cv_scores)

print(f"交叉验证显示的最佳 K 值: {best_k}")
print(f"对应的平均交叉验证准确率: {max_score:.4f}")

# 绘制 K 值与准确率的关系图
plt.figure(figsize=(10, 6))
plt.plot(k_range, cv_scores, marker=‘o‘, linestyle=‘dashed‘, color=‘blue‘, markersize=8)
# 标记出最佳点
plt.scatter(best_k, max_score, color=‘red‘, s=100, zorder=5, label=f‘Best k={best_k}‘)

plt.title(‘K 值选择: 交叉验证准确率 vs K 值‘, fontsize=15)
plt.xlabel(‘K 值‘, fontsize=12)
plt.ylabel(‘交叉验证准确率‘, fontsize=12)
plt.xticks(list(k_range)) # 确保X轴显示所有整数
plt.grid(True)
plt.legend()
plt.show()

通过观察生成的图表,你会发现准确率通常会随着 K 值的增加先上升,然后趋于平缓或下降。我们的目标是找到那个“峰值”位置。

6. 进阶可视化:绘制决策边界

为了更直观地理解 KNN 是如何工作的,我们可以绘制出模型的决策边界。决策边界是特征空间中的一条线(或面),它将不同的类别区域划分开来。

这段代码会生成一个网格,并预测网格中每个点的类别,然后用颜色填充背景。

from matplotlib.colors import ListedColormap

def plot_decision_boundary(X, y, model, title):
    """
    绘制 KNN 分类器的决策边界
    """
    # 设置色彩映射
    cmap_light = ListedColormap([‘#FFAAAA‘, ‘#AAAAFF‘])
    cmap_bold = [‘#FF0000‘, ‘#0000FF‘]

    # 创建网格
    h = .02  # 网格步长
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))

    # 对网格中的每个点进行预测
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])

    # 将结果放入颜色图中
    Z = Z.reshape(xx.shape)
    plt.figure(figsize=(10, 6))
    plt.pcolormesh(xx, yy, Z, cmap=cmap_light, shading=‘auto‘)

    # 绘制训练数据点
    sns.scatterplot(x=X[:, 0], y=X[:, 1], hue=y, palette=cmap_bold, alpha=1.0, edgecolor="k")
    plt.xlim(xx.min(), xx.max())
    plt.ylim(yy.min(), yy.max())
    plt.title(title)
    plt.xlabel("Feature 1 (Scaled)")
    plt.ylabel("Feature 2 (Scaled)")
    plt.show()

# 使用前面选定的最佳 k 值重新训练模型(假设 best_k 已经算出)
# 如果上面的代码没跑,我们可以手动指定一个,比如 best_k = 15 (示例值)
final_knn = KNeighborsClassifier(n_neighbors=best_k)
final_knn.fit(X_train, y_train)

# 绘制决策边界
plot_decision_boundary(X_train, y_train, final_knn, f"KNN 决策边界 (k={best_k})")

在决策边界图中,你可以看到 KNN 如何在空间中划分出红色和蓝色的区域。如果 K 值较小,边界会像迷宫一样曲折;如果 K 值较大,边界会变得相对平滑。

7. KNN 算法实战中的陷阱与解决方案

在实际项目中,使用 KNN 并不总是像上面那样顺利。以下是我在实战中总结的一些常见问题和解决方案:

7.1 数据维度灾难

KNN 对高维数据非常敏感。随着特征数量的增加,数据点之间的距离会变得不再具有区分力(所有点之间的距离都趋于相等)。

  • 解决方案:如果特征非常多,建议先使用 PCA(主成分分析)特征选择 来降低维度,然后再应用 KNN。

7.2 计算成本高

KNN 是一种懒惰学习算法。预测阶段需要计算新数据点与所有训练数据点的距离。如果训练集有 100 万条数据,预测一个点就要计算 100 万次距离,这在生产环境中是非常慢的。

  • 解决方案

1. 使用近似算法,如 KD-TreeBall-Tree(Sklearn 的 algorithm 参数默认会自动选择 ‘auto‘,在大数据量时通常会使用这些优化结构)。

2. 考虑减少训练集的大小或使用其他模型(如随机森林)作为替代。

7.3 数据不平衡

如果数据集中某一类的样本数量远远多于另一类(例如 99:1),KNN 的多数投票机制会导致少数类的声音被淹没。

  • 解决方案:使用 INLINECODE0606516c 作为权重(INLINECODEa7779b81),这样最近的邻居拥有更大的投票权。或者在训练前进行过采样/欠采样处理。

8. 总结与下一步

在本文中,我们完整地走过了使用 Python 和 Sklearn 构建 KNN 模型的全过程。我们学习了:

  • 原理:KNN 通过距离最近的邻居进行预测,直观且易于理解。
  • 预处理数据归一化对于基于距离的算法至关重要。
  • 模型选择:通过交叉验证科学地选择 K 值,平衡过拟合与欠拟合。
  • 可视化:通过绘制决策边界直观地理解模型行为。
  • 挑战:了解到了维度灾难和计算成本对 KNN 的限制。

给你的一些实战建议:

你可以尝试将今天的知识应用到更著名的数据集上,比如 Iris(鸢尾花) 数据集或 Wisconsin Breast Cancer(威斯康星乳腺癌) 数据集。这些数据集在 Sklearn 中都可以直接加载。

你可以尝试修改代码中的 INLINECODEe6451675 参数为 INLINECODE75800a66,看看决策边界会发生什么变化?或者尝试使用 GridSearchCV 来同时搜索最佳 K 值和最佳权重参数的组合。

希望这篇文章能帮助你建立起对 KNN 算法的扎实理解。机器学习的旅程才刚刚开始,继续探索吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/33403.html
点赞
0.00 平均评分 (0% 分数) - 0