《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类

最近有时间看书了,把以前想看的书捡一下,翻译中顺便加上自己的理解,写个读书笔记。

注意:下面的“数字+s”表示的是数字的类别,加上s是和纯数字区别开。

在上上篇中,我们提到最常见的监督学习任务是回归(预测值)和分类(预测类)。 在上篇中,我们使用各种算法(如线性回归,决策树和随机森林)来探索回归任务,预测住房价值(将在后面进一步详细解释)。 现在我们将注意力转向分类系统。

一、MNIST

在本篇中,我们将使用MNIST数据集,该数据集是由高中学生和美国人口普查局员工手写的70,000个小数字图像。 每个图像都标有它所代表的数字。 这个集已经被研究了很多,它通常被称为机器学习的“Hello World”:每当人们提出新的分类算法时,他们都很想知道它将如何在MNIST上执行。 每当有人学习机器学习时,他们迟早会解决MNIST问题。

Scikit-Learn提供了许多辅助函数来下载流行的数据集。 MNIST就是其中之一。 以下代码获取MNIST数据集:

>>>from	sklearn.datasets import	fetch_mldata
>>>mnist = fetch_mldata('MNIST original')
>>>mnist
{'COL_NAMES':['label', 'data'],
	'DESCR': 'mldata.org dataset:mnist-original',
	'data':	array([[0, 0, 0, ..., 0, 0, 0],
	               [0, 0, 0, ..., 0, 0, 0],
		       [0, 0, 0, ..., 0, 0, 0],
		       ...,
		       [0, 0, 0, ..., 0, 0, 0],
		       [0, 0, 0, ..., 0, 0, 0],
		       [0, 0, 0, ..., 0, 0, 0]], dtype=uint8),
	'target': array([0., 0., 0., ..., 9., 9., 9.])}

由Scikit-Learn加载的数据集通常具有类似的字典结构,包括:

  • 描述数据集的DESCR密钥
  • 包含数组的数据键,每个实例一行,每个功能一列
  • 包含带标签的数组的目标键

我们来看看这些数组:

>>> X, y = mnist["data"], mnist["target"]
>>> X.shape
(70000, 784)
>>> y.shape
(70000,)

有70,000张图像,每张图像有784个特征。 这是因为每个图像是28×28像素,并且每个特征仅表示一个像素的强度,从0(白色)到255(黑色)。 让我们看一下数据集中的一位数。 你需要做的就是获取实例的特征向量,将其重塑为28×28阵列,并使用Matplotlib的imshow()函数显示它:

%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
some_digit = X[36000]
some_digit_image = some_digit.reshape(28, 28)
plt.imshow(some_digit_image, cmap = matplotlib.cm.binary,
interpolation="nearest")
plt.axis("off")
plt.show()

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第1张图片

这看起来像一个5,实际上这就是标签告诉我们:

>>> y[36000]
5.0

下图显示了MNIST数据集中的一些图像,让你了解分类任务的复杂性。

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第2张图片

可是等等! 在检查数据之前,你应该始终创建一个测试集并将其放在一边。 MNIST数据集实际上已经分为训练集(前60,000个图像)和测试集(最后10,000个图像):

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

我们也将训练集洗牌;这将保证所有交叉验证折叠都相似(你一定不希望一个折叠丢失一些数字)。 此外,一些学习算法对训练实例的顺序很敏感,如果它们连续得到许多类似的实例,它们的表现很差。 对数据集进行混洗可确保不会发生这种情况:

import numpy as np

shuffle_index = np.random.permutation(60000)
X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]

二、训练二进制分类器

让我们现在简化问题,只尝试识别一个数字 - 例如,数字5。这个“5检测器”将是一个二元分类器的例子,能够区分两个类,5和不是5。 让我们为这个分类任务创建目标向量:

y_train_5 = (y_train == 5) #True for all 5s, False for all other digits.
y_test_5 = (y_test == 5)

OK,现在让我们选择一个分类器并训练它。 一个好的起点是使用Scikit-Learn的SGDClassifier类的随机梯度下降(SGD)分类器。 该分类器具有能够有效处理非常大的数据集的优点。
这部分是因为SGD独立处理培训实例,一次一个(这也使SGD非常适合在线学习),我们将在后面看到。 让我们创建一个SGDClassifier并在整个训练集上训练它:

from sklearn.linear_model import SGDClassifier

sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)

NOTE:SGDClassifier在训练期间依赖于随机性(因此称为“随机”)。 如果要获得可重现的结果,则应设置random_state参数。

