在第一章我们提到过最常用的监督学习任务是回归(用于预测某个值)和分类(预测某个类别)。在第二章我们探索了一个回归任务:预测房价。我们使用了多种算法,诸如线性回归,决策树,和随机森林(这个将会在后面的章节更详细地讨论)。现在我们将我们的注意力转到分类任务上。

  1. # 导入相应的包
  2. import numpy as np
  3. import pandas as pd
  4. import os
  5. # 为了保持稳定性一致
  6. np.random.seed(42)
  7. # 画图设置
  8. %matplotlib inline
  9. # %config InlineBackend.figure_format = 'svg'
  10. import warnings
  11. warnings.filterwarnings('ignore')
  12. import matplotlib as mpl
  13. import matplotlib.pyplot as plt
  14. import seaborn as sns
  15. # mpl.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体
  16. sns.set_style('darkgrid') # darkgrid,whitegrid,dark,white,ticks

1 MNIST

在本章当中,我们将会使用 MNIST 这个数据集,它有着 70000 张规格较小的手写数字图片,由美国的高中生和美国人口调查局的职员手写而成。这相当于机器学习当中的“Hello World”,人们无论什么时候提出一个新的分类算法,都想知道该算法在这个数据集上的表现如何。机器学习的初学者迟早也会处理 MNIST 这个数据集。
Scikit-Learn 提供了许多辅助函数,以便于下载流行的数据集。MNIST 是其中一个。下面的代码获取 MNIST

  1. # 国内网络环境无法使用下列代码获取数据
  2. from sklearn.datasets import fetch_mldata
  3. mnist = fetch_mldata('MNIST original')
  4. mnist

数据集需要去https://github.com/amplab/datascience-sp14/raw/master/lab7/mldata/mnist-original.mat这个链接里直接下载。

  1. from scipy.io import loadmat
  2. mnist = loadmat('./datasets/mnist-original.mat')
  3. mnist

image.png
般而言,由 sklearn 加载的数据集有着相似的字典结构,这包括:

  • DESCR键描述数据集
  • data键存放一个数组,数组的一行表示一个样例,一列表示一个特征
  • label键存放一个标签数组

让我们看一下这些数组:
image.png
MNIST 有 70000 张图片,每张图片有 784 个特征。这是因为每个图片都是2828像素的,并且每个像素的值介于 0~255 之间。让我们看一看数据集的某一个数字。你只需要将某个实例的特征向量,reshape为2828的数组,然后使用 Matplotlib 的imshow函数展示出来。

  1. some_digit = X[2022]
  2. some_digit_image = some_digit.reshape(28, 28)
  3. plt.imshow(some_digit_image, cmap=mpl.cm.binary)
  4. plt.axis("off")
  5. plt.show()

image.png
这看起来像个 0,实际上它的标签告诉我们:
image.png
下图展示了一些来自 MNIST 数据集的图片。当你处理更加复杂的分类任务的时候,它会让你更有感觉。
image.png

先等一下!你总是应该先创建测试集,并且在验证数据之前先把测试集晾到一边。MNIST 数据集已经事先被分成了一个训练集(前 60000 张图片)和一个测试集(最后 10000 张图片)

  1. X_train,X_test,y_train,y_test=X[:60000],X[60000:],y[:60000],y[60000:]

让我们打乱训练集。这可以保证交叉验证的每一折都是相似(你不会期待某一折缺少某类数字)。而且,一些学习算法对训练样例的顺序敏感,当它们在一行当中得到许多相似的样例,这些算法将会表现得非常差。打乱数据集将保证这种情况不会发生。

  1. #打乱训练集的index
  2. import numpy as np
  3. shuffle_index = np.random.permutation(60000)
  4. X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]

2 训练一个二分类器

现在我们简化一下问题,只尝试去识别一个数字,比如说,数字 5。这个“数字 5 检测器”就是一个二分类器,能够识别两类别,“是 5”和“非 5”。让我们为这个分类任务创建目标向量:

  1. #为分类任务创建一个识别数字0的目标向量
  2. y_train_0 = (y_train == 0)#如果y训练集数字为5则进入y_train_0 这个集合,否则就丢弃
  3. y_test_0 = (y_test == 0)

现在让我们挑选一个分类器去训练它。用随机梯度下降分类器 SGD,是一个不错的开始。使用 Scikit-Learn 的SGDClassifier类。这个分类器有一个好处是能够高效地处理非常大的数据集。这部分原因在于 SGD 一次只处理一条数据,这也使得 SGD 适合在线学习(online learning)。我们在稍后会看到它。让我们创建一个SGDClassifier和在整个数据集上训练它。

  1. from sklearn.linear_model import SGDClassifier
  2. sgd_clf = SGDClassifier(random_state=42)
  3. sgd_clf.fit(X_train, y_train_0)

SGDClassifier依赖于训练集的随机程度(所以被命名为 stochastic,随机之义)。如果你想重现结果,你应该固定参数random_state

现在你可以用它来查出数字 0 的图片。

  1. sgd_clf.predict([some_digit])

image.png
分类器猜测这个数字代表 0TRUE)。看起来在这个例子当中,它猜对了。现在让我们评估这个模型的性能。

3 对性能的评估

评估一个分类器,我们将会花大量的篇幅在这个话题上。有许多量度性能的方法。

3.1 使用交叉验证测量准确性

评估一个模型的好方法是使用交叉验证,就像第二章所做的那样。

实现交叉验证

