机器学习实战——分类

3.1 MNIST数据集

本章使用MNIST数据集(一组美国高中生和人口调查局员工有些的70000个数字的图片)。获取该数据集的代码如下:

from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1)
print(mnist.keys())

X,y = mnist["data"], mnist["target"]
print(X.shape)
print(y.shape)

Sklearn加载的数据集通常具有类似的字典结构,包括:

  • DESCR键,用于描述数据集
  • data键,包含一个数组,每个实例为一行 ,每个特征为一列
  • target键,包含一个带标记的数组

该数据集一共70000张图片,每张图片一共784个特赠。因此图片是2828像素。每个特征代表了一个像素点的强度。从0(白色)到255(黑色)。如果要绘制其中一个数字,只需要抓取一个实例的特征向量,将其重新形成一个2828的数组,然后使用Matplotlib的imshow()函数将其显示出来。

【这里书上源代码的X[0]会报错,使用X.to_numpy()[0]即可】

import matplotlib.pyplot as plt

some_digit = X.to_numpy()[0]
some_digit_image = some_digit.reshape(28,28)
plt.imshow(some_digit_image,cmap="binary")
plt.axis("off")
plt.show()

接下来在研究这些数据之前,需要分割训练集和测试集。事实上,MNIST数据集已经以6:1的比例分好了:

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

3.2 训练二元分类器

先简化问题,只尝试识别一个数字,比如5:该分类器只能区分两个类别(5和非5)。先为此分类任务创建目标向量。

X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
y_train_5 = (y_train==5) # 数值为5设置为真,数值为其他设置为假
y_test_5 = (y_test==5)

接着挑选一个分类器,一个好的初始选择是随机梯度下降(SGD)分类器。使用Sklearn的SGDClassifer类即可。该分类器的优势是能够有效处理非常大型的数据集。这是因为SGD独立处理训练实例,一次一个。

from sklearn.linear_model import SGDClassifier

sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)
print(sgd_clf.predict([X.to_numpy()[0]]))

3.3 性能测量

下面使用交叉验证来评估模式的测量准确率。使用Sklearn实现和自行实现:

# 使用Sklearn实现
from sklearn.model_selection import cross_val_score
scores = cross_val_score(sgd_clf, X_train, y_train_5, scoring="accuracy", cv=3)
print(scores)

# 自定实现交叉验证
from sklearn.model_selection import StratifiedKFold
from sklearn.base import clone

skfolds = StratifiedKFold(n_splits=3, random_state=42, shuffle=True)

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

得到的结果基本都在95%左右,但这不代表我们的分类器是好的,是为非5的图片占约90%。因此即使是全判断为非5正确率也在90%左右。这说明准确率通常无法称为分类器的首要性能指标,特别是当你处理有偏数据集的时候。

混淆矩阵

评估分类器性能的更好方法是混淆矩阵,其总体思路就是统计A类别实例被分为B类别的次数。例如想知道分类器将数字4和数字5混淆多少次,只需要查看矩阵第5行第3列。

要计算混淆矩阵,需要先有一组预测才能将其与实际目标进行比较。可以使用cross_val_predict()函数。与cross_val_score()函数一样,cross_val_predict()也执行K折交叉验证,但返回的不是评估分数,而是每个折叠的预测。)

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix

y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
print(confusion_matrix(y_train_5, y_train_pred))

>>>输出如下
[[53892   687]
 [ 1891  3530]]

混淆矩阵中的行表示实际类别,列表示预测类别。本例中第一行为负类(判别为非5):53892是被正确分类到“非5”的数量,称为真负类TN;687是被分为类到“5”的数量,称为假正类FP;第二行为正类(判别为5):1891是被错误的分为“非5”的数量,称为假负类FN;3530是被正确的分类到“5”的数量,称为真正类TP

【此处存在一个问题,sklearn的混淆矩阵的排列与大多数教材中混淆矩阵的排列不同,如下图】
在这里插入图片描述
一个完美的分类器只有真正类和真负类,所以其混淆矩阵只会在其对角线上有非零值。正类预测的准确率也成为分类器的精度公式如下:
在这里插入图片描述
精度常常与另一个指标以前使用,即召回率:它是分类器正确监测到正类实例的比例。
在这里插入图片描述
对此,Sklearn也提供了多种计算分类器指标的函数,包括精度和召回率:

from sklearn.metrics import precision_score, recall_score
print(precision_score(y_train_5, y_train_pred))
print(recall_score(y_train_5, y_train_pred))

我们可以将精度和召回率组合成一个单一的指标,称为F1分数。当你需要一个简单的方法比较两种分类器时,是一个非常不错的指标。F1分数是精度和召回率的谐波平均值。正常的平均值平等对待所有的值,谐波平均值会给低值更高的权重。因此只有精度和召回率都很高的时候,分类器才有很高的F1分数。
在这里插入图片描述

通常情况下,F1分数越高不一定能更符合期望。有时候更看重精度,有时候更看重召回率。宁愿错杀,也不愿放过。但鱼与熊掌不可兼得,不能同时增加精度又减少召回率。需要进行精度/召回率之间权衡。

SGDClassifier的分类标准是基于决策函数计算出一个分值,如果该值大于阈值,则将其分为正类,反之为负类。Sklearn不允许直接设置阈值,但是允许访问它用于预测的决策分数。

y_scores = sgd_clf.decision_function([some_digit])
print(y_scores)

threshold = 0 # 自己设定一个阈值
y_some_digit_pred = (y_scores>threshold)
print(y_some_digit_pred)

threshold = 8000 # 设定更高的阈值
y_some_digit_pred = (y_scores>threshold)
print(y_some_digit_pred)

由上可见,提高阈值确实可以降低召回率。如何决定使用什么阈值就要用到cross_val_predict()函数来获取训练集中所有实例的分数,但这次需要返回的是决策分数而不是预测结果:

from sklearn.model_selection import cross_val_predict
from sklearn.metrics import precision_recall_curve

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)

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.legend(loc="center right", fontsize=16)
    plt.xlabel("Threshold", fontsize=16)
    plt.grid(True)
    plt.axis([-50000, 50000, 0, 1])

plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.show()

在这里插入图片描述
从绘制结果来看,精度曲线之所以崎岖是因为在提高阈值的时候,精度也有可能下降(尽管总体趋势是上升的)。另一方面,当阈值上升时,召回率只会下降,所以召回率曲线相对平滑。

ROC曲线

ROC曲线(受试者工作特征曲线)经常与二元分类器一起使用。它与精度/召回率曲线相似,但是绘制的是真正率(召回率的另一种名称)和假正率FPR。FPR是错误被分类为正类的负类实例占比,等于1-真负类率TNR,后者是被正确分类为负类的比率,也成为特异度。因此ROC曲线是召回率和1-特异度之间的关系。

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

from sklearn.metrics import roc_curve

FPR, TPR, thresholds = roc_curve(y_train_5, y_scores)

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 (Fall-Out)', fontsize=16)
    plt.ylabel('True Positive Rate (Recall)', fontsize=16)
    plt.grid(True)

plot_roc_curve(FPR, TPR)
plt.show()

在这里插入图片描述
这里又面临一个折中权衡:召回率越高,分类器产生的假正类越多。虚线代表纯随机分类器的ROC曲线,一个优秀的分类器应该离这条线越远越好(向左上角)。

一种比较分类器的方法是测量曲线下面积(AUC)。完美的分类器的AUC等于1,随机分类器的AUC等于0.5。Sklearn提供计算AUC的函数:

from sklearn.metrics import roc_auc_score
print(roc_auc_score(y_train_5, y_scores))

由于ROC权限与精度召回(PR)曲线非常相似,因此当正类非常少见或者你更关注假正类而不是假负类的时候,选择PR曲线,反之是ROC曲线。

3.4 多元分类器

多元分类器可以区分两个以上的类,有一些算法(随机森林分类器和朴素贝叶斯分类器)可以直接处理多个类,也有一些只能处理两个的二元分类器(支持向量机分类器和线性分类器)。但是有多种策略可以让你用几个二元分类器实现多类分类的目的。

如果要创建一个系统将数字图片从0到9分为10类,可以训练10个二元分类器(0检测器、1检测器、2检测器…)。然后当需要对一张图片检测时,获取每个分类器的得分,哪个分类器得分最高,就将其分为哪个类。称为一对多策略。(OvR)

还有一种方法是每一对数字训练一个二元分类器:一个用于区分0和1,一个区分0和2,一个区分1和2,以此类推。称为一对一策略。如果是10个数字需要训练45个分类器,但优点在于每个分类器只需要用到部分的训练集对其必须区分的两个类进行训练(OvO)。

