Keras ResNet图像价格回归预测label_mode配置及报错排查
问题核心原因
你当前做的是图像回归预测任务(输出连续的价格值),之前的报错和损失为nan是几个配置错误叠加导致的:
- 误用了分类场景的损失函数(交叉熵类损失仅适用于离散类别预测,完全不匹配连续值回归需求)
image_dataset_from_directory的label_mode参数选择错误,int/categorical/binary都是为分类任务设计的,会对标签做类别编码转换,无法保留浮点型价格的原始数值- 未对价格标签做归一化,数值跨度过大容易引发梯度爆炸导致损失为nan
- 输出层激活函数选择错误,且未对输入图像做ResNet对应的预处理
- 额外加了多余的Flatten层,且优化器、学习率选择不合适
分步修复方案
1. 修正数据集加载逻辑
image_dataset_from_directory没有专门的回归标签模式,传入自定义连续值标签时需要将label_mode设为None,此模式下函数不会对标签做分类编码转换,会保留传入的原始数值格式。
需要特别注意:该函数默认会按文件名字典序遍历文件夹内的图像,你提取标签时的遍历顺序必须和函数内部顺序完全一致,否则会出现图像和标签错配的问题。最稳妥的实现方式是先获取排序后的文件列表,再按该顺序提取标签:
import os import numpy as np import tensorflow as tf img_height, img_width = 160, 160 batch_size = 32 data_dir = "你的图像存储文件夹路径" # 获取和函数内部加载顺序完全一致的文件列表 valid_exts = ('.png', '.jpg', '.jpeg', '.bmp') file_list = sorted([ os.path.join(data_dir, f) for f in os.listdir(data_dir) if f.lower().endswith(valid_exts) ]) # 按文件列表顺序从文件名提取价格标签,替换为你自己的价格提取逻辑 labels = [float(os.path.basename(f).split('_')[0]) for f in file_list] # 加载数据集,label_mode设为None train_ds = tf.keras.utils.image_dataset_from_directory( data_dir, labels=labels, validation_split=0.1, subset="training", seed=123, label_mode=None, image_size=(img_height, img_width), batch_size=batch_size ) val_ds = tf.keras.utils.image_dataset_from_directory( data_dir, labels=labels, validation_split=0.1, subset="validation", seed=123, label_mode=None, image_size=(img_height, img_width), batch_size=batch_size )
2. 对价格标签做归一化
价格数值如果跨度较大(比如从几元到几千元),直接用原始值训练很容易因为数值过大引发梯度爆炸,导致损失为nan。需要先把所有标签缩放到0~1区间,预测完成后再还原为真实价格:
labels_arr = np.array(labels) label_min = labels_arr.min() label_max = labels_arr.max() eps = 1e-8 # 避免除零错误 # 归一化:原始价格 -> 0~1区间 def normalize_label(y): return (y - label_min) / (label_max - label_min + eps) # 反归一化:模型输出 -> 真实价格 def denormalize_label(y_norm): return y_norm * (label_max - label_min + eps) + label_min # 将归一化操作映射到数据集 train_ds = train_ds.map(lambda x, y: (x, normalize_label(y))) val_ds = val_ds.map(lambda x, y: (x, normalize_label(y)))
3. 修正模型结构与训练配置
- ResNet预训练权重是在经过特定预处理的ImageNet数据集上训练的,直接喂原始0~255的像素值会导致特征分布不匹配,需要调用配套的预处理函数
- 你之前在池化层后加Flatten是多余的:设置
pooling="avg"后,ResNet的输出已经是一维特征向量,不需要再展平 - 回归任务的输出层不需要加激活函数,用线性输出即可;之前用
relu会截断所有负值输出,容易导致梯度传递异常 - 回归任务不要用
accuracy作为评估指标(该指标仅适用于分类任务),换成MAE(平均绝对误差)更能直观反映预测价格和真实价格的偏差 - 初始训练用小学习率的Adam优化器比SGD更稳定,不容易出现梯度爆炸
修正后的代码如下:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense from tensorflow.keras.applications import ResNet50 from tensorflow.keras.applications.resnet50 import preprocess_input # 给数据集加ResNet预处理、开启预取优化加速训练 train_ds = train_ds.map(lambda x, y: (preprocess_input(x), y)).prefetch(tf.data.AUTOTUNE) val_ds = val_ds.map(lambda x, y: (preprocess_input(x), y)).prefetch(tf.data.AUTOTUNE) resnet_model = Sequential() pretrained_model = ResNet50( include_top=False, input_shape=(160, 160, 3), pooling="avg", weights="imagenet" ) # 初始阶段冻结预训练层,先训头部全连接层 for layer in pretrained_model.layers: layer.trainable = False resnet_model.add(pretrained_model) resnet_model.add(Dense(512, activation="relu")) resnet_model.add(Dense(1)) # 线性输出,无激活函数 # 编译:回归任务用MSE做损失 resnet_model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-4), loss="mse", metrics=["mae"] )
4. 训练注意事项
- 训练前先检查提取的标签有没有脏数据:比如非数值、空值、负价格的异常值,这类脏数据也会导致损失为nan
- 等头部全连接层训练到验证集损失稳定下降后,再尝试解冻ResNet最后几层的参数,用1e-5级别的更小学习率微调,不要一开始就全量训练,否则会破坏预训练权重导致不收敛
- 模型推理得到的归一化预测值,必须用之前定义的
denormalize_label函数还原,才能得到真实的价格结果
内容的提问来源于stack exchange,提问作者zueri95
相关产品推荐
相关产品推荐