在交叉验证过程中,有时候你会需要更多的控制权,相较于函数cross_val_score()或者其他相似函数所提供的功能。这种情况下,你可以实现你自己版本的交叉验证。事实上它相当简单。以下代码粗略地做了和cross_val_score()相同的事情,并且输出相同的结果。

  1. from sklearn.model_selection import StratifiedKFold
  2. from sklearn.base import clone
  3. #
  4. skfolds = StratifiedKFold(n_splits=3,shuffle=True,random_state=42)
  5. for train_index, test_index in skfolds.split(X_train, y_train_0):
  6. clone_clf = clone(sgd_clf)
  7. X_train_folds = X_train[train_index]
  8. y_train_folds = y_train_0[train_index]
  9. X_test_fold = X_train[test_index]
  10. y_test_fold = y_train_0[test_index]
  11. clone_clf.fit(X_train_folds, y_train_folds)
  12. y_pred = clone_clf.predict(X_test_fold)
  13. n_correct = sum(y_pred == y_test_fold)
  14. print(n_correct / len(y_pred))

StratifiedKFold(分类)和Kfold(回归)的区别

StratifiedKFlod:分层采样,训练集与测试集中各类别样本的比列与原始数据中相同;(分类)
KFlod:分层采样,将数据分成训练集和测试集,不考虑训练集与测试集中各类别数据是否相同;(回归)
KFold(n_split, shuffle, random_state)与stratifiedKFold(n_split, shuffle, random_state)的参数:

  • n_splits:表示将数据划分几等份
  • shuffle:在每次划分时,是否进行洗牌,若为False,其效果相当于random_state为整数(含零),每次划分的结果相同。若为True,每次划分的结果不一样,表示经过洗牌,随机取样的
  • random_state:随机种子数,当设定值(一般为0)后可方便调参,因为每次生成的数据集相同。

StratifiedKFold和Kfold的主要区别是StratifiedKFold抽样时考虑各类别样本占原始数据中的比例,而Kfold则不考虑。因此StratifiedKFold适用于分类问题而Kfold适用于回归问题

StratifiedKFold类实现了分层采样(详见第二章的解释),生成的折(fold)包含了各类相应比例的样例。在每一次迭代,上述代码生成分类器的一个克隆版本,在训练折(training folds)的克隆版本上进行训练,在测试折(test folds)上进行预测。然后它计算出被正确预测的数目和输出正确预测的比例。

让我们使用cross_val_score()函数来评估SGDClassifier模型,同时使用 K 折交叉验证,此处让k=3。记住:K 折交叉验证意味着把训练集分成 K 折(此处 3 折),然后使用一个模型对其中一折进行预测,对其他折进行训练。

  1. from sklearn.model_selection import cross_val_score
  2. cross_val_score(sgd_clf,X_train,y_train_0,cv=3,scoring='accuracy')

image.png
哇!在交叉验证上有大于 98% 的精度(accuracy)?这看起来很令人吃惊。先别高兴,让我们来看一个非常笨的分类器去分类,看看其在“非 0”这个类上的表现。

  1. from sklearn.base import BaseEstimator
  2. class Not0_Classifier(BaseEstimator):
  3. def fit(self, X, y=None):
  4. pass
  5. def predict(self, X):
  6. return np.zeros((len(X), 1), dtype=bool)
  7. Not0_clf=Not0_Classifier()
  8. cross_val_score(Not0_clf,X_train,y_train_0,cv=3,scoring='accuracy')

image.png
没错,这个笨的分类器也有 90% 的精度。这是因为只有 10% 的图片是数字 0,所以你总是猜测某张图片不是0,你也会有 90% 的可能性是对的。
这证明了为什么准确率(accuracy)通常来说不是一个好的性能度量指标,特别是当你处理有偏差的数据集,比方说其中一些类比其他类频繁得多。

3.2 混淆矩阵

对分类器来说,一个好得多的性能评估指标混淆矩阵。大体思路是:输出类别A被分类成类别 B 的次数。举个例子,为了知道分类器将 5 误分为 3 的次数,你需要查看混淆矩阵的第五行第三列。
为了计算混淆矩阵,首先你需要有一系列的预测值,这样才能将预测值与真实值做比较。你或许想在测试集上做预测。但是我们现在先不碰它。(记住,只有当你处于项目的尾声,当你准备上线一个分类器的时候,你才应该使用测试集)。相反,你应该使用cross_val_predict()函数。

  1. from sklearn.model_selection import cross_val_predict
  2. y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_0, cv=3)

就像 cross_val_score(),cross_val_predict()也使用 K 折交叉验证。它不是返回一个评估分数,而是返回基于每一个测试折做出的一个预测值(用布尔值表示结果)。这意味着,对于每一个训练集的样例,你得到一个干净的预测(“干净”是说一个模型在训练过程当中没有用到测试集的数据)。

混淆矩阵

混淆矩阵(Confusion Matrix)是评价模型精度的一种标准格式,用一个N行N列的矩阵形式来表示。矩阵每一列代表预测值,每一行代表实际值。
重要概念:

  • 真正例(True Positive,TP):样本的真实类别是正例,并且模型预测的结果也是正例
  • 真负例(True Negative,TN):样本的真实类别是负例,并且模型将其预测成为负例
  • 假正例(False Positive,FP):样本的真实类别是负例,但是模型将其预测成为正例
  • 假负例(False Negative,FN):样本的真实类别是正例,但是模型将其预测成为负例

image.png

现在使用 confusion_matrix()函数,你将会得到一个混淆矩阵。传递目标类(y_train_0)和预测类(y_train_pred)给它。

  1. from sklearn.metrics import confusion_matrix
  2. cnf_matrix=confusion_matrix(y_train_0, y_train_pred)
  3. cnf_matrix

