简介这份资源是CycleGAN的Keras实现代码包面向具备一定深度学习基础、希望动手实践无监督图像到图像转换的开发者与研究者。CycleGAN通过循环一致性损失在无需成对样本的条件下完成跨域风格迁移可用于马与斑马、苹果与橘子、莫奈画风与照片等场景的转换实验。压缩包共7个文件以4个Python脚本和3个数据压缩包为主脚本分别承担模型构建、数据加载、预测推理与ResNet生成器实现等职责数据包则提供多组领域图像集整体约477.41MB。目前已有548人学习下载。借助这份实现读者可以完整梳理生成器、判别器、对抗损失与循环一致性损失的代码组织方式理解训练中两个生成器与两个判别器交替优化的流程并基于自带数据集快速复现图像转换效果为后续风格迁移、季节转换等应用提供可复用的工程参考。1. 拆开一个 CycleGAN 的 Keras 实现它到底能干什么手里有一批风格 A 的图又有一批风格 B 的图但两张图之间没有任何配对关系——这是很多图像处理项目里最让人头疼的场景。传统监督学习要求成对样本可现实中你很难拿到同一场景的“马”和“斑马”逐像素对齐的两张照片。CycleGAN 解决的正是这个问题它用两个生成器和两个判别器在无配对数据的前提下完成域迁移。这次拆的这个 Keras 实现把整套训练流程、损失函数、生成器结构都写成了可直接跑的 Python 脚本适合想入门 GAN 但不想一上来就啃原版 Torch 代码的开发者。读完你能搞清楚它怎么跑、数据怎么放、模型怎么改以及训练时那些玄学崩溃到底出在哪。2. 项目结构与技术选型为什么用 Keras 搭 CycleGAN2.1 目录布局与文件职责拿到源码包后先别急着python train.py。花两分钟把目录扫一遍能省掉后面半小时的报错排查。这个项目的结构是典型的“单脚本训练 模块化模型”风格常见做法是把生成器、判别器、损失、数据加载拆成独立文件主训练脚本只负责调度。cyclegan-keras/ ├── data/ │ ├── trainA/ # 域 A 训练图 │ ├── trainB/ # 域 B 训练图 │ └── testA/ # 推理用输入图 ├── models/ │ ├── generator.py # 生成器 G_A2B / G_B2A │ ├── discriminator.py # 判别器 D_A / D_B │ └── losses.py # 对抗损失 循环一致性损失 ├── utils/ │ ├── data_loader.py # 图像读取、归一化、增强 │ └── visualizer.py # 训练过程拼图保存 ├── config.py # 超参数集中管理 ├── train.py # 训练入口 └── inference.py # 单图/批量推理config.py是整个项目的控制面板图像尺寸、batch size、学习率、lambda 权重都在这里。我一般会先打开这个文件确认IMG_HEIGHT和IMG_WIDTH因为后面所有模型输入都依赖它。models/下三个文件各司其职losses.py里通常会把最小二乘 GAN 损失和循环一致性损失分开写方便单独调权重。2.2 生成器与判别器的网络设计CycleGAN 的生成器不是简单的编码器-解码器而是带残差块的结构。编码阶段用步长卷积降采样中间堆 6 到 9 个残差块解码阶段用转置卷积或上采样恢复分辨率。这个 Keras 实现里生成器通常长这样# models/generator.py from tensorflow.keras import layers, Model def build_generator(input_shape(256, 256, 3), n_res_blocks6): inputs layers.Input(shapeinput_shape) # 编码器两次步长卷积每次下采样 2 倍 x layers.Conv2D(64, 7, strides1, paddingsame)(inputs) x layers.InstanceNormalization()(x) if hasattr(layers, InstanceNormalization) else x x layers.Activation(relu)(x) x layers.Conv2D(128, 3, strides2, paddingsame)(x) x layers.BatchNormalization()(x) x layers.Activation(relu)(x) x layers.Conv2D(256, 3, strides2, paddingsame)(x) x layers.BatchNormalization()(x) x layers.Activation(relu)(x) # 残差块堆叠每个块包含两次卷积 跳跃连接 for _ in range(n_res_blocks): res layers.Conv2D(256, 3, paddingsame)(x) res layers.BatchNormalization()(res) res layers.Activation(relu)(res) res layers.Conv2D(256, 3, paddingsame)(res) res layers.BatchNormalization()(res) x layers.Add()([x, res]) # 残差相加 # 解码器两次上采样恢复尺寸 x layers.Conv2DTranspose(128, 3, strides2, paddingsame)(x) x layers.BatchNormalization()(x) x layers.Activation(relu)(x) x layers.Conv2DTranspose(64, 3, strides2, paddingsame)(x) x layers.BatchNormalization()(x) x layers.Activation(relu)(x) outputs layers.Conv2D(3, 7, paddingsame, activationtanh)(x) return Model(inputs, outputs, namegenerator)这段代码里几个参数值得注意。n_res_blocks默认 6对应 256×256 输入如果降到 128×128可以减到 3 个残差块否则显存吃紧。最后一层用tanh把输出压到 [-1, 1]和输入归一化方式必须匹配——如果数据加载时把像素映射到 [0, 1]这里就要换成sigmoid否则生成图会整体偏色。判别器用的是 PatchGAN输出不是单个真假值而是一个特征图每个点判断对应感受野的真假这对纹理细节的保留更友好。2.3 损失函数对抗损失与循环一致性的组合CycleGAN 能成立的核心在于两个约束同时作用。对抗损失让生成图看起来像目标域循环一致性损失保证 A→B→A 能回到原图。代码里通常这样组织# models/losses.py import tensorflow as tf def adversarial_loss(y_true, y_pred, use_lsganTrue): 最小二乘 GAN 损失比原始交叉熵更稳定 if use_lsgan: return 0.5 * tf.reduce_mean((y_pred - y_true) ** 2) return tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits(logitsy_pred, labelsy_true) ) def cycle_consistency_loss(real_A, cycled_A, real_B, cycled_B, lambda_cycle10.0): L1 距离衡量循环重建误差lambda 控制权重 loss_A tf.reduce_mean(tf.abs(real_A - cycled_A)) loss_B tf.reduce_mean(tf.abs(real_B - cycled_B)) return lambda_cycle * (loss_A loss_B)lambda_cycle默认 10这是原论文给的推荐值。调小会让风格迁移更激进但内容容易崩调大则生成图偏保守、风格化不明显。use_lsgan开关控制用最小二乘还是交叉熵实践中 LSGAN 训练初期更稳但最终效果差异不大。训练时总损失是四部分相加两个对抗损失加一个循环一致性损失有时还会加 identity loss 来约束颜色分布。3. 跑通训练数据准备、启动命令与显存控制3.1 数据目录规范与预处理数据放错位置是新手最常见的翻车点。这个项目要求data/trainA和data/trainB各放一个域的图文件名不需要对应数量也不必相等。图像格式支持 jpg、png但建议统一混用有时会让读取器报解码错误。# utils/data_loader.py import tensorflow as tf def load_image(path, height256, width256): img tf.io.read_file(path) img tf.image.decode_jpeg(img, channels3) img tf.image.resize(img, [height, width]) img (tf.cast(img, tf.float32) / 127.5) - 1.0 # 归一化到 [-1, 1] return img def build_dataset(folder, batch_size1, shuffleTrue): paths tf.data.Dataset.list_files(folder /*.jpg) ds paths.map(lambda x: load_image(x), num_parallel_callstf.data.AUTOTUNE) if shuffle: ds ds.shuffle(buffer_size200) return ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)归一化到 [-1, 1] 和生成器最后一层tanh是绑定的改一处必须改另一处。batch_size默认 1因为 CycleGAN 一次要过四张图real_A、real_B、fake_B、fake_A显存占用是普通 GAN 的两倍。如果显卡只有 6GB建议把图像裁到 128×128 再训。3.2 训练脚本启动与关键参数确认数据就位后训练入口通常是这样python train.py \ --data_dir ./data \ --epochs 200 \ --batch_size 1 \ --img_size 256 \ --lambda_cycle 10.0 \ --lr 0.0002 \ --output_dir ./checkpointslr设 0.0002 是 GAN 训练的常见起点配合 Adam 优化器的 beta10.5。epochs给 200 是因为 CycleGAN 收敛慢前 50 轮基本看不出效果100 轮后风格才开始稳定。训练过程中visualizer.py会每隔几个 step 保存一张拼图包含 real_A、fake_B、cycled_A 三列方便肉眼判断有没有崩。注意如果启动后 loss 直接飙到几千先检查数据归一化范围是否和生成器输出激活匹配这是最高频的翻车原因。3.3 显存不够时的降级策略显存不足报 OOM 时按优先级依次尝试把img_size从 256 降到 128把生成器残差块从 6 降到 3把判别器最后一层的通道数减半开启混合精度训练。混合精度在 Keras 里一行就能开from tensorflow.keras import mixed_precision mixed_precision.set_global_policy(mixed_float16)但要注意开了混合精度后损失计算里的reduce_mean最好显式转回 float32否则梯度可能下溢。这个项目里losses.py如果没做转换需要自己补一个tf.cast。4. 推理与效果验证加载权重、单图转换与批量测试4.1 加载检查点做单图推理训练完之后inference.py负责把权重加载回来做转换。核心逻辑是只用一个生成器比如 A→B 就用G_A2B# inference.py from models.generator import build_generator import tensorflow as tf import numpy as np def translate(image_path, weight_path, directionA2B): g build_generator() g.load_weights(weight_path) img tf.io.read_file(image_path) img tf.image.decode_jpeg(img, channels3) img tf.image.resize(img, [256, 256]) img (tf.cast(img, tf.float32) / 127.5) - 1.0 img tf.expand_dims(img, axis0) # 加 batch 维度 fake g(img, trainingFalse) fake (fake[0] 1.0) * 127.5 # 反归一化回 [0, 255] return tf.cast(fake, tf.uint8).numpy()trainingFalse必须显式传否则 BatchNormalization 层会用当前 batch 的统计量而不是训练时累积的滑动平均单张推理时结果会完全不对。这是很多人加载权重后觉得“效果和训练时不一样”的真正原因。4.2 判断训练是否成功的三个信号不能只看 loss 曲线GAN 的 loss 没有绝对参考价值。我一般看三个东西第一fake_B是否保留了real_A的结构轮廓如果内容完全变了说明循环一致性权重太低第二cycled_A和real_A的差异是否在逐步缩小第三判别器输出是否稳定在一个区间如果 D 的 loss 长期接近 0说明判别器太强生成器学不动需要降低 D 的学习率或加噪声。4.3 常见失败模式与对应调整生成图出现规律性网格伪影通常是转置卷积的棋盘效应把Conv2DTranspose换成UpSampling2D Conv2D能缓解。生成图整体偏灰或偏色检查归一化和激活是否匹配以及 identity loss 是否缺失。训练到一半突然崩成噪声多半是学习率太大或 batch 里出现了损坏图像加梯度裁剪和图像校验能挡掉大部分。5. 避坑与排查训练 CycleGAN 时最容易踩的五个坑现象一loss 从第一步就是 NaN。原因通常是数据里有全黑或全白图归一化后出现极端值或者学习率设成了 0.001 以上。解决办法是加一个图像有效性检查过滤掉像素方差过低的图同时把学习率降到 0.0002。现象二生成图内容和输入完全无关。循环一致性损失的权重被设得太小或者cycled_A的计算图接错了——比如把G_B2A(G_A2B(real_A))写成了G_A2B(G_B2A(real_A))。检查train.py里循环损失的输入顺序确保 A→B→A 和 B→A→B 两条链路各自独立。现象三训练几十轮后判别器 loss 恒为 0。判别器过强生成器梯度消失。把判别器的学习率乘以 0.5或者给判别器的输入加高斯噪声也可以把对抗损失从交叉熵换成最小二乘。现象四推理结果和训练时保存的预览图差距很大。除了前面说的trainingFalse还要确认推理时的图像尺寸和训练时一致。训练用 256 推理用 512生成器里的残差块数量对不上加载权重会直接报形状错误。现象五多卡训练时 loss 震荡剧烈。Keras 的MirroredStrategy下如果 batch size 没有按卡数放大每张卡上的有效 batch 太小BN 统计量不稳定。把总 batch size 设为卡数乘以 2 以上或者把 BN 换成 Instance Normalization。6. 二次开发换数据集、改网络、加新损失的实操思路想把这个项目用到自己的场景最直接的改造是换数据集。假设你要做“白天转夜晚”把data/trainA放白天图data/trainB放夜晚图然后重新跑训练就行。但有几个细节决定成败两个域的图像数量尽量接近比例超过 5:1 时少的那边会被过拟合图像内容最好有重叠的语义类别比如都是街景否则生成器学不到有意义的映射。改网络结构时我一般从生成器入手。如果目标域纹理复杂把残差块从 6 加到 9如果追求推理速度把编码器第一层通道从 64 降到 32。判别器可以换成多尺度 PatchGAN在 1/2 和 1/4 分辨率上各接一个判别头对小目标细节更敏感。改完网络后先用一个极小的数据集跑 10 个 epoch确认前向传播和损失计算不报错再上全量数据。加新损失是提升效果最有效的手段。除了标准的对抗损失和循环损失可以加 identity loss 来约束颜色让G_A2B(real_B)尽量等于real_B这样生成器不会随意改变目标域本身的色调。代码上就是在总损失里加一项# 在 train_step 里追加 identity 约束 same_B generator_A2B(real_B, trainingTrue) loss_identity_B lambda_identity * tf.reduce_mean(tf.abs(real_B - same_B))lambda_identity一般设 5 左右太大生成图会缺乏风格变化。另一个实用技巧是加感知损失用预训练 VGG 提取特征算 L1 距离能让生成图的结构更合理但会拖慢训练速度约 30%。验证改造是否有效别只看最终生成图。我习惯在训练中途固定几张测试图每隔 10 个 epoch 跑一次推理把结果按时间顺序拼成一张大图。这样能直观看到风格是逐步迁移还是中途崩掉。另外保存权重时同时存下优化器状态否则中断后恢复训练学习率调度会从头开始前几轮容易震荡。从那以后我每次改完损失函数或网络结构都强制先用 20 张图跑一个 5 轮的冒烟测试确认 loss 曲线没有异常跳变再开全量训练。这个习惯帮我省下了至少几十小时的无效等待。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?