新闻详情

新闻详情

首页 / 资讯中心 / 详情

手写汉字识别实战:从chinese_test.zip到ResNet-18模型训练与调优

发布时间:2026/10/1 12:42:10来源:尧图网络
手写汉字识别实战:从chinese_test.zip到ResNet-18模型训练与调优
简介这份资源面向深度学习与计算机视觉方向的开发者及学生聚焦手写汉字识别这一典型图像分类任务帮助读者理解如何借助深度卷积网络完成从样本到识别结果的完整流程。压缩包内共1个文件为Python脚本整体约1KB属于轻量级代码示例便于快速阅读与二次修改。脚本中通常涵盖数据加载与预处理、卷积网络结构定义、训练与验证循环以及模型评估等核心环节可对照MNIST等经典数据集的预处理思路迁移到汉字识别场景。目前已有177人学习下载适合作为入门练手或课程实验的参考代码。读者可从中获取网络搭建的骨架、训练流程的组织方式以及汉字识别任务的实现思路为后续处理更复杂的汉字多样性与书写风格变化打下基础。1. 手写汉字识别从 chinese_test.zip 说起为什么 MNIST 那套直接搬过来会翻车很多人第一次接触手写识别都是从 MNIST 手写数字识别入门的PyTorch 跑个 LeNet 半小时就能到 99% 准确率于是自然觉得手写汉字识别无非就是换个数据集的事。真上手才发现完全不是一回事MNIST 只有 10 个类别图片是 28×28 的灰度图字符居中、笔画简单而手写汉字常用字就有 3755 个GB2312 一级字库加上二级字库接近 7000 类笔画从一两画到三十几画都有同一个字不同人写出来结构差异极大。chinese_test.zip 这类压缩包通常就是一批手写汉字图片的测试集用来验证你训练好的模型在真实手写样本上到底行不行。这篇笔记面向的是已经会跑 MNIST、想把手写汉字识别真正落地到可用程度的工程师我会把数据准备、模型选型、训练参数、评估方法和踩过的坑按顺序讲清楚让你拿到一个汉字测试集就能自己跑通整条链路。2. 手写汉字数据集怎么准备从 chinese_test.zip 到可训练的格式2.1 先搞清楚你手里的压缩包是什么结构chinese_test.zip 这种命名方式很常见里面大概率是一堆按类别分文件夹的图片或者是一个扁平目录加一份标签文件。拿到之后第一件事不是急着写模型而是先摸清数据结构。我一般用下面这段脚本快速统计import os import zipfile from collections import Counter # 先解压到工作目录 with zipfile.ZipFile(chinese_test.zip, r) as z: z.extractall(./chinese_test) print(压缩包内文件总数:, len(z.namelist())) # 统计目录结构和文件扩展名 root ./chinese_test ext_counter Counter() dir_counter Counter() for dirpath, dirnames, filenames in os.walk(root): for f in filenames: ext os.path.splitext(f)[1].lower() ext_counter[ext] 1 # 记录一级子目录名通常就是类别标签 rel os.path.relpath(dirpath, root) top rel.split(os.sep)[0] dir_counter[top] 1 print(文件扩展名分布:, ext_counter.most_common()) print(一级子目录数量:, len(dir_counter)) print(前 10 个类别及样本数:, dir_counter.most_common(10))这段脚本做三件事解压、统计扩展名、统计一级子目录下的样本数。如果一级子目录数量在几千这个量级说明是按类别组织的目录名就是标签如果只有一个目录且全是图片那大概率需要额外的标签文件。扩展名分布能告诉你图片是 png、jpg 还是 bmp后续读取方式不同。参数方面extractall的目标路径建议单独建一个工作目录不要直接解压到项目根目录否则几千个文件夹会把项目结构搞乱。统计一级子目录时用os.path.relpath而不是硬编码路径分隔符这样在 Windows 和 Linux 上都能跑。2.2 图片预处理的四个关键决策搞清楚结构之后预处理阶段有四个决策直接影响最终精度尺寸统一。手写汉字识别常见输入尺寸是 64×64 或 96×96。28×28 对汉字来说太小了很多左右结构、上下结构的字在这个分辨率下笔画会糊在一起。我一般先用 64×64 跑 baseline如果某些形近字比如“未”和“末”混淆严重再升到 96×96。注意不要直接 resize汉字的长宽比和数字不同直接拉伸会变形建议先 padding 成正方形再缩放。灰度还是二值化。手写汉字图片如果是扫描件背景可能有噪点二值化Otsu 阈值能去掉大部分干扰。但如果是拍照得到的光照不均全局二值化反而会丢失笔画。我的经验是扫描件走二值化拍照件走灰度加自适应直方图均衡。归一化参数。不要直接用 ImageNet 的均值和方差手写汉字图片的像素分布和自然图像完全不同。正确做法是在你自己的训练集上统计均值和标准差import numpy as np from PIL import Image import os def compute_mean_std(data_root, img_size64): pixel_sum np.zeros(3) pixel_sq_sum np.zeros(3) count 0 for dirpath, _, filenames in os.walk(data_root): for f in filenames: if not f.lower().endswith((.png, .jpg, .bmp)): continue img Image.open(os.path.join(dirpath, f)).convert(RGB) img img.resize((img_size, img_size)) arr np.array(img, dtypenp.float32) / 255.0 pixel_sum arr.sum(axis(0, 1)) pixel_sq_sum (arr ** 2).sum(axis(0, 1)) count arr.shape[0] * arr.shape[1] mean pixel_sum / count std np.sqrt(pixel_sq_sum / count - mean ** 2) return mean, std mean, std compute_mean_std(./chinese_test) print(均值:, mean, 标准差:, std)这段代码遍历所有图片累加像素值和像素平方值最后算出每个通道的均值和标准差。注意count是像素总数而不是图片数因为要对每个像素位置求平均。算出来的值填进torchvision.transforms.Normalize里。数据增强的边界。手写汉字对旋转很敏感“一”转 15 度可能就变成别的字了。安全的小幅增强包括±5 度以内的旋转、±10% 的平移、轻微的弹性形变。不要用水平翻转很多汉字翻转后就不是字了不要用大幅裁剪会切掉笔画。2.3 构建 Dataset 和 DataLoader 的实操代码预处理决策定下来之后用 PyTorch 的 Dataset 封装import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import os class ChineseHandwritingDataset(Dataset): def __init__(self, root_dir, label_map, img_size64, augmentFalse): self.samples [] self.label_map label_map self.img_size img_size # 基础变换转灰度、缩放、转张量、归一化 base_tf [ transforms.Grayscale(num_output_channels1), transforms.Resize((img_size, img_size)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]), ] # 增强变换只加在训练集上 if augment: base_tf.insert(2, transforms.RandomAffine( degrees5, translate(0.1, 0.1), scale(0.9, 1.1) )) self.transform transforms.Compose(base_tf) for dirpath, _, filenames in os.walk(root_dir): label_name os.path.basename(dirpath) if label_name not in label_map: continue for f in filenames: if f.lower().endswith((.png, .jpg, .bmp)): self.samples.append( (os.path.join(dirpath, f), label_map[label_name]) ) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] img Image.open(img_path) img self.transform(img) return img, label关键参数说明RandomAffine的degrees5是旋转范围translate(0.1, 0.1)是水平和垂直平移比例scale(0.9, 1.1)是缩放范围。这三个值是我在多个手写汉字数据集上试出来的安全区间再大就容易产生不合理的样本。Normalize的 mean 和 std 先用 0.5 占位实际训练时替换成上一步算出来的值。DataLoader 的num_workers建议设为 CPU 核数的 2 倍pin_memoryTrue在 GPU 训练时能加速数据传输。batch size 从 64 起步显存够就往上加。3. 模型选型从 MNIST 的 LeNet 到能扛住 3755 类的手写汉字识别网络3.1 为什么 LeNet 和简单 CNN 不够用MNIST 上 LeNet 能到 99%因为数字只有 10 类笔画结构简单类间差异大。手写汉字识别面临三个 LeNet 解决不了的问题第一类别数从 10 跳到 3755最后的全连接层参数量暴增容易过拟合第二汉字笔画细、结构复杂浅层卷积感受野不够抓不到全局结构信息第三不同人写的同一个字在笔画粗细、倾斜角度、部件比例上差异巨大需要模型有一定的空间不变性。常见做法是换成 ResNet 系列作为 backbone。ResNet-18 或 ResNet-34 在 64×64 输入上表现稳定残差连接能缓解深层网络的梯度消失问题。如果追求更高精度可以上 DenseNet 或 SE-ResNet但推理速度会慢一些。我一般先用 ResNet-18 跑通全流程确认数据和训练策略没问题之后再换更大的模型对比。3.2 用 ResNet-18 改造手写汉字分类器的完整代码import torch import torch.nn as nn from torchvision import models def build_chinese_resnet(num_classes, pretrainedFalse): # 从 torchvision 加载 ResNet-18 model models.resnet18(pretrainedpretrained) # 第一层卷积改成单通道输入适配灰度图 model.conv1 nn.Conv2d( 1, 64, kernel_size7, stride2, padding3, biasFalse ) # 替换最后的全连接层输出类别数改为汉字类别数 in_features model.fc.in_features model.fc nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) return model # 假设有 3755 个类别 num_classes 3755 model build_chinese_resnet(num_classes) print(输出类别数:, model.fc[-1].out_features) print(总参数量:, sum(p.numel() for p in model.parameters()) / 1e6, M)这段代码做了两处关键修改第一conv1从 3 通道改成 1 通道因为手写汉字通常是灰度图第二fc层前面加了 Dropout(0.3)因为 3755 类的分类任务过拟合风险很高。pretrainedFalse是因为 ImageNet 预训练权重是针对自然图像的和手写汉字的特征分布差异较大迁移效果有限。如果你手头有大规模手写汉字预训练权重可以加载后再微调。参数量方面ResNet-18 大约 11M 参数换成 3755 类之后 fc 层会增加约 1.9M 参数总体在 13M 左右单张 8G 显存的卡跑 batch size 64 没问题。3.3 损失函数和优化器的选择分类任务用 CrossEntropyLoss 是标准做法但手写汉字数据集往往存在类别不平衡问题——常用字样本多生僻字样本少。两个应对策略一是在 loss 里加weight参数按类别频率的倒数加权二是用 Label Smoothing把硬标签变成软标签缓解模型对少数类的过度自信。import torch.nn as nn import torch.optim as optim # 带标签平滑的交叉熵损失 criterion nn.CrossEntropyLoss(label_smoothing0.1) # 优化器SGD 加动量或者 AdamW optimizer optim.SGD( model.parameters(), lr0.01, momentum0.9, weight_decay1e-4 ) # 学习率调度余弦退火 scheduler optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max50, eta_min1e-5 )label_smoothing0.1表示把真实标签的概率从 1.0 降到 0.9剩余 0.1 均匀分给其他类别。这个值不要设太大0.1 到 0.2 之间比较合适。优化器方面SGD 加动量在分类任务上通常比 Adam 泛化更好但收敛慢一些如果训练时间紧用 AdamW 把学习率设到 1e-3 也能跑出不错的结果。余弦退火让学习率从 0.01 平滑降到 1e-5避免训练后期震荡。4. 训练流程与参数调优让手写汉字识别模型真正收敛4.1 完整训练循环的代码骨架import torch from torch.utils.data import DataLoader from tqdm import tqdm def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() total_loss 0 correct 0 total 0 for imgs, labels in tqdm(dataloader, desc训练中): imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() # 梯度裁剪防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() * imgs.size(0) _, predicted outputs.max(1) correct predicted.eq(labels).sum().item() total labels.size(0) return total_loss / total, correct / total def evaluate(model, dataloader, criterion, device): model.eval() total_loss 0 correct 0 total 0 with torch.no_grad(): for imgs, labels in dataloader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) total_loss loss.item() * imgs.size(0) _, predicted outputs.max(1) correct predicted.eq(labels).sum().item() total labels.size(0) return total_loss / total, correct / total训练循环里有几个细节值得注意。clip_grad_norm_的max_norm5.0是梯度裁剪阈值手写汉字识别任务里梯度爆炸不算常见但加上这个能防止个别 batch 把参数带偏。评估阶段一定要用model.eval()和torch.no_grad()前者关闭 Dropout 和 BatchNorm 的训练行为后者省显存。4.2 学习率、batch size 和训练轮数的配合关系这三个参数不是独立的需要配合调整。我的一般策略是参数推荐范围调整方向初始学习率0.01 ~ 0.1 (SGD)大了震荡小了收敛慢batch size64 ~ 256受显存限制越大越稳训练轮数50 ~ 100配合余弦退火看验证集精度weight_decay1e-4 ~ 1e-3过拟合时加大如果训练 loss 下降但验证 loss 上升说明过拟合了优先加数据增强和 weight_decay其次考虑减小模型。如果训练 loss 都不降先检查学习率是不是太大或者数据标签有没有问题。如果训练 loss 降到很低但验证精度上不去大概率是训练集和测试集分布不一致需要检查两个数据集的采集方式是否相同。4.3 用混淆矩阵定位形近字问题手写汉字识别里整体准确率往往被常用字拉高生僻字和形近字的错误被掩盖。训练完之后一定要看混淆矩阵from sklearn.metrics import confusion_matrix import numpy as np def get_confusion_matrix(model, dataloader, device, num_classes): model.eval() all_preds [] all_labels [] with torch.no_grad(): for imgs, labels in dataloader: imgs imgs.to(device) outputs model(imgs) _, predicted outputs.max(1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds, labelsrange(num_classes)) return cm cm get_confusion_matrix(model, test_loader, device, num_classes) # 找出混淆最多的类别对 confused_pairs [] for i in range(num_classes): for j in range(num_classes): if i ! j and cm[i][j] 5: confused_pairs.append((i, j, cm[i][j])) confused_pairs.sort(keylambda x: -x[2]) print(混淆最多的类别对前 10:, confused_pairs[:10])拿到混淆对之后可以针对性地补充这些类别的训练样本或者在损失函数里给这些类别对加惩罚项。形近字问题在手写汉字识别里是硬骨头靠调参解决不了根本问题最终还是靠数据和模型容量。5. 避坑与排查手写汉字识别落地时最容易翻车的五个地方5.1 坑一测试集和训练集来自不同分布现象训练集准确率 98%测试集只有 60%。原因chinese_test.zip 里的图片可能是不同人写的、不同设备扫描的和训练集采集方式不一致。手写汉字对书写风格极其敏感同一个人写的“口”和另一个人写的“口”在像素层面差异可能比“口”和“日”还大。解决先可视化对比训练集和测试集的样本确认风格差异。如果差异大要么在训练集里加入测试集风格的样本要么做域适应。最直接的办法是把测试集的一部分加入训练重新划分验证集。5.2 坑二类别标签映射错位现象模型训练 loss 正常下降但预测结果全部偏移比如输入“一”预测成“二”。原因构建 label_map 时文件夹遍历顺序和标签编号没有对齐。os.walk的遍历顺序是不确定的如果先遍历到的类别编号为 0后遍历到的为 1但测试时用了另一套映射就会整体错位。解决把 label_map 保存成 JSON 文件训练和推理用同一份。构建时先收集所有类别名排序后再分配编号import json import os def build_label_map(root_dir): classes sorted([ d for d in os.listdir(root_dir) if os.path.isdir(os.path.join(root_dir, d)) ]) label_map {name: idx for idx, name in enumerate(classes)} with open(label_map.json, w, encodingutf-8) as f: json.dump(label_map, f, ensure_asciiFalse, indent2) return label_map排序这一步很关键保证每次运行得到的映射一致。5.3 坑三图片通道数不匹配导致推理报错现象训练时正常加载模型推理时提示expected input to have 1 channel, but got 3。原因训练时用了transforms.Grayscale转成单通道推理时忘了加这个变换PIL 读进来默认是 RGB 三通道。解决把预处理变换封装成一个函数训练和推理共用def get_inference_transform(img_size64, mean0.5, std0.5): return transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((img_size, img_size)), transforms.ToTensor(), transforms.Normalize(mean[mean], std[std]), ])推理时直接调用这个函数不要手写变换。5.4 坑四BatchNorm 在 batch size 太小时失稳现象训练 loss 剧烈震荡验证精度忽高忽低。原因ResNet 里的 BatchNorm 层在 batch size 小于 16 时统计量估计不准导致训练不稳定。解决要么把 batch size 加到 32 以上要么把 BatchNorm 换成 GroupNorm。GroupNorm 对 batch size 不敏感但收敛速度可能慢一点。如果显存实在不够用梯度累积模拟大 batchaccumulation_steps 4 optimizer.zero_grad() for i, (imgs, labels) in enumerate(dataloader): outputs model(imgs) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (i 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()5.5 坑五保存模型时只存了 state_dict 忘了存配置现象换一台机器加载模型不知道 num_classes 和 img_size 是多少加载失败。原因torch.save(model.state_dict())只保存参数不保存模型结构信息。解决保存时把配置一起存下来checkpoint { model_state_dict: model.state_dict(), num_classes: num_classes, img_size: 64, mean: 0.5, std: 0.5, label_map: label_map, } torch.save(checkpoint, chinese_ocr_best.pth)加载时先读配置再构建模型最后加载参数。这样无论换什么环境只要代码在就能复现。6. 进阶技巧用测试时增强和模型集成把手写汉字识别精度再推一档训练流程跑通之后如果精度还差几个点可以试试测试时增强TTA。思路很简单对同一张测试图片做多次轻微变换分别推理把 softmax 概率平均后取最大值。手写汉字识别里TTA 通常能带来 1 到 3 个点的提升代价是推理时间成倍增加。import torch.nn.functional as F def predict_with_tta(model, img_tensor, device, n_aug5): model.eval() # img_tensor 形状为 [1, 1, H, W] probs_sum torch.zeros(1, num_classes).to(device) with torch.no_grad(): # 原始图片 probs_sum F.softmax(model(img_tensor.to(device)), dim1) # 轻微旋转和缩放 for angle in [-3, 3, -5, 5][:n_aug - 1]: aug transforms.functional.rotate(img_tensor, angle) probs_sum F.softmax(model(aug.to(device)), dim1) avg_probs probs_sum / n_aug return avg_probs.argmax(dim1).item(), avg_probs.max().item()这段代码对输入图片做原始、±3 度、±5 度共 5 次推理把 softmax 输出平均。注意旋转角度不要超过 5 度否则汉字结构会变形。n_aug控制增强次数3 到 5 次是性价比比较高的区间再多提升有限但耗时线性增长。模型集成是另一个有效手段。训练 3 到 5 个不同初始化或不同 backbone 的模型推理时把概率平均。我一般用 ResNet-18、ResNet-34 和 DenseNet-121 各训一个集成后比单模型高 2 到 4 个点。代价是显存和推理时间适合对精度要求高、对延迟不敏感的场景。还有一个容易被忽略的技巧把分类损失和中心损失Center Loss结合。中心损失让同一类别的特征向量靠近类中心能显著改善形近字的区分度。实现上就是在 CrossEntropyLoss 基础上加一个0.003 * center_loss中心损失权重不要太大否则会破坏分类边界。最后说一个我自己的习惯每次训练完把验证集上预测错误的样本单独导出来按错误类型分类——是形近字混淆、笔画缺失还是书写风格差异。这个错误分析花不了多少时间但能告诉你下一步该补数据还是改模型比盲目调参有效得多。手写汉字识别没有银弹数据和模型的迭代才是正道。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

