【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)

参照《机器学习实战》第二版

1、MNIST

本章使用MNIST数据集,这是一组由70000张手写的数字图片,每张图片都用其代表的数字标记。因此也被成为机器学习领域的“Hello World”:但凡有人想到了一个新的分类算法,都会想看看在MNIST上的执行结果。

1.1、下载 MNIST 素材

Scikit-Learn提供了许多助手功能来帮你下载流行的数据集,MNIST 也是其一:

from sklearn.datasets import fetch_openml

# 从 Scikit-Learn 0.24 开始,fetch_openml() 默认返回 Pandas DataFrame。 
# 为了避免这种情况并保持与书中相同的代码,我们使用 as_frame=False。
# 下载失败可以多尝试几次,初次时间会稍微稍微久一点,我等了 12min7s,再次使用会优先检查缓存文件。
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
mnist.keys()
dict_keys(['data', 'target', 'frame', 'categories', 'feature_names', 'target_names', 'DESCR', 'details', 'url'])

1.2、了解素材数据基础

type(mnist)
sklearn.utils.Bunch

这里采用的数据格式是sklearn.utils.Bunch,是一个类似字典的结构。下面展示一部分:

{
    # 每一个实例为一行,每一个特征为一列,行70000 × 列784
    '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.]]),
    # 每个实例的标记数组,70000个
    'target': array(['5', '0', '4', ..., '4', '5', '6'], dtype=object),
    # 框架
    'frame': None,
    # 类别
    'categories': {},
    # 功能名称:28×28=784个,与data的列对应
    'feature_names': ['pixel1', 'pixel2', 'pixel3', ...'pixel783', 'pixel784'],
    # 标记名称
    'target_names': ['class'],
    # 数据集描述:作者、来源、引用
    'DESCR': "**Author**: ...  \n**Source**: ...  \n**Please cite**: ...",
    # 详细信息
    'details': {
        'id': '554',
        'name': 'mnist_784',
        'version': '1',
        'description_version': '1',
        'format': 'ARFF',
        'creator': ['Yann LeCun', 'Corinna Cortes', 'Christopher J.C. Burges'],
        'upload_date': '2014-09-29T03:28:38',
        'language': 'English',
        'licence': 'Public',
        'url': 'https://www.openml.org/data/v1/download/52667/mnist_784.arff',
        'file_id': '52667',
        'default_target_attribute': 'class',
        'tag': ['AzurePilot', 'OpenML-CC18', 'OpenML100', 'study_1', 'study_123', 'study_41', 'study_99', 'vision'],
        'visibility': 'public',
        'status': 'active',
        'processing_date': '2020-11-20 20:12:09',
        'md5_checksum': '0298d579eb1b86163de7723944c7e495'
    },
    # 地址
    'url': 'https://www.openml.org/d/554'
}
X, y = mnist['data'], mnist['target']
>>> X.shape
(70000, 784)
>>> y.shape
(70000)

一共有7万张图片,每张图片有784个特征。因为图片是28×28像素。每个特征代表了一个像素点的强度,从0(白色)255(黑色)。随便取一个实例的特征向量,将其重新组成一个28×28的数组,然后使用Matplotlibimshow()函数将其显示出来:

import matplotlib as mpl # 专业绘制图形、图像的库
import matplotlib.pyplot as plt

some_digit = X[0]
some_digit_image = some_digit.reshape(28, 28)

plt.imshow(some_digit_image, cmap="binary")  # cmap="binary" 颜色按二进制绘制
plt.axis("off")  # 关闭坐标轴
plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第1张图片

  • some_digit_image:在IDEA中查看,就是这个样子。
    【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第2张图片
    上图看起来像 5,而我们用标签验证一下:
import numpy as np

def plot_digits(instances, images_per_row=10, **options):
    size = 28
    images_per_row = min(len(instances), images_per_row)
    images = [instance.reshape(size,size) for instance in instances]
    n_rows = (len(instances) - 1) // images_per_row + 1
    row_images = []
    n_empty = n_rows * images_per_row - len(instances)
    images.append(np.zeros((size, size * n_empty)))
    for row in range(n_rows):
        rimages = images[row * images_per_row : (row + 1) * images_per_row]
        row_images.append(np.concatenate(rimages, axis=1))
    image = np.concatenate(row_images, axis=0)
    plt.imshow(image, cmap = mpl.cm.binary, **options)
    plt.axis("off")
plt.figure(figsize=(9,9))
example_images = X[:100]
plot_digits(example_images, images_per_row=10)
plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第3张图片

y[0], type(y[0])
('5', str)

这里我们可以注意到标签是字符,大部分机器学习算法希望是数字,那么就把 y 转成 整数:

import numpy as np

y = y.astype(np.uint8)

1.3、创建测试集

实际上,MNIST 已经分成训练集(前 6 万张图片),和测试集(最后 1 万张图片):如果你仔细看过mnist['DESCR'],里面有说明。

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

同样,我们先将训练集数据混洗,这样能保证交叉验证时所有的折叠都差不多。此外,有些机器学习算法对训练实例顺序敏感(如果连续输入许多相似的实例,可能导致执行性能不佳)。给数据集混洗正是为了确保这种情况不会发生(当前也有例外,例如具有时间序列的数据,股市或者天气,混洗就不是一个好主意)。

2、训练二元分类器

现在先简化问题,只尝试识别一个数字,比如数字 5,那么这个“数字 5 检测器”就是一个二元分类器的示例,它只能区分两个类别:5非5。先为此分类任务创建目标向量:

# 这里将得到两个由 True 和 False 组成的 列表
y_train_5 = (y_train == 5)
y_test_5 = (y_test == 5)

y_train[:20]
array([5, 0, 4, 1, 9, 2, 1, 3, 1, 4, 3, 5, 3, 6, 1, 7, 2, 8, 6, 9], dtype=uint8)
y_train_5[:20]
array([ True, False, False, False, False, False, False, False, False,
       False, False,  True, False, False, False, False, False, False,
       False, False])

接着挑选一个分类器并开始训练,一个好的初始选择是随机梯度下降(SGD)分类器,使用Scikit-LearnSGDClassifier类即可。这个分类器的优势是能够有效处理非常大型的数据集。这部分是因为 SGD 独立处理训练实例,一次一个(也就使得 SGD 非常适合在线学习)。此时先创建一个SGDClassifier并在整个训练集上进行训练:

from sklearn.linear_model import SGDClassifier

# 因为 SGDClassifier 是完全随机的,所以如果希望结果可复现,需要设置 random_state
# sgd_clf = SGDClassifier(random_state=42)
sgd_clf = SGDClassifier(max_iter=1000, tol=1e-3, random_state=42)  # 前两个是新版默认参数
sgd_clf.fit(X_train, y_train_5)
SGDClassifier(random_state=42)

现在来检测一下数字 5 的图片:

sgd_clf.predict([some_digit])
array([ True])

3、性能测量