现在你可以使用它来检测数字5的图像:

>>> sgd_clf.predict([some_digit])
array([ True], dtype=bool)

分类器猜测该图像代表5(真)。 看起来在这个特殊情况下猜对了! 现在,让我们评估一下这个模型的表现。

三、绩效评估

评估分类器通常比评估回归器要复杂得多,因此我们将在本篇的大部分内容中讨论此主题。 有很多可用的性能指标,所以再拿一杯咖啡,准备学习许多新的概念和缩略词吧!

1、使用交叉验证测量准确度

评估模型的一种好方法是使用交叉验证,就像在上篇中所做的那样。

实施交叉验证

有时候,你需要对cross_val_score()和类似函数提供的交叉验证过程进行更多控制。 在这些情况下,你可以自己实施交叉验证;它实际上相当简单。 以下代码与前面的cross_val_score()代码大致相同,并打印相同的结果:

from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone

skfolds = StratifiedKFold(n_splits=3, random_state=42)

for train_index, test_index in skfolds.split(X_train, y_train_5):
    clone_clf = clone(sgd_clf)
    X_train_folds = X_train[train_index]
    y_train_folds = (y_train_5[train_index])
    X_test_fold = X_train[test_index]
    y_test_fold = (y_train_5[test_index])

    clone_clf.fit(X_train_folds, y_train_folds)
    y_pred = clone_clf.predict(X_test_fold)
    n_correct = sum(y_pred == y_test_fold)
    print(n_correct / len(y_pred)) # prints 0.9502, 0.96565 and 0.96495

StratifiedKFold类执行分层抽样,以产生包含每个类别的代表性比率的折叠。 在每次迭代时,代码创建分类器的克隆,训练在训练折叠上克隆,并对测试折叠进行预测。 然后它计算正确预测的数量并输出正确预测的比率。

让我们使用cross_val_score()函数来评估您的SGDClassifier模型,使用K折叠交叉验证,有三个折叠。 请记住,K折叠交叉验证意味着将训练集分成K形折叠(在本例中为三个),然后使用在剩余折叠上训练的模型进行预测并在每个折叠上评估它们:

>>> from sklearn.model_selection import cross_val_score
>>> cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([ 0.9502 , 0.96565, 0.96495])

Wow! 所有交叉验证折叠的准确度(正确预测的比率)超过95%?这看起来很神奇,不是吗? 好吧,在你太兴奋之前,让我们看看一个非常愚蠢的分类器,它只是对“not-5”类中的每一个图像进行分类:

from sklearn.base import BaseEstimator

class Never5Classifier(BaseEstimator):
    def fit(self, X, y=None):
        pass
    def predict(self, X):
        return np.zeros((len(X), 1), dtype=bool)

你能确认这个分类器的准确率吗?让我们确认一下:

>>> never_5_clf = Never5Classifier()
>>> cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([ 0.909 , 0.90715, 0.9128 ])

没错,它的准确率超过90%! 这只是因为只有大约10%的图像是5s,所以如果你总是猜测图像不是5,那么你将在90%的时间内做到正确。

这说明了为什么精度通常不是分类器的首选性能度量,尤其是在处理偏斜数据集时(即当某些类比其他类更频繁时)。

2、混乱矩阵

评估分类器性能的更好方法是查看混淆矩阵。 一般的想法是计算A类实例被分类为B类的次数。例如,要知道分类器将5s的图像与3s混淆的次数,您将查看第5行和第3列的混淆矩阵。

要计算混淆矩阵,首先需要有一组预测,以便将它们与实际目标进行比较。 你可以对测试集进行预测,但是现在让它保持不变(记住你只想在项目的最后使用测试集,一旦你有一个准备好发布的分类器)。相反, 你可以使用cross_val_predict()函数:

from sklearn.model_selection import cross_val_predict

y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)

就像cross_val_score()函数一样,cross_val_predict()执行K-fold交叉验证,但不返回评估分数,而是返回每个测试折叠上的预测。 这意味着你可以获得训练集中每个实例的清晰预测(“清除”意味着预测是由在训练期间从未看过数据的模型进行的)。

现在你已准备好使用confusion_matrix()函数获取混淆矩阵。 只需将目标类(y_train_5)和预测类(y_train_pred)传递给它:

>>> from sklearn.metrics import confusion_matrix
>>> confusion_matrix(y_train_5, y_train_pred)
array([[53272, 1307],
       [ 1077, 4344]])

