从零开始的Python机器学习指南(三)——监督学习之kNN分类

介绍

本博客将结合样例介绍监督学习/Supervised Learning/SL下的另一个大分支:分类/Classification。 准确来说,我们将要用到的分类算法为邻近算法/k-nearest neighbors/kNN

开始前的准备

开始前,请先确保你的python环境中有以下包:
pandasnumpysklearnseaborn

本文的所有代码可在AnacondaJupyter Lab里运行。

正文

分类/Classification问题的本质是什么?
回顾下上一篇博客里讲的回归/Regression问题。回归问题的本质是为未知的数据点找到一个能够最准确预测它们的回归线。比如,给定某些连续变量作为输入,我们可以获取连续变量作为输出。

分类问题与之类似,都是需要找到能最准确描述特征集与标签集关系的模型。但不同的是,分类问题的标签集均为非连续变量。比如,给出一个人的年龄,这个人只能被分类为成年人或未成年人的其中之一;给出一张带有动物的图片,我们要准确识别并分辨动物的种类。也就是说,分类问题可被理解为,对于未知映射 f f f
f : R n ↦ L f:\mathbb{R}^n\mapsto \mathbb{L} f:RnL
,我们有它定义域/Domain R n \mathbb{R}^n Rn即特征集)和对应域/Codomain L \mathbb{L} L即标签集)的一些数组。我们想要通过这些已有的数据来找到一个最符合这些数据的模型。要注意的是,标签集 L \mathbb{L} L为有限集合。所以,区别于回归函数的是,分类函数可以为非参数模型/nonparametric function,即它无需参数(比如线性回归中的权重集 w \bf w w)就可以实现对数据的分类。最简单的例子就是本文将介绍的KNN算法。

KNN是什么?它如何实现分类?
我们从它的名字k-nearest neighbors中就能找到答案:对于一个新的数据点,该算法会判断它与哪些标签的数据最接近,类似“近朱者赤,近墨者黑”。看到一个没见过的东西,我们人类都会把它与我们见过最类似的东西进行比较。

对于一个新的数据特征 x = [ x 1   x 2   …   x n ] x=[x_1\ x_2\ \dots \ x_n] x=[x1 x2  xn],kNN算法需要考虑 k k k个与该数据特征最接近的邻居,并以这些邻居的标签来判断新数据的标签。换句话说,我们以输入的未知数据为球心画一个N维的球,使得球内的数据点恰好为 k k k,则球内的 k k k个点即为算法需要考虑的输入数据最近的邻居。判断的方法通常有两种:

  1. 多数决规则/Majority Rule。邻居里哪个标签最多则定为哪个标签;平票则随机选择。在下图的例子中,问号数据会在k=3的时候被分类为B,k=7的时候被分类为A。
    多数决规则
  2. 基于距离的规则/Distance-based Rule。靠的更近的邻居有更高的权重:靠的越近,权重越高。最终选择加权平均值最高的标签。在右下方图中,绿色数据与问号数据更接近,所以它们在决定问号数据标签时的权重更高。
    基于距离的规则

为了实现第二条规则和判断最近邻居,我们还需定义对于距离的衡量。对于两个数据特征 x x x y y y,二者之间的距离 d d d有以下几种定义方式:
3. L 1 L_1 L1曼哈顿距离/Manhattan Distance d ( x , y ) = ∑ i = 1 n ∣ x i − y i ∣ d(x,y)=\sum_{i=1}^{n} |{x_i-y_i}| d(x,y)=i=1nxiyi。对于平面上的两个点来说,这个公式算的是两点 x x x y y y坐标差值的和(下图中直角三角形的直角边长之和)。
4. L 2 L_2 L2欧几里得距离/Euclidian Distance d ( x , y ) = ∑ i = 1 n ∣ x i − y i ∣ 2 d(x,y)=\sqrt{\sum_{i=1}^{n} |{x_i-y_i}|^2} d(x,y)=i=1nxiyi2 。对于平面上的两个点来说,这个公式算的是两点之间的直线距离(下图中直角三角形的斜边长)。
5. L ∞ L_\infin L切比雪夫距离/Chebyshev Distance d ( x , y ) = max ⁡ 1 ≤ i ≤ n ∣ x i − y i ∣ d(x,y)=\max_{1\le i\le n}|x_i-y_i| d(x,y)=max1inxiyi.。对于平面上的两个点来说,这个公式算的两点 x x x y y y坐标差值的最大值(下图中直角三角形的最长直角边)。
距离公式
由上所述,我们也能看出来:KNN是个非参数模型/nonparametric function,因为模型的决定不是取决于最优的权重,而是取决于所有输入数据。因此,为了保持数据的真实性和全面性,kNN的模型通常来说会体量较大。

