Python和Scikit-Learn中的K-Nearest Neighbors算法指南

41 阅读31分钟

简介

该 K-近邻(KNN)算法是一种有监督的机器学习算法,用于分类、回归以及离群点检测。它在最基本的形式上非常容易实现,但可以执行相当复杂的任务。它是一种懒惰的学习算法,因为它没有一个专门的训练阶段。相反,它使用所有的数据进行训练,同时对一个新的数据点或实例进行分类(或回归)。

KNN是一种非参数学习算法,这意味着它对基础数据没有任何假设。这是一个非常有用的功能,因为大多数现实世界的数据并不真正遵循任何理论假设,例如线性分离性、均匀分布等。

在本指南中,我们将看到KNN如何通过Python的Scikit-Learn库来实现。在此之前,我们将首先探讨如何使用KNN并解释其背后的理论。之后,我们将看看我们将使用的加州住房数据集来说明KNN算法和它的几种变化。首先,我们将看看如何实现KNN算法的回归,然后是KNN分类和离群点检测的实现。最后,我们将总结一下该算法的一些优点和缺点。

什么时候应该使用KNN?

假设你想租一套公寓,最近发现你朋友的邻居可能会在2周内把她的公寓出租。由于该公寓还没有出现在租赁网站上,你怎么能尝试估计它的租值呢?

比方说,你的朋友支付1200美元的租金。你的租金价值可能在这个数字左右,但公寓并不完全相同(朝向、面积、家具质量等),所以,最好能有更多其他公寓的数据。

通过询问其他邻居,并查看在租房网站上列出的同一栋楼的公寓,最接近的三套相邻公寓的租金分别是1200美元、1210美元、1210美元和1215美元。这些公寓与你朋友的公寓在同一个街区和楼层。

其他公寓距离较远,在同一楼层,但在不同的街区,租金分别为1400美元、1430美元、1500美元和1470美元。似乎它们更贵,因为在晚上有更多的阳光照射。

考虑到该公寓的距离,你的估计租金似乎是1210美元左右。这就是**K-Nearest Neighbors (KNN)**算法的总体思路!它对新的分类或回归。它根据新数据与已有数据的接近程度对其进行分类或回归。

将实例转化为理论

当估计值是一个连续的数字时,如租值,KNN被用于 回归.但我们也可以根据最低和最高租金将公寓分为不同类别,例如。当数值是离散的,使其成为一个类别时,KNN被用于 分类.

还有一种可能,就是估计哪些邻居与其他邻居有很大的不同,以至于他们可能会停止支付租金。这与检测哪些数据点离得很远,以至于它们不适合任何数值或类别是一样的,当这种情况发生时,KNN被用于 离群点检测.

在我们的例子中,我们也已经知道了每个公寓的租金,这意味着我们的数据已经被标记了。KNN使用以前标记的数据,这使它成为一个 监督学习算法.

KNN以其最基本的形式极易实现,却能完成相当复杂的分类、回归或异常点检测任务。

每次有一个新的点加入到数据中,KNN只使用数据的一部分来决定这个新增点的值(回归)或类别(分类)。由于它不需要再看所有的点,这使它成为一个. 懒惰学习算法.

KNN也不假设任何关于基础数据特征的东西,它不期望数据适合某种类型的分布,如均匀分布,或者是线性可分离的。这意味着它是一个 非参数学习算法.这是一个非常有用的特性,因为大多数现实世界的数据并不真正遵循任何理论上的假设。

视觉化的KNN的不同用途

正如已经表明的那样,KNN算法背后的直觉是所有监督机器学习算法中最直接的一种。该算法首先计算一个新数据点与所有其他训练数据点的距离

注意:距离可以用不同的方式测量。你可以使用Minkowski、Euclidean、Manhattan、Mahalanobis或Hamming公式,仅举几个指标。对于高维数据,欧氏距离经常开始失效(高维度是......奇怪的),而曼哈顿距离则被使用。

在计算完距离后,KNN会选择一些最近的数据点--2、3、10,或者真的,任何整数。这个点数(2、3、10等)就是K-Nearest Neighbors中的K!

在最后一步,如果是回归任务,KNN将计算出预测的K最近点的平均加权和。如果是分类任务,新的数据点将被分配到大多数选定的K-最近的点所属的类别中。

让我们借助一个简单的例子来直观地了解该算法的作用。考虑一个有两个变量的数据集,K为3。

在执行回归时,任务是根据3个最近点的平均加权和,找到一个新数据点的值。

KNN与K = 3 ,当 用于回归的:


KNN算法将首先计算新点与所有点的距离。然后,它找到与新点距离最小的3个点。这显示在上面的第二个图中,其中三个最近的点,47,58, 和79 已经被包围起来了。之后,它计算47,5879 的加权和--在这种情况下,权重等于1--我们考虑所有的点都是相等的,但我们也可以根据距离分配不同的权重。在计算了加权和之后,新的点值是61,33

而在进行分类时,KNN的任务是将一个新的数据点,归入"Purple""Red" 类。

KNN与K = 3 ,当 用于分类:


KNN算法将以与之前相同的方式开始,通过计算新点与所有点的距离,找到与新点距离最小的3个最近的点,然后,不计算数字,而是将新点分配到三个最近的点中大多数属于的类别,即红色类别。因此,新的数据点将被分类为"Red"