评估分类器比评估回归器要困难得多,因此本章将用很多篇幅来讨论这个主题,同时会涉及许多性能考核的方法。

3.1、使用交叉验证测量准确率

3.1.1、实现交叉验证(手写交叉验证)

相比于Scikit-Learn提供的cross_val_score()这一类交叉验证的函数,有时你可以希望自己能控制的多一些。在这种情况下,你可以自行实现交叉验证。下面代码与cross_val_score()大致相同,并打印结果:

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

def self_cross_val_score(estimator, X, y, n_splits):
    # StratifiedKFold:K-交叉验证分层器,返回分层折叠
    skfolds = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
    # 返回两个折叠:训练集索引,测试集索引
    for train_index, test_index in skfolds.split(X, y):
        clone_clf = clone(estimator)    # 克隆分类器
        X_train_folds = X[train_index]  # 训练集
        y_train_folds = y[train_index]  # 训练标签集
        X_test_folds = X[test_index]    # 测试集
        y_test_folds = y[test_index]    # 测试标签集
        
        clone_clf.fit(X_train_folds, y_train_folds)  # 训练分配器
        y_pred = clone_clf.predict(X_test_folds)     # 进行预测
        n_correct = sum(y_pred == y_test_folds)      # 统计正确预测次数
        print(n_correct / len(y_pred))               # 输出准确率

self_cross_val_score(sgd_clf, X_train, y_train_5, 3)
0.9669
0.91625
0.96785

3.1.2、Scikit-Learn 的 cross_val_score()

现在用cross_val_score()函数来评估SGDClassifier模型,采用 K-折交叉验证法(老版本默认3个折叠,新版本默认5个折叠):

from sklearn.model_selection import cross_val_score

cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([0.95035, 0.96035, 0.9604 ])

所有折交叉验证的准确率超过95%,先不要激动,我们来简单的分类器,它将所有图片都分类成“非5”

from sklearn.base import BaseEstimator

class Never5Classifier(BaseEstimator):
    def fit(self, X, y=None):
        return self

    def predict(self, X):
        return np.zeros((len(X), 1), dtype=bool)  # 生成 len(X) 行,1 列,的 False(0)


never_5_clf = Never5Classifier()
cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([0.91125, 0.90855, 0.90915])

当我们把所有图片都分类为“非5”,准确率依然在90%以上,这是因为只有大约10%的图片是数字 5,所以如果你猜一张图片不是 590%的概率都是对的。

这说明准确率通常无法作为分类器的首要性能指标,特别是当你处理有偏数据集时(即某些类比其他类更为频繁)。

3.2、混淆矩阵

评估分类器,性能的更好方法是混淆矩阵,其总体思路就是统计A类别实例被分成为B类别的次数。例如,要想知道分类器将数字3和数字5混淆的次数。只需要通过混淆矩阵的第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)
y_train_pred.shape
(60000,)
y_train_pred[:27]
array([ True, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False, False, False, False, False])

cross_val_score()函数一样,cross_val_predict()函数相同执行K-折交叉验证,但是返回的不是评估分数,而是每个折叠的预测。这意味着对于每个实例都可以得到一个干净的预测(“干净”的意思是模型预测时使用的数据在其训练期间从未见过)。

现在可以使用confusion_matrix()函数来获取混淆矩阵了。只需要给出目标类别(y_train_5)和预测类别(y_train_pred)即可:

from sklearn.metrics import confusion_matrix

confusion_matrix(y_train_5, y_train_pred)
array([[53892,   687],
       [ 1891,  3530]])

混淆矩阵中的行表示实际类别,列表示预测类别。

混洗矩阵 非5(实际) 5(实际)
非5(负类) 53892(真负类) 687(假正类)
5(正类) 1891(假负类) 3530(真正类)

一个完美的分类器只有真负类真正类,所以它的混淆矩阵只会在其对角线上有非零值。

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

混淆矩阵能提供大量信息,但有时你可能希望指标更简洁一些。正类预测的准确率是一个有意思的指标,它也被称为分类器的精度

公式: 精 度 = T P T P + F P 精度 = \frac{TP}{TP + FP} =TP+FPTP

TP:真正类的数量,FP:假正类的数量。

做一个单独的正类预测,并保证它是正确的,就可以得到完美精度(1/1 = 100%)。但这没有意义,因为分类器会忽略这个正类实例之外的所有内容。因此,精度通常与另一个指标一起使用,这个指标就是召回率,也称为灵敏度或者真正类率:它是分类器正确检查到的正类实例的比例。

公式: 召 回 率 = T P T P + F N 召回率 = \frac{TP}{TP + FN} =TP+FNTP

TN:真负类的数量,FN:假负类的数量。

非(真) 是(真)
非(预) TN(真负类) FP(假正类)
是(预) FN(假负类) TP(真正类)

3.3、精度和召回率

Scikit-Learn提供了计算多种分类器指标的函数,包括精度和召回率:

from sklearn.metrics import precision_score, recall_score
precision_score(y_train_5, y_train_pred) # 精度:3530 / (3530 + 687)
0.8370879772350012
recall_score(y_train_5, y_train_pred) # 召回率:3530 / (3530 + 1891)
0.6511713705958311

现在在看,这个5-分类器看起来并不像它的准确率那么光线亮眼。

  1. 当一张图片被判断成5时,只有83.7%的准确率;
  2. 并且也只有65.1%的数字5被检查出来。

因此我们可以很方便地将精度和召回率组合成一个单一的指标,成为 F 1 F_1 F1分数。当你需要一个简单的方法来比较两种分类器时,这是个非常不错的指标。 F 1 F_1 F1分数是精度和召回率的谐波平均值。正常的平均值平等对待所有值,而谐波平均值会给予低值更高的权重。因此,只有当召回率和精度都很高时,分类器才能得到较高 F 1 F_1 F1分数。

公式: F 1 = 2 1 精 度 + 1 召 回 率 = 2 × 精 度 × 召 回 率 精 度 + 召 回 率 = T P T P + F N + F P 2 F_1 = \frac{2}{\frac{1}{精度} + \frac{1}{召回率}} = 2 × \frac{精度 × 召回率}{精度 + 召回率} = \frac{TP}{TP + \frac{FN + FP}{2}} F1=1+12=2××=TP+2FN+FPTP

要计算 F 1 F_1 F1分数,只需要调用f1_score()即可:

from sklearn.metrics import f1_score

f1_score(y_train_5, y_train_pred)
0.7325171197343846

F 1 F_1 F1分数对这些具有相近的精度和召回率的分类器更为有利。这不一定能一直符合你的期望:在某些情况下,你更关心的是精度,而另一些情况下,你可能真正关心的是召回率。

假如你训练一个分类来检测儿童可以放心观看的视频,那么你可以更青睐这种拦截了很多视频(低召回率),但是保留下来的视频都是安全(高精度)的分类器,而不是召回率虽高,但是在产品中可能会出现一些非常糟糕的视频的分类器。反过来讲,如果你训练一个分类器通过图像监控来检查小偷:你大概可以接收精度只有30%,但召回率达到99%。

