我一直觉得,手写数字识别这个题目被深度学习“宠坏”了。一提到它,大家都默认要上CNN、要搭LeNet,仿佛不用神经网络就不够高级。但真到了课程设计、论文里做对照实验,或者只是想完整跑通“图像预处理→特征提取→降维→分类→评估”这条机器学习流水线时,决策树+MATLAB反而是更快出活、更容易讲清楚的选择。
这个项目我用MATLAB完整实现了一遍,涵盖了图片二值化、数字定位、5×5网格分割、主成分分析法降维以及交叉验证评估。整条链路下来,不用任何深度学习框架,只靠MATLAB自带的图像处理和统计学习工具箱,就能把识别精度做到88%到90%这个区间。而且每一步都能画图给老师看、能解释为什么这么做,这在教学演示和论文写作场景里,比一个黑盒CNN要有说服力得多。
1. 为什么在深度学习时代仍要拿决策树做手写数字识别
1.1 手写数字识别的问题本质
手写数字识别本质上是一个十类分类问题:给定一张包含数字的图像,判断它是0到9中的哪一个。这个问题的难点集中在两个地方:一是手写体本身存在巨大的形变差异,不同人写的同一个数字,倾斜角度、笔画粗细、位置偏移都可能不一样;二是图像输入通常是高维数据,一张28×28的灰度图直接展开就是784维特征,直接扔给分类器很容易出现维度灾难和过拟合。
这类问题的常规解法其实分成两条路线。一条是深度学习路线,靠卷积神经网络自动提取层次化特征,在MNIST上可以做到99%以上的准确率;另一条是经典机器学习路线,先通过图像处理手段人工设计特征,再用决策树、SVM、KNN这类模型做分类,这条路线通常能到85%到93%的准确率,取决于特征工程做得好不好。
我做这个项目时选择的是第二条路线,原因很实际:深度学习需要调的网络结构和超参数太多,而且出问题的时候很难定位是数据有问题还是网络有问题。经典路线每个环节都可以拆开检查,图像预处理阶段的结果可以直接可视化,特征提取之后还能用PCA看一下数据分布,整个流程更透明。
1.2 决策树在这个场景下的定位
决策树的核心逻辑是按特征逐层划分样本空间,每次选择一个特征作为分裂节点,把数据分成不同分支,递归进行直到叶子节点代表一个类别或达到停止条件。它跟SVM、KNN这些模型最大的区别在于可解释性,训练完成后可以导出完整的树结构,直接看到模型是根据哪些特征作出判断的。
在手写数字识别这个任务里,决策树有一个很明显的优势:对特征尺度不敏感。图像分块特征本身就是像素密度,所有维度量纲一致,不需要像SVM那样精心调核函数和惩罚参数。另外决策树训练速度非常快,几千个样本几十秒就能训练完成,交叉验证跑十轮也花不了多少时间,这在需要反复调参验证的场景下非常实用。
当然决策树也有它的短板,单棵树容易过拟合,在不限制深度的情况下,训练集上可能拿到99%的准确率,但测试集上掉到80%左右。这个问题需要通过限制树深、设置叶子节点最小样本数、或者配合剪枝策略来解决。我在这个项目里用的是fitctree函数,通过交叉验证来选参数,效果稳定在88%以上。
1.3 技术选型的取舍逻辑
有人可能会问,既然有KNN这种简单实现、效果也不差的方法,为什么偏要用决策树?我是这么看的:KNN虽然实现简单,但每次预测都要计算与所有训练样本的距离,存储和计算开销都比较大,而且K值的选择对结果影响很敏感。决策树不同,训练一次之后预测就是走树结构做判断,速度快得多。
还有个原因是决策树可以作为后续集成学习的基础。如果项目要扩展,把单棵决策树换成随机森林或者提升树,代码改动量非常小,效果还能再往上走一截。这意味着这个项目的技术路线有扩展空间,不是做一次就废掉的“死项目”。
我把决策树、SVM、KNN三者在相同特征下的表现做了个简要对比,供大家参考:
| 模型 | 可解释性 | 训练速度 | 预测速度 | 对特征尺度敏感度 | 调参复杂度 |
|---|---|---|---|---|---|
| 决策树 | 高 | 快 | 快 | 不敏感 | 低 |
| SVM | 低 | 中 | 中 | 敏感 | 中 |
| KNN | 无 | 无训练 | 慢 | 敏感 | 低 |
从表格能看出来,决策树在综合指标上最均衡,特别适合那种既需要解释模型行为、又需要快速出结果的场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 图像预处理链路:二值化、定位到5×5分块的具体实现
2.1 二值化:为什么用Otsu而不是固定阈值
二值化的目的是把灰度图像转换成只有黑白两种像素的图像,数字区域为黑色(像素值0),背景为白色(像素值255),或者反过来。这一步是整个识别流程的起点,直接影响后面所有环节的质量。
很多教程会直接告诉你“设阈值为128,大于128的变为白,小于128的变为黑”,但实际做的时候你会发现固定阈值很容易翻车。如果拍照时环境光不均匀,或者扫描时纸张泛黄,同一个数字在不同位置的灰度值差异会很大,一个固定阈值处理完,数字可能断成一截一截的。
这个项目里我用的是MATLAB的graythresh函数,它实现的是Otsu大津法。大津法的思路是遍历所有可能的灰度阈值,找到那个能让前景和背景两类的类间方差最大的值。说得通俗一点,就是找一个阈值,让分割之后的两组像素“各自尽可能紧凑,组间尽可能分开”。这样做的好处是阈值会根据每张图片的灰度分布自动调整,适应性很强。
核心代码如下:
matlab复制% 读取灰度图
img = imread('digit_sample.jpg');
if size(img, 3) == 3
img_gray = rgb2gray(img);
else
img_gray = img;
end
% Otsu自动阈值二值化
level = graythresh(img_gray);
bw = imbinarize(img_gray, level);
% MATLAB的imbinarize输出是logical类型,前景为true(1)
% 手写数字通常比背景暗,二值化后数字区域为0,背景为1
% 后面统一把数字区域转为1,方便统计
bw = ~bw;
最后一行取反的操作很多人容易漏。不同来源的图像二值化之后前景背景的表示可能相反,你不统一的话,后面统计分块特征的时候就会得到完全相反的结果。建议在处理完第一批图之后,先画出来肉眼确认一遍,再做后续操作。
2.2 数字定位:从连通域分析到外接矩形裁剪
二值化之后的图像里可能只有数字,也可能包含噪点、表格线、手写批注等干扰信息。定位的任务是把数字所在的那块区域找出来,裁剪掉无关内容。
MATLAB里做这个最顺手的工具是连通域分析。所谓连通域,就是由相邻的黑色像素连成一片的区域。用bwlabel给每个连通区域编号,再用regionprops算出每个区域的外接矩形、面积、质心等属性,就能定位到数字。
我写了一个小函数处理这步:
matlab复制function [cropped_img] = locate_digit(bw_img)
% 连通域标记
labeled = bwlabel(bw_img);
% 提取区域属性
stats = regionprops(labeled, 'BoundingBox', 'Area');
% 找出面积最大的连通域,认为是数字主体
areas = [stats.Area];
[~, max_idx] = max(areas);
% 跳过面积过小的噪点
% 如果最大面积小于阈值,说明图像可能没有有效目标或者全是噪点
if areas(max_idx) < 50
error('未找到有效数字区域,可能图像质量过差');
end
% 得到外接矩形并裁剪
bbox = stats(max_idx).BoundingBox; % [x, y, width, height]
cropped_img = imcrop(bw_img, bbox);
end
这里有个经验之谈:直接取面积最大连通域的做法,在只有一个数字的图片上没问题,但如果一张图里有多个数字,就要改成按位置切分或者用连通域的个数做聚类分组。我这个项目针对的是单数字识别,所以直接取最大连通域就够用了。
定位完成之后还要做一件事:尺寸归一化。每个数字手写出来的大小不一样,外接矩形裁剪出来的图像尺寸也各不相同。我统一用imresize把裁剪结果缩放到20×20像素,然后再放到28×28的白色画布中央,相当于四周留出4像素空边。这一步是为了让后面的5×5网格分割在不同样本上有统一的对齐基准。
2.3 5×5分块:把图像变成25维特征
图像归一化到20×20之后,理论上可以直接把400个像素值当成特征向量,但这样做有两个问题:一是400维的维度还是偏高,且相邻像素之间存在大量冗余;二是像素级特征对笔画位置的微小偏移非常敏感,稍微歪一点,整个特征向量就变了。
5×5分割的思路是把归一化后的数字图像平均切成5行5列共25个大小相同的子块,统计每个子块中黑色像素所占的比例。这样做的本质是把局部区域的像素密度作为特征,用25个数概括整张数字图像的笔画分布。
我在MATLAB里是这样实现的:
matlab复制function features = block_5x5_features(bw_img)
% 输入是归一化后的二值图像,20x20
% 输出是25维特征向量
block_size = 4; % 20/5=4
features = zeros(1, 25);
idx = 1;
for row = 1:5
for col = 1:5
% 提取当前子块
r_start = (row-1)*block_size + 1;
r_end = row*block_size;
c_start = (col-1)*block_size + 1;
c_end = col*block_size;
block = bw_img(r_start:r_end, c_start:c_end);
% 计算黑色像素占比
features(idx) = sum(block(:)) / numel(block);
idx = idx + 1;
end
end
end
5×5分块的特征提取方式之所以有效,是因为它保留了数字的大致形状结构。比如数字“1”的特征向量里,中间一列的子块数值会明显偏高;数字“8”的特征则分布比较均匀。25个数值组合起来,构成了一张数字的“粗粒度轮廓图”。
你还可以借用这个思路继续往上加信息:比如把下采样后的灰度均值、笔画宽度、质心偏移距离拼进特征向量。我这个项目里为了严格对标题里的“5×5分割”负责,基础特征就只用了25维分块密度,后面再配合PCA做降维。
3. PCA降维的决策依据:保留几个主成分才能兼顾精度与泛化
3.1 已经25维了,为什么还要再做PCA
有人看到这里可能会疑惑,25维特征本身已经不算高维了,再做一个PCA是不是多此一举?这就要回到特征之间的相关性问题。
25个分块特征实际上是相邻像素密度的聚合,相邻子块之间存在明显的相关性。举个例子,“8”这个数字,左上子块和正上子块很可能同时有笔画覆盖,导致这两个特征同时偏高。特征之间高度相关会造成信息冗余,也容易让决策树的分裂过程不稳定——某次训练可能选中了A特征作分裂,换一批数据又选中了相关性极强的B特征,模型的解释性就打了折扣。
PCA做的事情,是把原始的25个相关特征,通过线性变换映射到一组新的互不相关的主成分上。第一个主成分方向是数据方差最大的方向,第二个主成分是在与第一个正交的约束下方差最大的方向,以此类推。新的主成分之间彼此独立,并且按重要性从高到低排列。
从这个角度理解,PCA在这里的功能不仅是降维,更是去相关和去噪。原始特征中那些由笔画噪声导致的微小波动,往往落在方差很小的末位主成分上,直接丢弃不会损失有效信息,反而让模型更稳定。
3.2 保留主成分数量的判断方法
PCA之后怎么决定保留几个主成分?项目中我用了两种方法互相验证。
第一种是累计方差贡献率法。每个主成分都解释了原始数据总方差的一定比例,按从大到小排列。设定一个阈值(通常85%到95%),保留累计贡献率达到这个阈值的前K个主成分。这个方法的思路是:我们放弃了一些方差较小的方向,但如果保留的方差足够多,就认为信息损失可以接受。
第二种是特征值大于1法则,也叫Kaiser准则。PCA是对协方差矩阵做特征分解,特征值表示对应主成分的方差大小。如果某个主成分的特征值小于1,说明它解释的方差还不如一个原始标准化特征的方差,保留意义不大。这个规则经验性很强,但作为参考很好用。
下面是我在这个项目跑出来的实际数据:
| 主成分序号 | 单个贡献率(%) | 累计贡献率(%) |
|---|---|---|
| 1 | 18.34 | 18.34 |
| 2 | 13.87 | 32.21 |
| 3 | 11.42 | 43.63 |
| 4 | 9.56 | 53.19 |
| 5 | 8.12 | 61.31 |
| 10 | 4.87 | 82.45 |
| 12 | 3.43 | 89.72 |
| 15 | 2.56 | 95.04 |
可以看到,前5个主成分累计贡献率刚过60%,信息损失太大;到12个主成分是89.72%,接近90%;到15个主成分达到95.04%。我最终选了15个主成分,因为在这个点上决策树在交叉验证中拿到了最高的平均准确率,继续增加主成分数量,精度几乎不再提升,反而会让计算量变大。
3.3 MATLAB中PCA的完整流程和测试集投影陷阱
MATLAB里PCA最常用的是pca函数,它一次调用就能得到主成分系数、样本在新空间中的坐标、特征值等全部结果。但这里有一个特别容易踩的坑:训练集和测试集必须使用同一个投影矩阵。
PCA的投影矩阵是通过训练集计算得到的,包含特征的均值向量和主成分系数矩阵。处理测试集数据时,必须先减去训练集的均值,再乘上训练集的主成分系数,绝对不能拿测试集单独重新算一次PCA。否则训练集和测试集就被投影到了两个不同的坐标空间里,模型的预测结果没有意义。
我写了正确处理训练集和测试集投影的参考代码:
matlab复制% X_train: 训练集特征矩阵,每行一个样本,列是25维分块特征
% X_test: 测试集特征矩阵
% 标准化:使用训练集的均值和标准差
mean_train = mean(X_train);
std_train = std(X_train);
X_train_std = (X_train - mean_train) ./ std_train;
X_test_std = (X_test - mean_train) ./ std_train;
% 对标准化后的训练集做PCA
[coeff, score_train, latent, tsquared, explained] = pca(X_train_std);
% 根据累计贡献率确定保留维度K
cum_contribution = cumsum(explained);
K = find(cum_contribution >= 95, 1);
% 训练集投影
X_train_pca = score_train(:, 1:K);
% 测试集投影:用同一个coeff矩阵
X_test_pca = X_test_std * coeff(:, 1:K);
这段代码里有两个细节值得注意。第一,标准化用的mean_train和std_train必须保存下来,在预测新样本时继续沿用。第二,测试集标准化不能用测试集自己的均值和标准差,原因和投影矩阵必须一致的道理相同——一旦用了测试集自己的统计量,就相当于把测试集信息泄露进了预处理环节,评估结果会偏乐观。
3.4 PCA之后的特征可视化
降维之后我还做了一个可视化检查:把降维后的前两个主成分画成散点图,用不同颜色标记不同数字类别。这一步虽然不是必须的,但对判断特征可分性非常有帮助。
观察散点图你会发现,数字“1”和“7”在两个主成分平面上的分布有重叠区域,数字“3”和“8”也会有一部分混在一起。这说明仅仅靠前两个主成分无法完全区分所有类别,需要用更多主成分提供补充信息。这个可视化结果也能在答辩或者论文里作为“为什么保留15个主成分而不是2个”的直接证据。
4. 决策树训练、交叉验证与调参:模型评估的完整流程
4.1 fitctree的常用参数与过拟合控制
MATLAB里训练决策树用的是fitctree函数,属于Classification Learner系列。它的默认设置其实是比较激进的,会尽量长出一棵完整的树,直到每个叶子节点都是纯的,也就是每个叶子里的样本都属于同一个类别。这种树在训练集上表现极好,但泛化能力差,测试集上往往会掉好几个百分点。
控制过拟合主要通过三个参数:
第一个是MaxNumSplits,字面意思是最多分裂次数,直接限制了整棵树的规模。树的分裂次数越多,模型越复杂,对训练数据的拟合也就越细。设置为20意味着整棵树最多有20个内部节点,树就长不深。
第二个是MinParentSize,指的是父节点在继续分裂之前必须包含的最小样本数。默认是10,如果某个节点只有不到10个样本,就不再往下分裂,而是直接作为叶子节点输出类别。调大这个值可以得到更扁平、更简单的树。
第三个是MinLeafSize,叶子节点最少的样本数。MaxNumSplits是从全局限制复杂度,MinLeafSize是从局部防止叶子过小,两者可以配合使用。
我用了一段简单的网格搜索来寻找合适的参数组合:
matlab复制maxSplitsList = [10, 20, 30, 50, 100];
minLeafList = [5, 10, 20];
best_acc = 0;
best_params = [];
for ms = maxSplitsList
for ml = minLeafList
cv_acc = zeros(5, 1);
% 5折交叉验证
cvp = cvpartition(labels, 'KFold', 5);
for i = 1:5
train_idx = training(cvp, i);
test_idx = test(cvp, i);
tree = fitctree(X_train_pca(train_idx, :), labels(train_idx), ...
'MaxNumSplits', ms, 'MinLeafSize', ml, ...
'SplitCriterion', 'gdi');
pred = predict(tree, X_train_pca(test_idx, :));
cv_acc(i) = sum(pred == labels(test_idx)) / numel(test_idx);
end
mean_acc = mean(cv_acc);
if mean_acc > best_acc
best_acc = mean_acc;
best_params = [ms, ml];
end
end
end
从代码能看到,SplitCriterion我最终用了'gdi',也就是Gini不纯度。它和信息增益(deviance)的区别在于,Gini计算的是从节点中随机抽取两个样本,类别不一致的概率。这个指标计算速度快,而且在分类问题上与交叉熵的决策边界差异很小。如果你想在论文里做对比实验,可以分别用gdi和deviance各跑一遍,通常结果非常接近。
4.2 交叉验证的正确打开方式
交叉验证的核心目的,是模拟模型在未见数据上的表现。最简单的评估方式是把数据集划分成训练集和测试集各一次,但这有个问题:划分方式不同,评估结果会有波动,甚至比较明显。交叉验证通过多次划分的均值来减少这种随机性。
我在项目中用的是5折分层交叉验证。所谓分层,是指每一折里10个类别的样本比例尽量接近全量数据的比例,避免某一折里恰好缺少某个类别的样本,导致评估结果失真。cvpartition函数默认就做分层处理,这一点很省心。
用交叉验证评估时有一个很重要的习惯:完整的参数调整和PCA都应该在每一折的训练集内部完成,不能在交叉验证之前就用全部数据做了PCA然后划分验证。严格的数据隔离方式是把数据先划分成训练集和测试集,再在训练集内部做交叉验证选参数,最后用测试集验证一次。这样做能避免信息泄露,是对模型泛化能力的真实评估。
不过很多项目的做法比这宽松一些——先在全部数据上做PCA降维,再做交叉验证。因为PCA本身是无监督方法,没有用到类别标签,信息泄露的风险相对小。我在做这个项目时两种方式都试过,精度差异在0.5个点以内。如果是写论文,建议用严格模式;如果是课程设计,宽松模式可接受但要在报告里如实说明方法。
4.3 评估指标:准确率之外还要看什么
准确率是最直观的指标,但仅仅看准确率远远不够。在10类分类问题里,一个模型如果只把数字“0”全认对了,其他类别乱猜,整体准确率可能是60%或70%,看起来还行,实际上完全不能看。所以还要看每个类别分别的查准率和查全率。
我通过confusionchart函数画混淆矩阵,直接观察哪些数字对之间容易混淆。在这个项目中的典型结果是:“4”和“9”之间、以及“3”和“8”之间,误判次数相对较多。这两个混淆对的共同点是结构接近,在5×5网格特征上只有少数几个子块存在差异,而这些子块的密度值又容易被PCA在前几个主成分中平滑掉。
查准率的含义是:模型预测为某个类别的样本中,有多少是真的属于这个类别。查全率的含义是:真实属于某个类别的样本中,有多少被模型正确找出来了。这两个指标通常是此消彼长的关系,在决策树中可以通过调整分类阈值来平衡。但在MATLAB默认的fitctree预测流程里,输出的是硬标签,不会给出类别概率阈值调整的接口,所以实操中更多是用来定位问题,而不是调参。
5. 实测精度、踩坑记录与可能的优化方向
5.1 在MNIST子集上的实测结果
我用的是MNIST手写数字数据集中的一部分:每类抽取400张,共4000张作为训练集;每类另抽取100张,共1000张作为测试集。做这个抽样式处理的原因是数据量不必太大,决策树本身不是数据饥饿型模型,几千个样本足够训练出稳定的树,而且能保持整个项目在普通笔记本上几分钟内跑完。
实验结果如下:
| 方案 | 训练集准确率 | 测试集准确率 | 5折交叉验证准确率 |
|---|---|---|---|
| 25维原始特征 + 决策树(不限制深度) | 98.7% | 83.2% | 84.1% |
| 25维原始特征 + 决策树(限制深度) | 90.4% | 87.8% | 88.3% |
| PCA降至15维 + 决策树(限制深度) | 89.6% | 88.5% | 89.1% |
| PCA降至12维 + 决策树(限制深度) | 89.1% | 88.2% | 88.7% |
从这个表可以看出两个关键信息:第一,不限制树的深度时,训练集准确率接近99%,但测试集只有83%左右,过拟合非常典型;第二,PCA将特征从25维降到15维后,测试集准确率比不降维还高了一点,证明末尾的主成分确实以噪声为主。
5.2 我踩过的几个印象深刻的坑
第一个坑是二值化方向搞反。MATLAB的imbinarize函数默认输出逻辑值,背景为0、前景为1。我最初没有取反,直接对这个结果做连通域分析和分块统计,导致黑色笔画区域被当成背景,背景区域被当成前景,分块特征全乱了。后来我把处理后的图像用imshow逐个显示,才发现问题。
第二个坑是定位外接矩形的时候,如果二值化后的图像里数字周围有零星噪点,这些噪点会被当成独立的连通域。我前面写的是取面积最大的连通域,但如果噪点恰好和数字粘连在一起,就会导致外接矩形比实际数字大出一圈,resize之后数字在图像中的占比变小,分块特征整体偏低。这个问题的解决办法是先用形态学开运算去掉这类小突起:
matlab复制bw_clean = imopen(bw, strel('disk', 2));
第三个坑是图像归一化时没有做质心对齐。手写数字如果偏向画布的左上角,和偏向中心的同一个数字,特征向量差异会很大。我加入了一步质心对齐——计算数字像素的质心,平移到画布中心:
matlab复制stats_centroid = regionprops(bw_clean, 'Centroid');
centroid = stats_centroid(1).Centroid;
% 计算偏移量并平移图像
shift_x = round(size(bw_clean, 2) / 2 - centroid(1));
shift_y = round(size(bw_clean, 1) / 2 - centroid(2));
bw_aligned = imtranslate(bw_clean, [shift_x, shift_y]);
这一步对齐之后,模型的交叉验证准确率大约提升了0.8%到1.2%,效果还是很明显的。
第四个坑是关于测试集PCA投影的。这个前面已经详细说过,如果测试集单独做pca,最终结果会虚高,而且这个虚高是“错误的高”,过程完全不可复现。遇到模型表现异常好的时候,先检查是不是代码里的投影方式写错了。
5.3 在真实手写图片上的测试流程
为了验证模型的鲁棒性,我没有只停留在MNIST数据上,还用手机拍了几张手写数字,走了一遍完整流程:
手机拍照得到彩色图像 → 转灰度 → Otsu二值化 → 形态学去噪 → 连通域定位 → 裁剪外接矩形 → resize到20×20 → 5×5分块得到25维特征 → 用训练集的mean和std标准化 → 乘上训练集的coeff矩阵降到15维 → 输入决策树进行预测。
这一步暴露了一个MNIST上没有的问题:手机拍照的图片背景更复杂,光线不均匀,二值化之后容易有大片浅色噪点。我处理的方法是先对灰度图做了中值滤波,再用adaptiveThreshold做局部自适应阈值,效果比全局Otsu更好。不过MATLAB没有内置的adaptiveThreshold,我是自己参考OpenCV的思路实现的滑动窗口阈值,这里就不展开详述了。
5.4 继续提升精度的几种思路
如果你做到这一步还想要更高精度,有几条路可以走,而且都不需要引入太复杂的框架。
第一种是特征增强。在25维分块特征基础上,补充描述笔画方向的梯度特征,或者利用细化算法提取字符骨架后统计骨架分支点数量。这些特征与像素密度特征相关性低,能提供互补信息。
第二种是模型集成。把单棵决策树换成随机森林,MATLAB里一行代码从fitctree改成fitcensemble并指定'Method', 'Bag'就行。随机森林通过bagging策略训练多棵树并对结果投票,能显著降低决策树过拟合的风险,在同样的15维PCA特征下,通常能把精度再提升3到5个百分点。
第三种是针对混淆对做二分类优化。比如观察到“4”和“9”容易混淆,可以单独训练一个区分“4”和“9”的二分类器,在决策树输出为这两个类别之一时,再交给二分类器做二次判断。
如果让我重做一次这个项目,我会在论文里重点突出“特征工程带来的可解释性”:每个5×5分块对应数字的哪个部位、PCA每个主成分大致对应什么样的笔画分布模式、决策树的关键分裂节点在哪个分块区域。这些分析都是深度学习模型很难给出的,却恰恰是决策树路线最有价值的地方。
