分类器MNIST交叉验证准确率、混淆矩阵、精度和召回率(PR曲线)、ROC曲线、多类别分类器、多标签分类、多输出分类

本博客是在Jupyter Notebook下进行的编译。

MNIST

MNIST数据集,这是一组由美国高中生和人口调查局员工手写的70000个数字的图片。每张图像都用其代表的数字标记。这个数据集被广为使用,因此也被称作是机器学习领域的“Hello World”。

首先,我们使用sklearn的函数来获取MNIST数据集,代码如下:

# 使用sklearn的函数来获取MNIST数据集
from sklearn.datasets import fetch_openml
import numpy as np
import os
# to make this notebook's output stable across runs
np.random.seed(42)
# To plot pretty figures
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)
# 为了显示中文
mpl.rcParams['font.sans-serif'] = [u'SimHei']
mpl.rcParams['axes.unicode_minus'] = False
# 耗时巨大
def sort_by_target(mnist):
    reorder_train=np.array(sorted([(target,i) for i, target in enumerate(mnist.target[:60000])]))[:,1]
    reorder_test=np.array(sorted([(target,i) for i, target in enumerate(mnist.target[60000:])]))[:,1]
    mnist.data[:60000]=mnist.data[reorder_train]
    mnist.target[:60000]=mnist.target[reorder_train]
    mnist.data[60000:]=mnist.data[reorder_test+60000]
    mnist.target[60000:]=mnist.target[reorder_test+60000]
    
mnist=fetch_openml('mnist_784',version=1,cache=True) #获取数据
mnist.target=mnist.target.astype(np.int8)
sort_by_target(mnist)

获取数据这部分,运行起来可能会稍微有点慢,可以加个定时器,看看运行的时间。
代码如下:

import time
y1 = time.time()
mnist=fetch_openml('mnist_784',version=1,cache=True)
mnist.target=mnist.target.astype(np.int8)
sort_by_target(mnist)
y2 = time.time()
display(y2-y1)

运行结果如下:
我的电脑性能一般,时间是33秒。如果电脑性能较好的会比较快一些。
在这里插入图片描述

添加下面这句代码,可对数据集进行排序。

mnist["data"], mnist["target"]

运行结果如下:
在这里插入图片描述

使用下面这三种方法,可以查看维度。
方法一:

mnist.data.shape

方法二:

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

方法三:

y.shape
28*28

运行结果如下:
可以看到结果都一样的,是70000张图片,784维
在这里插入图片描述
展示单张图片,代码如下:

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

运行结果如下:
在这里插入图片描述
展示十行十列的图片,代码如下:

def plot_digits(instances,images_per_row=10,**options):
    size=28
    # 每一行有一个
    image_pre_row=min(len(instances),images_per_row)
    images=[instances.reshape(size,size) for instances in instances]
#     有几行
    n_rows=(len(instances)-1) // image_pre_row+1
    row_images=[]
    n_empty=n_rows*image_pre_row-len(instances)
    images.append(np.zeros((size,size*n_empty)))
    for row in range(n_rows):
        # 每一次添加一行
        rimages=images[row*image_pre_row:(row+1)*image_pre_row]
        # 对添加的每一行的额图片左右连接
        row_images.append(np.concatenate(rimages,axis=1))
    # 对添加的每一列图片 上下连接
    image=np.concatenate(row_images,axis=0)
    plt.imshow(image,cmap=mpl.cm.binary,**options)
    plt.axis("off")

plt.figure(figsize=(9,9))
example_images=np.r_[X[:12000:600],X[13000:30600:600],X[30600:60000:590]]
plot_digits(example_images,images_per_row=10)
plt.show()

运行结果如下:
在这里插入图片描述
创建一个测试集,代码如下:

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

对训练集进行洗牌,这样是为了保证交叉验证的时候,所有的折叠都差不多。此外,有些机器学习算法对训练示例的循序敏感,如果连续输入许多相似的实例,可能导致执行的性能不佳。给数据洗牌,正是为了确保这种情况不会发生。
代码如下:

import numpy as np

shuffer_index=np.random.permutation(60000)
X_train,y_train=X_train[shuffer_index],y_train[shuffer_index]

训练一个二分类器

我们先尝试识别一个数字,比如数字5,那么这个"数字5检测器",就是一个二分类器的例子,它只能区分两个类别:5和非5。
为此分类任务创建目录标量,代码如下:

y_train_5=(y_train==5)
y_test_5=(y_test==5)

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

from sklearn.linear_model import SGDClassifier

sgd_clf=SGDClassifier(max_iter=5,tol=-np.infty,random_state=42)
sgd_clf.fit(X_train,y_train_5)

运行结果如下:
在这里插入图片描述
然后可以在用它来预测一下前面的“数据5”,代码如下:

sgd_clf.predict([some_digit])

运行结果如下:
输出了True,可以看到它预测出来了。
在这里插入图片描述

使用交叉验证测量精度

随机交叉验证和分层交叉验证效果对比
三折交叉验证,代码如下:

from sklearn.model_selection import cross_val_score
cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")

分3层交叉验证,代码如下:

# 类似于分层采样,每一折的分布类似
from sklearn.model_selection import StratifiedKFold #分层
from sklearn.base import clone

skfolds = StratifiedKFold(n_splits=3, random_state=42) #分3层

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%上下。
在这里插入图片描述
我们可以再用一个分类器来进行比较,代码如下:

from sklearn.base import BaseEstimator
# 随机预测模型
class Never5Classifier(BaseEstimator):
    def fit(self, X, y=None):
        pass
    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")

运行结果如下:
它的准确率也超过了90%,这是因为我们只有大约10%的图像是数字5,所以只要猜一张图片不是5,那么有90%的时间都是正确的。也就是说,准确率通常无法成为分类器的首要性能指标,特别是当我们处理偏斜数据集的时候(也就是某些类别比其他类更加频繁的时候)。
在这里插入图片描述

混淆矩阵

评估分类器性能的更好的方法是混淆矩阵。总体思路就是统计A类别实例被分成B类别的次数。例如,要想知道分类器将数字3和数字5混淆多少次,只需要通过混淆矩阵的第5行第3列来查看。
要计算混淆矩阵,需要一组预测才能将其与实际目标进行比较。当然可以通过测试集来进行预测,但是现在我们不动它(测试集最好保留到项目的最后,准备启动分类器时再使用)。最为代替,可以使用cross_val_predict()函数。cross_val_predict 和 cross_val_score 不同的是,前者返回预测值,并且是每一次训练的时候,用模型没有见过的数据来预测
代码如下:

from sklearn.model_selection import cross_val_predict

y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)#获取预测值
from sklearn.metrics import confusion_matrix

confusion_matrix(y_train_5, y_train_pred)#预测值与实际值的混淆矩阵构建

输出结果如下图所示:
结果表明第一行所有’非5’(负类)的图片中,有53417被正确分类(真负类),1162,错误分类成了5(假负类);第二行表示所有’5’(正类)的图片中,有1350错误分类成了非5(假正类),有4071被正确分类成5(真正类)。也就是说,实际不是5预测也不是5的个数53417,实际不是5预测是5的个数1162,实际是5预测不是5的个数1350,实际为5预测是5的个数4071。
在这里插入图片描述
一个完美的分类器只有真正类和真负类,所以其混淆矩阵只会在其对角线(左上到右下)上有非零值。代码如下:

y_train_perfect_predictions = y_train_5
confusion_matrix(y_train_5, y_train_perfect_predictions)

运行结果如下:
在这里插入图片描述

精度和召回率

混淆矩阵能提供大量信息,但有时我们可能会希望指标简洁一些。正类预测的准确率也称为分类器的精度。
P r e c i s i o n ( ) = T P T P + F P Precision(精度)=\frac{TP}{TP+FP}
其中TP是真正类的数量,FP是假正类的数量。
做一个简单的正类预测,并保证它是正确的,就可以得到完美的精度(精度=1/1=100%)
分类器会忽略这个正实例之外的所有内容。因此,精度通常会与另一个指标一起使用,这就是召回率,又称为灵敏度或者真正类率(TPR):它是分类器正确检测到正类实例的比率(如下):
R e c a l l ( ) = T P T P + F N Recall(召回率)=\frac{TP}{TP+FN}
FN是假负类的数量。

例如有一个二分类问题的算法。
在这里插入图片描述

图中的圆圈里面代表算法判定为正的一些样本,圆圈的外面代表算法判定为负的一些样本。
但实际上算法它是会有一些误判的,例如方形的左边一半,是实际上为正的样本。右边-半,是实际上为负的样本。那除了算法判断正确的,以外就是判断错误的样本。
可以对照这个图,看一下准确率,精度,和召回率的定义。
右上角是准确率的公式。意思就是,算法的所有预测结果中,预测正确的有多少。
左下角为precision精度查准率就是对于所有机器判定为正的里面,有多大的比例是真的正样本。
右下角为recall召回率查全率,顾名思义,就是实际的正样本中,有多大比例被检出了。
在图中有标记,阴阳,真假。真/假阴/阳性中,阴阳性是指的分类器的判断结果是阴性还是阳性,而真假指代的是是否和真是答案相符。不同的问题,他需要用的指标,希望达到的目标是不一样的。

回到我们原先的数字5检测器
使用sklearn的工具度量精度和召回率。代码如下:

from sklearn.metrics import precision_score, recall_score

precision_score(y_train_5, y_train_pred)
recall_score(y_train_5, y_train_pred)

运行结果如下:
这个数字5检测器,并不是那么完美,大多时候,它说一张图片为5时,只有77%的概率是准确的,并且也只有75%的5被检测出来了。
在这里插入图片描述
我们可以将精度和召回率组合成单一的指标,称为F1分数。
F 1 = 2 1 P r e c i s i o n + 1 R e c a l l = 2 P r e R e c P r e + R e c = T P T P + F N + F P 2 F_1=\frac{2}{\frac{1}{Precision}+\frac{1}{Recall}}=2*\frac{Pre*Rec}{Pre+Rec}=\frac{TP}{TP+\frac{FN+FP}{2}}
要计算F1分数,只需要调用f1_score()即可。代码如下:

from sklearn.metrics import f1_score
f1_score(y_train_5, y_train_pred)

运行结果如下:
F1分数对那些具有相近的精度和召回率的分类器更为有利。这不一定一直符合预期,因为在某些情况下,我们更关心精度,而另一些情况下,我们可能真正关系的是召回率。
在这里插入图片描述

精度/召回率权衡

在分类中,对于每个实例,都会计算出一个分值,同时也有一个阈值,大于为正例,小于为负例。通过调节这个阈值,可以调整精度和召回率。
得到召回率的代码如下:

y_scores = sgd_clf.decision_function([some_digit])
y_scores

运行结果如下:
在这里插入图片描述
调整阈值为0和20000时,代码如下:

threshold = 0 #调整阈值为0
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred

运行结果如下:
可以看到当阈值为0时,前面计算的分值大于0,返回True;它小于20000,返回False。
在这里插入图片描述
交叉验证。要注意它返回的是决策分数,而不是预测结果。

y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3,
                             method="decision_function")
y_scores.shape

运行结果如下:
在这里插入图片描述
画图:精度和召回率的曲线图
代码如下:

from sklearn.metrics import precision_recall_curve #画图

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", linewidth=2)
    plt.plot(thresholds, recalls[:-1], "g-", label="Recall", linewidth=2)
    plt.xlabel("Threshold", fontsize=16)
    plt.title("精度和召回率VS决策阈值", fontsize=16)
    plt.legend(loc="upper left", fontsize=16)
    plt.ylim([0, 1])

plt.figure(figsize=(8, 4))
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.xlim([-700000, 700000])
plt.show()

运行结果如下:
随着阈值的提高召回率下降了,有真例被判负了,精度上升,有部分原本被误判的负例,被舍弃了。
精度曲线会比召回率曲线要崎岖一些,原因在于,随着阈值提高,精度也有可能会下降 4/5 => 3/4(虽然总体上升)。另一方面,阈值上升,召回率只会下降。
在这里插入图片描述
画图:精度和召回率的函数图
代码如下:

def plot_precision_vs_recall(precisions, recalls):
    plt.plot(recalls, precisions, "b-", linewidth=2)
    plt.xlabel("Recall", fontsize=16)
    plt.title("精度VS召回率", fontsize=16)
    plt.ylabel("Precision", fontsize=16)
    plt.axis([0, 1, 0, 1])

plt.figure(figsize=(8, 6))
plot_precision_vs_recall(precisions, recalls)
plt.show()

运行结果如下:
从80%的召回率往右,精度开始急剧下降。我们可能会尽量在这个陡降之前选择一个精度/召回率权衡–比如召回率60%以上。至于如何选择取决于你的项目需求。
在这里插入图片描述
假设我们决定瞄准90%的精度目标。通过绘制的第一张图,得出需要使用的阈值大概是70000。要进行预测,除了调用分类器的predict方法,也可以使用这段代码:

y_train_pred_90 = (y_scores > 70000)
precision_score(y_train_5, y_train_pred_90)
recall_score(y_train_5, y_train_pred_90)

运行结果如下:
在这里插入图片描述

ROC曲线

1、roc曲线:接收者操作特征(receiveroperating characteristic),roc曲线上每个点反映着对同- -信号刺激的感受性。
横轴:负正类率(false postive rate FPR)特异度,划分实例中所有负例占所有负例的比例; (1-Specificity)
纵轴:真正类率(true postive rate TPR)灵敏度,Sensitivity(正类覆 盖率)
2、针对一个二分类问题,将实例分成正类(postive)或者负类(negative)。但是实际中分类时,会出现四种情况.
(1)若一个实例是正类并且被预测为正类,即为真正类(True Postive TP)
(2)若一个实例是正类,但是被预测成为负类,即为假负类(False Negative FN)
(3)若一个实例是负类,但是被预测成为正类,即为假正类(False Postive FP)
(4)若一个实例是负类,但是被预测成为负类,即为真负类(True Negative TN)
TP:正确的肯定数目
FN:漏报,没有找到正确匹配的数目
FP:误报,没有的匹配不正确
TN:正确拒绝的非匹配数目
列联表如下,1代表正类,0代表负类:
在这里插入图片描述

由上表可得出橫,纵轴的计算公式:
(1)真正类率(True Postive Rate)TPR: TPACTP+ FN.代表分类器预测的正类中实际正实例占所有正实例的比例。Sensitivity
(2)负正类率(False Postive Rate)FPR: FP/(FP+TN).代表分类器预测的正类中实际负实例占所有负实例的比例。1-Specificity
(3)真负类率(True Negative Rate)TNR: TN/(FP+ TN),代表分类器预测的负类中实际负实例占所有负实例的比例,TNR= 1-FPR。Specificity

ROC曲线绘制
ROC与精度/召回率曲线非常相似,但绘制的不是精度和召回率,而是真正类率(召回率的另一种称呼)和假正类率(FPR)。FPR是被错误分为正类的负类实例比率。它等于1-真负类率(TNR),后者正是被正确分类为负类的负类实例比率,也称为奇异度。因此ROC曲线绘制的是灵敏度和(1-奇异度)的关系。

~ 1 0
1 TP FN
0 FP TN

F P R = F P F P + T N FPR=\frac{FP}{FP+TN}
R e c a l l = T P T P + F N Recall=\frac{TP}{TP+FN}

使用 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', fontsize=16)
    plt.ylabel('True Positive Rate', fontsize=16)

plt.figure(figsize=(8, 6))
plot_roc_curve(fpr, tpr)
plt.show()

运行结果如下:
假设采用逻辑回归分类器,其给出针对每个实例为正类的概率,那么通过设定一个國值如0.6,概率大于等于0.6的为正类,小于0.6的为负类。对应的就可以算出一组(FPR,TPR),在平面中得到对应坐标点。随着阈值的逐渐减小,越来越多的实例被划分为正类,但是这些正类中同样也掺杂着真正的负实例,即TPR和FPR会同时增大。阈值最大时,对应坐标点为(0,0),阈值最小时,对应坐标点(1,1)。
ROC线上每个点对应一个阈值。
在这里插入图片描述
召回率(TPR)很高,分类器产生的假正类(FPR)就越多。虚线表示纯随机的ROC曲线;一个优秀的分类器(向左上角)。
有一种比较分类器的方式是测量曲线下面积(AUC)。完美的ROC AUC等于1,纯随机分类的ROC AUC等于0.5。
代码如下:

from sklearn.metrics import roc_auc_score

roc_auc_score(y_train_5, y_scores)

运行结果如下:
在这里插入图片描述

我们可以看到ROC曲线和精度/召回率(或PR)曲线非常相似,那么该如何决定使用哪种曲线呢?
当正类非常少见或者你更关注假正类而不是假负类时,应该选择PR曲线,反之选择ROC曲线。
例如,看前面的ROC曲线图时,以及ROC AUC分数时,你可能会觉得分类器真不错。但这主要是应为跟负类(非5)相比,正类(数字5)的数量真的很少。相比之下,PR曲线清楚地说明分类器还有改进的空间(曲线还可以更接近右上角)。

