小陈统计

V1

2022/11/29阅读:39主题:默认主题

机器学习实战第三章第二部分 :手写数字识别(多分类任务)

3.4 使用MNIST 数据集,进行手写数字多分类

以下是结果

记一下开始时间

import datetime
starttime = datetime.datetime.now()
  1. 准备工作,同上一部前一样,导入数据,
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
X, y = mnist["data"], mnist["target"]

import matplotlib as mpl
import matplotlib.pyplot as plt

字符数值化

import numpy as np
y = y.astype(np.uint8)

训练集、测试集划分

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

有一些算法(如随机森林分类器或朴素贝叶斯分类器)可以直接处理多个类。 也有一些严格的二元分类器(如支持向量机分类器或线性分类器) 有多种策略可以让你用几个二元分类器实现多类分类的目的。

要创建一个系统将数字图片分为10类(从0到9),一种方法是训练10个二元分类器,每个数字一个(0-检测器、1-检测器、2-检测器,以此类推)。然后,当你需要对一张图片进行检测分类时,获取每个分类器的决策分数,哪个分类器给分最高,就将其分为哪个类。这称为一对剩余(OvR)策略,也称为一对多(one-versus-all)。

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

Scikit-Learn可以检测到你尝试使用二元分类算法进行多类分类任务,它会根据情况自动运行OvR或者OvO:

下面开始分类器的训练, 还是先以第一个数字5开始

from sklearn.svm import SVC
some_digit = X[0]

svm_clf = SVC()
svm_clf.fit(X_train, y_train)

查看第一个数字的预测结果是多少,结果是5

print("svm结果:")
print(svm_clf.predict([some_digit]))

查看预测为0-9的分数是多少,可以看到预测为5的分数最高,超过9分

some_digit_scores = svm_clf.decision_function([some_digit])
print("预测为各类别分数:")
print(some_digit_scores)

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

例如,下面这段代码使用OvR策略,基于SVC创建了一个多类分类器

from sklearn.multiclass import OneVsRestClassifier
ovr_clf = OneVsRestClassifier(SVC(gamma="auto", random_state=42))
ovr_clf.fit(X_train[:1000], y_train[:1000])
print("OVR结果:")
print(list(ovr_clf.predict([some_digit])))

训练SGDClassifier或者RandomForestClassifier同样简单:

这次Scikit-Learn不必运行OvR或者OvO了,因为SGD分类器直接就可以将实例分为多个类

from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(max_iter=1000, tol=1e-3, random_state=42)
sgd_clf.fit(X_train, y_train)
print("SGD结果:")
print(sgd_clf.predict([some_digit]))

SGD把这个数预测成3了,确实。5和3有一点像。

调用decision_function()可以获得分类器将每个实例分类为每个类的概率列表:让我们看一下SGD分类器分配到的每个类:

print("各类别概率列表:")
print(sgd_clf.decision_function([some_digit]))

确实,上述结果表示,在各类别概率中,预测为3的得分最高,是1823.7315

使用cross_val_score()函数来评估SGDClassifier的准确性:

from sklearn.model_selection import cross_val_score
print("交叉验证SGD准确性:")
print(cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy"))

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

例如,将输入进行简单缩放(如第2章所述)可以将准确率提到89%以上:

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
print("标准化之后的结果:")
print(cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy"))

确实,从结果可以看出,经过标准化之后,得分稍有长进

3.5 误差分析

当然,如果这是一个真正的项目,你将遵循机器学习项目清单中的步骤(见附录B):探索数据准备的选项,尝试多个模型,列出最佳模型并用GridSearchCV对其超参数进行微调,尽可能自动化,等等。正如你在之前的章节里尝试的那些。

在这里,假设你已经找到了一个有潜力的模型,现在你希望找到一些方法对其进一步改进。方法之一就是分析其错误类型。

首先看看混淆矩阵。就像之前做的,使用cross_val_predict()函数进行预测,然后调用confusion_matrix()函数:

之前的5和非5是一个二分类的分类器,其混淆矩阵为2x2矩阵,现在为10类别分类器,混淆矩阵也变成了10x10分类器

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import 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)
print("混淆矩阵:")
print(conf_mx)

数字有点多,使用Matplotlib的matshow()函数来查看混淆矩阵的图像表示通常更加方便(见下图):

def plot_confusion_matrix(matrix):
    """If you prefer color and a colorbar"""
    fig = plt.figure(figsize=(8,8))
    ax = fig.add_subplot(111)
    cax = ax.matshow(matrix)
    fig.colorbar(cax)


plt.matshow(conf_mx, cmap=plt.cm.gray)
plt.show()

混淆矩阵看起来很不错,因为大多数图片都在主对角线上,这说明它们被正确分类。数字5看起来比其他数字稍稍暗一些,这可能意味着数据集中数字5的图片较少,也可能是分类器在数字5上的执行效果不如在其他数字上好。实际上,你可能会验证这两者都属实,让我们把焦点放在错误上。首先,你需要将混淆矩阵中的每个值除以相应类中的图片数量,这样你比较的就是错误率而不是错误的绝对值(后者对图片数量较多的类不公平):

import numpy as np
row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums

用0填充对角线,只保留错误,重新绘制结果(见下图)

np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()

现在可以清晰地看到分类器产生的错误种类了。记住,每行代表实际类,而每列表示预测类。第8列看起来非常亮,说明有许多图片被错误地分类为数字8了。然而,第8行不那么差,告诉你实际上数字8被正确分类为数字8,注意,错误不是完全对称的,比如,数字3和数字5经常被混淆(在两个方向上)

3.6 多标签分类

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

这种输出多个二元标签的分类系统称为多标签分类系统。

为了阐释清楚,这里不讨论面部识别,让我们来看一个更为简单的示例:

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实例(它支持多标签分类,不是所有的分类器都支持),然后使用多个目标数组对它进行训练。现在用它做一个预测,注意它输出两个标签:结果是正确的!数字5确实不大(False),为奇数(True)。

print("knn结果:")
print(knn_clf.predict([some_digit]))

评估多标签分类器的方法很多,如何选择正确的度量指标取决于你的项目。比如方法之一是测量每个标签的F1分数,然后简单地计算平均分数。

下面这段代码计算所有标签的平均F1分数:

from sklearn.metrics import f1_score
y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
mf1 = f1_score(y_multilabel, y_train_knn_pred, average="macro")
print("F1均分:", mf1)

最后记一下耗时

endtime = datetime.datetime.now()
print("总耗时:", endtime-starttime)

** 完整代码**

# 3.4 使用MNIST 数据集,进行手写数字多分类
# 记一下时
import datetime
starttime = datetime.datetime.now()

# 准备工作,同上一部前一样,导入数据,
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1, as_frame=False)
X, y = mnist["data"], mnist["target"]

import matplotlib as mpl
import matplotlib.pyplot as plt

# 字符数值化
import numpy as np
y = y.astype(np.uint8)

# 训练集、测试集划分
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]


# 有一些算法(如随机森林分类器或朴素贝叶斯分类器)可以直接处理多个类。
# 也有一些严格的二元分类器(如支持向量机分类器或线性分类器)
# 有多种策略可以让你用几个二元分类器实现多类分类的目的。

# 要创建一个系统将数字图片分为10类(从0到9),一种方法是训练10个二元分类器,每个数字一个(0-检测器、1-检测器、2-检测器,以此类推)。
# 然后,当你需要对一张图片进行检测分类时,获取每个分类器的决策分数,哪个分类器给分最高,就将其分为哪个类。
# 这称为一对剩余(OvR)策略,也称为一对多(one-versus-all)。

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


# Scikit-Learn可以检测到你尝试使用二元分类算法进行多类分类任务,它会根据情况自动运行OvR或者OvO
# 下面开始分类器的训练, 还是先以第一个数字5开始
from sklearn.svm import SVC
some_digit = X[0]

svm_clf = SVC()
svm_clf.fit(X_train, y_train)

# 查看第一个数字的预测结果是多少,结果是5
print("svm结果:")
print(svm_clf.predict([some_digit]))

# 查看预测为0-9的分数是多少,可以看到预测为5的分数最高,超过9分
some_digit_scores = svm_clf.decision_function([some_digit])
print("预测为各类别分数:")
print(some_digit_scores)

# 如果想要强制Scikit-Learn使用一对一或者一对剩余策略,可以使用OneVsOneClassifier或OneVsRestClassifier类。
# 只需要创建一个实例,然后将分类器传给其构造函数(它甚至不必是二元分类器)。
# 例如,下面这段代码使用OvR策略,基于SVC创建了一个多类分类器

from sklearn.multiclass import OneVsRestClassifier
ovr_clf = OneVsRestClassifier(SVC(gamma="auto", random_state=42))
ovr_clf.fit(X_train[:1000], y_train[:1000])
print("OVR结果:")
print(list(ovr_clf.predict([some_digit])))