image.png
混淆矩阵中的每一行表示一个实际的类, 而每一列表示一个预测的类。该矩阵的第一行认为“非 0”(反例)中的 53702 张图被正确归类为 “非0”(他们被称为真反例,true negatives), 而其余 375张 被错误归类为”是 5” (假正例,false positives)。第二行认为“是 0” (正例)中的 319 张被错误地归类为“非 0”(假反例,false negatives),其余 5604张 正确分类为 “是 0”类(真正例,true positives)。一个完美的分类器将只有真反例和真正例,所以混淆矩阵的非零值仅在其主对角线(左上至右下)。
下面我们通过用matplotlib画图,来展示一下混淆矩阵:
首先写一个底图的函数plot_confusion_matrix()

  1. def plot_confusion_matrix(cm, classes,
  2. title='Confusion matrix',
  3. cmap=plt.cm.Blues):
  4. plt.imshow(cm,cmap=cmap)
  5. plt.title(title)
  6. plt.colorbar()
  7. plt.rc('font',family='SimHei')
  8. tick_marks = np.arange(len(classes))
  9. plt.xticks(tick_marks, classes)
  10. plt.yticks(tick_marks, classes)
  11. thresh = cm.max() / 2.
  12. for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
  13. plt.text(j, i, cm[i, j],
  14. horizontalalignment="center",
  15. color="white" if cm[i, j] > thresh else "black")
  16. plt.tight_layout()
  17. plt.ylabel('真实值')
  18. plt.xlabel('预测值')

再把我们刚刚用confusion_matrix方法求得的矩阵带入函数直接画图

  1. import itertools
  2. class_names = ['Positive','Negative']
  3. plt.figure(dpi=400)
  4. plt.grid(False)
  5. plot_confusion_matrix(cnf_matrix
  6. , classes=class_names
  7. , title='Confusion matrix')
  8. plt.show()

image.png
混淆矩阵可以提供很多信息。有时候你会想要更加简明的指标。一个有趣的指标是正例预测的精度,也叫做分类器的准确率(precision)
第三章: 分类问题 - 图12
其中TP是真正例的数目,FP是假正例的数目。
想要一个完美的准确率,一个平凡的方法是构造一个单一正例的预测和确保这个预测是正确的(precision = 1/1 = 100%)。但是这什么用,因为分类器会忽略所有样例,除了那一个正例。所以准确率一般会伴随另一个指标一起使用,这个指标叫做召回率(recall),也叫做敏感度(sensitivity)或者真正例率(true positive rate, TPR)。这是正例被分类器正确探测出的比率。
第三章: 分类问题 - 图13
如果对混淆矩阵感到困惑,下面这个图将对你有帮助

image.png

3.3 准确率与召回率

Scikit-Learn 提供了一些函数去计算分类器的指标,包括精度和召回率。

  1. from sklearn.metrics import precision_score, recall_score
  2. precision_score(y_train_0, y_pred) # precision == 5604 / (5604 + 375)

image.png

  1. recall_score(y_train_0,y_train_pred) # recall == 5604 / (5604 + 319)

image.png
通常结合准确率和召回率会更加方便,这个指标叫做“F1 值”,特别是当你需要一个简单的方法去比较两个分类器的优劣的时候。F1 值是准确率和召回率的谐波均值。普通的平均值平等地看待所有的值,而谐波均值会给小的值更大的权重。所以,要想分类器得到一个高的 F1 值,需要召回率和准确率同时高。
F1值的计算公式:
第三章: 分类问题 - 图17

  1. from sklearn.metrics import f1_score
  2. f1_score(y_train_0,y_train_pred)

image.png
F1 对那些有着相近准确率和召回率的分类器支持度更高。这不会总是你想要的。有的场景你会绝大程度地关心准确率,而另外一些场景你会更关心召回率。举例子,如果你训练一个分类器去检测视频是否适合儿童观看,你会倾向选择那种即便拒绝了很多好视频、但保证所保留的视频都是好(高准确率)的分类器,而不是那种高召回率、但让坏视频混入的分类器(这种情况下你或许想增加人工去检测分类器选择出来的视频)。另一方面,加入你训练一个分类器去检测监控图像当中的窃贼,有着 30% 准确率、99% 召回率的分类器或许是合适的(当然,警卫会得到一些错误的报警,但是几乎所有的窃贼都会被抓到)。
不幸的是,你不能同时拥有两者。增加准确率会降低召回率,反之亦然。这叫做准确率与召回率之间的折衷。

3.4 准确率/召回率之间的折衷

为了弄懂这个折衷,我们看一SGDClassifier是如何做分类决策的。对于每个样例,它根据决策函数计算分数,如果这个分数大于一个阈值,它会将样例分配给正例,否则它将分配给反例。下面的图片显示了几个数字从左边的最低分数排到右边的最高分。假设决策阈值位于中间的箭头(介于两个 5 之间):您将发现4个真正例(数字 5)和一个假正例(数字 6)在该阈值的右侧。因此,使用该阈值,准确率为 80%(4/5)。但实际有 6 个数字 5,分类器只检测 4 个, 所以召回是 67% (4/6)。现在,如果你提高阈值(移动到右侧的箭头),假正例(数字 6)成为一个真反例,从而提高准确率(在这种情况下高达 100%),但一个真正例 变成假反例,召回率降低到 50%。相反,降低阈值可提高召回率、降低准确率

image.png