离群点检测过程与上述两者不同,我们将在回归和分类实现后的实施过程中更多地谈论它。

注意:本教程中提供的代码已经在以下Jupyter笔记本中执行和测试。

Scikit-Learn加州住房数据集

我们将使用加州住房数据集来说明KNN算法如何工作。该数据集来自1990年的美国人口普查。数据集的一行代表一个街区组的人口普查情况。

在这一节中,我们将介绍加州住房数据集的细节,这样你就可以对我们将要处理的数据有一个直观的了解。在你开始工作之前,了解你的数据是非常重要的。

区块组是美国人口普查局发布样本数据的最小地理单位。除了街区组,另一个术语是家庭,家庭是居住在一个家庭里的一群人。

该数据集由九个属性组成:

  • MedInc - 街区组的收入中位数
  • HouseAge - 街区组内的房龄中位数
  • AveRooms - 平均房间数(每户提供)。
  • AveBedrms - 平均卧室数(每户提供)。
  • Population - 小区组的人口
  • AveOccup - 家庭成员的平均数量
  • Latitude - 小区组纬度
  • Longitude - 区块组经度
  • MedHouseVal - 加州各区的房屋价值中值(几十万美元)

数据集已经是Scikit-Learn库的一部分,我们只需要导入它并将其作为数据框架加载。

from sklearn.datasets import fetch_california_housing
# as_frame=True loads the data in a dataframe format, with other metadata besides it
california_housing = fetch_california_housing(as_frame=True)
# Select only the dataframe part and assign it to the df variable
df = california_housing.frame

直接从Scikit-Learn导入数据,导入的不仅仅是列和数字,还包括作为Bunch 对象的数据描述--所以我们只是提取了frame 。关于数据集的更多细节,可以在这里找到。

让我们导入Pandas并偷看一下前几行的数据:

import pandas as pd
df.head()

执行代码将显示我们数据集的前五行:

	MedInc  HouseAge  AveRooms  AveBedrms  Population  AveOccup   Latitude  Longitude  MedHouseVal
0 	8.3252 	41.0 	  6.984127 	1.023810   322.0 	   2.555556   37.88 	-122.23    4.526
1 	8.3014 	21.0 	  6.238137 	0.971880   2401.0 	   2.109842   37.86 	-122.22    3.585
2 	7.2574 	52.0 	  8.288136 	1.073446   496.0 	   2.802260   37.85 	-122.24    3.521
3 	5.6431 	52.0 	  5.817352 	1.073059   558.0 	   2.547945   37.85 	-122.25    3.413
4 	3.8462 	52.0 	  6.281853 	1.081081   565.0 	   2.181467   37.85 	-122.25    3.422

在本指南中,我们将使用MedInc,HouseAge,AveRooms,AveBedrms,Population,AveOccup,Latitude,Longitude 来预测MedHouseVal 。与我们的动机叙述类似。

现在让我们直接跳到回归的KNN算法的实现上。

用Scikit-Learn的K-Nearest Neighbors回归

到目前为止,我们已经了解了我们的数据集,现在可以进行KNN算法的其他步骤了。

为KNN回归预处理数据

预处理是回归和分类任务之间的第一个差异出现的地方。由于本节是关于回归的,我们将相应地准备我们的数据集。

对于回归,我们需要预测另一个房屋价值中位数。为此,我们将把MedHouseVal 归入y ,把所有其他列归入X ,只需去掉MedHouseVal

y = df['MedHouseVal']
X = df.drop(['MedHouseVal'], axis = 1)

通过查看我们的变量描述,我们可以看到我们在测量方面存在差异。为了避免猜测,让我们使用describe() 方法来检查。

# .T transposes the results, transforming rows into columns
X.describe().T

这样的结果是:

			count 	  mean 		   std 			min 		25% 		50% 		75% 		max
MedInc 		20640.0   3.870671 	   1.899822 	0.499900 	2.563400 	3.534800 	4.743250 	15.000100
HouseAge 	20640.0   28.639486    12.585558 	1.000000 	18.000000 	29.000000 	37.000000 	52.000000
AveRooms 	20640.0   5.429000 	   2.474173 	0.846154 	4.440716 	5.229129 	6.052381 	141.909091
AveBedrms 	20640.0   1.096675 	   0.473911 	0.333333 	1.006079 	1.048780 	1.099526 	34.066667
Population 	20640.0   1425.476744  1132.462122 	3.000000 	787.000000 	1166.000000 1725.000000 35682.000000
AveOccup 	20640.0   3.070655 	   10.386050 	0.692308 	2.429741 	2.818116 	3.282261 	1243.333333
Latitude 	20640.0   35.631861    2.135952 	32.540000 	33.930000 	34.260000 	37.710000 	41.950000
Longitude 	20640.0   -119.569704  2.003532    -124.350000 -121.800000 	-118.490000 -118.010000 -114.310000

mean mean 在这里,我们可以看到MedInc 的值大约是3.87HouseAge 的值大约是28.64 ,这使得它比MedInc 大7.4倍。其他特征也有平均值和标准差的差异--要看到这一点,看一下meanstd 的值,观察它们之间的距离。对于MedInc std 大约是1.9 ,对于HouseAgestd12.59 ,同样适用于其他特征。

