使用Scikit-learn实现分类(MNIST)

2020-12-28 11:33:48 浏览数 (1)

参考链接: 使用Scikit-learn进行癌细胞分类

这是我学习hands on ml with sklearn and tf 这本书做的笔记,这是第三章 

MNIST  在本章当中,我们将会使用 MNIST 这个数据集,它有着 70000 张规格较小的手写数字图片,由美国的高中生和美国人口调查局的职员手写而成。这相当于机器学习当中的“Hello World”,人们无论什么时候提出一个新的分类算法,都想知道该算法在这个数据集上的表现如何。机器学习的初学者迟早也会处理 MNIST 这个数据集。 

1、下载数据集 

Scikit-Learn 提供了许多辅助函数,以便于下载流行的数据集。MNIST 是其中一个。下面的代码获取 MNIST 

from sklearn.datasets import fetch_mldata

mnist = fetch_mldata('MNIST original')

print(mnist) 

运行结果如下: 

{'DESCR': 'mldata.org dataset: mnist-original', 'COL_NAMES': ['label', 'data'], 'target': array([0., 0., 0., ..., 9., 9., 9.]), '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)} 

一般而言,由 sklearn 加载的数据集有着相似的字典结构,这包括:  DESCR 键描述数据集  data 键存放一个数组,数组的一行表示一个样例,一列表示一个特征  target 键存放一个标签数组  接下来,认真看看这些数组: 

x, y = mnist['data'], mnist['target']

print('x的大小为;', x.shape, 'n','x的大小为;', y) 

运行结果: 

  x的大小为; (70000, 784)  y的大小为; (70000,)   

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

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() 

运行结果如下: 

![53120515523](机器学习的‘hello world–手写数字识别MNIST.assets/1531205155236.png) 

这看起来像个 5,实际上它的标签告诉我们: 

print(y[36000]) 

5.0 

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

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

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] 

2、训练一个二分类器 

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

y_train_5 = (y_train == 5)  # 对于所有5为真,对于所有其他数字为假.

y_test_5 = (y_test == 5) 

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

from sklearn.linear_model import SGDClassifier

sgd_clf = SGDClassifier(random_state=42)

sgd_clf.fit(X_train, y_train_5)

predict = sgd_clf.predict([some_digit])

print(predict) 

运行结果如下: 

[ True] 

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

3、对性能的评估 

3.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)  # StratifiedKFold解释https://blog.csdn.net/cherdw/article/details/54986863

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))  

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

from sklearn.model_selection import cross_val_score