Scikit-Learn 不让你直接设置阈值,但是它给你提供了设置决策分数的方法,这个决策分数可以用来产生预测。它不是调用分类器的predict()方法,而是调用decision_function()方法。这个方法返回每一个样例的分数值,然后基于这个分数值,使用你想要的任何阈值做出预测

  1. y_scores = sgd_clf.decision_function([some_digit])
  2. y_scores
  3. # 这里的reshape是一个重点,因为fit中的数据的形状为60000*784
  4. # 而原本的some_digit的形状为一个list,所以我们要进行reshape操作使其变成一个(1, -1)的矩阵

image.png

  1. threshold = 0
  2. y_some_digit_pred = (y_scores > threshold)
  3. y_some_digit_pred

SGDClassifier用了一个等于 0 的阈值,所以前面的代码返回了跟predict()方法一样的结果(都返回了true)。让我们提高这个阈值:

  1. threshold = 20000
  2. y_some_digit_pred = (y_scores > threshold)
  3. y_some_digit_pred

当阈值等于 0 的时候,分类器可以探测到这是一个 5,当阈值提高到 20000 的时候,分类器将不能探测到这是数字 5。
如何决定使用哪个阈值呢?首先,你需要再次使用cross_val_predict()得到每一个样例的分数值,但是这一次指定返回一个决策分数,而不是预测值。

  1. # y_train_0需要用ravel()转化为一维数组形式
  2. y_scores = cross_val_predict(sgd_clf, X_train, y_train_0.ravel(), cv=3,
  3. method="decision_function")

image.png
现在有了这些分数值。对于任何可能的阈值,使用precision_recall_curve(),你都可以计算准确率和召回率:

  1. from sklearn.metrics import precision_recall_curve
  2. precisions, recalls, thresholds = precision_recall_curve(y_train_0, y_scores)

再定义一个函数然后用matplotlib画出精度和召回率相对于阈值的对比图:

  1. def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
  2. plt.figure(dpi=400)
  3. plt.plot(thresholds, precisions[:-1], "b--", label="精度")
  4. plt.plot(thresholds, recalls[:-1], "g-", label="召回率")
  5. plt.xlabel("Threshold")
  6. plt.legend(loc="upper right")
  7. plt.ylim([0, 1.1])
  8. plt.rcParams['axes.unicode_minus'] = False
  9. plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
  10. plt.show()

image.png

你也许会好奇为什么准确率曲线比召回率曲线更加起伏不平。原因是准确率有时候会降低,尽管当你提高阈值的时候,通常来说准确率会随之提高。回头看图 3-3,留意当你从中间箭头开始然后向右移动一个数字会发生什么: 准确率会由 4/5(80%)降到 3/4(75%)。另一方面,当阈值提高时候,召回率只会降低。这也就说明了为什么召回率的曲线更加平滑。

另一个选出好的准确率/召回率折衷的方法是直接画出准确率对召回率的曲线。

  1. def plot_precision_vs_recall(precisions, recalls):
  2. plt.figure(dpi=400)
  3. plt.rc('font',family='SimHei')
  4. plt.plot(recalls, precisions, "b-", linewidth=2)
  5. plt.xlabel("召回率", fontsize=16)
  6. plt.ylabel("精度", fontsize=16)
  7. plt.axis([0, 1.1, 0, 1.1])
  8. plt.figure(figsize=(8, 6))
  9. plot_precision_vs_recall(precisions, recalls)
  10. plt.show()

image.png
可以看到,在召回率在 90% 左右的时候,精度急剧下降。你可能会想选择在急剧下降之前选择出一个精度/召回率折衷点。比如说,在召回率 85% 左右的点。当然,这取决于你的项目需求。
我们假设你决定达到 90% 的准确率。你查阅第一幅图(放大一些),在 0 附近找到一个阈值。为了作出预测(目前为止只在训练集上预测),你可以使用np.argmax()的方法,而不是运行分类器的predict()方法:

  1. threshold_90_precision=thresholds[np.argmax(precisions>=0.9)]
  2. threshold_90_precision

image.png
我们根据这个方法得知,精度在达到90%是的阈值在-4124左右。

  1. y_train_pred_90 = (y_scores > threshold_90_precision)#阈值为-4124

让我们检查这些预测的准确率和召回率:

  1. y_train_pred_90 = (y_scores >=threshold_90_precision)
  2. precision_score(y_train_0.ravel(),y_train_pred_90)

image.png

  1. recall_score(y_train_0.ravel(),y_train_pred_90)

image.png

3.5 ROC 曲线(重要)

受试者工作特征(ROC)曲线是另一个二分类器常用的工具。它非常类似与准确率/召回率曲线,但不是画出准确率对召回率的曲线,ROC 曲线是真正例率(true positive rate,另一个名字叫做召回率)对假正例率(false positive rate, FPR)的曲线。FPR 是反例被错误分成正例的比率。它等于 1 减去真反例率(true negative rate, TNR)。TNR 是反例被正确分类的比率。TNR 也叫做特异性。所以 ROC 曲线画出召回率对(1 减特异性)的曲线。
为了画出 ROC 曲线,你首先需要计算各种不同阈值下的 TPR、FPR,使用roc_curve()函数:

  1. from sklearn.metrics import roc_curve
  2. fpr, tpr, thresholds = roc_curve(y_train_0, y_scores)