遗憾的是,鱼与熊掌不可兼得,你不能同时增加精度有减少召回率,反之亦然。这称作精度/召回率权衡

3.4、精度/召回率权衡

要理解这个权衡过程,我们来看看SGDClassifier如何进行分类决策的。

  1. 对于每个实例,它会基于决策函数计算出一个分值;
  2. 如果该值大于阈值,则将该实例判为正类,否则便将其判为负类;

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

y_scores = sgd_clf.decision_function([some_digit])
y_scores
array([2164.22030239])
threshold = 0  # 如果阈值设置为 0
y_some_digit_pred = (y_scores > threshold)  # 结果为 True
y_some_digit_pred
array([ True])
threshold = 3000  # 如果阈值设置为 0
y_some_digit_pred = (y_scores > threshold)  # 结果为 False
y_some_digit_pred
array([False])

上面证明了提高阈值的确可以降低召回率。这张图的确是 5,当阈值为0时,分类器可以检测到这个值(True),但是当阈值提高到 8000 时,就错过了这张图。

那么要如何决定使用什么阈值呢?首先,使用cross_val_predick()函数获取训练集中所有实例的分数,但是这次需要返回的是决策分数而不是预测结果:

y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method="decision_function")
y_scores[:52]
array([  1200.93051237, -26883.79202424, -33072.03475406, -15919.5480689 ,
       -20003.53970191, -16652.87731528, -14276.86944263, -23328.13728948,
        -5172.79611432, -13873.5025381 , -22112.989794  ,  -2315.51879869,
       -29304.06327411, -18276.25416535,  -6790.91252517, -16924.86869525,
       -24589.95425105, -18278.36420614,  -6027.9952283 , -22381.6171182 ,
       -49309.77476771, -17839.33188677, -18790.52598692, -17252.40958724,
       -15735.74829459, -26564.59912951,  -7330.87070698, -29867.39668611,
       -55517.28436239, -23393.91582122, -23748.85652153, -38673.64006081,
        -9727.17183759, -31510.32108813, -26321.37189264,   2128.09444578,
       -17549.24805908, -30196.1371314 , -27894.21110125, -12411.15070702,
       -16150.91103934, -20214.54378109,  -7708.78445896, -13618.81181688,
       -11098.63132251, -18164.82278546, -16222.80882902,   7218.00578357,
        -2050.26809833, -19307.4594483 , -10577.29484418, -21488.49452455])

3.4.1、精度、召回率和阈值的图:precision_recall_curve()

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

from sklearn.metrics import precision_recall_curve
# 准确率、召回率、阈值:返回值按 准确率 从低到高 排序
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
    plt.plot(thresholds, precisions[:-1], "b--", label="Precision", linewidth=2)
    plt.plot(thresholds, recalls[:-1], "g-", label="Recall", linewidth=2)
    plt.legend(loc="center right", fontsize=16)  # 显示图例,位置 中右
    plt.xlabel("Threshold", fontsize=16)         # X轴命名
    plt.grid(True)                               # 显示网格
    plt.axis([-50000, 50000, 0, 1])              # 坐标显示 X轴范围 -50000~50000;Y轴范围 0~1


recall_90_precision = recalls[np.argmax(precisions >= 0.90)]        # 当准确率第一次大于0.90时的召回率。
threshold_90_precision = thresholds[np.argmax(precisions >= 0.90)]  # 当准确率第一次大于0.90时的阈值。


plt.figure(figsize=(8, 4))  # 指定figure的宽和高,单位为英寸
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.plot([threshold_90_precision, threshold_90_precision], [0., 0.9], "r:")
plt.plot([-50000, threshold_90_precision], [0.9, 0.9], "r:")
plt.plot([-50000, threshold_90_precision], [recall_90_precision, recall_90_precision], "r:")
plt.plot([threshold_90_precision], [0.9], "ro")
plt.plot([threshold_90_precision], [recall_90_precision], "ro")
plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第4张图片

上图中,精确率后面有明显的下降与波折,原因在于,当你提高阈值时,精度有时也有可能会下降。所以并不是阈值越高越好。

3.4.2、PR曲线(精度/召回率曲线)

另一种找到好的精度/召回率权衡的方法是直接绘制精度和召回率的函数图:

def plot_precision_vs_recall(precisions, recalls):
    plt.plot(recalls, precisions, "b-", linewidth=2)
    plt.xlabel("Recall", fontsize=16)
    plt.ylabel("Precision", fontsize=16)
    plt.axis([0, 1, 0, 1])
    plt.grid(True)

plt.figure(figsize=(8, 6))
plot_precision_vs_recall(precisions, recalls)
plt.plot([recall_90_precision, recall_90_precision], [0., 0.9], "r:")
plt.plot([0.0, recall_90_precision], [0.9, 0.9], "r:")
plt.plot([recall_90_precision], [0.9], "ro")
plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第5张图片

从图中可以看到,从80%的召回率往右,精度开始急剧下降。你可能会尽量在这个陡降之前选择一个精度/召回率权衡–比如召回率60%。然后,如何选择取决于你的项目。假设你决定将精度设为90%:

threshold_90_precision  # 精度为90%时的阈值
3370.0194991439557

要进行预测(现在是在训练集上),除了调用分类器的predict()方法,也可以运行这段代码:

y_train_pred_90 = (y_scores >= threshold_90_precision)

# 精度、召回率
precision_score(y_train_5, y_train_pred_90), recall_score(y_train_5, y_train_pred_90)
(0.9000345901072293, 0.4799852425751706)

这样你就有一个90%精度的分类器了!

3.5、ROC 曲线

还有一种经常与二元分类器一起使用的工具,叫做受试者工作特征曲线(简称 ROC)。它与精度/召回率曲线非常类似,但绘制得不是精度和召回率,而是真正类率(召回率的另一名称)和假正类率(FPR)。

  1. 假正类率(FPR):是被错误分为正类的负类实例比例。它等于1减去真负类率(TNR);
  2. 真负类率(TNR):是被正确分类为负类的负类实例比例,也成为特异度;
  3. 因此,ROC 曲线绘制的是灵敏度(召回率)和(1 - 特异度)的关系。
非(真) 是(真)
非(预) TN(真负类) FP(假正类)
是(预) FN(假负类) TP(真正类)

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

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])                                      # X、Y轴范围均为 0~1
    plt.xlabel('False Positive Rate (Fall-Out)', fontsize=16)   # 假正率
    plt.ylabel('True Positive Rate (Recall)', fontsize=16)      # 真正率(召回率)
    plt.grid(True)

    