混淆矩阵中的每一行代表一个实际的类,而每一列代表一个预测的类。 该矩阵的第一行考虑非5图像(阴性类):其中53,272个被正确分类为非5s(它们被称为真阴性),而剩余的1,307被错误地分类为5(假阳性)。第二行考虑5s(正类)的图像:1,077被错误地分类为非5s(假阴性),而剩余的4,344被正确分类为5s(真阳性)。 一个完美的分类器只有真正的正面和真正的负面,所以它的混淆矩阵只在其主对角线(左上角到右下角)上有非零值:

>>> confusion_matrix(y_train_5, y_train_perfect_predictions)
array([[54579, 0],
        [0, 5421]])

混淆矩阵为你提供了大量信息,但有时你可能更喜欢更简洁的指标。 一个有趣的问题是积极预测的准确性;这称为分类器的精度

                                                                                          precision = \frac{TP}{TP + FP}

Note:TP是真阳性的数量,FP是假阳性的数量。

获得完美精度的一个简单方法是进行一次正面预测并确保其正确(精度= 1/1 = 100%)。 这不会非常有用,因为分类器会忽略除一个正面实例之外的所有实例。 因此,精度通常与另一个名为召回的度量标准一起使用,也称为敏感度或真阳性率(TPR):这是分类器正确检测到的正实例的比率

                                                                                         precision = \frac{TP}{TP + FN}

Note:FN是假阴性的数量。

如果对混淆矩阵感到困惑,下图可能有所帮助。

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第3张图片

3、精度和召回

Scikit-Learn提供了几种计算分类器指标的功能,包括精度和召回:

>>> from sklearn.metrics import precision_score, recall_score
>>> precision_score(y_train_5, y_pred) # == 4344 / (4344 + 1307)
0.76871350203503808
>>> recall_score(y_train_5, y_train_pred) # == 4344 / (4344 + 1077)
0.79136690647482011

现在你的5探测器看起来并不像你看到它的精确度那样闪亮。当它声称图像代表5时,它只有77%的时间是正确的。 而且,它只检测到5s的79%。

将精度和召回组合成称为F_{1}分数的单个度量通常很方便,特别是如果你需要一种简单的方法来比较两个分类器。 F_{1}分数是精度和召回的调和平均值。 虽然常规均值对所有值均等地进行处理,但是调和均值给予较低值更多的权重。因此,如果召回和精确度都很高,分类器将只获得高F_{1}分数。

要计算F_{1}分数,只需调用f1_score()函数:

>>> from sklearn.metrics import f1_score
>>> f1_score(y_train_5, y_pred)
0.78468208092485547

F_{1}评分倾向于具有相似精度和召回率的分类器。 这并不总是你想要的:在某些情况下,你最关心的是精确度,而在其他情况下,你真的很关心召回。 例如,如果你训练了一个分类器来检测对孩子来说安全的视频,你可能更喜欢一个拒绝许多好视频的分类器(低召回率)但只保留安全的(高精度),而不是一个有很多的分类器更高的召回率,但让你的产品中出现一些非常糟糕的视频(在这种情况下,你甚至可能想要添加人工管道来检查分类器的视频选择)。 另一方面,假设你训练一个分类器来检测监控图像上的商店扒手:如果你的分类器只有30%的精度,只要它有99%的召回率就可能很好(当然,保安人员会得到一些虚假警报, 但几乎所有商店扒手都会被抓住)。

不幸的是,你无法双管齐下:提高精确度可以减少召回,反之亦然。 这称为精确/召回权衡。

4、精确/召回权衡

为了理解这种权衡,让我们看一下SGDClassifier如何做出分类决策。对于每个实例,它根据决策函数计算得分,如果该得分大于阈值,则将实例分配给正类,否则将其分配给否定类。下图显示了从左侧最低分到右侧最高分的几位数。假设决策阈值位于中心箭头(两个5s之间):你将在该阈值的右侧找到4个真正的正数(实际5s),以及一个误报(实际为6s)。因此,使用该阈值,精度为80%(5个中的4个)。但是在6个实际5s里,分类器仅检测到4个,因此召回率为67%(6个中有4个)。现在,如果你提高阈值(将其移动到右边的箭头),误报(6)将成为真正的负数,从而提高精度(在这种情况下高达100%),但是一个真正的正数变为假阴性,将召回率降至50%。相反,降低阈值会增加召回率并降低精确度。

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第4张图片

Scikit-Learn不允许你直接设置阈值,但它确实可以让你访问用于进行预测的决策分数。 可以调用其decision_function()方法,而不是调用分类器的predict()方法,该方法返回每个实例的分数,然后使用你想要的任何阈值根据这些分数进行预测:

>>> y_scores = sgd_clf.decision_function([some_digit])
>>> y_scores
array([ 161855.74572176])
>>> threshold = 0
>>> y_some_digit_pred = (y_scores > threshold)
array([ True], dtype=bool)

SGDClassifier使用等于0的阈值,因此前面的代码返回与predict()方法相同的结果(即True)。 让我们提高门槛:

>>> threshold = 200000
>>> y_some_digit_pred = (y_scores > threshold)
>>> y_some_digit_pred
array([False], dtype=bool)

这证实提高门槛会减少召回。 图像实际上表示为5,并且分类器在阈值为0时检测到它,但是当阈值增加到200,000时它会错过它。
那么如何决定使用哪个阈值? 为此,你首先需要再次使用cross_val_predict()函数获取训练集中所有实例的分数,但这次指定您希望它返回决策分数而不是预测:

y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method="decision_function")

现在有了这些分数,你可以使用precision_recall_curve()函数计算所有可能阈值的精度和召回率:

from sklearn.metrics import precision_recall_curve

precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)

最后,你可以使用Matplotlib绘制精度并将其作为阈值的函数进行调用:

def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1], "b--", label="Precision")
    plt.plot(thresholds, recalls[:-1], "g-", label="Recall")
    plt.xlabel("Threshold")
    plt.legend(loc="upper left")
    plt.ylim([0, 1])

plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.show()

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第5张图片

Note:你可能想知道为什么精确曲线比召回曲线更崎岖。 原因是当你提高阈值时,精度有时会下降(尽管一般情况下它会上升)。 要了解原因,请回顾之前的图并注意当你从中心阈值开始并向右移动一位数时会发生什么:精度从4/5(80%)下降到3/4(75%))。 另一方面,召回只能在阈值增加时下降,这就解释了为什么它的曲线看起来很平滑。

现在,你可以简单地选择让你的任务的最佳精度/召回折衷的阈值。 选择良好的精度/召回权衡的另一种方法是直接绘制精确度以进行召回,如图所示:

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第6张图片

你可以看到,精确度确实开始急剧下降约在召回80%的时候。 你可能希望在该下降之前选择精确/召回权衡 - 例如,召回率约为60%。 但当然,选择取决于你的项目。
所以我们假设你决定瞄准90%的精度。 你查找第一个图(放大一点),发现你需要使用大约70,000的阈值。 要进行预测(暂时在训练集上),你可以运行以下代码,而不是调用分类器的predict()方法:

y_train_pred_90 = (y_scores > 70000)

让我们来查看一下这次预测的预测和召回:

>>> precision_score(y_train_5, y_train_pred_90)
0.8998702983138781
>>> recall_score(y_train_5, y_train_pred_90)
0.63991883416343853

太棒了,你有90%的精度分类器(或足够接近)! 正如你所看到的,创建一个几乎任何你想要的精度的分类器相当容易:只需设置足够高的阈值,就可以了。 嗯,没那么快。 如果召回率太低,高精度分类器就不是很有用了!

如果有人说“让我们达到99%的精确度”,你应该问,“召回怎么办?”

5、ROC曲线

接收器操作特性(ROC)曲线是与二元分类器一起使用的另一种常用工具。 它与精确度/召回曲线非常相似,但ROC曲线不是绘制精确度与召回率,而是绘制真假阳性率(召回的另一个名称)与误报率。 FPR是被错误归类为正面的负面实例的比率。 它等于1减去真实的负面率,即负面实例被正确归类为负面的比率。 TNR也称为特异性。 因此,ROC曲线绘制了灵敏度(召回)与1-特异性。

要绘制ROC曲线,首先需要使用roc_curve()函数计算各种阈值的TPR和FPR:

from sklearn.metrics import roc_curve

fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)

然后,你可以使用Matplotlib针对TPR绘制FPR:

def plot_roc_curve(fpr, tpr, label=None):
    plt.plot(fpr, tpr, linewidth=2, label=label)
    plt.plot([0, 1], [0, 1], 'k--')
    plt.axis([0, 1, 0, 1])
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')

plot_roc_curve(fpr, tpr)
plt.show()

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第7张图片

再次进行权衡:召回(TPR)越高,分类器产生的误报(FPR)越多。 虚线表示纯随机分类器的ROC曲线; 一个好的分类器尽可能远离该线(朝左上角)。

比较分类器的一种方法是测量曲线下面积(AUC)。 完美的分类器具有的ROC AUC等于1,而纯随机分类器具有的ROC AUC等于0.5。 Scikit-Learn提供了计算ROC AUC的功能:

>>> from sklearn.metrics import roc_auc_score
>>> roc_auc_score(y_train_5, y_scores)
0.97061072797174941

Note:由于ROC曲线与精度/召回(或PR)曲线非常相似,你可能想知道如何决定使用哪一条曲线。 根据经验,只要阳性类很少或者你更关心假阳性而不是假阴性,你应该更喜欢PR曲线,否则你应该更喜欢ROC曲线。 例如,查看之前的ROC曲线(以及ROC AUC分数),您可能会认为分类器非常好。 但这主要是因为与阴性(非5s)相比,阳性(5s)很少。 相反,PR曲线清楚地表明分类器有改进的余地(曲线可能更靠近右上角)。

让我们训练一个RandomForestClassifier,并将其ROC曲线和ROC AUC分数与SGDClassifier进行比较。 首先,你需要获得训练集中每个实例的分数。 但是由于它的工作方式,RandomForestClassifier类没有decision_function()方法。 相反,它有一个pre dict_proba()方法。 Scikit-Learn分类器通常有一个或另一个。 predict_proba()方法返回一个数组,每个实例包含一行,每个类包含一列,每个数组包含给定实例属于给定类的概率(例如,图像代表5的概率为70%):

from sklearn.ensemble import RandomForestClassifier

forest_clf = RandomForestClassifier(random_state=42)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3, method="predict_proba")

但要绘制ROC曲线,你需要得分,而不是概率。 一个简单的解决方案是使用正类的概率作为分数:

y_scores_forest = y_probas_forest[:, 1] # score = proba of positive class
fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5,y_scores_forest)

现在你已准备好绘制ROC曲线。 绘制第一条ROC曲线以及它们如何比较是很有用的:

plt.plot(fpr, tpr, "b:", label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")
plt.legend(loc="bottom right")
plt.show()

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第8张图片

正如上图所看到的,RandomForestClassifier的ROC曲线看起来比SGDClassifier的好得多:它更靠近左上角。 因此,其ROC AUC分数也明显更好:

>>> roc_auc_score(y_train_5, y_scores_forest)
0.99312433660038291

尝试测量精度和召回分数:你应该找到98.5%的精确度和82.8%的召回率。 还不错!希望你现在知道如何训练二元分类器,为你的任务选择合适的度量,使用交叉验证评估你的分类器,选择适合你需要的精确/召回权衡,并使用ROC曲线和ROC比较各种模型 AUC分数。 现在让我们尝试检测不仅仅是5s。

四、多类分类

尽管二元分类器区分两个类,但多类分类器(也称为多项分类器)可以区分两个以上的类。

一些算法(例如随机森林分类器或朴素贝叶斯分类器)能够直接处理多个类。其他(例如支持向量机分类器或线性分类器)是严格的二元分类器。但是,你可以使用各种策略来使用多个二进制分类器执行多类分类。

例如,创建一个可以将数字图像分类为10个等级(从0到9)的系统的一种方法是训练10个二进制分类器,每个数字一个(1个0检测器,1个1检测器,1个2检测器等等)。然后,当你想要对图像进行分类时,你将从该图像的每个分类器中获得决策分数,并选择其分类器输出最高分数的类。这被称为一对多(OvA)策略(也称为一对剩余)。

另一种策略是为每对数字训练一个二元分类器:一个用于区分0和1,另一个用于区分0和2,另一个用于1和2,依此类推。这称为一对一(OvO)战略。如果有N个类,则需要训练N×(N-1)/ 2个分类器。对于MNIST问题,这意味着训练45个二元分类器!当你想要对图像进行分类时,你必须通过所有45个分类器运行图像,并查看哪个类赢得最多的决斗。 OvO的主要优点是每个分类器只需要训练就必须区分的两个类的训练集。

一些算法(例如支持向量机分类器)随着训练集的大小而缩放得很差,因此对于这些算法,OvO是优选的,因为在小型训练集上训练许多分类器比在大型训练集上训练少量分类器更快。然而,对于大多数二进制分类算法,OvA是优选的。

当您尝试将二进制分类算法用于多类分类任务时,Scikit-Learn会检测它,并自动运行OvA(除了使用OvO的SVM分类器)。让我们用SGDClassifier尝试一下:

>>> sgd_clf.fit(X_train, y_train)
>>> sgd_clf.predict([some_digit])
array([ 5.])

那很简单! 此代码使用0到9(y_train)的原始目标类训练训练集上的SGDClassifier,而不是5对所有目标类(y_train_5)。 然后它进行预测(在这种情况下是正确的)。 在幕后,Scikit-Learn实际上训练了10个二元分类器,得到了他们对图像的决策分数,并选择了得分最高的班级。
要确定情况确实如此,你可以调用decision_function()方法。而不是每个实例仅返回一个分数,它现在返回10个分数,每个类一个:

>>> some_digit_scores = sgd_clf.decision_function([some_digit])
>>> some_digit_scores
array([[-311402.62954431, -363517.28355739, -446449.5306454 ,
        -183226.61023518, -414337.15339485, 161855.74572176,
        -452576.39616343, -471957.14962573, -518542.33997148,
        -536774.63961222]])

最高分确实是对应于第5类的分数:

>>> np.argmax(some_digit_scores)
5
>>> sgd_clf.classes_
array([ 0., 1., 2., 3., 4., 5.,
>>> sgd_clf.classes[5]
5.0

Warning:当训练分类器时,它将目标类的列表存储在其classes_属性中,按值排序。 在这种情况下,classes_数组中每个类的索引可以方便地匹配类本身(例如,索引5的类碰巧是类5),但一般来说,你不会那么幸运。

如果要强制ScikitLearn使用一对一或一对多,则可以使用OneVsOneClassifier或OneVsRestClassifier类。 只需创建一个实例并将二进制分类器传递给它的构造函数。 例如,此代码基于SGDClassifier使用OvO策略创建多类分类器:

>>> from sklearn.multiclass import OneVsOneClassifier
>>> ovo_clf = OneVsOneClassifier(SGDClassifier(random_state=42))
>>> ovo_clf.fit(X_train, y_train)
>>> ovo_clf.predict([some_digit])
array([ 5.])
>>> len(ovo_clf.estimators_)
45

训练RandomForestClassifier同样简单:

>>> forest_clf.fit(X_train, y_train)
>>> forest_clf.predict([some_digit])
array([ 5.])

这次Scikit-Learn不必运行OvA或OvO,因为Random Forest分类器可以直接将实例分类为多个类。 你可以调用predict_proba()来获取分类器为每个类分配给每个实例的概率列表:

>>> forest_clf.predict_proba([some_digit])
array([[ 0.1, 0. , 0. , 0.1, 0. , 0.8, 0. , 0. , 0. , 0. ]])

你可以看到分类器对其预测非常有信心:数组中第5个索引处的0.8表示模型估计图像表示为5的概率为80%。它还认为图像可能为0 或3(每次10%的几率)。
当然,你想要评估这些分类器。 像往常一样,你希望使用交叉验证。 让我们使用cross_val_score()函数评估SGDClassifier的准确性:

>>> cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
array([ 0.84063187, 0.84899245, 0.86652998])

所有测试折叠都超过84%。 如果你使用随机分类器,你将获得10%的准确度,因此这不是一个糟糕的分数,但你仍然可以做得更好。 例如,简单地缩放输入(如上篇所述)可将准确度提高到90%以上:

>>> from sklearn.preprocessing import StandardScaler
>>> scaler = StandardScaler()
>>> X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
>>> cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")
array([ 0.91011798, 0.90874544, 0.906636 ])

五、错误分析

当然,如果这是一个真实的项目,你可以按照你的机器学习项目清单中的步骤:探索数据准备选项,尝试多个模型,筛选最好的模型,并使用GridSearchCV微调他们的超参数, 正如你在上一章中所做的那样,尽可能地自动化。 在这里,我们假设您已找到一个有前途的模型,并且你想找到改进它的方法。 一种方法是分析它所犯的错误类型。

首先,你可以查看混淆矩阵。 你需要使用cross_val_predict()函数进行预测,然后调用confusion_matrix()函数,就像你之前做的那样:

>>> y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
>>> conf_mx = confusion_matrix(y_train, y_train_pred)
>>> conf_mx
array([[5725,    3,    24,    9,    10,    49,    50,    10,    39,    4],
       [   2, 6493,    43,   25,     7,    40,     5,    10,   109,    8],
       [  51,   41,  5321,  104,    89,    26,    87,    60,   166,   13],
       [  47,   46,   141, 5342,     1,   231,    40,    50,   141,   92],
       [  19,   29,    41,   10,  5366,     9,    56,    37,    86,  189],
       [  73,   45,    36,  193,    64,  4582,   111,    30,   193,   94],
       [  29,   34,    44,    2,    42,    85,  5627,    10,    45,    0],
       [  25,   24,    74,   32,    54,    12,     6,  5787,    15,  236],
       [  52,  161,    73,  156,    10,   163,    61,    25,  5027,  123],
       [  43,   35,    26,   92,   178,    28,     2,   223,    82, 5240]])

这是很多数字。 使用Matplotlib的matshow()函数来查看混淆矩阵的图像表示通常更方便:

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

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第9张图片

这种混淆矩阵看起来相当不错,因为大多数图像都在主对角线上,这意味着它们被正确分类。 5s看起来比其他数字稍暗,这可能意味着数据集中5s的图像较少,或者分类器在5s上的表现不如其他数字。 实际上,你可以验证两者都是如此。

让我们把重点放在错误上。 首先,你需要将混淆矩阵中的每个值除以相应类中的图像数量,这样你就可以比较错误率而不是绝对错误数(这会使丰富的类看起来不公平):

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

现在让我们用零填充对角线以仅保留错误,并让我们绘制结果:

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

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第10张图片

现在你可以清楚地看到分类器产生的错误类型。请记住,行表示实际的类,而列表示预测的类。类8和9的列非常明亮,它告诉你许多图像被错误分类为8或9。类似地,类8和9的行也非常明亮,告诉你8s和9s经常与其他数字混淆。相反,有些行非常暗,例如第1行:这意味着大多数1都被正确分类(少数与8s混淆,但这就是它)。请注意,错误并不完全对称;例如,有更多的5s被错误分类为8s而不是反向。

分析混淆矩阵通常可以让你深入了解改进分类器的方法。看看这个情节,似乎你的努力应该用于改进8s和9s的分类,以及修复特定的3/5混乱。例如,你可以尝试为这些数字收集更多训练数据。或者你可以设计有助于分类器的新功能 - 例如,编写算法来计算闭环的数量(例如,8有两个,6有一个,5没有)。或者你可以预处理图像(例如,使用Scikit-Image,Pillow或OpenCV)使某些图案更突出,例如闭环。

分析个别错误也是一种很好的方式,可以深入了解你的分类器正在做什么以及它失败的原因,但它更加困难和耗时。
例如,让我们绘制3s和5s的示例:

cl_a, cl_b = 3, 5
X_aa = X_train[(y_train == cl_a) & ((y_train_pred == cl_a)]
X_ab = X_train[(y_train == cl_a) & ((y_train_pred == cl_b)]
X_ba = X_train[(y_train == cl_b) & ((y_train_pred == cl_a)]
X_bb = X_train[(y_train == cl_b) & ((y_train_pred == cl_b)]

plt.figure(figsize=(8,8))
plt.subplot(221); plot_digits(X_aa[:25], images_per_row=5)
plt.subplot(222); plot_digits(X_ab[:25], images_per_row=5)

plt.subplot(223); plot_digits(X_ba[:25], images_per_row=5)
plt.subplot(224); plot_digits(X_bb[:25], images_per_row=5)
plt.show()

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第11张图片

上侧的两个5×5块显示分类为3s的数字,下侧的两个5×5块显示分类为5s的图像。分类器出错的一些数字编写得非常糟糕,甚至人类也难以对它们进行分类(例如,第8行和第1列的5)真的看起来像3)。然而,大多数错误分类的图像对我们来说似乎是明显的错误,并且很难理解分类器为什么会犯错误。 原因是我们使用了一个简单的SGDClassifier,它是一个线性模型。它所做的就是为每个像素分配每个类别的权重,当它看到一个新图像时,它只是将加权像素强度相加得到每个类别的分数。因此,由于3s和5s仅相差几个像素,因此该模型很容易混淆它们。

3s和5s之间的主要区别在于将顶线连接到底弧的小线的位置。如果你绘制一个3略微向左移动的交汇点,分类器可能会将其分类为5,反之亦然。换句话说,该分类器对图像移位和旋转非常敏感。因此,减少3/5混淆的一种方法是对图像进行预处理,以确保它们很好地居中并且不会过于旋转。这可能有助于减少其他错误。

六、多标签分类

到目前为止,每个实例始终只分配给一个类。 在某些情况下,你可能希望分类器为每个实例输出多个类。 例如,考虑一个人脸识别分类器:如果它识别出同一张图片中的几个人,该怎么办? 当然,每个人认可的标签应附上一个标签。 假设分类器已被训练以识别三个面孔,Alice,Bob和Charlie; 然后当它显示Alice和Charlie的图片时,它应该输出[1,0,1](意思是“Alice yes,Bob no,Charlie yes”)。 这种输出多个二元标记的分类系统称为多标记分类系统

我们暂时不会进行人脸识别,但让我们看一个更简单的例子,仅用于说明目的:

from sklearn.neighbors import KNeighborsClassifier

y_train_large = (y_train >= 7)
y_train_odd = (y_train % 2 == 1)
y_multilabel = np.c_[y_train_large, y_train_odd]

knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)

此代码创建一个y_multilabel数组,其中包含每个数字图像的两个目标标签:第一个指示数字是否大(7,8或9),第二个指示是否为奇数。 下一行创建一个KNeighborsClassifier实例(它支持多标记分类,但不是所有分类器都这样做),我们使用多个目标数组训练它。 现在你可以进行预测,并注意它输出两个标签:

>>> knn_clf.predict([some_digit])
array([[False, True]], dtype=bool)

它做对了! 数字5确实不大(False)和奇数(True)。

有许多方法可以评估多标签分类器,选择正确的度量标准实际上取决于你的项目。 例如,一种方法是测量每个单独标签(或前面讨论的任何其他二元分类器度量)的F_{1}分数,然后简单地计算平均分数。 此代码计算所有标签的平均F_{1}分数:

>>> y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_train, cv=3)
>>> f1_score(y_train, y_train_knn_pred, average="macro")
0.96845540180280221

这假设所有标签都同样重要,但情况可能并非如此。 特别是,如果你有比爱丽丝或查理更多的爱丽丝照片,你可能想要在爱丽丝的照片上给予分类者的分数更多的权重。 一个简单的选择是给每个标签一个等于其支持的权重(即具有该目标标签的实例数)。 要做到这一点,只需在前面的代码中设置average = “weighted”。

七、多输出分类

我们将在这里讨论的最后一种类型的分类任务称为多输出多类分类(或简称为多输出分类)。 它只是多标记分类的概括,其中每个标记可以是多类的(即,它可以具有多于两个可能的值)。

为了说明这一点,让我们构建一个从图像中去除噪声的系统。 它将输入一个有噪声的数字图像,它将(希望)输出一个干净的数字图像,表示为像素强度数组,就像MNIST图像一样。 请注意,分类器的输出是多标签(每个像素一个标签),每个标签可以有多个值(像素强度范围从0到255)。 因此,它是多输出分类系统的一个例子。

Note:分类和回归之间的界限有时是模糊的,例如在此示例中。 可以说,预测像素强度更像是回归而不是分类。 此外,多输出系统不限于分类任务; 你甚至可以有一个系统,每个实例输出多个标签,包括类标签和值标签。

让我们首先创建训练和测试集,方法是使用NISTPy的randint()函数获取MNIST图像并将噪声添加到像素强度。 目标图像将是原始图像:

noise = rnd.randint(0, 100, (len(X_train), 784))
noise = rnd.randint(0, 100, (len(X_test), 784))
X_train_mod = X_train + noise
X_test_mod = X_test + noise
y_train_mod = X_train
y_test_mod = X_test

让我们看看测试集中的图像(是的,我们正在窥探测试数据,所以你现在应该皱眉):

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第12张图片

左边是噪声输入图像,右边是干净的目标图像。 现在让我们训练分类器并使其清洁这个图像:

knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记(三):分类_第13张图片

看起来足够接近目标! 这就结束了我们的分类之旅。 希望你现在应该知道如何为分类任务选择好的指标,选择适当的精确度/召回权衡,比较分类器,以及更一般地为各种任务构建良好的分类系统。

八、练习

1、尝试为MNIST数据集构建一个分类器,在测试集上实现超过97%的准确率。 提示:KNeighborsClassifier可以很好地完成这项任务;你只需要找到好的超参数值(尝试对权重和n_neighbors超参数进行网格搜索)。

2、编写一个可以将MNIST图像向任意方向(左,右,上或下)移动一个像素的函数。然后,对于训练集中的每个图像,创建四个移位副本(每个方向一个)并将它们添加到训练集中。最后,在这个扩展的训练集上训练你的最佳模型,并测量其在测试集上的准确性。你应该观察到你的模型现在表现得更好!这种人工培养训练集的技术称为数据增强或训练集扩展。
3、解决泰坦尼克号数据集。一个很好的起点是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库)。
  • 然后尝试几个分类器,看看你是否可以建立一个伟大的垃圾邮件分类器,具有高召回率和高精度。

这些练习的解决方案可在在线Jupyter笔记本中找到,网址为https://github.com/ageron/handson-ml。

 

 

你可能感兴趣的:(读书笔记,读书笔记)