资讯动态

pix2pix图像翻译实战:U-Net生成器与PatchGAN判别器解析

发布时间:2026/10/18 12:26:18 来源:尧图企业网站定制
1. 项目概述pix2pix是什么解决什么问题1.1 pix2pix的适用范围与核心应用场景先说个场景。你手上有一张黑白素描想让它变成彩色照片或者有一张白天的卫星图想生成对应的夜晚地图又或者有一张语义分割图想还原成真实街景。这类输入和输出之间像素级别一一对应的任务就是pix2pix最擅长的领域。它的全称翻译过来是用条件GAN做图像到图像的翻译原论文发表于2017年前后作者团队来自某知名高校人工智能实验室论文标题用的是很直白的几个词Image-to-Image Translation with Conditional Adversarial Networks。当时最大的痛点在于传统方法处理这类任务要么需要针对每种任务单独设计复杂的损失函数要么需要手写一堆图像处理规则换一个场景就要重做一遍。而pix2pix用一个统一的框架把从A图生成B图这个问题变成了一个有监督的生成任务只要你有配对的数据集就能训练出效果不错的转换模型。用大白话说它像是给图像翻译问题做了一整套通用流水线。这意味着无论你想做边缘图到照片、灰度图上色、草稿图到成品图、姿态到人物图像还是把模糊图变清晰只要数据结构是成对的图都可以套用这套方案。在工程项目里生产环境用得最多的几个方向包括医疗图像中的CT到MRI风格转换、遥感图像中的地图风格迁移、工业质检里的缺陷样本增强以及电商场景的虚拟试衣和产品效果图生成。效果虽然不是所有任务都完美但胜在通用、稳定、迭代快作为团队内部工具落地性价比很高。1.2 为什么选择U-Net PatchGAN的组合很多第一次接触pix2pix的读者会问生成器为什么非要用U-Net判别器为什么要用PatchGAN这两个设计其实都是从大量实验中踩出来的。生成器用U-Net的核心原因是输入输出共享底层结构。在图像翻译任务里输入和输出共享很多低级特征比如边缘位置、轮廓走向、大体纹理区域。普通Encoder-Decoder结构的瓶颈层会把图像压缩到很小的特征图再解码放大回来这个过程中小尺寸的马赛克细节和边缘定位信息会大量丢失。U-Net通过跳跃连接skip connection把编码器每一层的特征直接拼接给解码器对应层相当于给解码器开了一条直达通道让它在重建细节时直接参照输入图的原始信息。这好比做菜时不是把所有食材先打成糊再做菜而是边做边保留一部分原食材最后成品更接近原貌。判别器用PatchGAN的原因是兼顾真实感与计算效率。普通GAN的判别器输出一个标量表示整张图真或假只能给出全局判断对于图像中的局部纹理是否真实失控。PatchGAN的思路是把输入图片切成若干个固定大小的小块比如每个patch是70x70像素对每个patch单独判断真假最后把所有patch的结果取平均。这样判别器被迫聚焦于局部纹理的细节一致性同时由于patch很小网络层的参数数量和计算量都大幅下降。更实际的好处是PatchGAN让生成器必须输出细节足够真实的图像否则在某个局部patch就会被判别器拆穿这就是它比普通判别器更适合图像生成任务的根本原因。2. 生成器架构U-Net的搭建设计2.1 U-Net的结构拆解编码器、解码器、跳跃连接U-Net名字源于网络结构形似字母U。左边是收缩路径编码器右边是扩张路径解码器。编码器负责逐步下采样每次分辨率减半、通道数翻倍提取越来越抽象的语义特征。解码器则逐步上采样恢复分辨率并输出最终图像。中间的跳跃连接把编码器第i层的输出直接拼接到解码器倒数第i层之前通道维上做concat让解码器同时拥有高层语义和底层细节两类信息。在pix2pix的原始设置里输入和输出图像都是256x256分辨率。生成器由多个卷积块组成每个编码块是Conv-BatchNorm-ReLU解码块是TransposedConv反卷积-BatchNorm-ReLU最后一层用Tanh把输出归一化到[-1,1]。一个值得注意的点是原论文里下采样次数是5次每层通道数按64, 128, 256, 512, 512递增。这个通道数配置不是随便定的64起步能在浅层保留足够的局部纹理随着层数加深逐步放大感受野到最深层时已经能看到整图范围的信息这种由细节到整体的层级特征堆叠是最适合图像翻译的分层结构。2.2 PyTorch实现U-Net代码逐段解析我直接给出一个可运行的U-Net生成器实现参数和原论文一致但做了两处工程优化一是用LeakyReLU替代部分ReLU来避免生成器的梯度稀疏问题二是所有卷积层默认开启biasFalse因为后面跟着BatchNormbias会被抵消掉留着只会增加参数总量和训练不稳定风险。import torch import torch.nn as nn class UnetBlock(nn.Module): def __init__(self, in_ch, out_ch, use_bnTrue): super().__init__() layers [nn.Conv2d(in_ch, out_ch, kernel_size4, stride2, padding1, biasFalse)] if use_bn: layers.append(nn.BatchNorm2d(out_ch)) layers.append(nn.LeakyReLU(0.2, inplaceTrue)) self.block nn.Sequential(*layers) def forward(self, x): return self.block(x) class UnetDecoderBlock(nn.Module): def __init__(self, in_ch, out_ch, dropoutFalse): super().__init__() layers [ nn.ConvTranspose2d(in_ch, out_ch, kernel_size4, stride2, padding1, biasFalse), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ] if dropout: layers.append(nn.Dropout(0.5)) self.block nn.Sequential(*layers) def forward(self, x): return self.block(x) class GeneratorUNet(nn.Module): def __init__(self, in_ch3, out_ch3): super().__init__() # 编码器 self.e1 UnetBlock(in_ch, 64, use_bnFalse) # 128x128 self.e2 UnetBlock(64, 128) # 64x64 self.e3 UnetBlock(128, 256) # 32x32 self.e4 UnetBlock(256, 512) # 16x16 self.e5 UnetBlock(512, 512) # 8x8 self.e6 UnetBlock(512, 512) # 4x4 self.e7 UnetBlock(512, 512) # 2x2 self.e8 UnetBlock(512, 512, use_bnFalse) # 1x1 # 解码器 self.d1 UnetDecoderBlock(512, 512, dropoutTrue) self.d2 UnetDecoderBlock(1024, 512, dropoutTrue) self.d3 UnetDecoderBlock(1024, 512, dropoutTrue) self.d4 UnetDecoderBlock(1024, 512) self.d5 UnetDecoderBlock(1024, 256) self.d6 UnetDecoderBlock(512, 128) self.d7 UnetDecoderBlock(256, 64) # 输出层 self.out_conv nn.ConvTranspose2d(128, out_ch, kernel_size4, stride2, padding1) def forward(self, x): skips [] # 编码 for enc in [self.e1, self.e2, self.e3, self.e4, self.e5, self.e6, self.e7, self.e8]: x enc(x) skips.append(x) skips skips[:-1] # 最深一层不参与跳跃连接 reversed_skips skips[::-1] # 解码 decoders [self.d1, self.d2, self.d3, self.d4, self.d5, self.d6, self.d7] x decoders[0](x) for i, dec in enumerate(decoders[1:], start1): x torch.cat([x, reversed_skips[i-1]], dim1) x dec(x) x self.out_conv(x) return torch.tanh(x)里面几个细节需要特别说明。第一个第一个编码块和最后一个编码块不接BatchNorm。原因是最浅层输入是原始像素分布已经固定接BN反而会把原始分布打乱最深层特征图只有1x1BN在单样本上统计均值方差没有意义还容易在batch较小时引入噪声。第二个解码器前三层加了Dropout。原论文提到Dropout能给生成器引入随机性在训练时帮助它探索更多可能的输出模式防止生成器过早收敛到安全但平庸的结果。第三个解码器每一层的输入通道数是上一层输出通道数加上跳跃连接的特征图通道数所以第d2层输入是5125121024这个拼接逻辑一定要算对很多新手在这里写错导致维度对不上。3. 判别器设计PatchGAN的原理与实现3.1 为什么pix2pix要用PatchGAN而不是普通判别器把判别器从输出单个标量改成输出一个小尺寸特征图是pix2pix最巧妙的设计之一。普通判别器本质上只在回答这张图整体是真还是假它对图像不同区域的判别权重是共享的一片区域纹理乱掉可能被其他区域的真实感盖过。PatchGAN则把输入图切分成若干N x N的patch对每个patch输出真假概率。以70x70 PatchGAN为例输入是256x256的图经过几层卷积后输出30x30的特征图每个像素对应输入图上大约70x70的感受野区域这一格负责判别对应patch的真假。这个设计的直接效果是生成器为了让所有patch都通过判别必须保证图像每个局部区域的纹理、边缘、颜色分布都足够真实。这在目标上是局部一致性但实际训练时它会倒逼生成器同时优化全局色调和局部细节因为局部patch的纹理和全局光照是耦合的。另一个好处是显存占用小PatchGAN的参数量只有普通全图判别器的一个零头训练时可以开更大的batch size对显存有限的开发者非常友好。还有一个很多人忽略的点PatchGAN天然支持任意尺寸输入。因为它是全卷积结构没有全局池化层和全连接层输入尺寸改变后输出特征图的尺寸也随之改变最后取平均即可。这意味着你可以用256x256训练好的权重去推理512x512或更大尺寸的图效果不会崩得太厉害。相比之下普通判别器用了全连接层输入尺寸固定死换尺寸就得重新训练这是PatchGAN在实际部署时隐藏的巨大优势。3.2 PatchGAN的PyTorch实现细节class PatchDiscriminator(nn.Module): def __init__(self, in_ch6): super().__init__() # 输入是生成图像和条件图像拼接3 3 6通道 self.conv1 nn.Sequential( nn.Conv2d(in_ch, 64, kernel_size4, stride2, padding1), nn.LeakyReLU(0.2, inplaceTrue) ) self.conv2 nn.Sequential( nn.Conv2d(64, 128, kernel_size4, stride2, padding1), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, inplaceTrue) ) self.conv3 nn.Sequential( nn.Conv2d(128, 256, kernel_size4, stride2, padding1), nn.BatchNorm2d(256), nn.LeakyReLU(0.2, inplaceTrue) ) self.conv4 nn.Sequential( nn.Conv2d(256, 512, kernel_size4, stride1, padding1), nn.BatchNorm2d(512), nn.LeakyReLU(0.2, inplaceTrue) ) self.conv5 nn.Conv2d(512, 1, kernel_size4, stride1, padding1) def forward(self, x): x self.conv1(x) x self.conv2(x) x self.conv3(x) x self.conv4(x) x self.conv5(x) return x这段代码里判别器输入是通道维拼接后的六通道张量前三个通道是输入条件图比如素描图后三个通道是待判别图真实照片或生成图。注意拼接顺序不要搞反且整个过程不能有全连接层最后输出的特征图尺寸取决于输入尺寸。以256x256输入为例这个网络输出的是16x16的patch特征图每个点对应感受野约为70x70正好和论文默认配置对应。这里有两个工程细节要特别强调。第一判别器第一个卷积层不加BatchNorm原论文明确这么设计理由是BN在浅层容易破坏特征的对比度信息让判别器过早变得迟钝。第二所有下采样层的stride不要统一设成2。我上面的代码里前三层stride2到第四层stride1最后卷积层stride1。这样做的目的是让感受野平滑增长如果每一层都是stride2最后一层输出的patch尺寸太小局部判别粒度会过粗生成器就有机会在某些小区域蒙混过关。4. 损失函数与训练策略4.1 GAN损失与L1损失的组合逻辑pix2pix的损失函数是经典的二合一结构一个对抗损失GAN Loss加一个L1重建损失总损失等于两者加权求和。原论文里权重系数lambda取100这个数值不是拍脑袋定的它决定了生成器优化方向的倾斜程度。对抗损失负责让生成图像在分布上接近真实图像。生成器希望骗过判别器尽力让判别器输出接近1判别器则尽力区分真实图像和生成的假图像。两者互为对手形成动态平衡。但单独使用对抗损失有个巨大问题模式坍塌。生成器可能会发现输出少数几种看起来相当真实的图像就能稳定骗过判别器导致生成结果多样性极高细节丢失严重。L1损失恰好能压制这种倾向它强制生成图像和真实图像在像素级别上尽量接近即使这会牺牲一部分视觉上的个性化风格但换来了稳定的内容保真度。简单说对抗损失管像不像真的L1损失管像不像正确答案。lambda100的含义是当生成图像在某个像素上有0.1的像素误差就相当于在对抗损失上产生了0.1*10010的惩罚。这个惩罚力度足够让生成器优先解决像素级一致性问题而不是天马行空地去生成看起来合理但与输入不匹配的结果。实际训练中你会发现如果lambda设成1生成器会疯狂尝试挑战判别器输出带有一部分幻觉内容设成1000生成器退化成一个纯L1回归器图像模糊但结构稳定。100这个数值是在大量任务上验证过的平衡点。4.2 优化器选择与学习率调整策略原论文用的是Adam优化器两个网络共用同一种配置学习率0.0002beta10.5beta20.999。这里beta10.5是关键。标准的Adam默认beta10.9动量太大在GAN这种非平稳目标中容易导致振荡不收敛。把beta1降到0.5相当于限制梯度历史的累计长度让参数更新更谨慎对波动剧烈的对抗训练更友好。学习率策略上原论文采用了一种朴素但有效的方案前一半epoch用固定学习率0.0002之后每epoch线性衰减到0。以200个epoch为例前100个epoch保持0.0002第二个100个epoch从0.0002线性降到0。这个做法的好处是前期用较大学习率快速收敛到大致的解空间后期逐渐减小学习率进行精细打磨让损失曲线在末尾稳定下来而不是来回乱跳。我个人实践下来的建议是如果你的数据集比较小比如只有几百对图可以适当降低初始学习率到0.0001并提前衰减可以有效避免过拟合式震荡。如果数据集很大几万对以上可以考虑把epoch数降到100但不要动学习率衰减的分段逻辑前一半固定、后一半线性衰减这个节奏本身非常稳。5. 完整训练流程与关键代码5.1 数据加载与预处理要注意什么pix2pix最理想的数据组织方式是每对输入输出图左右拼在一张图里训练时从中间切开。以某数据集为例每张训练图像的左半边是输入条件图例如语义分割图右半边是对应的真实图例如街景照片。代码里可以用一个简单的自定义Dataset来实现class PairedDataset(Dataset): def __init__(self, root_dir, size256): self.paths sorted(glob.glob(os.path.join(root_dir, *.jpg))) self.size size self.transform transforms.Compose([ transforms.Resize((size * 2, size), interpolationImage.BICUBIC), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ]) def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) img self.transform(img) # 尺寸: [3, size, size*2] w self.size input_img img[:, :, :w] # 左半部分 target_img img[:, :, w:] # 右半部分 return input_img, target_img这里两个细节容易出错。第一Resize时宽高一定要写成(size * 2, size)因为原图左右拼接后整体宽是高的两倍如果直接Resize到(256, 256)会严重拉伸变形。第二Normalize用0.5均值0.5方差会把像素从[0,1]转到[-1,1]这是配合生成器最后一层tanh输出的范围。如果你用别的归一化方法必须在生成器的输出层做对应调整。训练时的batch size建议单卡显存8GB以下用1或216GB可以用424GB以上可以尝试8。pix2pix的BatchNorm在小batch下统计不稳定如果batch只能设为1建议把编码器第一层和最后一层的use_bn设为False否则BN的归一化行为在单样本上几乎起不到正常作用。5.2 单轮训练循环生成器与判别器的交替更新训练循环是典型的GAN两步走先训判别器再训生成器。判别器训练时input和target拼接成真样本input和fake拼接成假样本分别计算损失并求平均然后反向传播更新判别器参数。生成器训练时冻结判别器参数不冻结也没关系因为只对生成器做backward把input通过生成器得到fake拼接后送进判别器用对抗损失加L1损失去更新生成器。def train_one_epoch(gen, disc, dataloader, opt_g, opt_d, criterion_gan, criterion_l1, lambda_l1100): for input_img, target_img in dataloader: input_img input_img.to(device) target_img target_img.to(device) bs input_img.size(0) # 真实与假标签判别器目标 real_label torch.ones(bs, 1, patch_h, patch_w, devicedevice) fake_label torch.zeros(bs, 1, patch_h, patch_w, devicedevice) # ---- 训练判别器 ---- opt_d.zero_grad() fake gen(input_img).detach() # detach很重要防止梯度传到生成器 real_pred disc(torch.cat([input_img, target_img], dim1)) fake_pred disc(torch.cat([input_img, fake], dim1)) loss_d (criterion_gan(real_pred, real_label) criterion_gan(fake_pred, fake_label)) / 2 loss_d.backward() opt_d.step() # ---- 训练生成器 ---- opt_g.zero_grad() fake gen(input_img) fake_pred disc(torch.cat([input_img, fake], dim1)) loss_gan criterion_gan(fake_pred, real_label) loss_l1 criterion_l1(fake, target_img) loss_g loss_gan lambda_l1 * loss_l1 loss_g.backward() opt_g.step()判别器真实标签不是标量1而是(bs, 1, patch_h, patch_w)的全1张量因为PatchGAN输出的本来就是特征图标签维度必须对齐。.detach()那一步极其重要如果忘了加生成器的梯度会通过fake回传到判别器再传到生成器导致判别器和生成器的梯度互相污染训练必崩。还有一个每轮训练都要注意的问题判别器不要训得太好也不要训得太差理想情况是让判别器损失维持在0.5左右如果判别器损失快速降到0.1以下说明判别器太强生成器学不到梯度信息可以考虑降低判别器的学习率或减少每轮判别器更新次数。6. 训练实战中的经验与避坑指南6.1 十个高频踩坑点以下是从实际训练中总结出来的高频问题按对训练结果影响程度排序。第一个坑loss曲线一直下降但生成图像模糊。这通常是L1权重过大导致的。lambda设为100没问题但如果数据集本身含有大量噪声或标注不准L1项会把噪声也作为正确答案来学导致输出出现重影或模糊。解决办法是适当降lambda到30~50或者检查数据配对是否准确。第二个坑生成器输出出现棋盘格纹路。这是反卷积的常见副作用。反卷积在步长大于1时输出像素的感受野不均匀某些位置的响应是其他位置的两倍形成类似棋盘的周期纹理。解决办法有两个一是把生成器中的ConvTranspose2d换成UpsampleConv2d的组合二是给反卷积接一个高斯模糊或者确保输出分辨率是输入分辨率的2的幂次倍数。前者效果更彻底建议直接改掉。第三个坑BatchNorm在batch size为1时训练失效。BatchNorm的小batch统计量极其不稳定batch为1时甚至几乎没有归一化效果。如果显存实在不够使用InstanceNorm替代BatchNorm或者像原论文里对第一层和最后一层不设BN这个改动对256x256输入通常不会有明显精度损失。第四个坑训练到一半判别器损失变成0或直接卡死在某个值。多半是学习率太大导致判别器参数爆炸或者是判别器的输入分布被生成器带偏到了不可能判别的位置。先降低判别器学习率再把学习率衰减的起点提前检查是否有输入图片出现了NaN或Inf。第五个坑数据集里输入和目标图没有严格对齐。pix2pix是像素级对齐的任务如果输入图和目标图有哪怕半个像素的偏移L1损失就会产生大量错误梯度模型会学出模糊化策略来对冲偏移。检查数据对齐的最快方法是把输入和目标拼成一张图用肉眼看几秒明显错位就重新处理数据。第六个坑训练完成后生成图偏暗或有偏色。pix2pix默认输出映射是tanh而如果标签没有归一化到[-1,1]模型学到的正确的暗色会被tanh截断导致输出整体偏暗。在数据预处理时确认target_img已经做过归一化如果是直接读图并转Tensor会落在[0,1]区间需要再映射到[-1,1]。第七个坑训练时间过长但效果提升很小。当判别器和生成器的loss都在低位徘徊不再变化时应当停止继续训练而不是盲目加epoch。常见的原因是数据量太少模型已经过拟合此时要么加数据增强要么减少网络层数降低模型容量。第八个坑使用colab或云GPU训练时突然断连损失全部丢失。强烈建议每10个epoch保存一次checkpoint并同时保存生成器和判别器的状态字典和优化器状态这样即使断了也能从最近的checkpoint恢复训练而不是重头再跑一遍。第九个坑多个实验共用同一个随机种子导致结果无法复现差异。开始训练前固定torch.manual_seed(42)并不代表每次运行的结果都一样因为数据集shuffle和CUDA的随机行为依然存在。需要同时对torch.backends.cudnn.benchmark和torch.cuda.manual_seed_all也做设置才能获得大致稳定的复现效果。第十个坑推理时BatchNorm行为不一致。模型训练时BN统计的是每个batch的均值和方差而推理时用的是训练过程中累计的running_mean和running_var。如果模型被切断了训练直接调用eval()后的效果和训练时差异巨大。务必在推理前调用model.eval()否则BN层会根据当前batch的输入动态计算统计量导致生成结果明显劣化。6.2 效果调优的实用技巧训练稳定之后如果想让生成质量再上一个台阶可以尝试以下几个操作。技巧一给生成器换用UpsampleConv2d替代反卷积。虽然反卷积在pix2pix原始实现里效果不错但换成nn.Upsample(scale_factor2, modebilinear)配合nn.Conv2d(kernel_size3, stride1, padding1)后棋盘格现象基本消除整体纹理更细腻。代价是参数量略微上升训练速度略慢。技巧二把判别器改成多尺度判别器。除了70x70 PatchGAN再建一个感受野更大的判别器和一个更小的判别器分别关注全局和局部最后把三个判别器的损失相加。这个方案常见于后续的改进工作在图像清晰度上有明显提升但显存占用也会翻一倍以上。技巧三数据增强方面不需要做太多花样。对于pix2pix只需要随机水平翻转即可垂直翻转和随机裁剪会破坏输入-输出的像素对齐关系反而有害。如果需要更大数据集考虑用弹性形变或仿射变换加噪声但注意不要破坏配对关系。技巧四训练时用梯度惩罚或谱归一化稳住判别器。原版pix2pix没加这些但对复杂数据集比如高分辨率医学图像加一层谱归一化能明显降低训练崩溃的概率。谱归一化的实现非常简单PyTorch里直接调用nn.utils.spectral_norm包在卷积层外边即可。技巧五关注图像池方案。原论文提出一个buffer存储最近训练出的N张生成图在训练判别器时从buffer里随机取一部分假图参与判别避免判别器对当前生成器的输出过拟合。代码上只需要维护一个固定大小的队列相对容易实现。我在多个项目里实测过图像池对训练稳定性有明显帮助尤其当数据集比较小时。7. 常见问题与排查实录7.1 训练不收敛怎么办训练不收敛是生成类任务最常见的状态。首先要分清楚不收敛的表现是loss波动极大且没有任何下降趋势还是loss持续下降但生成图仍然一塌糊涂。若是loss剧烈震荡多半是判别器和生成器的学习率不匹配。先把判别器学习率降到生成器的1/2甚至1/4让生成器有相对稳定的优化方向。还震荡就检查是不是有梯度爆炸在反向传播后加一个torch.nn.utils.clip_grad_norm_把梯度范数裁剪到1.0能缓解大部分剧烈波动。若是loss下降但生成图质量差问题多半在数据侧。移出几张训练样本用matplotlib.pyplot.imshow直接可视化输入图-目标图-生成图一眼就能发现目标图是否错位、是否被压缩、是否有重复样本。还有一个常见原因batch size太小导致BN统计量漂移换成InstanceNorm或增大batch后几分钟内就能看到明显变化。最后要说一个常被忽略的因素学习率衰减的起点。如果数据集很小几千张图网络很快就能拟合训练集此时到了衰减阶段已经没有学习的余地模型能力被白白浪费。建议小数据集训练时总epoch数不要超过50并把衰减起点设在第25个epoch同时提高生成器的dropout概率到0.5以上。7.2 输出图像模糊或出现棋盘格如何解决模糊问题通常有两个来源一是L1损失过强二是生成器容量不足。先做一次快速对照实验把lambda从100降到10跑20个epoch如果模糊程度明显改善但出现颜色失真说明确实是L1约束过强可以在50~100之间找一个折中值。如果几乎没有改善说明生成器容量不够把每层通道数乘一个1.5或2的系数或者增加一层编码器。棋盘格问题则是反卷积的经典副作用。最直接的解决方法前面提到过用Upsample Conv2d替换ConvTranspose2d。这里给出替换代码# 替换前 nn.ConvTranspose2d(in_ch, out_ch, kernel_size4, stride2, padding1, biasFalse) # 替换后 nn.Sequential( nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), nn.Conv2d(in_ch, out_ch, kernel_size3, stride1, padding1, biasFalse), nn.BatchNorm2d(out_ch) )替换后参数量略有增加但棋盘格能消失大半。如果仍然有规律性伪影可以进一步将Upsample改为modenearest搭配一个可学习的卷积层在实测中这种方法兼顾了速度和质量。替换完成后记得重新训练仅替换网络参数而不重新训练没有意义。还有一个排查思路棋盘格也可能是损失函数里L1权重过高导致生成器选择了某类高频纹理来拟合而不是学习真实纹理。降低L1权重配合替换反卷积一起做基本能解决绝大多数画面周期伪影问题。在踩过多次坑之后我个人的体会是pix2pix这类框架最花时间的往往不是模型结构设计而是数据准备、损失配比、训练稳定性这三角。结构照抄原论文就够了但数据配对质量、lambda系数的感知、以及训练中随时可视化中间输出这三件事才是决定最终效果走向的关键。任何一个环节偷懒模型都会用模糊化和细节丢失来惩罚你。所以如果你准备自己上手跑pix2pix先把数据管线做扎实再考虑调损失和改网络这个顺序能帮你少走至少一半弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