如何解决CycleGAN生成图像中出现的‘空洞’问题?
CycleGAN训练出现图像空洞问题的排查与解决
问题背景
从零开始基于PyTorch搭建CycleGAN模型,采用伯克利vangogh2photo数据集训练,第3个epoch左右生成图像出现空洞现象。
模型架构代码
import torch import torch.nn as nn import torch.nn.functional as F from torch.autograd import Variable import random '''This is a memory storage that stores 50 previously created images. This is in accordance with the paper that introduced CycleGAN, Unpaired Image to Image translation.''' class ReplayBuffer: def __init__(self, max_size=50): assert max_size > 0, "Empty buffer." self.max_size = max_size self.data = [] def push_and_pop(self, data): to_return = [] for element in data.data: element = torch.unsqueeze(element, 0) if len(self.data) < self.max_size: self.data.append(element) to_return.append(element) else: # Returns newly added image with a probability of 0.5. if random.uniform(0, 1) > 0.5: i = random.randint(0, self.max_size - 1) to_return.append(self.data[i].clone()) self.data[i] = element # replaces the older image with the newly generated image. else: # Otherwise, it sends an older generated image and to_return.append(element) return Variable(torch.cat(to_return)) '''Linear learning rate scheduler.''' class LambdaLR: def __init__(self, n_epochs, offset, decay_start_epoch): if (n_epochs - decay_start_epoch) < 0: raise Exception("Decay should start before training ends. Change decay_start_epoch to a value less than {}.".format(n_epochs)) self.n_epochs = n_epochs self.offset = offset self.decay_start_epoch = decay_start_epoch def step(self, epoch): return 1.0 - max(0, epoch + self.offset - self.decay_start_epoch) / (self.n_epochs - self.decay_start_epoch) '''Single Residual Block. InstanceNorm2d produces blob artefacts. Consider changing it to modulated convolutions later. Currently using augmentation and a low number of epochs to stop Generator from producing artefacts.''' class ResNetBlock(nn.Module): def __init__(self, channels): super(ResNetBlock, self).__init__() self.conv_block = nn.Sequential( nn.ReflectionPad2d(1), nn.Conv2d(channels, channels, kernel_size=3, padding=0, bias=True), nn.InstanceNorm2d(channels), nn.ReLU(inplace=True), nn.ReflectionPad2d(1), nn.Conv2d(channels, channels, kernel_size=3, padding=0, bias=True), nn.InstanceNorm2d(channels) ) def forward(self, x): return x + self.conv_block(x) class GeneratorResNet(nn.Module): def __init__(self, input_channels, output_channels, num_resnet_blocks=9): super(GeneratorResNet, self).__init__() # Initial convolutional layer self.initial_conv = nn.Sequential( nn.ReflectionPad2d(3), nn.Conv2d(input_channels, 64, kernel_size=7, padding=0, bias=True), nn.InstanceNorm2d(64), nn.ReLU(inplace=True) ) # Downsampling layers self.downsampling_1 = nn.Sequential( nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1, bias=True), nn.InstanceNorm2d(128), nn.ReLU(inplace=True) ) self.downsampling_2 = nn.Sequential( nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1, bias=True), nn.InstanceNorm2d(256), nn.ReLU(inplace=True) ) # Residual layers self.residual_layers = nn.Sequential( *[ResNetBlock(256) for _ in range(num_resnet_blocks)] ) # Upsampling layers self.upsampling_1 = nn.Sequential( nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, output_padding=1, bias=True), nn.InstanceNorm2d(128), nn.ReLU(inplace=True) ) self.upsampling_2 = nn.Sequential( nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1, bias=True), nn.InstanceNorm2d(64), nn.ReLU(inplace=True) ) # Final convolutional layer(已修正原代码中的粘贴错误) self.final_conv = nn.Sequential( nn.ReflectionPad2d(3), nn.Conv2d(64, output_channels, kernel_size=7, padding=0, bias=True), nn.Tanh() ) def forward(self, x): # Apply initial convolutional layer x = self.initial_conv(x) # Apply downsampling layers x = self.downsampling_1(x) x = self.downsampling_2(x) # Apply residual layers x = self.residual_layers(x) # Apply upsampling layers x = self.upsampling_1(x) x = self.upsampling_2(x) # Apply final convolutional layer x = self.final_conv(x) return x '''PatchGAN Discriminator''' class Discriminator(nn.Module): def __init__(self, input_shape): super(Discriminator, self).__init__() channels, height, width = input_shape # Calculate output shape of image discriminator (PatchGAN) self.output_shape = (1, height // 2 ** 4, width // 2 ** 4) def discriminator_block(in_channels, out_channels, normalize=True): """Returns downsampling layers of each discriminator block""" layers = [nn.Conv2d(in_channels, out_channels, kernel_size=4, stride=2, padding=1)] if normalize: layers.append(nn.InstanceNorm2d(out_channels)) layers.append(nn.LeakyReLU(0.2, inplace=True)) return layers # C64 -> C128 -> C256 -> C512 self.model = nn.Sequential( *discriminator_block(channels, out_channels=64, normalize=False), *discriminator_block(64, out_channels=128), *discriminator_block(128, out_channels=256), *discriminator_block(256, out_channels=512), nn.ZeroPad2d((1, 0, 1, 0)), nn.Conv2d(in_channels=512, out_channels=1, kernel_size=4, padding=1) ) def forward(self, img): return self.model(img)
训练超参数
{'name': 'CycleGan_VanGogh_Checkpoint', 'n_epochs': 20, 'batch_size': 4, 'lr': 0.0002, 'decay_start_epoch': 19, 'b1': 0.5, 'b2': 0.999, 'img_size': 256, 'channels': 3, 'num_residual_blocks': 9, 'lambda_cyc': 10.0, 'lambda_id': 5.0}
生成图像空洞示例

