跳至主要内容

Python 机器学习入门

在本教程中,您将通过 Python 初步认识机器学习(ML)的世界。为实践理解 ML,您将使用一种知名的机器学习算法——K 近邻(KNN)。
· 14分钟 阅读

使用 AI 探索

您将把 KNN 应用于著名的 Iris 数据集。

注意:您可能会考虑学习 Machine Learning with Python 课程;如果想了解 ML 的发展历程及更多内容,建议阅读这篇文章。

简介

机器学习源自计算机科学,主要研究能从经验中学习的算法设计。要“学习”,算法需要具备某些属性的数据,并据此寻找有意义的预测模式。总体而言,ML 任务可分为概念学习、聚类、预测建模等。ML 算法的终极目标是能够在无人干预的情况下正确做出决策。预测股票或天气就是机器学习算法的两个应用场景。

常见的机器学习算法包括决策树、朴素贝叶斯、随机森林、支持向量机、K 近邻法、K-means 聚类等。

在这类算法中,今天您将使用的是 k 近邻法。

现在的问题是:K 近邻算法到底是什么?让我们来一探究竟!

什么是 k 近邻法?

KNN(k 近邻)是一种监督学习算法。“监督”意味着它在学习阶段会使用训练数据的类别标签。它是基于实例的机器学习算法,新数据点的分类是基于已存储的带标签实例(数据点)。KNN 可用于分类和回归;但更常用于分类。

KNN 中的 k 是一个关键变量,也称为超参数,有助于准确地对数据点进行分类。更准确地说,k 是您在为新数据点分类时希望投票的最近邻数量。

visualization of knn visualization of knn

图 1. KNN 可视化 来源

可以看到,当 k 的值从 1 增加到 7 时,两个类别之间的决策边界会变得更平滑。

那么,这一切是如何实现的?每当有新数据点进来时,算法如何基于存储的数据点对其进行分类?

我们快速按以下步骤理解:

  • 首先,加载所有数据并初始化 k 的值,
  • 然后,使用多种相似度或距离度量(如曼哈顿距离 L1、欧氏距离 L2、余弦相似度、Bhattacharyya 距离、切比雪夫距离等)计算存储数据点与待分类新数据点之间的距离,
  • 接着,将距离值按升序或降序排序,确定前或后 k 个最近邻,
  • 收集这 k 个最近邻的标签,并使用多数投票或加权投票来对新数据点分类。新数据点将被赋予在所有存储数据点中得分最高的某一类别标签,
  • 最后,返回该新实例的预测类别。

预测可分两类:分类(为新数据点赋予一个类别标签)或回归(为新数据点赋予一个数值)。不同于分类,在回归中会将 k 个最近邻的均值赋予新数据点。

KNN 的缺点:其一,为每个新数据点搜索最近邻的计算复杂度较高。其二,确定合适的 k 值有时较为繁琐。其三,在计算最近邻时应使用哪种距离度量也并不总是清晰明确。

理论够多了吧?现在让我们加载、分析并理解今天小教程中将使用的数据。

加载 Iris 数据

Iris 数据集包含 150 个样本,有三个类别:Iris-Setosa、Iris-Versicolor 和 Iris-Virginica。用于唯一标识这三类的四个特征/属性是 sepal-length、sepal-width、petal-length 和 petal-width。

您也可以随意使用其他公开数据集或您的私有数据集。

Sklearn 是一个广泛用于数据科学任务的 Python 机器学习库,包含多种分类、回归和聚类算法,如 support vector machines、random forests、gradient boosting、k-means、KNN 等。在 sklearn 中有名为 datasets 的库,包含可直接加载用于不同任务的多个数据集,其中就包括 Iris。它非常直观易用。现在让我们快速加载 iris 数据集。

from sklearn.datasets import load_iris

load_iris 同时包含数据和每个样本的类别标签。我们来快速提取它们。

data = load_iris().data

data 变量将是一个形状为 (150,4) 的 numpy 数组,包含 150 个样本,每个样本有四个不同属性。每个类别各有 50 个样本。

data.shape
(150, 4)

我们来提取类别标签。