plt.figure(figsize=(8, 6))
plot_roc_curve(fpr, tpr)
fpr_90 = fpr[np.argmax(tpr >= recall_90_precision)]             # 准确率90%时,召回率的位置
plt.plot([fpr_90, fpr_90], [0., recall_90_precision], "r:")
plt.plot([0.0, fpr_90], [recall_90_precision, recall_90_precision], "r:")
plt.plot([fpr_90], [recall_90_precision], "ro")
plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第6张图片
同样这里再次面临一个折中权衡:召回率(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.9604938554008616

ROC 曲线和 PR 曲线非常类似,如何选择呢?有一个经验法则是:当正类非常少见或者你更关注假正类而不是假负类时,应该选择 PR 曲线,反之则是 ROC 曲线。

现在我们来训练一个RandomForestClassifier分类器,并比较它和SGDClassifier分类器的 ROC 曲线和 ROC AUC 分数。

首先,获取训练集中每个实例的分数。但是由于它的工作方式不同,RandomForestClassifier类没有decision_function()方法,相反,它有predict_proba()方法。Scikit-Learn的分类器通常都有这两种方法的一种(或两种都有)。

  • predict_proba()方法:返回一个数组,其中一行代表一个实例,每一列代表一个类别,意思是某个给定实例属于某个给定列表的概率(例如,这种图片 70% 可能是数字 5)
from sklearn.ensemble import RandomForestClassifier
# 耗时 50秒左右
forest_clf = RandomForestClassifier(random_state=42)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3, method="predict_proba")

y_probas_forest[:10]
array([[0.11, 0.89],
       [0.99, 0.01],
       [0.96, 0.04],
       [1.  , 0.  ],
       [0.99, 0.01],
       [1.  , 0.  ],
       [1.  , 0.  ],
       [1.  , 0.  ],
       [1.  , 0.  ],
       [0.99, 0.01]])
y_scores_forest = y_probas_forest[:, 1]
fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5, y_scores_forest)

recall_for_forest = tpr_forest[np.argmax(fpr_forest >= fpr_90)]

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, "b:", linewidth=2, label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")
plt.plot([fpr_90, fpr_90], [0., recall_90_precision], "r:")
plt.plot([0.0, fpr_90], [recall_90_precision, recall_90_precision], "r:")
plt.plot([fpr_90], [recall_90_precision], "ro")
plt.plot([fpr_90, fpr_90], [0., recall_for_forest], "r:")
plt.plot([fpr_90], [recall_for_forest], "ro")
plt.grid(True)
plt.legend(loc="lower right", fontsize=16)
plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第7张图片

roc_auc_score(y_train_5, y_scores_forest)
0.9983436731328145
# 耗时 53秒左右
y_train_pred_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3)

# 精度、召回率
precision_score(y_train_5, y_train_pred_forest), recall_score(y_train_5, y_train_pred_forest)
(0.9905083315756169, 0.8662608374838591)

上面数据表明随机森林分类器优于SGD分类器,ROC AUC 分数更高,99%的精度和86.8%的召回率,还不错!

4、多类分类器(最后附上加速库代码)

二元分类器在两个类中区分,而多元分类器(也称多项分类器)可以区分两个以上的类。

  • 有一些算法可以直接处理多个类;
  • 也有一些严格的二元分类器;
  • 有多种策略可以用几个二元分类器实现多类分类的目的。

要创建一个系统将数字图片分为10类(从0到9)有两个方法:

  • 方法一

训练10个二元分类器,每个数字一个。然后,当你对一张图片进行检测分类时,获取每个分类器的决策分数,哪个分数高,就将其分给哪一类,这称为一对剩余(OvR)策略,也称为一对多(one-versus-all)

  • 方法二

为每一对数字训练一个二元分类器:一个用于区分0和1,一个用于区分0和2…,一个用于区分0和9;一个用于区分1和2,一个用于区分1和3…,一个用于区分1和9;以此类推,这称为一对一(OvO)策略。如果存在N个类别,那么这需要训练N×(N-1)/2个分类器,对于MNIST问题,这意味需要训练45个二元分类器,当需要对一张图片进行分类时,你需要运行45个分类器来对图片进行分类,最后看哪一类获胜最多。OvO的主要优点在于,每个分类器只需要用到部分训练集对其必须区分的两类进行训练。

有些算法(例如支持向量机分类器)在数据规模扩大时表现很糟。对于这类算法,OvO是一个优先的选择,因为在较小训练集上分别训练多个分类器比在大型数据集上训练少数分类器要快得多。但是对大多数二元分类器来说,OvR策略还是更好的选择。

Scikit-Learn可以检测到你尝试使用二元分类器算法进行多类分类任务,它会根据情况自动运行OvR或者OvO。我们用sklearn.svm.SVC类来试试SVM分类器

from sklearn.svm import SVC

svm_clf = SVC(gamma="auto", random_state=42)
# 因为这里使用多类分类器,所以用的 y_train 而不是 y_train_5
# 因为是多元分类器,所以特别的慢,这里只用 前1000个实例,减少运算时间(只用几秒)
# 经过我多次尝试,完整运行预计 2小时左右
svm_clf.fit(X_train[:1000], y_train[:1000])
svm_clf.predict([some_digit])
array([5], dtype=uint8)

可是看出非常容易!SVC的预测结果不再是简单的是/非,而是预测具体是哪个类。而在内部,Scikit-Learn实际上训练了45个二元分类器,获得它们对图片的决策分数,然后选择了分数最高的类。

想要知道是不是这样的,可以调用decision_function()方法,它会返回10个分数,每个类一个,而不再是每个实例返回1个分数:

some_digit_scores = svm_clf.decision_function([some_digit])
some_digit_scores
array([[ 2.81585438,  7.09167958,  3.82972099,  0.79365551,  5.8885703 ,
         9.29718395,  1.79862509,  8.10392157, -0.228207  ,  4.83753243]])
# svm_clf.classes_:目标类的列表,按值的大小排序,一般来说,不会这么巧合。
# np.argmax[some_digit_scores]:列表中最大值的索引
svm_clf.classes_[np.argmax(some_digit_scores)]
5

如果想要强制Scikit-Learn使用OvO或者OvR,可以使用OneVsOneClassifierOneVsRestClassifier类。只需要创建一个实例,然后将分类器传给其构造函数(它甚至不必是二元分类器)。

例如下面,使用OvR策略,基于SVC创建一个多类分类器:

from sklearn.multiclass import OneVsRestClassifier

ovr_clf = OneVsRestClassifier(SVC(gamma="auto", random_state=42))
# 经过我多次尝试,完整运行预计 21小时左右
ovr_clf.fit(X_train[:1000], y_train[:1000])
ovr_clf.predict([some_digit])
array([5], dtype=uint8)

训练SGDClassifier或者RandomForestClassifier同样简单:

sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])
array([3], dtype=uint8)

这次Scikit-Learn不必运行OvR或者OvO了,因为SGD分类器直接就是可以将实例分为多个类。调用decision_function()可以获得分类器将每个实例分类为每个类的概率列表:

sgd_clf.decision_function([some_digit])
array([[-31893.03095419, -34419.69069632,  -9530.63950739,
          1823.73154031, -22320.14822878,  -1385.80478895,
        -26188.91070951, -16147.51323997,  -4604.35491274,
        -12050.767298  ]])

