跳至主要内容

使用 scikit-learn 的 K 近邻(KNN)分类

本文介绍何时以及如何使用 scikit-learn 的 k 近邻分类,重点讲解概念、工作流程与示例。同时涵盖距离度量以及如何通过交叉验证选择最佳的 k 值。
· 14分钟 阅读

使用 AI 探索

本教程将介绍 k 近邻(kNN)算法的概念、工作流程与示例。它是一种常用于分类与回归的流行监督学习模型,有助于理解距离函数、投票机制以及超参数优化。

为更好地理解本教程,您应具备 Python 基础知识,并有使用 DataFrame 的经验。熟悉 scikit-learn 的语法也会有所帮助。kNN 常与无监督方法 k-Means 聚类混淆。如果您对此感兴趣,请参阅使用 scikit-learn 在 Python 中进行 k-Means 聚类。您也可以立即注册我们的 Python 机器学习课程,更深入学习 kNN。

通过这个动手练习练习(KNN)分类。

尽管 kNN 可用于分类与回归,本文将专注于构建分类模型。机器学习中的分类是一种监督学习任务,旨在为给定输入数据点预测一个类别标签。算法在带标签的数据集上训练,使用输入特征学习输入与相应类别标签之间的映射关系。我们可以用训练好的模型来预测新的、未见过的数据。您也可以打开这个DataLab 工作簿来运行本教程中的代码。

K 近邻算法概览

kNN 算法可视作一种投票系统:在特征空间中,由最近的“k”(k 为整数)个邻居的多数类别来决定新数据点的类别。想象一个只有几百位居民的小村庄,您需要决定投票给哪个政党。为此,您可能会去询问最近的邻居他们支持哪个政党。如果最近的 k 位邻居中多数支持 A 党,那么您很可能也会投 A 党。这与 kNN 的工作方式类似:由最近 k 个邻居的多数类别来决定新数据点的类别。

我们再用另一个例子深入理解。假设您有关于水果的数据,具体是葡萄和梨。您为每个水果记录了“圆度得分”和“直径”,并将它们绘制在图上。如果有人给您一个新水果,您也可以把它绘制到图上,然后测量到最近 k(一个数值)个点的距离,以判断它是什么水果。在下例中,若选择测量 3 个点,最近的 3 个点都是梨,因此我们可以说 100% 确定它是梨。若选择测量最近的 4 个点,其中 3 个是梨、1 个是葡萄,则可以说有 75% 的把握它是梨。我们将在后文介绍如何选择最佳的 k 值,以及测量距离的不同方式。image2.png

数据集

为进一步说明 kNN 算法,我们来做一个数据科学家工作中常见的案例。假设您是某电商平台的数据科学家,任务是识别欺诈交易。目前您只有以下特征:

  • dist_from_home:用户住址与发生交易地点之间的距离。
  • purchase_price_ratio:本次交易中商品价格与该用户历史订单价格中位数之比。

数据包含 39 条观测,每条对应一笔交易。在本教程中,我们以变量 df 给出了数据集,其外观如下:

 
0
2.1
6.4
1
1
3.8
2.2
1
2
15.7
4.4
1
3
26.7
4.6
1
4
10.7
4.9
1

k 近邻的工作流程

为了拟合并训练该模型,我们将参考机器学习工作流程信息图。

Infographic.png

下载机器学习工作流程信息图

不过,由于我们的数据相当干净,并不会执行每一步。我们将进行以下步骤:

  • 特征工程
  • 划分数据
  • 训练模型
  • 超参数调优
  • 评估模型性能

数据可视化

让我们用 Matplotlib 来可视化数据,将两个特征绘制为散点图。

sns.scatterplot(x=df['dist_from_home'],y=df['purchase_price_ratio'], hue=df['fraud'])

可以看到,这些交易之间存在明显差异:与客户历史订单中位数相比,欺诈交易的金额更高。关于“离家距离”的趋势不太容易解读,非欺诈交易通常更接近住址,但也存在一些离群点。

image4.png

数据归一化与划分