然后你可以使用 matplotlib,画出 FPR 对 TPR 的曲线。下面的代码生成该分类器的ROC图

  1. def plot_roc_curve(fpr, tpr, label=None):
  2. plt.figure(dpi=400)
  3. plt.plot(fpr, tpr, linewidth=2, label=label)
  4. plt.plot([0, 1], [0, 1], 'k--')
  5. plt.axis([0, 1.1, 0, 1.1])
  6. plt.xlabel('False Positive Rate', fontsize=16)
  7. plt.ylabel('True Positive Rate', fontsize=16)
  8. plt.figure(figsize=(8, 6))
  9. plot_roc_curve(fpr, tpr)
  10. plt.show()

image.png
这里同样存在折衷的问题:召回率(TPR)越高,分类器就会产生越多的假正例(FPR)。图中的虚线是一个完全随机的分类器生成的 ROC 曲线;一个好的分类器的 ROC 曲线应该像本分类器一样尽可能远离这条线(即向左上角方向靠拢)
一个比较分类器之间优劣的方法是:测量 ROC 曲线下的面积(AUC)。一个完美的分类器的 ROC AUC 等于 1,而一个纯随机分类器的 ROC AUC 等于 0.5。Scikit-Learn 提供了一个函数来计算 ROC AUC:

  1. from sklearn.metrics import roc_auc_score
  2. roc_auc_score(y_train_0, y_scores)

image.png

因为 ROC 曲线跟准确率/召回率曲线(或者叫 PR)很类似,你或许会好奇如何决定使用哪一个曲线呢?一个笨拙的规则是,优先使用 PR 曲线当正例很少,或者当你关注假正例多于假反例的时候。其他情况使用 ROC 曲线。举例子,回顾前面的 ROC 曲线和 ROC AUC 数值,你或许认为这个分类器很棒。但是这几乎全是因为只有少数正例(“是 5”),而大部分是反例(“非 5”)。相反,PR 曲线清楚显示出这个分类器还有很大的改善空间(PR 曲线应该尽可能地靠近右上角)

让我们训练一个RandomForestClassifier,然后拿它的的 ROC 曲线和 ROC AUC 数值去跟SGDClassifier的比较。首先你需要得到训练集每个样例的数值。但是由于随机森林分类器的工作方式,RandomForestClassifier不提供decision_function()方法。相反,它提供了predict_proba()方法。Skikit-Learn 分类器通常二者中的一个。predict_proba()方法返回一个数组,数组的每一行代表一个样例,每一列代表一个类。数组当中的值的意思是:给定一个样例属于给定类的概率。(比如,70% 的概率这幅图是数字 5)

  1. from sklearn.ensemble import RandomForestClassifier
  2. RF_clf = RandomForestClassifier(random_state=42)
  3. y_probas_forest = cross_val_predict(RF_clf, X_train, y_train_0.flatten(), cv=3,
  4. method="predict_proba")

但是要画 ROC 曲线,你需要的是样例的分数,而不是概率。一个简单的解决方法是使用正例的概率当作样例的分数。

  1. y_probas_forest

image.png

  1. y_scores_forest = y_probas_forest[:, 1]# score只取正例的概率作为分数(即第二列的数据)
  2. y_scores_forest

image.png

  1. fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_0,y_scores_forest)

现在我们通过画出两个分类器的ROC_AUC曲线图来对比两个分类器的优劣。

  1. plt.figure(figsize=(12, 6),dpi=400)
  2. plt.xlabel('False Positive Rate', fontsize=16)
  3. plt.ylabel('True Positive Rate', fontsize=16)
  4. plt.plot(fpr, tpr, "b:", label="SGD")
  5. plt.plot(fpr_forest, tpr_forest, label="Random Forest")
  6. plt.legend(loc="lower right")
  7. plt.xlim((-0.01,1.1))
  8. plt.ylim((0,1.1))
  9. plt.show()

image.png
RandomForestClassifier的 ROC 曲线比SGDClassifier的更好:它更靠近左上角。所以,它的 ROC AUC 也会更大。
我们再来看看随机森林分类器的ROC_AUC分数:

  1. #计算随机森林模型的ROC_AUC
  2. roc_auc_score(y_train_0, y_scores_forest)

image.png
得分达到了0.999!再来计算一下精度和召回率

  1. #计算精度和召回率
  2. y_train_pred_forest = cross_val_predict(RF_clf, X_train, y_train_0, cv=3)
  3. precision_score(y_train_0, y_train_pred_forest)

image.png

  1. recall_score(y_train_0, y_train_pred_forest)

image.png
现在你知道如何训练一个二分类器,选择合适的标准,使用交叉验证去评估你的分类器,选择满足你需要的准确率/召回率折衷方案,和比较不同模型的 ROC 曲线和 ROC AUC 数值。现在让我们检测更多的数字,而不仅仅是一个数字 0。

4 多类分类器