labels = load_iris().target
labels.shape
(150,)

接下来需要合并数据和类别标签,为此将使用一个出色的 Python 库 NumPy。NumPy 支持大型多维数组和矩阵,并提供丰富的高级数学函数来操作这些数组。我们先导入它!

import numpy as np

由于 data 是二维数组,您也需要将 labels 重塑为二维数组。

labels = np.reshape(labels,(150,1))

现在使用 numpy 库中的 concatenate 函数,并设置 axis=-1,即按第二维进行拼接。

data = np.concatenate([data,labels],axis=-1)
data.shape
(150, 5)

接下来导入 Python 的数据分析库 pandas,当您希望以表格方式组织数据并对其进行操作和处理时,它非常有用。尤其是它为数值表格和时间序列提供了数据结构与操作方法。

在今天的教程中,您将大量使用 pandas。

import pandas as pd
names = ['sepal-length', 'sepal-width', 'petal-length', 'petal-width', 'species']
dataset = pd.DataFrame(data,columns=names)

现在,您已经得到了包含数据和类别标签的 dataset 数据框!

在继续之前,请记住 labels 变量中的类别标签是数值,您将把这些数值转换为花名或物种。

为此,您将仅选择 class 列,并将三种数值分别替换为对应的物种名称。您将使用 inplace=True,这会直接修改数据框 dataset。

dataset['species'].replace(0, 'Iris-setosa',inplace=True)
dataset['species'].replace(1, 'Iris-versicolor',inplace=True)
dataset['species'].replace(2, 'Iris-virginica',inplace=True)

我们打印 dataset 的前五行,看看它长什么样!

dataset.head(5)
 sepal-lengthsepal-widthpetal-lengthpetal-widthspecies
05.13.51.40.2Iris-setosa
14.93.01.40.2Iris-setosa
24.73.21.30.2Iris-setosa
34.63.11.50.2Iris-setosa
45.03.61.40.2Iris-setosa

分析您的数据

我们快速看看三种花在可视化后的表现,以及它们彼此之间不仅在数值上、在现实中有多不同!

iris(来源)

让我们使用 scatterplot 可视化上面加载的数据,看看一个变量对另一个变量的影响程度,或者说这两个变量之间的相关性有多大。

您将使用 matplotlib 库通过散点图来可视化数据。

import matplotlib.pyplot as plt

提示:想系统学习在 Python 中可视化数据的不同方法?请查看课程 Introduction to data visualization with matplotlib。

plt.figure(4, figsize=(10, 8))

plt.scatter(data[:50, 0], data[:50, 1], c='r', label='Iris-setosa')

plt.scatter(data[50:100, 0], data[50:100, 1], c='g',label='Iris-versicolor')

plt.scatter(data[100:, 0], data[100:, 1], c='b',label='Iris-virginica')