我们使用的是一种基于距离的算法,而基于距离的算法在不在同一尺度上的数据中受到很大的影响,比如这个数据。点的比例可能(而且在实践中,几乎总是如此)扭曲了数值之间的真实距离。

为了执行特征缩放,我们稍后将使用Scikit-Learn的StandardScaler 类。如果我们现在就应用缩放(在训练-测试分割之前),计算将包括测试数据,有效地测试数据信息泄露到管道的其他部分。不幸的是,这种数据泄露通常会被跳过,导致不可复制的或虚幻的发现。

将数据分割成训练集和测试集

为了能够在不泄漏数据的情况下扩展我们的数据,同时也为了评估我们的结果和避免过度拟合,我们将把我们的数据集分成训练和测试两部分。

创建训练和测试分片的直接方法是Scikit-Learn的train_test_split 方法。这个分割并不是在某个点上线性分割,而是随机地对X%和Y%进行采样。为了使这一过程具有可重复性(使该方法总是对相同的数据点进行采样),我们将random_state 参数设置为某个SEED

from sklearn.model_selection import train_test_split

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

这段代码对75%的数据进行训练采样,对25%的数据进行测试。例如,通过将test_size 改为0.3,你可以用70%的数据进行训练,用30%的数据进行测试。

通过使用75%的数据进行训练,25%的数据进行测试,在20640条记录中,训练集包含15480条,测试集包含5160条。我们可以通过打印完整数据集和分割数据的长度来快速检查这些数字。

len(X)       # 20640
len(X_train) # 15480
len(X_test)  # 5160

很好!X_test 我们现在可以在X_train 集上拟合数据缩放器,并在不泄露任何数据的情况下将X_trainX_test 缩放到X_train

建议:如果你想了解更多关于train_test_split() 方法、训练-测试-验证分离的重要性,以及如何将验证集也分离出来,请阅读我们的 "Scikit-Learn的train_test_split() - 训练、测试和验证集".

用于KNN回归的特征缩放

通过导入StandardScaler ,实例化它,根据我们的训练数据进行拟合(防止泄漏),并转换训练和测试数据集,我们可以进行特征缩放。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
# Fit only on X_train
scaler.fit(X_train)

# Scale both X_train and X_test
X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)

注意:由于你经常调用scaler.fit(X_train) ,然后是scaler.transform(X_train) - 你可以调用一个scaler.fit_transform(X_train) ,然后是scaler.transform(X_test) ,以使调用更简短

现在我们的数据被缩放了!当应用于DataFrame ,缩放器只保留数据点,而不是列名。让我们再次用列名将数据组织到一个DataFrame中,并使用describe() ,观察meanstd 的变化。

col_names=['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup', 'Latitude', 'Longitude']
scaled_df = pd.DataFrame(X_train, columns=col_names)
scaled_df.describe().T

这将给我们带来:

			count 		mean 			std 		min 		25% 		50% 		75% 		max
MedInc 		15480.0 	2.074711e-16 	1.000032 	-1.774632 	-0.688854 	-0.175663 	0.464450 	5.842113
HouseAge 	15480.0 	-1.232434e-16 	1.000032 	-2.188261 	-0.840224 	0.032036 	0.666407 	1.855852
AveRooms 	15480.0 	-1.620294e-16 	1.000032 	-1.877586 	-0.407008 	-0.083940 	0.257082 	56.357392
AveBedrms 	15480.0 	7.435912e-17 	1.000032 	-1.740123 	-0.205765 	-0.108332 	0.007435 	55.925392
Population 	15480.0 	-8.996536e-17 	1.000032 	-1.246395 	-0.558886 	-0.227928 	0.262056 	29.971725
AveOccup 	15480.0 	1.055716e-17 	1.000032 	-0.201946 	-0.056581 	-0.024172 	0.014501 	103.737365
Latitude 	15480.0 	7.890329e-16 	1.000032 	-1.451215 	-0.799820 	-0.645172 	0.971601 	2.953905
Longitude 	15480.0 	2.206676e-15 	1.000032 	-2.380303 	-1.106817 	0.536231 	0.785934 	2.633738

观察一下,现在所有的标准差都是1 ,平均值也变小了。这就是使我们的数据更加均匀的原因!让我们来训练和评估一个基于KNN的回归器。

训练和预测KNN回归

Scikit-Learn的直观和稳定的API使得训练回归器和分类器非常简单。让我们从sklearn.neighbors 模块中导入KNeighborsRegressor 类,将其实例化,并将其适用于我们的训练数据。

from sklearn.neighbors import KNeighborsRegressor
regressor = KNeighborsRegressor(n_neighbors=5)
regressor.fit(X_train, y_train)

在上面的代码中,n_neighborsK的值,或者说算法在选择新的房屋中值时将考虑到的邻居数量。5KNeighborsRegressor() 的默认值。K没有理想的值,它是在测试和评估后选择的,然而,开始时,5 是KNN常用的值,因此被设置为默认值。

最后一步是对我们的测试数据进行预测。要做到这一点,请执行以下脚本。

y_pred = regressor.predict(X_test)

我们现在可以评估我们的模型对我们有标签(地面真相)的新数据的概括程度--测试集

评估KNN回归的算法

