尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于TensorFlow和ResNet的字母数字识别完整实践

基于TensorFlow和ResNet的字母数字识别完整实践 简介基于TensorFlow 2与EMNIST数据集的字母数字识别项目面向课程设计、深度学习初学者和需要图像分类入门代码的开发者解决手写英文字母与数字的模型训练和识别问题。该项目是ResNet在TF2上的一份简洁实现运行环境为Windows 10、TensorFlow 2.1、Python 3.7代码结构清楚适合本地复现与二次改造。压缩包共50个文件大小约104.79MB核心包括4个Python源码脚本模型定义、训练、测试与演示、h5权重文件、checkpoint训练断点及index/data分片、12张PNG训练曲线与测试样例图、README说明和LICENSE许可目录划分清楚便于按需查看。已有280人学习/下载。资料中提供20轮与80轮训练效果对比图可直接观察不同迭代步数下的识别表现训练断点与权重文件支持直接加载模型推理省去重新训练的时间。训练与测试脚本可配合自定义图片完成字母数字识别演示适合作为课程设计成果展示。对正在完成字母数字识别课程设计、或希望对照TensorFlow 2实现ResNet的读者来说这是一份可运行、可扩展的完整参考。1. 字母数字识别不只是“Hello World”做课程设计或者准备CV面试时字母数字识别是绕不开的题。网上的教程大多止步于MNIST数字识别可真实场景里字母和数字混在一起比如邮编识别、车牌识别、答题卡批改你都得区分“0”和“O”、“1”和“l”。这份基于Python实现的字母数字识别项目用TensorFlow 2.1在EMNIST数据集上把英文字母和数字一起训练模型部分是一个ResNet在tf2上的简洁实现。它能识别26个字母和10个数字适合做课设、毕设的基座也适合想看看ResNet在小数据集上怎么调参的人。你拿到的不是demo玩具而是一套带训练、测试、推理和checkpoints的完整工程。2. 认识资源与模型结构从EMNIST到ResNet2.1 压缩包里有什么文件清单与职责先直接给一张文件清单避免你下载完一脸懵。项目根目录下的文件各司其职最重要的几个如下文件/目录作用model.pyResNet模型定义包含残差块、网络堆叠和输出层mytrain.py训练入口负责数据加载、训练循环、checkpoint保存test.py在测试集上跑评估输出准确率等信息demo.py加载训练好的checkpoint对单张图片做预测和可视化checkpoints/训练过程中保存的权重比如pro1-10.ckpt系列assets/图片素材里面的0.png、3.png、5.png等就是现成的测试图characters.txt类别映射表把模型输出的索引对应到字符LICENSE/README看依赖说明和运行步骤注意checkpoints目录里pro1-10.ckpt.index和pro1-10.ckpt.data-00001-of-00002这类文件是TensorFlow 2.1保存checkpoint的标准格式。index文件是索引data文件是真正的权重两部分缺一不可。如果你迁移时丢了其中一个加载模型就会报“找不到匹配的checkpoint”错误。assets里的png可以直接拿来试demo不用自己再找图。先跑demo是最快的确认方式只要环境装好执行python demo.py --image assets/5.png如果终端打印出“预测结果5”以及对应的置信度说明checkpoint没坏、前向推理没问题。常见做法是先用自带图片验证环境再换自己的图。很多版本的demo.py在没加--image参数时会默认跑assets里第一张图具体看README说明不一定报错。2.2 为什么选ResNet而不是简单CNN模型设计逻辑字母数字识别看似简单但实际比MNIST难得多。MNIST只有10类且数字写法和背景相对规范EMNIST里同一个字母有不同写法而且“0”和“O”这种形状几乎一致。如果你用一层卷积加两层全连接很快会过拟合验证集准确率卡在85%左右就上不去了。ResNet的残差学习让梯度更容易穿过深层网络在只有28x28这样的小输入上也能把特征抽象到足够深。model.py里核心是残差块典型实现如下import tensorflow as tf def conv3x3(x, out_planes, stride1, name): return tf.keras.layers.Conv2D( out_planes, (3, 3), strides(stride, stride), paddingsame, use_biasFalse, namename)(x) def residual_block(x, planes, stride1, downsampleNone, name): identity x out conv3x3(x, planes, stride, namename _conv1) out tf.keras.layers.BatchNormalization(namename _bn1)(out) out tf.keras.layers.ReLU()(out) out conv3x3(out, planes, 1, namename _conv2) out tf.keras.layers.BatchNormalization(namename _bn2)(out) if downsample is not None: identity downsample(x) out tf.keras.layers.add([identity, out]) out tf.keras.layers.ReLU()(out) return out这里的downsample通常是1x1卷积作用是当步长不为1或通道数变化时把残差输入的维度对齐到输出维度。BatchNormalization在tf2.1里默认训练时更新均值方差但推理时用的是滑动均值这两个阶段的行为不能混否则会出现训练好但预测不准的灵异现象。网络上很多ResNet实现用预训练权重但EMNIST这种灰度小图不适合直接拿ImageNet权重。ImageNet预训练模型输入是224x224三通道而EMNIST是28x28单通道硬改输入和第一层卷积通道数不仅参数浪费还容易把预训练学到的彩色纹理迁移到手写字符上反而有害。我一般会把第一个卷积层的输入通道改成1然后从头训练。所以你直接看model.py会发现第一层卷积是单通道输入后面再接BN和ReLU。接着看整体结构def resnet_model(num_classes36): inputs tf.keras.Input(shape(28, 28, 1), nameinput) x conv3x3(inputs, 32, 1, namestem_conv) x tf.keras.layers.BatchNormalization(namestem_bn)(x) x tf.keras.layers.ReLU()(x) x residual_block(x, 32, nameblock1) x residual_block(x, 64, stride2, nameblock2) x tf.keras.layers.GlobalAveragePooling2D()(x) x tf.keras.layers.Dense(num_classes, activationsoftmax, namefc)(x) return tf.keras.Model(inputs, x)这里输入固定为28x28x1类别数num_classes默认36也就是10个数字加26个大写字母。你如果只想识别数字可以把num_classes改成10但要注意训练数据不是MNIST而是EMNIST里抽出数字子集。用GlobalAveragePooling2D替代Flatten再接Dense能大幅降低参数量也更符合ResNet设计习惯。损失函数这边代码里常用sparse_categorical_crossentropy因为它要求标签是整数而不是one-hot。如果你自己准备了标签且做了one-hot编码就改成categorical_crossentropy并检查最后一层是否用了softmax。这两个混用是新手最容易踩的报错来源。要确认模型能不能跑通在项目根目录执行python -c from model import resnet_model; model resnet_model(36); model.summary()如果正常打印网络结构和参数量说明model.py没有语法错误、依赖也齐了。看到Total params在几十万级别是正常的比VGG16的上亿参数小太多CPU也能训只是慢一点。但还有一处容易被忽略类别顺序。模型输出是36维向量第几个分量对应哪个字符需要跟训练时一致。项目里characters.txt就是这份映射表常见内容如下0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ训练脚本里构造数据时如果用了这个顺序那么demo.py预测时就用characters[index]来映射。不要改这个文件里字符的顺序否则加载checkpoint后预测结果全错看起来就像模型训练了一个寂寞。3. 把训练跑起来环境配置与mytrain.py实战3.1 环境要求与依赖安装项目README里写的运行环境是Windows10 tf2.1 python3.7这个组合很经典。tf2.1支持CPU和GPU但GPU版本需要cuda10.1和cudnn7.6。如果你想省事先在CPU上把流程跑通再换GPU训练。依赖清单大概包括tensorflow2.1.0 numpy1.16 matplotlib3.0 opencv-python4.0如果你用的是pycharm或vscode配置python环境推荐先给这个项目单独建一个虚拟环境别和系统Python混在一起。我的做法是python -m venv venv venv\Scripts\activate pip install tensorflow2.1.0 numpy matplotlib opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple注意Windows下激活虚拟环境是venv\Scripts\activateLinux/macOS是source venv/bin/activate。这里用清华镜像快很多。如果你之前装过tf2.3甚至更高版本建议卸载干净再装2.1版本混着装会让protobuf报一些奇怪的符号错误。装完可以验证一下python -c import tensorflow as tf; print(tf.__version__)能打印2.1.0就通过。之后在pycharm里做Python环境配置时解释器选venv下的python.exevscode则按CtrlShiftP选择Python: Select Interpreter指向venv路径。这一步卡住的人大部分是忘了切换解释器导致跑的时候用的还是全局Python。3.2 数据加载与预处理EMNIST和MNIST不一样mytrain.py里数据部分通常是先用tensorflow_datasets加载或者从网上下载emnist的mat格式再转numpy。建议你直接看脚本里有没有“emnist”关键字。如果用的tensorflow_datasets加载代码类似import tensorflow_datasets as tfds (ds_train, ds_test), ds_info tfds.load( emnist, split[train, test], data_dir./data, as_supervisedTrue, with_infoTrue )这里有个坑EMNIST默认图像是反的需要先转置否则训练出的模型识别手写字会上下颠倒或左右颠倒。因为EMNIST的存储方向是从左下角开始的标准显示是转置加翻转。常见做法是先拿一张图可视化确认方向再进行归一化。预处理函数如下def preprocess(image, label): image tf.image.convert_image_dtype(image, tf.float32) image tf.transpose(image, perm[1, 0, 2]) image tf.image.flip_left_right(image) image tf.reshape(image, (28, 28, 1)) image (image - 0.1307) / 0.3081 return image, label这里用transpose和flip把EMNIST的方向纠正到跟MNIST一致然后用0.1307和0.3081做标准化。这两个数值是MNIST数据集的全局均值和方差用在EMNIST上也可以但不是最优。注意convert_image_dtype会把0-255的图片转成0-1浮点所以后面减均值除方差才有效果。然后设置batch和打乱顺序BATCH_SIZE 128 AUTOTUNE tf.data.experimental.AUTOTUNE ds_train ds_train.map(preprocess, num_parallel_callsAUTOTUNE) ds_train ds_train.shuffle(10000).batch(BATCH_SIZE).prefetch(AUTOTUNE) ds_test ds_test.map(preprocess, num_parallel_callsAUTOTUNE) ds_test ds_test.batch(BATCH_SIZE).prefetch(AUTOTUNE)shuffle缓冲区大小会影响随机性10000在EMNIST的12万训练集里够用。如果你显存小可以先把BATCH_SIZE调成64后面再调回128。为了确认预处理没做错可以把增强后的图片用matplotlib画出来import matplotlib.pyplot as plt for img, label in ds_train.take(1): plt.imshow(img[0].numpy().squeeze(), cmapgray) plt.title(str(label.numpy())) plt.show()看看显示的字符是不是正的有没有左右翻转。这一步能提前避开很多“训练玄学”。3.3 训练参数说明与启动训练看训练脚本之前先理解几个关键超参。在mytrain.py里通常有类似下面的参数区参数推荐值说明epochs80训练轮数20轮能看到趋势80轮能收敛batch_size128每批样本数OOM就减半learning_rate0.001初始学习率配合指数衰减或ReduceLROnPlateaunum_classes3610个数字26个大写字母checkpoint_dircheckpoints/权重保存目录训练命令就是python mytrain.py如果你的设备是N卡想用GPU训练一般不需要额外设置tf2.1会自动识别。想强制用某张卡可加环境变量set CUDA_VISIBLE_DEVICES0 python mytrain.py训练过程中会在命令行看到loss和acc变化同时每N个epoch保存一次模型到checkpoints目录。资源里看到pro1-10.ckpt说明训练到第10轮时保存了一份。如果想改保存频率看mytrain.py里的ModelCheckpoint回调的period参数callbacks [ tf.keras.callbacks.ModelCheckpoint( filepathcheckpoints/pro{epoch:02d}-{val_accuracy:.2f}.ckpt, save_best_onlyFalse, period10 ), tf.keras.callbacks.TensorBoard(log_dirlogs) ]filepath里的{epoch:02d}会自动替换成当前epoch数字我习惯把val_accuracy也写进去这样光看文件名就知道哪份权重准。注意tf2.1里ModelCheckpoint的参数是periodtf2.3以后改成了save_freq如果你换了版本这行会报TypeError。训练时还可以配合ReduceLROnPlateau当验证准确率连续几个epoch不涨就降学习率callbacks.append( tf.keras.callbacks.ReduceLROnPlateau( monitorval_accuracy, factor0.5, patience5, verbose1 ) )factor是衰减因子patience是等待回合数。这样不用手动调学习率训练后期不会在最优位置附近震荡太久。很多新手直接在训练里把学习率设为固定值最后发现损失曲线一直波动就是等着用这个回调。TensorBoard是排查训练曲线的利器。训练结束后运行tensorboard --logdir logs然后浏览器打开localhost:6006看loss和acc曲线。如果train loss和val loss相差太大就是过拟合两者都在高位就是学习率问题。这比看终端输出直观得多。4. 测试和演示test.py与demo.py怎么用4.1 用test.py评估模型看准确率和混淆矩阵训练完成后第一步不是急着识别自己写的字而是先在测试集上看模型真实水平。test.py会加载checkpoints里最新权重然后对测试集做预测。运行方式python test.py如果test.py没有写死模型路径通常会用tf.train.latest_checkpoint自动找checkpoint_dir ./checkpoints model_path tf.train.latest_checkpoint(checkpoint_dir) model.load_weights(model_path) print(restored from, model_path)load_weights只恢复权重不恢复优化器状态所以继续训练时学习率、动量等参数会丢失。如果你只是做推理或评估这没影响想接着上次的训练跑需要加载整个checkpoint而不是weights。test.py里评估部分典型代码loss, acc model.evaluate(ds_test, verbose1) print(Test accuracy: {:.4f}.format(acc))tf2.1的evaluate会返回损失和指标指标是在编译模型时指定的。如果mytrain.py里用model.compile(optimizer..., losssparse_categorical_crossentropy, metrics[accuracy])这里就能拿到准确率。很多情况下你会看到测试准确率在95%左右但如果只是93%也别慌EMNIST里字母数字混合本来就有一些难以区分的类别比如数字0和大写字母O。除了准确率建议在test.py里顺便算出混淆矩阵看看哪些类别最容易被认错。常见做法import numpy as np from sklearn.metrics import confusion_matrix y_true, y_pred [], [] for images, labels in ds_test: pred np.argmax(model.predict(images), axis1) y_true.extend(labels.numpy()) y_pred.extend(pred) cm confusion_matrix(y_true, y_pred)打印混淆矩阵后你会发现“0”和“O”的对应位置数值异常后面可以针对这类错误增加训练样本或合并类别。这一步能帮你理解模型边界而不是只盯着一个准确率数字。4.2 用demo.py识别单张图片预处理与预测逻辑demo.py是给教学演示用的核心逻辑是读入图片、转灰度、resize到28x28、归一化然后model.predict。我整理了一个可复用的清洗函数import cv2 import numpy as np def load_and_preprocess(image_path): img cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) if img is None: raise ValueError(cannot read image: image_path) img cv2.resize(img, (28, 28), interpolationcv2.INTER_AREA) img img.astype(np.float32) / 255.0 img (img - 0.1307) / 0.3081 img np.expand_dims(img, axis-1) # (28,28,1) img np.expand_dims(img, axis0) # (1,28,28,1) return img注意cv2.imread读回来是0-255的uint8除以255变成0-1浮点再做标准化。很多新手漏了除以255直接把0-255的图喂给模型预测结果完全不对。resize用INTER_AREA是因为原图大概率是大图缩小到28x28这个插值方式能保留边缘轮廓。如果原图是黑白反转的比如白底黑字变黑底白字也需要先反转成黑底白字因为训练集是黑底白字。预测代码predictions model.predict(img) class_id np.argmax(predictions[0]) confidence np.max(predictions[0]) print(class_id:, class_id, confidence: {:.4f}.format(confidence))class_id对应的是训练时类别顺序。在字母数字识别里通常0-9是数字10-35是字母。如果你的characters.txt是“0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ”那么class_id11就对应大写字母B。demo.py应该会帮你完成这个映射但如果自己写脚本千万别忘了查表。4.3 恢复checkpoint训练中断与继续训练训练到一半断电或者因为其他原因中断这是家常便饭。如果你用的是ModelCheckpoint保存的权重恢复逻辑是model resnet_model(num_classes36) checkpoint_dir ./checkpoints latest tf.train.latest_checkpoint(checkpoint_dir) if latest: model.load_weights(latest) print(resumed from, latest)但load_weights只加载模型参数不恢复优化器的动量、学习率衰减步数。如果你想做到无缝续训得用完整的tf.train.Checkpoint保存模型和优化器ckpt tf.train.Checkpoint(modelmodel, optimizeroptimizer) manager tf.train.CheckpointManager(ckpt, ./checkpoints/train_ckpt, max_to_keep3) # 训练中 manager.save() # 恢复 ckpt.restore(manager.latest_checkpoint)这样优化器里的动量也回来了学习率衰减位置也对。项目里mytrain.py默认用哪个方式你看checkpoints目录下有没有train_ckpt就能判断。如果你发现续训后loss突然变大多半就是因为优化器状态丢了学习率变回了初始值不是模型坏了。如果你打算把模型放到服务端或手机端建议导出成SavedModel格式model.save(saved_model/emnist_resnet, save_formattf)之后再加载用tf.keras.models.load_model(saved_model/emnist_resnet)不需要依赖model.py的结构定义。这样避免在别的机器上因为model.py缺失而报错。注意先load_weights再保存否则保存的是随机权重。5. 踩坑与排查字母O和数字0的经典翻车现场5.1 现象训练损失不下降甚至震荡训练跑起来后前几个epoch loss不是快速下降而是一会儿0.4一会儿0.5完全没有收敛趋势。原因最常见的是学习率太高。tf2.1默认的Adam学习率是0.001但如果你按照某些ResNet教程设成了0.1小数据集上一开始就会震荡。另外还有可能是图像预处理方向没改EMNIST图片颠倒着学模型只能靠记忆很难泛化。解决把初始学习率降到0.001到0.01之间打开3.2节说的转置和翻转预处理。我一般先用0.01跑20个epoch观察损失如果降到0.1以下说明正常如果还在0.5上下打转就用0.001重启一次。python mytrain.py --lr 0.001 --epochs 20如果脚本不支持--lr参数直接去mytrain.py里把learning_rate改掉。注意改完重启训练时checkpoint里如果已有旧权重最好先备份再重新跑免得覆盖掉之前还不错的中间结果。5.2 现象验证准确率卡在70%左右上不去20个epoch后训练准确率已经达到99%但验证集只有70%明显是过拟合了。字母数字一共有36类训练集规模只有12万张对于ResNet来说仍然不够模型把训练集里的手写风格背下来了。原因训练集和验证集分布差异大又缺少数据增强。EMNIST数据本身是手写体不同人写同一个字母形态差距很大不用增强就很容易记住训练集。解决加数据增强。在preprocess里加随机旋转、平移、缩放。注意不要加翻转因为字母“b”翻转变“d”数字“6”翻转就变了类别语义会错。def augment(image, label): image tf.image.random_shift(image, 0.1, 0.1) image tf.image.random_rotation( image, 0.15, fill_modeconstant, interpolationbilinear) return image, label训练集map时先augment再preprocess。随机旋转范围0.15弧度大约是8.6度太大容易把“1”和“/”弄混。另外可以用Dropout但ResNet里靠BN已经能压一部分过拟合优先做数据增强。增强后要把shuffle的buffer调大不然同一批增强样本可能重复出现。5.3 现象tensorflow2.1和python3.7报protobuf相关错误运行导入时出现“TypeError:init() got an unexpected keyword argument file”或者protobuf版本冲突直接导致无法启动训练。原因tf2.1依赖protobuf 3.x但你可能在装其他库时升级到了4.x接口不兼容。还有numpy版本过高也会造成类似怪问题因为tf2.1只支持numpy1.19。解决固定版本安装别用最新的。pip install protobuf3.20.3 numpy1.19.5装完重新导入tf确认没有报错。如果pycharm里已经把解释器指向了新环境这一步也要在同一个虚拟环境里操作。vscode里踩过这个坑的多半是选错了解释器装了两次包。还有一个更隐蔽的如果你电脑上同时有python3.6和3.7pip install装到了3.6而pycharm用的是3.7也会出现包找不到。用pip --version看一下当前pip属于哪个解释器。5.4 现象GPU显存不足batch_size调64仍然OOM训练到第5个epoch直接报ResourceExhaustedErrorbatch_size已经降到64了还是崩。原因EMNIST是28x28小图单个样本很小占显存的主要是模型和中间激活。ResNet的每个残差块都会把特征图保留到反向传播层数多、通道数大就容易爆显存。另外你可能没有限制GPU内存增长tf2.1默认会抢占全部显存其他程序也在用时就会OOM。解决第一在训练脚本开头加显存动态增长设置gpus tf.config.experimental.list_physical_devices(GPU) if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError as e: print(e)第二把第一个残差块通道数从64减到32。model.py里的stem_conv输出通道数改成32会牺牲一点准确率但能大幅度降低显存。第三把batch_size降到32配合梯度累积效果也不错。第四如果还是不行就直接用CPU跑这个项目的数据量CPU训练也就慢一到两倍不至于不能接受。6. 进阶把自定义图片接进来批量识别6.1 准备自己的数据集并按文件夹批量预测很多课程设计收尾时需要批处理识别一批图片。我会在demo.py基础上写一个批量脚本遍历某个文件夹里所有png输出csv结果import os import cv2 import numpy as np def batch_predict(folder, model, names): results [] for name in sorted(os.listdir(folder)): if not name.endswith(.png): continue img load_and_preprocess(os.path.join(folder, name)) pred model.predict(img, verbose0) class_id np.argmax(pred) results.append([name, names[class_id], float(np.max(pred))]) return results # 使用示例 names list(0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ) model resnet_model(num_classes36) model.load_weights(tf.train.latest_checkpoint(./checkpoints)) res batch_predict(./test_imgs, model, names) with open(result.csv, w) as f: f.write(filename,pred,confidence\n) for r in res: f.write({},{},{:.4f}\n.format(r[0], r[1], r[2]))这个脚本识别完会得到每张图的文件名、预测字符、置信度。我一般会把置信度低于0.9的单独存到need_verify目录人工复查。实际项目里人工复核量会少很多因为大部分错误都集中在低置信度样本上。6.2 用OpenCV做一个简易摄像头识别如果想把模型接到实时视频流可以调用摄像头然后逐帧预处理。但不要每帧都用model.predict那样CPU占用会很高。常见做法是设置帧间隔或者只对画面中心的固定区域做识别并画框。cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break roi frame[100:228, 100:228] # 中心区域 img load_and_preprocess_roi(roi) # 复用灰度/缩放逻辑 pred model.predict(img, verbose0) label names[np.argmax(pred)] cv2.putText(frame, label, (50, 80), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow(frame, frame) if cv2.waitKey(1) 27: # ESC break cap.release() cv2.destroyAllWindows()摄像头分辨率不确定固定裁剪区域不通用可以再结合边缘检测或轮廓提取但那就是另一个项目了。这里只做演示不用过度追求鲁棒性。6.3 保存错误样本反向优化训练最后说一个我一直坚持的习惯训练结束后不要只看准确率一定要把预测错误的图导出来看。做法很简单在test.py里判断pred ! label然后把图片用cv2.imwrite存到errors目录。你会看到自己模型哪些类老是混。我自己遇到过“0”和“O”大面积混淆后来发现是测试集里数字0和字母O用了同一批样本标签本身就有噪声。从那以后我每次训练完都会强制走一遍错误样本分析不只是看数字。希望帮到你。本文还有配套的精品资源点击获取
返回列表