二分类器只能区分两个类,而多类分类器(也被叫做多项式分类器)可以区分多于两个类。
一些算法(比如随机森林分类器或者朴素贝叶斯分类器)可以直接处理多类分类问题。其他一些算法(比如 SVM 分类器或者线性分类器)则是严格的二分类器。然后,有许多策略可以让你用二分类器去执行多类分类。
举例子,创建一个可以将图片分成 10 类(从 0 到 9)的系统的一个方法是:训练 10 个二分类器,每一个对应一个数字(探测器 0,探测器 1,探测器 2,以此类推)。然后当你想对某张图片进行分类的时候,让每一个分类器对这个图片进行分类,选出决策分数最高的那个分类器。这叫做“一对所有”(OvA)策略(也被叫做“一对其他”)。
另一个策略是对每一对数字都训练一个二分类器:一个分类器用来处理数字 0 和数字 1,一个用来处理数字 0 和数字 2,一个用来处理数字 1 和 2,以此类推。这叫做“一对一”(OvO)策略。如果有 N 个类。你需要训练N*(N-1)/2个分类器。对于 MNIST 问题,需要训练 45 个二分类器!当你想对一张图片进行分类,你必须将这张图片跑在全部 45 个二分类器上。然后看哪个类胜出。OvO 策略的主要优点是:每个分类器只需要在训练集的部分数据上面进行训练。这部分数据是它所需要区分的那两个类对应的数据。
一些算法(比如 SVM 分类器)在训练集的大小上很难扩展,所以对于这些算法,OvO 是比较好的,因为它可以在小的数据集上面可以更多地训练,较之于巨大的数据集而言。但是,对于大部分的二分类器来说,OvA 是更好的选择。
Scikit-Learn 可以探测出你想使用一个二分类器去完成多分类的任务,它会自动地执行 OvA(除了 SVM 分类器,它使用 OvO)。让我们试一下SGDClassifier.

  1. sgd_clf.fit(X_train, y_train) #注意这里是y_train不是y_train_0了
  2. sgd_clf.predict([some_digit])

image.png
上面的代码在训练集上训练了一个SGDClassifier。这个分类器处理原始的目标类,从 0 到 9(y_train),而不是仅仅探测是否为 0 (y_train_0)。然后它做出一个判断(在这个案例下只有一个正确的数字)。在幕后,Scikit-Learn 实际上训练了 10 个二分类器,每个分类器都产到一张图片的决策数值,选择数值最高的那个类。
为了证明这是真实的,你可以调用decision_function()方法。不是返回每个样例的一个数值,而是返回 10 个数值,一个数值对应于一个类。分数最高的对应的预测值所对应的那一类。

  1. some_digit_scores = sgd_clf.decision_function([some_digit])
  2. some_digit_scores

image.png
可以看到第一个类得分最高,对应的类应该就是数字为0的那一类。

一个分类器被训练好了之后,它会保存目标类别列表到它的属性classes 中去,按照值排序。在本例子当中,在classes 数组当中的每个类的索引方便地匹配了类本身,比如,索引为 0 的类恰好是类别 0 本身。但通常不会这么幸运

训练一个RandomForestClassifier

  1. forest_clf.fit(X_train, y_train)
  2. forest_clf.predict([some_digit])

这次 Scikit-Learn 没有必要去运行 OvO 或者 OvA,因为随机森林分类器能够直接将一个样例分到多个类别。你可以调用predict_proba(),得到样例对应的类别的概率值的列表:

  1. RF_clf.predict_proba([some_digit])

image.png
可见为0的概率达到了96%。
想评估这些分类器。像平常一样,你想使用交叉验证。让我们用cross_val_score()来评估SGDClassifier的准确性。

  1. cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")

image.png

在所有测试折(test fold)上,SGD模型它有 87% 的准确性。如果你是用一个随机的分类器,你将会得到 10% 的正确率。所以这不是一个坏的分数,但是你可以做的更好。举例子,简单将输入正则化,将会提高精度到 90% 以上。

  1. from sklearn.preprocessing import StandardScaler
  2. scaler = StandardScaler()
  3. X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
  4. cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")

image.png
可见正则化后SGD模型的准确性评分提升到了90%以上。
再用cross_val_score()来评估RandomForestClassifier的准确性。

  1. cross_val_score(RF_clf, X_train, y_train, cv=3, scoring="accuracy")

image.png
RF模型的准确性达到了96%以上

5 误差分析

如果这是一个实际的项目,你会在你的机器学习项目当中,跟随以下步骤(见附录 B):探索准备数据的候选方案,尝试多种模型,把最好的几个模型列为入围名单,用GridSearchCV调试超参数,尽可能地自动化,像你前面的章节做的那样。在这里,我们假设你已经找到一个不错的模型,你试图找到方法去改善它。一个方式是分析模型产生的误差的类型。
首先,你可以检查混淆矩阵。你需要使用cross_val_predict()做出预测,然后调用confusion_matrix()函数,像你早前做的那样。

  1. y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
  2. conf_mx = confusion_matrix(y_train, y_train_pred)
  3. conf_mx

image.png
这个矩阵的数字比较多,看起来不是很直观,现在我们使用 Matplotlib 的matshow()函数,将混淆矩阵以图像的方式呈现,将会更加方便。

  1. plt.matshow(conf_mx, cmap=plt.cm.gray)
  2. plt.show()

image.png
这个混淆矩阵看起来相当好,因为大多数的图片在主对角线上。在主对角线上意味着被分类正确。数字 5 对应的格子看起来比其他数字要暗淡许多。这可能是数据集当中数字 5 的图片比较少,又或者是分类器对于数字 5 的表现不如其他数字那么好。你可以验证两种情况。
让我们关注仅包含误差数据的图像呈现。首先你需要将混淆矩阵的每一个值除以相应类别的图片的总数目。这样子,你可以比较错误率,而不是绝对的错误数(这对大的类别不公平)。

  1. row_sums = conf_mx.sum(axis=1, keepdims=True)
  2. norm_conf_mx = conf_mx / row_sums

然后再用0来填充对角线,然后再对各个分类器错误率高低进行画图。

  1. np.fill_diagonal(norm_conf_mx, 0)
  2. plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
  3. plt.show()