该模型的超参/Hyperparameters只有邻居的数量 k k k,并且我们要自己决定用哪种标签判定规则和距离计算方法,所以很多情况下我们可以尝试不同的组合,找到最合适的模型。

最后一点:不是所有的数据都适合用KNN来分类。当数据不好分开的时候(即不同标签的数据有很多重合之处),kNN的表现将会很差。这个时候,我们就要考虑换一个模型,或者用数据工程/Data Engineering来为数据增加额外的信息来分离标签。

代码

理解了原理后,我们可以用python实现上述的kNN分类算法。

我们首先导入需要的库。

import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import numpy as np

我们本次用到的数据集是sklearn包里自带的,是用来分类鸢尾花的数据。数据共有150行4列,特征集包括了花的花瓣与萼片的长度和宽度,标签集则是该鸢尾花所属的物种(Setosa/山鸢尾,Versicolour/杂色鸢尾,Virginica/维吉尼亚鸢尾)。

from sklearn.datasets import load_iris 
iris = load_iris() # 从sklearn中引入数据集

iris_df = pd.DataFrame(iris.data, columns=iris.feature_names) # 创建DataFrame

iris_df['Iris species'] = iris.target # 在DataFrame里加入标签集
print(iris_df) # 看看长什么样

下图为上面代码的输出结果。我们可以看到特征集有四个特征(即DataFrame的前四列),标签集是数字代表的鸢尾花物种。
DataFrame
我们可以简单进行一些可视化。seaborn是一个很好用的画图库,能为我们省下画不同变量排列组合的时间。代码如下:

import seaborn as sb

sb.set(style="ticks", color_codes=True) # 设置视觉效果

g = sb.pairplot(iris_df, hue="Iris species", diag_kind='hist')

我们可以看到,下图就是seaborn画出来的结果。从图中我们可以观察到不同变量两两之间的关系。上面讲到kNN需要有可分离的标签类。总体来说,0类可以很好地从其他类中被分离出来,但1和2类之间似乎有一些重合。我们通过这种简单的观察即可猜测,模型会对0类的分类准确率较高,对1和2类之间的准确率较低。
sb
接下来我们训练模型。训练模型也非常简单,如下:

from sklearn.neighbors import KNeighborsClassifier
neighbors_num = 10 # 考虑的邻居数量
weights = 'uniform' # 多数决规则

classifier = KNeighborsClassifier(n_neighbors=neighbors_num, weights=weights)

classifier.fit(iris.data, iris.target) # 学习数据

我们的模型已经训练好了,现在我们来思考下如何评估它的性能。sklearn用的方法叫做0-1损失函数/Zero-one Loss。简单来讲,记正确分类为0,不正确分类为1,把所有分类的分数都加起来并除以总数据个数,定义如下:
E ˉ = 1 m ∑ t ∈ a l l   d a t a 1 c l f ( t ) ≠ t \bar{E} = \frac{1}{m}\sum_{t \in all\ data} {\bf 1}_{clf(t) \ne t} Eˉ=m1tall data1clf(t)=t
也就是说,准确率就是 1 − E ˉ 1-\bar{E} 1Eˉ。我们可以直接用sklearn.metrics来计算:

from sklearn import metrics 

Y_pred = classifier.predict(iris.data) # 模型预测的标签集

accuracy = metrics.accuracy_score(iris.target, Y_pred)

print('Training accuracy of kNN: {:.3f}'.format(accuracy))
# Training accuracy of kNN: 0.980

可以看到,准确率还是非常高的。但我们怎么知道哪些数据被分错类了呢?我们可以画出混淆矩阵/Confusion Matrix来直观的判断:

from sklearn.metrics import confusion_matrix