好吧,看起来结果并不太准确,像之前一样,使用交叉验证来评估一下:

cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
array([0.87365, 0.85835, 0.8689 ])

在所有的测试折叠上都超过了85%,如果是一个纯随机分类器,准确率大概在10%,所以这个结果还不太糟糕,但是依然有提升的空间。

例如,进行简单缩放(上一章有讲到)可以将准确率提高到89%:

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
# 下面耗时 9分钟左右
cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")
array([0.8983, 0.891 , 0.9018])

5、误差分析

当你找到一个有潜力的模型,现在需要找到一些方法对其进一步改进。方法之一就是分析其错误类型。

首先看看混淆矩阵。就像之前做的,使用cross_val_predick()函数进行预测,然后调用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.shape
(10, 10)
conf_mx
array([[5577,    0,   22,    5,    8,   43,   36,    6,  225,    1],
       [   0, 6400,   37,   24,    4,   44,    4,    7,  212,   10],
       [  27,   27, 5220,   92,   73,   27,   67,   36,  378,   11],
       [  22,   17,  117, 5227,    2,  203,   27,   40,  403,   73],
       [  12,   14,   41,    9, 5182,   12,   34,   27,  347,  164],
       [  27,   15,   30,  168,   53, 4444,   75,   14,  535,   60],
       [  30,   15,   42,    3,   44,   97, 5552,    3,  131,    1],
       [  21,   10,   51,   30,   49,   12,    3, 5684,  195,  210],
       [  17,   63,   48,   86,    3,  126,   25,   10, 5429,   44],
       [  25,   18,   30,   64,  118,   36,    1,  179,  371, 5107]])

上面是一个 10×10 的混淆矩阵,我们使用Matplotlibmatshow()函数来擦汗混淆矩阵的图像表示通常更加方便:

fig = plt.figure(figsize=plt.figaspect(1/2))

ax = fig.add_subplot(121)
im = ax.matshow(conf_mx, cmap=plt.get_cmap('gray'))
plt.colorbar(im, cax=None, ax=None, shrink=0.75)

ax = fig.add_subplot(122)
im = ax.matshow(conf_mx)
plt.colorbar(im, cax=None, ax=None, shrink=0.75)

plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第8张图片
混淆矩阵看起来很不错,大多数图片都在主对角线上,这说明它们被正确分类。

不过上面色块的颜色不同,比如数字5(最绿或最暗)。

让我们把焦点放在错误上。首先,需要将混淆矩阵中的每个值除以类中的图片数量,这样你比较的就是错误率而不是错误的绝对值(后者对图片数量多的类不公平):

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

plt.matshow(norm_conf_mx)
plt.colorbar(shrink=0.8)
plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第9张图片
由于正确率远远大于错误率(上图可知,错误率颜色不明显),所以我们要用0填充主对角线,只保留错误,重新绘制结果:

np.fill_diagonal(norm_conf_mx, 0)  # 填充对角线为0

fig = plt.figure(figsize=plt.figaspect(1./2))

ax = fig.add_subplot(121)
im = ax.matshow(norm_conf_mx, cmap=plt.get_cmap('gray'))
plt.colorbar(im, cax=None, ax=None, shrink=0.75)

ax = fig.add_subplot(122)
im = ax.matshow(norm_conf_mx)
plt.colorbar(im, cax=None, ax=None, shrink=0.75)

plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第10张图片
现在可以清晰地看到分类器产生的错误种类了。每一行代表实例列,每一列代表预测类。第8列看起来非常亮(其他数字被预测成数字8),而第8行看起来就正常了许多(数字8被正确分类为数字8);另外一个比较明显的错误就是数字3数字5经常混淆(在两个方向上)。

分析混淆矩阵通常可以帮助你深入了解如何改进分类器。通过上图看了,你的精力可以花在改进数字8的分类错误上:

  1. 可以收集更多看起来像数字8的训练数据;
  2. 可以写一个算法来计算闭环的数量(比如:数组8有有两个,数字6有一个,数字5没有);
  3. 可以对图片进行预处理,让某些模式更为突出(比如,闭环之类的)。

分析单个错误也可以为分类器提供洞察:它在做什么?它为什么失败?但这通常更加困难和耗时。例如:看看数字3和数字5的示例:

def plot_digits_ax(ax, instances, images_per_row=10, **options):
    size = 28
    images_per_row = min(len(instances), images_per_row)
    images = [instance.reshape(size,size) for instance in instances]
    n_rows = (len(instances) - 1) // images_per_row + 1
    row_images = []
    n_empty = n_rows * images_per_row - len(instances)
    images.append(np.zeros((size, size * n_empty)))
    for row in range(n_rows):
        rimages = images[row * images_per_row : (row + 1) * images_per_row]
        row_images.append(np.concatenate(rimages, axis=1))
    image = np.concatenate(row_images, axis=0)
    ax.imshow(image, cmap="binary", **options)
    ax.axis("off")
cl_3, cl_5 = 3, 5
X_33 = X_train[(y_train == cl_3) & (y_train_pred == cl_3)]
X_35 = X_train[(y_train == cl_3) & (y_train_pred == cl_5)]
X_53 = X_train[(y_train == cl_5) & (y_train_pred == cl_3)]
X_55 = X_train[(y_train == cl_5) & (y_train_pred == cl_5)]

fig, ax = plt.subplots(2, 2, figsize=(8, 8))
plot_digits_ax(ax[0, 0], X_33[:25], 5)  # 左上,正确分类的 数字3
plot_digits_ax(ax[0, 1], X_35[:25], 5)  # 右上,被分为 数字5 的 数字3
plot_digits_ax(ax[1, 0], X_53[:25], 5)  # 左下,被分为 数字3 的 数字5
plot_digits_ax(ax[1, 1], X_55[:25], 5)  # 右下,正确分类的 数字5
plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第11张图片
有一些写的的确很糟糕,例如:左下第一行第二个数字,看起来更像数字3。

虽然对于我们来讲,上面大多数图片都有明显的区别,很难理解为什么分类器会分类错误。原因在于,我们使用的简单的SGDClassifier模型是简单线性模型。它做作的就是为每一个像素分配一个各个类别的权重,当它看到新的图像时,将加权后的像素强度汇总,从而得到一个分数进行分类。而数字3和数字5只在一部分像素位上有区别,所以分类器很容易将其混淆。

而解决方法就是对图片进行预处理。

6、多标签分类

到目前为止,每个实例都只会被分在一个类里,而在某些情况下,你希望分类器为每个实例输出多个类。例如,人脸识别的分类:如果在一张照片里识别出多个人怎么办?当然,应该为识别出来的每个人都附上一个标签。假设分类器经过训练,已经可以识别出三张人脸 – 张三、赵四、王五。那么当看到一张张三和王五的合照时,它应该输出 [1, 0, 1](意思是“是张三,不是赵四,是王五”)这种输出多个二元标签的分类系统称为多标签分类系统。