image.png
现在你可以清楚看出分类器制造出来的各类误差。记住:行代表实际类别,列代表预测的类别。第 5行第 8 列相当亮,这告诉你许多实际数字是5的图片被误分成数字 8。误差图不是严格对称的。举例子,比起将数字 8 误分类为数字 5 的数量,有更多的数字 5 被误分类为数字 8。并且有相当多的其他数字被错误分类为8,整个第8列都偏亮
分析混淆矩阵通常可以给你提供直观的角度去改善你的分类器。比如这幅图,看样子你应该努力改善分类器在数字 8 上的表现,和纠正数字8与数字5的混淆。举例子,你可以尝试去收集更多的数据,或者你可以构造新的、有助于分类器的特征。举例子,写一个算法去数闭合的环(比如,数字 8 有两个环,数字 6 有一个, 5 没有)。又或者你可以预处理图片(比如,使用 Scikit-Learn,Pillow, OpenCV)去构造一个模式,比如闭合的环。
举例子,我们可以画出数字 5 和 8 的例子:

  1. def plot_digits(instances,images_per_row=5,**options):
  2. size=28
  3. # 每一行有一个
  4. image_pre_row=min(len(instances),images_per_row)
  5. images=[instances.reshape(size,size) for instances in instances]
  6. #有几行
  7. n_rows=(len(instances)-1) // image_pre_row+1
  8. row_images=[]
  9. n_empty=n_rows*image_pre_row-len(instances)
  10. images.append(np.zeros((size,size*n_empty)))
  11. for row in range(n_rows):
  12. # 每一次添加一行
  13. rimages=images[row*image_pre_row:(row+1)*image_pre_row]
  14. # 对添加的每一行的额图片左右连接
  15. row_images.append(np.concatenate(rimages,axis=1))
  16. # 对添加的每一列图片 上下连接
  17. image=np.concatenate(row_images,axis=0)
  18. plt.imshow(image,cmap=mpl.cm.binary,**options)
  19. plt.axis("off")

这里先用plt.imshow()定义一个画图的函数,目的是可以把每行显示5张图片,然后再拼接到一起。然后再将所有的数字5和8(包括实际值和预测值)用4个子图的方式画出来,构成类似混淆矩阵的样式:

  1. #需要先对y_train和y_train_pred进行reshape转置成同一维度
  2. cl_a, cl_b = 5, 8
  3. X_aa = X_train[(y_train.reshape(-1) == cl_a) & (y_train_pred.reshape(-1) == cl_a)]
  4. X_ab = X_train[(y_train.reshape(-1) == cl_a) & (y_train_pred.reshape(-1) == cl_b)]
  5. X_ba = X_train[(y_train.reshape(-1) == cl_b) & (y_train_pred.reshape(-1) == cl_a)]
  6. X_bb = X_train[(y_train.reshape(-1) == cl_b) & (y_train_pred.reshape(-1) == cl_b)]
  7. plt.figure(figsize=(8,8))
  8. plt.subplot(221); plot_digits(X_aa[:25], images_per_row=5)
  9. plt.subplot(222); plot_digits(X_ab[:25], images_per_row=5)
  10. plt.subplot(223); plot_digits(X_ba[:25], images_per_row=5)
  11. plt.subplot(224); plot_digits(X_bb[:25], images_per_row=5)
  12. plt.show()

image.png
观察此图,左上角为实际值为5预测值也为5,右上角为实际值为5预测值为8。
左下角为实际值为8预测值为5,右上角为实际值为8预测值也为8。
以是否为数字5做为分类器那么上图可以用以下的混淆矩阵表示:
image.png
在我们看来都是显而易见的错误。很难明白为什么分类器会分错。原因是我们使用的简单的SGDClassifier,这是一个线性模型。它所做的全部工作就是分配一个类权重给每一个像素,然后当它看到一张新的图片,它就将加权的像素强度相加,每个类得到一个新的值。所以,因为 5 和 8只有一小部分的像素有差异,这个模型很容易混淆它们,减少5/8混淆的一个方法是对图片进行预处理,确保它们都很好地中心化和不过度旋转。这同样很可能帮助减轻其他类型的错误。

6 多标签分类

到目前为止,所有的样例都总是被分配到仅一个类。有些情况下,你也许想让你的分类器给一个样例输出多个类别。比如说,思考一个人脸识别器。如果对于同一张图片,它识别出几个人,它应该做什么?当然它应该给每一个它识别出的人贴上一个标签。比方说,这个分类器被训练成识别三个人脸,Alice,Bob,Charlie;然后当它被输入一张含有 Alice 和 Bob 的图片,它应该输出[1,0,1](意思是:Alice 是,Bob 不是,Charlie 是)。这种输出多个二值标签的分类系统被叫做多标签分类系统。
我们可以先看一个简单点的例子,仅仅是为了阐明的目的:

  1. from sklearn.neighbors import KNeighborsClassifier
  2. y_train_large = (y_train >= 7)
  3. y_train_odd = (y_train % 2 == 1)
  4. y_multilabel = np.c_[y_train_large, y_train_odd]
  5. knn_clf = KNeighborsClassifier()
  6. knn_clf.fit(X_train, y_multilabel)

这段代码创造了一个y_multilabel数组,里面包含两个目标标签。第一个标签指出这个数字是否为大数字(7,8 或者 9),第二个标签指出这个数字是否是奇数。接下来几行代码会创建一个KNeighborsClassifier样例(它支持多标签分类,但不是所有分类器都可以),然后我们使用多目标数组来训练它。现在你可以生成一个预测,然后它输出两个标签:

  1. knn_clf.predict([some_digit])

