ARTICLE DETAIL

资讯详情

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

手写汉字识别实战:从CNN模型训练到部署的完整指南

手写汉字识别实战:从CNN模型训练到部署的完整指南 简介面向计算机相关专业的课程设计与期末大作业场景这份基于CNN神经网络实现的手写汉字识别分类Python源码项目是经导师指导并认可通过的98分大作业。资源覆盖从模型搭建、训练、评估到Web端可视化识别的完整链路适合需要完整课题方案或项目实战练习的学习者。压缩包内共42个文件以Python脚本、PNG图片、JS/CSS前端文件、XML配置为主同时包含模型权重pkl、字体文件、HTML页面及项目文档等整体大小33.72MB目录结构按功能模块划分便于对照学习。已有285人学习浏览。参考该项目可理解CNN卷积层、池化层、全连接层的设计思路掌握手写汉字数据集的加载与增强方法以及模型训练、保存和部署调用的完整流程。代码注释清晰可直接运行测试也支持在此基础上扩展更多类别或优化网络结构。1. 手写汉字识别不是“CNN能跑就行”而是几十个工程点在排队手写汉字识别和手写数字识别最大的差别不在网络结构而在“类别数”和“形状密度”。MNIST 只有 10 类随便一个两层卷积就能到 99% 以上但常用汉字按 GB2312 一级字表也有 3755 类加上二级字就是 6763 类。类别一多全连接层的参数量会爆炸Softmax 输出的概率分布会被压得极扁训练时的收敛速度、学习率策略、数据增强都会变得敏感。更麻烦的是汉字结构复杂形近字多“未”和“末”、“土”和“士”只差一笔的长短笔画稍微歪一点就崩。所谓“基于 CNN 神经网络实现的手写汉字识别分类”本质上不是调一个现成模型而是要把数据管线、网络结构、训练参数、验证手段四件事同时做对。这篇就按做这类项目最常见的落地路径从数据组织讲到模型设计再给一份能直接改的训练配置最后落在“如何验证你的识别器真的可用”上。适合正在做人工智能大作业、准备简历项目、或者想把 OCR 能力往垂直领域推进一步的工程师。2. 数据与预处理从 HWDB 到能直接喂进 CNN 的 Tensor2.1 数据集长得像“字典”不像 MNIST——gnt 转 png 的取舍手写汉字识别绕不开 CASIA-HWDB 数据集。HWDB1.0 和 HWDB1.1 是脱机手写样本库前者覆盖 3740 类、约 123 万汉字后者覆盖 3755 类、约 105 万汉字加起来超过 200 万样本按 8:2 切训练集和验证集就已经足够训练一个像样的 CNN。但它的原始格式是.gnt不是png和jpg需要自己解析。解析.gnt的常见做法是先读 4 字节总长度、4 字节图片宽度、4 字节图片高度再读 2 字节标签编码GBK 编码的汉字最后读宽乘高个字节的灰度像素按 0 到 255 填进numpy数组输出成单通道灰度图。需要注意.gnt内部宽度和高度是uint16标签是GBK的两个字节直接decode(gbk)就可以。如果只想要“能跑通”的结果可以只取 GK 编码范围的常用字子集比如自己维护一个“目标字表”从原始数据里过滤出这些字避免一开始就面对全量 3755 类的训练时长。还有一个常见选择是直接用别人预处理好的“HWDB png 版本”这类数据在中文技术社区里流传很广一般按“train / test”分好目录目录名就是汉字。用这种数据效率高但要警惕来源不明的数据里混入了重复样本导致验证集指标虚高。我的建议是拿到数据后先算一遍所有图片的md5把完全相同的图片去重再去切分数据集。别省这一步否则后面查“为什么 loss 降不下去”的时候会多绕很多弯路。表手写汉字识别数据集选型对比数据来源类别数样本量级格式适合场景CASIA-HWDB1.03740约 123 万GNT论文级、完整模型CASIA-HWDB1.13755约 105 万GNT同左可与 1.0 合并自制语料 字体渲染自定不设上限PNG专有字表、垂直场景社区分发 png 版不定不定PNG快速验证、大作业不建议一上来就把 3755 类全灌进网络。类别数越多FC 层越大训练一轮的时间越长排错也越困难。先拿 100 到 300 类的子集把整个流程跑通再逐步扩类这是做这类项目最稳的节奏。2.2 预处理管线灰度归一化、字形居中和数据增强手写汉字图片预处理有四个关键点尺寸统一、背景处理、居中对齐、增强策略。尺寸上常见做法是把图片缩放为 64x64 或 96x96。过小的分辨率会丢失笔画细节形近字更难分过大的分辨率会增加全连接层参数训练变慢。我一般在 64 和 96 之间二选一优先试 96因为对“横平竖直”信息更友好。背景处理不能想当然用全局二值化。手写笔迹墨迹深浅不一致全局阈值容易把浅笔迹断掉。常见做法是先用 3x3 或 5x5 的滤波做一次平滑再用 Otsu 阈值求得二值图或者直接保留灰度值做归一化到[0,1]区间后输入网络。保留灰度比二值化更稳健因为二值化会把笔锋和起笔收笔的灰度信息丢掉而这些信息在识别“力”“刀”这种笔画少、结构紧凑的字时很有用。居中问题比很多人以为的重要。原始数据里汉字不一定在图片正中心偏左偏右都很常见。如果不做处理CNN 的平移不变性又不足以完全吸收这种偏移识别率会掉 1 到 2 个百分点。常见做法是计算灰度质心然后把质心平移到图片几何中心。用scipy.ndimage.center_of_mass可以很快估算质心再通过np.roll或仿射变换把质心挪到中心。注意这里的“质心对齐”必须在缩放之后做不要先对齐再缩放否则对齐信息会被缩放破坏。数据增强上手写汉字和通用分类任务不完全相同。随机旋转角度超过 15 度就会破坏汉字可读性横竖笔画会变成斜笔画反而让模型学到“歪的字”。我一般只用±8 度随机旋转、0.9 到 1.1 倍缩放、2 像素内平移、轻微 elastic deformation。灰度扰动不要加太多手写识别的鲁棒性更多依赖字形变化而不是颜色变化。小结数据管线的顺序是“解析 → 去重 → 切分 → 缩放 → 质心对齐 → 归一化 → 增强 → 进 DataLoader”。这条链路里每一环都有参数可以调但调整优先级最高的是“质心对齐”和“旋转幅度”这两个直接决定模型能不能学到稳定的字形特征。下面是基于 PyTorch 的预处理与数据加载代码按“从图片目录读入 → 在线增强 → 输出 Tensor”的完整链路实现import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import numpy as np from scipy.ndimage import center_of_mass import os class HWDBDataset(Dataset): 读取目录结构为 label/img.png 的手写汉字数据集 def __init__(self, root_dir, resize96, trainTrue): self.root_dir root_dir self.train train self.resize resize self.samples [] # (img_path, class_id) self.classes sorted(os.listdir(root_dir)) # 每个目录名是一个汉字 self.class_to_id {c: i for i, c in enumerate(self.classes)} for cls_name in self.classes: cls_path os.path.join(root_dir, cls_name) for fname in os.listdir(cls_path): if fname.lower().endswith((.png, .jpg)): self.samples.append( (os.path.join(cls_path, fname), self.class_to_id[cls_name]) ) if train: self.transform self._train_transform() else: self.transform self._eval_transform() def _center_crop_by_mass(self, img): 将灰度质心移动到图像几何中心 arr np.array(img, dtypenp.float32) / 255.0 # 避免全黑图片导致质心计算出 nan if arr.max() 0: return img cy, cx center_of_mass(arr) h, w arr.shape dy, dx int(h / 2 - cy), int(w / 2 - cx) # shift 操作比构造仿射矩阵更快 arr_shifted np.roll(arr, dy, axis0) arr_shifted np.roll(arr_shifted, dx, axis1) if dy 0: arr_shifted[:dy, :] 0 elif dy 0: arr_shifted[dy:, :] 0 if dx 0: arr_shifted[:, :dx] 0 elif dx 0: arr_shifted[:, dx:] 0 return Image.fromarray((arr_shifted * 255).astype(np.uint8)) def _train_transform(self): return transforms.Compose([ transforms.Resize((self.resize, self.resize)), transforms.Lambda(self._center_crop_by_mass), transforms.RandomRotation(8), # 单位度 transforms.RandomAffine(0, translate(0.02, 0.02)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]), ]) def _eval_transform(self): return transforms.Compose([ transforms.Resize((self.resize, self.resize)), transforms.Lambda(self._center_crop_by_mass), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]), ]) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(L) img self.transform(img) return img, label代码逻辑说明__init__里扫描目录把目录名当作类别 ID这种做法要求目录名是汉字本身不要用数字 ID否则后面排查类别错位会非常难受。__getitem__里先转灰度、再走增强管线。_center_crop_by_mass用np.roll实现质心平移平移后空白区域补 0注意如果图片是全黑center_of_mass返回 nan所以先判断了最大值。验证集不用随机增强但质心对齐保留这能保证训练和验证的数据分布一致。关于参数说明RandomRotation(8)的 8 度是一个相对安全的经验值如果发现验证集上“横折类”字符错误率高可以进一步降到 5 度。RandomAffine里的translate(0.02, 0.02)表示水平和垂直各 2% 的随机平移配合质心对齐使用效果更好。Normalize(mean[0.5], std[0.5])把 0~255 像素映射到 -1~1这是 Tanh 类激活函数友好区间如果网络中用了 ReLU可以改成mean[0.5], std[0.25]或直接不归一化对最终结果影响不大但训练初期 loss 曲线会不同。3. CNN 模型选型与架构在预算内做一个能收敛的分类器3.1 为什么从卷积堆叠起步而不是直接上 ResNet手写汉字识别属于典型的中等复杂度图像分类任务输入是 96x96 灰度图不是 224x224 的三通道自然图。在这种情况下直接上 ResNet-50 属于杀鸡用牛刀训练速度慢、过拟合风险高而且调参难度更大。常见做法是参考 LeNet-5 和 VGG 的堆叠思路自己搭一个“Conv → BN → ReLU → Pool”的浅层 CNN。网络深度方面3 到 4 个卷积块足够。原因有两个第一手写字的判别信息集中在小尺度局部笔画组合上“横折钩”这种特征在浅层卷积的 3x3 感受野内就能捕捉到第二类别多但每个类别的样本量相对少网络太深容易在训练集上记住噪声泛化反而变差。通道数方面从 32 开始每隔一个 block 乘以 2最终到 128 或 256 就够。如果继续翻倍到 512对 96x96 的输入来说特征图已经接近全连接参数量暴涨收益却不明显。激活函数选 ReLU 就够用但配合 BatchNorm 几乎成为标配。BatchNorm 在这里的作用不仅是加速收敛更关键的是稳定训练类别多时 FC 层的输入分布一旦偏移Softmax 的梯度会变得很不稳定BN 能显著抑制这个问题。池化建议用 MaxPooling不用 AveragePooling因为手写笔画的“有无”更重要最大池化能保住最显著的笔画响应。3.2 一个可复现的 PyTorch CNN 实现Conv-BN-Act 堆叠下面这份代码按“三个卷积块 两个全连接层”的模式构建输入 1 通道灰度图输出类别数num_classes维 logits。设计目标是在单张 GTX 1060 级别显卡上能跑通,训练一轮时间控制在分钟级。import torch.nn as nn class HandwritingCNN(nn.Module): 三块卷积 两层全连接的轻量 CNN 输入: (batch, 1, 96, 96) 灰度手写汉字 输出: (batch, num_classes) def __init__(self, num_classes100, dropout0.3): super().__init__() # Block 1: 1 - 32 通道 self.conv1 nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 96 - 48 ) # Block 2: 32 - 64 通道 self.conv2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 48 - 24 ) # Block 3: 64 - 128 通道 self.conv3 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 24 - 12 ) # 经过 3 次下采样特征图尺寸 12x12展平后 128*12*12 18432 self.fc nn.Sequential( nn.Dropout(pdropout), nn.Linear(128 * 12 * 12, 512), nn.ReLU(inplaceTrue), nn.Dropout(pdropout), nn.Linear(512, num_classes) ) def forward(self, x): x self.conv1(x) # (B, 32, 48, 48) x self.conv2(x) # (B, 64, 24, 24) x self.conv3(x) # (B, 128, 12, 12) x x.view(x.size(0), -1) x self.fc(x) return x这段代码的逻辑说明每个 block 都由两个 3x3 卷积加 BN 加 ReLU 构成最后接一个 2x2 MaxPool。两个 3x3 卷积堆叠等价于一个 5x5 卷积的感受野但参数量更少、非线性更强这是 VGG 风格的标准做法。最后一层卷积输出 128 通道、12x12 空间尺寸展平成 18432 维向量再接两层全连接。全连接之前用了 Dropout 来减轻过拟合——在类别多、数据量相对有限的任务里Dropout 的作用比在自然图像分类里更明显。参数选择的理由通道数 32→64→128 是一个相对平滑的扩增曲线每层倍增不会让计算量突然暴涨也不会让特征表达能力不足。Dropout 设置在 0.3 到 0.5 之间类别数越多建议越大因为 FC 层的参数量与类别数线性相关类别多意味着更容易过拟合。kernel_size3, padding1是最常用的选择既能保证输出尺寸不萎缩又不会引入过多参数。3.3 从这个模型结构能读到的边界在哪里把num_classes从 100 改成 3755 时最后的Linear(512, num_classes)会有近 192 万参数但这在总参数量里占比不算夸张真正的瓶颈在训练时间和显存占用。手写汉字识别做 3755 类全量分类时如果输入 96x96 灰度图、batch size 64上面这个模型在 8GB 显存的 GPU 上可以正常训练但在 4GB 显存的卡上会有些紧张。解决办法有三个一是把输入缩到 64x64二是减少第三个 block 的通道数到 96三是加大nn.MaxPool2d的下采样倍数。三种方法都会带来几个百分点的精度损失一般先尝试缩输入尺寸因为对汉字来说64x64 依然保留了足够的笔画信息。另外需要注意这个模型输出的是 logits不是概率。训练时用nn.CrossEntropyLoss它内部会把 logits 送进 Softmax 再算损失推理时如果想得到概率需要手动torch.softmax但用于 Top-1 精度评估时argmax(logits)的结果和argmax(softmax(logits))完全一致没必要多算一次 Softmax。4. 训练、评估与部署三个高频坑和一个避坑策略4.1 训练超参数与损失函数怎么选手写汉字识别任务中CrossEntropyLoss是最稳妥的起点。类别多时标签平滑label smoothing能带来可见的收益把 one-hot 硬标签替换成[0.9, 0.1/(num_classes-1), ...]这种软标签可以抑制模型对训练集过拟合让各类别的 logits 不会拉得过大。一般把平滑系数设为 0.1不要超过 0.2否则模型会学不到区分度。优化器方面SGD 带动量比 Adam 在最终精度上通常好 0.5 到 1 个百分点但 Adam 前期收敛快、方便调试。我一般先用 Adam 跑几十个 epoch 验证数据和代码的正确性确认 loss 能稳定下降后再切到 SGD 重新训练做最终精度。SGD 的 momentum 设 0.9weight_decay 设 1e-4。学习率方面初始学习率 Adam 用 1e-3SGD 用 1e-2配合“Warmup Cosine Annealing”或“StepLR”调度。表手写汉字识别训练超参数推荐参数建议值说明输入尺寸96x9664 更快但精度略低Batch size64显存不足时降为 32初始学习率Adam1e-3超过 3e-3 容易发散初始学习率SGD1e-2需配合 momentumMomentum0.9SGD 必需Weight decay1e-4防过拟合标签平滑0.1类别多时推荐训练轮数30 - 50超过 50 需关注过拟合4.2 训练循环写法与监控指标训练循环本身没有特殊之处但要关注“类别多导致的验证慢”。3755 类的验证集如果每轮都全量跑会拖慢实验迭代速度。常见做法是每 5 个 epoch 才在验证集上评估一次 Top-1 和 Top-5 精度中间只监控训练集的 loss 曲线。另外由于类别不平衡有些字写得少训练时要加权采样或直接用WeightedRandomSampler。如果数据来自 HWDB类别样本量总体均衡不必须加权重如果用了自采数据就要检查每个字最少有多少张图低于 50 张的类别直接剔除或加入增强权重。下面给一个简洁的 PyTorch 训练循环骨架涵盖损失计算、反向传播、梯度裁剪和每 5 轮评估一次验证精度import torch import torch.nn as nn def train_one_epoch(model, loader, optimizer, criterion, device, clip2.0): model.train() total_loss 0.0 total_num 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() # 梯度裁剪防止类别多时 FC 层梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), clip) optimizer.step() total_loss loss.item() * images.size(0) total_num images.size(0) return total_loss / total_num torch.no_grad() def evaluate(model, loader, device, topk(1, 5)): model.eval() correct {k: 0 for k in topk} total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds outputs.topk(max(topk), dim1) for k in topk: correct_k (preds[:, :k] labels.view(-1, 1)).sum().item() correct[k] correct_k total labels.size(0) return {k: correct[k] / total for k in topk}训练循环里的几个细节clip_grad_norm_的阈值设为 2.0这在大类别数任务里是常用保守值如果训练曲线出现突然的尖刺又回落可以降低到 1.0。model.train()和model.eval()切换不能省因为 BatchNorm 在两种模式下的行为不同——训练时用当前 batch 的均值方差验证时用全局累计的均值方差。如果忘了切回eval模式就直接做推理BatchNorm 会导致结果出现微小但不可忽视的偏移。关于评估Top-5 精度在手写汉字识别里比 Top-1 更能说明问题。3755 类下 Top-1 到 98% 很难但 Top-5 到 99.5% 以上是可以实现的。如果 Top-1 掉点但 Top-5 正常说明模型在“模糊字形”上能给出合理的候选集问题可能出在易混淆字对的区分上而不是整体欠拟合。4.3 推理与模型导出从 PyTorch 到落地使用的两个注意事项训练完的模型不能直接拿去给业务用。常见路径是先做推理验证再决定是否导出为 TorchScript 或 ONNX。推理时最容易踩的坑有两个一是输入图片的预处理必须与训练时完全一致二是要记得torch.inference_mode()替代torch.no_grad()后者仍然会维护一些推断用的中间状态前者在 PyTorch 1.9 中更省显存且更快。预处理一致性这一点看似简单实际经常出错。比如训练时用了质心对齐推理时也必须用质心对齐训练时先缩放再旋转推理时就不要旋转。很多人拿一张手机拍的手写照片直接 resize 后丢进网络效果差就怀疑模型没有训练好其实问题出在“输入分布和训练分布不一致”上。导出 ONNX 的代码非常简单核心是固定输入尺寸和 batch 维度import torch model HandwritingCNN(num_classes3755) checkpoint torch.load(best_model.pth, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict]) model.eval() dummy_input torch.randn(1, 1, 96, 96) torch.onnx.export( model, dummy_input, handwriting_cnn.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}}, opset_version11, )导出时设定了dynamic_axes让 batch 维度可变这样同一个 ONNX 文件既可以单张推理也可以批量推理。opset_version11是一个兼容性很好的版本太高的 opset 在部分推理框架中可能不被支持。另外ONNX 导出后建议用onnxruntime跑一遍和二值比较 PyTorch 推理结果是否一致误差超过 1e-4 就要检查模型里是否有自定义算子。5. 用混淆矩阵、相似字错误分析和量化导出验证识别器的真实水平5.1 混淆矩阵看“已”“己”“巳”这类形近字Top-1 精度到 97% 以后常规的平均指标就失去了指导意义。这个时候最有价值的动作是画混淆矩阵找“哪些字总是互相认错”。手写汉字识别里最典型的错误模式是笔画长短差异形成的形近字对“士”和“土”、“未”和“末”、“日”和“曰”、“侯”和“候”。这些错误不是随机噪声而是模型“看到的特征不够”。画混淆矩阵用sklearn.metrics.confusion_matrix可以一步到位但类别多时全量矩阵太大看不过来。常见做法是只统计“错误对”也就是把预测结果和真实标签不一致的样本单独抽出来按字符对聚合输出出现次数最多的前 20 组这比看大矩阵直观得多。下面是一个简化的错误对统计脚本import numpy as np from collections import Counter def collect_error_pairs(model, loader, device, id_to_char): model.eval() error_pairs Counter() with torch.inference_mode(): for images, labels in loader: images, labels images.to(device), labels.to(device) preds model(images).argmax(dim1) mask preds ! labels for p, l in zip(preds[mask], labels[mask]): error_pairs[(id_to_char[l.item()], id_to_char[p.item()])] 1 return error_pairs.most_common(20)这个代码的逻辑是把预测与标签不一致的样本过滤出来用id_to_char把类别 ID 映射回汉字字符统计“真实字 → 预测字”的组合频次。如果你发现“未”和“末”的错误对排在前三名说明模型对最后一笔的长短不敏感。解决办法不是盲目加训练轮数而是针对性检查数据增强里是否有过度旋转、缩放导致的笔画长度失真或在损失函数上加大难例权重。5.2 不需要 TensorRT 也能做的 INT8 量化导出如果模型要部署到边缘设备比如嵌入式设备、树莓派或安卓手机上PyTorch 原生态的 FP32 模型可能太占内存。这时候可以用 PyTorch 自带的量化 API把模型压缩到 INT8。注意手写汉字识别模型的参数量不大量化收益主要来自更小的内存占用和更快的矩阵运算。量化最常见的问题是精度回退在手写汉字这种类别多、形近字敏感的任务上量化后 Top-1 掉 1% 到 2% 都属于正常如果掉超过 3%先检查校准集是否与验证集分布一致。一个经典的量化做法是“训练后动态量化”它对 FC 层和卷积层都适用不需要重新训练import torch model HandwritingCNN(num_classes3755) checkpoint torch.load(best_model.pth, map_locationcpu) model.load_state_dict(checkpoint[model_state_dict]) model.eval() # 动态量化适合 CPU 部署权重转 INT8激活仍为浮点 quantized_model torch.quantization.quantize_dynamic( model, {torch.nn.Linear, torch.nn.Conv2d}, dtypetorch.qint8 ) torch.save(quantized_model.state_dict(), model_int8.pth)代码里第二参数指定要量化的层类型。量化后模型在 CPU 上的推理速度一般能提升 1.5 到 3 倍内存占用降到原来的四分之一左右。需要强调的是量化模型的精度验证不能只看整体 Top-1要专门把易混淆字对拉出来看如果“土”和“士”在量化后错误率暴增说明量化把最后一笔的微小差异抹掉了这时应该放弃卷积层量化只量化全连接层或者改用校准集做量化感知训练。5.3 最后一个实用技巧用“留出你自己的手写样本”做最终验收无论是训练精度、验证精度还是混淆矩阵都只能说明模型在数据集上的表现。做这类项目最终提交或上线前一个非常实用但容易被忽略的做法是自己拿白纸写 30 到 50 个汉字拍照、切图、走一遍推理看识别成什么。这个步骤能暴露数据管线里所有“看起来没问题但实际不对”的问题比如拍照时的透视畸变、笔画颜色与纸张对比度不同、手写字体自带连笔等。如果自己的样本效果比验证集差很多先检查预处理而非网络先确认图片的灰度范围是否归一化到[0,1]再确认质心对齐是否工作最后看是否是因为拍照背景不是纯白。手写识别项目的复杂度往往不在网络结构而在这些“从实验室到真实环境”的细节差异。能把自己手写的字稳定识别对这个项目才算真正闭环了。本文还有配套的精品资源点击获取
返回列表