机器学习实战_分类(一)
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 曲线应该尽可能地靠近右上角)。