基于LSTM的电商评论情感分析:从数据清洗到模型部署的完整实战 2026/10/1 13:23:11

基于LSTM的电商评论情感分析:从数据清洗到模型部署的完整实战

简介:这份资源是面向计算机相关专业学生与Python实战学习者的深度学习项目包,以LSTM为核心完成电商购物评论的情感分析任务,可直接用于毕业设计、课程设计或期末大作业。项目围绕京东商城购物评论展开,涵盖数据采集、中文分词与停…

阅读更多 →
KMV与CCA循环违约建模:从原理到Python实战 2026/10/1 13:23:11

KMV与CCA循环违约建模:从原理到Python实战

简介:这份资源面向金融风险管理学习者与量化编程入门者,围绕CCA信用风险评估与KMV违约概率模型展开,重点演示如何通过循环结构逐时间节点计算企业违约距离,进而估计预期违约频率EDF。压缩包共7个文件,以m脚本、docx文档…

阅读更多 →
CrazyGames 远程公司档案全解析:remoteintech 目录中的 Remote-First 游戏平台 2026/10/1 13:23:11

CrazyGames 远程公司档案全解析:remoteintech 目录中的 Remote-First 游戏平台

数据集 【免费下载链接】remote-jobs Source for remoteintech.company — a community-maintained directory of remote-friendly tech companies 项目地址: https://gitcode.com/GitHub_Trending/re/remote-jobs 点击查看 免费下载 本文以 src/companies/crazyga…

