首页 / 资讯中心 / 文章详情

CIFAR10.zip:TensorFlow图像分类完整工程

CIFAR10.zip:TensorFlow图像分类完整工程 ★ FEATURED ARTICLE
简介CIFAR10.zip 是一份面向深度学习入门者与图像分类实战学习者的配套资料包围绕卷积神经网络在 CIFAR10 数据集上的训练与测试展开适合正在学习 TensorFlow、想跑通第一个图像分类项目、或需要对照完整实现来理解 CNN 结构的学生与开发者。包内共 6 个文件以 2 份 Python 训练脚本为主体配合已训练好的 h5 权重文件、记录损失与准确率变化的曲线图、测试效果截图以及一份说明文档压缩包大小约 2.82MB体量轻巧但环节完整。目前已有 4780 人学习下载。读者可直接加载权重做推理验证也可以基于脚本修改网络层数、超参数重新训练用曲线图比对收敛情况、用测试截图核对分类效果两份脚本还可相互参照帮助理解不同写法下的模型搭建与训练流程减少从零调试的成本。1. 别再从 MNIST 起步了CIFAR10.zip 里那套 32×32 图像分类工程带过几个刚入门的同学几乎每个人都在 MNIST 上刷到过 99%然后卡住不知道下一步该写什么。MNIST 太干净了干净到把真实项目里的麻烦全掩盖了三通道彩色图、要做数据增强、验证集得单独切、模型导出后前后处理对不上。CIFAR10.zip 填的正是这个断层它是一套基于 TensorFlow 的卷积神经网络图像分类完整工程把数据加载、增强、训练、评估、导出串成一条能跑的线10 类 32×32 彩色小图50000 张训练、10000 张测试笔记本就能跑复杂度又足够把问题暴露出来。适合刚学完深度学习基础想跑通一个可改可调项目的人也适合手上正好有 32×32 级别小图分类需求、想找个干净骨架往里塞数据的开发者。2. 工程骨架六个文件撑起数据、模型、训练、导出四层压缩包解开之后你会发现它没有用任何脚手架没有 Flask 后端、没有前端页面就是一个纯训练工程。这种素反而是好事——每一层都摊在明面上改哪一行会影响哪一步读一遍就有数。我一般拿到这类项目先不急着跑先花十分钟把目录和配置读一遍比盲目python train.py然后对着报错猜要快得多。2.1 目录骨架每个文件只负责一段链路合理的工程目录不是把代码按多长切而是按职责切。这个项目的划分方式是这样的cifar10/ ├── config.py # 超参与路径所有脚本的唯一入口 ├── data.py # 下载、切分、增强、构建 tf.data 管道 ├── model.py # 卷积主干 分类头纯函数式构建 ├── train.py # 训练入口callbacks 与断点续训 ├── evaluate.py # 测试集评估、混淆矩阵、按类召回 ├── export.py # SavedModel / TFLite 导出 ├── requirements.txt └── checkpoints/ # best.keras、train_log.csvconfig.py是全局唯一的参数来源data.py只吐tf.data.Datasetmodel.py只吐tf.keras.Modeltrain.py把它们拼起来。这种拆法的好处是评估脚本和导出脚本可以直接复用同一份数据和同一份模型定义不会出现训练时归一化了、推理时忘了这类血泪问题。我在别的项目里见过把模型定义抄两遍的写法训练脚本一遍、推理脚本一遍改了个激活函数忘了同步线上精度直接掉十几个点。2.2 配置入口一个字典管住所有超参新手最容易犯的错是把batch_size128这种数字直接写死在五个地方改一次要全文件搜索替换。这个项目用了一个普通 dict 收口# config.py —— 所有可调项集中在这里脚本里不再出现魔法数字 CONFIG { data_dir: ./data, # 数据缓存位置首次运行会自动下载 num_classes: 10, # 类别数换数据集时必须改 image_size: 32, # 输入边长换数据集时必须改 batch_size: 128, # 显存不够就往下调64/32 都行 epochs: 60, base_filters: 32, # 第一个 stage 的通道数后面每 stage 翻倍 dropout: 0.3, lr: 1e-3, # AdamW 的初始学习率 weight_decay: 5e-4, ckpt_dir: ./checkpoints, export_dir: ./exported_model, }字典本身没什么技术含量价值在于约束任何脚本from config import CONFIG之后就拿得到全部参数实验记录也只需要存这一份 dict。我做对比实验时习惯在train.py开头把CONFIG连同git rev-parse HEAD一起 dump 到checkpoints/run_meta.json过两周回头看那次 86.3% 是怎么跑出来的不用靠记忆。base_filters和dropout这两个参数后面调参章节会反复动先记住它们分别控制模型宽度和正则强度。2.3 环境与依赖TensorFlow 版本对齐比装包本身更麻烦深度学习项目跑不起来的头号原因从来不是代码错是版本不对。这个项目的依赖很少requirements.txt里核心就三行# 建议 Python 3.10 或 3.11太新的版本部分预编译轮子还没跟上 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install tensorflow2.16.1 numpy scikit-learn matplotlibTensorFlow 2.16 之后默认走 Keras 3模型保存后缀从.h5变成了.kerasmodel.save()的行为也有细微差别。如果你的环境里还留着 2.13 之类的老版本ModelCheckpoint写出来的文件新版可能读不了——这不是玄学就是 Keras 2 和 Keras 3 的格式不兼容。装完先做一次自检把版本和 GPU 情况打出来import tensorflow as tf print(TF:, tf.__version__) gpus tf.config.list_physical_devices(GPU) print(GPU:, gpus) # 按需申请显存避免一上来就把整张卡占满导致别的进程起不来 for g in gpus: tf.config.experimental.set_memory_growth(g, True)set_memory_growth(True)这行在共享机器上尤其重要。不开的话 TensorFlow 默认一次性吃掉几乎全部显存同事的进程排队都排不进来。如果你看到GPU: []也别慌CIFAR-10 这个规模纯 CPU 跑 60 个 epoch 大概一两个小时能跑通就行调参阶段再想办法上卡。提示Windows 上原生 TensorFlow 2.11 之后就停止支持 GPU 了。要么走 WSL2要么老老实实 CPU 跑别在这上面耗一晚上。3. 核心链路拆解一个 batch 从磁盘到 logits 的完整路径跑通训练只是开始真正要改东西得知道数据在每一步变成了什么形状。这一章按数据流的顺序拆load_data拿出来的是uint8的(50000, 32, 32, 3)中间经历切片、打乱、增强、批处理进模型时是(128, 32, 32, 3)的float32出来是(128, 10)的 logits。任何一步形状对不上报错信息都会指回这里。3.1 数据层tf.data 管道的算子顺序决定增强有没有生效先把原始数据读进来。CIFAR-10 官方给的测试集是 10000 张很多教程直接拿它当验证集调参调完再报测试精度——这是典型的自己骗自己。这个项目从训练集里切 5000 张做验证测试集留到最后只用一次# data.py import tensorflow as tf from config import CONFIG def load_splits(cfg): (x_tr, y_tr), (x_te, y_te) tf.keras.datasets.cifar10.load_data() # 标签从 (N, 1) 压成 (N,)否则 sparse 损失会报 rank 不匹配 y_tr, y_te y_tr.reshape(-1), y_te.reshape(-1) # 从训练集尾部切 5000 张做验证测试集原封不动留到最后 x_val, y_val x_tr[-5000:], y_tr[-5000:] x_tr, y_tr x_tr[:-5000], y_tr[:-5000] # 保持 0~255 的 uint8 值域转 float32 只是为了让增强算子好算 cast lambda a: a.astype(float32) return (cast(x_tr), y_tr), (cast(x_val), y_val), (cast(x_te), y_te)这里刻意没有除以 255。归一化被放进了模型的第一层下一节会看到这样导出的模型自带预处理端上部署时不用再实现一遍除法和通道顺序。这是我在一个移动端项目上踩过坑之后养成的习惯训练脚本里的预处理逻辑只要没写进计算图早晚会在某个客户端上漏掉。增强函数负责把 32×32 随机平移加翻转注意先 pad 到 40 再裁回 32否则random_crop在原尺寸上等于什么都没做def augment(image, label): # 先补边到 40×40再随机裁回 32×32等价于 ±4 像素的平移 image tf.image.resize_with_crop_or_pad(image, 40, 40) image tf.image.random_crop(image, size[32, 32, 3]) image tf.image.random_flip_left_right(image) return tf.clip_by_value(image, 0.0, 255.0), label管道组装是全文最容易翻车的地方map必须在batch之前def build_dataset(x, y, cfg, trainingFalse): ds tf.data.Dataset.from_tensor_slices((x, y)) if training: ds ds.shuffle(10_000, seed42) # 缓冲 1 万够打散了 ds ds.map(augment, num_parallel_calls4) # 增强作用在单张图上 ds ds.batch(cfg[batch_size]) ds ds.prefetch(tf.data.AUTOTUNE) # 训练与取数重叠 return ds如果map(augment)写在batch之后augment收到的就是(128, 32, 32, 3)的整批张量random_crop会对整批做同一次裁剪——128 张图裁的位置一模一样等于只做了一个全局平移增强效果基本为零。这个错误不报错、不警告只体现在验证集精度比预期低三四个点上属于典型的黑匣子问题。数据常驻内存这件事也值得算一笔50000×32×32×3 的 float32 大约 586 MiB加上验证集和测试集总共不到 750 MiB普通笔记本完全吃得下。所以这里不需要from_tensor_slices之外的花活也别去开什么多进程读盘纯属给自己找麻烦。3.2 模型层三层 stage 的 Conv-BN-ReLU 堆叠模型结构是标准的 VGG 风格小网络两个卷积一组、池化一次、通道翻倍一共三个 stage。先把卷积块抽出来避免重复代码# model.py from tensorflow.keras import layers, Model def conv_bn(x, filters): x layers.Conv2D(filters, 3, paddingsame, use_biasFalse)(x) x layers.BatchNormalization()(x) # 紧跟卷积卷积层就可以关掉 bias return layers.Activation(relu)(x) def build_model(num_classes10, base32, dropout0.3): inp layers.Input(shape(32, 32, 3), nameimage) # 归一化写进模型导出后外部只需要喂 0~255 的原始像素 x layers.Rescaling(1.0 / 255)(inp) x conv_bn(x, base) # 32×32×32 x conv_bn(x, base) x layers.MaxPooling2D(2)(x) # 16×16×32 x conv_bn(x, base * 2) # 16×16×64 x conv_bn(x, base * 2) x layers.MaxPooling2D(2)(x) # 8×8×64 x conv_bn(x, base * 4) # 8×8×128 x layers.GlobalAveragePooling2D()(x) # 8×8×128 - 128比 Flatten 少 8 倍参数 x layers.Dropout(dropout)(x) out layers.Dense(num_classes, namelogits)(x) # 不加 softmax return Model(inp, out, namecifar_cnn)几个刻意的选择值得说一下。use_biasFalse是因为 BN 的 beta 参数已经把偏置吸收了再留一个纯属浪费GlobalAveragePooling2D代替Flatten把 8192 维压到 128 维参数量从 80 万降到 8 万小数据集上泛化明显更稳最后一层不接 softmax输出 logits配合损失函数里的from_logitsTrue数值上更稳定因为交叉熵内部会做 log-softmax不需要先算一遍指数再取对数。Rescaling(1/255)放在最前面同样是部署导向的。模型文件里带着这一步后面导出 TFLite、写 Android 端推理代码时Bitmap读出来的0~255字节数组直接往输入张量里塞就行不用在 Java 侧再写一遍归一化——少写一处就少一处和训练不一致的可能。3.3 训练层fit 与自定义 train_step 两条路常规训练用compilefit就够这个项目默认走这条路# train.py import tensorflow as tf from config import CONFIG from data import load_splits, build_dataset from model import build_model cfg CONFIG (xtr, ytr), (xval, yval), (xte, yte) load_splits(cfg) train_ds build_dataset(xtr, ytr, cfg, trainingTrue) val_ds build_dataset(xval, yval, cfg, trainingFalse) model build_model(cfg[num_classes], cfg[base_filters], cfg[dropout]) model.compile( optimizertf.keras.optimizers.AdamW( learning_ratecfg[lr], weight_decaycfg[weight_decay]), # 标签是整数索引用 Sparse 版本如果标签是 one-hot 就换成 Categorical losstf.keras.losses.SparseCategoricalCrossentropy(from_logitsTrue), metrics[tf.keras.metrics.SparseCategoricalAccuracy(nameacc)], ) callbacks [ tf.keras.callbacks.ModelCheckpoint( f{cfg[ckpt_dir]}/best.keras, monitorval_acc, save_best_onlyTrue, modemax), tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.5, patience3, min_lr1e-5), tf.keras.callbacks.EarlyStopping( monitorval_loss, patience8, restore_best_weightsTrue), tf.keras.callbacks.CSVLogger(f{cfg[ckpt_dir]}/train_log.csv), ] model.fit(train_ds, validation_dataval_ds, epochscfg[epochs], callbackscallbacks)三个 callback 各管一件事ModelCheckpoint只留验证精度最高的那一版权重ReduceLROnPlateau在验证损失停滞时把学习率砍半EarlyStopping兜底防止白跑。注意restore_best_weightsTrue训练结束后模型会自动回滚到最优 epoch 的状态不然你拿到的是最后一个 epoch 的权重很可能已经过拟合了。SparseCategoricalCrossentropy配整数标签CategoricalCrossentropy配 one-hot 标签这两者混用是新手最常见的静默失败——损失能算出来梯度也在动但数值完全不对精度永远卡在 10%。如果你要做梯度裁剪、多任务损失加权、或者混合精度下需要手动LossScaleOptimizer就得下沉到自定义train_stepclass CifarModel(tf.keras.Model): def train_step(self, data): x, y data with tf.GradientTape() as tape: logits self(x, trainingTrue) loss self.compiled_loss(y, logits) grads tape.gradient(loss, self.trainable_variables) # 全局范数裁剪防止个别 batch 把权重带偏 grads, _ tf.clip_by_global_norm(grads, 5.0) self.optimizer.apply_gradients(zip(grads, self.trainable_variables)) self.compiled_metrics.update_state(y, logits) return {m.name: m.result() for m in self.metrics}train_step的返回值必须是一个{指标名: 标量}的字典fit的进度条和 CSVLogger 全靠它。忘了update_state的话进度条会一直显示 0但损失在正常下降——又是一个不报错的坑。3.4 评估与导出混淆矩阵比单一准确率有信息量evaluate.py里做的事情比model.evaluate多一步它把预测结果拆成按类的召回率import numpy as np from sklearn.metrics import classification_report, confusion_matrix logits model.predict(build_dataset(xte, yte, cfg), verbose0) pred np.argmax(logits, axis1) # logits - 类别索引 print(classification_report(yte, pred, target_namesCLASS_NAMES, digits4)) cm confusion_matrix(yte, pred) print(cm.diagonal() / cm.sum(axis1)) # 每一类的召回率CLASS_NAMES就是 CIFAR-10 那十类顺序固定为飞机、汽车、鸟、猫、鹿、狗、蛙、马、船、卡车。看混淆矩阵比看总的 85% 有用得多——猫和狗互相混淆、鹿和马互相混淆是这套数据的经典现象两者加起来能占全部错误的四成。如果你的业务场景对某一类特别敏感就该针对性地去做这一类别的数据增强或者类别加权而不是笼统地加 epoch。导出用 Keras 3 的export接口拿 SavedModel 格式model.export(cfg[export_dir]) # 生成带 signatures 的 SavedModel 目录导出之后立刻做一次自检拿一张原图跑一遍确认输出维度对得上img tf.io.decode_jpeg(tf.io.read_file(sample.jpg)) img tf.image.resize(img, [32, 32]) img tf.expand_dims(tf.cast(img, tf.float32), 0) # (1, 32, 32, 3)值域 0~255 prob tf.nn.softmax(model(img, trainingFalse))[0] print(CLASS_NAMES[int(tf.argmax(prob))], float(tf.reduce_max(prob)))trainingFalse不能省它决定了 BN 层走移动均值还是走当前 batch 统计量。推理时忘了传这个参数同一张图两次预测结果不一样排查起来能让人怀疑人生。4. 跑通与调参从首次运行到 85% 验证准确率的实操顺序代码读完了就该跑。这一章给的是我自己在这个工程上的实际推进顺序包括每一步的预期数字——有了预期看到异常才知道是代码问题还是正常波动。4.1 首次运行命令、日志与合理预期第一次跑先砍掉一半 epoch确认链路是通的python train.py --epochs 20 --batch-size 128 # 另开一个终端盯日志 tail -f checkpoints/train_log.csv单卡 GPU 上每个 epoch 大概 8 到 15 秒20 个 epoch 两三分钟就能出结果。CPU 上慢十倍左右第一次验证建议先用--epochs 3确认能跑通再放开。预期数字这样看没做增强的裸网络大概在 70% 到 75% 验证准确率加上数据增强和余弦衰减能到 85% 上下把base_filters提到 64 再堆一个卷积能摸到 88%。如果跑了 20 个 epoch 验证精度还在 10% 附近别怀疑人生先去看损失函数的标签格式对不对如果在 30% 到 50% 之间徘徊八成是增强写错了位置或者学习率大了十倍。train_log.csv里会记下每个 epoch 的loss、acc、val_loss、val_acc、lr把它拖进表格软件画两条曲线比盯着进度条刷屏有用。4.2 调参顺序先学习率后增强最后才动模型宽度新手常犯的错是同时改五个参数然后不知道是哪个起的作用。我一般按下面这个顺序推进一次只动一个变量步骤动什么看哪个指标大致效果1学习率 1e-4 到 3e-3 之间扫三档前 5 个 epoch 的 val_loss 下降速度70% 到 78%2打开随机裁剪加翻转train 与 val 的精度差到 83% 左右3base_filters 32 提到 64单 epoch 耗时涨了多少到 86%4dropout 与 weight_decay 微调val_loss 尾部是否上翘稳住不掉点学习率放第一位是因为它影响最大、试错成本最低跑三个 epoch 就能看出趋势。增强放第二位是因为它直接决定过拟合程度而 CIFAR-10 这个数据量不加增强的模型几乎必然在 20 个 epoch 之后开始背训练集。模型宽度放最后因为它会让训练时间线性甚至超线性增长先确认前两步都到位了再花这个钱。ReduceLROnPlateau的patience3配factor0.5是个保守组合如果你观察到验证损失在某个值附近抖动但整体还在降可以把 patience 调到 5 让它再等等如果曲线已经明显上翘就让它早点砍。4.3 训练曲线怎么读三种形态对应三种病看曲线是调参的基本功我把常见形态归成三类。第一种是两条线一起高高挂着下不来train_acc 和 val_acc 都在 60% 附近。这是欠拟合模型容量不够或者学习率太小先加base_filters不行就把epochs拉长别急着加正则。第二种是 train_acc 一路冲到 99%val_acc 在 80% 附近打转两条线的缝越拉越宽。这是过拟合增强不够或者正则太弱。优先检查 3.1 节那个map的位置确认增强真的生效了再考虑提高dropout到 0.4、weight_decay到 1e-3。第三种是 val_loss 在某个 epoch 突然跳高一大截然后慢慢降回来。这通常是学习率过大撞上了某个梯度大的 batchReduceLROnPlateau会自动处理如果跳得太频繁把初始学习率降一半或者上clip_by_global_norm。还有一个容易被忽略的细节BN 层的移动均值需要几百个 batch 才能稳下来所以前两三个 epoch 的 val_acc 偏低是正常现象别在第一个 epoch 结束就急着下结论说模型不行。5. 避坑与排查五个把我卡了半天的现场记录这一节里每一条都是真发生过的不是从文档里抄的常见问题列表。共同特点是不报错——报错的坑好查静默失败的坑才要命。5.1 笔记本里反复建模型第二次训练报显存不足现象Jupyter 里先跑了一遍小实验改完超参重新执行训练单元第 1 个 epoch 就抛ResourceExhaustedError但nvidia-smi看着显存还有余量。原因Keras 的模型对象被全局变量和旧的计算图引用着Python 的垃圾回收不会立刻释放显存第二次建模型是在旧的基础上再占一份。Jupyter 里的全局变量生命周期跟脚本完全不一样脚本跑完进程退出显存自然回收所以同样的代码在命令行里没问题。解决训练单元开头固定加三行清场再配合set_memory_growth。import gc, tensorflow as tf tf.keras.backend.clear_session() # 清掉默认计算图和层命名状态 gc.collect() # 触发一次回收让旧模型真正被释放清完之后显存占用立刻回落到基线。真要在同一个进程里连着跑多组实验就把每组实验包成一个函数函数返回时局部变量自动失效比在顶层堆一堆model1、model2干净得多。5.2 训练 20 个 epoch精度死死卡在 10%现象损失在 2.3 附近小幅震荡准确率始终 0.1000 左右跟随机猜十类一样。没有报错没有警告。原因标签形状和损失函数不匹配。load_data()返回的y_train是(50000, 1)如果直接喂给SparseCategoricalCrossentropyTF 会把它当成一个长度为 1 的序列标签去处理算出来的损失值对但标签全被当成第 0 类附近的东西模型学到的是一个恒等输出。另一种变体是标签做了 one-hot 但损失用了 Sparse 版本表现一样。解决在数据加载处统一把标签压成一维并在训练前打一次形状断点。y_tr y_tr.reshape(-1) # (N, 1) - (N,) assert y_tr.ndim 1 and y_tr.max() cfg[num_classes]养成习惯凡是标签进损失函数之前都把shape和min/max打一次。代价是一行 print收益是省下两小时。5.3 增强加了验证精度反而比不加低现象加完随机裁剪和翻转训练集精度涨得更慢了验证精度比不加增强时还低两三个点两条曲线的缝隙也没收窄。原因map(augment)写在batch()之后augment收到的是整批(128, 32, 32, 3)random_crop的size参数作用在批维度上退化了整批图被裁在同一个位置、同一个翻转状态。等价于对每个 epoch 做了一个全局的、所有样本一致的变换既没增加样本多样性又引入了额外的分布偏移。解决把map挪到batch之前见 3.1 节的build_dataset并在augment里加一句打印验证它拿到的形状def augment(image, label): tf.debugging.assert_rank(image, 3) # 必须是单张 (H, W, C)批进来就会炸 ...assert_rank在生产代码里可以去掉但写数据增强的时候留一行能省掉大量为什么增强没效果的困惑。5.4 断点续训之后验证精度突然掉了五个点现象训练跑到第 30 个 epoch 手动中断用model.load_weights(best.keras)恢复继续跑第 31 个 epoch 的验证精度比中断前那次低了一大截学习率也回到了初始值。原因只存了权重没存优化器状态。AdamW 内部维护两阶动量恢复时这两个缓冲区是空的前几个 step 的更新方向基本靠当前梯度硬拽相当于把优化器重启了一遍。同时ReduceLROnPlateau的衰减历史也没了学习率回到1e-3模型被推离了原来的收敛点。解决要么存完整的.keras文件含优化器状态要么用tf.train.Checkpoint把模型、优化器、epoch 计数一起管起来。ckpt tf.train.Checkpoint(modelmodel, optimizermodel.optimizer, epochtf.Variable(0)) manager tf.train.CheckpointManager(ckpt, ./checkpoints/resumable, max_to_keep3) # 训练循环里每个 epoch 末尾manager.save() # 恢复时ckpt.restore(manager.latest_checkpoint).expect_partial()expect_partial()是必须的不然加载旧 checkpoint 时缺字段会直接抛异常。恢复之后把optimizer.learning_rate手动设回中断时的值再继续跑。5.5 训练好的模型在别人机器上预测全是同一类现象本地evaluate.py跑出来 86%把模型和一张图发给同事他那边推理出来十次有九次是 cat概率分布还特别平。原因预处理不一致。同事的推理脚本直接把0~255的数组喂进了一个期望0~1的模型或者通道顺序按 BGR 读的图。BN 层对输入尺度极其敏感输入整体放大 255 倍激活值直接饱和输出退化成常数。解决把归一化写进模型3.2 节那个Rescaling层导出时它会被固化进计算图同时在导出脚本里加一份preprocess.md写清楚输入的形状、值域、通道顺序跟模型文件放一起。后来我干脆在 SavedModel 的 signature 里带上input_nameimage_0_255_rgb名字本身就是文档。6. 二次开发换成自己的数据集并压到端侧模型这个工程真正的价值不在 CIFAR-10 本身而在它是个可以往里塞数据的壳子。换成自己的 32×32 或者 64×64 分类任务改动面比想象中小很多。6.1 换数据集只需要改四个地方第一处是config.py里的num_classes和image_size。第二处是数据加载函数把load_splits换掉最省事的是走目录式读取train_ds tf.keras.utils.image_dataset_from_directory( my_data/train, image_size(cfg[image_size], cfg[image_size]), batch_sizecfg[batch_size], label_modeint, # int 对应 sparse 损失categorical 对应 one-hot )第三处是augment里的尺寸常量40 和 32 这两个数字要跟着image_size一起变裁剪的 padding 一般取边长的八分之一左右。第四处是model.py的池化次数——输入 64×64 就多加一次MaxPooling2D输入 32×32 保持两次输入 128×128 加到四次保证进GlobalAveragePooling之前的特征图还有 4×4 以上。改完先别急着训练跑一遍一个 batch 的过拟合验证拿 32 张图关掉增强和 dropout训练 200 步看能不能把损失压到接近 0。压不下去说明模型结构或者标签映射有问题压下去了才说明链路是通的。这个检查我每次都做。6.2 端侧导出TFLite 转换与前后处理对齐要在移动端或者边缘设备上跑转成 TFLiteconverter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] # 动态范围量化体积压到 1/4 tflite_model converter.convert() open(cifar_cnn.tflite, wb).write(tflite_model)量化之后精度通常会掉 0.5 到 1.5 个点可接受。如果掉得多就准备两百张训练图做代表性数据集走Optimize.DEFAULT加converter.representative_dataset的全整数量化精度损失能收回一部分。转换完必须做一次数值对齐验证同一张图分别用 Keras 模型和 TFLite 解释器跑比较两个 logits 向量的余弦相似度低于 0.99 就说明转换过程出了问题常见原因是模型里有 TFLite 不支持的自定义算子被替换了。interp tf.lite.Interpreter(model_pathcifar_cnn.tflite) interp.allocate_tensors() inp interp.get_input_details()[0] interp.set_tensor(inp[index], batch_0_255) # 注意 dtype量化模型是 int8 interp.invoke() tflite_logits interp.get_tensor(interp.get_output_details()[0][index])量化模型的输入是int8需要在喂之前按scale和zero_point做转换别直接把 float 塞进去解释器不报错但结果全错。6.3 改完之后照着这张表验一遍检查项怎么验不通过说明什么形状链路打印每个 batch 的x.shape、y.shape数据管道或批次维度出错标签映射抽样 20 张图人眼核对类别名class_names顺序和训练时不一致过拟合小样本32 张图训练 200 步损失应接近 0模型结构或损失配置有问题增强生效同一 batch 内可视化 8 张增强后的图map位置错了或增强幅度为零导出一致性Keras 与 TFLite 输出余弦相似度大于 0.99转换丢算子或预处理没固化端到端从原始文件读图一路到类别名前后处理在某处漏了一步最后一行表格是我吃过亏才加上的曾经有一次模型精度没问题、TFLite 转换也没问题卡在客户端读图的通道顺序上BGR 当 RGB 喂进去整批预测都偏成了卡车。查了两天才发现是拍照库的默认格式。从那以后我每次换数据集都强制走一遍完整验证尤其是从原始文件读一张图一路走到打印类别名这一条——不看配置文件、不看中间变量就看最终输出对不对。模型这行当里中间任何一步的自我感觉良好都不作数只有端到端跑通的那一次才算数。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站