简介:这份资源围绕基于支持向量机(SVM)的手写字体识别展开,面向计算机视觉与机器学习入门者、课程设计或实验项目开发者,帮助理解从图像预处理、特征提取到分类器训练与评估的完整流程。压缩包共85个文件,约149KB,包含50张jpg与30张bmp手写数字样本图片,可用于训练与测试;2个m脚本文件承担预处理与识别主逻辑,另有2张png与1个html页面辅助展示识别效果。已有262人学习下载,说明该主题在入门实践中具有一定关注度。资源以手写数字样本和MATLAB脚本为核心,读者可据此复现图像去噪、二值化、轮廓提取、HOG或PCA特征提取,并搭建SVM分类器,同时结合章节内容比较不同策略的识别表现,适合作为课程实验或算法练手的参考素材。
1. 从一张 28×28 的手写数字图说起:这套 SVM 识别资源到底能跑出什么
很多人第一次接触手写字体识别,都是从 MNIST 那种规整数据集开始的,图片居中、笔画粗细一致,跑个 99% 的准确率并不稀奇。但真到自己拿手机拍一张作业本上的数字,或者扫描一份手写表格,识别率立刻掉到六七成,这时候才发现问题不在 SVM 本身,而在预处理和特征这一整条链路。这份基于 SVM 的手写字体识别资源,恰好把这条链路完整地摆了出来:pic_preprocess.m负责图像预处理,Chapter_CharacterRecognitionUsingLibsvm.m负责调用 LibSVM 完成训练与识别,配套的numX_Y.bmp和numX_Y.jpg分别是测试样本和训练样本,还有一份 HTML 说明页把整个流程串起来。它适合两类人:一类是正在做课程设计或毕设、需要一套能跑通的手写数字识别代码的人;另一类是想搞清楚 SVM 在图像分类里到底怎么落地、参数怎么调、预处理为什么比模型更重要的工程师。下面我按自己拆包复现的顺序,把这份资源从环境到调参完整走一遍。
2. 拆开 chapter19.zip:文件结构、样本组织与 LibSVM 调用链
2.1 资源里到底有什么,各自扮演什么角色
先把压缩包解开,目录结构并不复杂,但每个文件的用途需要分清楚,否则很容易把训练样本和测试样本搞混。资源里的图片命名规则是numX_Y,其中 X 代表数字类别(0 到 9),Y 代表同一类别的第几个样本。测试样本用的是.bmp格式,训练样本用的是.jpg格式,这个差异不是随便定的,后面预处理那章会讲到为什么。
| 文件/目录 | 类型 | 作用 |
|---|---|---|
pic_preprocess.m | MATLAB 脚本 | 图像去噪、二值化、尺寸归一化、特征向量生成 |
Chapter_CharacterRecognitionUsingLibsvm.m | MATLAB 脚本 | 读取特征、构建标签、调用 LibSVM 训练与预测 |
num0_1.bmp~num9_3.bmp | 测试样本 | 每类 3 张,共 30 张,用于验证识别效果 |
num0_1.jpg~num9_5.jpg | 训练样本 | 每类 5 张,共 50 张,用于模型训练 |
Chapter_CharacterRecognitionUsingLibsvm.html | 说明文档 | 流程说明与结果截图 |
Chapter_CharacterRecognitionUsingLibsvm.png | 结果图 | 识别结果可视化 |
训练样本每类 5 张、测试样本每类 3 张,这个量级很小,所以这份资源的核心价值不在于刷高准确率,而在于把「预处理 → 特征 → SVM 训练 → 预测」这条链路用最少的代码跑通。你完全可以把这套流程迁移到更大的数据集上,比如自己采集几百张手写数字,替换掉numX_Y的命名和目录即可。
2.2 LibSVM 在 MATLAB 里的调用方式与版本选择
资源用的是 LibSVM 而不是 MATLAB 自带的fitcsvm,这一点值得说一下。LibSVM 是台湾林智仁教授团队维护的经典 SVM 库,支持多分类、核函数切换、交叉验证,接口在 MATLAB 里就是svmtrain和svmpredict两个函数。常见做法是把 LibSVM 编译成 MATLAB 的 mex 文件,然后把所在目录加到路径里。
% 把 LibSVM 的 matlab 目录加入搜索路径 addpath('D:\libsvm-3.24\matlab'); % 验证 mex 文件是否可用,能输出版本信息说明编译成功 svmtrain([], []);这里有个坑:LibSVM 的svmtrain和 MATLAB 自带的svmtrain(旧版本)或fitcsvm(新版本)名字冲突。如果你用的是 R2018b 之后的 MATLAB,自带函数已经改名,冲突会少一些,但路径顺序仍然要注意,LibSVM 的路径必须放在前面。参数说明上,svmtrain的核心参数是-s(SVM 类型,0 是 C-SVC)、-t(核函数类型,0 线性、2 高斯)、-c(惩罚系数 C)、-g(高斯核的 gamma)。这份资源默认走的是 C-SVC + 高斯核的组合,这也是手写数字识别里最常用的配置。
2.3 从图片到特征向量:预处理脚本的输入输出约定
pic_preprocess.m的输出必须和Chapter_CharacterRecognitionUsingLibsvm.m的输入对齐,否则训练阶段直接报维度错误。我一般会先单独跑预处理脚本,确认生成的特征矩阵行数等于样本数、列数等于特征维度。
% 读取一张测试样本并做预处理 img = imread('num3_2.bmp'); % 转灰度,防止彩色图导致后续二值化异常 if size(img, 3) == 3 img = rgb2gray(img); end % 二值化,阈值用 Otsu 自适应 level = graythresh(img); bw = im2bw(img, level); % 尺寸归一化到 28x28,和训练样本保持一致 bw = imresize(bw, [28 28]); % 展平成 1x784 的特征向量,double 类型 feature = double(bw(:))';这段代码的逻辑是:先统一灰度,再用 Otsu 阈值做二值化,然后缩放到固定尺寸,最后展平。参数上,graythresh返回的是归一化阈值,im2bw用它做二值化;imresize的目标尺寸必须和训练阶段完全一致,否则特征维度对不上。注意bw(:)是按列优先展平的,MATLAB 默认就是列优先,所以训练和测试只要都用同样的展平方式,顺序就是一致的。
3. 预处理与特征工程:为什么二值化和归一化决定了识别上限
3.1 去噪、二值化、尺寸归一化的先后顺序
预处理这三步的顺序不能乱。我见过有人先缩放再去噪,结果噪声也被缩放得面目全非,二值化之后噪声变成了一块块黑斑,直接污染特征。正确的顺序是:先去噪,再二值化,最后归一化尺寸。
% 中值滤波去噪,窗口大小 3x3 img_denoised = medfilt2(img, [3 3]); % 二值化 bw = im2bw(img_denoised, graythresh(img_denoised)); % 尺寸归一化 bw = imresize(bw, [28 28]); % 可选:形态学开运算去掉孤立噪点 bw = bwareaopen(bw, 5);medfilt2的中值滤波对椒盐噪声特别有效,窗口用 3×3 就够了,太大反而会把笔画细节抹掉。bwareaopen用来删除面积小于 5 像素的连通区域,这一步对扫描件里的孤立噪点很有用。参数上,bwareaopen的阈值要根据图片分辨率调,28×28 的图用 5 比较合适,如果原图是 256×256,这个值要相应放大。
3.2 特征提取:HOG、PCA 与直接展平的取舍
这份资源走的是直接展平像素作为特征的路线,也就是 28×28=784 维的原始像素。这个做法简单,但对书写风格差异很敏感。常见做法是换成 HOG 特征,它对边缘方向敏感,对光照和小幅位移更鲁棒。
% 提取 HOG 特征,cellSize 用 4x4 [hog_feat, ~] = extractHOGFeatures(bw, 'CellSize', [4 4]); % hog_feat 维度约为 1764,比原始像素更紧凑extractHOGFeatures的CellSize决定每个 cell 的大小,4×4 在 28×28 的图上会得到 7×7 个 cell,每个 cell 9 个方向 bin,再考虑 block 归一化,最终维度在 1764 左右。如果你用 PCA 降维,可以在 HOG 之后再做,把维度压到 100 以内,训练速度会明显提升。但要注意,PCA 的投影矩阵必须用训练集拟合,然后同时应用到测试集,否则就是数据泄露,测试准确率会虚高。
3.3 训练集与测试集的划分陷阱
资源里训练样本是.jpg、测试样本是.bmp,这个格式差异本身就是一个隐藏的坑。JPEG 是有损压缩,会在笔画边缘产生振铃效应,而 BMP 是无损的。如果你直接用同一套预处理参数处理两种格式,JPEG 样本的二值化结果会比 BMP 样本更毛糙,导致训练和测试的分布不一致。
% 统一转成灰度后再做后续处理,避免格式差异带来的通道数问题 train_img = imread('num7_4.jpg'); train_gray = rgb2gray(train_img); test_img = imread('num7_1.bmp'); test_gray = rgb2gray(test_img); % 两者都走同一套预处理函数 train_feat = preprocess(train_gray); test_feat = preprocess(test_gray);我一般会写一个preprocess函数,把去噪、二值化、归一化、展平全部封装进去,训练和测试都调它,保证处理逻辑完全一致。参数上,如果发现 JPEG 样本的二值化效果差,可以在去噪阶段把中值滤波窗口调大一点,或者改用高斯滤波先做一次平滑。
4. 训练与调参:C、gamma 和交叉验证怎么设才不翻车
4.1 LibSVM 训练命令的完整参数拆解
训练阶段的核心就是一行svmtrain,但参数怎么设直接决定模型是能用还是废掉。
% 假设 train_label 是 50x1 的标签向量,train_feat 是 50x784 的特征矩阵 % -s 0 表示 C-SVC,-t 2 表示高斯核,-c 10 是惩罚系数,-g 0.01 是 gamma % -v 5 表示 5 折交叉验证,只返回准确率不生成模型 model = svmtrain(train_label, train_feat, '-s 0 -t 2 -c 10 -g 0.01 -v 5');-c控制对误分类的惩罚力度,C 越大越容易过拟合,C 越小越容易欠拟合。-g是高斯核的 gamma,gamma 越大,单个样本的影响范围越小,决策边界越复杂。这两个参数是联动的,常见做法是用网格搜索找最优组合。-v 5做 5 折交叉验证,返回的是交叉验证准确率,不生成模型文件,适合调参阶段用。调完参之后去掉-v,才会生成真正的 model。
4.2 网格搜索找最优 C 和 gamma 的实操脚本
手动试参数效率太低,我一般写个双层循环做网格搜索。
best_acc = 0; best_c = 0; best_g = 0; for c = [0.1 1 10 100] for g = [0.001 0.01 0.1 1] acc = svmtrain(train_label, train_feat, ... sprintf('-s 0 -t 2 -c %g -g %g -v 5 -q', c, g)); if acc(1) > best_acc best_acc = acc(1); best_c = c; best_g = g; end end end fprintf('最优 C=%g, gamma=%g, 交叉验证准确率=%.2f%%\n', best_c, best_g, best_acc);sprintf用来动态拼接参数字符串,-q是 quiet 模式,不打印训练过程,网格搜索时能省不少屏幕输出。acc(1)取的是交叉验证准确率,LibSVM 返回的是一个向量,第一个元素就是准确率。参数上,C 的搜索范围从 0.1 到 100,gamma 从 0.001 到 1,这个范围对 784 维的原始像素特征基本够用。如果你的特征维度更高,gamma 的上界可以再放大。
4.3 多分类策略:一对一还是一对多
LibSVM 默认用一对一(one-vs-one)策略做多分类,10 个数字会产生 45 个二分类器。这个策略在类别数不多的时候效果很好,但训练时间会随类别数平方增长。如果你要识别的不只是数字,而是几百个汉字类别,一对一就不太合适了,常见做法是改用一对多(one-vs-rest)或者直接上线性分类器。
% 查看模型的支持向量数量,判断模型复杂度 model = svmtrain(train_label, train_feat, '-s 0 -t 2 -c 10 -g 0.01'); fprintf('支持向量总数:%d\n', model.totalSV); fprintf('类别数:%d\n', model.nr_class);model.totalSV是支持向量总数,如果这个数接近训练样本总数,说明模型几乎记住了所有样本,过拟合风险很高。model.nr_class是类别数,10 个数字应该输出 10。参数上,如果发现支持向量太多,优先调小 C 或者调小 gamma,让决策边界更平滑。
5. 避坑与排查:这套代码最容易翻车的五个地方
5.1 现象:训练时报「标签向量维度不匹配」
原因通常是标签向量的行数和特征矩阵的行数不一致。资源里的训练样本是 50 张,标签也必须是 50×1,如果你手动改了样本数量但忘了改标签,就会报这个错。解决方法是先打印size(train_label)和size(train_feat),确认第一个维度相等。
5.2 现象:预测结果全是同一个类别
这通常是因为特征没有归一化,或者 gamma 设得太大导致所有测试样本都被判成同一类。解决方法是先把特征缩放到 [0,1] 或 [-1,1],再把 gamma 调小一个数量级重新训练。LibSVM 官方也建议对特征做缩放,原始像素值虽然是 0 和 1,但如果你换成 HOG 特征,数值范围会大很多,不缩放基本没法用。
5.3 现象:交叉验证准确率很高,但测试准确率很低
这是典型的数据泄露。常见原因是 PCA 投影矩阵用了全部数据拟合,或者归一化参数用了测试集的统计量。解决方法是把所有拟合操作都限制在训练集上,测试集只能用训练集得到的参数做变换。我一般会在代码里显式注释「以下操作仅用训练集拟合」,防止自己后面改代码时搞混。
5.4 现象:LibSVM 的 svmtrain 和 MATLAB 自带函数冲突
MATLAB 旧版本的svmtrain和 LibSVM 的svmtrain同名,调用时可能走到自带函数上,报参数错误。解决方法是把 LibSVM 路径放在搜索路径最前面,或者用which svmtrain确认当前调用的是哪个文件。如果还是冲突,可以把 LibSVM 的函数名改成libsvmtrain,但这样要改所有调用处,比较麻烦。
5.5 现象:JPEG 训练样本和 BMP 测试样本识别率差异大
前面提过,JPEG 有损压缩会让笔画边缘变毛糙,二值化后笔画粗细不一致。解决方法是统一预处理参数,或者在训练阶段只用 BMP 样本,把 JPEG 样本也转成 BMP 再训练。如果样本量够,最好统一采集格式,避免混用。
6. 把识别率再往上推一档:特征融合与结果验证的实操技巧
这套资源跑通之后,识别率大概在 80% 到 90% 之间,取决于你的预处理参数和 C、gamma 的取值。想再往上推,我一般会做两件事:一是特征融合,把原始像素和 HOG 特征拼在一起,让 SVM 同时看到灰度信息和边缘方向信息;二是做结果验证,把预测错的样本单独挑出来看,分析是预处理问题还是特征问题。
% 特征融合:原始像素 + HOG raw_feat = double(bw(:))'; [hog_feat, ~] = extractHOGFeatures(bw, 'CellSize', [4 4]); fused_feat = [raw_feat, hog_feat]; % 注意:融合后维度变高,gamma 要相应调小 model = svmtrain(train_label, train_feat_fused, '-s 0 -t 2 -c 10 -g 0.005');融合之后特征维度从 784 涨到 2500 左右,gamma 要调小,否则单个样本的影响范围太窄,决策边界会碎掉。我一般会把 gamma 除以特征维度的增长比例,比如维度涨了 3 倍,gamma 就除以 3 左右,然后再微调。
验证环节,我会把预测错的样本单独存出来,用imwrite写到error_samples目录,然后一张张看。
[pred_label, acc, ~] = svmpredict(test_label, test_feat, model); error_idx = find(pred_label ~= test_label); for i = 1:length(error_idx) idx = error_idx(i); imwrite(test_img_all{idx}, sprintf('error_samples/err_%d_true%d_pred%d.bmp', ... idx, test_label(idx), pred_label(idx))); endsvmpredict返回三个值:预测标签、准确率向量、决策值。error_idx找出预测错的索引,然后把对应图片存出来,文件名里带上真实标签和预测标签,方便快速定位问题。参数上,test_img_all是预处理前的原始图片元胞数组,需要在预处理阶段就存好,否则后面拿不到原图。
从那以后我每次跑手写识别,都会强制走一遍「预处理可视化 → 交叉验证调参 → 错误样本分析」这三步,少一步心里就没底。这套 SVM 手写字体识别资源虽然样本量小,但链路完整,把预处理、特征、训练、调参、排查都串起来了,拿来改造成自己的项目或者当课程设计的底子都够用。希望帮到你。
本文还有配套的精品资源,点击获取