image.png
它工作正确。数字 0 不是大于7的数(False),同时也不是一个奇数(False)。
有许多方法去评估一个多标签分类器,和选择正确的量度标准,这取决于你的项目。举个例子,一个方法是对每个个体标签去量度 F1 值(或者前面讨论过的其他任意的二分类器的量度标准),然后计算平均值。下面的代码计算全部标签的平均 F1 值:

  1. y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_train, cv=3)
  2. f1_score(y_train, y_train_knn_pred, average="macro")

image.png
这里假设所有标签有着同等的重要性,但可能不是这样。特别是,如果你的 Alice 的照片比 Bob 或者 Charlie 更多的时候,也许你想让分类器在 Alice 的照片上具有更大的权重。一个简单的选项是:给每一个标签的权重等于它的支持度(比如,那个标签的样例的数目)。为了做到这点,简单地在上面代码中设置average=”weighted”。

7 多输出分类

我们即将讨论的最后一种分类任务被叫做“多输出-多类分类”(或者简称为多输出分类)。它是多标签分类的简单泛化,在这里每一个标签可以是多类别的(比如说,它可以有多于两个可能值)。
为了说明这点,我们建立一个系统,它可以去除图片当中的噪音。它将一张混有噪音的图片作为输入,期待它输出一张干净的数字图片,用一个像素强度的数组表示,就像 MNIST 图片那样。注意到这个分类器的输出是多标签的(一个像素一个标签)和每个标签可以有多个值(像素强度取值范围从 0 到 255)。所以它是一个多输出分类系统的例子。

分类与回归之间的界限是模糊的,比如这个例子。按理说,预测一个像素的强度更类似于一个回归任务,而不是一个分类任务。而且,多输出系统不限于分类任务。你甚至可以让你一个系统给每一个样例都输出多个标签,包括类标签和值标签。

让我们从 MNIST 的图片创建训练集和测试集开始,然后给图片的像素强度添加噪声,这里是用 NumPy 的randint()函数。目标图像是原始图像。

  1. #生成左边的噪声图
  2. import numpy.random as rnd
  3. noise1 = rnd.randint(0, 100, (len(X_train), 784))
  4. noise2 = rnd.randint(0, 100, (len(X_test), 784))
  5. X_train_mod = X_train + noise1
  6. X_test_mod = X_test + noise2
  7. y_train_mod = X_train
  8. y_test_mod = X_test
  9. plt.subplot(1,2,1)
  10. plt.imshow(X_train_mod[36000].reshape(28,28),cmap=plt.cm.gray)
  11. plt.axis("off")
  12. plt.subplot(1,2,2)
  13. plt.imshow(X_train[36000].reshape(28,28),cmap=plt.cm.gray)
  14. plt.axis("off")

image.png
左边的加噪声的输入图片。右边是干净的目标图片。现在训练KNN模型实现多输出分类(去噪):

  1. knn_clf.fit(X_train_mod, y_train_mod)
  2. clean_digit = knn_clf.predict([X_train_mod[36000]])
  3. plt.imshow(clean_digit.reshape(28,28),cmap=plt.cm.gray)
  4. plt.axis("off")

image.png
可以看到训练的模型能够对每个像素进行分类,从而实现去噪,但是降噪过程中原像素可能会产生一些损失。
现在总结我们的分类之旅。希望你现在应该知道如何选择好的量度标准,挑选出合适的精度/召回率的折衷方案,用ROC下AUC面积大小来比较分类器,更概括地说,就是为不同的任务建立起好的分类系统。

8 练习

  1. 尝试在 MNIST 数据集上建立一个分类器,使它在测试集上的精度超过 97%。提示:KNeighborsClassifier非常适合这个任务。你只需要找出一个好的超参数值(试一下对权重weights和超参数n_neighbors进行网格搜索)。
  2. 写一个函数可以是 MNIST 中的图像任意方向移动(上下左右)一个像素。然后,对训练集上的每张图片,复制四个移动后的副本(每个方向一个副本),把它们加到训练集当中去。最后在扩展后的训练集上训练你最好的模型,并且在测试集上测量它的精度。你应该会观察到你的模型会有更好的表现。这种人工扩大训练集的方法叫做数据增强,或者训练集扩张。
  3. 拿 Titanic 数据集去捣鼓一番。开始这个项目有一个很棒的平台:Kaggle!
  4. 建立一个垃圾邮件分类器(这是一个更有挑战性的练习):
    • 下载垃圾邮件和非垃圾邮件的样例数据。地址是 Apache SpamAssassin 的公共数据集
    • 解压这些数据集,并且熟悉它的数据格式。
    • 将数据集分成训练集和测试集
    • 写一个数据准备的流水线,将每一封邮件转换为特征向量。你的流水线应该将一封邮件转换为一个稀疏向量,对于所有可能的词,这个向量标志哪个词出现了,哪个词没有出现。举例子,如果所有邮件只包含了”Hello”,”How”,”are”, “you”这四个词,那么一封邮件(内容是:”Hello you Hello Hello you”)将会被转换为向量1, 0, 0, 1,或者[3, 0, 0, 2],如果你想数出每个单词出现的次数。
    • 你也许想给你的流水线增加超参数,控制是否剥过邮件头、将邮件转换为小写、去除标点符号、将所有 URL 替换成”URL”,将所有数字替换成”NUMBER”,或者甚至提取词干(比如,截断词尾。有现成的 Python 库可以做到这点)。

然后 尝试几个不同的分类器,看看你可否建立一个很棒的垃圾邮件分类器,同时有着高召回率和高准确率。