训练一个随机森林分类器,计算ROC和ROC AUC分数
绘制SGD和RL的ROC曲线对比图。代码如下:

from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(n_estimators=10, random_state=42)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3,
                                    method="predict_proba")
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)

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, "b:", linewidth=2, label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")
plt.title("SGD和RL的ROC曲线对比")
plt.legend(loc="lower right", fontsize=16)
plt.show()

运行结果如下:
在这里插入图片描述
测量曲线下面积(AUC),代码如下:

roc_auc_score(y_train_5, y_scores_forest)

运行结果如下:
在这里插入图片描述
测量精度和召回率,代码如下:

y_train_pred_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3)
precision_score(y_train_5, y_train_pred_forest)
recall_score(y_train_5, y_train_pred_forest)

运行结果如下:

在这里插入图片描述

多类别分类器

二元分类器在两个类别中区分,而多类别分类器(也称为多项分类器),可以区分两个以上的类别。
随机森林算法和朴素贝叶斯分类器可以直接处理多个类别。也有一些严格的二元分类器,比如支持向量分类器或线性分类器。但有多种策略,可以让我们用几个二元二类器实现多类别分类的目的 。
例如:我们可以训练0-9的10个二元分类器组合,那个分类器给的高,就分为哪一类,这称为一对多(OvA)策略 。
另一种方法,是为每一对数字训练一个二元分类器:一个用来区分0-1,一个区分0-2,一个区分1-2,依次类推。这称为一对一(OvO)策略,解决N分类,需要(N)*(N-1)/2分类器,比如MNIST问题,需要45个分类器。OvO的主要优点在于每个分类器只需要用到部分训练集对其必须区分的两个类别进行训练。
有些算法(例如支持向量机算法),在数据规模增大时,表现糟糕,因此对于这类算法,OvO是一个优秀的选择,由于在较小的训练集上分别训练多个分类器比在大型数据集上训练少数分类器要快得多。但对于大多数二元分类器,OvA策略还是更好的选择。

使用0-9进行训练,在sgd内部,sklearn使用了10个二元分类器,获得它们对图片的决策分数,然后选择最高的类别。
代码如下:

sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit])

运行结果如下:
在这里插入图片描述
我们可以看到 sgd对输入的结果输出了10个预测分数,而不是1个。
十个概率值,取最大的为预测分值。代码如下:

some_digit_scores = sgd_clf.decision_function([some_digit])
some_digit_scores
np.argmax(some_digit_scores)

运行结果如下:
在这里插入图片描述
训练分类器的时候,目标类别的列表会存储在classes_这个属性中,按值的大小进行排序。
代码如下:

sgd_clf.classes_

运行结果如下:
在这里插入图片描述
强制使用OVO策略,代码如下:

from sklearn.multiclass import OneVsOneClassifier
ovo_clf = OneVsOneClassifier(SGDClassifier(max_iter=5, tol=-np.infty, random_state=42))
ovo_clf.fit(X_train, y_train)
ovo_clf.predict([some_digit])

运行结果如下:
在这里插入图片描述
求概率,代码如下:

len(ovo_clf.estimators_)

运行结果如下:
在这里插入图片描述
随机森林的多分类,不需要OvA或者OVO策略。代码如下:

forest_clf.fit(X_train, y_train)
forest_clf.predict([some_digit])
forest_clf.predict_proba([some_digit])

运行结果如下:
在这里插入图片描述
对分类器进行评估,代码如下:

cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")

运行结果如下:
在这里插入图片描述
评测结果大概都为80%以上,如果是随机分类器,准确率大概是10%左右,所以这个结果不是太糟糕,但是依然有提升的空间,比如使用标准化,进行简单的缩放。
代码如下:

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

运行结果如下:
在这里插入图片描述

错误分析

做项目应遵循机器学习项目清单的步骤:探索数据准备的选项,尝试多个模型,列出最佳模型并使用GridSearchCV对超参数进行微调,尽可能自动化,等等。假设我们已经找到一个有潜力的模型,现在希望找到一些方法,对其进一步改进。方法之一就是分析其类型错误。

我们先看一下混淆矩阵,代码如下:

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

运行结果如下:
在这里插入图片描述
对个数大小进行绘图,代码如下:

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上的执行效果不行。实际上,这二者都属实。
在这里插入图片描述

