CycleGAN and pix2pix in PyTorch 是一个经典的图像到图像翻译(Image-to-Image Translation) 工具箱。它的核心功能是,将一张图片转换为具有另一种风格或特征的新图片,同时保留其主体内容。
以支持Python 3.11和PyTorch 2.4版本。该版本还支持DDP用于单机多GPU训练。(请使用torchrun --nproc_per_node=4 train.py ...)
两大核心功能:
-
pix2pix:配对图像的翻译
它适用于输入和输出图像是"成对"出现的场景,即需要精确对应的"翻译"。
-
工作原理 :模型学习从输入到输出的像素级映射 。例如,输入一张边缘线条图 ,模型会学习生成对应的彩色猫咪 ;或者输入建筑草图 ,生成真实建筑照片。
-
适用场景 :当你有大量一一对应的图像对时,效果最好,比如"线稿上色"、"卫星图转地图"、"白天转黑夜"等。
-
-
CycleGAN:非配对图像的翻译
它适用于输入和输出图像"不成对" 的场景。
-
工作原理 :CycleGAN的核心创新在于"循环一致性(Cycle-Consistency) "。它通过两个生成器(A→B 和 B→A)形成循环,确保一张图被转换到另一领域后,还能被转换回来,从而在没有一一对应样本的情况下也能学习风格迁移。
-
适用场景 :当你只有两类不同风格 的图像集合,但不存在一一对应关系时。最经典的例子就是"马 ↔ 斑马 "和"苹果 ↔ 橘子"的风格转换。
-


