十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于CNN的手写汉字识别:从数据预处理到Flask部署实战

基于CNN的手写汉字识别:从数据预处理到Flask部署实战 简介这是一份基于CNN神经网络实现手写汉字识别分类的Python项目源码适合正在做课程设计、期末大作业的计算机专业学生以及需要项目实战练习的开发者。项目以98分通过导师指导认可针对汉字结构复杂、类别多样等难点提供了完整的数据预处理、CNN模型搭建、训练评估与可视化交互方案。压缩包共42个文件大小约33.72MB主要包含6个Python脚本run.py、train_MyModel.py、views.py等、1个pkl模型权重文件、HTML/CSS/JS构建的Web界面以及XML配置、TTC字体、PNG/JPG图片素材和README.md说明文档train_model与src目录划分清晰便于按需学习与二次修改。其中XML/iml配置面向IDE工程db文件可记录数据集git配置则便于版本管理。目前已有285人学习使用。借助该资源读者可复现并理解CNN在中文手写识别中的完整流程学习模型持久化、Flask前后端整合等实战技巧也可作为高分课程设计模板快速应用到自己的项目或毕业设计中。1. 手写汉字识别为什么最终选了 CNN手写汉字识别和手写数字识别MNIST完全是两个难度等级。MNIST 只有 10 类、28x28 的灰度图而汉字常用字就有 3755 个国标一级字库不同人的笔迹在笔画粗细、结构比例、连笔方式上差异极大。用传统的全连接网络做这件事第一个问题就是参数量爆炸——一张 128x128 的图展开成 16384 维向量第一层全连接接 1024 个神经元就是 1600 多万参数训练集不够大时直接过拟合而且完全丢失了像素之间的空间结构关系。项目正文里包含 cnn_handwritten_chinese_recognition-master 完整工程目录有训练脚本、Flask 推理界面和模型权重可以直接跑通从训练到 Web 部署的完整链路。本文按这个项目的实际组织方式把数据加载、模型搭建、训练参数设定和部署验证四个环节拆开讲。2. 数据集与预处理流程2.1 数据来源与目录组织这个项目的训练数据放在src目录下标准做法是每个类别一个子文件夹文件夹名即汉字标签。目录结构大致长这样src/ ├── 0/ │ ├── 0_001.png │ ├── 0_002.png │ └── ... ├── 1/ ├── 2/ └── ...这种按类别分目录的组织方式有一个好处PyTorch 的torchvision.datasets.ImageFolder可以直接读取它会自动把子目录名映射为整数标签。注意标签是从 0 开始按字母序排列的所以训练之前要单独存一份类别名 - 索引的映射字典否则推理时拿到索引号也不知道是哪个汉字。我一般会把这映射存成 JSON 文件放在src同级目录下避免训练和推理时两次扫描目录得到不同的顺序。2.2 图像预处理参数设定汉字图像预处理是整个流程里最容易被低估的环节。项目里使用torchvision.transforms做在线增强核心配置如下transform_train transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((128, 128)), transforms.RandomRotation(degrees10), transforms.RandomAffine(degrees0, translate(0.05, 0.05)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ])这里几个参数需要解释。Resize((128, 128))是精度和速度的折中汉字笔画细节多64x64 会丢失顿笔和折笔信息256x256 则让训练时间翻好几倍。RandomRotation的 degrees 只给 10 度不能给大——汉字是有朝向规范的文字旋转超过 15 度会让模型学到错误的姿态不变性反而干扰真实场景中的识别。RandomAffine的 translate 参数表示水平和垂直方向最大平移比例5% 的平移能提升对书写位置偏移的鲁棒性。2.3 数据加载与迭代器数据加载部分项目里应该是用DataLoader做批次迭代。关键参数要这样设from torch.utils.data import DataLoader train_loader DataLoader( train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue )batch_size64是根据常见 GPU 显存6-8GB设定的如果你的显卡只有 4GB 显存降到 32否则会 OOM。num_workers4让数据加载和 GPU 计算并行CPU 核数少就设成 2。pin_memoryTrue在 GPU 训练时能减少 CPU 到 GPU 的数据拷贝时间能开就开。验证集不要做增强只 Resize、ToTensor、Normalize否则验证集分布和训练集不一致loss 曲线会周期性震荡。3. 面向汉字识别的 CNN 模型结构设计3.1 为什么用卷积神经网络而不是全连接网络卷积神经网络CNN处理图像的核心优势在参数共享和局部感受野。一个 3x3 的卷积核在整张图上滑动同样一组的 9 个权重被反复使用参数量远小于全连接层而且卷积操作天然保留像素的邻域关系笔画这种局部特征在不同位置被同一组权重提取正好契合汉字「横、竖、撇、捺」在不同区域反复出现的特性。如果换用 Transformer 或者 MLP要么需要海量数据要么参数量大到在课程设计这种规模的数据集上完全训不动。3.2 网络结构与通道数设计这个项目的模型定义在train_model或src下的模型文件里。一个适合汉字识别的 CNN 结构可以这样组织import torch.nn as nn class CNN_Hanzi(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(128, 256, kernel_size3, padding1), nn.BatchNorm2d(256), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(256, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, num_classes) ) def forward(self, x): return self.classifier(self.features(x))每一层卷积后都接BatchNorm2d作用是把每批数据的特征分布拉回均值为 0、方差为 1 的状态这样可以用更大的学习率而不会梯度爆炸。汉字类别数多最后全连接层的输入维度需要足够大——中间层从 32 逐层翻倍到 256就是在不断抽象笔画的组合模式浅层学横竖撇捺中层学偏旁部首深层学整体字形结构。Dropout(0.5)只放在全连接层前卷积层不放因为卷积层本身参数共享已经有一定正则化效果。3.3 参数量和计算量估算按输入 128x128、类别数 3755 来估算nn.Linear(512, 3755)这部分有约 192 万参数整个模型参数量在 290 万左右。作为对比如果第一层就用全连接接 128x128 的输入同样到 256 维参数就是 128x128x256 ≈ 419 万这还只是第一层。而且全连接网络不具有平移等变性同一个笔画往左偏两个像素所有激活值都变了模型必须靠大量扩增数据来硬学这种变化——CNN 因为卷积核滑动共享权重天然免疫这个问题。4. 训练流程、超参数策略与模型保存4.1 损失函数与优化器选择汉字识别是一个多分类问题损失函数用交叉熵nn.CrossEntropyLoss。这个函数把模型输出的 logits 内部做 softmax所以模型最后一层不需要额外接 softmax 激活。优化器项目里选的是 AdamPyTorch 里按下面的方式初始化import torch.optim as optim criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4)这里的lr1e-3是 Adam 的常见起点。weight_decay1e-4是 L2 正则化的系数系数过大模型欠拟合过小起不到约束作用。如果换用 SGD学习率通常要调到 1e-2 左右并配合 momentum0.9收敛更慢但泛化可能更好——课程设计项目按默认 Adam 就能拿不错的效果。4.2 学习率调度与早停训练部分项目里是封装在train_MyModel这类脚本里的。完整训练循环会做学习率衰减用ReduceLROnPlateau按验证集 loss 变化来调整scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, verboseTrue ) for epoch in range(30): model.train() for images, labels in train_loader: outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() val_loss evaluate(model, val_loader) scheduler.step(val_loss)scheduler.step(val_loss)每轮用验证集 loss 去判断如果连续 5 轮patience5loss 没有下降就把学习率乘 0.5。这样可以避免 loss 在平台期反复震荡。这个项目里我还建议加一条早停逻辑——连续 10 轮验证集准确率不提升就终止训练不然在 3755 类的任务上每一轮都是几分钟到几十分钟的 GPU 开销。4.3 训练日志与性能基准训练的监控维度主要有三个训练集 loss、验证集 loss、Top-1 准确率。汉字 3755 类分类的 Top-1 准确率能过 95% 就已经是很能看的成绩实际部署时还会看 Top-5即模型预测概率最大的前 5 个候选里有没有正确答案Top-5 到 99% 左右说明模型对易混字形近字的判断基本可用。训练时把每个 epoch 的这三个数打印到控制台或者写到logs/目录下的 CSV 文件里后面画 loss 曲线做答辩展示也方便。模型保存要注意不能只存model.state_dict()还要把类别映射 JSON 一起存。推荐直接存成 tar 包checkpoint { model_state: model.state_dict(), class_to_idx: train_dataset.class_to_idx, idx_to_class: {v: k for k, v in train_dataset.class_to_idx.items()} } torch.save(checkpoint, checkpoints/best_model.tar)加载的时候先torch.load再重建模型结构否则推理时不知道类别顺序。4.4 GPU 显存不足的替代方案如果你的机器没有 NVIDIA GPU代码里所有.cuda()调用需要做一个设备判断常见做法是定义device torch.device(cuda if torch.cuda.is_available() else cpu)然后把模型和数据都.to(device)。纯 CPU 训练这个规模的网络预期速度约为 GPU 的 1/20 到 1/30所以数据集超过 2 万张时建议先跑 5 个 epoch 做 smoke test确认 loss 在下降再挂机全量训练。此外可以把num_workers调大一些让 CPU 的数据加载瓶颈尽量缓解。5. Flask 推理接口与项目路由配置5.1 Flask 应用的文件结构和路由项目里app/目录下有views.py和__init__.py这是一个 Flask 应用的标准组织方式——__init__.py创建应用工厂views.py注册路由static放前端静态资源templates放 HTML 模板。启动入口是根目录的run.py核心逻辑是加载训练好的模型把用户上传或手写的图片做预处理后送入模型返回预测结果。推理接口的路由大致长这样from flask import Flask, request, jsonify from PIL import Image import torch from torchvision import transforms app Flask(__name__) model load_model(checkpoints/best_model.tar) model.eval() transform_infer transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ]) app.route(/predict, methods[POST]) def predict(): file request.files[image] image Image.open(file.stream) tensor transform_infer(image).unsqueeze(0) with torch.no_grad(): logits model(tensor) probs torch.softmax(logits, dim1) top5 torch.topk(probs, k5) results [{ char: idx_to_class[idx.item()], confidence: round(prob.item(), 4) } for prob, idx in zip(top5.values[0], top5.indices[0])] return jsonify({predictions: results})这段代码里有几个细节要强调。model.eval()必须调用——它关闭了 Dropout 和 BatchNorm 的批次统计模式否则同样的图片两次推理结果不同。torch.no_grad()关闭梯度计算推理时省内存还能提速。unsqueeze(0)给单张图加一个 batch 维度因为模型期望输入是 (N, 1, 128, 128) 的四维张量。查询接口返回 Top-5 而不是只返回 Top-1这样前端可以把候选字列出来让用户点选实际体验比直接给一个结果好得多。5.2 真实部署中会遇到的两个坑第一个坑PIL 打开用户上传的图片可能是 RGBA 四通道transforms.Grayscale会做转换但如果图是modeL的灰度图Image.open的stream对象读取不到文件末尾——需要在Image.open(file.stream)之前加上file.stream.seek(0)否则偶尔会报cannot identify image file的错。第二个坑模板渲染和静态资源路径。如果前端页面里引用了{{ url_for(static, filenamejs/main.js) }}要确认templates和static目录和views.py在同一个 Flask 应用根下否则 404 的可能是 JS 和 CSS不是接口本身。调试的时候建议先用curl -X POST -F imagetest.png http://127.0.0.1:5000/predict测接口返回绕过前端直接验证模型效果。5.3 在浏览器里做手写输入的小技巧templates下如果是一个 HTML 页面带 canvas 画板前端获取 canvas 里的手写笔迹并传给后端用 JavaScript 把 canvas 内容转成 Blob 再 POST 到/predict免去用户先画完再上传文件的割裂感。canvas 转图片的核心代码如下canvas.toBlob(function(blob) { var formData new FormData(); formData.append(image, blob, handwrite.png); fetch(/predict, { method: POST, body: formData }) .then(res res.json()) .then(data { renderCandidates(data.predictions); }); }, image/png);这里的canvas.toBlob会把画板内容编码成 PNG 格式的 Blob 对象FormData.append的第三个参数是文件名Flask 的request.files[image]拿到的就是一个合法的 FileStorage 对象后端逻辑不用改。注意前端 canvas 不要设 CSS 缩放画板元素的实际像素尺寸和显示尺寸不一致时画出来的字会被拉伸变形识别率直线下降。5.4 模型加载失败时的排查顺序运行run.py后如果接口报错排查顺序是先看控制台有没有FileNotFoundError——模型路径checkpoints/best_model.tar是相对路径从项目根目录启动没问题是常识但如果你用 IDE 单独运行app/__init__.py工作目录变了就找不到文件再检查torch.load的 map_location 参数训练用的 GPU 保存的权重加载到 CPU 机器上要加map_locationtorch.device(cpu)否则会报 cuda 相关的 RuntimeError最后看类别数量是否和模型最后一层输出维度一致如果重新训练时改了num_classes但还在用旧的 tar 包加载时load_state_dict会报 size mismatch这时候需要重新训练或者从 checkpoint 里只取features部分的权重。本文还有配套的精品资源点击获取
返回列表