让我们把焦点都放在错误上。首先,我们需要将混淆矩阵中的每个值都除以相应类别中的图片数,这样比较的是错误率,而不是错误的绝对值(后者对图片数量较多的类别不公平)。
将中间都置为0,每一列除以每一列的个数,代码如下:

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 、9 列比较亮,表示其他数字容易被分错为8 、9, 8 、9 行比较亮,说明 8、 9 容易被错误分为其他数字。此外3 容易被错分为 5,5也容易被错分为4。
在这里插入图片描述
分析混淆矩阵,通常可以帮助我们深入了解如何改进分类器。通过上面的图,我们可以花费更多时间来改进8 、9的分类,以及修正 3 、5 的混淆上。
例如,可以试着收集更多这些数字的训练集, 或者开发新特征来改进分类器。可以写一个算法来计算闭环的数量,比如(8有两个,6有一个,5没有)。 或者对图片进行预处理,让某些模式更加突出,比如闭环之类。
代码如下:

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

运行结果如下:
我们可以看到有一些数字容易混淆,但大多数还是比较好分类的,可算法还是会分错。因为SGD模型是一个线性模型,它所做的就是为每一个像素分配一个各个类别的权重,当它看到新的图像时,将加权后的像素强度汇总,从而得到一个分数进行分类。而数字3和5只在一部分像素位上有区别,所以分类器很容易将其搞混。
数字3和5之间的主要区别在于连接顶线和下方弧线中间的小线条的位置。如果我们写的数字3将连续点略往左移,分类器就可能将其分类为5,反之亦然。换言之,这个分类器对图像位移和旋转非常敏感,因此,减少3 、5混淆的方法之一是对数字进行预处理,确保他们位于中心位置,并且没有旋转。这也有助于减少其他错误。
在这里插入图片描述

多标签分类

每个实例都只有一个输出,但某些情况下,我们需要分类器为每个实例产出多个类别,比如,为照片中的每个人脸附上一个标签。 假设分类器经过训练,已经可以识别三张脸 A、 B 、C,那么当看到A和C的合照时,应该输出[1,0,1],这种输出多个二元标签的分类系统成为多标签分类系统 。
下面以k近邻算法为例(不是所有的分类器都支持多标签)。代码如下:

from sklearn.neighbors import KNeighborsClassifier

y_train_large = (y_train >= 7)#标签1,是否大于7
y_train_odd = (y_train % 2 == 1)#标签2,是否为奇数
y_multilabel = np.c_[y_train_large, y_train_odd]#组合标签,按列连接

knn_clf = KNeighborsClassifier() #序列
knn_clf.fit(X_train, y_multilabel)

knn_clf.predict([some_digit]) #预测我们的数值5

运行结果如下:
输出结果正确,5小于7返回False;5是奇数,返回True
在这里插入图片描述
交叉验证,对k近邻算法做一个验证。代码如下:

y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3, n_jobs=-1)
f1_score(y_multilabel, y_train_knn_pred, average="macro")

运行结果如下:
运行起来可能会比较慢,耐心等一下哦。这里假设了所有的标签都是同等重要,但实际的数据可能并不均衡,可以修改average=“weighted”,来给每个标签设置一个等于其自身支持的权重。
在这里插入图片描述

多输出分类

我们将讨论最后一种分类任务–多输出多分类任务(简称为多输出分类)。简单而言,它是多标签分类的泛化,其标签也可以是多种类别的(比如有两个以上的值) 。
说明:构建一个去除图片中噪声的系统。给它输入一个带噪声的图片,它将(希望)输出一张干净的数字图片,跟其他MNIST图片一样,以像素强度的一个数组作为呈现方式。 需要注意的是,这个分类器的输出时多个标签(一个像素点一个标签),每一个标签有多个值(0-255)。所以这是一个多输出分类器系统的例子。

创建训练集和测试集,使用Numpy的randint 来给Mnist图片的像素强度增加噪声。目标是将图片还原为原始图片。
代码如下:

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

some_index = 5500
plt.subplot(121); plot_digit(X_test_mod[some_index])
plt.subplot(122); plot_digit(y_test_mod[some_index])
plt.show()

运行结果如下:
左边为添加噪声后的样子。
在这里插入图片描述
代码如下:

knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)

运行结果如下:
可见已经消除噪声,还原为数字5了。
在这里插入图片描述

原创文章 41 获赞 65 访问量 8375

猜你喜欢

转载自blog.csdn.net/weixin_44436677/article/details/105747642
今日推荐