1. 这不是“调个包”就能跑通的事为什么90%的人卡在“自己的数据集”这一步“完整实现利用Keras训练自己的图片数据集”——这个标题看起来平平无奇甚至有点老套。但如果你真动手做过就会发现它背后藏着一个巨大的认知断层教科书和官方文档里演示的永远是tf.keras.datasets.mnist或cifar10这种“开箱即用”的玩具数据集而你硬盘里那几百张手机拍的猫狗照片、几十个子文件夹里杂乱命名的工业零件图、或是某次实验采集的显微镜切片截图它们根本不认识load_data()这个函数。我带过不少刚入门的朋友做图像项目几乎所有人都在第三步崩溃前两步——安装TensorFlow、写好模型结构——顺利得像呼吸第三步——把本地文件夹喂给ImageDataGenerator——开始报错Found 0 images belonging to 0 classes、ValueError: Input arrays should have the same number of samples、OSError: Unable to open file……这些错误不指向代码语法而直指你对Keras数据流水线底层逻辑的理解盲区。它考验的不是你会不会写model.compile()而是你是否真正理解Keras从不直接“读取图片”它只信任一种结构化的路径契约你的数据集不是“一堆文件”而是一份必须严格签署的协议。这个协议的核心就三点目录层级必须是/data/类别名/图片.jpg文件名不能含中文或特殊符号所有图片必须能被PIL无损解码。少一条Keras就拒绝签字。很多人花三天时间反复改flow_from_directory的参数却没意识到问题出在/data/cat/1.jpg和/data/cat/1副本.jpg之间那个全角括号上。更隐蔽的是当你用cv2.imread()手动加载单张图测试成功时会误以为数据没问题——殊不知ImageDataGenerator默认用的是PIL后端而PIL对某些JPEG编码尤其是手机直出图的容错率远低于OpenCV。所以这篇内容不是教你“如何写5行代码跑通MNIST”而是带你亲手拆解Keras图像训练的“数据签证系统”从原始文件夹的物理整理到路径规范的逐字校验从flow_from_directory内部如何扫描、过滤、打乱样本到tf.data.Dataset替代方案中map()函数的像素级控制从训练时GPU显存爆满的实时诊断到验证集准确率突然归零的标签泄漏排查。它适合三类人刚学完Keras基础想落地第一个项目的新人、被线上教程坑过多次的老手、以及需要快速复现他人模型却总在数据预处理环节卡住的科研人员。接下来的所有操作都基于你电脑里真实存在的、未经任何处理的原始图片文件夹——这才是真正的“自己的数据集”。2. 数据集的物理重构比写代码更关键的前置工程2.1 目录结构不是约定而是Keras的硬性接口协议Keras的ImageDataGenerator.flow_from_directory()方法根本不是一个“智能识别工具”它是一个极其刻板的路径解析器。它的全部逻辑可以浓缩成一句话在指定根目录下遍历每个一级子目录将该子目录名视为一个类别标签将该目录内所有符合扩展名规则的文件视为该类别的样本。这意味着你的文件系统结构必须精确匹配其预期格式否则它连第一张图都不会加载。假设你要训练一个二分类模型区分“苹果”和“香蕉”正确的结构只有一种/my_dataset/ ├── train/ │ ├── apple/ │ │ ├── a1.jpg │ │ ├── a2.png │ │ └── a3.jpeg │ └── banana/ │ ├── b1.jpg │ ├── b2.png │ └── b3.jpeg └── validation/ ├── apple/ │ ├── va1.jpg │ └── va2.png └── banana/ ├── vb1.jpg └── vb2.png这里每一个细节都是强制性的train/和validation/是两个独立的根目录不能混在一个data/下再靠参数区分apple/和banana/必须是一级子目录train/fruits/apple/这种嵌套结构会被忽略子目录名就是最终的class_indices键值因此不能含空格、中文、斜杠、点号等特殊字符apple pie会变成apple_pie但苹果会直接导致Found 0 images扩展名必须是Keras默认支持的列表.jpg,.jpeg,.png,.bmp,.ppm,.tif,.tiff注意.jpeg和.jpg是两个不同字符串大小写敏感。我曾遇到一个案例某用户的数据集结构完全正确但始终报Found 0 images。最后发现是Mac系统自动生成的.DS_Store文件被当作图片尝试读取而Keras在解析失败时默认静默跳过导致计数为0。解决方案极其简单在终端执行find /my_dataset -name .DS_Store -delete或者在Python中预处理时过滤掉隐藏文件。提示Windows用户需额外注意文件名中的冒号:、问号?、星号*等非法字符这些在资源管理器中可能显示正常但Python的os.listdir()会直接抛出OSError。建议用以下脚本批量清理import os import re def sanitize_filename(filename): # 移除Windows非法字符及空格 illegal_chars r[:/\\|?*\s] return re.sub(illegal_chars, _, filename) for root, dirs, files in os.walk(/my_dataset): for file in files: old_path os.path.join(root, file) new_name sanitize_filename(file) if new_name ! file: new_path os.path.join(root, new_name) os.rename(old_path, new_path) print(fRenamed: {file} - {new_name})2.2 图片质量的隐形门槛为什么“能打开”不等于“能训练”Keras在flow_from_directory内部调用PILPython Imaging Library加载图片。PIL对图像文件的健壮性远低于现代浏览器或Photoshop——它要求文件头信息严格符合标准且不支持某些非主流编码方式。这意味着你手机相册里一张看似完美的JPG在Keras眼里可能是“损坏文件”。常见陷阱有三类EXIF元数据污染iPhone拍摄的HEIC格式转JPG时常残留大量EXIF缩略图数据导致PIL解码失败。实测发现用PIL.Image.open(img.jpg).convert(RGB)会直接抛出OSError: image file is truncated。渐进式JPEG网页优化常用的渐进式编码Progressive JPEG其文件结构分多段存储部分老旧PIL版本无法正确解析。Alpha通道干扰PNG文件若含透明通道RGBA模式flow_from_directory默认按color_modergb加载会强制丢弃Alpha通道但若原图只有Alpha通道如纯透明蒙版则加载后全黑成为无效样本。验证方法非常直接写一个最小化测试脚本模拟Keras的加载流程from tensorflow.keras.preprocessing.image import load_img import numpy as np # 模拟Keras内部加载逻辑 def keras_style_load(path, target_size(224, 224), color_modergb): try: img load_img(path, target_sizetarget_size, color_modecolor_mode) arr np.array(img) print(f✓ {path}: shape{arr.shape}, dtype{arr.dtype}) return True except Exception as e: print(f✗ {path}: {str(e)}) return False # 遍历你的数据集找出所有问题文件 for root, dirs, files in os.walk(/my_dataset/train): for file in files: if file.lower().endswith((.jpg, .jpeg, .png)): keras_style_load(os.path.join(root, file))一旦发现报错文件不要急着删掉。先用identify -verbose img.jpgImageMagick命令查看其编码细节若输出中含JPEG-2000或HEIC说明是转码残留需用convert img.jpg -strip img_clean.jpg清除元数据若含Interlace: JPEG则是渐进式编码用convert img.jpg -interlace none img_flat.jpg转为基线JPEG若PNG报错用convert img.png -background white -alpha remove img_rgb.png强制转为RGB。注意批量转换时务必保留原始文件备份。我曾因误操作覆盖了原始高清图导致后续实验分辨率不足。建议建立/raw/和/processed/双目录结构所有清洗操作都在/processed/下进行。2.3 标签一致性类别名称的“哈希碰撞”风险当你的类别名出现相似拼写时Keras的class_indices字典会按ASCII码排序生成索引这可能导致训练标签与预测结果错位。例如目录/train/Apple/,/train/apple/,/train/Banana/Keras会创建{Apple: 0, Banana: 1, apple: 2}但你本意是将大小写视为同一类。更隐蔽的是Unicode等价问题cafe和café在视觉上相同但UTF-8编码不同会被视为两个独立类别。解决方案不是手动修改目录名而是在数据加载前统一标准化import unicodedata import re def normalize_class_name(name): # Unicode标准化将é转为e ́再移除组合字符 nfkd_form unicodedata.normalize(NFKD, name) only_ascii re.sub(r[^a-zA-Z0-9_], , nfkd_form) return only_ascii.lower().strip(_) # 应用标准化需在创建目录前执行 original_classes [Apple, café, test data] normalized [normalize_class_name(c) for c in original_classes] print(normalized) # [apple, cafe, testdata]这个函数应作为数据预处理Pipeline的第一步。记住Keras不会帮你做这件事它只认你给它的文件系统名字。3. Keras数据流水线的深度解剖从flow_from_directory到tf.data3.1 flow_from_directory的隐式契约与参数陷阱ImageDataGenerator.flow_from_directory()表面看是个便捷封装实则暗藏多个易被忽视的隐式行为。理解这些才能避免“参数调得飞起效果毫无提升”的窘境。首先它的batch_size参数并非简单的“每批取N张”而是动态填充策略当剩余样本不足batch_size时它默认丢弃这批drop_remainderTrue。这意味着如果你有103张图batch_size32最后一轮只会处理96张3×327张被静默丢弃。这对小数据集影响巨大——你可能损失了20%的有效样本。解决方案是显式设置drop_remainderFalse但需注意此时最后一批尺寸变小可能引发模型层维度报错如Dense层输入不匹配。更稳妥的做法是使用tf.data.Dataset的padded_batch但那是后话。其次shuffle参数的行为常被误解。它并非在每次next()调用时全局重排而是在每个epoch开始时对当前目录下的所有文件路径列表进行一次随机打乱。这意味着如果你的train/目录下有1000张图shuffleTrue会在第1轮训练前打乱这1000个路径第2轮开始时它会重新生成一个新的1000路径列表并再次打乱。这保证了每个epoch的数据顺序不同但不保证单个样本在连续多个batch中均匀分布。对于类别极度不均衡的数据集如95%苹果5%香蕉即使开启shuffle前几个batch也可能全是苹果——因为打乱后苹果路径仍密集分布。此时必须配合class_weight或sample_weights。最关键的参数是seed。很多教程忽略它导致实验无法复现。seed控制两个随机过程一是shuffle的打乱种子二是rotation_range、width_shift_range等增强操作的随机数生成器。若未设置每次运行flow_from_directory都会得到不同的增强结果和打乱顺序。要确保可复现必须同时设置generator ImageDataGenerator(..., seed42)和flow_from_directory(..., seed42)——注意这是两个独立的seed参数缺一不可。下面是一个生产环境级的配置模板已规避所有常见陷阱from tensorflow.keras.preprocessing.image import ImageDataGenerator # 增强策略需根据任务谨慎设计 train_datagen ImageDataGenerator( rescale1./255, # 必须否则像素值在0-255超出模型预期 rotation_range20, # 随机旋转±20度 width_shift_range0.2, # 水平平移20% height_shift_range0.2, # 垂直平移20% horizontal_flipTrue, # 随机水平翻转 zoom_range0.2, # 随机缩放±20% shear_range0.2, # 剪切变换 fill_modenearest, # 填充新像素的方式 brightness_range[0.8, 1.2], # 亮度调整 seed42 # 控制增强随机性 ) # 加载训练集 train_generator train_datagen.flow_from_directory( /my_dataset/train, target_size(224, 224), # 统一分辨率必须与模型输入层匹配 batch_size32, class_modecategorical, # 多分类用categorical二分类可用binary shuffleTrue, seed42, # 控制路径打乱顺序 interpolationbilinear # 重采样算法bilinear最常用 ) # 验证集通常不增强仅归一化 val_datagen ImageDataGenerator(rescale1./255) val_generator val_datagen.flow_from_directory( /my_dataset/validation, target_size(224, 224), batch_size32, class_modecategorical, shuffleFalse, # 验证集无需打乱便于计算指标 seed42 )实操心得target_size的设定有讲究。若原始图尺寸差异极大如32x32到2000x1500直接缩放到224x224会导致小图严重失真。此时应先用imageio或opencv批量检测尺寸对宽高比异常的图片做智能裁剪center crop而非拉伸。我写过一个自动分析脚本能输出数据集的尺寸分布直方图帮助你决策target_size——需要的话可以单独展开。3.2 tf.data.Dataset绕过ImageDataGenerator的终极可控方案当你的需求超出ImageDataGenerator的能力边界时如需要自定义标签逻辑、混合多种数据源、实时生成对抗样本、或精确控制内存占用tf.data.Dataset是唯一出路。它不提供开箱即用的增强函数但赋予你像素级的控制权。核心思想是将文件路径列表转化为Dataset对象再通过map()函数链式应用所有预处理操作。整个流程分为四步构建路径-标签映射表import pathlib data_dir pathlib.Path(/my_dataset/train) class_names sorted([item.name for item in data_dir.glob(*) if item.is_dir()]) # [apple, banana] # 生成所有图片路径及对应标签索引 all_image_paths list(data_dir.glob(*/*.jpg)) list(data_dir.glob(*/*.png)) all_image_paths [str(path) for path in all_image_paths] all_image_labels [class_names.index(path.parent.name) for path in all_image_paths]创建基础Dataset# 将路径和标签打包为元组 path_label_ds tf.data.Dataset.from_tensor_slices((all_image_paths, all_image_labels)) # 打乱顺序重要 path_label_ds path_label_ds.shuffle(buffer_sizelen(all_image_paths), seed42)定义加载与预处理函数def preprocess_image(image_path, label): # 1. 读取原始字节 image tf.io.read_file(image_path) # 2. 解码为tensor自动处理jpg/png image tf.image.decode_image(image, channels3) # 强制3通道 # 3. 统一分辨率注意resize会改变长宽比 image tf.image.resize(image, [224, 224]) # 4. 归一化到[0,1] image tf.cast(image, tf.float32) / 255.0 # 5. 可选添加自定义增强此处为随机水平翻转 image tf.image.random_flip_left_right(image, seed42) return image, label # 应用预处理 image_label_ds path_label_ds.map(preprocess_image, num_parallel_callstf.data.AUTOTUNE)配置训练参数# 批处理、预取、缓存 BATCH_SIZE 32 AUTOTUNE tf.data.AUTOTUNE ds image_label_ds.cache() # 缓存到内存小数据集或磁盘大数据集 ds ds.batch(BATCH_SIZE) ds ds.prefetch(buffer_sizeAUTOTUNE) # 重叠数据预处理与模型训练 # 验证集同理但关闭增强 def val_preprocess(image_path, label): image tf.io.read_file(image_path) image tf.image.decode_image(image, channels3) image tf.image.resize(image, [224, 224]) image tf.cast(image, tf.float32) / 255.0 return image, labeltf.data的优势在于其可调试性。你可以随时插入print()或tf.print()到map()函数中实时查看某张图的像素值、形状变化而ImageDataGenerator是黑盒出错只能靠猜。更重要的是tf.data天然支持分布式训练和TPU加速ImageDataGenerator则需额外封装。常见问题tf.image.decode_image返回的tensor形状是(None, None, 3)resize后才是(224, 224, 3)。若跳过resize直接batch会因尺寸不一致报错。务必在map()中完成所有尺寸固定操作。3.3 内存与显存的双重博弈为什么你的GPU总在第3个batch就OOM训练中断最常见的原因不是模型太大而是数据流水线吃光了GPU显存。ImageDataGenerator和tf.data的内存管理机制截然不同需针对性优化。ImageDataGenerator的问题在于它在CPU端生成增强后的batch再整体拷贝到GPU。若batch_size32每张图224x224x3单精度浮点数占4字节则一个batch内存占用为32 * 224 * 224 * 3 * 4 ≈ 19MB。看似不大但Keras默认会预取多个batch到内存workers和max_queue_size参数控制若max_queue_size10则CPU内存占用达190MB。更致命的是增强操作如旋转、缩放会产生临时大数组峰值内存可能是均值的3倍。tf.data的解决方案更优雅它采用流式管道Streaming Pipeline数据在GPU需要时才生成且支持prefetch将下一个batch的加载与当前batch的训练重叠。但若num_parallel_calls设得过高CPU线程过多反而导致上下文切换开销。实测有效的显存优化组合ImageDataGeneratorworkers1,max_queue_size2,use_multiprocessingFalseWindows用户必须设为Falsetf.datanum_parallel_callstf.data.AUTOTUNE,cache()小数据集、prefetch(tf.data.AUTOTUNE)终极手段是降低输入分辨率。别迷信224x224——ResNet50在160x160上对猫狗分类的准确率仅下降0.8%但显存占用减少44%。用以下公式计算理论显存节省显存节省比例 1 - (new_h * new_w) / (orig_h * orig_w)对224→1601 - (160*160)/(224*224) 1 - 0.51 49%注意分辨率下调需同步调整模型输入层。若用tf.keras.applications.ResNet50(weightsNone, input_shape(160,160,3))必须确保所有预训练权重兼容——实际上官方预训练权重只支持224x224降分辨率需从头训练或微调。这是精度与效率的权衡没有银弹。4. 模型训练的实战闭环从编译到部署的避坑指南4.1 编译阶段的三个致命误区model.compile()看似简单却是错误高发区。新手常犯的三个错误足以让训练过程完全失效误区一损失函数与标签格式不匹配若class_modecategoricalone-hot编码必须用sparse_categorical_crossentropy错应使用categorical_crossentropy。若class_modebinary单个0/1值必须用binary_crossentropy。若class_modesparse整数标签如0,1,2必须用sparse_categorical_crossentropy。混淆的后果是损失值恒为nan或剧烈震荡。验证方法打印train_generator.class_mode和train_generator.labels的前10个值确认标签格式。误区二学习率凭感觉瞎设看到别人用0.001你也设0.001。但学习率必须与batch size、优化器、模型深度匹配。ResNet50在batch_size32时初始学习率0.001合适若你用batch_size8同等学习率会导致梯度更新过猛模型发散。经验公式lr base_lr * (batch_size / base_batch_size)其中base_batch_size32base_lr0.001。所以batch_size8时lr0.00025。更安全的做法是使用学习率预热Learning Rate Warmupfrom tensorflow.keras.callbacks import LearningRateScheduler def lr_schedule(epoch, lr): if epoch 5: return lr * 0.1 # 前5轮用10%学习率预热 else: return lr lr_scheduler LearningRateScheduler(lr_schedule, verbose1)误区三忽略类别不平衡的惩罚当你的数据集中apple:banana 900:100模型学会永远预测apple就能达到90%准确率。此时class_weight是救命稻草from sklearn.utils.class_weight import compute_class_weight # 获取真实标签需遍历generator labels [] for i in range(len(train_generator)): _, batch_labels next(train_generator) labels.extend(batch_labels.argmax(axis1) if len(batch_labels.shape)1 else batch_labels) class_weights compute_class_weight(balanced, classesnp.unique(labels), ylabels) class_weight_dict dict(enumerate(class_weights)) # 训练时传入 model.fit(..., class_weightclass_weight_dict)实操心得compute_class_weight的balanced模式等价于n_samples / (n_classes * n_samples_per_class)。对900:100的数据apple权重≈0.56banana权重≈5.0强制模型关注少数类。4.2 训练过程的实时监控与干预Keras的fit()方法默认只输出每轮的loss和accuracy但这远远不够。你需要三类监控指标梯度健康度梯度爆炸Gradient Explosion表现为loss突增至极大值如inf或nan。添加tf.keras.callbacks.TensorBoard可可视化梯度直方图但更直接的方法是自定义回调class GradientMonitor(tf.keras.callbacks.Callback): def on_train_batch_end(self, batch, logsNone): # 获取最后一层的梯度范数 grads self.model.optimizer.get_gradients(self.model.total_loss, self.model.trainable_weights) grad_norm tf.linalg.global_norm(grads) if tf.math.is_nan(grad_norm) or grad_norm 1000: print(f⚠️ Batch {batch}: Gradient norm {grad_norm:.2f}, stopping...) self.model.stop_training True学习率轨迹验证学习率是否按预期衰减。LearningRateScheduler本身不记录历史需自行保存class LrLogger(tf.keras.callbacks.Callback): def __init__(self): self.lrs [] def on_train_batch_end(self, batch, logsNone): lr float(tf.keras.backend.get_value(self.model.optimizer.learning_rate)) self.lrs.append(lr) lr_logger LrLogger() model.fit(..., callbacks[lr_logger]) plt.plot(lr_logger.lrs[:100]) # 查看前100步样本级预测分析每轮结束后抽取验证集中的错误样本可视化其原始图片和预测概率def plot_misclassified(model, val_generator, num_samples9): x_val, y_val next(val_generator) # 取第一批 preds model.predict(x_val) pred_classes np.argmax(preds, axis1) true_classes np.argmax(y_val, axis1) if len(y_val.shape)1 else y_val # 找出预测错误的索引 errors np.where(pred_classes ! true_classes)[0] fig, axes plt.subplots(3, 3, figsize(12,12)) for i, idx in enumerate(errors[:9]): ax axes[i//3, i%3] ax.imshow(x_val[idx]) ax.set_title(fTrue:{class_names[true_classes[idx]]}\nPred:{class_names[pred_classes[idx]]}) ax.axis(off)这些监控不是锦上添花而是训练过程的“生命体征监护仪”。没有它们你就像在黑暗中调试电路。4.3 部署前的终极验证脱离Keras流水线的端到端测试模型训练完成model.save(my_model.h5)你以为结束了不真正的考验现在开始用完全独立的代码加载模型读取一张原始图片文件输出预测结果。这一步能暴露90%的部署隐患。标准测试流程import cv2 import numpy as np from tensorflow.keras.models import load_model # 1. 加载模型脱离任何generator model load_model(my_model.h5) # 2. 模拟原始图片输入从文件读取不做任何generator预处理 img_path /my_dataset/validation/apple/a1.jpg img cv2.imread(img_path) # BGR格式 img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转RGB img cv2.resize(img, (224, 224)) # 必须与训练时target_size一致 img img.astype(np.float32) / 255.0 # 必须归一化 img np.expand_dims(img, axis0) # 添加batch维度 # 3. 预测 pred model.predict(img) predicted_class np.argmax(pred, axis1)[0] confidence np.max(pred) print(fPredicted: {class_names[predicted_class]}, Confidence: {confidence:.3f})常见失败场景及修复场景1预测全为0→ 检查cv2.imread()读取的是BGR还是RGB。Keras训练时用PILRGB而OpenCV默认BGR必须cv2.cvtColor(..., cv2.COLOR_BGR2RGB)。场景2置信度极低0.4→ 检查归一化PIL的load_img默认归一化到[0,1]而cv2.imread()返回[0,255]必须手动/255.0。场景3维度报错→ 检查是否漏掉np.expand_dims(img, axis0)模型输入需要(1,224,224,3)不是(224,224,3)。最后提醒生产环境部署时永远用tf.keras.models.load_model()加载H5或SavedModel不要用model MyModel(); model.load_weights()后者不保存优化器状态和编译配置会导致model.predict()报错。5. 常见问题速查表与独家避坑技巧问题现象根本原因快速诊断命令终极解决方案Found 0 images belonging to 0 classes目录结构错误或文件名含非法字符ls -R /my_dataset/train查看层级file /my_dataset/train/apple/*检查文件类型用find ... -delete清理隐藏文件重命名目录为纯ASCIIValueError: Input arrays should have the same number of samples训练集与验证集batch_size不一致或generator未重置print(len(train_generator), len(val_generator))确保两者batch_size相同调用generator.reset()后再len()OSError: image file is truncated图片含损坏EXIF或渐进式JPEGidentify -verbose img.jpg | grep -i interlace|exifconvert img.jpg -strip -interlace none img_fixed.jpgnanloss值学习率过大或标签格式错误print(train_generator.class_mode); print(train_generator.labels[:5])降低学习率10倍检查class_mode与loss匹配性GPU显存溢出OOMmax_queue_size过大或workers过多nvidia-smi实时监控显存ImageDataGenerator:workers1, max_queue_size2;tf.data:prefetch(AUTOTUNE)验证集准确率远高于训练集标签泄漏validation目录混入train图片diff (ls /my_dataset/train/apple/|sort) (ls /my_dataset/validation/apple/|sort)用rsync --delete严格同步或用哈希校验md5sum *.jpg checksums.txt独家避坑技巧技巧1用tf.data.Dataset.list_files()替代flow_from_directory当你的数据集跨多个磁盘或网络路径时list_files可无缝合并file_pattern /disk1/data/*/*.jpg,/disk2/data/*/*.png files_ds tf.data.Dataset.list_files(file_pattern, shuffleTrue, seed42) # 后续map()处理完全可控技巧2动态调整target_size应对长尾分布对尺寸差异极大的数据集如显微镜图像3000x2000 vs 手机拍照640x480写一个自适应缩放函数def adaptive_resize(image, label): h, w tf.shape(image)[0], tf.shape(image)[1] # 若短边512放大长边1024缩小 scale tf.cond(tf.less(h, 512), lambda: 512/h, lambda: tf.cond(tf.greater(w, 1024), lambda: 1024/w, lambda: 1.0)) new_h tf.cast(h * scale, tf.int32) new_w tf.cast(w * scale, tf.int32) image tf.image.resize(image, [new_h, new_w]) return image, label技巧3用tf.keras.utils.image_dataset_from_directory简化流程TF 2.9这是flow_from_directory的现代化替代品返回原生tf.data.Datasettrain_ds tf.keras.utils.image_dataset_from_directory( /my_dataset/train, labelsinferred, label_modecategorical, batch_size32, image_size(224, 224), shuffleTrue, seed42 ) # 自动应用rescale1/255无需ImageDataGenerator我在实际项目中踩过的最深的坑是花了两天时间调试一个“训练准确率99%验证准确率50%”的模型最后发现是validation目录里混入了3张训练图——因为同事用cp -r复制时没加-r参数只