Sklearn可以检测到你尝试使用二类分类算法来进行多类分类任务,它会根据情况自动运行OvR和OvO,下面以SVM分类器尝试:

from sklearn.svm import SVC
svm_clf = SVC()
svm_clf.fit(X_train, y_train)
svm_clf.predict([some_digit])
some_digit_scores = svm_clf.decision_function([some_digit])
print(some_digit_scores)

上面的代码Sklearn实际采用了一对一的策略,输出了10个分数,每个类对应一个分数。如果想强制Sklearn使用一对一或一对多的策略,可以使用OneVsOneClassfier或OneVsRestClassifier类。只需要创建一个实例然后将分类器传给其构造函数,甚至不必为二元分类器。例如下面的代码使用OvR策略基于SVC创建了一个多元分类器:

from sklearn.svm import SVC
from sklearn.multiclass import OneVsRestClassifier
ovr_clf = OneVsRestClassifier(SVC())
ovr_clf.fit(X_train, y_train)
ovr_clf.predict([some_digit])

3.5 误差分析

输出混淆矩阵

除了使用cross_val_predict函数输出预测的数值,还可以使用Matplotlib的matshow()函数查看混淆矩阵的图像来直观的观察。

from sklearn.preprocessing import StandardScaler
from sklearn.metrics import confusion_matrix

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

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

在这里插入图片描述
可以看出大多数图片都在主对角线上,这说明他们被正确分类。数字5看起来比其他数字稍微暗一点,说明数据集中数字5的图片较少,也可能是分类器在数字5上的执行效果不如在其他数字上好。

现在需要将混淆矩阵中的每个值都除以相应类中的图片数量,这样比较的就是错误率而不是绝对值(否则对图片数量较多的类不公平)

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(conf_mx, cmap=plt.cm.gray) # 重新绘制结果
plt.show()

在这里插入图片描述
这样可以清晰的看到分类器产生的错误种类了。每行代表实际类,而每列表示预测类。第八列非常亮,说明许多图片被错误的分类为8了。分析混淆矩阵可以深入了解如何改进分类器。通过上图来看,可以将精力花在改进数字8的分类错误上。例如:收集更多看起来像8的训练数据、开发馨的特征改进分类器。

3.6 多标签分类

到目前为止,每个实例都只会被分在一个类里。而在某些情况下,希望分类器为每个实例输出多个类。例如人脸识别的分类器识别出多个人,这时就可以给每个人附上标签。出现的人标记为1,没有出现标记为0。这种输出多个二元标签的分类器称为多标签分类系统。

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)

print(knn_clf.predict([some_digit]))

代码创建包含两个数字图片的目标标签:是否为大数、是否为奇数。然后创建支持多标签分类的K近邻分类器,然后使用多个目标数组对其训练,然后对其作预测。

评估多标签分类的器的方法有很多,比如方法之一就是测量每个标签的F1分数,然后简单计算平均分数。

from sklearn.metrics import f1_score
y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
f1_scores = f1_score(y_multilabel, y_train_knn_pred, average="macro")
print(f1_scores)

这里假设所有的标签等同重要,但是如果要分配权重。一个简单的方法就是给每个标签设置一个等于其自身支持的权重(也就是具有该目标标签的实例的数量)。为此,只需要在上面的代码中设置average=”weighted”即可。

3.7 多输出分类

多输出分类是多标签分类的泛化,其标签也可以是多类的。比如构建一个去除噪声的系统,输入一章有噪声的图片,希望输出一张干净的图片,以像素强度的数组作为呈现方式。这里分类器的输出是多个标签(一个像素点一个标签),每个标签可以有多个值(像素强度0~255)。因此这是一个多输出分类的示例。

先从创建训练集和测试集开始,使用randint函数添加噪声,目标是还原图片。

noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train+noise
noise = np.random.randint(0, 1000, (len(X_test)), 784)
X_test_mod = X_test+noise
y_train_mod = X_train
y_test_mod = X_test

然后进行清洗降噪:

def plot_digit(data):
    image = data.reshape(28, 28)
    plt.imshow(image, cmap = mpl.cm.binary,
               interpolation="nearest")
    plt.axis("off")

some_index = 0
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(y_test_mod[some_index]) # 原图片
plot_digit(X_test_mod[some_index]) # 噪声图片
plot_digit(clean_digit) # 降噪图片
plt.show()

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值