print(cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")) 

运行结果如下: 

[0.96465 0.96725 0.9655 ] 

在交叉验证上有大于 95% 的精度(accuracy)?这看起来很令人吃惊。先别高兴,让我们来看一个非常笨的分类器去分类,看看其在“非 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()

print(cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")) 

运行结果如下: 

[0.90895 0.91155 0.90845] 

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

3.2、混淆矩阵 

对分类器来说,一个好得多的性能评估指标是混淆矩阵。大体思路是:输出类别A被分类成类别 B 的次数。举个例子,为了知道分类器将 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 折交叉验证。它不是返回一个评估分数,而是返回基于每一个测试折做出的一个预测值。这意味着,对于每一个训练集的样例,你得到一个干净的预测(“干净”是说一个模型在训练过程当中没有用到测试集的数据)。  现在使用 confusion_matrix() 函数,你将会得到一个混淆矩阵。传递目标类( y_train_5 )和预测类( y_train_pred )给它。 

from sklearn.metrics import confusion_matrix

print(confusion_matrix(y_train_5, y_train_pred)) 

结果如下: 

[[53003  1576]

 [ 1025  4396]] 

混淆矩阵中的每一行表示一个实际的类, 而每一列表示一个预测的类。该矩阵的第一行认为“非5”(反例)中的 53272 张被正确归类为 “非 5”(他们被称为真反例,true negatives), 而其余1307 被错误归类为”是 5” (假正例,false positives)。第二行认为“是 5” (正例)中的 1077被错误地归类为“非 5”(假反例,false negatives),其余 4344 正确分类为 “是 5”类(真正例,true positives)。一个完美的分类器将只有真反例和真正例,所以混淆矩阵的非零值仅在其主对角线(左上至右下)。 

混淆矩阵可以提供很多信息。有时候你会想要更加简明的指标。一个有趣的指标是正例预测的精度,也叫做分类器的准确率(precision)。 

准确率:![53155721857](机器学习的‘hello world–手写数字识别MNIST.assets/1531557218572.png),其中 TP 是真正例的数目,FP 是假正例的数目。 

想要一个完美的准确率,一个平凡的方法是构造一个单一正例的预测和确保这个预测是正确的( precision = 1/1 = 100% )。但是这什么用,因为分类器会忽略所有样例,除了那一个正例。所以准确率一般会伴随另一个指标一起使用,这个指标叫做召回率(recall),也叫做敏感度(sensitivity)或者真正例率(true positive rate, TPR)。这是正例被分类器正确探测出的比率。 

召回率:![53155734602](机器学习的‘hello world–手写数字识别MNIST.assets/1531557346024.png),FN 是假反例的数目。 

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

from sklearn.metrics import precision_score, recall_score

print(precision_score(y_train_5, y_train_pred))

print(recall_score(y_train_5, y_train_pred)) 

运行结果: 

0.9187339606501284

0.594355285002767 

当你去观察精度的时候,你的“数字 5 探测器”看起来还不够好。当它声明某张图片是 5 的时候,它只有 77% 的可能性是正确的。而且,它也只检测出“是 5”类图片当中的 79%。  通常结合准确率和召回率会更加方便,这个指标叫做“F1 值”,特别是当你需要一个简单的方法去比较两个分类器的优劣的时候。F1 值是准确率和召回率的调和平均。普通的平均值平等地看待所有的值,而调和平均会给小的值更大的权重。所以,要想分类器得到一个高的 F1值,需要召回率和准确率同时高。 

F1值:![53155808242](机器学习的‘hello world–手写数字识别MNIST.assets/1531558082420.png) 

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

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

为了弄懂这个折衷,我们看一下 SGDClassifier 是如何做分类决策的。对于每个样例,它根据决策函数计算分数,如果这个分数大于一个阈值,它会将样例分配给正例,否则它将分配给反例。现在,如果你 提高阈值(移动到右侧的箭头),假正例(数字6)成为一个真反例,从而提高准确率(在这种情况下高达 100%),但一个真正例 变成假反例,召回率降低到 50%。相反,降低阈值可提高召回率、降低准确率。 

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

  y_scores = sgd_clf.decision_function([some_digit])  print(y_scores)  threshold = 0  y_some_digit_pred = (y_scores > threshold)  print(y_some_digit_pred)   

运行结果: 

[4874.97938248]

[ True] 

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

threshold = 200000

y_some_digit_pred = (y_scores > threshold)

print(y_some_digit_pred) 

运行结果: 

[False] 

这证明了提高阈值会降调召回率。这个图片实际就是数字 5,当阈值等于 0 的时候,分类器可以探测到这是一个 5,当阈值提高到 20000 的时候,分类器将不能探测到这是数字 5。  那么,你应该如何使用哪个阈值呢?首先,你需要再次使用 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 画出准确率和召回率,这里把准确率和召回率当作是阈值的一个函数。 

我们假设你决定达到 90% 的准确率。你查阅第一幅图(放大一些),在 70000 附近找到一个阈值。为了作出预测(目前为止只在训练集上预测),你可以运行以下代码,而不是运行分类器的 predict() 方法。 

y_train_pred_90 = (y_scores > 70000) 

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

  y_train_pred_90 = (y_scores > 70000)  print(precision_score(y_train_5, y_train_pred_90))  print(recall_score(y_train_5, y_train_pred_90))   

结果如下: 

  0.8717649801818605  0.689725142962553   

很棒!你拥有了一个(近似) 90% 准确率的分类器。它相当容易去创建一个任意准确率的分类器,只要将阈值设置得足够高。但是,一个高准确率的分类器不是非常有用,如果它的召回率太低! 

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() 函数: 

from sklearn.metrics import roc_curve

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

然后你可以使用 matplotlib,画出 FPR 对 TPR 的曲线 

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() 

结果如下: 

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

from sklearn.metrics import roc_auc_score

print(roc_auc_score(y_train_5, y_scores)) 

结果如下: 

0.9537859009183066 

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

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 . 

  sgd_clf.fit(X_train, y_train) # y_train, not y_train_5  print(sgd_clf.predict([some_digit]))   

[5.] 

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

some_digit_scores = sgd_clf.decision_function([some_digit])

print(some_digit_scores) 

运行结果如下: 

[[  -5261.48991879 -576743.33615384 -296162.58682833 -226539.03249516

  -582963.24819051  -75508.30809277 -895117.67290645 -211633.28217169

  -491193.19885868 -654833.33725292]] 

最高数值是对应于类别 5 : 

print(np.argmax(some_digit_scores))

print(sgd_clf.classes_)

print(sgd_clf.classes_[5]) 

运行结果如下: 

5

[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]

5.0 

5、误差分析 

当然,如果这是一个实际的项目,你会在你的机器学习项目当中,跟随以下步骤:探索准备数据的候选方案,尝试多种模型,把最好的几个模型列为入围名单,用 GridSearchCV 调试超参数,尽可能地自动化,像你前面的章节做的那样。在这里,我们假设你已经找到一个不错的模型,你试图找到方法去改善它。一个方式是分析模型产生的误差的类型。  首先,你可以检查混淆矩阵。你需要使用 cross_val_predict() 做出预测,然后调用 confusion_matrix() 函数,像你早前做的那样。这个混淆矩阵看起来相当好,因为大多数的图片在主对角线上。在主对角线上意味着被分类正确。数字 5 对应的格子看起来比其他数字要暗淡许多。这可能是数据集当中数字 5 的图片比较少,又或者是分类器对于数字 5 的表现不如其他数字那么好。你可以验证两种情况。  让我们关注仅包含误差数据的图像呈现。首先你需要将混淆矩阵的每一个值除以相应类别的图片的总数目。这样子,你可以比较错误率,而不是绝对的错误数(这对大的类别不公平)。

0 人点赞