# 训练SGDClassifier或者RandomForestClassifier同样简单:
# 这次Scikit-Learn不必运行OvR或者OvO了,因为SGD分类器直接就可以将实例分为多个类
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(max_iter=1000, tol=1e-3, random_state=42)
sgd_clf.fit(X_train, y_train)
print("SGD结果:")
print(sgd_clf.predict([some_digit]))
# SGD把这个数预测成3了,确实。5和3有一点像

# 调用decision_function()可以获得分类器将每个实例分类为每个类的概率列表:让我们看一下SGD分类器分配到的每个类
print("各类别概率列表:")
print(sgd_clf.decision_function([some_digit]))
# 确实,在各类别概率中,预测为3的得分最高,是1823.7315

# 使用cross_val_score()函数来评估SGDClassifier的准确性:
from sklearn.model_selection import cross_val_score
print("交叉验证SGD准确性:")
print(cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy"))

# 结果显示在所有的测试折叠上都超过了84%。如果是一个纯随机分类器,准确率大概是10%,所以这个结果不是太糟,
# 但是依然有提升的空间。例如,将输入进行简单缩放(如第2章所述)可以将准确率提到89%以上:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
print("标准化之后的结果:")
print(cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy"))
# 确实,从结果可以看出,经过标准化之后,得分稍有长进

# 3.5 误差分析
# 当然,如果这是一个真正的项目,你将遵循机器学习项目清单中的步骤(见附录B):探索数据准备的选项,尝试多个模型,
# 列出最佳模型并用GridSearchCV对其超参数进行微调,尽可能自动化,等等。正如你在之前的章节里尝试的那些。
# 在这里,假设你已经找到了一个有潜力的模型,现在你希望找到一些方法对其进一步改进。方法之一就是分析其错误类型。

# 首先看看混淆矩阵。就像之前做的,使用cross_val_predict()函数进行预测,然后调用confusion_matrix()函数:
# 之前的5和非5是一个二分类的分类器,其混淆矩阵为2x2矩阵,现在为10类别分类器,混淆矩阵也变成了10x10分类器
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import 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)
print("混淆矩阵:")
print(conf_mx)

# 数字有点多,使用Matplotlib的matshow()函数来查看混淆矩阵的图像表示通常更加方便(见下图):
def plot_confusion_matrix(matrix):
    """If you prefer color and a colorbar"""
    fig = plt.figure(figsize=(8,8))
    ax = fig.add_subplot(111)
    cax = ax.matshow(matrix)
    fig.colorbar(cax)


plt.matshow(conf_mx, cmap=plt.cm.gray)
plt.show()

# 混淆矩阵看起来很不错,因为大多数图片都在主对角线上,这说明它们被正确分类。数字5看起来比其他数字稍稍暗一些,
# 这可能意味着数据集中数字5的图片较少,也可能是分类器在数字5上的执行效果不如在其他数字上好。实际上,你可能会验证这两者都属实

# 让我们把焦点放在错误上。首先,你需要将混淆矩阵中的每个值除以相应类中的图片数量,
# 这样你比较的就是错误率而不是错误的绝对值(后者对图片数量较多的类不公平):
import numpy as np
row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums

# 用0填充对角线,只保留错误,重新绘制结果(见下图)
np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()

# 现在可以清晰地看到分类器产生的错误种类了。记住,每行代表实际类,而每列表示预测类。
# 第8列看起来非常亮,说明有许多图片被错误地分类为数字8了。然而,第8行不那么差,告诉你实际上数字8被正确分类为数字8
# 注意,错误不是完全对称的,比如,数字3和数字5经常被混淆(在两个方向上)

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

# 为了阐释清楚,这里不讨论面部识别,让我们来看一个更为简单的示例:
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实例(它支持多标签分类,不是所有的分类器都支持),然后使用多个目标数组对它进行训练。
# 现在用它做一个预测,注意它输出两个标签:结果是正确的!数字5确实不大(False),为奇数(True)。
print("knn结果:")
print(knn_clf.predict([some_digit]))

# 评估多标签分类器的方法很多,如何选择正确的度量指标取决于你的项目。比如方法之一是测量每个标签的F1分数,然后简单地计算平均分数。
# 下面这段代码计算所有标签的平均F1分数:
from sklearn.metrics import f1_score
y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
mf1 = f1_score(y_multilabel, y_train_knn_pred, average="macro")
print("F1均分:", mf1)

# 记一下时
endtime = datetime.datetime.now()
print("总耗时:", endtime-starttime)

分类:

后端

标签:

后端

作者介绍

小陈统计
V1