
简介这份资源围绕基于支持向量机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 到 9Y 代表同一类别的第几个样本。测试样本用的是.bmp格式训练样本用的是.jpg格式这个差异不是随便定的后面预处理那章会讲到为什么。文件/目录类型作用pic_preprocess.mMATLAB 脚本图像去噪、二值化、尺寸归一化、特征向量生成Chapter_CharacterRecognitionUsingLibsvm.mMATLAB 脚本读取特征、构建标签、调用 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的核心参数是-sSVM 类型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×28784 维的原始像素。这个做法简单但对书写风格差异很敏感。常见做法是换成 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是高斯核的 gammagamma 越大单个样本的影响范围越小决策边界越复杂。这两个参数是联动的常见做法是用网格搜索找最优组合。-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 到 100gamma 从 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 手写字体识别资源虽然样本量小但链路完整把预处理、特征、训练、调参、排查都串起来了拿来改造成自己的项目或者当课程设计的底子都够用。希望帮到你。本文还有配套的精品资源点击获取