持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
1. 多类分类器
之前我们已经了解了二元分类器,能够从两个类中区别,那么多类分类器就需要区分两个及两个以上的类。
有些类可以直接处理多个类(如随机森林分类器或朴素贝叶斯分类器),但是有些只能是二元分类器(如支持向量机或线性分类器)。当然我们可以根据一定的策略,将二元分类器实现多类分类的功能。
如果我们需要将mnist数据集分成0-9,那么我们可以有两种方法:
- 我们创建10个分类器(0-检测器,1-检测器和2-检测器等等),将测试的图片放入10个分类器,获得10个决策分数,最后比较出最大的分数,将其分成该类。这中方法叫一对剩余(one-versus-the-rest,OvR)策略,也称为一对多(one-versus-all,OvA)策略。
图1 一对多策略
- 我们为每一对数字训练一个二元分类器,如区分0和1,区分0和2,区分0和3,以此类推。这是一对一(one-versus-one,OvO)策略。当我们存在N个分类时,就需要个分类器。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这个问题,我们可以这么做:
- 当然是继续增加8的数据,继续训练,提高分类能力
- 可以根据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其实只有部分像素点的区别,因此很容易混在一下。也说明了这个分类器对于图像的移位和旋转很敏感,因此我们可能需要预处理图片,需要确保他们位于中心位置,且没有旋转。