阅读更多 →
基于LSTM的电商评论情感分析:从数据预处理到模型部署的完整实战指南 2026/10/1 13:23:04

基于LSTM的电商评论情感分析:从数据预处理到模型部署的完整实战指南

简介:这份资源是面向计算机相关专业学生与Python实战学习者的深度学习项目包,以LSTM为核心模型完成电商购物评论的情感分析任务,可直接用于毕业设计、课程设计或期末大作业。项目围绕京东商城购物评论展开,涵盖数据采集、中文分词…

阅读更多 →
自然语言处理大作业实战指南:从文本分类到BERT微调,拿高分的关键工程细节 2026/10/1 13:23:04

自然语言处理大作业实战指南:从文本分类到BERT微调,拿高分的关键工程细节

简介:这是一份面向自然语言处理课程期末大作业的完整项目包,来自作者大三学期经导师指导并获得98分评审的高分作品,适合计算机相关专业学生、课程设计者以及需要项目实战练习的NLP学习者。压缩包共275个文件,约128.51MB&#xff0…

阅读更多 →
Python超市管理系统毕设全攻略:Flask+MySQL从建表到部署 2026/10/1 13:22:57

Python超市管理系统毕设全攻略:Flask+MySQL从建表到部署

每年计算机毕业设计选题里,“Python超市管理系统”都能排到前三。专科本科都有人选,有的图省事找个源码改改,有的真想从零敲出一个能演示的系统。这个题目看起来简单,但真要做扎实并不容易:要有能跑的界面、能看的业务…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