评价算法最常用的回归指标是平均绝对误差(MAE)、平均平方误差(MSE)、平均平方根误差(RMSE)和决定系数(R2)。

  1. 平均绝对误差(MAE)。当我们从实际值中减去预测值,得到误差,将这些误差的绝对值相加,得到其平均值。这个指标给出了模型每次预测的总体误差的概念,越小(越接近0)越好。
mae=(frac1n)/sum_i=1nleftActualPredictedrightmae = (frac{1}{n})/sum\_{i=1}^{n}\\left | Actual - Predicted \\right |

注意:你可能还会在方程中遇到yŷ (读作y-hat)的符号。y 是指实际值,ŷ 是指预测值。

  1. 平均平方误差(MSE)。它与MAE指标类似,但它将误差的绝对值进行平方。另外,与MAE一样,越小,或越接近0,越好。MSE值的平方是为了使大的误差变得更大。需要注意的是,由于其数值的大小以及它们与数据不在同一尺度上的事实,它通常是一个难以解释的指标。
mse=sum_i=1D(实际预测2mse = sum\_{i=1}^{D}(实际-预测)^2
  1. 均方根误差(RMSE)。试图通过获取其最终值的平方根来解决与MSE有关的解释问题,以便将其缩回到与数据相同的单位。当我们需要展示或显示数据的实际值与误差时,它更容易解释,也很好。它显示了数据的变化程度,因此,如果我们的RMSE为4.35,我们的模型可能会出现错误,因为它在实际值上增加了4.35,或者需要4.35才能达到实际值。越接近于0,也就越好。
rmse=sqrtsˋum_i=1D(实际预测)2rmse = \\sqrt{ \`sum\_{i=1}^{D}(实际 - 预测)^2}

mean_absolute_error() sklearn.metrics 和 的方法可以用来计算这些指标,从下面的片段中可以看出。mean_squared_error()

from sklearn.metrics import mean_absolute_error, mean_squared_error

mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = mean_squared_error(y_test, y_pred, squared=False)

print(f'mae: {mae}')
print(f'mse: {mse}')
print(f'rmse: {rmse}')

上述脚本的输出看起来是这样的:

mae: 0.4460739527131783 
mse: 0.4316907430948294 
rmse: 0.6570317671884894

R2可以直接用score() 方法来计算:

regressor.score(X_test, y_test)

其中输出:

0.6737569252627673

结果显示,我们的KNN算法总体误差和平均误差都在0.44 ,和0.43 。另外,RMSE显示,我们可以通过添加0.65 或减去0.65 ,高于或低于数据的实际值。这有多好呢?

让我们看看价格是什么样子的:

y.describe()
count    20640.000000
mean         2.068558
std          1.153956
min          0.149990
25%          1.196000
50%          1.797000
75%          2.647250
max          5.000010
Name: MedHouseVal, dtype: float64

平均数是2.06 ,平均数的标准差是1.15 ,所以我们的分数~0.44 ,不是真正的明星,但也不是太坏。

对于R2,我们得到的分数越接近1(或100)就越好。R2告诉我们数据的变化或数据方差有多少被KNN所理解或解释

R2=1fracsum(ActualPredicted)2sum(ActualActualMean)2R^2 = 1 - frac{\\sum(Actual - Predicted)^2}{sum(Actual - Actual Mean)^2}

值为0.67 ,我们可以看到,我们的模型解释了67%的数据方差。这已经超过了50%,这还可以,但不是很好。我们有什么办法可以做得更好吗?

我们使用了一个预定的K值,即5 ,所以,我们使用5个邻居来预测我们的目标,这不一定是最好的数字。为了了解哪个是理想的K数,我们可以分析我们的算法错误,并选择损失最小的K。

寻找KNN回归的最佳K

理想情况下,你会看到哪个指标更适合你的环境--但通常测试所有指标是很有趣的。只要你能测试所有的指标,就去做吧。在这里,我们将展示如何只用平均绝对误差来选择最佳K值,但是你可以把它改成任何其他指标,并比较结果。

要做到这一点,我们将创建一个for循环,运行有1到X个邻居的模型。在每个交互作用中,我们将计算MAE,并将K的数量与MAE的结果一起绘制出来。

error = []

# Calculating MAE error for K values between 1 and 39
for i in range(1, 40):
    knn = KNeighborsRegressor(n_neighbors=i)
    knn.fit(X_train, y_train)
    pred_i = knn.predict(X_test)
    mae = mean_absolute_error(y_test, pred_i)
    error.append(mae)

现在,让我们绘制errors:

import matplotlib.pyplot as plt 

plt.figure(figsize=(12, 6))
plt.plot(range(1, 40), error, color='red', 
         linestyle='dashed', marker='o',
         markerfacecolor='blue', markersize=10)
         
plt.title('K Value MAE')
plt.xlabel('K Value')
plt.ylabel('Mean Absolute Error')

看一下这个图,似乎最低的MAE值是在K是12 。让我们仔细看看这个图,通过绘制更少的数据来确定。

plt.figure(figsize=(12, 6))
plt.plot(range(1, 15), error[:14], color='red', 
         linestyle='dashed', marker='o',
         markerfacecolor='blue', markersize=10)
plt.title('K Value MAE')
plt.xlabel('K Value')
plt.ylabel('Mean Absolute Error')

你也可以使用内置的min() 函数获得最低误差和该点的索引(对列表有效),或者将列表转换成NumPy数组并获得argmin() (具有最低值的元素的索引)。

import numpy as np 

print(min(error))               # 0.43631325936692505
print(np.array(error).argmin()) # 11

我们从1开始计算邻居,而数组是以0为基础的,所以第11个索引是12个邻居!

这意味着,我们需要12个邻居才能以最低的MAE误差预测一个点。我们可以用12个邻居再次执行模型和度量,以比较结果:

knn_reg12 = KNeighborsRegressor(n_neighbors=12)
knn_reg12.fit(X_train, y_train)
y_pred12 = knn_reg12.predict(X_test)
r2 = knn_reg12.score(X_test, y_test) 

mae12 = mean_absolute_error(y_test, y_pred12)
mse12 = mean_squared_error(y_test, y_pred12)
rmse12 = mean_squared_error(y_test, y_pred12, squared=False)
print(f'r2: {r2}, \nmae: {mae12} \nmse: {mse12} \nrmse: {rmse12}')

下面的代码输出:

r2: 0.6887495617137436, 
mae: 0.43631325936692505 
mse: 0.4118522151025172 
rmse: 0.6417571309323467

有了12个邻居,我们的KNN模型现在可以解释69%的数据方差,而且损失也小了一些,从 0.440.430.430.410.650.64 ,各自的度量都有了。这不是一个非常大的改进,但它仍然是一个改进。

**注意:**在这个分析中,进一步做探索性数据分析(EDA)和残差分析可能有助于选择特征并取得更好的结果。

我们已经看到了如何使用KNN进行回归--但是如果我们想对一个点进行分类而不是预测它的值呢?现在,我们可以看一下如何使用KNN进行分类。

使用Scikit-Learn的K-Nearest Neighbors进行分类

在这个任务中,我们不是预测一个连续值,而是要预测这些区块组所属的类别。为了做到这一点,我们可以将各区的房屋价值中位数划分为具有不同房屋价值范围的组或

当你想使用一个连续值进行分类时,你通常可以将数据分仓。通过这种方式,你可以预测群体,而不是数值。

用于分类的数据预处理

让我们创建数据仓,将我们的连续值转化为类别。

# Creating 4 categories and assigning them to a MedHouseValCat column
df["MedHouseValCat"] = pd.qcut(df["MedHouseVal"], 4, retbins=False, labels=[1, 2, 3, 4])

然后,我们可以将我们的数据集分成其属性和标签。

y = df['MedHouseValCat']
X = df.drop(['MedHouseVal', 'MedHouseValCat'], axis = 1)

由于我们使用了MedHouseVal 列来创建bin,我们需要从MedHouseVal 列和MedHouseValCat 列中删除X 。这样,DataFrame 将包含数据集的前8列(即属性、特征),而我们的y 将只包含MedHouseValCat 分配的标签。

**注意:**你也可以用.iloc() 来选择列,而不是丢弃它们。当丢弃时,只需注意你需要在分配y 值之前分配X 值,因为你不能将丢弃的DataFrame 列分配给内存中的另一个对象。

将数据分割成训练集和测试集

正如在回归中所做的那样,我们也将把数据集分成训练和测试两部分。由于我们有不同的数据,我们需要重复这个过程。

from sklearn.model_selection import train_test_split

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

我们将再次使用Scikit-Learn的标准值:75%的训练数据和25%的测试数据。这意味着我们将拥有与之前回归中相同的训练和测试记录数。

分类的特征缩放

由于我们处理的是同一个未经处理的数据集及其不同的测量单位,我们将再次进行特征缩放,方法与回归数据的方法相同。

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit(X_train)

X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)

分类的训练和预测

在对数据进行分类、分割和缩放后,我们最终可以在上面拟合一个分类器。对于预测,我们将再次使用5个邻居作为基线。你也可以在没有任何参数的情况下实例化KNeighbors_ 类,它将自动使用5个邻居。在这里,我们没有导入KNeighborsRegressor ,而是导入KNeighborsClassifier ,类。

from sklearn.neighbors import KNeighborsClassifier

classifier = KNeighborsClassifier()
classifier.fit(X_train, y_train)

拟合完KNeighborsClassifier ,我们就可以预测测试数据的类别了。

y_pred = classifier.predict(X_test)

是时候评估预测结果了!在这种情况下,预测类比预测值会是一个更好的方法吗?让我们评估一下这个算法,看看会发生什么。

评估KNN的分类

对于评估KNN分类器,我们也可以使用score 方法,但它执行的是不同的度量,因为我们是给分类器而不是给回归器打分。分类的基本指标是accuracy - 它描述了我们的分类器有多少预测是正确的。最低的准确度值是0,最高的是1。我们通常将该值乘以100,得到一个百分比。

准确率==frac{text{正确预测数}{text{总预测数}

**注意:**在任何真实的数据上获得100%的准确性是非常困难的,如果发生这种情况,要注意可能会发生一些泄漏或错误的事情--在理想的准确性值上没有达成共识,它也是取决于上下文的。根据错误的代价(如果我们相信分类器,但结果却是错误的,这有多糟糕),可接受的错误率可能是5%、10%甚至30%。

让我们给我们的分类器打分。

acc =  classifier.score(X_test, y_test)
print(acc) # 0.6191860465116279

通过观察所得的分数,我们可以推断出我们的分类器有~62%的类是正确的。这已经有助于分析,尽管只知道分类器做对了什么,就很难改进它。

在我们的数据集中有4个类--如果我们的分类器在1、2、3类中有90%是正确的,但在4类中只有30%是正确的呢?

某个类的系统性故障,与类之间共享的平衡性故障相比,都可以得到62%的准确率。准确率并不是实际评估的一个真正好的指标--但确实可以作为一个好的代理。更多的时候,在平衡的数据集中,62%的准确率是相对均匀的。而且,更多的时候,数据集并不平衡,所以我们又回到了原点,准确率是一个不充分的衡量标准。

我们可以使用其他指标深入研究结果,以便能够确定这一点。这一步也与回归不同,这里我们将使用。

  1. 混淆矩阵。要想知道我们的预测在多大程度上是正确的或错误的 各类.正确的和正确预测的值被称为真阳性,那些被预测为阳性但不是阳性的值被称为假阳性真阴性假阴性的命名方法同样适用于负值。
  2. 精度。来了解哪些正确的预测值被我们的分类器认为是正确的。精度将把这些真阳性值除以任何被预测为阳性的东西。

$
精度={frac{text{true positive}}{text{true positive}}

+ 虚假阳性}}

3.召回率:了解有多少真阳性被我们的分类器所识别。召回率的计算方法是用真阳性结果除以任何应该被预测为阳性的结果。 3. **召回**率:了解有多少真阳性被我们的分类器所识别。召回率的计算方法是用真阳性结果除以任何应该被预测为阳性的结果。

召回率={frac{text{true positive}}{text{true positive}}

+ frac{text{false negative}}

4.F1得分:是精度和召回率的平衡或谐波平均值。最低值为0,最高值为1。当f1score等于1时,意味着所有类别的预测都是正确的这是一个在真实数据中很难获得的分数(几乎总是存在例外)。 4. **F1得分**:是精度和召回率的平衡或*谐波平均值*。最低值为0,最高值为1。当`f1-score` 等于1时,意味着所有类别的预测都是正确的--这是一个在真实数据中很难获得的分数(几乎总是存在例外)。

\text{f1-score} = 2* \frac{text{precision} * \text{recall} * \f1-score* `text{recall}}{text{precision}}。+text{recall}}