这里看一个简单的示例:

from sklearn.neighbors import KNeighborsClassifier

y_train_large = (y_train >= 7)  # 大于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)
# 预测
knn_clf.predict([some_digit])
array([[False,  True]])

结果是正确的,数字5的确不大于7(False),为奇数(True)。

评估多标签分类器的方法很多,如果选择正确的度量指标取决于你的项目。比如方法之一是测量每个标签的F1分数(或者之前用过的的任何其他二元分类器指标),然后简单地计算平均分数。下面计算所有标签的平均F1分数:

y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
f1_score(y_multilabel, y_train_knn_pred, average="macro")
0.976410265560605

这里假设所有的标签都同等重要,但实际可能不是这样。特别地,如果训练的照片里张三比赵四和王五的多很多,你可能想给区分张三的分类器更高的权重。一个简单地办法是给每个标签设置一个等于其自身支持的权重(也就是具有该目标标签的实例的数量),为此,只需要在上面的代码中设置average="weighted"即可。

7、多输出分类

这种分类也称为多输出 - 多类分类,简单的来说,它是多标签分类的泛化,其标签也可以是多类的(比如它可以有两个以上可能的值)。

为了说明这一点,构建一个系统去除图片中的噪点。给它输入一张有噪点的图片,它将(希望)输出一张干净的数字图片,与其他 MNIST 图片一样,以像素强度的一个数组作为呈现方式,请注意,这个分类器的输出是多个标签(一个像素点一个标签),每个标签可以有多个值(像素强度范围为 0~255)。所以这是个多输出分类器系统的示例。

还是先从创建训练集和测试集开始,使用NumPyrandint()函数为MNIST图片的像素强度增加噪点。目标是将图片还原为原始图片:

# 随机生成0~100的整数,填充(60000,784)的数组
noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise  # 训练集添加噪点
# 随机生成0~100的整数,填充(10000,784)的数组
noise = np.random.randint(0, 100, (len(X_test), 784))
X_test_mod = X_test + noise  # 测试集添加噪点

# 添加噪点之前的数组作为标签
y_train_mod = X_train
y_test_mod = X_test

查看一下添加噪点前后的对比:

fig = plt.figure(figsize=plt.figaspect(1./2))

# 加了噪点的数字图片
ax = fig.add_subplot(121)
mod_digit = X_train_mod[0]
mod_digit_image = mod_digit.reshape(28, 28)
ax.imshow(mod_digit_image, cmap="binary")
plt.axis("off")

# 希望还原的数字图片
ax = fig.add_subplot(122)
mod_digit = y_train_mod[0]
mod_digit_image = mod_digit.reshape(28, 28)
ax.imshow(mod_digit_image, cmap="binary")
plt.axis("off")

plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第12张图片
左边是有噪点的输入图片, 右边是根据的图片。现在我们通过训练分类器,清洗这张照片:

some_index = 23
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
fig = plt.figure(figsize=plt.figaspect(1./2))

# 清洗前的数字图片
ax = fig.add_subplot(121)
mod_digit = X_test_mod[some_index]
mod_digit_image = mod_digit.reshape(28, 28)
ax.imshow(mod_digit_image, cmap="binary")
plt.axis("off")

# 清洗后的数字图片
ax = fig.add_subplot(122)
mod_digit_image = clean_digit.reshape(28, 28)
ax.imshow(mod_digit_image, cmap="binary")
plt.axis("off")

plt.show()

【推荐收藏】【机器学习实战】分类(以MNIST为例)(挑战全网最全,没有之一,另附完整代码与加速库的使用)_第13张图片
看起来离目标够接近了。分类器之旅到此就结束了。

8、完整关键代码

import matplotlib.pyplot as plt
import numpy as np
from sklearn.base import BaseEstimator
from sklearn.base import clone
from sklearn.datasets import fetch_openml
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import confusion_matrix
from sklearn.metrics import f1_score
from sklearn.metrics import precision_recall_curve
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
from sklearn.model_selection import cross_val_predict
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import StratifiedKFold
from sklearn.multiclass import OneVsRestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC


class Never5Classifier(BaseEstimator):
    """
    返回全部为 False 的列表
    """
    def fit(self, X, y=None):
        return self

    def predict(self, X):
        return np.zeros((len(X), 1), dtype=bool)  # 生成 len(X) 行,1 列,的 False(0)
    
    
def self_cross_val_score(estimator, X, y, n_splits):
    """
    交叉验证,输出每次准确率。
    :param estimator: 分类器模型
    :param X: 训练集
    :param y: 标签集
    :param n_splits: 折叠次数
    :return: 打印每次交叉验证的准确率
    """
    # StratifiedKFold:K-交叉验证分层器,返回分层折叠
    skfolds = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42)
    accuracy_list = []
    # 返回两个折叠:训练集索引,测试集索引
    for train_index, test_index in skfolds.split(X, y):
        clone_clf = clone(estimator)    # 克隆分类器
        X_train_folds = X[train_index]  # 训练集
        y_train_folds = y[train_index]  # 训练标签集
        X_test_folds = X[test_index]    # 测试集
        y_test_folds = y[test_index]    # 测试标签集

        clone_clf.fit(X_train_folds, y_train_folds)    # 训练分配器
        y_pred = clone_clf.predict(X_test_folds)       # 进行预测
        n_correct = sum(y_pred == y_test_folds)        # 统计正确预测次数
        accuracy_list.append(n_correct / len(y_pred))  # 输出准确率
    return accuracy_list


def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
    """
    绘制 精度、召回率 和 阈值 的图像
    :param precisions: 精度
    :param recalls: 召回率
    :param thresholds: 阈值
    """
    plt.plot(thresholds, precisions[:-1], "b--", label="Precision", linewidth=2)
    plt.plot(thresholds, recalls[:-1], "g-", label="Recall", linewidth=2)
    plt.legend(loc="center right", fontsize=16)  # 显示图例,位置 中右
    plt.xlabel("Threshold", fontsize=16)         # X轴命名
    plt.grid(True)                               # 显示网格
    plt.axis([-50000, 50000, 0, 1])              # 坐标显示 X轴范围 -50000~50000;Y轴范围 0~1
    
    
def plot_precision_vs_recall(precisions, recalls):
    """
    绘制 精度 和 召回率 的图像
    :param precisions: 精度
    :param recalls: 召回率
    """
    plt.plot(recalls, precisions, "b-", linewidth=2)
    plt.xlabel("Recall", fontsize=16)
    plt.ylabel("Precision", fontsize=16)
    plt.axis([0, 1, 0, 1])
    plt.grid(True)
    
    
