ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Matlab的CNN手写汉字识别系统设计与实现

基于Matlab的CNN手写汉字识别系统设计与实现 手写汉字识别是图像分类任务里很有代表性的一类问题。与数字和字母不同汉字类别多、结构复杂、相似字形多单纯依赖传统特征工程做识别效果往往不稳定。用 Matlab 搭建卷积神经网络CNN来识别手写汉字是一个适合课程设计、毕业设计和工程入门练习的完整项目。它把数据准备、图像预处理、网络设计、模型训练、结果评估和交互界面串在一起既能体现深度学习的核心流程又能直接展示一个看得见结果的系统。这个项目的核心链路可以写成准备手写汉字图像数据 - 用 Matlab 深度学习工具箱构建 CNN - 训练并评估模型 - 导出模型文件 - 在交互界面中手写输入并实时识别。项目还支持增加新的汉字含义、扩充类别并重新训练所以不是只能跑通演示的一次性代码。后续如果想把识别范围从若干个汉字扩展到更多类别只需要按同样的流程补数据、调网络、再训练即可。1. 为什么手写汉字识别要选择卷积神经网络1.1 手写汉字识别的难点在哪里手写汉字识别和印刷体汉字识别不同。印刷体字迹规范、笔画清晰、字体统一传统模板匹配和特征工程就能处理得很好。手写汉字的难点在于同一个字的形态变化非常大不同人的书写习惯不同笔画长短、倾斜角度、连笔程度、笔顺都有差异甚至同一人不同时间写同一个字也会不一样。汉字本身还有很强的结构特征。很多汉字由多个部件组成例如“明”由“日”和“月”组成“好”由“女”和“子”组成。这些部件在不同汉字中会重复出现但组合方式不同。如果只用全局像素特征去匹配模型很难抓住这种结构规律如果人工设计特征又需要大量图像处理经验而且换一套字体或书写风格后可能失效。所以手写汉字识别本质上面临两个问题一是类内差异大二是类别数量多。这决定了模型必须能够自动学习到分层级的特征表示低层学习边缘、笔画之类的局部纹理高层组合成部件、结构等语义信息。1.2 CNN 如何解决特征提取与平移鲁棒性问题卷积神经网络Convolutional Neural NetworkCNN是专门为图像类数据设计的深度神经网络。它通过卷积核在图像上滑动来提取局部特征通过池化操作降低特征图尺寸、增强平移不变性通过多层叠加逐步扩大感受野从而让网络从“笔画”学到“部件”再学到“整个字形”。相比传统的全连接网络CNN 有三个明显优势参数共享同一个卷积核在所有位置共用参数量大幅减少。局部感受野每个神经元只关注输入图像的局部区域更符合图像局部的空间相关性。层级特征浅层提取边缘、纹理深层提取部件、结构这对汉字这种结构化图像非常重要。在 Matlab 中深度学习工具箱提供了完整的 CNN 构建函数convolution2dLayer、batchNormalizationLayer、reluLayer、maxPooling2dLayer、fullyConnectedLayer、classificationLayer等模块可以直接串联成网络结构。训练过程中可以使用trainingOptions配置学习率、批次大小、验证频率等超参数并通过trainNetwork启动训练。1.3 Matlab 实现 CNN 的技术路线在 Matlab 中做手写汉字识别通常采用的流程是准备图像数据集按类别存放图像统一尺寸。使用imageDatastore读取图像和标签。划分训练集、验证集和测试集。设计 CNN 网络结构。配置训练参数并训练模型。在测试集上评估准确率输出混淆矩阵。保存训练好的模型。编写 GUI 界面实现手写输入、预处理和实时识别。整个流程不需要自己实现卷积、反向传播等底层算法Matlab 已经把训练过程封装好开发者重点放在数据组织、网络设计和结果分析上。这也正是“Matlab 基于 CNN 卷积神经网络手写汉字识别系统”这个项目的核心价值用最短的工程代码验证一个完整的深度学习任务。2. 环境准备与数据组织先搭好能反复实验的工程目录2.1 Matlab 版本与工具箱检查训练 CNN 依赖的 Matlab 工具箱主要是 Deep Learning Toolbox建议使用 R2021a 以上版本新版本对imageDatastore、augmentedImageDatastore和训练过程可视化的支持更好。在运行代码之前先用下面命令检查工具箱是否可用ver(deep)如果当前环境没有安装 Deep Learning Toolbox可以联系学校或单位获取合适的授权方式或使用官方试用版在本地学习环境验证。不同 Matlab 版本对部分 API 有细微差异。例如trainingOptions中的Plots参数在 R2018b 之后才稳定支持augmentedImageDatastore的输入尺寸参数在不同版本也略有区别。旧版本跑不通时优先查看当前版本帮助文档doc trainingOptions doc augmentedImageDatastore2.2 数据集准备与类别目录规划这个项目支持“增加其它含义、代码可以重新训练”所以数据集目录结构直接影响后续扩展。推荐按类别建立文件夹每个文件夹放一类汉字的手写图片目录结构如下data/ chinese_handwritten/ yi/ % 对应汉字“一” 001.png 002.png ... er/ % 对应汉字“二” 001.png 002.png ... san/ % 对应汉字“三” 001.png 002.png ...这里有一个很容易踩的坑Matlab 的imageDatastore会把第一层子文件夹名作为类别标签。如果直接用中文“一”“二”“三”作为文件夹名在简体中文 Windows 上多数情况可以识别但在部分 Linux 环境或旧版本 Matlab 中容易出现编码问题。更稳妥的做法是文件夹名使用拼音、数字或英文别名例如yi、er、san。单独维护一个类别映射表把拼音和真实汉字关联起来。映射表示例labelMap table( ... string({yi,er,san,si,wu}), ... string({一,二,三,四,五}), ... VariableNames, {ClassName, ChineseName});这样即使不同系统对中文文件名处理不一致程序内部仍然可以使用稳定的英文字符串作为类别标识展示给用户时再用中文。如果原始材料没有提供现成数据集学习阶段可以自建一个小型数据集。例如邀请多位同学在固定大小的白纸上书写常用汉字扫描或拍照后切分成单字图片图像尺寸统一为 64×64 或 128×128。也可以使用公开的手写汉字数据库但要注意公开数据集常见为学术研究用途落地到课程设计时先确认授权方式和具体要求。2.3 图像预处理统一尺寸、灰度化和归一化CNN 要求输入的图像尺寸固定。真实采集的手写汉字图片可能来自相机、扫描仪或屏幕截图尺寸和背景差异很大所以要在数据加载阶段做预处理。Matlab 的imageDatastore支持自定义读取函数。可以在读取阶段完成灰度化、缩放和归一化function img preprocessHanzi(filename) img imread(filename); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, [64 64]); img im2double(img); % 归一化到 [0,1]保持单通道 img reshape(img, 64, 64, 1); end将这个函数挂载到数据读取器上imds imageDatastore(data/chinese_handwritten, ... IncludeSubfolders, true, ... LabelSource, foldernames, ... ReadFcn, preprocessHanzi);预处理要解决的核心问题有三个尺寸统一网络输入层是固定大小的图像。通道统一灰度图是单通道彩色图是三通道不能混用。数值范围统一浮点输入通常归一化到 [0,1] 区间避免梯度更新不稳定。注意如果采集的图像是白底黑字有些网络对黑白极性敏感。可以在预处理阶段统计图像均值必要时把前景和背景翻转成统一风格。3. 构建 CNN 模型并完成训练3.1 用 imageDatastore 加载图片与划分数据集数据组织完成后可以用imageDatastore统一管理图片路径和标签。划分数据集时常见做法是按样本比例随机划分rng(42); % 固定随机种子保证复现 [trainImds, valImds, testImds] splitEachLabel(imds, 0.7, 0.15, 0.15, randomized);划分后的三个数据集的标签和图片数量可以通过下面命令检查countEachLabel(trainImds) numel(trainImds.Files)这里要区分两种随机划分方式按样本随机划分简单但同一张图片可能同时出现在训练集和测试集中的近邻位置容易造成评估结果偏高。按书写者或书写批次划分更严格能反映模型面对陌生人笔迹时的泛化能力。在课程设计中如果数据量有限且没有作者信息可以先用随机划分跑通流程再人工查看测试集中的错分样本判断模型是否存在“记答案”的嫌疑。3.2 设计适合汉字识别的网络结构网络结构要兼顾识别能力和训练成本。汉字图像是 64×64 的单通道灰度图类别数开始时可能是 5 类或 10 类后续扩展到几十类。一个适合中小规模手写汉字识别的网络结构如下inputSize [64 64 1]; numClasses numel(categories(trainImds.Labels)); layers [ imageInputLayer(inputSize, Name, input, Normalization, none) convolution2dLayer(3, 32, Padding, same, Name, conv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) maxPooling2dLayer(2, Stride, 2, Name, pool1) convolution2dLayer(3, 64, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) reluLayer(Name, relu2) maxPooling2dLayer(2, Stride, 2, Name, pool2) convolution2dLayer(3, 128, Padding, same, Name, conv3) batchNormalizationLayer(Name, bn3) reluLayer(Name, relu3) maxPooling2dLayer(2, Stride, 2, Name, pool3) fullyConnectedLayer(256, Name, fc1) reluLayer(Name, relu_fc) dropoutLayer(0.5, Name, dropout) fullyConnectedLayer(numClasses, Name, fc_out) softmaxLayer(Name, softmax) classificationLayer(Name, output)];这个结构里几个设计点值得解释卷积核大小选 3×3是实践证明性价比很高的选择。多个 3×3 卷积堆叠可以扩大感受野同时参数量比大卷积核更少。每个卷积层后面加 Batch Normalization可以缓解深层网络训练时梯度不稳定问题加快收敛。池化层每隔一个阶段降低特征图分辨率空间尺寸从 64 变为 32、16、8最终交给全连接层的特征维度可控。全连接层前加入 Dropout随机丢弃一部分神经元降低过拟合风险。如果想快速跑通可以先用一个更小的网络例如只保留两组卷积池化再加一个全连接层训练时间更短。但识别效果通常会比三组卷积的结构弱。3.3 训练选项配置与训练过程训练选项决定了模型能否稳定收敛。下面是一组适合该任务的初始配置options trainingOptions(sgdm, ... InitialLearnRate, 0.01, ... MaxEpochs, 30, ... MiniBatchSize, 32, ... ValidationData, valImds, ... ValidationFrequency, 10, ... Shuffle, every-epoch, ... Plots, training-progress, ... Verbose, true);训练参数的作用如下参数含义初始推荐值调优方向InitialLearnRate初始学习率0.01损失震荡时调小为 0.001收敛慢时可适当调大MaxEpochs训练轮数30数据量大时可减少数据少时可增加MiniBatchSize批次大小32显存不足时调小为 16 或 8ValidationFrequency验证频率10表示每 10 次迭代验证一次Shuffle数据打乱方式every-epoch每个 epoch 重新打乱避免模型记住批次顺序启动训练net trainNetwork(trainImds, layers, options);训练过程中应观察两点训练损失是否逐渐下降。验证准确率是否同步提升。如果训练损失下降但验证准确率停在很低水平说明模型过拟合或数据划分有问题如果训练损失和验证损失都在波动说明学习率可能过大。4. 模型评估、单字测试与模型导出4.1 训练曲线怎么看Matlab 训练界面会显示训练准确率、验证准确率、训练损失和验证损失四条曲线。理想的曲线是准确率整体上升最后趋于平缓。损失整体下降最后趋于平缓。训练准确率和验证准确率差距不大。如果训练准确率很高但验证准确率明显低属于典型过拟合。此时优先调整方向不是盲目加深网络而是增加数据、加入数据增强、增大 Dropout或减少全连接层神经元数量。% 训练结束后在测试集上快速评估 preds classify(net, testImds); testLabels testImds.Labels; acc mean(preds testLabels); fprintf(测试集准确率%.2f%%\n, acc * 100);4.2 混淆矩阵与错误样本分析准确率只能反映整体情况混淆矩阵能看出每个类别之间容易混淆的程度。用confusionchart可以直观展示figure; confusionchart(testLabels, preds);对于手写汉字识别混淆矩阵常见问题集中在结构相似的字例如“日”和“目”“干”和“于”“人”和“入”“大”和“太”如果某两个类别频繁混淆需要专门增强这一类样本或者在预处理器中补充更能体现笔画差异的操作例如细化笔迹宽度。查看具体的错分样本也是必要步骤。可以遍历测试集找出预测错误并打印文件名、真实标签和预测标签idxWrong find(preds ~ testLabels); for i 1:min(10, numel(idxWrong)) k idxWrong(i); fprintf(文件%s真实标签%s预测标签%s\n, ... testImds.Files{k}, string(testLabels(k)), string(preds(k))); end4.3 如何保存和重新加载模型训练好的net是一个SeriesNetwork或DAGNetwork对象可以直接保存到 mat 文件中save(hanziModel.mat, net, labelMap);保存时建议同时保存labelMap否则以后重新加载模型后还要手动创建类别映射容易出错。重新加载并调用data load(hanziModel.mat); net data.net; labelMap data.labelMap;注意保存模型只保存网络结构和权重不保存训练数据。如果换机器使用时遇到加载报错先确认 Matlab 版本和工具箱一致。5. 手写汉字识别演示界面5.1 界面布局与交互流程模型训练完成后需要提供一个直观的手写演示入口。常见做法是做一个简单 GUI左侧是手写画板用户用鼠标写汉字。右侧是“识别”和“清除”按钮。下方显示识别结果和置信度。交互流程是用户在画板上拖动鼠标写下一个字 - 点击识别按钮 - 程序把画板内容转换成 64×64 灰度图 - 调用classify得到预测标签 - 通过labelMap显示中文结果。这里以传统figure为例实现兼容性比较稳定。Matlab 不同版本对 GUI 组件风格的默认样式有差异但核心逻辑一致。5.2 手写笔迹捕获与预处理简化实现可以这样设计先在坐标轴上收集鼠标点再把这些点绘制成曲线图像最后用getframe抓取画板区域。function demoDrawUI(net, labelMap) fig figure(Name, 手写汉字识别演示, NumberTitle, off, ... Position, [200 200 800 560]); ax axes(fig, Position, [0.08 0.12 0.55 0.76]); axis(ax, [0 10 0 10]); axis(ax, equal); hold(ax, on); grid(ax, on); title(ax, 用鼠标在此区域手写汉字); btnRecognize uicontrol(fig, Style, pushbutton, ... String, 识 别, ... Position, [560 320 200 50], ... FontSize, 14, ... Callback, (src, evt) recognizeCallback()); btnClear uicontrol(fig, Style, pushbutton, ... String, 清 除, ... Position, [560 240 200 50], ... FontSize, 14, ... Callback, (src, evt) clearCallback()); resultLabel uicontrol(fig, Style, text, ... String, 识别结果, ... Position, [300 20 420 50], ... FontSize, 18, ... BackgroundColor, get(fig, Color)); points []; lineObj []; set(fig, WindowButtonDownFcn, (src, evt) beginDraw()); set(fig, WindowButtonMotionFcn, (src, evt) moveDraw()); set(fig, WindowButtonUpFcn, (src, evt) endDraw()); function beginDraw() cp get(ax, CurrentPoint); points cp(1, 1:2); lineObj plot(ax, points(1), points(2), k-, LineWidth, 4); end function moveDraw() if isempty(points) return; end cp get(ax, CurrentPoint); points(end1, :) cp(1, 1:2); set(lineObj, XData, points(:, 1), YData, points(:, 2)); end function endDraw() points []; end function clearCallback() cla(ax); title(ax, 用鼠标在此区域手写汉字); set(resultLabel, String, 识别结果); end function recognizeCallback() frame getframe(ax); img frame.cdata; grayImg rgb2gray(img); grayImg imresize(grayImg, [64 64]); grayImg im2double(grayImg); grayImg reshape(grayImg, [64 64 1]); [pred, scores] classify(net, grayImg); [maxScore, idx] max(scores); chineseName labelMap.ChineseName(labelMap.ClassName string(pred)); set(resultLabel, String, sprintf(识别结果%s置信度%.2f%%, ... chineseName, maxScore * 100)); end end这段代码里有几个细节需要说明getframe抓取的是坐标轴区域图像包含坐标轴刻度和网格线。本示例先保留网格线实际上线框和文字也会进入图像影响识别结果。更严谨的做法是在抓取前隐藏坐标轴刻度、网格线和标题识别完成后再恢复。% 在抓取前隐藏坐标轴装饰 set(ax, XTick, [], YTick, [], XColor, white, YColor, white); frame getframe(ax);这样抓出来的图像更接近纯手写笔迹。5.3 识别结果展示classify的返回值是类别标签scores是每个类别的置信度向量。为了展示真实汉字需要借助labelMap做转换。[pred, scores] classify(net, grayImg); [maxScore, idx] max(scores); chineseName labelMap.ChineseName(labelMap.ClassName string(pred));这里要注意如果直接显示pred得到的是训练时的文件夹名例如yi而不是汉字“一”。这也是为什么类别映射表是工程中不可缺少的一部分。6. 扩展类别与重新训练6.1 如何扩充新的汉字类别项目标题中的“可以增加其它含义”指的是系统不只识别固定几个汉字而是可以扩充新的类别并重新训练。扩充步骤是新建一个文件夹用拼音或英文字母命名。将该汉字的手写样本放入文件夹。在labelMap中增加一行映射。重新运行数据读取和训练脚本。保存新的模型文件。假设原来有 5 个类别现在要增加“六”目录结构新增一个liu文件夹类别映射表改为labelMap table( ... string({yi,er,san,si,wu,liu}), ... string({一,二,三,四,五,六}), ... VariableNames, {ClassName, ChineseName});训练脚本中numClasses会根据imageDatastore自动识别不需要手动修改网络输出层大小。但要注意新类别样本数量不能太少。如果某个类别只有 5 张图而其他类别有 50 张模型很容易倾向多数类。建议每个类别样本数量尽量均衡。6.2 数据增强降低过拟合扩充类别后样本量通常仍然是瓶颈。手写汉字数据采集成本高很难快速收集大量样本。此时可以使用数据增强在训练过程中对每张图片做随机旋转、平移、缩放等变换扩充有效训练样本。Matlab 使用imageDataAugmenter配置增强策略再用augmentedImageDatastore包装训练集imageAug imageDataAugmenter( ... RandRotation, [-15 15], ... RandXTranslation, [-3 3], ... RandYTranslation, [-3 3], ... RandScale, [0.9 1.1], ... RandXShear, [-5 5], ... RandYShear, [-5 5]); augTrainImds augmentedImageDatastore([64 64], trainImds, ... DataAugmentation, imageAug);训练时将训练集换成augTrainImdsnet trainNetwork(augTrainImds, layers, options);数据增强对汉字识别的作用比较明显。手写汉字经常出现轻微倾斜、笔画不在画板正中央、字迹偏大或偏小这些变换恰好模拟了真实书写中的常见变化。注意验证集和测试集不要做随机数据增强否则评估结果不能真实反映模型在原始数据上的表现。6.3 增量训练与迁移学习策略如果已有训练好的模型又想扩展到新类别有两种常见策略全量重新训练把所有旧类别和新类别放在一起重新训练稳定但耗时。迁移学习微调在已训练模型基础上替换最后一层输出并调整学习率继续训练。迁移学习在 Matlab 中实现的核心是加载已训练模型替换最后的全连接层和分类层然后使用较小的学习率继续训练。这种方法在新类别数据较少时也能获得不错的效果因为模型已经掌握了手写笔画的通用特征。但需要注意旧模型的输出层只包含旧类别扩展新类别时必须重建分类层而重建后全连接层的权重是随机初始化的所以微调时新层的学习率通常要设置得大一些、旧层小一些。在课程项目中更稳妥的做法是保留旧模型同时把新类别样本和旧类别样本合并后重新训练。虽然耗时更长但对初学者来说逻辑最清晰也不容易引入迁移学习特有的配置问题。7. 常见问题排查7.1 “内存不足”或训练中断现象训练到一定轮数后报内存不足或直接闪退。可能原因MiniBatchSize 设置过大。输入图像尺寸过大。同时打开太多大数据集变量。检查方式% 查看当前变量内存占用 whos处理建议把MiniBatchSize从 32 降到 16 或 8。把图像尺寸从 128×128 降到 64×64。训练前清理不需要的大变量clear imdsTest测试时再重新加载。如果是 Windows 系统关闭多余程序释放物理内存。7.2 验证准确率低、过拟合明显现象训练准确率 95% 以上验证准确率只有 60%。可能原因训练样本太少。没有使用数据增强。网络太深或全连接层神经元太多。训练集和测试集划分不合理。检查方式查看每个类别样本数量。查看训练曲线中训练准确率与验证准确率的差值。随机抽取测试集图像人工判断图片是否存在标注错误。处理建议先增加每个类别的样本数量这是最有效的手段。加入数据增强模拟书写变化。在分类前增加dropoutLayer(0.5)。减少fc1层神经元数量降低模型容量。检查数据集是否错混类例如“日”和“目”的图片放反。7.3 界面手写笔画不显示或识别错误现象鼠标在画板区域画不出笔画或者识别精度很低。可能原因回调函数事件绑定错误WindowButtonMotionFcn在未按下鼠标时也在执行。getframe抓取的图像包含坐标轴边框、网格线干扰模型输入。画板图像与训练图像的背景极性不一致。检查方式在beginDraw和endDraw中加disp打印调试信息。将抓取后的图像用imshow显示出来确认实际输入。figure; imshow(grayImg);处理建议抓取前隐藏坐标轴刻度和网格线。保证训练数据和界面手写输入都是白底黑字或统一进行反转。训练时增加多种笔迹粗细的样本减少界面画笔粗细对识别的影响。7.4 重新训练后旧模型无法加载现象加载之前保存的hanziModel.mat报错提示找不到变量或网络对象无效。可能原因保存时只保存了net没保存labelMap。当前脚本工作区没有该变量路径不对。Matlab 版本或工具箱不同网络对象格式不兼容。检查方式data load(hanziModel.mat); disp(fieldnames(data));处理建议保存时同时保存net和labelMap。使用完整路径加载例如load(fullfile(pwd, models, hanziModel.mat))。新的 Matlab 环境必须安装 Deep Learning Toolbox。8. 工程化建议与后续扩展方向8.1 学习环境与生产环境的差异在课程设计中跑通训练和识别界面就可以了。但如果要把它放到产品环境或做更大规模实验需要补充以下内容配置外置化图像尺寸、网络参数、训练轮数不应该写在脚本里应该读配置文件。日志与监控训练过程要记录准确率、损失、耗时和模型文件方便复现。模型版本管理每次训练后给模型打版本号保留历史模型方便回滚。错误处理界面识别时可能输入空白、画板内容过少要给出提示而不是直接classify报错。性能优化如果使用 CPU 训练训练速度会很慢建议使用 NVIDIA GPU 并安装合适版本的 CUDA 和 cuDNN。在 Matlab 中确认 GPU 是否可用gpuDeviceCount如果返回0说明当前环境没有可用 GPU训练时只能使用 CPU 选项。8.2 项目发布前需要检查的清单在交付或提交项目前可以使用下面清单逐项检查检查项检查内容数据完整性每个类别样本数量是否均衡是否有空目录预处理一致性训练、验证、测试和界面输入是否都走同一个预处理函数随机种子是否固定rng结果能否复现类别映射labelMap是否和文件夹名一一对应模型保存是否同时保存网络和映射表是否记录了训练参数界面鲁棒性空白画板、快速笔画、非常规书写是否有保护文档可读性代码注释是否说明每个文件的作用和运行顺序8.3 下一步可以做这个项目还有很多可以扩展的方向增加类别数从 5 个汉字扩展到 20 个、50 个甚至更多挑战集中在相似字和样本量。引入手写笔顺信息如果数据来自平板或触摸屏记录笔画顺序后可以训练序列模型。多模型对比把当前 CNN 与 LeNet、ResNet、VGG 等结构对比分析准确率和训练时间。调参优化用贝叶斯优化或网格搜索找出更好的学习率和网络深度。部署方向训练好的模型可以通过 MATLAB Compiler 打包成独立程序或把推理逻辑转换成 ONNX 后集成到其他平台。对于初学者最重要的不是继续堆叠更多网络结构而是把这个项目的每个环节都亲手验证一遍数据是怎么进的、网络是怎么学的、错分样本为什么错、界面输入和处理流程是否一致。只要这些细节都清楚后续扩展到更复杂的汉字识别系统时基础就是扎实的。
返回列表