作者的话 :在前面几篇文章中,我们学习了决策树、随机森林和支持向量机。今天要介绍的**K近邻算法(K-Nearest Neighbors, KNN)**是机器学习中最简单、最直观的算法之一。它没有显式的训练过程,通过计算样本之间的距离来进行分类或回归。本文将带你深入理解KNN的原理、距离度量方法和实际应用!
一、KNN算法概述
1.1 什么是K近邻算法?
K近邻算法(K-Nearest Neighbors, KNN) 是一种基于实例的学习算法,也是最简单的机器学习算法之一。它的核心思想是:如果一个样本在特征空间中的k个最相似(即距离最近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。
KNN的特点:
- 简单直观,易于理解和实现
- 无需训练过程(惰性学习)
- 适用于分类和回归问题
- 对数据分布没有假设
- 计算复杂度高,存储开销大
1.2 KNN算法流程
KNN算法步骤:
- 计算距离:计算待分类样本与训练集中所有样本的距离
- 选择近邻:选取距离最近的k个样本
- 投票决策:根据k个近邻的类别进行投票(分类)或取平均值(回归)
1.3 KNN算法的优缺点
| 优点 | 缺点 |
|---|---|
| 算法简单,易于实现 | 计算复杂度高,预测速度慢 |
| 无需训练过程 | 存储开销大 |
| 对数据分布没有假设 | 对异常值敏感 |
| 可用于分类和回归 | 需要选择合适的k值 |
| 适合多分类问题 | 特征尺度影响大 |
二、距离度量方法
2.1 欧氏距离(Euclidean Distance)
欧氏距离是最常用的距离度量方法,表示两点之间的直线距离:
d(x,y) = sqrt(sum((x_i - y_i)^2))
欧氏距离适用于连续特征,对异常值敏感。
2.2 曼哈顿距离(Manhattan Distance)
曼哈顿距离表示两点在坐标轴上的绝对距离之和:
d(x,y) = sum(|x_i - y_i|)
曼哈顿距离适用于高维数据,对异常值不那么敏感。
2.3 闵可夫斯基距离(Minkowski Distance)
闵可夫斯基距离是欧氏距离和曼哈顿距离的推广:
d(x,y) = (sum(|x_i - y_i|^p))^(1/p)
当p=1时为曼哈顿距离,p=2时为欧氏距离。
2.4 距离度量对比
| 距离度量 | 公式特点 | 适用场景 |
|---|---|---|
| 欧氏距离 | 直线距离 | 连续特征,低维数据 |
| 曼哈顿距离 | 坐标轴距离之和 | 高维数据,网格状数据 |
| 切比雪夫距离 | 最大坐标差 | 棋盘距离 |
| 余弦相似度 | 向量夹角 | 文本分类,推荐系统 |
三、K值的选择
3.1 K值的影响
K值是KNN算法中最重要的超参数,直接影响模型的性能:
- K值较小:模型复杂,容易过拟合,对噪声敏感
- K值较大:模型简单,容易欠拟合,决策边界平滑
3.2 K值选择方法
交叉验证法:通过交叉验证选择使验证误差最小的k值
经验法则:k = sqrt(n),其中n为训练样本数
奇数原则:二分类时选择奇数k,避免平局
四、KNN的Python实现
4.1 使用sklearn的KNN
import numpy as np
import matplotlib.pyplot as plt
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import make_classification, load_iris
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report
# 生成数据
X, y = make_classification(n_samples=500, n_features=2, n_redundant=0,
n_informative=2, n_clusters_per_class=1,
random_state=42)
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,
random_state=42)
# 标准化(KNN对特征尺度敏感)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 创建KNN分类器
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train_scaled, y_train)
# 预测
y_pred = knn.predict(X_test_scaled)
print(f"KNN准确率: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred))
4.2 不同K值的性能对比
# 测试不同k值的性能
k_values = range(1, 31)
train_scores = []
test_scores = []
for k in k_values:
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train_scaled, y_train)
train_scores.append(knn.score(X_train_scaled, y_train))
test_scores.append(knn.score(X_test_scaled, y_test))
# 绘制对比图
plt.figure(figsize=(10, 6))
plt.plot(k_values, train_scores, label="Train", marker="o")
plt.plot(k_values, test_scores, label="Test", marker="s")
plt.xlabel("K Value")
plt.ylabel("Accuracy")
plt.title("KNN: Effect of K Value")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# 找到最优k值
best_k = list(k_values)[np.argmax(test_scores)]
print(f"最优k值: {best_k}, 准确率: {max(test_scores):.4f}")
4.3 不同距离度量对比
# 测试不同距离度量
distances = ["euclidean", "manhattan", "minkowski", "chebyshev"]
distance_scores = []
for dist in distances:
if dist == "minkowski":
knn = KNeighborsClassifier(n_neighbors=5, metric=dist, p=3)
else:
knn = KNeighborsClassifier(n_neighbors=5, metric=dist)
knn.fit(X_train_scaled, y_train)
score = knn.score(X_test_scaled, y_test)
distance_scores.append(score)
print(f"{dist}: {score:.4f}")
# 可视化
plt.figure(figsize=(8, 5))
plt.bar(distances, distance_scores, color=["skyblue", "lightcoral", "lightgreen", "gold"])
plt.ylabel("Accuracy")
plt.title("KNN with Different Distance Metrics")
plt.ylim([0.5, 1.0])
for i, v in enumerate(distance_scores):
plt.text(i, v + 0.01, f"{v:.3f}", ha="center")
plt.show()
4.4 决策边界可视化
# 可视化决策边界
def plot_decision_boundary(X, y, model, title):
h = 0.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, 8))
plt.contourf(xx, yy, Z, alpha=0.4, cmap=plt.cm.RdYlBu)
scatter = plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.RdYlBu,
edgecolors="black")
plt.title(title)
plt.colorbar(scatter)
plt.show()
# 不同k值的决策边界
for k in [1, 5, 15, 30]:
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train_scaled, y_train)
plot_decision_boundary(X_train_scaled, y_train, knn,
f"KNN Decision Boundary (k={k})")
五、实战案例:鸢尾花分类
5.1 加载数据
# 加载鸢尾花数据集
iris = load_iris()
X_iris = iris.data
y_iris = iris.target
print(f"特征名称: {iris.feature_names}")
print(f"类别: {iris.target_names}")
print(f"数据形状: {X_iris.shape}")
# 划分数据集
X_train_i, X_test_i, y_train_i, y_test_i = train_test_split(
X_iris, y_iris, test_size=0.3, random_state=42, stratify=y_iris)
# 标准化
scaler_i = StandardScaler()
X_train_i_scaled = scaler_i.fit_transform(X_train_i)
X_test_i_scaled = scaler_i.transform(X_test_i)
5.2 交叉验证选择最优K
# 使用交叉验证选择最优k
k_range = range(1, 31)
cv_scores = []
for k in k_range:
knn = KNeighborsClassifier(n_neighbors=k)
scores = cross_val_score(knn, X_train_i_scaled, y_train_i, cv=5)
cv_scores.append(scores.mean())
# 找到最优k
best_k = list(k_range)[np.argmax(cv_scores)]
print(f"最优k值: {best_k}")
print(f"交叉验证最高分: {max(cv_scores):.4f}")
# 绘制k值与准确率关系
plt.figure(figsize=(10, 6))
plt.plot(k_range, cv_scores, marker="o")
plt.xlabel("K Value")
plt.ylabel("Cross-Validation Accuracy")
plt.title("K Selection using Cross-Validation")
plt.axvline(x=best_k, color="r", linestyle="--",
label=f"Best k={best_k}")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
5.3 模型评估
# 使用最优k训练模型
best_knn = KNeighborsClassifier(n_neighbors=best_k)
best_knn.fit(X_train_i_scaled, y_train_i)
# 预测
y_pred_i = best_knn.predict(X_test_i_scaled)
# 评估
print(f"训练集准确率: {best_knn.score(X_train_i_scaled, y_train_i):.4f}")
print(f"测试集准确率: {best_knn.score(X_test_i_scaled, y_test_i):.4f}")
print("\n分类报告:")
print(classification_report(y_test_i, y_pred_i,
target_names=iris.target_names))
# 混淆矩阵
from sklearn.metrics import confusion_matrix
import seaborn as sns
cm = confusion_matrix(y_test_i, y_pred_i)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues",
xticklabels=iris.target_names,
yticklabels=iris.target_names)
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.title("Confusion Matrix - Iris Classification")
plt.show()
六、KNN回归(KNeighborsRegressor)
6.1 KNN回归原理
KNN不仅可以用于分类,也可以用于回归问题。在回归问题中,KNN通过计算k个最近邻的目标值的平均值(或加权平均值)来预测新样本的值。
6.2 KNN回归实现
from sklearn.neighbors import KNeighborsRegressor
from sklearn.datasets import make_regression
from sklearn.metrics import mean_squared_error, r2_score
# 生成回归数据
X_reg, y_reg = make_regression(n_samples=500, n_features=1, noise=20,
random_state=42)
X_train_r, X_test_r, y_train_r, y_test_r = train_test_split(
X_reg, y_reg, test_size=0.3, random_state=42)
# 标准化
scaler_r = StandardScaler()
X_train_r_scaled = scaler_r.fit_transform(X_train_r)
X_test_r_scaled = scaler_r.transform(X_test_r)
# 创建KNN回归器
knn_reg = KNeighborsRegressor(n_neighbors=5)
knn_reg.fit(X_train_r_scaled, y_train_r)
# 预测
y_pred_r = knn_reg.predict(X_test_r_scaled)
# 评估
print(f"MSE: {mean_squared_error(y_test_r, y_pred_r):.4f}")
print(f"R2: {r2_score(y_test_r, y_pred_r):.4f}")
# 可视化
plt.figure(figsize=(10, 6))
plt.scatter(X_test_r, y_test_r, color="blue", label="Actual", alpha=0.5)
plt.scatter(X_test_r, y_pred_r, color="red", label="Predicted", alpha=0.5)
# 排序以便绘制平滑曲线
sort_idx = np.argsort(X_test_r.ravel())
plt.plot(X_test_r[sort_idx], y_pred_r[sort_idx], "g-",
linewidth=2, label="KNN Regression")
plt.xlabel("X")
plt.ylabel("y")
plt.title("KNN Regression")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
七、KNN算法优化
7.1 KD树(KD-Tree)
KD树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。它通过构建二叉树来减少搜索最近邻时的计算量。
适用场景:低维数据(维度小于20)
# 使用KD树
knn_kdtree = KNeighborsClassifier(n_neighbors=5, algorithm="kd_tree")
knn_kdtree.fit(X_train_scaled, y_train)
print(f"KD树准确率: {knn_kdtree.score(X_test_scaled, y_test):.4f}")
7.2 球树(Ball Tree)
球树使用超球体而不是超矩形来划分空间,适用于高维数据。
适用场景:高维数据或数据分布不均匀的情况
# 使用球树
knn_balltree = KNeighborsClassifier(n_neighbors=5, algorithm="ball_tree")
knn_balltree.fit(X_train_scaled, y_train)
print(f"球树准确率: {knn_balltree.score(X_test_scaled, y_test):.4f}")
7.3 算法对比
| 算法 | 适用维度 | 时间复杂度 | 特点 |
|---|---|---|---|
| 暴力法 | 任意 | O(nd) | 简单,低维时可用 |
| KD树 | 低维(d<20) | O(log n) | 低维高效 |
| 球树 | 中高维 | O(log n) | 适合不均匀分布 |
八、特征缩放的重要性
8.1 为什么需要特征缩放?
KNN算法基于距离计算,如果不同特征的尺度差异很大,尺度大的特征会主导距离计算,导致算法偏向于这些特征。
# 演示特征缩放的重要性
from sklearn.datasets import make_classification
# 生成具有不同尺度的数据
X_scale, y_scale = make_classification(n_samples=500, n_features=2,
n_redundant=0, random_state=42)
# 放大第一个特征
X_scale[:, 0] *= 100
X_train_s, X_test_s, y_train_s, y_test_s = train_test_split(
X_scale, y_scale, test_size=0.3, random_state=42)
# 未缩放
knn_no_scale = KNeighborsClassifier(n_neighbors=5)
knn_no_scale.fit(X_train_s, y_train_s)
acc_no_scale = knn_no_scale.score(X_test_s, y_test_s)
# 标准化后
scaler_s = StandardScaler()
X_train_s_scaled = scaler_s.fit_transform(X_train_s)
X_test_s_scaled = scaler_s.transform(X_test_s)
knn_scaled = KNeighborsClassifier(n_neighbors=5)
knn_scaled.fit(X_train_s_scaled, y_train_s)
acc_scaled = knn_scaled.score(X_test_s_scaled, y_test_s)
print(f"未缩放准确率: {acc_no_scale:.4f}")
print(f"标准化后准确率: {acc_scaled:.4f}")
print(f"提升: {acc_scaled - acc_no_scale:.4f}")
九、KNN与其他算法对比
9.1 分类算法对比
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
# 定义模型
models = {
"KNN": KNeighborsClassifier(n_neighbors=5),
"Logistic Regression": LogisticRegression(max_iter=1000),
"Decision Tree": DecisionTreeClassifier(max_depth=5),
"Random Forest": RandomForestClassifier(n_estimators=50),
"SVM": SVC(kernel="rbf")
}
# 训练和评估
results = []
for name, model in models.items():
model.fit(X_train_scaled, y_train)
train_acc = model.score(X_train_scaled, y_train)
test_acc = model.score(X_test_scaled, y_test)
results.append({"Model": name, "Train": train_acc, "Test": test_acc})
import pandas as pd
results_df = pd.DataFrame(results)
print(results_df.to_string(index=False))
9.2 可视化对比
# 绘制对比图
fig, ax = plt.subplots(figsize=(12, 6))
x = np.arange(len(results_df))
width = 0.35
bars1 = ax.bar(x - width/2, results_df["Train"], width,
label="Train", color="skyblue")
bars2 = ax.bar(x + width/2, results_df["Test"], width,
label="Test", color="lightcoral")
ax.set_ylabel("Accuracy")
ax.set_title("Model Comparison")
ax.set_xticks(x)
ax.set_xticklabels(results_df["Model"], rotation=45, ha="right")
ax.legend()
ax.set_ylim([0.5, 1.0])
ax.grid(True, alpha=0.3, axis="y")
# 添加数值标签
for bars in [bars1, bars2]:
for bar in bars:
height = bar.get_height()
ax.annotate(f"{height:.3f}",
xy=(bar.get_x() + bar.get_width() / 2, height),
xytext=(0, 3), textcoords="offset points",
ha="center", va="bottom", fontsize=8)
plt.tight_layout()
plt.show()
十、总结与学习建议
10.1 核心要点回顾
- 算法思想:通过计算样本间的距离,选择最近的k个邻居进行投票或平均
- 距离度量:欧氏距离、曼哈顿距离、闵可夫斯基距离等
- K值选择:通过交叉验证选择最优k值,避免过拟合或欠拟合
- 特征缩放:必须进行标准化或归一化,避免尺度影响
- 算法优化:使用KD树或球树加速最近邻搜索
10.2 适用场景
- 数据量小:训练样本较少时
- 数据分布复杂:非线性可分数据
- 解释性要求高:算法简单,易于理解
- 多分类问题:天然支持多分类
10.3 不适用场景
- 大规模数据:计算和存储开销大
- 高维稀疏数据:维度灾难问题
- 实时性要求高:预测速度慢
- 特征维度高:距离度量失效
10.4 进阶学习
- 加权KNN:根据距离给邻居赋予不同权重
- 距离度量学习:学习最优的距离度量方法
- 近似最近邻:使用局部敏感哈希(LSH)等算法加速
- 集成KNN:结合多个KNN模型提升性能
下一篇预告:【第12篇】朴素贝叶斯分类器:基于概率的分类方法
本文为系列第11篇,深入讲解了KNN算法的原理、距离度量方法和实际应用。有任何问题欢迎在评论区交流!
标签:#KNN #K近邻 #机器学习 #分类算法 #Python #人工智能 #教程