训练任何机器学习模型时,重要的一步是将数据划分为训练集和测试集。训练集用于拟合模型。算法利用训练数据学习特征与目标之间的关系,寻找可用于对新、未见数据进行预测的模式。测试集用于评估模型性能,即用模型在测试集上进行预测,并将预测结果与真实目标值进行比较。

在训练 kNN 分类器时,务必对特征进行归一化。这是因为 kNN 通过测量点与点之间的距离来工作。默认使用欧氏距离,即两个点之间差值的平方和再开方。在我们的例子中,purchase_price_ratio 的范围在 0 到 8 之间,而 dist_from_home 要大得多。如果不进行归一化,距离计算会被 dist_from_home 的较大数值严重主导。

我们应在将数据划分为训练集和测试集之后再进行归一化。这样可以防止“数据泄露”:若一次性对全部数据归一化,模型将获得关于测试集的额外信息。

以下代码将数据划分为训练/测试集,并使用scikit-learn 的 StandardScaler 进行归一化。我们先对训练数据调用 .fit_transform(),使缩放器拟合训练数据的均值与标准差。然后对测试数据调用 .transform(),使用先前学到的参数进行变换。

# Split the data into features (X) and target (y)
X = df.drop('fraud', axis=1)
y = df['fraud']

# Split the data into training and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# Scale the features using StandardScaler
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

模型拟合与评估

现在可以训练模型了。这里我们先固定 k 为 3,稍后再进行优化。我们先创建一个 kNN 模型实例,然后将其拟合到训练数据上。我们同时传入特征与目标变量,以便模型学习。

knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)

模型已训练完成!我们可以对测试集进行预测,稍后用来对模型打分。

y_pred = knn.predict(X_test)

评估该模型最简单的方法是使用准确率。我们将预测与测试集中的真实值进行比对,统计模型预测正确的数量。

accuracy = accuracy_score(y_test, y_pred)
print("Accuracy:", accuracy)
Accuracy: 0.875

这是个不错的成绩!不过,我们也许可以通过优化 k 的取值来进一步提升。

使用交叉验证获取最佳 k 值

很遗憾,没有“魔法公式”可以直接得到最佳 k。我们需要遍历多个取值,再结合判断进行选择。

在下面的代码中,我们选取一组 k 的取值范围,并创建一个空列表用于存储结果。我们使用交叉验证来获取准确率分数,这样就不需要再单独划分训练集与测试集,但仍需要对数据进行缩放。随后我们遍历各个取值,并将分数加入列表。

为实现交叉验证,我们使用scikit-learn 的 cross_val_score。我们传入一个 kNN 模型实例,以及数据与折数。在下方代码中使用了 5 折,这意味着模型将数据分为 5 份等大小的子集,每次用其中 4 份训练、1 份测试。它会对每一份循环并给出准确率,最后我们取平均以选择最佳模型。

k_values = [i for i in range (1,31)]
scores = []

scaler = StandardScaler()
X = scaler.fit_transform(X)

for k in k_values:
    knn = KNeighborsClassifier(n_neighbors=k)
    score = cross_val_score(knn, X, y, cv=5)
    scores.append(np.mean(score))

我们可以用下面的代码绘制结果

sns.lineplot(x = k_values, y = scores, marker = 'o')
plt.xlabel("K Values")
plt.ylabel("Accuracy Score")

从图中可见,k = 9、10、11、12 和 13 的准确率都略低于 95%。既然它们并列为最佳,通常建议选取较小的 k 值。这是因为较大的 k 值会纳入更多距离原点更远的数据点。另一种做法是进一步探索其他评估指标。

image3.png

更多评估指标

现在我们可使用最佳 k 值来训练模型,代码如下。

best_index = np.argmax(scores)
best_k = k_values[best_index]

knn = KNeighborsClassifier(n_neighbors=best_k)
knn.fit(X_train, y_train)

然后用准确率、精准率与召回率进行评估(注意由于随机性,您的结果可能不同)

y_pred = knn.predict(X_test)

accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred)
recall = recall_score(y_test, y_pred)

print("Accuracy:", accuracy)
print("Precision:", precision)
print("Recall:", recall)
Accuracy: 0.875
Precision: 0.75
Recall: 1.0

更进一步





主题