def plot_roc_curve(fpr, tpr, label=None):
    """
    绘制 受试者工作特征曲线(简称 ROC)
    :param fpr: 假正类率(FPR):是被错误分为正类的负类实例比例。它等于1减去真负类率(TNR);
    :param tpr: 真负类率(TNR):是被正确分类为负类的负类实例比例,也成为特异度;
    :param label: 图像名称
    """
    plt.plot(fpr, tpr, linewidth=2, label=label)
    plt.plot([0, 1], [0, 1], 'k--')  # 绘制对角虚线
    plt.axis([0, 1, 0, 1])           # X、Y轴范围均为 0~1
    plt.xlabel('False Positive Rate (Fall-Out)', fontsize=16)  # 假正率
    plt.ylabel('True Positive Rate (Recall)', fontsize=16)     # 真正率(召回率)
    plt.grid(True)
    
    
def plot_digits_ax(ax, instances, images_per_row=10, **options):
    size = 28
    images_per_row = min(len(instances), images_per_row)
    images = [instance.reshape(size,size) for instance in instances]
    n_rows = (len(instances) - 1) // images_per_row + 1
    row_images = []
    n_empty = n_rows * images_per_row - len(instances)
    images.append(np.zeros((size, size * n_empty)))
    for row in range(n_rows):
        rimages = images[row * images_per_row : (row + 1) * images_per_row]
        row_images.append(np.concatenate(rimages, axis=1))
    image = np.concatenate(row_images, axis=0)
    ax.imshow(image, cmap="binary", **options)
    ax.axis("off")
    
    
"""
下载素材
"""
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
# X:数据集;y:标签集
X, y = mnist['data'], mnist['target']
y = y.astype(np.uint8)  # 标签集 字符串 转 数字
# 随便找一个数据集数据,用于后续数据探索示例
some_digit = X[0]
"""
创建 训练集与测试集
"""
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
"""
训练二元分类器:SGDClassifier
"""
# 分类器以 数字5 为示例
y_train_5 = (y_train == 5)  # 判断 y_train 中的数据是否等于 5,得到一个 布尔列表。
y_test_5 = (y_test == 5)
# 随机梯度下降(SGD)分类器
sgd_clf = SGDClassifier(max_iter=1000, tol=1e-3, random_state=42)  # 前两个是新版默认参数
sgd_clf.fit(X_train, y_train_5)              # 训练
sgd_predict = sgd_clf.predict([some_digit])  # 预测
print("SGD预测结果:", sgd_predict)
"""
性能测量
"""
# 手写交叉验证
sgd_score = self_cross_val_score(sgd_clf, X_train, y_train_5, 3)
print("手写交叉验证:", sgd_score)
# Scikit-Learn 提供的;scoring="accuracy":选择输出类型为"准确率"
sgd_score = cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
print("Scikit-Learn:", sgd_score)

# 验证全部分为"非5"的准确率
never_5_clf = Never5Classifier()
never_5_score = cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")
print("Never5Classifier:", sgd_score)

# 混淆矩阵
# 与cross_val_score()函数一样,cross_val_predict()函数相同执行K-折交叉验证
# cross_val_score() 返回评估分数;cross_val_predict() 返回每个实例的预测(即 一个布尔列表)
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
print("混淆矩阵:\n", confusion_matrix(y_train_5, y_train_pred))
print("完美分类器的混淆矩阵:\n", confusion_matrix(y_train_5, y_train_5))

# 精度与召回率
print("精度\t:", precision_score(y_train_5, y_train_pred))
print("召回率\t:", recall_score(y_train_5, y_train_pred))
print("F1\t:", f1_score(y_train_5, y_train_pred))

# 精度/召回率权衡
# sgd_clf.predict:返回预测结果;sgd_clf.decision_function:返回预测分数
y_scores = sgd_clf.decision_function([some_digit])
print("SGD预测分数:", y_scores)

threshold = 0  # 如果阈值设置为 0
y_some_digit_pred = (y_scores > threshold)
print("通过设置阈值为 {},把SGD预测分数变为结果:{} -> {}".format(threshold, y_scores, y_some_digit_pred))
threshold = 3000  # 如果阈值设置为 0
y_some_digit_pred = (y_scores > threshold)
print("通过设置阈值为 {},把SGD预测分数变为结果:{} -> {}".format(threshold, y_scores, y_some_digit_pred))

# cross_val_predict 这里不再返回每个实例的预测结果(布尔值),而是使用 decision_function 返回每个实例的预测分数
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3, method="decision_function")
# 精度、召回率、阈值:返回值按 精度 从低到高 排序
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)

recall_90_precision = recalls[np.argmax(precisions >= 0.90)]        # 当准确率第一次大于0.90时的召回率。
threshold_90_precision = thresholds[np.argmax(precisions >= 0.90)]  # 当准确率第一次大于0.90时的阈值。
# 精度、召回率 与 阈值 的关系曲线
plt.figure(figsize=(8, 4))  # 指定figure的宽和高,单位为英寸
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.plot([threshold_90_precision, threshold_90_precision], [0., 0.9], "r:")
plt.plot([-50000, threshold_90_precision], [0.9, 0.9], "r:")
plt.plot([-50000, threshold_90_precision], [recall_90_precision, recall_90_precision], "r:")
plt.plot([threshold_90_precision], [0.9], "ro")
plt.plot([threshold_90_precision], [recall_90_precision], "ro")
plt.show()

# PR曲线(精度/召回率曲线)
plt.figure(figsize=(8, 6))
plot_precision_vs_recall(precisions, recalls)
plt.plot([recall_90_precision, recall_90_precision], [0., 0.9], "r:")
plt.plot([0.0, recall_90_precision], [0.9, 0.9], "r:")
plt.plot([recall_90_precision], [0.9], "ro")
plt.show()

y_train_pred_90 = (y_scores >= threshold_90_precision)
print("当精度为:{} 时,召回率为:{},阈值为:{}".format(
    precision_score(y_train_5, y_train_pred_90),  # 精度
    recall_score(y_train_5, y_train_pred_90),     # 召回率
    threshold_90_precision))                      # 阈值

# ROC 曲线
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)

plt.figure(figsize=(8, 6))
plot_roc_curve(fpr, tpr)
fpr_90 = fpr[np.argmax(tpr >= recall_90_precision)]  # 准确率90%时,召回率的位置
plt.plot([fpr_90, fpr_90], [0., recall_90_precision], "r:")
plt.plot([0.0, fpr_90], [recall_90_precision, recall_90_precision], "r:")
plt.plot([fpr_90], [recall_90_precision], "ro")
plt.show()
# 测量曲线下面积(AUC)
print("SGDClassifier 的 ROC AUC:", roc_auc_score(y_train_5, y_scores))
"""
训练二元分类器:RandomForestClassifier
"""
forest_clf = RandomForestClassifier(random_state=42)
# 由于工作方式不同,RandomForestClassifier类没有decision_function()方法,相反,它有predict_proba()方法。
# Scikit-Learn的分类器通常都有这两种方法的一种(或两种都有)。
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3, method="predict_proba")
print("RandomForestClassifier分类器 交叉验证部分结果:\n", y_probas_forest[:10])
y_scores_forest = y_probas_forest[:, 1]
fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5, y_scores_forest)

recall_for_forest = tpr_forest[np.argmax(fpr_forest >= fpr_90)]

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, "b:", linewidth=2, label="SGD")       # 绘制上面 SGD 的 ROC 曲线
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")  # 绘制新的 随机森林分类器 的 ROC 曲线
plt.plot([fpr_90, fpr_90], [0., recall_90_precision], "r:")
plt.plot([0.0, fpr_90], [recall_90_precision, recall_90_precision], "r:")
plt.plot([fpr_90], [recall_90_precision], "ro")
plt.plot([fpr_90, fpr_90], [0., recall_for_forest], "r:")
plt.plot([fpr_90], [recall_for_forest], "ro")
plt.grid(True)
plt.legend(loc="lower right", fontsize=16)
plt.show()

print("RandomForestClassifier 的 ROC AUC:", roc_auc_score(y_train_5, y_scores_forest))

y_train_pred_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3)
print("精度为:{} 时,召回率为:{}".format(
    precision_score(y_train_5, y_train_pred_forest),   # 精度
    recall_score(y_train_5, y_train_pred_forest)))     # 召回率

"""
多类分类器
"""
svm_clf = SVC(gamma="auto", random_state=42)
svm_clf.fit(X_train[:1000], y_train[:1000])           # 训练
print("SVC 预测结果:", svm_clf.predict([some_digit]))  # 预测
# svm_clf.decision_function 返回结果是 每个类的预测分数
some_digit_scores = svm_clf.decision_function([some_digit])
print("SVC 每个类的预测分数:\n", svm_clf.predict([some_digit]))
# svm_clf.classes_:目标类的列表,按值的大小排序,一般来说,不会这么巧合。
# np.argmax[some_digit_scores]:列表中最大值的索引
print("最大预测分数的类:", svm_clf.classes_[np.argmax(some_digit_scores)])

# 一对多分类器
ovr_clf = OneVsRestClassifier(SVC(gamma="auto", random_state=42))
ovr_clf.fit(X_train[:1000], y_train[:1000])                   # 训练
print("一对多 SVC 预测结果:\n", ovr_clf.predict([some_digit]))  # 预测

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
conf_mx = confusion_matrix(y_train, y_train_pred)
print("SGD 数据缩放后 多类分类交叉验证后 混淆矩阵:\n", conf_mx)
# 绘制 conf_mx 图像
plt.matshow(conf_mx)
plt.colorbar(shrink=0.8)
plt.show()
# 绘制 conf_mx 错误概率的图像
row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums
np.fill_diagonal(norm_conf_mx, 0)  # 填充对角线为0

plt.matshow(norm_conf_mx)
plt.colorbar(shrink=0.8)
plt.show()

# 绘制 数字3、数字5,正确分类和相互错误分类的图像
cl_3, cl_5 = 3, 5
X_33 = X_train[(y_train == cl_3) & (y_train_pred == cl_3)]
X_35 = X_train[(y_train == cl_3) & (y_train_pred == cl_5)]
X_53 = X_train[(y_train == cl_5) & (y_train_pred == cl_3)]
X_55 = X_train[(y_train == cl_5) & (y_train_pred == cl_5)]

fig, ax = plt.subplots(2, 2, figsize=(8, 8))
plot_digits_ax(ax[0, 0], X_33[:25], 5)  # 左上,正确分类的 数字3
plot_digits_ax(ax[0, 1], X_35[:25], 5)  # 右上,被分为 数字5 的 数字3
plot_digits_ax(ax[1, 0], X_53[:25], 5)  # 左下,被分为 数字3 的 数字5
plot_digits_ax(ax[1, 1], X_55[:25], 5)  # 右下,正确分类的 数字5
plt.show()

"""
多标签分类
"""
y_train_large = (y_train >= 7)    # 大于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)        # 训练
print("多标签分类 预测结果:", knn_clf.predict([some_digit]))  # 预测

y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
print("多标签分类 F1 分数:", f1_score(y_multilabel, y_train_knn_pred, average="macro"))

"""
多输出分类
"""
# 随机生成0~100的整数,填充(60000,784)的数组
noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise
# 随机生成0~100的整数,填充(10000,784)的数组
noise = np.random.randint(0, 100, (len(X_test), 784))
X_test_mod = X_test + noise

y_train_mod = X_train
y_test_mod = X_test
# 加入噪点前后对比
fig = plt.figure(figsize=plt.figaspect(1./2))
# 加了噪点的数字图片
ax = fig.add_subplot(121)
mod_digit = X_train_mod[0]
mod_digit_image = mod_digit.reshape(28, 28)
ax.imshow(mod_digit_image, cmap="binary")
plt.axis("off")
# 希望还原的数字图片
ax = fig.add_subplot(122)
mod_digit = y_train_mod[0]
mod_digit_image = mod_digit.reshape(28, 28)
ax.imshow(mod_digit_image, cmap="binary")
plt.axis("off")

plt.show()
# 多输出分类学习清洗
some_index = 23
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
# 清洗前后对比
fig = plt.figure(figsize=plt.figaspect(1./2))
# 清洗前的数字图片
ax = fig.add_subplot(121)
mod_digit = X_test_mod[some_index]
mod_digit_image = mod_digit.reshape(28, 28)
ax.imshow(mod_digit_image, cmap="binary")
plt.axis("off")
# 清洗后的数字图片
ax = fig.add_subplot(122)
mod_digit_image = clean_digit.reshape(28, 28)
ax.imshow(mod_digit_image, cmap="binary")
plt.axis("off")

plt.show()

9、加速库的使用

# pip install scikit-learn-intelex

import time

import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.multiclass import OneVsRestClassifier
from sklearnex.svm import SVC # intel官方,scikit-learn加速库,使用与原库相同,可加速10-100倍。


def times():
    time_now = time.time()
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time_now)))
    return time_now


def svc_clf_fit():
    svm_clf = SVC(gamma="auto", random_state=42)
    st = times()
    svm_clf.fit(X_train, y_train)
    et = times()
    print("SVC 预测结果:{}".format(svm_clf.predict([some_digit])))
    print("SVC 共耗时:{} 秒".format(et - st))


def ovr_clf_fit():
    ovr_clf = OneVsRestClassifier(SVC(gamma="auto", random_state=42))
    st = times()
    ovr_clf.fit(X_train, y_train)
    et = times()
    print("OvR 预测结果:{}".format(ovr_clf.predict([some_digit])))
    print("OvR 共耗时:{} 秒".format(et - st))


if __name__ == '__main__':
    mnist = fetch_openml('mnist_784', version=1, as_frame=False)

    X, y = mnist['data'], mnist['target']
    y = y.astype(np.uint8)
    X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]

    some_digit = X[0]

    svc_clf_fit()  # 原库耗时 约 2小时,加速后 只需要 约40秒
    ovr_clf_fit()  # 原库耗时 约21小时,加速后 只需要 约43秒

你可能感兴趣的:(机器学习,分类,sklearn)