本教程中的 k-近邻算法实战,使用的数据集是非常经典的鸢尾花数据集。该数据集最初是由美国植物学家埃德加·安德森(Edgar Anderson)整理出来的。在加拿大加斯帕半岛上,安德森通过观察采集了因地理位置不同而导致鸢尾属花性状发生变异的外显特征数据。
鸢尾花数据集共包含 150 个样本,涵盖鸢尾花属下的三个亚属,分别是山鸢尾(Iris Setosa)、变色鸢尾(Iris Versicolor)和维吉尼亚鸢尾(Iris Virginica),如图 1 所示。
图 1:鸢尾花的三个亚属
鸢尾花数据集使用四个特征对样本进行定量分析,分别是花萼长度(sepal_length)、花萼宽度(sepal_width)、花瓣长度(petal_width)、花瓣宽度(petal_width)。读者可以从 UCI(加州大学埃文分校)的机器学习库中下载这个数据集。
访问这个数据集的方式通常有两种。一种是利用 Pandas 库直接将这个数据集远程转换为 DataFrame 对象,并加载到内存中,Python 代码如下:
<pre class="python sh_python snippet-formatted sh_sourceCode" style="margin: 0px; display: block; padding: 0px; font-size: 14px; line-height: 1.6em; color: rgb(102, 102, 102); white-space: pre-wrap; overflow-wrap: break-word; background: none; border: none; border-radius: 0px;">
- import pandas as pd
- df = pd.read_csv('https://archive.ics.uci.edu/m1/machine-learning-databases/ iris/iris.data', header=None)
</pre>
当然,我们也可以把上述网络连接的数据下载到本地,然后在本地计算机中读取,之前我们已经详细地讨论过,这里不再赘述。
由于这个数据集过于经典,已经内置于 sklearn,所以加载这个数据集最简单的方法,莫过于使用《Python sklearn实例:预测波士顿房价》一节中表 1 中介绍的专用加载方法 load_iris()。其实通过 sklearn 框架来完成 k-近邻算法实战,其流程与通过 sklearn 实现波士顿房价预测是类似的,具体请参考例 1。
【例 1】使用 sklearn 实现 k-近邻算法(scikit-learn-knn.py)
<pre class="python sh_python snippet-formatted sh_sourceCode" style="margin: 0px; display: block; padding: 0px; font-size: 14px; line-height: 1.6em; color: rgb(102, 102, 102); white-space: pre-wrap; overflow-wrap: break-word; background: none; border: none; border-radius: 0px;">
- import pandas as pd
- from sklearn.datasets import load_iris
-
(1)加载莺尾花数据集
- iris = load_iris()
- X = iris.data
- y = iris.target
-
(2)分割数据
- from sklearn.model_selection import train_test_split
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 123)
-
(3)选择模型
- from sklearn.neighbors import KNeighborsClassifier
-
(4)生成模型对象
- knn = KNeighborsClassifier(n_neighbors = 3)
-
(5)训练模型(数据拟合)
- knn.fit (X, y)
-
(6)模型预测
-
(6)-A单个数据预测
- knn.predict ([[4,3,5,3]]) #输出 array( [2 ])
-
(6)-B大集合数据预测
- y_predict_on_train = knn.predict(X_train)
- y_predict_on_test = knn.predict(X_test)
-
(7)模型评估
- from sklearn.metries import accuracy_score
- print('训练集的准确率为:{ : . 2f} %' . format (100 * accuracy_score (y_train, y_predict_train)))
- print('测试集的准确率为:{ : . 2f} % ' .format (100 * accuracy_score (y_test, y_predict_train )))
</pre>
程序执行结果为:
训练集的准确率为:97.14%
测试集的准确率为:93.33%
下面从代码层面对例 1 进行简要说明。第 04 行用于加载鸢尾花数据集,由于 sklearn 库使用的是内置数据加载方法,这些内置数据集已存储在特定位置,sklearn 对此“了然于胸”,所以不需要用户指定路径。
代码第 10~15 行的功能分别是选择模型、生成模型对象、训练模型(数据拟合),它们环环相扣,缺一不可。其中,第 13 行生成 k-近邻分类器,我们把这个 k 值设定为 3。为什么这么设置,其实是没有多少道理可讲的。我们把这种基于设计者经验而非机器学习算法学习得来的参数,称为超参数。
此外,KneighborsClassifier( ) 分类器中有多个参数可以选用,详情请参考 sklearn 的官方资料。我们可以用 print( ) 很方便地打印这个分类器的参数。
<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [1]: print(knn)
KNeighborsClassifier(algorithm= 'auto', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=None, n_neighbors=3, p=2, weights='uniform')</pre>
简单介绍一下其中的两个参数:
n_neighbors 表示的就是 k 值。k值的选择与样本分布有关,一般要选择一个较小的 k 值,可以通过交叉验证来确定,默认是 5。
weights 用于指定投票权重类型,主要是标识每个样本的近邻样本的权重。如果取值为 uniform,就表示“众生平等”,即所有最近邻样本权重都一样(这也是默认值)。但这个设置可能会产生“多数人的暴政”问题,因此还可以取值为 distance,此时表明权重和距离成反比,即距离预测目标更近的近邻具有更高的权重。当然,我们还可以自定义权重,这时需要传入与距离同样维度的权重数组。
https://docs.qq.com/pdf/DR1doYmNBYUZ3RVNX
代码第 18 行给出了单个数据点的预测语句。需要注意的是,k-近邻分类器的预测方法为 predict( ),其中的参数是一个类似于二维数组形式的数据。在 Python 中,通常将用方括号[ ]
括起来的数据理解为列表。为避免歧义,我们还需要在外面多添加一对方括号,用 [[特征1,特征2,…]] 这种形式来表示二维数组。所以第 18 行代码的外层方括号[ ]
是不可缺少的,否则不能通过编译。
此外,为了便于处理,sklearn 仅接纳数字类型的特征或标签。因此,例 1 对鸢尾花的三个亚属做了数值处理,比如用“0”表示山鸢尾,用“1”表示变色鸢尾,用“2”表示维吉尼亚鸢尾。因此,我们可以看到,在代码第 18 行,特征值为 [4,3,5,3] 的鸢尾花,其输出为一个数组形式 array([2]),其中的数字“2”表明预测结果为第 2 类,即维吉尼亚鸢尾。
我们已经使用 sklearn 对单个数据点进行了预测分类。由于样本寥寥,模型准确与否并不能得到很好的反应。难道预测准了,准确率就是 100%,而预测失误,准确率就是 0?自然不能这样草率判断。
其实,判断一个分类模型的好坏,需要用到一系列的数据,那就是测试集。由于测试集和训练集并没有本质的区别,所以在第 24 行和第 25 行中,我们在训练集和测试集中分别对模型做了预测。从运行结果上看,在训练集中,预测准确率能达到 97% 以上,但在测试集中,预测准确率仅为 93% 左右。
之所以把训练集和测试集的准确率都输出出来,是因为,如果训练集中的准确率大大超过了测试集,就有理由怀疑我们的模型可能陷入了“过拟合”状态,这是应该避免的。
另一方面,如果看到在测试集上的预测准确率不是很高,我们也要做到“不以物喜,不以己悲”。影响预测准确率的因素实在是太多了,当然有可能是模型设计有缺陷还有待提高,但如果大家的模型都是一样的呢?比如说我们都用 sklearn 的相同模型,为何偏偏我的预测精度比较低?这是因为,很多超参数也会影响预测准确率。比如,当我们改变本例的 k 值时,准确率可能大幅改变。我们可以修改代码第 13 行的参数,重新看一下结果是怎样的。
甚至,我们可以改变第 09 行的随机种子数值,它也能影响预测准确率。这是为啥呢?在前面我们已经提到,随机种子的选取可以影响训练集和测试集数据的提取,而分类算法通常是对数据敏感的。
很多算法之所以有很高的性能,在一定程度上,都是尝试过设置多种参数的结果。通常耗时不菲,这个过程称为“调参”。因此,当你看到某人论文里有非常高的预测准确率时,羡慕之余,也要捎带怜悯:这得需要多少精力调参啊!在人工智能领域就有这样的自我调侃:所谓人工智能,就是“有多少人工,就有多少智能”。
言归正传。如果我们想评估模型的性能,可利用 sklearn.metric 模块中提供的很多好用的评估函数。这里有一个常见的约定:以 _score 结尾的函数返回一个最大值,值越大越好;以 _error 结尾的函数返回一个最小值,值越小越好。
对于 accuracy_score( ) 函数,它返回分类正确的百分比。它的原型如下:
<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">accuracy_score(y_true, y_pred, normalize=True, sample_weight=None)</pre>
其参数的含义如下:y_true 表示真实的分类向量;y_pred 表示预测正确的向量;normalize 的默认值为 True,用于返回正确分类的百分比,如果为 False,则返回正确分类的样本数;sample_weight 表示样本的权值向量。
这个函数的实现原理非常简单,就是看预测结果和实际结果是否相同,将二者之中对应位置一致的数值加和,然后除以总预测样本数,便可以得到预测准确率。如果你对 Python 和 NumPy 的基础知识比较了解的话,这个函数实际上用一行代码即可实现。
我们以测试集的准确率为例来说明预测准确率是如何计算的。让我们先来看看预测的结果是什么。
<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [2]: y_predict_on_test
Out[2]: array( [2, 2, 2, 1, 0, 2, 1, 0, 0, 1, 2, 0, 1, 2, 2, 2, 0, 0, 1, 0, 0,1, 0, 2, 0, 0, 0, 2, 2, 0, 2, 1, 0, 0, 1, 1, 2, 0, 0, 1, 1, 0, 2, 2, 2])</pre>
以上结果输出了数组中的 0、1 或 2(各种鸢尾花类别的编号)。
下面,我们再来看看实际的标签是什么。
<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [3]: y_test
Out[3]: array([1, 2, 2, 1, 0, 2, 1, 0, 0, 1, 2, 0, 1, 2, 2, 2, 0, 0, 1, 0, 0, 2, 0, 2, 0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 1, 1, 2, 0, 0, 1, 1, 0, 2, 2, 2])</pre>
计算预测准确率,实际上就是求这两个数组之间相同位置的数值一致的比例,简单方法如下。
<pre class="info-box" style="margin: 6px auto; display: block; padding: 10px; font-size: 14px; line-height: 1.6em; color: rgb(68, 68, 68); white-space: pre-wrap; overflow-wrap: break-word; background: none rgb(248, 248, 248); border: 1px solid rgb(225, 225, 225); border-radius: 4px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">In [4]: sum(y_predict_on_test == y_test) / y_test.shape[0] * 100
Out[4]: 93.33333333333333</pre>
这样计算得到的结果和利用 sklearn 的 accuracy_score 方法得到的结果完全一致,请读者朋友自行分析上述代码的语法意义。