分类(三):多类分类器和误差分析

489 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

1. 多类分类器

之前我们已经了解了二元分类器,能够从两个类中区别,那么多类分类器就需要区分两个及两个以上的类。

有些类可以直接处理多个类(如随机森林分类器或朴素贝叶斯分类器),但是有些只能是二元分类器(如支持向量机或线性分类器)。当然我们可以根据一定的策略,将二元分类器实现多类分类的功能。

如果我们需要将mnist数据集分成0-9,那么我们可以有两种方法:

  1. 我们创建10个分类器(0-检测器,1-检测器和2-检测器等等),将测试的图片放入10个分类器,获得10个决策分数,最后比较出最大的分数,将其分成该类。这中方法叫一对剩余(one-versus-the-rest,OvR)策略,也称为一对多(one-versus-all,OvA)策略。

图1 一对多策略

  1. 我们为每一对数字训练一个二元分类器,如区分0和1,区分0和2,区分0和3,以此类推。这是一对一(one-versus-one,OvO)策略。当我们存在N个分类时,就需要N×(N1)2N\times \frac{(N-1)}{2}个分类器。OvO的优点在于,每个分类器只需要处理部分需要判断的两个类。

需要注意的时有些算法(例如支持向量机)在数据扩大时表现的很差,对于这类算法,可以优先选择OvO。但是对于大部分二元分类器来说,OvR策略还是更好的选择。

from sklearn.svm import SVC

svm_clf = SVC()
svm_clf.fit(X_train, y_train)
print(svm_clf.predict([some_digit])) # 输出 [5]
some_digit_scores = svm_clf.decision_function([some_digit])
some_digit_scores
# 输出
array([[ 1.72501977,  2.72809088,  7.2510018 ,  8.3076379 , -0.31087254,
         9.3132482 ,  1.70975103,  2.76765202,  6.23049537,  4.84771048]])

现在我们使用y_train而不是y_train_5,这样SVC就会将目标分成数字0-9,而不是5和非5。Scikit-Learn在内部实际会训练45个二分分类器。

同时我们调用decision_function()也是返回了10个类的分数,如果我们需要看有哪些类,则可以调用svm_clf.classes_他的输出内容为:[0 1 2 3 4 5 6 7 8 9]

我们通过np.argmax(some_digit_scores)计算some_digit_scores中最大值的index,通过这个index去访问svm_clf.classes_,也能获得SVC判断为哪个类。

当然我们也可以强制选择时一对一还是一对剩余策略,只需要sklearn.multiclass模块下的OneVsOneClassifier和OneVsRestClassifier类。

只需要设置成ovr_clf = OneVsRestClassifier(SVC()),这样SVC就会被选择一对剩余策略。

我们也可以训练一个SGDClassifier或者RandomForestClassifier:

# 需要注意y输入的y_train,不再是y_train_5
sgd_clf.fit(X_train, y_train)
sgd_clf.predict([some_digit]) # 输出 array([3], dtype=uint8)

sgd_clf.decision_function([some_digit])
# 输出
array([[-31893.03095419, -34419.69069632,  -9530.63950739, 1823.73154031, -22320.14822878,  -1385.80478895, -26188.91070951, -16147.51323997,  -4604.35491274,-12050.767298  ]])

这里对于some_digit的预测出错了,正确应该是5,但是输出为3,为什么3和5会出错呢?读者可以思考一下,下面也会给出答案。

我们看到sgd_clf.decision_function([some_digit])对于别的分数都是负数,只有第4类是正的,所以预测为3。最后我们需要评估一下这个分类器的性能:

from sklearn.preprocessing import StandardScaler
# 直接使用交叉验证评估
cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
# 直接评估准确率:
# array([0.87365, 0.85835, 0.8689 ])

# 还记的我们特征缩放,这里使用标准化提高我们的准确率
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.8983, 0.891 , 0.9018])

2. 误差分析

我们现在探讨一下产生错误的原因。