问题成因分析
- 生成器上采样层的固有缺陷:ConvTranspose2d容易产生棋盘格/空洞伪影,即使尺寸参数理论匹配,训练初期的梯度波动也会放大这种伪影。
- 超参数设置不合理:
- 学习率衰减启动过晚(第19个epoch才开始,总epoch仅20),前19个epoch全用满额学习率,导致训练初期梯度不稳定,输出异常。
- 身份损失权重
lambda_id=5.0过高,过度限制生成器的风格转换能力,导致细节生成失败。
- 模型代码存在错误:原生成器的
final_conv模块混入了无关的图片链接,导致网络结构不完整,直接引发输出异常。 - 训练初期缺乏图像增强:CycleGAN前几个epoch本身输出不稳定,没有增强操作会加重伪影问题。
解决方法
1. 修复模型代码错误
修正生成器final_conv中的粘贴错误,恢复完整的层结构(已在上方代码中修正)。
2. 替换上采样层,避免ConvTranspose2d伪影
改用双线性插值上采样+卷积的组合,彻底解决棋盘格/空洞问题:
# 修改生成器的上采样层 self.upsampling_1 = nn.Sequential( nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True), nn.Conv2d(256, 128, kernel_size=3, padding=1, bias=True), nn.InstanceNorm2d(128), nn.ReLU(inplace=True) ) self.upsampling_2 = nn.Sequential( nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True), nn.Conv2d(128, 64, kernel_size=3, padding=1, bias=True), nn.InstanceNorm2d(64), nn.ReLU(inplace=True) )
3. 调整训练超参数
- 提前启动学习率衰减:将
decay_start_epoch改为10,让训练中期开始缓慢降低学习率,稳定训练过程。 - 降低身份损失权重:将
lambda_id调整为2.0,平衡风格转换与身份保留的权重,给生成器足够创作空间。
4. 添加图像增强操作
在数据加载阶段加入随机裁剪、水平翻转等增强,提升模型泛化能力,缓解训练初期的不稳定:
from torchvision import transforms transform_ = transforms.Compose([ transforms.Resize(int(256*1.12), transforms.InterpolationMode.BILINEAR), transforms.RandomCrop(256), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ])
5. 可选:调整归一化层
如果InstanceNorm导致的伪影持续存在,可以尝试将部分InstanceNorm替换为LayerNorm,或在生成器输出前添加微小高斯噪声,缓解训练初期波动。
内容的提问来源于stack exchange,提问作者dorito
相关产品推荐
相关产品推荐