**注意:**加权的F1得分也是存在的,它只是一个不对所有类别适用相同权重的F1。权重通常是由类的**支持度**决定的--有多少实例 "支持 "F1得分(属于某个类的标签比例)。支持率越低(某类的实例越少),该类的加权F1就越低,因为它更不可靠: `confusion_matrix()` `sklearn.metrics` 和 模块的方法可以用来计算和显示所有这些度量。 ,使用热图可以更好地进行可视化。分类报告已经给了我们 , , , 和 , 但你也可以从 中导入这些指标。`classification_report()` `confusion_matrix` `accuracy` `precision` `recall` `f1-score` `sklearn.metrics` 要获得指标,请执行以下代码段: ``` from sklearn.metrics import classification_report, confusion_matrix #importing Seaborn's to use the heatmap import seaborn as sns # Adding classes names for better interpretation classes_names = ['class 1','class 2','class 3', 'class 4'] cm = pd.DataFrame(confusion_matrix(yc_test, yc_pred), columns=classes_names, index = classes_names) # Seaborn's heatmap to better visualize the confusion matrix sns.heatmap(cm, annot=True, fmt='d'); print(classification_report(y_test, y_pred)) ``` 上述脚本的输出看起来像这样: ![](https://s3.stackabuse.com/media/articles/k-nearest-neighbors-algorithm-python-scikit-learn-7.png) ``` precision recall f1-score support 1 0.75 0.78 0.76 1292 2 0.49 0.56 0.53 1283 3 0.51 0.51 0.51 1292 4 0.76 0.62 0.69 1293 accuracy 0.62 5160 macro avg 0.63 0.62 0.62 5160 weighted avg 0.63 0.62 0.62 5160 ``` 结果显示,KNN能够对测试集中的所有5160条记录进行分类,准确率为62%,高于平均值。支持率是相当相等的(数据集中的类别分布均匀),所以加权F1和非加权F1将大致相同。 我们还可以看到4个类别中每个类别的度量结果。从中我们可以注意到,`class 2` 的精度最低,`recall` 的精度最低,`f1-score` 的精度最低。`Class 3` 紧随其后,`class 2` 的得分最低,然后,我们有`class 1` ,得分最好,其次是`class 4` 。 通过查看混淆矩阵,我们可以看到: * `class 1` 在238个案例中,大部分被误认为是`class 2` * `class 2` 在256个条目中被误认为是 ,而在260个条目中被误认为是 。`class 1` `class 3` * `class 3` 有374个条目被误认为是 ,193个条目被误认为是 。`class 2` `class 4` * `class 4` 在339个条目中被错划为 ,在130个条目中被错划为 。`class 3` `class 2` 另外,注意到对角线显示的是真正的正值,当看到它时,可以清楚地看到`class 2` 和`class 3` 的正确预测值最少。 有了这些结果,我们可以通过进一步检查它们来深入分析,以弄清楚为什么会发生这种情况,同时了解4类是否是分割数据的最佳方式。也许来自`class 2` 和`class 3` 的数值太接近了,所以变得难以区分。 > 总是尝试用不同数量的分仓来测试数据,看看会发生什么。 除了任意数量的数据仓,还有一个我们选择的任意数量,即K邻居的数量。在确定使某一指标值最大化或最小化的K数时,我们应用于回归任务的技术也可以应用于分类。 #### 为KNN分类找到最佳K 让我们重复对回归所做的工作,绘制测试集的K值和相应度量的图表。你也可以选择哪个指标更适合你的情况,在这里,我们将选择`f1-score` 。 这样一来,我们将为所有K值在1到40之间的测试集的预测值绘制`f1-score` 。 `f1_score` 首先,我们从`sklearn.metrics` ,然后计算它对K-近邻分类器的所有预测值,其中K在1到40之间。 ``` from sklearn.metrics import f1_score f1s = [] # Calculating f1 score for K values between 1 and 40 for i in range(1, 40): knn = KNeighborsClassifier(n_neighbors=i) knn.fit(X_train, y_train) pred_i = knn.predict(X_test) # using average='weighted' to calculate a weighted average for the 4 classes f1s.append(f1_score(y_test, pred_i, average='weighted')) ``` 下一步是将`f1_score` 值与K值作对比。与回归不同的是,这次我们不是选择使误差最小的K值,而是选择使`f1-score` 最大的值。 执行下面的脚本来创建该图: ``` plt.figure(figsize=(12, 6)) plt.plot(range(1, 40), f1s, color='red', linestyle='dashed', marker='o', markerfacecolor='blue', markersize=10) plt.title('F1 Score K Value') plt.xlabel('K Value') plt.ylabel('F1 Score') ``` 输出的图看起来像这样: ![](https://s3.stackabuse.com/media/articles/k-nearest-neighbors-algorithm-python-scikit-learn-8.png) 从输出结果中,我们可以看到,当K值为`15` ,`f1-score` 是最高的。让我们用15个邻居重新训练我们的分类器,看看它对我们的分类报告结果有何影响。 ``` classifier15 = KNeighborsClassifier(n_neighbors=15) classifier15.fit(X_train, y_train) y_pred15 = classifier15.predict(X_test) print(classification_report(y_test, y_pred15)) ``` 这个输出。 ``` precision recall f1-score support 1 0.77 0.79 0.78 1292 2 0.52 0.58 0.55 1283 3 0.51 0.53 0.52 1292 4 0.77 0.64 0.70 1293 accuracy 0.63 5160 macro avg 0.64 0.63 0.64 5160 weighted avg 0.64 0.63 0.64 5160 ``` 注意到我们的指标在有了15个邻居后有了改善,我们有63%的准确率和更高的`precision`,`recall`, 和`f1-scores`, 但我们仍然需要进一步看一下bin,试图理解为什么`2` 和`3` 类的`f1-score` 仍然很低。 除了使用KNN进行回归和确定块值,以及用于分类,确定块类--我们还可以使用KNN来检测哪些平均块值与大多数不同--那些不遵循大多数数据的情况。换句话说,我们可以用KNN来*检测离群值*。 ### 用Scikit-Learn实现KNN的离群点检测 *离群点检测*使用的是另一种方法,与我们之前为回归和分类所做的不同。 在这里,我们将看到每个邻居离一个数据点有多远。让我们使用默认的5个邻居。对于一个数据点,我们将计算与每个K-最近的邻居的距离。为了做到这一点,我们将从Scikit-learn中导入另一种KNN算法,这种算法并不专门用于回归或分类,称为`NearestNeighbors` 。 导入后,我们将实例化一个有5个邻居的`NearestNeighbors` 类--你也可以实例化12个邻居来识别我们回归例子中的异常值,或者用15个邻居来做同样的分类例子。然后,我们将拟合我们的训练数据,并使用`kneighbors()` 方法来找到我们对每个数据点和邻居索引的计算距离。 ``` from sklearn.neighbors import NearestNeighbors nbrs = NearestNeighbors(n_neighbors = 5) nbrs.fit(X_train) # Distances and indexes of the 5 neighbors distances, indexes = nbrs.kneighbors(X_train) ``` 现在我们有每个数据点的5个距离--它自己和它的5个邻居之间的距离,以及识别它们的索引。让我们看一下前三个结果和数组的形状,以便更好地观察。 要看前三个距离的形状,执行: ``` distances[:3], distances.shape ``` ``` (array([[0. , 0.12998939, 0.15157687, 0.16543705, 0.17750354], [0. , 0.25535314, 0.37100754, 0.39090243, 0.40619693], [0. , 0.27149697, 0.28024623, 0.28112326, 0.30420656]]), (3, 5)) ``` 观察一下,有3行,每行5个距离。我们还可以看看和邻居的索引: ``` indexes[:3], indexes[:3].shape ``` 这样的结果是。 ``` (array([[ 0, 8608, 12831, 8298, 2482], [ 1, 4966, 5786, 8568, 6759], [ 2, 13326, 13936, 3618, 9756]]), (3, 5)) ``` 在上面的输出中,我们可以看到5个邻居各自的索引。现在,我们可以继续计算这5个距离的平均值,并绘制一个图表,在X轴上对每一行进行计数,在Y轴上显示每个平均距离。 ``` dist_means = distances.mean(axis=1) plt.plot(dist_means) plt.title('Mean of the 5 neighbors distances for each data point') plt.xlabel('Count') plt.ylabel('Mean Distances') ``` ![](https://s3.stackabuse.com/media/articles/k-nearest-neighbors-algorithm-python-scikit-learn-9.png) 请注意,图中有一个部分,其中平均距离的值是统一的。这个Y轴的点,其平均值不会太高或太低,正是我们需要确定的点,以切断离群值。 在这种情况下,它就是平均距离为3的地方。让我们用一条水平虚线再次绘制图表,以便能够发现它。 ``` dist_means = distances.mean(axis=1) plt.plot(dist_means) plt.title('Mean of the 5 neighbors distances for each data point with cut-off line') plt.xlabel('Count') plt.ylabel('Mean Distances') plt.axhline(y = 3, color = 'r', linestyle = '--') ``` ![](https://s3.stackabuse.com/media/articles/k-nearest-neighbors-algorithm-python-scikit-learn-10.png) 这条线标志着平均距离,在它上面的所有数值都是不同的。`mean` 这意味着所有距离高于`3` 的点都是我们的离群点。我们可以用`np.where()` ,找出这些点的指数。这个方法将为每个指数输出`True` 或`False` ,以满足`mean` *高于3的*条件。 ``` import numpy as np # Visually determine cutoff values > 3 outlier_index = np.where(dist_means > 3) outlier_index ``` 上述代码的输出: ``` (array([ 564, 2167, 2415, 2902, 6607, 8047, 8243, 9029, 11892, 12127, 12226, 12353, 13534, 13795, 14292, 14707]),) ``` 现在我们有了我们的离群点的索引。让我们在数据框架中定位它们: ``` # Filter outlier values outlier_values = df.iloc[outlier_index] outlier_values ``` 这样的结果是: ``` MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude MedHouseVal 564 4.8711 27.0 5.082811 0.944793 1499.0 1.880803 37.75 -122.24 2.86600 2167 2.8359 30.0 4.948357 1.001565 1660.0 2.597809 36.78 -119.83 0.80300 2415 2.8250 32.0 4.784232 0.979253 761.0 3.157676 36.59 -119.44 0.67600 2902 1.1875 48.0 5.492063 1.460317 129.0 2.047619 35.38 -119.02 0.63800 6607 3.5164 47.0 5.970639 1.074266 1700.0 2.936097 34.18 -118.14 2.26500 8047 2.7260 29.0 3.707547 1.078616 2515.0 1.977201 33.84 -118.17 2.08700 8243 2.0769 17.0 3.941667 1.211111 1300.0 3.611111 33.78 -118.18 1.00000 9029 6.8300 28.0 6.748744 1.080402 487.0 2.447236 34.05 -118.78 5.00001 11892 2.6071 45.0 4.225806 0.903226 89.0 2.870968 33.99 -117.35 1.12500 12127 4.1482 7.0 5.674957 1.106998 5595.0 3.235975 33.92 -117.25 1.24600 12226 2.8125 18.0 4.962500 1.112500 239.0 2.987500 33.63 -116.92 1.43800 12353 3.1493 24.0 7.307323 1.460984 1721.0 2.066026 33.81 -116.54 1.99400 13534 3.7949 13.0 5.832258 1.072581 2189.0 3.530645 34.17 -117.33 1.06300 13795 1.7567 8.0 4.485173 1.120264 3220.0 2.652389 34.59 -117.42 0.69500 14292 2.6250 50.0 4.742236 1.049689 728.0 2.260870 32.74 -117.13 2.03200 14707 3.7167 17.0 5.034130 1.051195 549.0 1.873720 32.80 -117.05 1.80400 ``` 我们的离群点检测已经完成。这就是我们如何发现每个偏离一般数据趋势的数据点。我们可以看到,在我们的训练数据中,有16个点应该被进一步观察、调查,也许被处理,甚至从我们的数据中删除(如果它们被错误地输入)以改善结果。这些点可能是由于打字错误、平均块值不一致或甚至两者都有造成的。 ### KNN的优点和缺点 在本节中,我们将介绍使用KNN算法的一些优点和缺点。 #### 优点 * 它很容易实现 * 它是一种懒惰的学习算法,因此不需要对所有的数据点进行训练(只用K-最近的邻居来预测)。这使得KNN算法比其他需要对整个数据集进行训练的算法(如[支持向量机](https://stackabuse.com/implementing-svm-and-kernel-svm-with-pythons-scikit-learn/)、[线性回归](https://stackabuse.com/linear-regression-in-python-with-scikit-learn/)等)快得多。 * 由于KNN在进行预测之前不需要训练,所以可以无缝地添加新的数据 * 使用KNN只需要两个参数,即K值和距离函数。 #### 缺点 * KNN算法在处理高维数据时效果不佳,因为随着维数的增加,点与点之间的距离会变得 "很奇怪",我们使用的距离度量也就不成立了。 * 最后,KNN算法不能很好地处理分类特征,因为很难找到分类特征的维度之间的距离。 ### 总结 KNN是一个简单而强大的算法。它可以用于许多任务,如回归、分类或离群点检测。 KNN已被广泛用于寻找文档相似性和模式识别。它还被用于开发推荐系统和计算机视觉的降维和预处理步骤--特别是人脸识别任务。 在本指南中,我们使用Scikit-Learn的K-Nearest Neighbor算法的实现,经历了回归、分类和离群检测。