机器学习实战_分类(一)

yangzzguang 2019-06-27

MNIST:手写数字分类数据集

我们将会使用 MNIST 这个数据集,它有着 70000 张规格较小的手写数字图片,由美国的高中生和美国人口调查局的职员手写而成。这相当于机器学习当中的“Hello World”。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',   # DESCR键描述数据集
'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.])}    # target键存放一个标签数组
X, y = mnist["data"], mnist["target"]  # 获取样本或标签

MNIST 有 70000 张图片,每张图片有 784 个特征。这是因为每个图片都是28×28像素的,并且每个像素的值介于 0~255 之间。让我们看一看数据集的某一个数字。你只需要将某个实例的特征向量,reshape为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)    # 将样本转为28大小的像素矩阵
# 按‘0’‘1’数值转为灰度图像 
# interpolation当小图像放大时,interpolation ='nearest'效果很好,否则用None。
plt.imshow(some_digit_image, cmap = matplotlib.cm.binary, interpolation="nearest")    
plt.axis("off")
plt.show()

机器学习实战_分类(一)

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

>>> y[36000]
5.0

MNIST 数据集已经事先被分成了一个训练集(前 6000 张图片)和一个测试集(最后 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]

训练一个二分类器

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

# 在训练和测试集上区分是否为5转为0,1标签矩阵
y_train_5 = (y_train == 5) # True for all 5s, False for all other digits.
y_test_5 = (y_test == 5)

采用随机梯度下降分类器

from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)    #如果你想重现结果,你应该固定参数random_state 
sgd_clf.fit(X_train, y_train_5)

输出预测结果

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

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

使用交叉验证测量准确性

评估一个模型的好方法是使用交叉验证,像之前提过一样。但有时为了有更好的控制权,可以写自己版本的交叉验证,以下代码粗略地做了和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类实现了分层采样,生成的折(fold)包含了各类相应比例的样例。在每一次迭代,上述代码生成分类器的一个克隆版本,在训练折(training folds)的克隆版本上进行训,在测试折(test folds)上进行预测。然后它计算出被正确预测的数目和输出正确预测的比例。

这里使用sklearn提供的cross_val_score()函数来评估SGDClassifier模型

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

有大于 95% 的精度(accuracy),特别高!但要注意这是一个有数据偏差的数据集,这是因为只有 10% 的图片是数字 5,所以你总是猜测某张图片不是 5,你也会有90%的可能性是对的。处理这类问题,要回归到之前讲的准确率和召回率和ORC曲线了。

混淆矩阵

对分类器来说,一个好得多的性能评估指标是混淆矩阵,为了计算混淆矩阵,首先你需要有一系列的预测值,这样才能将预测值与真实值做比较。你或许想在测试集上做预测。但是我们现在先不碰它。(记住,只有当你处于项目的尾声,当你准备上线一个分类器的时候,你才应该使用测试集)。相反,你应该使用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
>>> confusion_matrix(y_train_5, y_train_pred)
array([[53272, 1307],
        [ 1077, 4344]])

Scikit-Learn 提供了一些函数去计算分类器的指标,包括精确率和召回率(之前的文章是tensorflow,这里主要讲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

通常结合精确率和召回率会更加方便,这个指标叫做“F1 值”,特别是当你需要一个简单的方法去比较两个分类器的优劣的时候。F1 值是精确率和召回率的调和平均。普通的平均值平等地看待所有的值,而调和平均会给小的值更大的权重。所以,要想分类器得到一个高的 F1 值,需要召回率和精确率同时高

$$F1 = \frac{2}{\frac{1}{precision} + \frac{1}{recall}} = 2 * \frac{precison * recall}{precison + recall} = \frac{TP}{TP + \frac{FN + FP}{2}}$$

为了计算 F1 值,简单调用f1_score()

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

F1 支持那些有着相近精确率和召回率的分类器。这不会总是你想要的。有的场景你会绝大程度地关心精确率,而另外一些场景你会更关心召回率。不幸的是,你不能同时拥有两者。增加精确率会降低召回率,反之亦然。这叫做精确率与召回率之间的折衷.一般来讲,提高阈值,会有假正例成为一个真反例,从而提高准确率相反,降低阈值可提高召回率、降低精确率。(可以回顾之前的文章)

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

>>> 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 的时候,分类器可以探测到这是一个 5,当阈值提高到 20000 的时候,分类器将不能探测到这是数字 5。(这里的精确率衡量的是真反例,偏预测为非5,召回率衡量是真正例,偏预测为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 画出精确率和召回率,这里把精确率和召回率当作是阈值的一个函数。

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

机器学习实战_分类(一)

你也许会好奇为什么精确率曲线比召回率曲线更加起伏不平(右上部分)。原因是精确率有时候会降低,尽管当你提高阈值的时候,通常来说精确率会随之提高。另一方面,当阈值提高时候,召回率只会降低。这也就说明了为什么召回率的曲线更加平滑。

现在你可以选择适合你任务的最佳阈值。另一个选出好的精确率/召回率折衷的方法是直接画出精确率对召回率的曲线(PR曲线),如图所示。

机器学习实战_分类(一)

我们假设你决定达到 90% 的准确率,在 70000 附近找到一个阈值。为了作出预测(目前为止只在训练集上预测),你可以运行以下代码,而不是运行分类器的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

ROC 曲线

受试者工作特征(ROC)曲线是另一个二分类器常用的工具。它非常类似与准确率/召回率曲线(PR曲线),但不是画出准确率对召回率的曲线,ROC 曲线是真正例率(true positive rate,另一个名字叫做召回率)对假正例率(false positive rate, FPR)的曲线。FPR 是反例被错误分成正例的比率。它等于 1 减去真反例率(true negative rate, TNR)。TNR是反例被正确分类的比率。TNR也叫做特异性。所以 ROC 曲线画出召回率对(1 减特异性)的曲线。
$$TPR = \frac{TP}{P} = \frac{TP}{TP+FN}$$
$$FPR = \frac{FP}{N} = \frac{FP}{FP+TN} = 1-TNR$$
$$TNR = \frac{TN}{N} = \frac{TN}{TN+FP}$$

机器学习实战_分类(一)

为了画出 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()

一个比较分类器之间优劣的方法是:测量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

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

相关推荐