YAOTU INSIGHTS

基于TensorFlow与CNN的天气识别图像分类实战

基于TensorFlow与CNN的天气识别图像分类实战
1. 项目思路与整体设计1.1 天气识别到底在解决什么问题先说点实际的。天气识别本质上是一个图像分类问题而且是深度学习入门阶段最适合练手的任务之一。它不像目标检测那样需要画框也不像语义分割那样要逐像素分类只需要给一张图片打上一个标签晴天、多云、下雨、起雾完事。任务本身不复杂但麻雀虽小五脏俱全数据预处理、模型设计、训练调参、过拟合抑制、结果评估这些环节一个都跑不了。这个系列做到第三篇前两篇已经把TensorFlow的基础API、张量操作、Keras模型构建的套路过了一遍。这篇拿天气识别当载体正好把前面的知识点串起来怎么加载图片数据、怎么划分训练集和测试集、怎么搭建一个能用的卷积神经网络、怎么通过训练曲线判断模型状态、怎么在测试集上客观评估效果。学完这一整套流程你其实就掌握了图像分类项目的标准操作流程后面换成猫狗识别、场景分类、农作物病害识别无非是换数据、调结构而已套路完全一致。天气识别的使用场景其实挺多的。最典型的是辅助气象观测很多户外摄像头会持续拍摄天空画面如果能在端侧或者服务端自动识别天气状态就能大幅减少人工标注成本。还有智能家居场景智能窗户可以根据外部天气自动调节透光率摄影爱好者也能用天气分类帮海量照片自动打标签回头找图的时候不用一张张翻。即便是拿来当毕业设计或者个人项目练手天气识别这个主题也足够完整、足够有展示价值。适合来看这篇的人大概分三类。第一类是把TensorFlow基础语法过了一遍、但还没完整跑过一个图像项目的初学者这篇可以帮你把零散的知识点串成完整链路第二类是做过一些传统机器学习、但想了解深度学习如何处理图像数据的朋友我尽量在讲每一步的时候把“为什么这么做”解释透第三类是已经在做图像分类但经常被过拟合、准确率上不去这些老问题折磨的人第五部分整理了一批排查经验应该能用得上。1.2 技术选型为什么是TensorFlow、为什么是CNN选TensorFlow这个事儿其实没什么悬念。行业里做深度学习落地的TensorFlow和PyTorch两家占比最大TensorFlow的优势在于生产部署链路成熟——训练好的模型可以转成TensorFlow Lite跑在手机端也可以借助TensorFlow Serving在服务端部署Keras这套高层API对新手来说又足够友好。这个系列一直在用TensorFlow所以这一篇延续技术栈你可以在原有环境上直接往下走不用额外折腾。模型选型上天气识别用卷积神经网络CNN是标准答案。要理解为什么是CNN得先看图片数据的特殊性。一张彩色图片比如224×224的展平之后是150528个像素值如果用全连接网络来训练光输入层和第一个隐藏层之间的参数量就是天文数字不仅训练极慢而且极容易过拟合。CNN通过两个核心机制来化解这个问题第一个机制是局部感受野。卷积核只关注图片的局部区域相当于给网络装了一个局部视野它先看纹理、边缘这些小特征再通过层层堆叠组合成大特征比如云层边缘、雨线方向、雾气浓度。第二个机制是权值共享。同一个卷积核在整张图片上滑动时权重是固定的这大大减少了参数量相当于用同一套模板去扫全图。这两个机制让CNN天然适合处理有空间结构的图像数据因此成了图像分类的事实标准。不过这里我提一句天气识别有个特殊性就是单张图片的信息量其实不够充分。比如“下雨”这个类别静态照片里可能只看到地面的积水水汽大的时候天空看起来和雾天差不多。所以如果你是做实际项目可以考虑多帧图像或附加气象传感器数据来辅助判断。作为学习项目的天气识别我们就把范围限定在单帧图像分类这个定位要在心里明确——后续如果准确率卡在某个水平上不去不一定是模型不行也可能是问题本身的边界决定的。1.3 方案整体预览从数据到部署完整流程整个项目的流程可以拆成五段数据准备、数据预处理、模型构建、训练调优、评估预测。我在实际跑项目时习惯画一张这样的流程清单避免在中途迷失方向。数据准备从公开渠道获取天气图像数据整理成标准的分类目录结构检查标签质量数据预处理统一图片尺寸、归一化像素值、按比例划分训练集与验证集、做数据增强模型构建搭建CNN主干合理配置卷积层、池化层、全连接层设置Dropout与批归一化防过拟合训练调优选择合适的优化器与损失函数配置学习率调度和早停策略边训练边观察曲线评估预测在从未参与训练的测试集上评估准确率与损失对单张图片做推理这篇文章会按照这个顺序展开每个环节我都尽量给出可以直接照抄的代码、参数和操作路径同时把背后的取舍讲清楚。有一点要先说明天气数据集的公开版本比较多不同来源的类别定义和图片质量差别很大。我在本文中使用的是公开渠道常见的五类别天气数据集包含日出、多云、下雨、晴天、起雾五个类别每类约六七百张图片总量在三千多张。这个规模对深度学习来说不算大所以整个项目会很自然地暴露过拟合问题对学习来说反而是好事——你能亲眼看到过拟合长什么样以及怎么把它压下去。2. 数据准备与预处理2.1 数据集的目录组织与标签设计拿到原始图片之后第一件事是整理目录结构。Keras的image_dataset_from_directory函数有一个硬性约定按类别建子文件夹图片放到对应的文件夹里。我的习惯是建一个根目录下面按标签分子目录形如weather_dataset/ ├── train/ │ ├── cloudy/ │ ├── foggy/ │ ├── rainy/ │ ├── shine/ │ └── sunrise/ ├── validation/ │ ├── cloudy/ │ ├── foggy/ │ ├── rainy/ │ ├── shine/ │ └── sunrise/ └── test/ ├── cloudy/ ├── foggy/ ├── rainy/ ├── shine/ └── sunrise/看到这里你可能会问为什么不直接把所有图片放在一个文件夹让代码自动划分这是因为image_dataset_from_directory虽然也支持validation_split参数自动切分但它的随机划分方式在类别不平衡时可能出问题而手动划分可以精确控制每一类在训练集和验证集中都有足够的样本。我的做法是先用脚本按8:1:1的比例做分层随机划分确保每个类别在三个集合中的分布都接近原始比例避免某一个类别在验证集里只剩十几张的尴尬局面。标签这块有一个特别容易踩的坑文件夹名字就是标签。有些公开数据集下载下来文件夹命名不规范比如叫1、2、3或者混着空格、中文。image_dataset_from_directory倒是能处理中文但我强烈建议统一改成小写英文单词。原因有二第一后续如果要输出标签名称做展示英文处理起来省事第二如果哪一天你想把模型转到移动端或者服务端部署标签字符串的映射表越简单越不容易出错。我自己遇到过一次文件夹名里带空格的情况数据加载时报了一堆莫名其妙的路径错误排查了半天才发现是空格闹的从此以后所有数据集一律先规范化目录名。2.2 图片加载与尺寸统一深度学习模型要求输入尺寸固定而网上扒下来的图片尺寸各异所以数据管道的第一个环节就是把图片统一缩放。尺寸的选择会直接影响训练速度和模型效果。身体第一原则不要上来就用224×224。这是ImageNet时代的惯用尺寸但那是为了匹配上千类的大规模分类任务。对于五分类天气识别数据集本身就小每类几百张图片用太高的分辨率意味着模型要学习更多的参数在小数据集上极易过拟合。我在实践中推荐180×180这个尺寸在保留足够细节的同时把计算量压到224×224的约三分之二对模型容量的要求也相应降低。你要是机器性能紧张降到128×128也可以只是模型精度上限会低一些。用image_dataset_from_directory加载数据的代码很简单from tensorflow.keras.preprocessing import image_dataset_from_directory train_ds image_dataset_from_directory( weather_dataset/train, image_size(180, 180), batch_size32, label_modecategorical, shuffleTrue ) val_ds image_dataset_from_directory( weather_dataset/validation, image_size(180, 180), batch_size32, label_modecategorical, shuffleFalse )有几个参数要特别说明。label_modecategorical会生成one-hot编码的标签向量和后面模型输出层的softmax激活函数匹配。shuffle对训练集必须开否则模型会按类别顺序学导致某些批次里全是同一类图片模型几乎是靠“背顺序”来收敛验证时立刻暴露原型验证集通常关掉shuffle方便我们观察每一步的损失曲线是否有规律性波动。batch_size选32这是一个比较稳的中间值太小则梯度更新频繁训练震荡剧烈太大会把显存撑满而且在小数据集上大batch往往不如小batch效果好。加载完成之后我一般会做一步检查把每个批次的张量形状打印出来看一眼。很多新手容易在这一步开始就错——比如图片通道数不对、标签维度不对如果不在数据进入模型前发现后面排错会非常痛苦。2.3 归一化与数据增强原始图片的像素值是0到255之间的整数直接喂给模型虽然也能训但效果会打折扣。原因在于梯度下降法的收敛速度依赖于各维度量纲的一致性。神经网络内部的权重初始值通常是在零点附近的小数值如果输入值都在200多这个量级加权求和后可能直接超过激活函数的饱和区间梯度变得极小训练半天也不动一步。常见的做法是把像素值缩放到0到1之间。image_dataset_from_directory加载出的数据类型是uint8不能直接做除法缩放需要先转成float32。我习惯写一个简单的映射函数配合map方法应用def normalize(image, label): image tf.cast(image, tf.float32) / 255.0 return image, label train_ds train_ds.map(normalize) val_ds val_ds.map(normalize)接下来是数据增强。这是小数据集训练的关键一步也是新手最容易忽略的一步。所谓数据增强就是在训练过程中对图片做随机变换比如水平翻转、小角度旋转、轻微缩放、亮度抖动从而让模型“凭空”看到更多变体。天气类图片的方向性通常不那么敏感但亮度变化对天气识别很有意义同一片天空不同曝光强度拍出来的效果迥异让模型适应亮度变化能有效提升泛化能力。TensorFlow在keras.layers里直接内置了数据增强层用法非常顺手from tensorflow.keras import layers data_augmentation tf.keras.Sequential([ layers.RandomFlip(horizontal), layers.RandomRotation(0.1), layers.RandomZoom(0.1), layers.RandomBrightness(0.2), ])这里有个容易犯的错误数据增强只应该应用在训练集上不能用在验证集和测试集上。想一想就明白了验证集是用来模拟真实场景的真实场景里你不会把图片翻转、旋转之后再送去识别所以验证集必须保持原始状态。代码上我把增强层放进模型前部并且只在训练时生效——Keras的很多内置层自带training参数在模型训练时执行增强在推理时自动跳过这一点设计得很聪明。增强强度也要控制。旋转角设成0.1弧度大约5.7度是合适的设成0.5弧度图片就没法看了模型等于在学一堆扭曲图形反而不利于收敛。亮度抖动0.2也是中等强度太强的亮度变化会让晴天照片暗得像个阴天标签和内容就对不上了。3. CNN模型构建与核心原理3.1 从头搭建CNN卷积、池化、激活、全连接模型构建是整个项目的核心环节。我先把一个能跑的CNN结构写出来然后逐层解释设计逻辑。from tensorflow.keras import layers, models model models.Sequential([ data_augmentation, layers.Rescaling(1./255), layers.Conv2D(32, (3, 3), activationrelu, input_shape(180, 180, 3)), layers.MaxPooling2D(2, 2), layers.Conv2D(64, (3, 3), activationrelu), layers.MaxPooling2D(2, 2), layers.Conv2D(128, (3, 3), activationrelu), layers.MaxPooling2D(2, 2), layers.Conv2D(128, (3, 3), activationrelu), layers.MaxPooling2D(2, 2), layers.Flatten(), layers.Dropout(0.5), layers.Dense(128, activationrelu), layers.Dense(5, activationsoftmax) ])逐层来看。卷积层用Conv2D实现括号里的32表示这一层输出32个特征图(3, 3)是卷积核大小。3×3是目前实践中最常用的卷积核尺寸理论上用更大的核能覆盖更大的区域但多个小卷积核堆叠起来可以获得相同感受野同时参数量更少。举个例子两层3×3卷积堆叠的感受野等于一层5×5卷积但参数量为18对25而且多了中间的非线性激活表达能力更强。激活函数用relu数学形式是f(x) max(0, x)。它最大的优势是缓解梯度消失问题正区间的导数恒为1梯度可以顺畅地反向传播到前面的层。相比之下传统的sigmoid函数在两端导数趋近于0深度网络训练时梯度传几层就消失殆尽模型根本学不动。现在CNN的卷积层激活函数基本默认relu几乎不需要讨论。池化层用MaxPooling2D(2, 2)作用是把特征图尺寸减半取每个2×2窗口里的最大值。池化有两个好处一是降低计算量二是引入平移不变性——图片里的云、雨点稍微偏移几个像素池化层依然能选出近似相同的最大值模型对位置微小变动不敏感泛化能力更强。随着网络加深特征图通道数从32翻到64再到128这是CNN设计的通行做法空间分辨率逐步降低通道数逐步增加让网络在更粗糙的空间尺度上表达更丰富的语义特征。最后Flatten把二维特征图拉成一维向量接上Dropout和全连接层映射到五个类别的概率分布。3.2 批归一化与Dropout的实际用法上面这个结构里我用了Dropout但故意没在卷积层后面加批归一化。这里我展开讲一下两者的适用场景因为很多初学者会把这两种防过拟合手段混在一起乱放。Dropout的原理是训练时随机“丢弃”一部分神经元的输出让网络不能依赖某个特定的节点组合。它的效果相当于同时训练了大量共享权重的子网络推理时再用全部节点做平均泛化能力自然更强。在天气分类中类别之间的视觉差异不算特别明显全连接层又集中了模型大部分待训练参数是全模型最容易过拟合的位置所以在Flatten之后的Dense层前加一个Dropout(0.5)非常有效。批归一化BatchNormalization的作用机制完全不同。它把每一层激活前的输入分布强行拉回零均值、单位方差附近让每一层都能在一个稳定的分布区间内学习。这样做的直接收益是训练显著加速、对学习率不再那么敏感。批归一化通常放在卷积层之后、激活函数之前顺序是卷积→批归一化→ReLU激活。我见过很多人在激活函数之后放批归一化虽然也能训但违背了设计初衷效果会打折。我的经验是数据集规模小、担心过拟合时优先用Dropout重点放在全连接部分模型结构深、训练速度慢时加批归一化。两者叠加使用也是常规操作但要控制好量——Dropout概率在0.3到0.5之间选择批归一化加得太多会明显拖慢单次迭代速度。3.3 为什么不直接用迁移学习看到这里熟悉深度学习的朋友可能会问分类任务为什么不直接用预训练模型用预训练模型做迁移学习比如加载在ImageNet上训练好的权重然后冻结前面所有层只训练分类头小数据集上通常能拿到更好的效果训练时间也短得多。这个质疑完全合理。迁移学习在工程上几乎是无脑的最优解尤其当你的数据集只有几千张图片时预训练模型的泛化能力远胜从零训练的CNN。但这是学习系列不是工程部署系列。从零搭建CNN的价值在于它能逼你亲手理解卷积、池化、感受野、过拟合这些核心概念到底是怎么运作的而迁移学习把这一切都封装在“加载权重”四个字里了学不到底层逻辑。我的建议是先把从零搭建的流程跑通一遍理解每个环节的角色然后你再去做迁移学习把两者对比一下准确率和训练时间。这个对比过程本身是最有价值的学习素材。后面如果有机会我可以单独写一篇迁移学习实战在天气识别这个项目上把预训练模型的效果展示出来。顺带说一句如果你只是想快速出一个成果给导师展示或应付项目验收直接跳到迁移学习是更聪明的选择。学习成本和成果产出之间你要清楚自己当前更看重哪个。4. 训练配置与调参实战4.1 编译配置优化器、损失函数、评估指标模型搭好之后第一步是编译。编译就是告诉TensorFlow三件事用什么方法更新权重、用什么标准衡量当前预测和真实标签的差距、用什么指标监控训练效果。model.compile( optimizeradam, losscategorical_crossentropy, metrics[accuracy] )优化器选择Adam。它是目前最常用的自适应学习率优化器核心思路是对每个参数单独调整学习率梯度大的方向步长小一点梯度小的方向步长大一点整体上收敛平稳、对初始学习率不敏感。SGD当然也能训但对学习率的选择非常挑剔需要手动调学习率衰减策略对新手不太友好。RMSprop在RNN中表现优异但在CNN中Adam的默认参数通常已经足够好没必要折腾。损失函数选择categorical_crossentropy这是多分类任务的标准配置。因为标签是one-hot编码每个样本的真实分布是一个只在对应类别位置为1的分布交叉熵衡量的是模型预测分布和真实分布之间的距离。之所以不用均方误差是因为分类任务的输出是概率概率分布之间的距离用交叉熵度量更合理而且交叉熵配合softmax输出层在梯度更新上有独特的数学优势——当预测错得离谱时梯度大、接近正确时梯度自然变小不会像MSE那样训练后期收敛缓慢。评估指标用accuracy在五分类均衡数据集上能直观反映整体正确率。但要注意如果类别不平衡比较严重单看accuracy容易被多数类带偏。到时候可以加precision、recall或者干脆画混淆矩阵逐类观察。4.2 训练循环epoch、batch与回调机制训练过程的参数设置我分成两个维度来理解一个是资源维度比如epoch数、batch大小决定训练多久另一个是控制维度比如早停、学习率衰减决定训练多稳。epoch数不能拍脑袋。我在实践中先跑30个epoch观察训练曲线形态再决定要不要加。如果验证损失在第10个epoch之后就开始回升说明再过拟合阶段后面的epoch纯属浪费如果30个epoch结束时验证损失还在下降就继续加。这个“观察-判断-调整”的循环比重设一个很大的epoch数再干等高效得多。训练循环里最容易忽略的是回调机制。我每次训练必挂三个回调from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint callbacks [ EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience2, min_lr1e-6), ModelCheckpoint(best_weather_model.keras, monitorval_loss, save_best_onlyTrue) ]EarlyStopping的作用是监控验证损失连续5个epoch没有明显改善就提前终止训练并且自动恢复到验证集表现最好的那一轮权重。这对小数据集训练尤其重要——你很可能在第20轮遇到过拟合如果硬生生训完40轮最终拿到的反而是糟糕的尾期模型。restore_best_weightsTrue这个参数很多人不写结果保存的还是最后一轮的权重早停等于白早停我踩过一次坑印象很深。ReduceLROnPlateau做自适应学习率衰减。训练后期loss曲线经常进入平台期此时把学习率减半常能突破瓶颈继续下降。patience2意味着验证损失连续两轮不降就衰减一次这个频率对天气分类比较合适。ModelCheckpoint保存验证集表现最好的模型文件。这里要特别注意保存的文件名后缀要用.keras格式这是新版TensorFlow的推荐格式旧版的.h5也能用但Keras官方已经将新格式作为默认标准。模型文件建议单独放一个目录免得后来保存混淆。训练命令本身很简单history model.fit( train_ds, validation_dataval_ds, epochs30, callbackscallbacks )4.3 训练曲线的判读方法训练结束之后我从来不看终端里最后一行输出就宣告胜利。历史记录history.history里保存了每个epoch的训练/验证准确率和损失用一行代码就能画出四条曲线这是判断模型状态的关键依据。如何区分正常训练与过拟合训练损失持续下降而验证损失在第10轮左右触底反弹这是典型的过拟合信号。此时模型已经在“背”训练集的细节特征比如某张特定照片的水印、某类图片的固定色调而不是在学习泛化的天气特征。应对方案按优先级排列第一验证集上效果最好的模型已经被ModelCheckpoint保存直接用它第二增加数据增强强度逼模型学不变特征第三调大Dropout比例或减少模型容量。如何识别欠拟合训练损失和验证损失都居高不下、两条曲线贴得很近这是模型容量不够或者训练不充分。小学里欠拟合的对策恰恰和过拟合相反加深加宽网络、去掉一些Dropout、调大学习率。如果训练损失下降极慢看看是不是输入没有归一化干净——我就遇到过Rescaling层和Map函数里的归一化重复执行了两次负负得正像素值还是0到255模型训了半天损失纹丝不动。4.4 准确率上不去的常见瓶颈我接触过不少初学者在天气识别上准确率卡在70%原因通常不是模型而是数据本身。天气图像有一些特性会影响分类难度阴天和雾天的视觉分界很模糊、雨天的动态特征在静态图片上不明显、晴天和日出的色调相近。这些类别之间的相似性决定了纯视觉分类的准确率天花板再好的模型也难以完全克服。我建议把这个问题拆开看。第一检查验证集里有哪些具体图片被分错了整理成错分类图像集肉眼观察错误模式。如果“雾天”图片大量被识别成“多云”可能不是模型问题而是这两类在数据上本来就混在一起。第二用混淆矩阵看每个类别的精确率和召回率找出模型偏爱的类别。第三计算每个类别的样本量如果某一类图片特别少模型会天然偏向样本多的类别。这里给一个提升小技巧做类别加权。在训练时为样本少的类别分配更高的loss权重强制模型多关注这些少数类。class_weight参数在model.fit里可以直接传非常方便。class_weight { 0: 1.0, 1: 1.2, 2: 1.0, 3: 1.5, 4: 1.0 }5. 常见问题与排查技巧实录5.1 训练卡死与显存不足实际训练中最常见的硬性问题有两个一个是训练到某个epoch突然卡住不动另一个是直接报显存不足。这两个问题我都遇到过处理思路完全不同。卡死问题优先怀疑数据管道。image_dataset_from_directory加载的dataset本质上是惰性求值的每次取一个batch都执行一次文件读取和预处理。如果数据集中混入了损坏图片、某些格式TensorFlow解码不了的图片训练到那个batch时就会卡住或报错。排查方法是独立写一个循环把数据集的所有batch过一遍确认都能正常取到for images, labels in train_ds.take(3): print(images.shape, labels.shape)如果这里报错把报错信息里的文件路径拿出来检查。有一个更隐蔽的情况cache()函数用得好能加速数据读取但如果磁盘空间不足cache到一半也会卡住。我的经验是数据集不超过几千张图片时先用cache()预取进内存速度提升明显。显存不足检查batch_size和图片尺寸。32的batch、180×180的尺寸在大多数消费级显卡上都能跑如果仍然爆显存把batch_size降到16或者把图片缩到128×128。还有一个冷门排查点训练过程中如果同时开了多个TensorBoard、模型可视化工具可能在显存里留下了没释放的上下文关了重启进程往往就好了。5.2 加载数据报错与标签不匹配数据加载报错是高频问题常见的包括“No such file or directory”、“Found 0 images belonging to 2 classes”这类信息。“Found 0 images”的意思是某个子文件夹里没有图片。我遇到过一次有人把代码里的目录路径写错了train写成了trainsshell里不报错但屏幕上显示找到了0张图。这种错一般出现在路径拼接上Debug时第一件事是打印出image_dataset_from_directory的返回值中class_names属性确认它扫描出了哪几个类别、每个类别多少样本。标签不匹配的问题更隐蔽image_dataset_from_directory按文件夹名称的字母序自动生成标签索引。文件夹顺序是cloudy、foggy、rainy、shine、sunrise那索引0就是cloudy。如果你在其他地方手动构建标签时用了不同的顺序测试集评估结果会错位成乱序。解决方式是在加载数据集后第一时间打印class_names核对顺序并把这个顺序固定下来后续所有标签操作都参考它不要凭记忆写死。5.3 测试集评估与模型导出训练结束后需要用测试集做最终评估。测试集全程没有参与训练和验证是模型真正没见过的新数据所以它的准确率才是有说服力的数字。我常用的评估代码非常简单test_loss, test_acc model.evaluate(test_ds) print(f测试集准确率: {test_acc:.4f})做这一步之前确认你没有在评估数据上去掉数据增强层——模型结构里包含增强层时推理阶段Keras会自动跳过增强操作这个不需要手动处理。评估后我还会做一次单张图片的推理演示确认为导入的图片格式、尺寸、归一化方式与训练时一致import numpy as np from tensorflow.keras.preprocessing import load_img, img_to_array img load_img(sample_test.jpg, target_size(180, 180)) img_array img_to_array(img) / 255.0 img_array np.expand_dims(img_array, axis0) pred model.predict(img_array) class_idx np.argmax(pred[0]) print(f预测类别: {class_idx}, 概率分布: {pred[0]})np.expand_dims这一步经常被漏掉。模型训练时输入是四维张量(batch, height, width, channels)单张预测时你手动构造的数组是三维的不补一个批维度模型就会直接报维度错误。这个报错信息很常见看到的时候第一反应就应该是补维度。模型导出可以用model.save(weather_model.keras)保存完整模型包含结构和权重。如果还想在移动端用转成TensorFlow Lite版本也是一行代码的事。作为学习项目先把model.keras存好后续复现时直接load_model就能恢复一个可用于推理的模型。5.4 数据泄露与评估结果虚高这个坑比较高级但它一旦踩中会让你的测试集评估彻底失去意义。数据泄露的意思是训练数据里混入了测试集或验证集的图片导致模型“见过”答案评测分数虚高。小建议准备数据时三个人为划分文件的脚本要保留好并且确保在划分时刻原始全量数据集被随机打乱过。有一种容易中招的情况网络上某个数据集本身就有重复图片不同的子文件夹里可能放着同一张图片的不同压缩版本。你去重时不能只看文件名而应该对图片内容做感知哈希匹配把重复内容全部清理掉。我在天气识别项目上吃过一次亏测试集准确率96%单看结果漂亮得不像话结果仔细核对发现测试集里有一批图片和平时的训练图片高度相似基本就是同一批照片的不同尺寸缩放。清理重复之后测试准确率掉到86%这个数字才和验证集的表现对得上。所以永远记住一句话结果好得反常时先找数据问题再调模型。5.5 一个快速实验的替代方案如果你现在机器资源紧张或者只是想快速验证整个流程能不能走通建议先把图片尺寸降到96×96epoch数压缩到10不跑数据增强用一个极简版CNN跑通全流程。这个实验的意义不在于追求准确率而是把数据加载、模型构建、训练、评估这条链路完整验证一次。链路通了再逐步加上增强、调大尺寸、加深网络。这个方法我从带人入门时反复强调因为新手最容易犯的错误就是在完整流程还没跑通时就开始追求性能结果被一个低级错误卡住好几个小时。先用最快的速度把整套代码跑通再回头优化每一个环节学习效率会高很多。根据我跑天气识别项目下来的体会整个流程里最关键的不是模型结构多么花哨而是对每个环节的掌控力知道数据是什么状态、模型在学什么、曲线在告诉你什么、指标到底意味着什么。能把这套思维建立起来天气识别这个项目才算真正学透了。