手写汉字识别是图像识别里比较有代表性的任务,它和手写数字识别最大的区别在于类别数量多、结构复杂、相似字形多。早些年做汉字识别大多依赖传统特征加分类器,后来 CNN 普及之后,模型直接从原始图像学习特征,识别流程简单了很多。本文围绕一套基于 MATLAB 的 CNN 手写汉字识别系统展开,从原理、数据准备、网络搭建、训练、评估到界面识别,把完整流程拆开讲清楚。代码部分基于 Deep Learning Toolbox 编写,结构上做了模块化处理,方便你改成数字识别、字母识别或者其他图案分类任务,也可以重新训练自己的数据集。
1. 背景与核心概念
1.1 手写汉字识别为什么比数字识别难
手写数字识别 MNIST 是深度学习的“Hello World”,但手写汉字识别远没有这么简单。主要原因有几点:
- 类别数量大。常用汉字几千个,GB2312 一级汉字就有 3755 个,二级汉字 3008 个,哪怕只做一个演示系统,类别数量也远大于 10 个数字。
- 字形结构复杂。汉字由笔画、偏旁、部首组合而成,同样的“木”字旁在不同汉字里位置和比例都有差异。
- 书写风格差异大。不同人的笔顺、连笔、倾斜角度、笔画粗细都不一样,模型需要具备较强的泛化能力。
- 相似字多。“未”和“末”、“人”和“入”、“日”和“目”这类相近字形,稍有不慎就会误判。
传统方法通常先做笔画提取、结构分析,再结合模板匹配或统计分类器,流程长、泛化差。CNN 的核心思路是让网络自己从图像中学习“哪些局部特征重要”,所以它天然适合这种视觉结构复杂的任务。
1.2 CNN 在汉字识别中的工作原理
CNN 是卷积神经网络的简称,专门用来处理网格结构数据,比如二维图像。它通过卷积核在图像上滑动,提取局部特征,再用池化层压缩特征尺寸,最后通过全连接层完成分类。
用一个通俗的类比来说:
- 卷积层相当于“局部观察员”,每个卷积核负责看一种小模式,比如横线、竖线、折角、圆弧等。
- 池化层相当于“信息压缩器”,把相邻区域的特征合并成一个值,减少计算量,也让模型对位置偏移更鲁棒。
- 全连接层相当于“决策委员会”,把前面提取到的所有特征综合起来,输出每个类别的概率。
在汉字识别任务中,CNN 不需要人工设计笔画特征,它自己会从训练数据中学习到类似“端点的形状”“横竖交叉的模式”“包围结构的轮廓”等抽象特征。层数越深,提取的特征越抽象,表达能力也越强。
1.3 为什么选择 MATLAB 做 CNN 训练
MATLAB 的 Deep Learning Toolbox 提供了比较完整的深度学习流程支持,从数据导入、网络搭建、训练可视化到模型部署都有现成接口。相比 Python 的 PyTorch、TensorFlow,MATLAB 的优势在于:
- 不需要手动处理很多底层张量操作,API 设计更接近“搭积木”。
trainNetwork会自动处理训练循环、梯度计算和进度显示,对入门者友好。- 内置
imageDatastore可以非常方便地管理按文件夹分类的图像数据。 - 训练过程中会实时显示损失曲线和准确率曲线,观察模型收敛情况很直观。
classify一行代码就能完成模型推理,快速验证效果。
如果你的主要工作环境是 MATLAB,或者你希望快速完成一个可视化演示系统,那么用 MATLAB 做 CNN 手写汉字识别是一个性价比很高的选择。
2. 环境准备与工程结构
2.1 MATLAB 环境要求
本文示例基于 Deep Learning Toolbox 编写,所需环境如下:
- MATLAB 版本建议 R2021a 及以上。不同版本对
imageDatastore、trainingOptions的参数支持略有差异,本文代码以常见用法演示。 - Deep Learning Toolbox。
- Image Processing Toolbox(用于图像预处理和显示)。
- 如果使用 GPU 训练,需要安装 Parallel Computing Toolbox,并确保显卡驱动和 CUDA 版本与 MATLAB 匹配。
如果不确定自己的版本是否支持,可以在 MATLAB 命令行输入以下命令验证:
ver('deep')如果正确安装了 Deep Learning Toolbox,会输出对应的版本号;如果报错,请在附加功能资源管理器中安装。
2.2 项目文件结构
建议按照下面的目录结构组织工程,方便后续扩展和重新训练:
HanziCNN/ │ ├── data/ │ ├── train/ % 训练集,每个子文件夹一种汉字 │ │ ├── 一/ │ │ ├── 二/ │ │ └── ... │ └── test/ % 测试集,结构同训练集 │ ├── scripts/ │ ├── loadData.m % 数据加载与划分 │ ├── createNetwork.m % 创建 CNN 网络 │ ├── trainModel.m % 训练主脚本 │ ├── evaluateModel.m % 模型评估 │ └── predictImage.m % 单张图片识别 │ └── models/ % 保存训练好的模型 └── hanziNet.mat2.3 数据集准备说明
手写汉字数据集可以使用公开学术数据集(如 CASIA-HWDB 等),也可以自行采集。自行采集时,建议按以下方式整理:
- 每一类汉字建立一个文件夹,文件夹名就是类别标签。
- 图片格式统一为 png 或 jpg。
- 图片尺寸不需要提前统一,
augmentedImageDatastore会在训练前自动缩放。 - 每类样本尽量均衡,避免某些类别样本过多导致模型偏向。
- 图片背景尽量干净,只需要保留手写笔画信息。
如果只是做功能演示,可以先取少量汉字类别跑通流程,比如“一、二、三、十、人、大”等,确认代码无误后再扩展到更多类别。
3. CNN 网络结构设计
3.1 输入层设计
CNN 的输入层需要指定图像尺寸。对于汉字识别,考虑到字形结构复杂度,推荐输入尺寸为 64×64 或 128×128。尺寸太小会丢失笔画细节,尺寸太大会增加训练时间。
inputSize = [64 64 1]; % 宽 64、高 64、单通道灰度图如果数据集中有彩色图片,会在预处理时先转为灰度图,所以输入通道数为 1。
3.2 卷积层与激活层
卷积层通过多个卷积核提取特征。设计网络时要注意:
- 卷积核大小常用 3×3,它计算量小,堆叠多层能获得更大感受野。
- 卷积核数量从少到多,常见做法是 32 → 64 → 128 逐层增加。
- 卷积层后通常接 Batch Normalization 层和 ReLU 激活层,这样训练更稳定,收敛更快。
layers = [ imageInputLayer(inputSize, 'Name', 'input') 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(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ];3.3 池化层的作用
池化层的作用有两个:
- 降低特征图尺寸,减少参数量和计算量。
- 增强平移不变性,让模型对小范围的位置偏移不那么敏感。
常用的最大池化窗口是 2×2,步长为 2,它会把每个 2×2 区域取最大值输出。无论笔画向左偏一点还是向右偏一点,只要仍然落在相邻区域,最大池化都能提取到相似的响应。
3.4 全连接层与 Softmax
最后一个卷积块输出的特征图,会先被展平成一维向量,然后送入全连接层。全连接层的输出节点数等于类别数。Softmax 层负责把全连接层的输出变成概率分布,所有类别的概率之和为 1。
在trainNetwork中,classificationLayer会自动配合 softmax 计算交叉熵损失,不需要手动实现损失函数。
3.5 训练选项设置
trainingOptions控制训练过程中的各项参数。常用的设置项如下:
options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MiniBatchSize', 32, ... 'MaxEpochs', 20, ... 'ValidationData', augimdsValidation, ... 'ValidationFrequency', 10, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', true, ... 'VerboseFrequency', 20);InitialLearnRate:初始学习率,0.001 是常见起点。学习率太大会震荡不收敛,太小则收敛慢。MiniBatchSize:每批样本数量,需要根据显存或内存调整。MaxEpochs:训练轮数,数据集越小,通常需要越多轮数。ValidationData:验证集,用于监控模型在未见数据上的表现。Plots设为training-progress可以弹出实时训练进度窗口。
4. 完整实战案例
本节从创建工程开始,按步骤完成数据加载、网络创建、模型训练、模型评估和单张图片识别。
4.1 创建项目结构
在你常用的工作目录下创建文件夹HanziCNN,并在其中创建data、scripts、models三个子文件夹。然后将整理好的训练图片和测试图片分别放入data/train和data/test。
4.2 编写数据加载脚本
新建脚本文件scripts/loadData.m,内容如下:
function [imdsTrain, imdsValidation, imdsTest] = loadData(dataDir) % dataDir 是包含 train 和 test 子文件夹的根目录 % 例如: dataDir = fullfile(pwd, 'data'); trainDir = fullfile(dataDir, 'train'); testDir = fullfile(dataDir, 'test'); % 读取训练集和测试集 imdsTrain = imageDatastore(trainDir, ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames'); imdsTest = imageDatastore(testDir, ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames'); % 从训练集中划分一部分作为验证集 % 这里按 8:2 划分,可根据数据量调整 splitRatio = 0.8; [imdsTrain, imdsValidation] = splitEachLabel(imdsTrain, splitRatio, 'randomized'); fprintf('训练集样本数: %d\n', numel(imdsTrain.Files)); fprintf('验证集样本数: %d\n', numel(imdsValidation.Files)); fprintf('测试集样本数: %d\n', numel(imdsTest.Files)); fprintf('类别数: %d\n', numel(unique(imdsTrain.Labels))); endimageDatastore会自动读取train目录下每个子文件夹中的图片,并以子文件夹名作为标签。splitEachLabel会按比例随机划分训练集和验证集。
4.3 编写网络创建函数
新建脚本文件scripts/createNetwork.m,内容如下:
function layers = createNetwork(inputSize, numClasses) layers = [ imageInputLayer(inputSize, 'Name', 'input') 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(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]; end这个网络包含 3 个卷积块,每个卷积块由“卷积 + 批归一化 + ReLU + 最大池化”组成。输入 64×64 图像时,经过三次 2×2 池化后,全连接层之前特征图尺寸变为 8×8,参数量适中,适配小数据集和中等规模数据集。
4.4 编写图像预处理
在训练和验证之前,需要将图像统一缩放为网络输入尺寸。使用augmentedImageDatastore完成:
inputSize = [64 64 1]; augimdsTrain = augmentedImageDatastore(inputSize, imdsTrain, ... 'ColorPreprocessing', 'gray2rgb');如果数据集中是灰度图,augmentedImageDatastore会自动处理。对于彩色图也可以直接指定输入尺寸,让工具箱自动转换。
4.5 编写模型训练脚本
新建脚本文件scripts/trainModel.m,内容如下:
%% 训练 CNN 手写汉字识别模型 clear; clc; close all; %% 1. 数据准备 dataDir = fullfile(fileparts(pwd), 'data'); [imdsTrain, imdsValidation, imdsTest] = loadData(dataDir); %% 2. 网络参数 inputSize = [64 64 1]; numClasses = numel(unique(imdsTrain.Labels)); %% 3. 数据增强与预处理 augimdsTrain = augmentedImageDatastore(inputSize, imdsTrain, ... 'DataAugmentation', imageDataAugmenter(... 'RandRotation', [-5 5], ... 'RandXTranslation', [-2 2], ... 'RandYTranslation', [-2 2])); augimdsValidation = augmentedImageDatastore(inputSize, imdsValidation); %% 4. 创建网络 layers = createNetwork(inputSize, numClasses); %% 5. 设置训练选项 options = trainingOptions('adam', ... 'InitialLearnRate', 0.001, ... 'MiniBatchSize', 16, ... 'MaxEpochs', 30, ... 'ValidationData', augimdsValidation, ... 'ValidationFrequency', 20, ... 'Shuffle', 'every-epoch', ... 'Plots', 'training-progress', ... 'Verbose', true, ... 'VerboseFrequency', 20); %% 6. 训练 net = trainNetwork(augimdsTrain, layers, options); %% 7. 保存模型 modelDir = fullfile(fileparts(pwd), 'models'); if ~exist(modelDir, 'dir') mkdir(modelDir); end save(fullfile(modelDir, 'hanziNet.mat'), 'net'); %% 8. 保存测试集,供评估使用 save(fullfile(modelDir, 'testData.mat'), 'imdsTest'); disp('训练完成,模型已保存到 models/hanziNet.mat');需要说明的是,RandRotation的范围不能太大。汉字方向是固定语义信息,旋转角度过大反而会破坏字形可辨识性。这里使用 [-5, 5] 度的小角度旋转即可。
4.6 编写模型评估脚本
新建脚本文件scripts/evaluateModel.m,内容如下:
%% 评估模型 clear; clc; close all; %% 1. 加载训练好的模型和测试集 modelDir = fullfile(fileparts(pwd), 'models'); loaded = load(fullfile(modelDir, 'hanziNet.mat')); net = loaded.net; loadedTest = load(fullfile(modelDir, 'testData.mat')); imdsTest = loadedTest.imdsTest; inputSize = net.Layers(1).InputSize; augimdsTest = augmentedImageDatastore(inputSize, imdsTest); %% 2. 预测 predLabels = classify(net, augimdsTest); trueLabels = imdsTest.Labels; %% 3. 计算整体准确率 accuracy = mean(predLabels == trueLabels); fprintf('测试集准确率: %.2f%%\n', accuracy * 100); %% 4. 显示混淆矩阵 figure; confusionchart(trueLabels, predLabels); title('CNN 手写汉字识别混淆矩阵');4.7 编写单张图片识别脚本
新建脚本文件scripts/predictImage.m,内容如下:
%% 单张图片识别 clear; clc; close all; %% 1. 加载模型 modelDir = fullfile(fileparts(pwd), 'models'); loaded = load(fullfile(modelDir, 'hanziNet.mat')); net = loaded.net; %% 2. 读取图片 imgPath = fullfile(fileparts(pwd), 'data', 'test', '一', 'yi_001.png'); if ~exist(imgPath, 'file') error('图片不存在,请修改 imgPath 为实际路径'); end img = imread(imgPath); if size(img, 3) == 3 img = rgb2gray(img); end %% 3. 预处理:缩放到网络输入尺寸 inputSize = net.Layers(1).InputSize(1:2); imgResized = imresize(img, inputSize); %% 4. 单张图片需要补一个 batch 维度 imgBatch = repmat(imgResized, [1 1 1 1]); %% 5. 预测 predLabel = classify(net, imgBatch); disp(['预测结果: ', char(predLabel)]); %% 6. 显示图片和结果 figure; imshow(img); title(['预测结果: ', char(predLabel)]);4.8 运行流程与预期输出
在 MATLAB 中按以下顺序运行即可:
- 打开
trainModel.m,点击运行。 - 等待训练完成,训练进度窗口会显示损失曲线和准确率曲线。
- 运行
evaluateModel.m,查看测试集准确率和混淆矩阵。 - 修改
predictImage.m中的图片路径,运行单张识别。
如果数据量较小,训练过程可能在几十秒到几分钟内完成。训练时若显存不足,可以调小MiniBatchSize或调小输入尺寸。
5. 自定义类别与重新训练
5.1 如何扩展到其他识别任务
这套代码不只限于汉字识别。只要数据目录结构是“每个类别一个文件夹”,同样可以用来做:
- 手写数字识别(0~9)。
- 手写英文字母识别(A~Z)。
- 简单图形分类(圆形、三角形、正方形)。
- 印刷体字符识别。
- 其他自定义图案分类。
修改方式很简单:替换data/train和data/test下的子文件夹为新的类别数据,然后重新运行训练脚本即可。代码会根据文件夹自动读取类别数,不需要改网络结构。
如果类别数差异很大,比如从 10 类扩展到 100 类,建议适当增加卷积层的通道数,比如把第二个卷积层从 64 改为 128,第三个卷积层从 128 改为 256,以提升模型容量。
5.2 重新训练时需要注意的问题
重新训练时,如果你的数据集与原有的“34期源码”示例数据集完全不同,建议注意以下几点:
- 删除旧的
models/hanziNet.mat模型文件,或者另存为新名称,避免混淆。 - 清洗新数据,剔除模糊、残缺、标签错误的样本。
- 类别名称建议使用有意义的标签,例如
digit_0、letter_A或汉字本身。 - 每类样本数量尽量均衡,每类至少 50 张以上,否则模型容易过拟合。
- 如果训练准确率高但验证准确率低,说明过拟合,可以增加数据增强、增加 dropout 或减小网络容量。
5.3 数据增强方法
数据增强可以在不增加真实样本的情况下,扩充训练数据量,提高模型泛化能力。MATLAB 的imageDataAugmenter支持多种增强方式:
augmenter = imageDataAugmenter(... 'RandRotation', [-5 5], ... 'RandScale', [0.9 1.1], ... 'RandXTranslation', [-2 2], ... 'RandYTranslation', [-2 2]); augimdsTrain = augmentedImageDatastore(inputSize, imdsTrain, ... 'DataAugmentation', augmenter);需要注意的是,augmentedImageDatastore的增强发生在读取图片时,不会改变磁盘上的原始图片,因此每次训练读取到的增强图像可能略有不同。这有助于模型看到更多样化的输入。
6. 常见问题与排查思路
6.1 常见报错及解决方案
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
imageDatastore读不到图片 | 目录路径不正确,或图片格式不受支持 | 检查data目录层级,确认图片格式为 png/jpg/bmp |
| 内存不足 | 数据集太大或MiniBatchSize过大 | 减小MiniBatchSize,或使用imds的subset方法抽样 |
| GPU 显存不足 | 网络输入尺寸过大,或 batch 过大 | 调小MiniBatchSize,或改用 CPU 训练 |
| 准确率很低 | 数据量太少、标签错误、学习率不合适 | 检查数据质量,增大训练轮数,调整学习率 |
| 训练 loss 一直不下降 | 学习率太小或数据未归一化 | 调大初始学习率,确认图片灰度范围正常 |
| 预测结果全是一个类 | 类别样本严重不均 | 增加少数类样本,或使用加权损失函数 |
| 无法加载模型 | 模型文件损坏或版本不兼容 | 重新训练并覆盖模型文件 |
6.2 训练进度观察
训练时打开的 “training-progress” 窗口会显示两个最重要的曲线:
- 损失曲线:训练损失应该整体下降,如果震荡剧烈,说明学习率偏大;如果几乎不下降,说明学习率偏小。
- 准确率曲线:训练准确率和验证准确率同时上升是最理想的情况。如果训练准确率很高、验证准确率低,说明模型过拟合。
如果发现验证准确率在某个阶段不再提升,可以考虑提前停止训练,或者使用'OutputNetwork'参数选择最佳模型。
6.3 CPU 训练太慢怎么处理
如果电脑没有可用的 NVIDIA GPU,或 MATLAB 未正确配置 GPU,训练会退回到 CPU。此时可以:
- 调小
inputSize,比如从 64×64 改为 48×48。 - 调小卷积层通道数,比如 32 → 16。
- 减小
MaxEpochs。 - 减少训练图片数量,先跑通流程,验证无误后再用全量数据训练。
7. 最佳实践与工程建议
7.1 数据集管理建议
数据集是整个识别系统的基础,比网络结构更容易影响最终效果。实践中有几点值得注意:
- 数据采集时尽量覆盖不同书写风格,不要只找一个人写。
- 对采集到的图片做去噪和背景统一处理,减少无关干扰。
- 按类别固定好目录结构,避免中途频繁改动导致标签错乱。
- 保存一份原始数据集副本,避免图像处理操作覆盖原始数据。
- 训练前打印各类别样本数量,确认没有极端不均衡。
7.2 网络结构设计建议
对于初学者,不要一上来就尝试 ResNet、VGG 等大模型。先从简单的 3 层卷积网络开始,跑通流程后再逐步调整:
- 先用较小的输入尺寸 32×32 或 48×48,减少训练时间。
- 通道数从 16 或 32 开始,逐层翻倍。
- 在工程验证阶段,以“能跑通”为优先,使用小数据集验证,再扩展类别和样本量。
- 如果模型过拟合,优先增加数据增强,而不是盲目增大网络。
7.3 模型保存与版本管理
训练好的模型建议按日期或准确率命名保存,例如:
hanziNet_v1_acc98.mat hanziNet_v2_acc99.mat这样方便回溯模型效果。如果训练脚本有改动,建议同步记录改动说明,避免时间久了分不清哪个脚本对应哪个模型。
7.4 识别系统的工程化扩展
从演示脚本到实际系统,还需要考虑以下几个方面:
- 批量识别:写一个循环遍历文件夹下所有图片,调用
classify批量预测,并保存结果为 CSV。 - GUI 封装:使用 MATLAB App Designer 设计一个手写板界面,用户绘制汉字后实时识别。
- 模型更新:当新增类别或收集到更多样本时,可以在原模型基础上继续训练,也可以从零重新训练。
- 日志记录:记录每次预测的图片路径、预测结果和置信度,方便排查错例。
下面是一个简单的批量识别示例:
function results = batchPredict(net, imgFolder) imds = imageDatastore(imgFolder, ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames'); inputSize = net.Layers(1).InputSize; augimds = augmentedImageDatastore(inputSize, imds); [predLabels, scores] = classify(net, augimds); results = table(imds.Files, string(predLabels), max(scores, [], 2), ... 'VariableNames', {'FilePath', 'PredictedLabel', 'Confidence'}); end7.5 性能优化与部署注意
在资源受限环境下运行识别系统,可以采取以下优化措施:
- 将输入尺寸降到 32×32 或 48×48,推理速度大幅提升。
- 使用
dlarray和dlnetwork自定义预测循环,减少不必要的预处理开销。 - 如果需要部署到 Web 或嵌入式端,MATLAB 支持通过 MATLAB Compiler 或 GPU Coder 生成独立应用和 C++ 代码。具体部署方式需要根据目标平台单独配置。
8. 总结
本文围绕 MATLAB 环境下的 CNN 手写汉字识别系统,从原理、环境准备、数据组织、网络设计、训练评估到单张识别,给出了完整的代码实现。这套代码的核心价值在于它不是一个写死的工具,而是一个可扩展的模板:
- 替换
data目录下的数据集,就能重新训练新的模型。 - 修改类别文件夹,就能扩展到数字、字母或自定义图形识别。
- 调整网络通道数和训练参数,就能适配不同的数据规模和硬件条件。
- 配合 GUI 或批量识别脚本,就能从实验演示变成可用的工程工具。
如果你手头正好有手写汉字或字符识别的需求,建议先从一个小规模数据集开始,跑通本文流程,再根据实际准确率和速度需求逐步调整。训练过程多观察损失曲线和验证准确率,识别效果不好时优先检查数据质量,其次再调整网络结构。把基础流程掌握熟练后,再去尝试更复杂的网络和部署方案都会顺畅很多。