🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
在第 1 章中,我提到最常见的监督学习任务是回归(预测值)和分类(预测类)。在第 2 章中,我们探讨了回归任务,预测住房价值,使用各种算法,如线性回归、决策树和随机森林(将在后面的章节中更详细地解释)。现在我们将注意力转向分类系统。
MNIST
在本章我们将使用 MNIST 数据集,它是一组由高中生和美国人口普查局员工手写的 70,000 张小数字图像。每个图像都标有它所代表的数字。这个集合已经被大量研究,以至于它通常被称为机器学习的“hello world”:每当人们提出一种新的分类算法时,他们都很想知道它在 MNIST 上的表现如何,任何学习机器学习的人都能解决这个问题数据集迟早。
Scikit-学习提供许多帮助函数来下载流行的数据集。MNIST 就是其中之一。以下代码获取 MNIST 数据集:1
>>> from sklearn.datasets import fetch_openml
>>> mnist = fetch_openml('mnist_784', version=1)
>>> mnist.keys()
dict_keys(['data', 'target', 'feature_names', 'DESCR', 'details',
'categories', 'url'])
Scikit-Learn 加载的数据集通常具有类似的字典结构,包括以下内容:
-
DESCR
描述数据集的键 -
包含一个数组的
data
键,每个实例一行,每个特征一列 -
target
包含带有标签的数组的键
让我们看看这些数组:
>>> X, y = mnist["data"], mnist["target"]
>>> X.shape
(70000, 784)
>>> y.shape
(70000,)
有 70,000 张图像,每张图像有 784 个特征。这是因为每张图像都是 28 × 28 像素,每个特征只是代表一个像素的强度,从 0(白色)到 255(黑色)。让我们看一下数据集中的一位数。您需要做的就是抓取一个实例的特征向量,将其重塑为一个 28 × 28 的数组,然后使用 Matplotlib 的imshow()
函数显示它:
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")
plt.axis("off")
plt.show()
这看起来像一个 5,实际上这就是标签告诉我们的:
>>> y[0]
'5'
请注意,标签是一个字符串。大多数 ML 算法都需要数字,所以让我们y
转换为整数:
>>> import numpy as np
>>> y = y.astype(np.uint8)
为了让您了解分类任务的复杂性,图 3-1显示了来自 MNIST 数据集的更多图像。
图 3-1。来自 MNIST 数据集的数字
可是等等!在仔细检查数据之前,您应该始终创建一个测试集并将其放在一边。MNIST 数据集实际上已经分为训练集(前 60,000 张图像)和测试集(最后 10,000 张图像):
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
训练集已经为我们打乱了,这很好,因为这保证了所有交叉验证折叠都是相似的(你不希望一个折叠丢失一些数字)。此外,一些学习算法对训练实例的顺序很敏感,如果连续获得许多相似实例,它们的性能就会很差。改组数据集可确保不会发生这种情况。2
训练二元分类器
让我们简化现在的问题,只尝试识别一个数字——例如,数字 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)
现在让我们选择一个分类器并训练它。一个很好的起点带有随机梯度下降(SGD)分类器,使用 Scikit-Learn 的SGDClassifier
类。该分类器的优点是能够有效地处理非常大的数据集。这部分是因为 SGD 一次一个地独立处理训练实例(这也使得 SGD 非常适合用于在线学习),我们稍后会看到。让我们创建一个SGDClassifier
并在整个训练集上训练它:
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)
小费
SGDClassifier
依赖于训练期间的随机性(因此称为“随机”)。如果您想要可重现的结果,您应该设置random_state
参数。
现在我们可以用它来检测数字 5 的图像:
>>> sgd_clf.predict([some_digit])
array([ True])
分类器猜测这个图像代表一个 5 ( True
)。看起来它在这种特殊情况下猜对了!现在,让我们评估这个模型的性能。
绩效指标
评估分类器通常比评估回归器要复杂得多,因此我们将在本章的大部分时间里讨论这个主题。有许多可用的性能指标,所以再喝杯咖啡,准备学习许多新概念和首字母缩略词!
使用交叉验证测量准确性
一个评估模型的好方法是使用交叉验证,就像您在第 2 章中所做的那样。
实施交叉验证
有时,您需要对交叉验证过程进行更多控制,而不是 Scikit-Learn 提供的现成功能。在这些情况下,您可以自己实施交叉验证。这以下代码与 Scikit-Learn 的功能大致相同cross_val_score()
,并且打印相同的结果:
from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone
skfolds = StratifiedKFold(n_splits=3, shuffle=True, 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
班级_执行分层抽样(如第 2 章所述)以生成包含每个类的代表性比率的折叠。在每次迭代中,代码都会创建分类器的克隆,在训练折叠上训练克隆,并在测试折叠上进行预测。然后它计算正确预测的数量并输出正确预测的比率。
让我们使用cross_val_score()
函数来评估我们的SGDClassifier
模型,使用三折的 K 折交叉验证。请记住,K 折交叉验证意味着将训练集分成 K 折(在本例中为 3 折),然后使用在剩余折上训练的模型对每一折进行预测和评估(参见第 2 章):
>>> from sklearn.model_selection import cross_val_score
>>> cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([0.96355, 0.93795, 0.95615])
哇!以上所有交叉验证折叠的 93% 准确率(正确预测的比率)?这看起来很神奇,不是吗?好吧,在你太兴奋之前,让我们看一个非常愚蠢的分类器,它只对“非 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)
你能猜出这个模型的准确性吗?让我们来了解一下:
>>> 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])
没错,它有超过 90% 的准确率!这仅仅是因为只有大约 10% 的图像是 5s,所以如果你总是猜测图像不是5,那么大约 90% 的时间你是对的。击败诺查丹玛斯。
这说明了为什么准确性通常不是分类器的首选性能指标,尤其是在处理倾斜数据集时(即,当某些类比其他类更频繁时)。
混淆矩阵
一个评估分类器性能的更好方法是查看混淆矩阵。一般的想法是计算 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 折交叉验证,但它不是返回评估分数,而是返回对每个测试折叠所做的预测。这意味着您可以为训练集中的每个实例获得一个干净的预测(“干净”意味着预测是由一个在训练期间从未见过数据的模型做出的)。
现在您已准备好使用该confusion_matrix()
函数获取混淆矩阵。只需将目标类 ( y_train_5
) 和预测类 ( y_train_pred
) 传递给它:
>>> from sklearn.metrics import confusion_matrix
>>> confusion_matrix(y_train_5, y_train_pred)
array([[53057, 1522],
[ 1325, 4096]])
混淆矩阵中的每一行代表一个实际类别,而每一列代表一个预测类别。该矩阵的第一行考虑非 5 图像(负类):其中 53,057 个被正确分类为非 5(它们称为真负),而其余 1,522 个被错误分类为 5(假正)。第二行考虑 5s(正类)的图像:1,325 个被错误分类为非 5s(假阴性),而其余 4,096 个被正确分类为 5s(真阳性))。一个完美的分类器只有真阳性和真阴性,所以它的混淆矩阵只会在其主对角线上(左上到右下)有非零值:
>>> y_train_perfect_predictions = y_train_5 # pretend we reached perfection
>>> confusion_matrix(y_train_5, y_train_perfect_predictions)
array([[54579, 0],
[ 0, 5421]])
混淆矩阵为您提供了大量信息,但有时您可能更喜欢更简洁的指标。一个有趣的问题是正面预测的准确性。这是称为分类器的精度(公式 3-1)。
公式 3-1。精确
precision=TP/(TP+FP)TP是真阳性数,FP是假阳性数。
获得完美精度的一种简单方法是进行一次正面预测并确保它是正确的(精度 = 1/1 = 100%)。但这不会很有用,因为分类器会忽略除一个正例之外的所有实例。所以精度通常与另一个名为召回的指标一起使用,也称为灵敏度或真阳性率(TPR):这是分类器正确检测到的阳性实例的比率(公式 3-2)。
公式 3-2。记起
recall=TP/(TP+FN)当然, FN是假阴性的数量。
如果您对混淆矩阵感到困惑,图 3-2可能会有所帮助。
图 3-2。图示的混淆矩阵显示了真阴性(左上)、假阳性(右上)、假阴性(左下)和真阳性(右下)的示例
精确度和召回率
Scikit-学习提供了几个函数来计算分类器指标,包括精度和召回率:
>>> from sklearn.metrics import precision_score, recall_score
>>> precision_score(y_train_5, y_train_pred) # == 4096 / (4096 + 1522)
0.7290850836596654
>>> recall_score(y_train_5, y_train_pred) # == 4096 / (4096 + 1325)
0.7555801512636044
现在,您的 5 检测器看起来不像您查看其准确性时那样闪亮。当它声称图像代表 5 时,它只有 72.9% 的时间是正确的。此外,它只检测到 75.6% 的 5s。
将精确率和召回率组合成一个单独的指标通常很方便,称为F 1分数,特别是如果您需要一种简单的方法来比较两个分类器。F 1分数是准确率和召回率的调和平均值(公式 3-3)。常规均值平等对待所有值,而调和均值赋予低值更多的权重。因此,如果召回率和准确率都很高,分类器只会获得高 F 1分数。
公式 3-3。F 1分
要计算 F 1分数,只需调用f1_score()
函数:
>>> from sklearn.metrics import f1_score
>>> f1_score(y_train_5, y_train_pred)
0.7420962043663375
F 1score 有利于具有相似精度和召回率的分类器。这并不总是您想要的:在某些情况下,您主要关心精度,而在其他情况下,您真正关心召回。例如,如果您训练了一个分类器来检测对儿童安全的视频,那么您可能更喜欢一个拒绝许多好的视频(低召回率)但只保留安全视频(高精度)的分类器,而不是一个有很多更高的召回率,但会让一些非常糟糕的视频出现在您的产品中(在这种情况下,您甚至可能需要添加人工管道来检查分类器的视频选择)。另一方面,假设你训练一个分类器来检测监控图像中的小偷:如果你的分类器只有 30% 的准确率,只要它有 99% 的召回率(当然,
不幸的是,你不能同时拥有它:提高精度会降低召回率,反之亦然。这称为精度/召回权衡。
精度/召回权衡
为了理解这种权衡,让我们看看SGDClassifier
它是如何做出分类决策的。对于每个实例,它根据决策函数计算分数。如果该分数大于阈值,则将实例分配给正类;否则它将其分配给负类。图 3-3显示了从左侧最低分数到右侧最高分数的几个数字。假设决策阈值位于中心箭头处(两个 5 之间):您会在该阈值的右侧找到 4 个真阳性(实际 5)和 1 个假阳性(实际上是 6)。因此,使用该阈值,精度为 80%(5 分中的 4 分)。但在 6 个实际 5 中,分类器只检测到 4 个,因此召回率为 67%(6 个中有 4 个)。如果提高阈值(将其移至右侧的箭头),则误报(6)变为真负,从而提高精度(在这种情况下高达 100%),但一个真正变为假负,将召回率降低到 50%。相反,降低阈值会增加召回率并降低精度。
图 3-3。在这种精度/召回率的权衡中,图像按其分类器分数进行排序,高于所选决策阈值的图像被认为是正数;阈值越高,召回率越低,但(通常)精度越高
Scikit-Learn 不允许您直接设置阈值,但它确实允许您访问它用于进行预测的决策分数。predict()
您可以调用它的方法,而不是调用分类器的方法,该decision_function()
方法返回每个实例的分数,然后使用您想要根据这些分数进行预测的任何阈值:
>>> y_scores = sgd_clf.decision_function([some_digit])
>>> y_scores
array([2412.53175101])
>>> threshold = 0
>>> y_some_digit_pred = (y_scores > threshold)
array([ True])
使用SGDClassifier
等于 0 的阈值,因此前面的代码返回与predict()
方法相同的结果(即True
)。让我们提高门槛:
>>> threshold = 8000
>>> y_some_digit_pred = (y_scores > threshold)
>>> y_some_digit_pred
array([False])
这证实了提高阈值会降低召回率。图像实际上代表了一个 5,当阈值为 0 时分类器检测到它,但当阈值增加到 8,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 将精度和召回率绘制为阈值的函数(图 3-4):
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")
[...] # highlight the threshold and add the legend, axis label, and grid
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.show()
图 3-4。精度和召回率与决策阈值
笔记
您可能想知道为什么精度曲线比图 3-4中的召回曲线更颠簸。原因是当您提高阈值时,精度有时可能会下降(尽管通常会上升)。要了解原因,请回顾图 3-3,并注意当您从中心阈值开始并将其向右移动一位时会发生什么:精度从 4/5 (80%) 下降到 3/4 (75%) )。另一方面,召回率只有在阈值增加时才会下降,这解释了为什么它的曲线看起来很平滑。
另一种选择准确率/召回率折衷的方法是直接绘制准确率与召回率的关系图,如图 3-5所示(突出显示与之前相同的阈值)。
图 3-5。精确率与召回率
你可以看到准确率在 80% 召回率左右开始急剧下降。您可能希望在该下降之前选择精确度/召回率权衡 - 例如,召回率约为 60%。但是,当然,选择取决于您的项目。
假设您决定以 90% 的精度为目标。您查看第一个图,发现您需要使用大约 8,000 的阈值。更准确地说,您可以搜索至少为您提供 90% 精度的最低阈值(np.argmax()
将为您提供最大值的第一个索引,在这种情况下表示第一个True
值):
threshold_90_precision = thresholds[np.argmax(precisions >= 0.90)] # ~7816
要进行预测(目前在训练集上)predict()
,您可以运行以下代码,而不是调用分类器的方法:
y_train_pred_90 = (y_scores >= threshold_90_precision)
让我们检查这些预测的准确率和召回率:
>>> precision_score(y_train_5, y_train_pred_90)
0.9000380083618396
>>> recall_score(y_train_5, y_train_pred_90)
0.4368197749492714
太好了,你有一个 90% 精度的分类器!如您所见,创建具有几乎任何您想要的精度的分类器是相当容易的:只需设置足够高的阈值,您就完成了。但是等等,没那么快。如果召回率太低,那么高精度分类器就不是很有用!
ROC曲线
受试者工作特征( ROC) 曲线是与二元分类器一起使用的另一个常用工具。它与精确率/召回率曲线非常相似,但不是绘制精确率与召回率,ROC 曲线绘制的是真阳性率(召回的另一个名称)与假阳性率(FPR) 的关系。这FPR 是被错误分类为正例的负例的比率。它等于 1——真阴性率(TNR),即正确分类为阴性的阴性实例的比率。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 绘制 FPR 和 TPR。此代码生成图 3-6中的图:
def plot_roc_curve(fpr, tpr, label=None):
plt.plot(fpr, tpr, linewidth=2, label=label)
plt.plot([0, 1], [0, 1], 'k--') # Dashed diagonal
[...] # Add axis labels and grid
plot_roc_curve(fpr, tpr)
plt.show()
再次进行权衡:召回率 (TPR) 越高,分类器产生的误报 (FPR) 就越多。虚线表示纯随机分类器的ROC曲线;一个好的分类器尽可能远离那条线(朝向左上角)。
图 3-6。这条 ROC 曲线绘制了所有可能阈值的假阳性率与真阳性率;红色圆圈突出显示所选比率(召回率为 43.68%)
一比较分类器的方法是测量曲线下面积(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.9611778893101814
小费
由于 ROC 曲线与准确率/召回率 (PR) 曲线非常相似,您可能想知道如何决定使用哪一种。根据经验,当阳性类别很少或您更关心假阳性而不是假阴性时,您应该更喜欢 PR 曲线。否则,使用 ROC 曲线。比如看之前的 ROC 曲线(以及 ROC AUC 分数),你可能会认为分类器真的很好。但这主要是因为与负数(非 5s)相比,正数(5s)很少。相比之下,PR 曲线清楚地表明分类器有改进的空间(曲线可能更接近右上角)。
现在让我们训练 aRandomForestClassifier
并将其 ROC 曲线和 ROC AUC 分数与SGDClassifier
. 首先,您需要获取训练集中每个实例的分数。但是由于它的工作方式(参见第 7 章),RandomForestClassifier
该类没有decision_function()
方法。相反,它有一个predict_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_curve()
函数需要标签和分数,但您可以给它类别概率而不是分数。让我们使用正类的概率作为分数:
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 曲线以查看它们的比较也很有用(图 3-7):
plt.plot(fpr, tpr, "b:", label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")
plt.legend(loc="lower right")
plt.show()
图 3-7。比较 ROC 曲线:随机森林分类器优于 SGD 分类器,因为它的 ROC 曲线更接近左上角,并且具有更大的 AUC
正如您在图 3-7中看到的,RandomForestClassifier
的 ROC 曲线看起来比SGDClassifier
' 的要好得多:它更靠近左上角。结果,它的 ROC AUC 得分也明显更好:
>>> roc_auc_score(y_train_5, y_scores_forest)
0.9983436731328145
尝试测量准确率和召回率:您应该会发现 99.0% 的准确率和 86.6% 的召回率。还不错!
您现在知道如何训练二元分类器,为您的任务选择合适的指标,使用交叉验证评估您的分类器,选择适合您需求的精度/召回率权衡,以及使用 ROC 曲线和 ROC AUC 分数来比较各种模型. 现在让我们尝试检测的不仅仅是 5s。
多类分类
然而二元分类器区分两个类,多类分类器(也称为多项分类器)可以区分两个以上的类。
一些算法(例如逻辑回归分类器、随机森林分类器和朴素贝叶斯分类器)能够本地处理多个类。其他(例如 SGD 分类器或支持向量机分类器)是严格的二元分类器。但是,您可以使用多种策略对多个二元分类器执行多类分类。
创建一个可以将数字图像分类为 10 个类别(从 0 到 9)的系统的一种方法是训练 10 个二进制分类器,每个数字一个(一个 0 检测器、一个 1 检测器、一个 2 检测器等)上)。然后,当您想要对图像进行分类时,您可以从该图像的每个分类器中获得决策分数,然后选择其分类器输出最高分数的类。这个称为one-versus-the-rest (OvR) 策略(也称为one-versus-all)。
另一种策略就是为每一对数字训练一个二元分类器:一个区分0和1,另一个区分0和2,另一个区分1和2,以此类推。这称为一对一(OvO) 策略。如果有N个类,则需要训练N × ( N – 1) / 2 个分类器。对于 MNIST 问题,这意味着要训练 45 个二元分类器!当你想对图像进行分类时,你必须通过所有 45 个分类器运行图像,看看哪个类赢得了最多的决斗。OvO 的主要优点是每个分类器只需要针对它必须区分的两个类的训练集部分进行训练。
一些算法(例如支持向量机分类器)随着训练集的大小而难以扩展。对于这些算法,首选 OvO,因为在小型训练集上训练多个分类器比在大型训练集上训练少数分类器更快。然而,对于大多数二元分类算法,OvR 是首选。
Scikit-Learn 会检测您何时尝试将二进制分类算法用于多类分类任务,并根据算法自动运行 OvR 或 OvO。让我们用支持向量机分类器(见第 5 章)来试试这个,使用sklearn.svm.SVC
类:
>>> from sklearn.svm import SVC
>>> svm_clf = SVC()
>>> svm_clf.fit(X_train, y_train) # y_train, not y_train_5
>>> svm_clf.predict([some_digit])
array([5], dtype=uint8)
那很简单!此代码SVC
使用从 0 到 9 ( ) 的原始目标类y_train
而不是 5-vs-the-rest 目标类 ( y_train_5
) 在训练集上训练。然后它做出预测(在这种情况下是正确的)。在后台,Scikit-Learn 实际上使用了 OvO 策略:它训练了 45 个二元分类器,获得了它们对图像的决策分数,并选择了赢得最多决斗的类别。
如果您调用该decision_function()
方法,您将看到它为每个实例返回 10 个分数(而不仅仅是 1 个)。这是每个班级的一个分数(它是赢得决斗的数量加上或减去打破平局的小调整,基于二进制分类器分数):
>>> some_digit_scores = svm_clf.decision_function([some_digit])
>>> some_digit_scores
array([[ 2.92492871, 7.02307409, 3.93648529, 0.90117363, 5.96945908,
9.5 , 1.90718593, 8.02755089, -0.13202708, 4.94216947]])
最高分确实是对应于 class 的分数5
:
>>> np.argmax(some_digit_scores)
5
>>> svm_clf.classes_
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)
>>> svm_clf.classes_[5]
5
警告
训练分类器时,它将目标类列表存储在其
classes_
属性中,按值排序。在这种情况下,classes_
数组中每个类的索引很方便地与类本身匹配(例如,索引 5 处的类恰好是 class5
),但通常你不会那么幸运。
如果你想强制 Scikit-Learn 使用一对一或一对一,你可以使用OneVsOneClassifier
orOneVsRestClassifier
类。只需创建一个实例并将分类器传递给它的构造函数(它甚至不必是二元分类器)。例如,此代码使用 OvR 策略创建一个多类分类器,基于SVC
:
>>> from sklearn.multiclass import OneVsRestClassifier
>>> ovr_clf = OneVsRestClassifier(SVC())
>>> ovr_clf.fit(X_train, y_train)
>>> ovr_clf.predict([some_digit])
array([5], dtype=uint8)
>>> len(ovr_clf.estimators_)
10
训练 anSGDClassifier
同样简单:
>>> sgd_clf.fit(X_train, y_train)
>>> sgd_clf.predict([some_digit])
array([5], dtype=uint8)
这次 Scikit-Learn 在底层使用了 OvR 策略:因为有 10 个类,所以它训练了 10 个二元分类器。该decision_function()
方法现在为每个类返回一个值。让我们看一下SGD分类器分配给每个类的分数:
>>> sgd_clf.decision_function([some_digit])
array([[-15955.22628, -38080.96296, -13326.66695, 573.52692, -17680.68466,
2412.53175, -25526.86498, -12290.15705, -7946.05205, -10631.35889]])
你可以看到分类器对其预测相当有信心:几乎所有的分数都是负数,而类5
的分数是 2412.5。该模型对 class 略有怀疑3
,得分为 573.5。现在你当然要评估这个分类器。像往常一样,您可以使用交叉验证。使用cross_val_score()
函数来评估SGDClassifier
的准确性:
>>> cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
array([0.8489802 , 0.87129356, 0.86988048])
它在所有测试折叠中都超过 84%。如果你使用随机分类器,你会得到 10% 的准确率,所以这不是一个糟糕的分数,但你仍然可以做得更好。简单地缩放输入(如第 2 章所述)将准确度提高到 89% 以上:
>>> 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.89707059, 0.8960948 , 0.90693604])
错误分析
如果这是一个真实的项目,您现在可以按照机器学习项目清单中的步骤进行操作(请参阅附录 B)。您将探索数据准备选项,尝试多个模型(将最佳模型列入候选名单并使用 微调它们的超参数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([[5578, 0, 22, 7, 8, 45, 35, 5, 222, 1],
[ 0, 6410, 35, 26, 4, 44, 4, 8, 198, 13],
[ 28, 27, 5232, 100, 74, 27, 68, 37, 354, 11],
[ 23, 18, 115, 5254, 2, 209, 26, 38, 373, 73],
[ 11, 14, 45, 12, 5219, 11, 33, 26, 299, 172],
[ 26, 16, 31, 173, 54, 4484, 76, 14, 482, 65],
[ 31, 17, 45, 2, 42, 98, 5556, 3, 123, 1],
[ 20, 10, 53, 27, 50, 13, 3, 5696, 173, 220],
[ 17, 64, 47, 91, 3, 125, 24, 11, 5421, 48],
[ 24, 18, 29, 67, 116, 39, 1, 174, 329, 5152]])
这是很多数字。使用 Matplotlib 的matshow()
函数查看混淆矩阵的图像表示通常更方便:
plt.matshow(conf_mx, cmap=plt.cm.gray)
plt.show()
这个混淆矩阵看起来不错,因为大多数图像都在主对角线上,这意味着它们被正确分类。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()
您可以清楚地看到分类器所犯的错误种类。请记住,行代表实际类别,而列代表预测类别。第 8 类的列非常亮,它告诉您许多图像被错误分类为 8s。但是,第 8 类的行并没有那么糟糕,它告诉您实际的 8s 通常被正确归类为 8s。如您所见,混淆矩阵不一定是对称的。您还可以看到 3s 和 5s 经常混淆(双向)。
分析混淆矩阵通常可以让您深入了解改进分类器的方法。看这个情节,看来你的努力应该花在减少假8上。例如,您可以尝试为看起来像 8(但不是)的数字收集更多训练数据,以便分类器可以学习将它们与真正的 8 区分开来。或者你可以设计有助于分类器的新特性——例如,编写一个算法来计算闭环的数量(例如,8 有两个,6 有一个,5 没有)。或者,您可以对图像进行预处理(例如,使用 Scikit-Image、Pillow 或 OpenCV)以使某些模式(例如闭环)更加突出。
分析单个错误也可以是深入了解分类器正在做什么以及它失败的原因的好方法,但它更加困难和耗时。例如,让我们绘制 3s 和 5s 的例子(该plot_digits()
函数只是使用了 Matplotlib 的imshow()
函数;详见本章的 Jupyter notebook):
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()
左侧的两个 5×5 块显示分类为 3 的数字,右侧的两个 5×5 块显示分类为 5 的图像。分类器出错的一些数字(例如,在左下角和右上角的块中)写得非常糟糕,以至于即使是人类也很难对它们进行分类(例如,第一行和第二列中的 5 确实看起来就像写得不好 3)。然而,大多数错误分类的图像对我们来说似乎是明显的错误,很难理解为什么分类器会犯错误。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]])
它做对了!数字 5 确实不大(False
)和奇数(True
)。
评估多标签分类器的方法有很多,选择正确的指标真的取决于你的项目。一种方法是测量每个单独标签(或前面讨论的任何其他二元分类器指标)的 F 1分数,然后简单地计算平均分数。此代码计算所有标签的平均 F 1分数:
>>> 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
这假设所有标签都同等重要,然而,情况可能并非如此。特别是,如果 Alice 的照片比 Bob 或 Charlie 的照片多,您可能希望对分类器在 Alice 的照片上的得分给予更大的权重。一个简单的选择是给每个标签一个等于其支持的权重(即,具有该目标标签的实例数)。至这样做,只需average="weighted"
在前面的代码中设置。4
多输出分类
这我们将在这里讨论的最后一种分类任务称为多输出-多类分类(或简称为多输出分类)。它只是多标签分类的一种概括,其中每个标签都可以是多类的(即,它可以有两个以上的可能值)。
为了说明这一点,让我们构建一个从图像中去除噪声的系统。它将一个嘈杂的数字图像作为输入,它会(希望)输出一个干净的数字图像,表示为像素强度数组,就像 MNIST 图像一样。请注意,分类器的输出是多标签(每个像素一个标签),每个标签可以有多个值(像素强度范围从 0 到 255)。因此,它是多输出分类系统的一个示例。
笔记
分类和回归之间的界限有时是模糊的,例如在这个例子中。可以说,预测像素强度更类似于回归而不是分类。此外,多输出系统不仅限于分类任务;您甚至可以拥有一个系统,为每个实例输出多个标签,包括类标签和值标签。
让我们首先通过获取 MNIST 图像并将噪声添加到它们的像素强度来创建训练和测试集使用 NumPy 的randint()
功能。目标图像将是原始图像:
noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise
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
让我们看一下测试集中的图像(是的,我们正在窥探测试数据,所以你现在应该皱着眉头):
左边是有噪声的输入图像,右边是干净的目标图像。现在让我们训练分类器并让它清理这个图像:
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)
看起来离目标足够近了!我们的分类之旅到此结束。您现在应该知道如何为分类任务选择好的指标、选择适当的精度/召回率权衡、比较分类器,以及更一般地为各种任务构建好的分类系统。
练习
-
尝试为 MNIST 数据集构建一个分类器,在测试集上达到 97% 以上的准确率。提示:
KNeighborsClassifier
这项任务的效果很好;您只需要找到好的超参数值(尝试对weights
和n_neighbors
超参数进行网格搜索)。 -
编写一个函数,可以将 MNIST 图像在任何方向(左、右、上或下)移动一个像素。5然后,对于训练集中的每个图像,创建四个移位副本(每个方向一个)并将它们添加到训练集中。最后,在这个扩展的训练集上训练你最好的模型,并在测试集上测量它的准确性。您应该观察到您的模型现在表现得更好!这种人为地增加训练集的技术称为数据增强或训练集扩展。
-
处理泰坦尼克号数据集。Kaggle是一个很好的起点。
-
构建垃圾邮件分类器(更具挑战性的练习):
-
从Apache SpamAssassin 的公共数据集下载垃圾邮件和火腿示例。
-
解压缩数据集并熟悉数据格式。
-
将数据集拆分为训练集和测试集。
-
编写数据准备管道,将每封电子邮件转换为特征向量。您的准备管道应将电子邮件转换为(稀疏)向量,指示每个可能的单词的存在或不存在。例如,如果所有电子邮件只包含四个词,“Hello”、“how”、“are”、“you”,那么电子邮件“Hello you Hello Hello you”将被转换为向量 [1, 0, 0 , 1](表示[“Hello”存在,“how”不存在,“are”不存在,“you”存在]),或者 [3, 0, 0, 2] 如果您更喜欢计算每个单词的出现次数。
您可能希望在准备管道中添加超参数,以控制是否去除电子邮件标题、将每封电子邮件转换为小写、删除标点符号、将所有 URL 替换为“URL”、将所有数字替换为“NUMBER”,甚至执行词干提取(即,修剪词尾;有可用的 Python 库来执行此操作)。
最后,尝试几个分类器,看看您是否可以构建一个出色的垃圾邮件分类器,同时具有高召回率和高精度。
-
这些练习的解决方案可以在GitHub - ageron/handson-ml2: A series of Jupyter notebooks that walk you through the fundamentals of Machine Learning and Deep Learning in Python using Scikit-Learn, Keras and TensorFlow 2.上的 Jupyter 笔记本中找到。
1默认情况下,Scikit-Learn 将下载的数据集缓存在名为$HOME/scikit_learn_data的目录中。
2在某些情况下,洗牌可能不是一个好主意——例如,如果您正在处理时间序列数据(例如股票市场价格或天气状况)。我们将在接下来的章节中探讨这一点。
3但请记住,我们的大脑是一个奇妙的模式识别系统,在任何信息到达我们的意识之前,我们的视觉系统会进行大量复杂的预处理,因此感觉简单并不意味着它确实如此。
4Scikit-Learn 提供了一些其他的平均选项和多标签分类器指标;有关更多详细信息,请参阅文档。
5您可以使用模块中的shift()
功能scipy.ndimage.interpolation
。例如,shift(image, [2, 1], cval=0)
将图像向下移动两个像素,向右移动一个像素。