手写汉字识别这题目放在深度学习毕设里属于那种既有难度摔打、又能在有限周期内做完整的类型。我最初接触DRSN深度残差收缩网络纯粹是看中它对噪声特征的处理方式——手写汉字里连笔、断笔、涂抹、墨迹深浅这些噪声太常见了普通分类网络会把它们一并学进去导致测试时稍微遇到点“不规矩”的笔画就翻车。这篇博文就聊清楚一件事用python从零把深度残差收缩网络用到手写汉字识别上并且把训练实验和可交互的系统实现整条链路走通。如果你是正在选毕设题的同学或者想把手写识别做成一个小工具的工作人员这篇内容对你应该有直接的参考价值。1. 选题拆解与整体方案设计1.1 这个题目好在哪难度、创新点与落地性先说结论这不是一个“水题”但也不是一个会让人做到一半想放弃的题。从难度看手写汉字识别比MNIST、CIFAR这类公开玩具数据集高一个量级类别多GB2312一级汉字就有3755个、结构复杂、书写差异大做出来天然比“手写数字识别”有说服力。比起目标检测、语义分割这类动辄要调几个月的大工程它的工作量又可控得多数据预处理、模型设计、训练评估、界面展示四大块每块正好对应毕业论文的一章写起来结构清晰导师审阅时也很容易看出你做了完整的工作。创新点方面DRSN本身属于较新的网络结构演进跟“拿一个现成ResNet直接跑”相比答辩时有明显的亮点可讲。你可以选择在阈值生成网络上做简化或者把DRSN和其他注意力模块结合甚至针对汉字结构做分区域阈值处理这些都是能写进论文“改进与创新”章节的方向。除了网络本身实验设计也有发挥空间对比不同噪声强度下的识别精度、分析模型对书写风格的鲁棒性、讨论阈值变化对特征表达的影响都能让论文的论证更丰满。最重要的是它有落地场景试卷批改、表单数字化、手写笔记检索这些都是实际业务里会遇到的。选一个既有研究内容又有应用出口的题目毕设答辩时老师问“这个有什么用”你永远答得上来。1.2 DRSN是什么从ResNet到残差收缩的演进深度残差收缩网络这个名字听起来唬人拆开看其实不复杂。ResNet解决的是深度网络梯度回传问题用残差连接让网络可以做得更深。SENet则更进一步给特征图的每个通道学习一个权重把有用的通道放大、没用的通道压小。DRSN的思路是在SENet“加权”的基础上多做了一个操作软阈值化。软阈值化是信号去噪里常用的手段公式就一句话把绝对值小于阈值的部分变成0绝对值大于阈值的部分向零收缩。你可以把它类比成音频处理里的降噪把录音中低于一定音量的杂音抹掉剩下的就是干净的人声。在神经网络里这个操作的作用也一样——把那些携带噪声、对分类没有帮助的通道特征直接压掉。难点在于阈值怎么定定大了把有效特征抹没了定小了噪声去不掉。DRSN的解决办法是让网络自己学对特征图取绝对值后做全局平均池化接一个小的全连接网络最后用Sigmoid输出一个0到1之间的系数这个系数乘以池化结果就得到每通道的阈值。需要说明的是这里用的约束基准是每个通道绝对值的平均而不是最大值这样既能保证阈值小于特征最大幅度又避免了取最大值的不可导问题训练过程会稳定很多。我真正觉得DRSN适合汉字识别是因为手写汉字的噪声太“个性化”了。不同人写同一笔画的起笔收笔、墨水量、纸张纹理都会产生不同的噪声模式而且这些噪声在浅层特征和深层特征里分布还不一样。DRSN在每个残差模块里都可以插一个收缩单元相当于在网络的每一层都给特征做了一次“自动清洗”让骨干网络专注于字形结构本身。相比普通ResNet它天然多了一层对扰动特征的抑制能力相比SENet它不只是给通道加权而是真正把低于阈值的噪声分量从特征里剔除这两种处理方式在数学性质和实际效果上都有明显区别。网络结构应对噪声的方式额外参数量适用场景ResNet无显式处理依赖数据增强无常规图像分类SENet对通道加权抑制弱通道少量强调关键通道DRSN软阈值化自动学习每通道阈值中量含噪声、强扰动的分类1.3 系统模块划分与整体技术选型整个系统我拆成两块训练实验子系统和交互部署子系统。训练部分的工作包括数据集加载、预处理、模型训练、评估曲线绘制、对比实验部署部分是一个本地GUI用户可以用鼠标写汉字或者上传图片程序输出识别的Top-K候选结果。两部分共用同一个模型定义和预处理管道保证训练和推理看到的数据分布完全一致。这个划分也直接对应到论文的章节结构逻辑非常顺。技术选型上框架我选PyTorch。原因很实际一是DRSN这种自定义模块在PyTorch里实现非常顺手可以随意改卷积、池化、阈值模块的组合不用受高层API限制二是调试体验好打印特征图、挂断点、查看每一层的数值变化都很直白对初学者友好三是生态成熟数据加载、预训练权重、可视化工具都有现成的。界面部分用PyQt5画板交互实现简单功能对于一个演示系统来说足够。GUI这块你也可以换成Web方案后端接口加前端手写板但本地工具类应用用PyQt5开发效率更高打包也方便。整体链路就是原始图片→统一预处理→DRSN特征提取→全连接分类→Top-K结果展示。2. 数据准备与预处理细节2.1 数据集选型公开数据为主自采数据为辅手写汉字识别的公开数据集首选是CASIA-HWDB这是学术界做手写汉字识别最常用的基准。我建议毕设直接用这个理由很简单样本量大、类别覆盖广、有标准评测划分用同一份数据和别人对比实验时有参考价值。如果只想先跑通流程可以按GB2312一级汉字表做一个子集取500到1000个常见字每个字保留几百个样本先证明整个链路能跑通、能出结果再扩到3755类。这样的策略在时间紧张时特别有用不至于一上来就被大数据量拖垮。加载和转换的过程建议写成单独的数据准备脚本把原始格式统一转成常规图片后面所有环节都从同一份转换后的数据出发。如果导师要求必须有自己的数据你可以自己用纸笔写几页字拍照、切割成单字或者用数位板写一批样本补充进训练集。手写数据要注意保证多样性最好找几位不同书写风格的人写不要全是一个人写的否则模型会对书写者“过拟合”。自采数据在进入训练前需要做和别人公开数据集一样的预处理不然分布不一致会影响最终精度。这里有一个经验公开数据和自采数据混合时建议把自采数据的比例控制在三分之一以内太多会把模型整体分布带偏。划分策略上如果公开数据集本身没有标准划分就按7:2:1切分成训练集、验证集、测试集。注意两个细节第一划分的时候要按“书写者”分层不要让同一个人的同一批字既出现在训练集又出现在测试集否则测试指标会虚高答辩时老师一问“你的验证集怎么划分的”就露馅第二验证集和测试集一定不能参与任何训练阶段的决策包括调整阈值、选择增强策略否则你的结果就不是真实泛化性能这样的实验结论在学术上是站不住脚的。2.2 预处理与数据增强汉字识别特有的坑图像的预处理链路我总结为四步灰度化、二值化、尺寸统一、数值归一化。首先读进来转成单通道灰度图然后做二值化背景置为0、笔画置为1这样能去掉纸张纹理和背景噪声尺寸统一到64×64或者96×96。这里要注意汉字笔画丰富64×64勉强够用如果是楷书这种结构复杂的字建议上96×96否则笔画细节会被挤压丢失。最后做数值归一化除以255或者按均值方差归一化这一步决定了训练时梯度是否稳定很多人忽略了它导致模型怎么调都差一口气。数据增强是手写识别里最值得花时间的环节。建议的组合是随机旋转±10度、随机缩放0.9到1.1倍、随机平移几个像素、轻微的弹性形变、再叠加少许高斯噪声。弹性形变对手写体特别有效因为真实手写不是一个像素一个像素“标准”的字的笔画会有自然的弯曲变形弹性形变相当于给模型模拟了这种变化。需要提醒的是旋转角度别超过15度。汉字的形近字特别多“人”和“入”、“土”和“士”本身的差别就是笔画的长短和倾斜角度旋转过头会造成语义混淆。另外DRSN模型的优势在于抗噪训练时加一点高斯噪声反而能让它发挥出真正的能力所以我建议噪声增强不要省这是后续对比实验的亮点之一。2.3 标签映射与样本不均衡处理多类别中文分类首先要解决的是“汉字和整数怎么对应”。我习惯用两个字典一个是char_to_idx把汉字映射到整数索引另一个是idx_to_char推理的时候把预测索引转回汉字。这两个字典必须保存下来最好存为JSON或文本文件和模型权重放在一起否则换台机器做预测时你都不知道预测出来的第3072类是什么字。映射表的构建要基于固定的字符集顺序比如GB2312的顺序不要用Python字典默认顺序不然每次运行结果都可能不同。样本不均衡是手写汉字数据集里一定会碰到的问题。常用汉字每个字的样本可能大几百个冷门字可能只有几十个如果直接按原始分布训练模型会把大量注意力集中在热门字上冷门字的准确率会非常难看。我建议做三件事第一用PyTorch的WeightedRandomSampler做加权采样让每个类别在每轮迭代里被抽到的概率差不多第二对样本少的类别做更强的数据增强相当于给它们“补充数据”第三如果还是不理想就把损失函数换成Focal Loss它对少数类样本会给予更高的关注。这几招下来一般能把不均衡带来的偏差压下去很多写实验分析时也顺便把这个现象记录下来论文里又多了一段有价值的讨论。3. 网络实现与训练调参实操3.1 环境配置与工程目录依赖方面Python用3.8以上版本PyTorch用1.10以上图像处理用OpenCV和Pillow界面用PyQt5训练进度可视化用tqdm和matplotlib。GPU有无都能做有GPU训练3755类会快很多没有GPU就先用小子集跑通代码再决定是否扩大规模。这里有个建议如果只有CPU环境可以把图像尺寸降到48×48网络通道数减半一个小的子集训练几十轮还是可以出结果的关键是先把链路走通再考虑性能上限。工程目录我习惯这样组织handwritten_hanzi/ ├── data/ # 原始数据 ├── checkpoints/ # 模型权重保存 ├── configs/ # 超参数配置文件 ├── models/ # 网络结构定义 │ └── drsn.py ├── utils/ # 数据集加载、预处理、可视化 ├── train.py # 训练主脚本 ├── evaluate.py # 评估脚本 └── app.py # GUI系统入口这样组织有实际的好处每个文件职责单一训练、评估、部署互不干扰想改哪个模块都容易定位不会出现改了一个函数导致另一个脚本崩掉的情况。配置文件单独放也非常重要毕设后期你可能要试几十组参数把batch size、学习率、图像尺寸这些写在配置文件里比每次改代码里的硬编码要可靠得多。模型权重按epoch命名保存比如drsn_epoch_50_acc_0.94.pth这样你调优的时候随时可以回退到之前的版本不会因为覆盖了旧权重而后悔。3.2 核心模块代码软阈值化与残差收缩单元网络搭建是毕设最核心的代码部分。先看软阈值化模块的实现。它的输入是一个特征图输出是经过阈值处理的特征关键在于阈值要能“自动学习”。import torch import torch.nn as nn class MeanThreshold(nn.Module): def __init__(self, channels, hidden_channelsNone): super().__init__() hidden hidden_channels or channels // 4 self.gap nn.AdaptiveAvgPool2d(1) self.attend nn.Sequential( nn.Linear(channels, hidden), nn.BatchNorm1d(hidden), nn.ReLU(inplaceTrue), nn.Linear(hidden, channels), nn.Sigmoid(), ) def forward(self, x): # x: [B, C, H, W] abs_x torch.abs(x) gap_x self.gap(abs_x).view(x.size(0), -1) # 每个通道绝对值的平均 z self.attend(gap_x) # 0到1的自适应缩放系数 threshold z.unsqueeze(2).unsqueeze(3) * gap_x.view(x.size(0), x.size(1), 1, 1) y torch.sign(x) * torch.clamp(abs_x - threshold, min0) return y这段代码里有几个细节值得展开。首先取绝对值是因为软阈值化需要比较的是“幅度”而特征图有正有负不能直接拿原值比较。然后做全局平均池化得到一个通道级统计量这个统计量会乘上Sigmoid输出的系数作为阈值。用Sigmoid保证系数在0和1之间这样阈值不会超过该通道绝对值的平均也就不会有阈值大于特征最大值、把通道全部清零的极端情况这种约束保证了训练的稳定性。实际用的时候Sigmoid前的线性层bias可以初始化成负值让初始阈值偏小避免训练前期大范围把特征压掉导致信息丢失。有了软阈值模块残差收缩单元的结构就很清晰输入先走两层卷积中间穿插BN和ReLU在第二层卷积之后接入上面这个MeanThreshold模块最后整体与原始输入做残差相加。如果你想让网络更强可以借鉴残差网络的思路在不同阶段堆叠多个这样的单元并在阶段之间加入步长为2的下采样来扩大感受野。我建议先复现一个基础版DRSN再逐步加深做对比实验而不是一开始就把结构做得特别复杂。初次跑的版本控制在整个模型参数在50万以内训练速度和显存压力都友好很多等跑通后再加宽度和深度。3.3 训练参数与评估策略训练配置我给出一个起点实测下来在汉字识别上表现稳定优化器用Adam初始学习率3e-4batch size看显存定32或64都行学习率用余弦退火或者每隔15轮衰减0.1的StepLR损失函数用CrossEntropyLoss训练轮数50到100轮配合早停——连续10轮验证集准确率不上升就停止。如果训练过程中loss震荡明显先检查是不是学习率太高降到1e-4再看如果loss下降很慢可以先用小数据跑一轮确认代码逻辑没问题再上全量。评估时别只看最终准确率。top-1准确率和top-5准确率都要统计因为手写汉字里形近字太多模型经常把“候”错成“侯”这种情况下top-5其实还是对的。除了准确率我还建议做两块内容一是混淆矩阵挑出最容易混淆的几组字写进论文里能非常直观地说明模型的问题二是对比实验用同样的数据分别训练ResNet、SENet和DRSN比较它们在原始数据上的准确率再用加了噪声的测试集比较它们的抗噪能力。噪声对比实验是DRSN最出彩的地方你可以往测试集里叠加不同程度的高斯噪声看DRSN的准确率下降幅度明显小于另外两个基线这个结论就是你论文实验章里最有说服力的证据。3.4 GUI与推理链路的实现系统实现这一块我的做法是用PyQt5做一个手写画板加图片上传的界面。画板部分用QPainter记录鼠标轨迹把用户的书写笔画画到一个纯白的QImage上。图片上传部分用QFileDialog选择图片支持PNG、JPG格式。核心逻辑在于“画板内容变成模型输入”这一条链路首先从画板上拿到QImage转成OpenCV的灰度图然后找笔画的bounding box裁剪出字所在的区域四周留白几个像素最后缩放到和训练时一致的64×64或96×96再做和训练完全相同的归一化。很多人做出来识别率低问题就出在这里——不裁剪直接缩放字的位置和大小都变了模型看到的数据分布跟训练时完全不同精度自然会掉。推理部分记得把模型切到eval模式用torch.no_grad()包起来关闭梯度计算。输出的时候取softmax概率最高的前5个类别通过idx_to_char映射回去在界面上排成一列展示。为了让展示更直观可以把每类置信度也用进度条画出来用户一眼就能看到模型对哪个候选最有把握。推理速度上单张64×64的图在CPU上跑基础DRSN大概几十毫秒完全能满足交互需求不需要担心性能。如果想更快可以导成ONNX再用ONNXRuntime做推理能再压掉一批时间但这属于优化项先把链路跑通再说。界面布局不要做得太花哨画板加按钮加结果展示区清晰就好过度设计反而容易引入界面层和模型层的对接问题。4. 实测问题与避坑速查4.1 数据处理环节的典型问题数据环节最容易出问题的是标签错乱。多类别数据集的文件夹命名、文件名解析、索引映射这三者的顺序一旦不一致模型就会在“看似收敛但准确率死活上不去”的状态里卡很久。我的排查方法是训练前先写一段校验代码随机抽几张图打印出“路径-标签-汉字”的三元组人工确认对应关系是否正确。这个检查看起来笨但真的能省下大量排查时间比训练完了再去猜问题在哪要高效得多。预处理的另一个坑是二值化参数。公开数据集的灰度图背景可能不是纯白有轻微的纸纹直接用固定阈值二值化可能把墨迹和背景分割不干净。我建议先统计每个样本的灰度分布用Otsu自适应阈值代替固定阈值效果会稳定很多。还有训练和测试的预处理函数要抽成同一个工具函数确保验证、测试、GUI推理调用的都是同一份代码。我这里写过一次“训练时做了灰度归一化测试时忘了做”的低级错误结果模型线上表现远不如训练查了很久才发现是预处理链路不一致这种问题靠自查很难发现最好的办法就是共用函数。4.2 训练环节的典型问题训练不收敛常见原因有三个学习率过大导致震荡、预处理不一致导致数据分布异常、标签错位。排查顺序建议是先小批量跑一轮看loss是否有下降趋势再检查验证集的预处理最后确认数据集加载的索引映射。如果loss能下降但验证集准确率低基本可以断定是过拟合对应策略是减小网络宽度、增加Dropout、增强数据增强强度。还有一个容易忽略的点多分类任务的输出层bias在初始化时最好设成均匀的先验比如用log(1/num_classes)否则前几个epoch的loss可能异常大。显存不够是个现实问题3755类的输入即使64×64batch size设小了也会有压力。省钱的办法是开梯度累积先用小batch算梯度攒几步再更新参数效果和大batch基本一致。实在不行就用混合精度训练PyTorch自带的AMP可以把显存占用降一半对精度几乎无影响。如果连CPU内存都吃紧可以用内存映射的方式加载数据而不是一次性把所有图片读进内存训练时按需读区虽然慢一些但不会爆内存。这些问题解决起来都不难关键是要提前想到别等训练跑了一半才发现资源不够。现象可能原因排查方法解决方案loss不降或震荡学习率过大观察loss波动幅度调低学习率重训loss下降但验证准确率低过拟合对比训练与验证曲线增强、Dropout、减小宽度训练正常但测试反差大数据划分泄漏检查划分方式按书写者分层划分显存溢出batch过大查看报错信息梯度累积或混合精度4.3 GUI部署环节的典型问题部署侧最常见的坑有三个。第一坐标系颠倒。QPainter的坐标轴是以左上角为原点、y向下的从QImage转成numpy数组时行列顺序对应的是x和y这个方向搞反了模型看到的字就是镜面的识别率会断崖式下降。第二归一化不一致。训练时用的是均值方差归一化GUI推理时却只简单除以255模型在两种分布之间切换结果必然不稳定。第三背景干扰。画板带了网格线或者背景纹理如果这些线条没有在预处理阶段被滤掉模型就会把它们当成笔画特征。我自己的画板是白底白纸关闭一切网格这一步看着简单实际体感差异非常大特别是识别那种笔画较多、结构复杂的字时背景噪声对被当作笔画的影响是致命的。还有一个细节是画板里字的大小和位置。如果用户在画板上只写了一小团字四周留白特别多裁剪函数仍然会正常找到字的边界框这个没问题但如果你把裁剪框固定死就得提醒用户把字写在画的中央否则部分笔画可能被裁掉。裁剪后建议统一加到64×64的尺寸上且不要直接拉伸用等比例缩放到合适大小后再居中补边这样能最大程度保留笔画比例。部署调试时可以做一个调试模式把每次推理的预处理中间结果存成图片一眼就能看出是哪个环节出了问题。每个人跑毕设都会踩几个类似的坑。我最大的体会是动手写代码之前先把数据加载和预处理这条链路用一个小实验验证透再谈模型和调参模型选型再花哨数据链路不通后面全是无效时间。如果时间紧张先从100个字的子集跑通全流程把准确率、界面、论文配图的数据都拿到手再逐步扩到3755类整个过程会从容很多。这个题目的可扩展空间也很大比如把模型导成ONNX做离线部署加上笔画序列信息做更细粒度的时序建模或者把手写识别和检索业务结合起来做一个小工具。只要把基础链路做得扎实后面想往哪个方向生长都有余地。
阅读完成 · 觉得有帮助?