CycleGan详细的原理介绍:
完整原理:两大"阵营"的博弈
CycleGAN的完整原理可以理解为**两个"翻译官"(生成器)和两个"鉴宝专家"(判别器)**之间的博弈,外加一个"闭环复查机制"(循环一致性)。
1. 两个生成器(翻译官)
G:负责把"X域"的图翻译成"Y域"(马➡️斑马)。
F:负责把"Y域"的图翻译成"X域"(斑马➡️马)。
2. 两个判别器(鉴宝专家)
D_Y:只看Y域的图,判断G生成的"假斑马"是否足够逼真,能不能骗过自己。
D_X:只看X域的图,判断F生成的"假马"是否足够逼真。
3. 三大损失函数(训练的指导方针)
为了让模型学得好,CycleGAN的损失函数由三部分组成:
对抗损失(Adversarial Loss) :让生成器努力骗过判别器,保证生成的图像风格足够像目标域(比如条纹像斑马)。
循环一致性损失(Cycle Loss) :保证转换后的图像,经过逆转换后还能变回来,保证内容结构足够像原图(比如姿势像原来的马)。
身份损失(Identity Loss,可选) :把一张马图输入给生成器F(它本该生成马,但输入已经是马了),让它尽量保持原样。这有助于保留背景色调,防止模型把不该变的东西(如绿草地)也改成斑马纹。
通俗理解CycleGan原理:
前向过程:生成器G拿到一张"真马"照片。
风格转换:G将其转换成一张带有斑马纹的"假斑马"照片。
判别反馈:判别器D_Y比较"假斑马"和"真斑马"数据集,如果觉得太假,就惩罚G,迫使G下次画得更像。
循环复查(关键):把这张"假斑马"交给反向生成器F,F必须把它变回一张"重构的马"。
计算循环损失:对比"重构的马"和最初的"真马"。如果两者差异太大(比如马头变了方向),就狠狠惩罚G和F,逼它们下次转换时务必保留马的姿态。
通过这种"既要风格像(对抗损失),又要变回去一模一样(循环损失)"的强约束,CycleGAN最终学会了在保留原图底层结构的前提下,进行精准的风格纹理迁移。
-
在线Demo :CycleGAN PyTorch Colab
此外我把DCGan的原理也说明一下,帮助学习:
先回顾一下代码:
def train_step(real_images):
"""单步训练:更新生成器和判别器"""
batch_size = real_images.size(0) #real_images 的形状是 (64, 3, 32, 32),表示有 64 张 3 通道 32×32 的图像
# 真实标签和假标签(用于损失计算)
real_labels = torch.ones(batch_size, 1, device=device) #真实标签全1
fake_labels = torch.zeros(batch_size, 1, device=device)#假标签全0
# --------------------- 训练判别器 ---------------------
# 生成随机噪声
noise = torch.randn(batch_size, noise_dim, device=device)#从标准正态分布中采样随机噪声 noise
fake_images = generator(noise)#生成器 generator 将噪声映射为假图像 fake_images(与真实图像形状相同)
# 判别器对真实图片和假图片的输出
real_output = discriminator(real_images)
fake_output = discriminator(fake_images.detach()) # detach 防止梯度传到生成器#若没有 detach(),梯度的流向会同时更新生成器
# 计算先判别器的总损失 = 真实样本损失 + 假样本损失。
d_loss_real = criterion(real_output, real_labels)# 判别器对真实图像的预测与真实标签(全1)的损失,希望真实图像输出接近1
d_loss_fake = criterion(fake_output, fake_labels)# 判别器对假图像的预测与假标签(全0)的损失,希望假图像输出接近0
d_loss = d_loss_real + d_loss_fake # 总判别器损失为两者之和。这样判别器学会同时正确分类真实图像和假图像
# 反向传播和优化
d_optimizer.zero_grad()
d_loss.backward()
d_optimizer.step()
# --------------------- 训练生成器 ---------------------
# 生成器的目标是欺骗判别器,使判别器将生成的假图像判断为真实
# 重新使用之前生成的 fake_images,但此时要计算生成器梯度
fake_output = discriminator(fake_images)
g_loss = criterion(fake_output, real_labels) # 希望判别器将假图片判断为真
# 大的空间会延迟平衡的修补,这样增加了周期的时间。所以空间不断增加,周期也不断增大,其中的因果也不断的增加。
# 空间、因果、平衡什么时候会达到临界,这决定了整个宇宙的结局。
# 简单的东西周期短时间也短也深。从简单到复杂的东西周期长也深入。正态分布图的宇宙意义非常独特!
g_optimizer.zero_grad()
g_loss.backward()
g_optimizer.step()
return g_loss.item(), d_loss.item() # 返回去生成器的损失和判别器的损失
流程记录:
完整的训练流程(按步骤拆解)
在实际的代码训练循环中,每一轮迭代 都会严格按照以下 3步 进行。我把它叫做"左右互搏":
第 1 步:训练判别器(D)------ 让它变"聪明"
准备真图 :从数据集中取一张真实图片(比如一张真实的人脸),输入判别器。我们希望判别器输出 1(真)。计算损失 LrealLreal。
准备假图 :生成器吃进 100 维噪声,产出一张假图片,输入判别器。我们希望判别器输出 0(假)。计算损失 LfakeLfake。
更新参数 :总判别器损失 LD=Lreal+LfakeLD=Lreal+Lfake 。正如你所理解的,两者相加,然后用这个总损失去反向传播更新判别器(D) 的权重。这一步结束后,D 变得更强了。
第 2 步:训练生成器(G)------ 让它变"狡猾"
制造假图:生成器再吃进一组新的 100 维噪声,产出一张假图片。
关键操作 :把这张假图片再次输入判别器。但此时,我们"欺骗"判别器,把这张假图的标签强行设置为 1(真)。
计算损失:因为判别器认为它是假的,输出可能只有 0.2,但我们的目标(Label)是 1,所以会产生一个巨大的损失 LGLG。
更新参数 :只更新生成器(G) 的权重,冻结判别器(D)(不更新 D)。生成器为了减小这个损失,会拼命调整自己的参数,让自己下次生成得更逼真。
第 3 步:重复循环
下一次迭代时,判别器因为第 1 步变强了,又能识破第 2 步生成的假图,于是生成器又在第 2 步变强......如此反复。
非常重要:
DCgan网络输出的是无法控制的随机图片,这些图片都与训练样本随机保持一致。如果想要控制训练输出的图片,有一下几种方式。
为什么DCgan不能直接指定输出的图片?(根本原因)
DCGAN的输入层仅仅是一个连续的随机向量 zz(比如100维的噪声)。这个向量里面没有"标签"或"类别"信息的位置。模型在学习时,只是把图像空间映射到这个随机向量空间,而没有学习"向量"和"标签"之间的对应关系,所以你看不懂这100个数字分别代表什么,自然也无法通过修改它们来指定生成内容。
那么如何实现DC干能够控制输出图片的呢?
1. 直接升级架构:条件DCGAN(Conditional DCGAN,即CGAN)
这是最标准、最直接的解决方案。简单来说,就是给模型加个"遥控器"。
做法 :把你想指定的条件(比如"猫"的标签
0,"狗"的标签1)也作为输入的一部分,同时喂给生成器和判别器。效果 :训练完成后,你只需要输入
(噪声z + 标签"猫"),它就必定生成猫;输入(噪声z + 标签"蓝眼睛"),它就生成蓝眼睛的猫(前提是训练数据里有标注)。注意 :这已经不再是标准的DCGAN了,而是它的变体(通常称为AC-GAN或cGAN)。
2. 训练后探索:潜在空间"向量算术"(无需改模型)
如果你手里已经有一个训练好的标准DCGAN模型,不想重新训练,可以尝试"找方向"。
原理 :研究者发现,在DCGAN的潜在空间(那100维的噪声向量)里,特定的语义特征(如"微笑"、"戴眼镜"、"金发")往往对应着特定的向量移动方向。
操作:你先随机生成1000张图,手动挑出"微笑"和"不微笑"的图片,算出它们对应噪声向量的平均值差,就得到了"微笑方向向量"。下次你想生成微笑的人,就在随机噪声上加上这个方向向量。
缺点:这个操作很繁琐,而且只能处理模型已经"见过"的特征,无法生成数据集中没有的类别。
3. 后期筛选(最"笨"但也最简单的方法)
如果你只是偶尔玩玩,不想改代码,那么:
做法 :固定一个随机种子,比如
z = torch.randn(1, 100),多运行几次生成器,每次生成64张图,从里面"手动挑选"你看着顺眼的那张。本质:这不叫"指定",这叫"盲抽"。很多早期的GAN演示Demo用的就是这种方法。
输入(噪声z + 标签"猫")是如何实现的?
你提到的输入"噪声z + 标签",在代码中叫 拼接(Concatenate)。
生成器的输入维度 = 噪声维度 + 标签维度 = 100 + 10 = 110 维。
具体步骤如下:
生成一个100维的随机噪声 z (例如:
[0.23, -0.56, ..., 0.89])生成一个10维的One-Hot标签 y (假设要生成数字"5",则为
[0,0,0,0,0,1,0,0,0,0])在维度上拼接 :把这两个向量首尾相连,变成一个 110维 的新向量。
[来自z的100个数字] + [来自y的10个数字]输入生成器:这个110维的向量就是生成器的输入,经过转置卷积,最终输出一张"数字5"的图片。
如果是"蓝眼睛"这种多属性呢?
如果你想把"蓝眼睛"作为条件,通常情况会更复杂一点。因为"蓝眼睛"只是众多属性中的一个,你的标签向量往往包含多个属性(比如:性别、发色、眼镜等)。
假设你总共要控制 5个属性(例如:性别、是否微笑、发色、年龄、是否戴眼镜),并且每个属性都用One-Hot编码。
性别(男/女):2维
是否微笑(是/否):2维
发色(黑/金/棕/红):4维
年龄(年轻/中年/老年):3维
是否戴眼镜(是/否):2维
那么,标签的总维度 = 2 + 2 + 4 + 3 + 2 = 13维。
这个时候,生成器的输入维度 = 噪声100维 + 标签13维 = 113维。你输入拼接好的113维向量,模型就会依据这13维的具体数值来生成对应的图片。
两个重要的实操提示(帮你避坑)
拼接后的维度必须匹配模型第一层 :如果你的生成器第一层是一个
Linear(全连接)层,定义时必须写成self.fc = nn.Linear(110, 256),而不是原来的self.fc = nn.Linear(100, 256)。除了One-Hot,还有嵌入(Embedding) :当类别数特别大(比如1000种鸟类)时,One-Hot维度太高,浪费算力。这时可以用
nn.Embedding把标签压缩成低维向量(比如把10维压缩成2维),然后再和噪声拼接。这种情况下,标签维度就是你可以自己设定的超参数 (比如设为 2维)。
总结与建议
| 你的需求 | 建议方案 |
|---|---|
| 必须精准控制类别(指定猫/狗/人) | 放弃原版DCGAN,直接学习 条件GAN(CGAN) 或 AC-GAN。代码改动不大,只是在输入层把标签和噪声拼接在一起。 |
| 想控制细微属性(微笑/年龄/眼镜) | 建议直接学习 StyleGAN 系列,它的潜在空间解耦性极好,天生就适合做属性编辑。 |
| 只是好奇,想试试水 | 跑通DCGAN后,固定随机种子(np.random.seed(666)),观察生成结果,感受一下"随机性"。 |