既然上面有错误,那我们就需要先找一下错误的原因,我们可以先看看混淆矩阵,因为他有着预测和真实之间的关系,能够让我们很方便看出错误所在。

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([[5577,    0,   22,    5,    8,   43,   36,    6,  225,    1],
       [   0, 6400,   37,   24,    4,   44,    4,    7,  212,   10],
       [  27,   27, 5220,   92,   73,   27,   67,   36,  378,   11],
       [  22,   17,  117, 5227,    2,  203,   27,   40,  403,   73],
       [  12,   14,   41,    9, 5182,   12,   34,   27,  347,  164],
       [  27,   15,   30,  168,   53, 4444,   75,   14,  535,   60],
       [  30,   15,   42,    3,   44,   97, 5552,    3,  131,    1],
       [  21,   10,   51,   30,   49,   12,    3, 5684,  195,  210],
       [  17,   63,   48,   86,    3,  126,   25,   10, 5429,   44],
       [  25,   18,   30,   64,  118,   36,    1,  179,  371, 5107]],
      dtype=int64)

# 我们发现直接看confusion_matrix,这样是很困难的,因此可以将其化成图像显示出来
plt.matshow(conf_mx, cmap=mpl.cm.gray)
plt.show()

图2 混淆矩阵

首先解释一下cmap=mpl.cm.gray的作用:这样plt画出的图像的颜色会根据矩阵的数字大小而呈现黑白两色,数值越高颜色越白。我们可以看出,对角线上是最亮的,对角线的含义看上一篇文章《分类二》,这说明了预测的比例还是比较高。

但是这不方便我们看错误的原因,我们需要先将对角线设置为0,将其暂时先分离出去。同时我们可以通过对矩阵的数值除去数值所在行的总数,获得错误率。通过查看错误分析和定位错误所在。

# 计算每一行的总和
# keepdims = True,这样求出的总和的维度就和原来的矩阵保持一致
row_sum = conf_mx.sum(axis=1, keepdims=True)
# 将矩阵的每行的值都除去每行的总和
norm_conf_mx = conf_mx / row_sum
# fill_diagonal 设置对角线为对应的数值
np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=mpl.cm.gray)
plt.show()

图3 norm_conf_mx

现在我们可以一下再次专注于白点,我们发现不仅3和5之间的白度比较高,而且第8列好像也挺多白点的。

我们知道列表示预测的值,只有在对角线上是预测正确的,其余的都是预测错误的,说明8这个预测错误挺多的。但是第八行,表示8被分成了哪几类,这个却不是很差。因此错误之间是不对称,也就是行列之间可以是不相关的。

当然处理8这个问题,我们可以这么做:

  1. 当然是继续增加8的数据,继续训练,提高分类能力
  2. 可以根据8的特点设计新的分类算法,比如他有两个闭环,0,6和9只有一个,其余没有以此区分出8。

让我们继续查一下3和5的错误原因,不如我们看看数据中3和5的样子如何:


cl_a, cl_b = 3, 5
# 选择实际是3 预测是3的图片集
X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a) ]
# 选择实际是3 预测是5的图片集
X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b) ]
# 选择实际是5 预测是3的图片集
X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a) ]
# 选择实际是5 预测是5的图片集
X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b) ]

# 中间省去画图过程,就是调用plt的子图功能和imshow方法
...
plt.show()

图3 3,5对比图

解释一下图片内容:

  • 左上角是实际是3,预测是3的图片集,也就是预测3对的
  • 右上角是实际是3,预测是5的图片集,也就是预测3错了,这里需要我们仔细看看
  • 左下角是实际是5,预测是3的图片集,也就是预测5错了,这里需要我们仔细看看
  • 右下角是实际是5,预测是5的图片集,也就是预测5对的

不知道读者对于预测错误的两个图片集中,是不是也有对于分类困惑的。我们分类器现在还远远不如我们的大脑分类强,如果我们都会出错,那么分类器更容易出错。

那么为什么会出错呢?

SGDClassifier是一个线性模型,他所做的就是对每一个像素进行加权,最后求出所有的像素点加权总和。再根据总和去分类。而3和5其实只有部分像素点的区别,因此很容易混在一下。也说明了这个分类器对于图像的移位和旋转很敏感,因此我们可能需要预处理图片,需要确保他们位于中心位置,且没有旋转。