plt.xlabel('Sepal length',fontsize=20)
plt.ylabel('Sepal width',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.title('Sepal length vs. Sepal width',fontsize=20)
plt.legend(prop={'size': 18})
plt.show()
sepal length x width scatter plot

从上图可以明显看出,Iris setosa 的萼片长度与萼片宽度之间具有较高的相关性。另一方面,Iris versicolor 与 Iris virginica 之间的相关性较弱。与更为密集的 setosa 相比,versicolor 和 virginica 的数据点分布更分散。

我们再快速绘制一下 petal-length 与 petal-width 的图。

plt.figure(4, figsize=(8, 8))

plt.scatter(data[:50, 2], data[:50, 3], c='r', label='Iris-setosa')

plt.scatter(data[50:100, 2], data[50:100, 3], c='g',label='Iris-versicolor')

plt.scatter(data[100:, 2], data[100:, 3], c='b',label='Iris-virginica')
plt.xlabel('Petal length',fontsize=15)
plt.ylabel('Petal width',fontsize=15)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.title('Petal length vs. Petal width',fontsize=15)
plt.legend(prop={'size': 20})
plt.show()
petal length x width scatter plot

在 petal-length 和 petal-width 上,上图同样表明 setosa 花呈现出强相关性,且高度聚集。

接下来,为了进一步验证 petal-length 与 petal-width 的相关性,我们针对三个物种绘制相关矩阵。

dataset.iloc[:,2:].corr()
 petal-lengthpetal-width
petal-length1.0000000.962865
petal-width0.9628651.000000

上表显示,当将三个物种合并时,petal-length 与 petal-width 的强相关性为 0.96。

我们再分别分析三个物种之间的相关性。

dataset.iloc[:50,:].corr() #setosa
 sepal-lengthsepal-widthpetal-lengthpetal-width
sepal-length1.0000000.7425470.2671760.278098
sepal-width0.7425471.0000000.1777000.232752
petal-length0.2671760.1777001.0000000.331630
petal-width0.2780980.2327520.3316301.000000
dataset.iloc[50:100,:].corr() #versicolor
 sepal-lengthsepal-widthpetal-lengthpetal-width
sepal-length1.0000000.5259110.7540490.546461
sepal-width0.5259111.0000000.5605220.663999
petal-length0.7540490.5605221.0000000.786668
petal-width0.5464610.6639990.7866681.000000
dataset.iloc[100:,:].corr() #virginica
 sepal-lengthsepal-widthpetal-lengthpetal-width
sepal-length1.0000000.4572280.8642250.281108
sepal-width0.4572281.0000000.4010450.537728
petal-length0.8642250.4010451.0000000.322108
petal-width0.2811080.5377280.3221081.000000

从上面三张表可以清楚看出,setosa 与 virginica 的 petal-length 与 petal-width 的相关性分别为 0.33 和 0.32,而 versicolor 为 0.78。

接下来,通过绘制直方图来可视化特征分布:

fig = plt.figure(figsize = (8,8))
ax = fig.gca()
dataset.hist(ax=ax)
plt.show()
bar charts

petal-length、petal-width 和 sepal-length 呈单峰分布,而 sepal-width 呈近似高斯分布。这些分析很有用,因为您可以据此考虑使用在这类分布下表现良好的算法。

接下来分析四个属性是否在同一量纲上;这对 ML 至关重要。pandas 数据框有一个内置函数 describe,可以表格形式给出数据的 count、mean、max、min。

dataset.describe()
 sepal-lengthsepal-widthpetal-lengthpetal-width
count150.000000150.000000150.000000150.000000
mean5.8433333.0573333.7580001.199333
std0.8280660.4358661.7652980.762238
min4.3000002.0000001.0000000.100000
25%5.1000002.8000001.6000000.300000
50%5.8000003.0000004.3500001.300000
75%6.4000003.3000005.1000001.800000
max7.9000004.4000006.9000002.500000

您可以看到四个属性的量纲相近,介于 0 与 8 之间,单位为厘米;如有需要,也可进一步缩放到 0 到 1。

虽然我们知道每个类别有 50 个样本,即约占总分布的 33.3%,但我们还是再核对一下!

print(dataset.groupby('species').size())
species
Iris-setosa        50
Iris-versicolor    50
Iris-virginica     50
dtype: int64

数据预处理

在加载并充分分析数据之后,是时候准备将数据馈入 ML 模型了。本节将从两方面预处理数据:数据归一化,以及将数据划分为训练集与测试集。

数据归一化

归一化数据主要有两种方式:

  • 样本归一化:对每个样本单独归一化,
  • 特征归一化:对每个特征在所有样本上以相同方式归一化。

那么,为什么或何时需要归一化数据?对于 Iris 数据是否需要标准化?

答案基本上是:大多数时候。良好的实践是对数据做归一化,以将所有样本带到相同的量纲与范围。当数据不一致时,归一化尤为关键。您可以使用上文提到的 describe() 函数检查不一致性,它会给出 max 与 min。如果某个特征的 max 和 min 明显大于另一个特征,那么将二者归一化到相同量纲非常重要。

假设 X 是范围更大的特征,Y 是范围更小的特征,那么 Y 的影响可能会被 X 的影响所淹没。在这种情况下,对 X 和 Y 进行归一化很重要。

对于 Iris 数据,并不需要归一化。

我们再打印一次 describe() 的结果,看看为何无需归一化。

dataset.describe()
 sepal-lengthsepal-widthpetal-lengthpetal-width
count150.000000150.000000150.000000150.000000
mean5.8433333.0573333.7580001.199333
std0.8280660.4358661.7652980.762238
min4.3000002.0000001.0000000.100000
25%5.1000002.8000001.6000000.300000
50%5.8000003.0000004.3500001.300000
75%6.4000003.3000005.1000001.800000
max7.9000004.4000006.9000002.500000

sepal-length 的取值范围为 4.3 到 7.9,sepal-width 为 2 到 4.4,petal-length 为 1 到 6.9,petal-width 为 0.1 到 2.5。所有特征的取值都在 0.1 到 7.9 之间,属于可接受范围。因此,无需对 Iris 数据集做归一化。

数据划分

这是机器学习的另一重要环节,因为我们的目标是让模型在测试环境下无需人工干预即可做出决策或完成分类。因此,在将 ML 模型投入生产前,需要确保模型能在测试数据上具有良好的泛化能力。

为此,您需要训练集与测试集。回到 Iris 数据,您共有 150 个样本,将用 80% 的数据训练模型,剩余 20% 用于测试。

在数据科学中,您会经常看到一个术语 Overfitting(过拟合),它指模型对训练数据学得很好,但在测试数据上表现不佳。因此,将数据划分为训练集与测试集或验证集,通常有助于判断模型是否过拟合。

用于训练/测试划分,您将使用 sklearn 库的内置函数 train_test_split。我们来进行划分。

from sklearn.model_selection import train_test_split

train_data, test_data, train_label, test_label = train_test_split(dataset.iloc[:,:3], dataset.iloc[:,3], test_size=0.2, random_state=42) 

注意,random_state 是一个种子,输入不同的 random_state 值会导致不同的数据划分。不过,如果保持 random_state 不变,多次运行结果也会保持不变。

我们快速打印训练与测试数据及其标签的形状。

train_data.shape,train_label.shape,test_data.shape,test_label.shape
((120, 3), (120,), (30, 3), (30,))

最后,是时候将数据馈入 k 近邻算法了!

KNN 模型

在完成数据的加载、分析和预处理之后,现在将数据输入 KNN 模型。为此,您将使用 sklearn 的内置模块 neighbors 中的 KNeigborsClassifier 类。

先导入分类器。

from sklearn.neighbors import KNeighborsClassifier

注意:k(n_neighbors)参数通常取奇数,以避免投票分数出现平局。

为确定超参数 k 的最佳取值,您将进行所谓的 grid-search。我们将在 10 个不同的 k 值上训练与测试模型,最终选取效果最佳的那个。

初始化变量 neighbors(k),其取值范围 1-9,并各创建一个 numpy 的零矩阵 train_accuracy 与 test_accuracy,分别用于记录训练与测试精度。稍后将用它们绘图以选择最佳 neighbor 值。

neighbors = np.arange(1,9)
train_accuracy =np.zeros(len(neighbors))
test_accuracy = np.zeros(len(neighbors))

接下来的代码就是“魔法”发生之处。我们将 enumerate 遍历九个邻居数,对每个邻居数分别在训练集与测试集上做预测,最终将精度存入 train_accuracy 与 test_accuracy 数组。

for i,k in enumerate(neighbors):
    knn = KNeighborsClassifier(n_neighbors=k)

    #Fit the model
    knn.fit(train_data, train_label)

    #Compute accuracy on the training set
    train_accuracy[i] = knn.score(train_data, train_label)

    #Compute accuracy on the test set
    test_accuracy[i] = knn.score(test_data, test_label)

接下来使用 matplotlib 绘制训练与测试精度。通过 accuracy vs. varying number of neighbors 的图,您可以选择模型表现最佳的 k 值。

plt.figure(figsize=(10,6))
plt.title('KNN accuracy with varying number of neighbors',fontsize=20)
plt.plot(neighbors, test_accuracy, label='Testing Accuracy')
plt.plot(neighbors, train_accuracy, label='Training accuracy')
plt.legend(prop={'size': 20})
plt.xlabel('Number of neighbors',fontsize=20)
plt.ylabel('Accuracy',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.show()
knn accuracy chart

从上图来看,当 n_neighbors=3 时,模型在训练与测试上表现都最好。因此我们固定使用 n_neighbors=3 并重新训练一次。

knn = KNeighborsClassifier(n_neighbors=3)

#Fit the model
knn.fit(train_data, train_label)

#Compute accuracy on the training set
train_accuracy = knn.score(train_data, train_label)

#Compute accuracy on the test set
test_accuracy = knn.score(test_data, test_label)

评估您的模型

在本教程的最后一部分,您将使用 confusion_matrix 和 classification_report 等方法在测试数据上评估模型。

先来查看模型在测试数据上的准确率。

test_accuracy
0.9666666666666667

太棒了!模型似乎能够以 96.66% 的准确率正确分类测试数据。是不是很惊人?只用几行代码,您就训练出了一个 ML 模型,它现在仅用四个特征就能以 96.66% 的准确率告诉您花的名称。或许它的表现已经远超人类。

混淆矩阵

混淆矩阵主要用于描述模型在测试数据(其真实值或标签已知)上的表现。

Scikit-learn 提供了用于计算混淆矩阵的函数。

prediction = knn.predict(test_data)

下面的 plot_confusion_matrix() 函数已根据该来源进行修改并引用。

import itertools
def plot_confusion_matrix(cm, classes,
                          normalize=False,
                          title='Confusion matrix',
                          cmap=plt.cm.Blues):


    plt.imshow(cm, interpolation='nearest', cmap=cmap)
    plt.title(title)
    plt.colorbar()
    tick_marks = np.arange(len(classes))
    plt.xticks(tick_marks, classes, rotation=45)
    plt.yticks(tick_marks, classes)

    fmt = '.2f' if normalize else 'd'
    thresh = cm.max() / 2.
    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
        plt.text(j, i, format(cm[i, j], fmt),
                 horizontalalignment="center",
                 color="white" if cm[i, j] > thresh else "black")

    plt.ylabel('True label',fontsize=30)
    plt.xlabel('Predicted label',fontsize=30)
    plt.tight_layout()
    plt.xticks(fontsize=18)
    plt.yticks(fontsize=18)
class_names = load_iris().target_names


# Compute confusion matrix
cnf_matrix = confusion_matrix(test_label, prediction)
np.set_printoptions(precision=2)

# Plot non-normalized confusion matrix
plt.figure(figsize=(10,8))
plot_confusion_matrix(cnf_matrix, classes=class_names)
plt.title('Confusion Matrix',fontsize=30)
plt.show()
confusion matrix

从上述 confusion_matrix 图可以看到,除了一个 virginica 被分类为 versicolor 外,其余花朵均被正确分类。

分类报告

分类报告通过给出每个类别的 precision、recall 和 F1 score,帮助您更细致地识别被误分类的类别。我们将使用 sklearn 库来查看分类报告。

from sklearn.metrics import classification_report
print(classification_report(test_label, prediction))
                 precision    recall  f1-score   support

    Iris-setosa       1.00      1.00      1.00        10
Iris-versicolor       0.90      1.00      0.95         9
 Iris-virginica       1.00      0.91      0.95        11

      micro avg       0.97      0.97      0.97        30
      macro avg       0.97      0.97      0.97        30
   weighted avg       0.97      0.97      0.97        30

更进一步!

首先,恭喜所有坚持到最后的您!但这只是开始,前方还有很长的路。

本教程主要讲解了机器学习基础,以及一种 ML 算法(KNN)在 Python 中的实现。您使用的 Iris 数据集规模较小,也相对简单。

如果本教程激发了您继续学习的兴趣,您可以尝试使用其他数据集,或学习更多 ML 算法,并将其应用到 Iris 数据集上,观察对准确率的影响。通过这种方式,您将学到远不止理论!

如果您已充分实践本教程介绍的基础内容及其他机器学习算法,不妨在 Python 与数据分析方面继续进阶。

主题