资讯动态

手写汉字识别实战:从MNIST到深度卷积网络的代码实现与避坑指南

发布时间:2026/10/1 4:55:47 来源:尧图企业网站定制
简介这份资源面向深度学习入门者与计算机视觉方向的学习者聚焦手写汉字识别这一典型图像分类任务帮助读者理解如何借助深度卷积网络完成从样本到识别结果的完整流程。压缩包内共1个文件为Python脚本整体约1KB属于轻量级代码示例便于快速阅读与二次修改。脚本内容预计涵盖数据加载与预处理、卷积网络结构定义、训练与验证循环以及模型评估等核心环节可对照MNIST的经典处理思路迁移到汉字识别场景。目前已有177人学习下载适合希望以最小成本跑通手写汉字识别基线、理解DCN在复杂字形上特征提取逻辑的读者参考也可作为课程实验或项目起步的脚手架。1. 拆开 chinese_test.zip一个能跑的手写汉字识别起点如果你正在找一份能直接跑起来的手写汉字识别代码而不是又一篇讲 CNN 原理的科普那chinese_test.zip值得花十分钟拆开看看。它里面只有一个chinese_test.py配合一份手写汉字样本数据用深度卷积网络完成从图像到汉字类别的映射。这正好卡在「mnist手写数字识别」和真实汉字识别之间的过渡地带——MNIST 那套 28×28 灰度图、归一化、卷积池化堆叠的流程可以直接借鉴但汉字类别更多、笔画结构更复杂直接套 MNIST 的 LeNet 会翻车。这份资源适合已经跑通过手写数字识别、想把手感迁移到汉字场景的开发者也适合需要快速验证某个预处理或网络结构改动是否有效的熟手。它不解决通用 OCR 的全部问题但能让你在一个干净的小闭环里看清数据怎么进、卷积怎么提特征、损失怎么收敛、结果怎么评估。2. 从 MNIST 到汉字识别数据管线与网络选型的落地差异2.1 为什么不能直接复用 MNIST 的输入尺寸和类别数MNIST 是 10 类、28×28 单通道灰度图而手写汉字识别的类别数取决于你用的字表。常见做法是先从几百个常用字起步输入尺寸拉到 32×32 或 64×64保留单通道灰度。这里有个容易被忽略的点汉字笔画密度远高于数字28×28 下很多相似字比如「未」和「末」的区分特征会被池化层吃掉。我一般会把输入统一到 64×64第一层卷积核从 3×3 起步步长 1padding 设为 same先保住空间分辨率再靠后续池化降维。chinese_test.py里数据加载部分大概率是遍历文件夹、按类别名做标签映射然后做归一化和尺寸调整。如果你拿到的样本是按字分文件夹存放的下面这段预处理逻辑可以直接抄import os import cv2 import numpy as np from sklearn.model_selection import train_test_split def load_dataset(root_dir, img_size64): images, labels [], [] class_names sorted(os.listdir(root_dir)) for idx, char in enumerate(class_names): char_dir os.path.join(root_dir, char) if not os.path.isdir(char_dir): continue for fname in os.listdir(char_dir): img_path os.path.join(char_dir, fname) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: continue # 先二值化再缩放避免插值把笔画糊掉 _, img cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) img cv2.resize(img, (img_size, img_size), interpolationcv2.INTER_AREA) images.append(img) labels.append(idx) X np.array(images, dtypenp.float32) / 255.0 y np.array(labels, dtypenp.int64) X X.reshape(-1, img_size, img_size, 1) return train_test_split(X, y, test_size0.2, random_state42, stratifyy), class_names逻辑说明先做 OTSU 二值化是为了把扫描或拍照带来的灰度渐变压成黑白两级减少光照不均对卷积核的干扰INTER_AREA在缩小图像时比默认的双线性更少产生伪影。参数上img_size建议从 64 起步如果你的样本本身是 128×128 以上且笔画细可以拉到 96 或 128但显存和训练时间会线性上涨。stratifyy保证每个字在训练集和验证集里的比例一致否则某些字可能全被分到一边验证准确率会变成玄学。2.2 卷积网络结构怎么搭才不浪费算力chinese_test.py里用的深度卷积网络结构上大概率是「卷积-池化」堆叠若干次后接全连接。对于汉字识别我一般会控制在 4 到 6 个卷积块每块两个 3×3 卷积加一个 2×2 最大池化通道数从 32 翻到 64、128、256。再深的话在小样本上容易过拟合除非你加了 BatchNorm 和 Dropout。下面是一个可以直接替换进脚本的 Keras 风格定义from tensorflow.keras import layers, models def build_dcn(num_classes, input_shape(64, 64, 1)): model models.Sequential([ layers.Conv2D(32, (3, 3), paddingsame, activationrelu, input_shapeinput_shape), layers.BatchNormalization(), layers.Conv2D(32, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), paddingsame, activationrelu), layers.BatchNormalization(), layers.Conv2D(64, (3, 3), paddingsame, activationrelu), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), paddingsame, activationrelu), layers.BatchNormalization(), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(256, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ]) return model逻辑说明每个卷积块里两个 3×3 卷积叠在一起感受野等效于一个 5×5但参数量更少非线性更强。BatchNorm 放在卷积和激活之间还是之后Keras 里默认顺序是 Conv-BN-Activation我上面写的是 Conv-Activation-BN实际跑的时候建议改成 Conv-BN-ReLU收敛更稳。Dropout 只加在全连接层前卷积层不加因为卷积核本身有参数共享过拟合风险相对低。num_classes就是你字表的字数如果只有几十个字最后一层 Dense 可以降到 128防止参数过多。2.3 训练循环里损失函数和优化器的选择汉字识别是多分类任务损失函数用 categorical crossentropy标签需要 one-hot如果标签是整数就用 sparse categorical crossentropy。优化器我习惯用 Adam学习率从 1e-3 起步配合 ReduceLROnPlateau 在验证损失不降时减半。chinese_test.py里如果用的是 SGD收敛会慢很多除非你调了动量。下面这段训练配置可以直接嵌进去from tensorflow.keras import optimizers, callbacks model build_dcn(num_classeslen(class_names)) model.compile( optimizeroptimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[accuracy] ) callbacks_list [ callbacks.ReduceLROnPlateau(monitorval_loss, factor0.5, patience3, min_lr1e-6), callbacks.EarlyStopping(monitorval_loss, patience8, restore_best_weightsTrue) ] history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs50, batch_size64, callbackscallbacks_list )逻辑说明patience3表示验证损失连续 3 个 epoch 不降就减半学习率min_lr防止学习率降到 0 导致训练停滞。EarlyStopping的restore_best_weightsTrue是关键否则你拿到的可能是过拟合后的权重。batch_size 设 64 是在显存和梯度稳定性之间的折中如果样本少于 5000可以降到 32。训练时盯着val_accuracy如果它比accuracy低超过 10 个百分点说明过拟合已经很明显需要加数据增强或减网络深度。3. 把 chinese_test.py 跑起来环境、参数与验证步骤3.1 环境依赖和最小可运行配置chinese_test.py是 Python 脚本依赖无外乎 TensorFlow 或 PyTorch、OpenCV、NumPy、scikit-learn。我建议用 conda 建一个干净环境Python 3.8 到 3.10 之间TensorFlow 2.x 或 PyTorch 1.12 以上。下面这套命令在我这边跑通过直接抄conda create -n chinese_ocr python3.9 -y conda activate chinese_ocr pip install tensorflow2.12.0 opencv-python4.8.0.74 numpy1.24.3 scikit-learn1.3.0 matplotlib3.7.2逻辑说明TensorFlow 2.12 对 CUDA 11.8 支持较好如果你用 CPU 跑把tensorflow换成tensorflow-cpu可以省掉一堆显卡驱动报错。OpenCV 用 4.8 是因为cv2.threshold和cv2.resize的接口稳定老版本 3.x 在INTER_AREA上行为略有差异。scikit-learn 只用来做 train_test_split 和混淆矩阵版本不敏感。3.2 数据目录结构和标签映射的坑chinese_test.zip解压后样本数据大概率是按字分文件夹的。如果你拿到的是一张张散图加一个标签文件需要先整理成root/字/图片的结构。下面这个脚本可以把「图片名即标签」的格式转成文件夹结构import os import shutil def reorganize_by_label(src_dir, dst_dir): os.makedirs(dst_dir, exist_okTrue) for fname in os.listdir(src_dir): if not fname.lower().endswith((.png, .jpg, .jpeg, .bmp)): continue # 假设文件名格式为 字_序号.png取第一个下划线前的部分作为标签 label fname.split(_)[0] label_dir os.path.join(dst_dir, label) os.makedirs(label_dir, exist_okTrue) shutil.copy(os.path.join(src_dir, fname), os.path.join(label_dir, fname))逻辑说明split(_)[0]只适用于文件名里用下划线分隔标签的情况如果你的命名规则不同改这一行就行。复制而不是移动是为了保留原始数据万一整理错了还能重来。整理完之后load_dataset里的sorted(os.listdir(root_dir))会按字排序生成类别索引这个索引顺序在训练和推理时必须一致否则预测结果会张冠李戴。3.3 训练过程监控和模型保存训练时除了看 loss 和 accuracy我建议把混淆矩阵和错分样本可视化出来。chinese_test.py如果只打印准确率你很难知道模型到底在哪些字上翻车。下面这段代码可以在训练结束后跑一次验证集评估import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns y_pred model.predict(X_val) y_pred_classes np.argmax(y_pred, axis1) print(classification_report(y_val, y_pred_classes, target_namesclass_names)) cm confusion_matrix(y_val, y_pred_classes) plt.figure(figsize(12, 10)) sns.heatmap(cm, annotFalse, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png, dpi150)逻辑说明classification_report会给出每个字的 precision、recall、f1-score如果某个字的 recall 明显低说明模型把它和别的字搞混了。混淆矩阵热力图能直观看到哪两个字之间误判最多比如「日」和「曰」、「己」和「已」这些往往是笔画长短或开口方向差异需要针对性做数据增强。模型保存用model.save(chinese_dcn.h5)加载时用tf.keras.models.load_model注意自定义层或损失函数要加custom_objects参数。4. 避坑与排查手写汉字识别里最容易翻车的五个点4.1 现象训练准确率很高验证准确率死活上不去原因样本量太少或者类别不均衡模型把多数类背下来了。汉字识别里常用字和生僻字的样本数可能差几十倍stratify只能保证划分比例不能解决总量不足。解决对样本少的字做数据增强旋转 ±10 度、平移 10%、轻微弹性形变用ImageDataGenerator或albumentations在线生成。同时把Dropout从 0.5 提到 0.6全连接层神经元减半。4.2 现象模型把「未」和「末」全判成同一个字原因输入分辨率太低两个字的横画长短差异在池化后消失了。解决把img_size从 32 提到 64 或 96第一层卷积不要急着池化先做两个 3×3 卷积保住细节。如果还不行在数据预处理阶段对图像做骨架化或笔画方向特征提取把横画长度作为显式特征拼进去。4.3 现象训练 loss 震荡剧烈偶尔跳到 nan原因学习率太大或者输入数据没有归一化到 0-1 之间。解决检查X X / 255.0这行有没有漏掉Adam 的学习率从 1e-3 降到 1e-4加梯度裁剪clipnorm1.0。如果用了 BatchNorm检查 batch_size 是不是太小小于 16 时 BN 的统计量不稳定。4.4 现象推理时单张图片预测结果和验证集对不上原因推理时的预处理和训练时不一致比如训练用了 OTSU 二值化推理时直接传了灰度图。解决把预处理逻辑封装成一个函数训练和推理都调同一个。另外检查输入 shape 是不是(1, 64, 64, 1)少一维会报错多一维会静默出错。4.5 现象模型文件加载报「Unknown layer」或「Unknown loss」原因保存时用了自定义层或自定义损失函数加载时没有注册。解决如果是自定义层在load_model时传custom_objects{MyLayer: MyLayer}。更省事的做法是保存权重model.save_weights(weights.h5)加载时先重建结构再load_weights这样不依赖序列化。5. 进阶技巧用迁移学习和学习率预热把准确率再拉一截如果你已经把chinese_test.py跑通验证准确率卡在 85% 左右上不去可以试试迁移学习。拿一个在 ImageNet 上预训练过的轻量网络比如 MobileNetV2 或 EfficientNetB0把输入改成单通道去掉顶层接自己的全连接分类头。下面这段代码展示怎么在 Keras 里做from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, models base MobileNetV2(input_shape(64, 64, 3), include_topFalse, weightsimagenet) base.trainable False model models.Sequential([ layers.Input(shape(64, 64, 1)), layers.Conv2D(3, (3, 3), paddingsame, activationrelu), # 单通道转三通道 base, layers.GlobalAveragePooling2D(), layers.Dense(256, activationrelu), layers.Dropout(0.5), layers.Dense(num_classes, activationsoftmax) ])逻辑说明第一层 3×3 卷积把灰度图升到三通道是为了适配 ImageNet 预训练权重的输入要求。base.trainable False先冻结主干只训练分类头等 loss 稳定后再解冻最后几个 block 做微调学习率降到 1e-5。GlobalAveragePooling2D替代 Flatten参数量少很多小样本上更不容易过拟合。学习率预热是另一个立竿见影的技巧。前 5 个 epoch 把学习率从 1e-5 线性升到 1e-3再按余弦退火降下去。这样模型不会一开始就跳进局部最优尤其在你换了新数据或新结构的时候。我一般用tf.keras.optimizers.schedules.CosineDecay配合WarmUp回调或者手写一个LearningRateScheduler。验证方法上除了看整体准确率我习惯把验证集里置信度低于 0.6 的样本单独拎出来看一遍。这些是模型的「犹豫区」往往对应着书写潦草或标注错误的样本。把标注错的挑出来修正比调网络结构带来的提升更直接。从那以后我每次跑完训练都强制走一遍「低置信度样本人工复核」的流程十次里有三次能发现标签问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