|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
K最近邻(K-Nearest Neighbors,简称KNN)算法是一种基本的分类与回归方法,是机器学习中最简单直观的算法之一。1967年由Cover和Hart提出,虽然已经过去了半个多世纪,但KNN算法因其简单、有效和易于理解的特点,至今仍被广泛应用于各种分类和回归问题中。KNN是一种非参数的、基于实例的学习算法,它不需要训练过程,而是直接使用训练数据进行预测。在本文中,我们将通过Python的scikit-learn库,深入探讨KNN算法的工作原理、实现步骤、参数调优、模型评估以及在实际项目中的应用案例,帮助读者全面掌握这一基础但强大的机器学习技术。
K最近邻算法的工作原理
算法基本概念
K最近邻算法的核心思想非常直观:给定一个训练数据集,对新的输入实例,在训练集中找到与该实例最邻近的K个实例,如果这K个实例的多数属于某个类别,则将该输入实例分类到这个类别中。简单来说,就是”物以类聚,人以群分”的思想。
KNN算法可以用于分类和回归:
• 在分类任务中,输出是K个最近邻样本中出现最多的类别(多数表决)。
• 在回归任务中,输出是K个最近邻样本的平均值。
距离度量方法
在KNN算法中,”最近”是通过距离度量来定义的。常用的距离度量方法有:
1. 欧氏距离(Euclidean Distance):最常用的距离度量,在二维空间中就是两点之间的直线距离。
对于n维空间中的两个点\(x = (x_1, x_2, ..., x_n)\)和\(y = (y_1, y_2, ..., y_n)\),欧氏距离定义为:
\(d(x, y) = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2}\)
1. 曼哈顿距离(Manhattan Distance):在二维空间中,两点之间的曼哈顿距离是它们在坐标轴上的绝对差值之和。
\(d(x, y) = \sum_{i=1}^{n}|x_i - y_i|\)
1. 闵可夫斯基距离(Minkowski Distance):是欧氏距离和曼哈顿距离的推广。
\(d(x, y) = (\sum_{i=1}^{n}|x_i - y_i|^p)^{1/p}\)
当p=1时,闵可夫斯基距离就是曼哈顿距离;当p=2时,就是欧氏距离。
1. 余弦相似度(Cosine Similarity):衡量两个向量之间的夹角,常用于文本分类等领域。
\(\text{similarity} = \cos(\theta) = \frac{x \cdot y}{\|x\| \cdot \|y\|} = \frac{\sum_{i=1}^{n}x_i y_i}{\sqrt{\sum_{i=1}^{n}x_i^2} \sqrt{\sum_{i=1}^{n}y_i^2}}\)
决策规则
在KNN算法中,决策规则通常有以下几种:
1. 多数表决(Majority Voting):在分类问题中,最常见的决策规则是多数表决,即选择K个最近邻中出现次数最多的类别作为预测结果。
2. 加权多数表决(Weighted Majority Voting):考虑到距离越近的样本对预测结果的影响应该越大,可以为每个近邻样本分配一个权重,通常是距离的倒数,然后进行加权表决。
3. 回归决策:在回归问题中,通常采用K个最近邻样本的平均值作为预测结果,也可以使用加权平均,权重与距离成反比。
多数表决(Majority Voting):在分类问题中,最常见的决策规则是多数表决,即选择K个最近邻中出现次数最多的类别作为预测结果。
加权多数表决(Weighted Majority Voting):考虑到距离越近的样本对预测结果的影响应该越大,可以为每个近邻样本分配一个权重,通常是距离的倒数,然后进行加权表决。
回归决策:在回归问题中,通常采用K个最近邻样本的平均值作为预测结果,也可以使用加权平均,权重与距离成反比。
使用scikit-learn实现KNN算法的步骤
scikit-learn是Python中最流行的机器学习库之一,提供了简单高效的KNN实现。下面我们将介绍如何使用scikit-learn实现KNN算法。
数据准备
首先,我们需要准备数据。这里我们使用scikit-learn自带的鸢尾花(Iris)数据集作为示例:
- # 导入必要的库
- import numpy as np
- import matplotlib.pyplot as plt
- from sklearn import datasets
- from sklearn.model_selection import train_test_split
- from sklearn.preprocessing import StandardScaler
- from sklearn.neighbors import KNeighborsClassifier
- from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
- # 加载鸢尾花数据集
- iris = datasets.load_iris()
- X = iris.data
- y = iris.target
- # 查看数据集信息
- print("特征名称:", iris.feature_names)
- print("目标类别:", iris.target_names)
- print("数据集大小:", X.shape)
- print("类别分布:", np.bincount(y))
- # 将数据集分为训练集和测试集
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
- # 特征标准化
- scaler = StandardScaler()
- X_train_scaled = scaler.fit_transform(X_train)
- X_test_scaled = scaler.transform(X_test)
复制代码
模型构建
使用scikit-learn的KNeighborsClassifier类构建KNN模型:
- # 创建KNN分类器,设置K=3
- knn = KNeighborsClassifier(n_neighbors=3)
- # 查看模型参数
- print("KNN模型参数:", knn.get_params())
复制代码
模型训练
KNN是一种”懒惰学习”算法,实际上没有显式的训练过程,训练阶段主要是存储训练数据:
- # "训练"模型(实际上是存储数据)
- knn.fit(X_train_scaled, y_train)
复制代码
预测
使用训练好的模型对测试数据进行预测:
- # 对测试集进行预测
- y_pred = knn.predict(X_test_scaled)
- # 输出预测结果
- print("预测结果:", y_pred)
- print("真实标签:", y_test)
- # 计算准确率
- accuracy = accuracy_score(y_test, y_pred)
- print(f"模型准确率: {accuracy:.4f}")
复制代码
KNN算法参数调优
KNN算法有几个关键参数需要调优,以获得最佳性能。下面我们将介绍这些参数及其调优方法。
K值的选择
K值是KNN算法中最重要的参数,它决定了预测时考虑的邻居数量。
• K值太小:模型容易受到噪声数据的影响,导致过拟合。
• K值太大:模型会忽略样本中的局部模式,导致欠拟合。
我们可以通过交叉验证来选择最佳的K值:
- from sklearn.model_selection import cross_val_score
- # 尝试不同的K值
- k_values = list(range(1, 31))
- cv_scores = []
- for k in k_values:
- knn = KNeighborsClassifier(n_neighbors=k)
- scores = cross_val_score(knn, X_train_scaled, y_train, cv=10, scoring='accuracy')
- cv_scores.append(scores.mean())
- # 找到最佳K值
- best_k = k_values[np.argmax(cv_scores)]
- print(f"最佳K值: {best_k}")
- print(f"最高交叉验证准确率: {max(cv_scores):.4f}")
- # 绘制K值与准确率的关系图
- plt.figure(figsize=(10, 6))
- plt.plot(k_values, cv_scores)
- plt.xlabel('K值')
- plt.ylabel('交叉验证准确率')
- plt.title('K值与模型性能的关系')
- plt.grid(True)
- plt.show()
复制代码
距离度量参数
scikit-learn的KNeighborsClassifier提供了多种距离度量方法,通过metric参数设置:
- # 尝试不同的距离度量
- metrics = ['euclidean', 'manhattan', 'minkowski']
- for metric in metrics:
- knn = KNeighborsClassifier(n_neighbors=best_k, metric=metric)
- scores = cross_val_score(knn, X_train_scaled, y_train, cv=10, scoring='accuracy')
- print(f"{metric} 距离的平均准确率: {scores.mean():.4f}")
复制代码
权重参数
weights参数控制邻居的投票权重:
• ‘uniform’:所有邻居的权重相同(默认)。
• ‘distance’:权重与距离成反比,距离越近权重越大。
- # 尝试不同的权重方案
- weights = ['uniform', 'distance']
- for weight in weights:
- knn = KNeighborsClassifier(n_neighbors=best_k, weights=weight)
- scores = cross_val_score(knn, X_train_scaled, y_train, cv=10, scoring='accuracy')
- print(f"{weight} 权重的平均准确率: {scores.mean():.4f}")
复制代码
使用GridSearchCV进行综合参数调优
我们可以使用GridSearchCV来系统地搜索最佳参数组合:
- from sklearn.model_selection import GridSearchCV
- # 定义参数网格
- param_grid = {
- 'n_neighbors': list(range(1, 31)),
- 'weights': ['uniform', 'distance'],
- 'metric': ['euclidean', 'manhattan', 'minkowski']
- }
- # 创建KNN分类器
- knn = KNeighborsClassifier()
- # 创建网格搜索对象
- grid_search = GridSearchCV(knn, param_grid, cv=10, scoring='accuracy', n_jobs=-1)
- # 执行网格搜索
- grid_search.fit(X_train_scaled, y_train)
- # 输出最佳参数和对应的准确率
- print(f"最佳参数: {grid_search.best_params_}")
- print(f"最高交叉验证准确率: {grid_search.best_score_:.4f}")
- # 使用最佳参数的模型进行预测
- best_knn = grid_search.best_estimator_
- y_pred = best_knn.predict(X_test_scaled)
- print(f"测试集准确率: {accuracy_score(y_test, y_pred):.4f}")
复制代码
模型评估方法
在机器学习中,评估模型性能是非常重要的一步。下面我们将介绍几种常用的KNN模型评估方法。
准确率
准确率是最直观的评估指标,表示正确预测的样本比例:
- # 计算准确率
- accuracy = accuracy_score(y_test, y_pred)
- print(f"模型准确率: {accuracy:.4f}")
复制代码
混淆矩阵
混淆矩阵提供了更详细的分类结果信息,显示每个类别的正确和错误预测数量:
- # 计算混淆矩阵
- cm = confusion_matrix(y_test, y_pred)
- print("混淆矩阵:")
- print(cm)
- # 可视化混淆矩阵
- plt.figure(figsize=(8, 6))
- plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
- plt.title('混淆矩阵')
- plt.colorbar()
- tick_marks = np.arange(len(iris.target_names))
- plt.xticks(tick_marks, iris.target_names, rotation=45)
- plt.yticks(tick_marks, iris.target_names)
- # 在混淆矩阵每个单元格上添加数值
- thresh = cm.max() / 2.
- for i in range(cm.shape[0]):
- for j in range(cm.shape[1]):
- plt.text(j, i, format(cm[i, j], 'd'),
- horizontalalignment="center",
- color="white" if cm[i, j] > thresh else "black")
- plt.tight_layout()
- plt.ylabel('真实标签')
- plt.xlabel('预测标签')
- plt.show()
复制代码
精确率、召回率和F1分数
对于不平衡数据集,准确率可能不是最好的评估指标。我们可以使用精确率、召回率和F1分数:
- # 计算分类报告
- report = classification_report(y_test, y_pred, target_names=iris.target_names)
- print("分类报告:")
- print(report)
- # 从分类报告中提取各个指标
- from sklearn.metrics import precision_score, recall_score, f1_score
- precision = precision_score(y_test, y_pred, average='weighted')
- recall = recall_score(y_test, y_pred, average='weighted')
- f1 = f1_score(y_test, y_pred, average='weighted')
- print(f"加权精确率: {precision:.4f}")
- print(f"加权召回率: {recall:.4f}")
- print(f"加权F1分数: {f1:.4f}")
复制代码
ROC曲线和AUC值
ROC(Receiver Operating Characteristic)曲线和AUC(Area Under the Curve)值是评估二分类模型性能的常用工具。对于多分类问题,我们可以使用”一对多”(One-vs-Rest)方法为每个类别绘制ROC曲线:
- from sklearn.preprocessing import label_binarize
- from sklearn.metrics import roc_curve, auc
- from scipy import interp
- from itertools import cycle
- # 将标签二值化
- y_test_bin = label_binarize(y_test, classes=[0, 1, 2])
- n_classes = y_test_bin.shape[1]
- # 获取每个类别的预测概率
- y_score = best_knn.predict_proba(X_test_scaled)
- # 计算每个类别的ROC曲线和AUC值
- fpr = dict()
- tpr = dict()
- roc_auc = dict()
- for i in range(n_classes):
- fpr[i], tpr[i], _ = roc_curve(y_test_bin[:, i], y_score[:, i])
- roc_auc[i] = auc(fpr[i], tpr[i])
- # 计算微观平均ROC曲线和AUC值
- fpr["micro"], tpr["micro"], _ = roc_curve(y_test_bin.ravel(), y_score.ravel())
- roc_auc["micro"] = auc(fpr["micro"], tpr["micro"])
- # 计算宏观平均ROC曲线和AUC值
- # 首先聚合所有假阳性率
- all_fpr = np.unique(np.concatenate([fpr[i] for i in range(n_classes)]))
- # 然后在这些点上插值所有ROC曲线
- mean_tpr = np.zeros_like(all_fpr)
- for i in range(n_classes):
- mean_tpr += interp(all_fpr, fpr[i], tpr[i])
- # 最后平均并计算AUC
- mean_tpr /= n_classes
- fpr["macro"] = all_fpr
- tpr["macro"] = mean_tpr
- roc_auc["macro"] = auc(fpr["macro"], tpr["macro"])
- # 绘制所有ROC曲线
- plt.figure(figsize=(10, 8))
- plt.plot(fpr["micro"], tpr["micro"],
- label=f'微观平均 ROC曲线 (AUC = {roc_auc["micro"]:.2f})',
- color='deeppink', linestyle=':', linewidth=4)
- plt.plot(fpr["macro"], tpr["macro"],
- label=f'宏观平均 ROC曲线 (AUC = {roc_auc["macro"]:.2f})',
- color='navy', linestyle=':', linewidth=4)
- colors = cycle(['aqua', 'darkorange', 'cornflowerblue'])
- for i, color in zip(range(n_classes), colors):
- plt.plot(fpr[i], tpr[i], color=color, lw=2,
- label=f'{iris.target_names[i]}的ROC曲线 (AUC = {roc_auc[i]:.2f})')
- plt.plot([0, 1], [0, 1], 'k--', lw=2)
- plt.xlim([0.0, 1.0])
- plt.ylim([0.0, 1.05])
- plt.xlabel('假阳性率')
- plt.ylabel('真阳性率')
- plt.title('多类别ROC曲线')
- plt.legend(loc="lower right")
- plt.show()
复制代码
实际项目应用案例
现在,让我们通过一个实际的项目案例来展示KNN算法的应用。我们将使用一个手写数字识别的数据集,展示从数据预处理到模型评估的完整流程。
案例背景和数据介绍
手写数字识别是机器学习中的经典问题,目标是识别0-9的手写数字。我们将使用scikit-learn中自带的手写数字数据集。
- # 导入必要的库
- import numpy as np
- import matplotlib.pyplot as plt
- from sklearn import datasets
- from sklearn.model_selection import train_test_split, GridSearchCV
- from sklearn.preprocessing import StandardScaler
- from sklearn.neighbors import KNeighborsClassifier
- from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
- from sklearn.decomposition import PCA
- # 加载手写数字数据集
- digits = datasets.load_digits()
- X = digits.data
- y = digits.target
- # 查看数据集信息
- print("特征数量:", X.shape[1])
- print("样本数量:", X.shape[0])
- print("类别数量:", len(np.unique(y)))
- # 显示一些手写数字图像
- fig, axes = plt.subplots(2, 5, figsize=(10, 4))
- for i, ax in enumerate(axes.ravel()):
- ax.imshow(digits.images[i], cmap='binary')
- ax.set_title(f"标签: {digits.target[i]}")
- ax.axis('off')
- plt.tight_layout()
- plt.show()
复制代码
数据预处理
手写数字数据集包含8x8像素的图像,共64个特征。我们可以进行以下预处理步骤:
- # 将数据集分为训练集和测试集
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
- # 特征标准化
- scaler = StandardScaler()
- X_train_scaled = scaler.fit_transform(X_train)
- X_test_scaled = scaler.transform(X_test)
- # 使用PCA进行降维(可选)
- pca = PCA(n_components=0.95) # 保留95%的方差
- X_train_pca = pca.fit_transform(X_train_scaled)
- X_test_pca = pca.transform(X_test_scaled)
- print(f"原始特征数量: {X_train_scaled.shape[1]}")
- print(f"PCA降维后特征数量: {X_train_pca.shape[1]}")
复制代码
模型构建与训练
现在,我们构建KNN模型并使用网格搜索进行参数调优:
- # 定义参数网格
- param_grid = {
- 'n_neighbors': list(range(1, 20)),
- 'weights': ['uniform', 'distance'],
- 'metric': ['euclidean', 'manhattan']
- }
- # 创建KNN分类器
- knn = KNeighborsClassifier()
- # 创建网格搜索对象
- grid_search = GridSearchCV(knn, param_grid, cv=5, scoring='accuracy', n_jobs=-1)
- # 使用原始数据执行网格搜索
- print("使用原始数据进行网格搜索...")
- grid_search.fit(X_train_scaled, y_train)
- print(f"最佳参数: {grid_search.best_params_}")
- print(f"最高交叉验证准确率: {grid_search.best_score_:.4f}")
- # 使用最佳参数的模型进行预测
- best_knn = grid_search.best_estimator_
- y_pred = best_knn.predict(X_test_scaled)
- print(f"测试集准确率: {accuracy_score(y_test, y_pred):.4f}")
- # 使用PCA降维后的数据执行网格搜索
- print("\n使用PCA降维后的数据进行网格搜索...")
- grid_search_pca = GridSearchCV(knn, param_grid, cv=5, scoring='accuracy', n_jobs=-1)
- grid_search_pca.fit(X_train_pca, y_train)
- print(f"最佳参数: {grid_search_pca.best_params_}")
- print(f"最高交叉验证准确率: {grid_search_pca.best_score_:.4f}")
- # 使用最佳参数的模型进行预测
- best_knn_pca = grid_search_pca.best_estimator_
- y_pred_pca = best_knn_pca.predict(X_test_pca)
- print(f"测试集准确率: {accuracy_score(y_test, y_pred_pca):.4f}")
复制代码
模型评估
让我们对使用原始数据和PCA降维数据的两个模型进行详细评估:
- # 评估使用原始数据的模型
- print("使用原始数据的模型评估:")
- print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
- # 混淆矩阵
- cm = confusion_matrix(y_test, y_pred)
- plt.figure(figsize=(10, 8))
- plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
- plt.title('混淆矩阵(原始数据)')
- plt.colorbar()
- tick_marks = np.arange(10)
- plt.xticks(tick_marks, range(10))
- plt.yticks(tick_marks, range(10))
- # 在混淆矩阵每个单元格上添加数值
- thresh = cm.max() / 2.
- for i in range(cm.shape[0]):
- for j in range(cm.shape[1]):
- plt.text(j, i, format(cm[i, j], 'd'),
- horizontalalignment="center",
- color="white" if cm[i, j] > thresh else "black")
- plt.tight_layout()
- plt.ylabel('真实标签')
- plt.xlabel('预测标签')
- plt.show()
- # 分类报告
- report = classification_report(y_test, y_pred)
- print("分类报告:")
- print(report)
- # 评估使用PCA降维数据的模型
- print("\n使用PCA降维数据的模型评估:")
- print(f"准确率: {accuracy_score(y_test, y_pred_pca):.4f}")
- # 混淆矩阵
- cm_pca = confusion_matrix(y_test, y_pred_pca)
- plt.figure(figsize=(10, 8))
- plt.imshow(cm_pca, interpolation='nearest', cmap=plt.cm.Blues)
- plt.title('混淆矩阵(PCA降维数据)')
- plt.colorbar()
- tick_marks = np.arange(10)
- plt.xticks(tick_marks, range(10))
- plt.yticks(tick_marks, range(10))
- # 在混淆矩阵每个单元格上添加数值
- thresh = cm_pca.max() / 2.
- for i in range(cm_pca.shape[0]):
- for j in range(cm_pca.shape[1]):
- plt.text(j, i, format(cm_pca[i, j], 'd'),
- horizontalalignment="center",
- color="white" if cm_pca[i, j] > thresh else "black")
- plt.tight_layout()
- plt.ylabel('真实标签')
- plt.xlabel('预测标签')
- plt.show()
- # 分类报告
- report_pca = classification_report(y_test, y_pred_pca)
- print("分类报告:")
- print(report_pca)
复制代码
结果解释与应用
通过上述实验,我们可以得出以下结论:
1. 模型性能:KNN算法在手写数字识别任务上表现良好,使用原始数据和PCA降维数据都能达到较高的准确率。
2. 降维的影响:PCA降维可以显著减少特征数量(从64个减少到约29个),同时保持较高的准确率。这有助于减少计算复杂度和存储需求。
3. 参数选择:通过网格搜索,我们找到了最佳的K值、权重方法和距离度量方法。这些参数对模型性能有重要影响。
4. 错误分析:从混淆矩阵中可以看出,某些数字对(如4和9,3和8)更容易混淆,这可能是因为它们在形状上相似。
模型性能:KNN算法在手写数字识别任务上表现良好,使用原始数据和PCA降维数据都能达到较高的准确率。
降维的影响:PCA降维可以显著减少特征数量(从64个减少到约29个),同时保持较高的准确率。这有助于减少计算复杂度和存储需求。
参数选择:通过网格搜索,我们找到了最佳的K值、权重方法和距离度量方法。这些参数对模型性能有重要影响。
错误分析:从混淆矩阵中可以看出,某些数字对(如4和9,3和8)更容易混淆,这可能是因为它们在形状上相似。
在实际应用中,我们可以根据具体需求选择是否使用降维,以及如何调整KNN的参数。例如,如果计算资源有限,可以选择PCA降维;如果对准确率要求极高,可以使用原始数据并进一步优化参数。
KNN算法的优缺点
优点
1. 简单直观:KNN算法原理简单,易于理解和实现。
2. 无需训练:KNN是一种懒惰学习算法,不需要显式的训练过程。
3. 适应性强:可以用于分类和回归问题,能够处理多分类问题。
4. 对数据分布没有假设:不像许多其他算法那样对数据分布有先验假设。
5. 适合多模态数据:能够处理具有多个类别的数据集。
缺点
1. 计算复杂度高:预测时需要计算与所有训练样本的距离,当训练集很大时,计算成本高。
2. 内存需求大:需要存储整个训练数据集,内存消耗大。
3. 对特征尺度敏感:不同尺度的特征会影响距离计算,通常需要进行特征标准化。
4. 维度灾难:在高维空间中,所有点之间的距离趋于相等,导致算法性能下降。
5. 对噪声和异常值敏感:噪声和异常值会影响K近邻的选择,从而影响预测结果。
总结与展望
K最近邻算法作为一种基础但强大的机器学习技术,因其简单性和有效性在许多领域得到了广泛应用。通过本文,我们详细介绍了KNN算法的工作原理、实现步骤、参数调优、模型评估以及在实际项目中的应用案例。
KNN算法的核心优势在于其简单直观,不需要复杂的训练过程,能够适应各种数据分布。然而,它也存在计算复杂度高、内存需求大等缺点,特别是在处理大规模数据集时。
在实际应用中,我们可以通过以下方式优化KNN算法:
1. 特征选择和降维:使用PCA、LDA等技术减少特征数量,缓解维度灾难问题。
2. 高效的索引结构:使用KD树、球树等数据结构加速近邻搜索。
3. 并行计算:利用多核CPU或GPU并行计算距离。
4. 近似最近邻搜索:牺牲一定的准确性换取计算效率的提升。
未来,随着大数据和高维数据的普及,KNN算法的研究可能会集中在以下几个方面:
1. 高效的近似算法:开发更高效的近似最近邻搜索算法,在保持较高准确率的同时提高计算效率。
2. 自适应距离度量:研究能够根据数据特点自动调整的距离度量方法。
3. 与其他算法的结合:将KNN与深度学习等其他技术结合,发挥各自的优势。
4. 增量学习:开发能够支持增量学习的KNN变体,适应动态变化的数据环境。
总之,尽管KNN算法已经存在了半个多世纪,但它仍然是一个活跃的研究领域,并在实际应用中发挥着重要作用。通过深入理解KNN算法的原理和实现,我们可以更好地应用这一技术解决实际问题。 |
|