def show_confusion_matrix(true_labels, learned_labels, class_names):
	# 用sklearn创建混淆矩阵对象
    cmat = confusion_matrix(true_labels, learned_labels) 
    
	# 设置图像大小
    plt.figure(figsize=(14, 5))
    plt.tick_params(labelsize=8)
    
    # 画出热度图
    hm = sb.heatmap(cmat.T, square=True, annot=True, fmt='d', cbar=True,
                     xticklabels=class_names,
                     yticklabels=class_names, 
                     cmap="seismic", 
                     annot_kws={
    
    "size":12}, cbar_kws={
    
    'label': 'Counts'})

    # 添加图例
    hm.figure.axes[-1].yaxis.label.set_size(10)
    hm.figure.axes[-1].tick_params(labelsize=8)

	# 增加坐标轴标题
    plt.xlabel('True label', fontsize=9)
    plt.ylabel('Predicted label', fontsize=9)
    
    plt.show()

Y_test = iris.target # 真实标签
show_confusion_matrix(Y_test, Y_pred, iris.target_names)

我们得到如下混淆矩阵。可以看到,和我们的猜测一样,模型在分辨1类(versicolor)和2类(virginica)的时候产生了一些错误。我们的模型准确率已经相当高了,但如果准确率较低的话,就要考虑为模型容易犯错的类别添加更多的数据样本,或者为数据提供更多有用的特征(比如叶片厚度等等)。
混淆矩阵
当然,上面选择的k=10是一个我们随意选择的数字。有没有比它更好的邻居数量呢?我们可以尝试取不同的k值并用交叉验证/Cross-validation评估不同模型的表现,如下:

from sklearn.model_selection import cross_validate

features = iris.data # 特征集
labels = iris.target # 标签集

k_fold = 10

for k in [1,2,3,4,5,6,7,8,9,10,20,50]:
    classifier = KNeighborsClassifier(n_neighbors=k, weights='uniform')
    classifier.fit(features, labels)

    cv_results = cross_validate(classifier, features, labels, 
                                cv=k_fold, return_train_score=True)

    print('[{}-NN] Mean test score: {:.3f} (std: {:.3f})'
          '\nMean train score: {:.3f} (std: {:.3f})\n'.format(k,
                                                  np.mean(cv_results['test_score']),
                                                  np.std(cv_results['test_score']),
                                                  np.mean(cv_results['train_score']),
                                                  np.std(cv_results['train_score'])))
'''
[1-NN] Mean test score: 0.960 (std: 0.053)
Mean train score: 1.000 (std: 0.000)

[2-NN] Mean test score: 0.953 (std: 0.052)
Mean train score: 0.979 (std: 0.005)

[3-NN] Mean test score: 0.967 (std: 0.045)
Mean train score: 0.961 (std: 0.007)

[4-NN] Mean test score: 0.967 (std: 0.045)
Mean train score: 0.964 (std: 0.007)

[5-NN] Mean test score: 0.967 (std: 0.045)
Mean train score: 0.969 (std: 0.007)

[6-NN] Mean test score: 0.967 (std: 0.045)
Mean train score: 0.973 (std: 0.008)

[7-NN] Mean test score: 0.967 (std: 0.045)
Mean train score: 0.973 (std: 0.006)

[8-NN] Mean test score: 0.967 (std: 0.045)
Mean train score: 0.980 (std: 0.006)

[9-NN] Mean test score: 0.973 (std: 0.033)
Mean train score: 0.979 (std: 0.006)

[20-NN] Mean test score: 0.980 (std: 0.031)
Mean train score: 0.974 (std: 0.013)

[50-NN] Mean test score: 0.927 (std: 0.036)
Mean train score: 0.933 (std: 0.017)
'''

由上可见,k=1似乎是训练得最好的,但这种情况很明显是模型过度拟合了,因为模型的训练准确率比验证准确率高很多。k=50的时候准确率低了很多,因为我们样本数量不够多(只有150个),考虑的邻居又太多了,导致模型的决策会受到较多误导。我们要尽量挑选一个训练准确率和验证准确率均较高并差值不大的,所以k=9是个比较好的选择。

结语

在下一篇博客博主会介绍如何用非监督学习中的K-均值聚类/K-means Clustering算法实现分类/Classification。有任何问题和建议请随时评论或私信。码字不易,喜欢博主内容的话请点赞支持!

猜你喜欢

转载自blog.csdn.net/EricFrenzy/article/details/131468505