资讯详情

资讯详情

TensorFlow手写文字识别案例:从源码拆解到模型实战

简介这是一份基于TensorFlow框架实现手写文字识别的Python源码案例适合刚接触深度学习、对图像识别感兴趣的初学者。案例以手写数字数据集为输入完整演示了从数据预处理、构建卷积神经网络模型、训练调优到保存与测试识别的全流程并对比了Softmax回归与卷积神经网络的实现差异。资源共6个文件包含Softmax回归模型、卷积神经网络模型、数据准备脚本及识别脚本等4个Python源文件另有1份PDF说明文档和1个数据压缩包整体仅2.32MB结构紧凑清晰。PDF说明文档对实现原理与代码结构进行了讲解有助于读者将理论知识与具体实现一一对应。目前已有1580人学习下载是入门图像识别与深度学习流程的实用素材通过学习该案例读者能直观理解TensorFlow的核心工作机制掌握图像分类任务的常规实现方法并积累模型训练、验证与调参经验为后续更复杂的深度学习项目打下扎实基础。1. TensorFlow手写文字识别案例破解之前先确定任务边界拿到这份 TensorFlow 框架手写文字识别 Python 源码案例最忌直接双击就开跑。手写识别跟普通图像分类不完全是一回事同样叫“手写文字识别”源码可能是单字分类也可能是整行文字识别两种任务的模型结构、损失函数和评估方式完全不一样。你在压缩包里看到的标签文件结构往往比任何一行模型代码都更能说明问题。这篇文章会先把这类源码包的常见目录结构讲清楚再给出能照着改的训练骨架最后把几个低频但致命的坑一并处理掉。适合刚接手 TensorFlow OCR 方向、手上有图片和标签数据、准备把手写识别案例改造成自己任务的开发者。2. 拆包后先定位三件事数据格式、字符集边界和TensorFlow版本解压之后不要急着建环境先用文件树和标签文件回答三个问题数据怎么读、标签是单字还是整行、代码是 TensorFlow 1.x 还是 2.x。这三个答案决定了后面所有改法也决定你最终能拿到一个分类模型还是一个 CRNN 模型。2.1 解压后先列目录手写识别案例的四个模块怎么认常见的手写识别源码包无论简单还是复杂都会在某个目录里同时出现四类模块。先看文件树再决定从哪个文件切入unzip tensorflow框架手写文字识别python源码案例.zip -d htr_case cd htr_case find . -maxdepth 2 -type f | sort-d htr_case指定解压目录避免文件散落一地find只列两层目录先看清大结构不用一头扎进子目录。如果文件名带中文且解压后乱码不要继续操作先跳到第 4.2 节把编码问题解决否则后面的路径全部对不上。看到文件列表后按下面这张表归类模块常见文件线索你要确认的事数据入口data/、loader.py、dataset.py图片路径和标签字段怎么对齐模型定义models/、network.py、net.pyCNN 还是 CRNN输入尺寸是多少训练入口train.py、train.sh是 Keras 的 model.fit 还是老式 Session 循环推理导出infer.py、export.py、app.py推理时的预处理和训练是否完全一致这套归类方式不是针对某个特定项目而是这类源码包普遍长这样。你不需要读完所有代码先定位训练入口和模型文件把数据格式确认清楚就能跑通一个最小训练循环。2.2 数据集的三种选择MNIST、EMNIST 和中文手写库的取舍手写文字识别源码里出现频率最高的是三类数据MNIST 风格的数字、EMNIST 风格的字母数字混合、以及中文手写字符库。它们的字符集和任务难度完全不同直接影响最后一个全连接层的类别数。数据集类型字符集典型类别规模适用场景MNIST 风格0-9 手写数字10验证模型和训练流程入门最快EMNIST 风格大小写字母 数字47 / 62 / 145 不等英文手写字识别、表单字段中文手写库GB2312 一级汉字等3755 类起步中文单据、笔记、考卷识别判断源码是哪种任务不用猜统计标签就能确定。下面这段脚本读一个常见的标签文件假设每行是“图片路径,标签文本”from collections import Counter with open(labels.txt, encodingutf-8) as f: lines [line.strip() for line in f if line.strip()] paths [] labels [] for line in lines: parts line.split(,) paths.append(parts[0]) labels.append(parts[-1]) counter Counter(labels) print(类别数:, len(counter)) print(最常见标签:, counter.most_common(10))encodingutf-8在 Windows 下不一定安全如果这里报错或者读出来全是问号去改第 4.2 节的编码方案。统计结果里如果标签长度大部分是 1说明是单字分类后面模型按 CNN 走如果标签长度不一致且大部分大于 1说明是整行识别要用 CRNN 加 CTC。这一步判断错了后面所有参数都白调。2.3 先确认TensorFlow版本从placeholder到Keras的快速判断与迁移老一批手写识别源码是 TensorFlow 1.x 写的代码里大量出现tf.placeholder、tf.Session、tf.global_variables_initializer。这些在 TensorFlow 2.x 里默认不可用。先 grep 一遍grep -n tf\.Session\|tf\.placeholder\|tf\.global_variables *.py如果没有任何输出说明代码大概率基于 TensorFlow 2.x 的 Keras API可以继续往下看如果输出一片那源码是典型的 1.x 写法。临时复现可以用兼容层import tensorflow.compat.v1 as tf tf.disable_v2_behavior()这段代码放在训练脚本最前面可以让 1.x 代码在 TensorFlow 2.x 环境里跑起来但代价是失去 Keras 回调、动态图和 SavedModel 导出等便利。我一般不建议在这种源码上长期打补丁。1.x 的 Session 循环本身调试成本高数据管道也多半是手工喂 batch不如把训练入口改成model.fit模型结构部分抽出来保留损失函数和数据管道按新写法重写。后面第 3 章给的骨架就是按 TensorFlow 2.x 来的。3. 把训练骨架跑通模型选型、数据管道和关键参数任务边界确定后核心就剩三件事选什么模型、用什么数据管道、训练时看什么指标。这一章给的代码都是可运行的骨架你拿到源码后可以逐行对照也能直接抽到自己的任务里改。3.1 模型选型单字分类用CNN整行识别用CRNN加CTC单字识别是典型的图像分类问题输入是一张包含单个字符的灰度图输出是字符类别概率。汉字类别多模型在宽度上要多给通道深度反而不宜过深否则小数据集上极易过拟合。下面这个骨架是我常用的单字分类模型def build_cls_model(num_classes, input_shape(32, 32, 1)): model tf.keras.Sequential(namehtr_cls) model.add(tf.keras.layers.Conv2D( 32, (3, 3), activationrelu, paddingsame, input_shapeinput_shape)) model.add(tf.keras.layers.MaxPooling2D((2, 2))) model.add(tf.keras.layers.Conv2D( 64, (3, 3), activationrelu, paddingsame)) model.add(tf.keras.layers.MaxPooling2D((2, 2))) model.add(tf.keras.layers.Conv2D( 128, (3, 3), activationrelu, paddingsame)) model.add(tf.keras.layers.Flatten()) model.add(tf.keras.layers.Dropout(0.5)) model.add(tf.keras.layers.Dense(256, activationrelu)) model.add(tf.keras.layers.Dense(num_classes, activationsoftmax)) return modelpaddingsame是为了让卷积不改变特征图尺寸MaxPooling2D把空间尺寸逐步缩小Dropout(0.5)放在全连接之前对抗手写样本的过拟合。num_classes对应字符集大小中文单字任务至少 3755英文数字混合任务按实际类别数填。注意input_shape固定为 32x32 单通道如果你的源码里训练分辨率是 64 或 96这里要对应调整。整行手写文字识别是序列问题输入一行文字标签是一串不等长字符。常见做法是 CRNN用 CNN 提取特征再用双向 LSTM 建模字符顺序最后接 CTC 损失。核心片段长这样# 假设 features 已经由 CNN 特征图压缩成 (batch, time, channels) 序列 lstm_out tf.keras.layers.Bidirectional( tf.keras.layers.LSTM(128, return_sequencesTrue) )(features) logits tf.keras.layers.Dense(num_classes 1, activationsoftmax)(lstm_out)return_sequencesTrue必须保留每个时间步的输出否则 CTC 没有序列可以解码Bidirectional让模型同时看到当前字符的左右两侧。num_classes 1是给 CTC 的 blank 占位这是整行识别最容易漏的参数。CTC 训练时还要把变长标签对齐到 logits 的时间步源码里一般用tf.nn.ctc_loss配合label_length和logit_length这两个长度都不能传错。3.2 找到训练入口后先跑通一个最小命令不管源码结构多复杂训练入口脚本通常都会接收几个路径和超参。如果源码里已经写好命令行入口先跑一个最小命令把数据流程验证通再谈效果python train.py \ --data_dir ./data \ --train_list ./data/train.txt \ --val_list ./data/val.txt \ --image_size 32 32 \ --batch_size 64 \ --epochs 50 \ --lr 0.001参数名不一定和源码里完全一致常见的有--data-root、--train-file、--img-size。第一次跑不要直接上 50 轮也不要全量数据。先看数据能不能被正确读进来、preprocessing 有没有报错再把--epochs临时调成 1。源码里如果没有命令行参数就打开训练脚本找文件路径变量改成自己的绝对路径。--batch_size 64对单字分类比较稳妥整行识别为了塞进显存通常要降到 16 或 32。训练脚本内部的数据管道在 TensorFlow 2.x 里基本都是tf.data写的一个最小实现长这样def preprocess(path, label): img tf.io.read_file(path) img tf.image.decode_image(img, channels1) img tf.image.resize(img, (32, 32)) img tf.cast(img, tf.float32) / 255.0 return img, label train_ds tf.data.Dataset.from_tensor_slices((paths, labels)) train_ds train_ds.map( lambda p, l: preprocess(p, l), num_parallel_callstf.data.AUTOTUNE) train_ds train_ds.shuffle(4096).batch(64).prefetch(1)decode_image同时支持 JPG 和 PNG比decode_jpeg省心num_parallel_callstf.data.AUTOTUNE让 TensorFlow 自己决定并行度避免把 CPU 打满反而拖慢。prefetch(1)让数据加载和模型计算重叠这一步能显著缩短每个 epoch 的时间。这里preprocess在后面推理时还要用到最好单独放一个模块不要只在训练脚本里定义。3.3 用TensorBoard盯训练Loss不降和准确率虚高分别看什么训练不是跑完就结束中间要能看到变化。TensorBoard 是最直接的观察工具配置也不复杂callbacks [ tf.keras.callbacks.TensorBoard( log_dir./logs/run1, histogram_freq1, update_freqepoch, profile_batch0, ), tf.keras.callbacks.ModelCheckpoint( filepath./checkpoints/best.keras, monitorval_loss, save_best_onlyTrue, ), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience5 ), ] model.fit( train_ds, validation_dataval_ds, epochs50, callbackscallbacks, )histogram_freq1会在每轮结束后记录权重分布对判断梯度是否异常有帮助但会拖慢训练profile_batch0关掉性能分析省下不必要的开销。ModelCheckpoint保存的是val_loss最低的那轮权重不要用val_accuracy做唯一标准这在类别不平衡时会骗你。训练时启动tensorboard --logdir ./logs重点看两条曲线。第一训练 loss 完全不降先怀疑学习率和预处理。学习率 0.001 是常见起点如果 loss 一直横在初始值附近把 lr 降到 0.0001 再试如果同时报 NaN多半是输入里有空白图片或者归一化除以了 0。第二训练 loss 持续下降、验证 loss 先降后升这是过拟合信号优先加数据增强和 Dropout而不是堆更多卷积层。4. 手写识别案例实战避坑五个踩出来的经验手写识别项目的坑不在模型而在数据流和前后处理一致性。下面五条都是我在实际跑这类源码时踩过的每条按现象、原因、解决写清楚你可以直接对照排查。4.1 训练和推理预处理不一致训练精度高推理全部翻车现象训练集准确率能到 96%验证集也正常但拿自己随手写的图去预测结果一片混乱甚至输出一堆同样的类别。原因训练时数据管道里做了灰度化、resize 和除以 255推理脚本里没做或者用了不同的插值方式。比较隐蔽的是训练用tf.image.resize推理用 OpenCV 的cv2.resize两者边界像素有差异单字图本来就只有 32x32这点差异足以改变分类结果。解决把预处理抽成同一个函数训练和推理都调它不要各写一份。逐行检查推理脚本里是否有/ 255.0这行一旦漏掉模型输入的分布就不对。最好在模型入口之前强制接入同一套预处理。def preprocess(image_path, size(32, 32)): img tf.io.read_file(image_path) img tf.image.decode_image(img, channels1) img tf.image.resize(img, size) img tf.cast(img, tf.float32) / 255.0 return imgdecode_image会自动判断图片格式但如果你把训练数据全转成了 JPG推理时也最好统一转一遍。不要在训练时额外做减均值、除标准差推理时却只做除以 255这类不一致是手写识别推理翻车最普遍的原因。4.2 中文标签在Windows下乱码成问号现象标签文件用记事本打开很正常Python 读进来却全是???统计类别数直接变成一个训练 loss 根本不降。原因Windows 下 Python 默认用 GBK 去读 UTF-8 的标签文件中文字符解不出来。另外压缩包里如果文件名是中文解压工具和解压参数不一致也会出现路径乱码。解决读文件时显式指定编码不要依赖系统默认。import codecs with codecs.open(labels.txt, r, utf-8) as f: lines [line.strip() for line in f if line.strip()]如果读进来还乱说明标签文件本身不是 UTF-8用编辑器另存为 UTF-8 再试。如果是压缩包内的文件名乱码用 Python 的zipfile先检查原始文件名编码再按实际编码修正。大部分中文 zip 在 Python 里乱码是 cp437 解码导致的处理方式如下import zipfile with zipfile.ZipFile(tensorflow框架手写文字识别python源码案例.zip) as zf: for info in zf.infolist(): raw info.filename.encode(cp437) try: print(raw.decode(gbk)) except UnicodeDecodeError: print(info.filename)encode(cp437)是把 Python 解析出的乱码字符串恢复成原始字节再用gbk解出正确中文。Mac 和 Linux 下不一定是 gbk可能是 utf-8 或 big5实测失败就换编码试。先把文件名修好再解压后面所有路径才靠谱。4.3 类别不平衡让准确率虚高现象训练和验证准确率都超过 90%但看详细结果大量低频字符完全不准。比如高频的“的”字样本几千张生僻字只有几十张模型只要把所有样本都预测为“的”准确率照样难看不了。原因Softmax 分类器的损失被高频类别主导低频类别对梯度的贡献几乎被淹没。整体准确率在这种情况下是一个严重失真的指标。解决先统计类别分布如果最低类样本数和最高类差一个数量级以上给模型加类别权重或者对低频类做过采样。from collections import Counter counts Counter(labels) total sum(counts.values()) num_classes len(counts) class_weight { cls: total / (num_classes * cnt) for cls, cnt in counts.items() }这个权重公式是逆向频率加权的标准写法某个类样本越少权重越大。在model.fit里直接传class_weightclass_weight就能生效。不过类别数到 3755 时每轮都要查几千个键值训练速度会受影响。如果发现明显变慢改成采样策略在每个 epoch 里按等概率从每个类别抽样保证稀有类不会缺席。优先看稀有类的召回率而不是整体准确率。4.4 变长序列用固定输入尺寸SavedModel导出失败现象整行识别模型训练正常loss 降得也不错但用model.save或导出 SavedModel 时报错说输入形状不匹配或者导出的模型只能处理训练时固定的宽度。原因训练时为了对齐 batch把整行图片 padding 到固定宽度模型输入层也写成了类似(64, 32, 1)的固定形状。CRNN 本质要处理变长序列固定宽度一到实际推理就露馅。解决模型输入层把宽度设为动态维度。inputs tf.keras.Input(shape(None, 32, 1))高度保持 32宽度设为None。导出后网络就能接受任意宽度的图片。要注意 CTC 解码时仍需要知道真实的时间步长度因为 padding 的部分不应参与解码。推理时拿着原始图片宽度除以 CNN 的降采样倍数算出有效序列长度再传给解码函数。这个长度算错识别结果会多出一串空字符或乱码。4.5 数据增强过强小样本上把字形练坏现象加完随机旋转 30 度、随机缩放之后训练 loss 不但没降反而比不增强还高验证集也跟着变差。原因手写文字对方向极其敏感。数字 6 转 90 度变成 9汉字“人”和“入”在小角度下本来就难分旋转 30 度相当于把训练标签改成了一张错字图。增强强度一旦超过字形容差模型学到的不是泛化而是噪声。解决把增强强度压到很低尤其是汉字任务。data_augmentation tf.keras.Sequential([ tf.keras.layers.RandomRotation(factor0.02), tf.keras.layers.RandomTranslation( height_factor0.05, width_factor0.05), ])RandomRotation的factor0.02对应大约正负 3.6 度手写中文一般不要超过 0.05。数字和英文字母可以稍微放宽汉字这种近形字多的字符集必须保守。另外增强只应该在训练数据管道里做验证集和测试集保持原始图否则验证指标会被增强干扰你根本不知道模型真实水平。5. 交付前最后一步用混淆矩阵定位易错字再导出SavedModel整体准确率过了 95% 不代表能交付。手写识别的风险集中在少数长相相近的字上准确率曲线把这些问题全掩盖了。我在模拟项目X上吃过一次亏测试集准确率 96%上线后手写“未”字大量被识别成“末”查了模型输出才发现这两个字几乎 100% 混淆。从那以后我拿到训练好的模型第一件事永远是看混淆矩阵。用 TensorFlow 的计算图算混淆矩阵也算方便测试集遍历一轮就能得到import numpy as np import tensorflow as tf model tf.keras.models.load_model(./checkpoints/best.keras) y_true [] y_pred [] for imgs, labels in test_ds: y_true.extend(labels.numpy().tolist()) logits model(imgs) y_pred.extend(tf.argmax(logits, axis-1).numpy().tolist()) conf tf.math.confusion_matrix(y_true, y_pred, num_classeslen(class_weight)) conf_np np.array(conf) np.fill_diagonal(conf_np, 0) error_pairs [] for i in range(conf_np.shape[0]): for j in range(conf_np.shape[1]): if conf_np[i, j] 0: error_pairs.append((conf_np[i, j], i, j)) for cnt, i, j in sorted(error_pairs, reverseTrue)[:10]: print(classes[i], -, classes[j], 样本数:, cnt)num_classes要和模型输出维度一致遍历测试集时不要开 shuffle否则y_true和y_pred对不上。打印出来的前几名基本就是你交付前要重点补数据的字对。补数据不是盲目加量而是针对这些易混淆字对收集更多相近写法的样本重新训练。验证通过后导出 SavedModel 用于部署。导出前再把预处理也包进去最省事的方式是让导出的模型接受原始图片字节内部完成解码、缩放、归一化。这样部署侧不需要依赖训练脚本里的 Python 预处理代码也不会再犯第 4.1 节那种前后处理不一致的错。tf.saved_model.save(model, ./saved_model)SavedModel 和.keras文件用途不同.keras用于继续训练SavedModel 用于推理部署。导出后要写一个小脚本试用签名输入一张真实图片确认输出和训练时的预测一致。这一步走完整个源码案例才算真正从“能跑”变成“能用”。无论是单字分类还是整行识别我都会在交付前至少做一次这样的验证养成习惯后能少翻很多次车。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →