☰
StarGAN多域图像翻译代码解析:标签注入、损失项与训练复现
2026/11/3 3:26:36 网站建设 项目流程

StarGAN的代码我第一次读的时候,卡在的不是那些花哨的损失函数,而是一行极其朴素的torch.cat([x, c], dim=1)。就这么一拼,多域图像翻译这个原本要训 N 个模型的问题,被压成了单生成器 + 单判别器的结构。如果你手上正跑着好几对 CycleGAN 的权重,文件夹按域A到域B、域B到域C这样命名,磁盘一路涨、训练时间一路翻,那你大概能体会到这里面的价值:一套权重覆盖全部方向的互转,新增一个域不是再训一个模型,而是在标签维度上加一个 bit。

这篇解析面向的是已经能跑通基础 GAN、想搞清楚这套多域翻译代码究竟怎么落地的人。我会从模型结构、标签注入方式、四个损失项的逐行作用,一直写到训练循环里那些顺序不能乱的地方,再到我自己复现时遇到的几个隐蔽 bug。代码以主流的 PyTorch 实现为参照,参数配置对着 128×128 的人脸属性编辑任务来讲,这套配置最容易复现,显存要求也最友好。读完之后你应该能做到:看懂每一行前向传播的意图,自己搭出训练脚本,并且在生成结果崩掉时知道往哪儿查。

1. 多域翻译为什么不能靠堆模型解决

1.1 从两个域到五个域,成本不是线性增长

一对一的图像翻译,比如把夏天的风景变成冬天,一个生成器加一个判别器足够了。但真实的业务需求很少这么干净。假设你要做的是人脸属性编辑:换发色、改性别、加年龄感,这里是 3 个属性,每个属性两个取值,组合起来就是 8 种状态。如果你用一对一模型的思路去覆盖所有方向,需要的权重数量是 8×7=56 个方向。到 5 个属性就是 32 种状态、992 个方向,光是把这些权重存下来就够呛,更别提每个方向还要单独准备成对或非成对的训练数据。

这不是单纯的工程量问题,而是数据利用率的问题。每一对方向的训练,只能看到那两个域的数据,剩下 30 种状态下的图像对当前模型来说完全是浪费。而多域翻译的核心诉求恰恰相反:我希望整个数据集里的所有图像,无论它属于哪个域,都能为所有方向的翻译提供监督信号。

StarGAN 解决这件事的思路非常直接:与其让模型学“从域 A 到域 B 的映射”,不如让它学“从任意域到任意域”这一个映射,把目标域作为额外的输入条件喂进去。这样一来,域的数量只影响标签向量的维度,不影响模型的数量。这个思路在条件生成模型里并不新鲜,难的是怎么让判别器也理解“域”这个概念,以及怎么防止模型在多个域之间来回“作弊”。

1.2 把域标签当作条件输入注入

具体做法是:数据集里每张图除了图像本身,还带一个 one-hot 向量,长度等于属性的总类别数。比如我们管 3 个二值属性(发色黑/金、性别男/女、年龄老/少),每个属性占 1 位,标签向量长度就是 3。原图对应的标签记作c_org,目标标签记作c_trg,两者都是这个 3 维向量,区别只在某几位翻转了。

生成器的输入因此变成两路:图像张量[B, 3, H, W]和目标标签向量[B, 3]。但卷积网络处理的是空间张量,怎么把一个 3 维向量塞进去?StarGAN 选择的方案是在空间维度上复制它,拼到通道维上。3 维标签复制成[B, 3, H, W],与图像在通道维拼接,得到[B, 6, H, W]的输入。这个操作只有一行,但它决定了整个模型的架构走向——生成器第一层卷积的输入通道数必须从 3 改成3 + c_dim。

判别器这边要做的事更微妙。常规判别器只输出一个真/假分数,StarGAN 的判别器要在同一次前向里输出两样东西:真伪判断,以及对输入图像所属域的预测。这两件事共享前面的卷积特征,只是最后接了两个不同的头。这种做法在业界叫辅助分类器,它的存在是整套方案能收敛的关键——没有它,生成器完全可以把目标标签当耳旁风,反正判别器也看不出来。

1.3 几种多域方案的横向对比

为了说清楚这个取舍,把几条常见路线摆在一起看比较直观。

方案模型数量新增域的代价主要短板
一对一模型(每方向一套)N×(N-1)重训一整套权重体积爆炸,跨域数据无法复用
共享权重的多生成器N重训一个生成器判别器仍需分开做,且域间无法交叉监督
多域联合训练(本文方案)1标签维度加 1 位标签设计需谨慎,属性间存在耦合
独立训练 + 后期融合1权重合并融合后普遍掉点,难以定位问题

表里第四种方案是我早期走过的一段弯路,想着各域先分开训、训好再合并参数,结果合并后亮度整体偏掉,排查成本远超一开始就做联合训练。这个教训值得单独说一句:多域训练的收益来自监督信号的共享,如果前期各自为战,这个共享就没了。

提示:属性之间存在耦合是本方案最容易被低估的坑。发色和性别在人脸数据里相关性很强,强制翻转发色时,性别属性经常跟着漂。标签设计阶段最好先算一遍候选属性的相关系数矩阵。

2. 网络结构拆解:生成器与判别器到底长什么样

2.1 生成器:下采样、残差块、上采样三段式

生成器的骨架沿用了经典的编码器-解码器结构,三段清晰可辨。第一段是一个 7×7 大核卷积,步长为 1,padding 为 3,输入通道3 + c_dim,输出通道conv_dim(通常取 64)。用大核的理由是扩大初始感受野,让模型在处理早期就能感知较大范围的纹理与色彩分布,同时保持输出分辨率和输入一致。

第二段是两次下采样,每次用 4×4 卷积、步长 2、padding 1,通道数翻倍。从 128×128 降到 32×32,通道从 64 涨到 256。这一步的目的是在保留信息的前提下压缩空间尺寸,让后面的残差块能在更大的感受野上工作。步长 2 配 4×4 核是个经验组合:输出尺寸正好是输入的一半,卷积核覆盖时不重叠,不会出现棋盘格纹路。

中间是 6 个残差块,通道数不变。这块的作用是特征变换的主体,负责把“原域特征 + 目标域标签”融合成“目标域特征”。残差块内部结构是标准的 Conv-InstanceNorm-ReLU-Conv-InstanceNorm,最后加回输入。这里用 InstanceNorm 而不是 BatchNorm,原因是图像生成任务对每个样本自身的风格更敏感,InstanceNorm 按单样本单通道统计,风格迁移类任务里普遍表现更稳,而且推理时不受 batch 内其他样本影响。

第三段是两次转置卷积上采样,4×4 核、步长 2、padding 1,通道数逐次减半,回到 64;最后接一个 7×7 卷积把通道压回 3,外面套一个 Tanh 把输出约束到[-1, 1]。这个取值范围要和数据预处理对齐——如果你的数据只归一化到[0, 1],Tanh 输出会永远偏低,生成图整体发灰。

2.2 判别器:PatchGAN 加两个输出头

判别器全部用 4×4 卷积、步长 2 的堆叠,中间夹 LeakyReLU(负斜率 0.01),没有归一化层。这里不用 InstanceNorm 是有意为之:判别器配合梯度惩罚使用时,归一化层会破坏梯度惩罚“在样本空间上约束梯度范数”的语义,很多复现版本掉点的原因就在这。

堆叠 6 层之后,128×128 的输入会降到 2×2。接下来分两个头:conv1是 3×3 卷积、输出 1 通道,得到真伪分数图;conv2的卷积核尺寸要能正好把 2×2 覆盖完,所以核大小取image_size / 2^repeat_num,128 配 6 层就是 2,输出[B, c_dim, 1, 1],再 squeeze 掉空间维度,得到每个属性类别的分类 logits。

这个核大小的计算是复现时的高频错误点。有人把repeat_num调成 7,但忘记改核大小推导,conv2会拿着一个尺寸不匹配的核去做卷积,报错信息又指向形状不匹配,很难一眼看出根因。我建议把这行写成表达式而不是写死数字,让它跟着分辨率自动算。

2.3 参数量的实测与显存估算

按conv_dim=64、repeat_num=6、c_dim=6这组配置,生成器的卷积层参数在 800 万量级,判别器在 500 万量级。两个模型加起来 1300 万参数左右,只是 ResNet-50 的四分之一,单卡完全吃得下。

显存方面,真正的大头不在参数量而在中间激活。128×128、batch 取 16、FP32 训练,实测单卡显存占用在 6 到 7 GB 之间浮动。如果显存吃紧,有几个不牺牲效果的调整点:把 7×7 首层卷积换成两层 3×3 堆叠(参数量略涨但激活峰值下降)、把残差块从 6 个减到 4 个(效果损失可控)、或者用梯度累积把等效 batch 做上去而实际 batch 压到 8。

注意:切换分辨率时,不只是改数据加载的 resize 参数。生成器的下采样次数、判别器的卷积层数、conv2的核大小,三者必须同步改。128 用 6 层是常见配置,到 256 一般要加到 7 层,同时把转置卷积的次数也加到 3 次,否则中间特征图尺寸对不上,报错还特别隐晦。

3. 四个损失项,每一行都在解决什么

3.1 对抗损失:为什么换成了带梯度惩罚的版本

原始 GAN 的对抗损失用交叉熵,配上 Sigmoid,在图像生成任务上训练极不稳定。代码里换成了 Wasserstein 距离的变体:判别器不再输出概率,而是输出一个无界实数,损失直接取-D(真图)和D(假图)的均值差。判别器越能拉开真假分数差,损失越小;生成器则要尽力让假图分数变大。

Wasserstein 形式有一个约束:判别器必须是 1-Lipschitz 连续。早期的做法是权重裁剪,把判别器的每个参数硬夹到[-0.01, 0.01],简单但副作用明显——参数被压得过于集中,判别器容量浪费严重。代码里用的是梯度惩罚:在真假样本之间随机插值出一批“中间样本”,计算判别器在这些点上的梯度范数,把它往 1 拉。写法是这样的:

def gradient_penalty(self, y, x): weight = torch.ones(y.size()).to(self.device) dydx = torch.autograd.grad(outputs=y, inputs=x, grad_outputs=weight, retain_graph=True, create_graph=True, only_inputs=True)[0] dydx = dydx.view(dydx.size(0), -1) dydx_l2norm = torch.sqrt(torch.sum(dydx ** 2, dim=1) + 1e-12) return torch.mean((dydx_l2norm - 1) ** 2)

create_graph=True是关键,没有它二阶导就断了,反向传播会静默失败或者报“梯度为零”的怪错。retain_graph=True保证这张计算图后面还能再用。1e-12那个小量是我自己加进去的,避免范数为零时开方求导出现 NaN,原版代码没写,但实测在某些退化样本上确实会出问题。

插值系数的生成方式是alpha = torch.rand(B, 1, 1, 1),把它广播到图像尺寸上做加权。这里有个细节:插值必须在.data层面做,不能带着历史梯度,否则插值点的梯度会串进上一轮的图里,训练后期表现为损失突然大幅抖动。

3.2 分类损失:判别器的第二个头怎么用

这是个双头分类任务。对真实图像,判别器要把它分类到它自己的原始域标签c_org;对生成图像,要分类到生成时指定的目标标签c_trg。两部分都算交叉熵,求和送进判别器的总损失。

这个设计解决了“生成器可以无视标签”的问题。如果判别器只看真伪,生成器把目标标签随便填个零向量也能骗过判别器,输出只是原图的一个随机变体。加上分类约束之后,判别器在看到一张本该是“金发”的图却检测出发色特征是黑,会立刻给出高分类损失,反向传播就会推着生成器真的去改发色。

生成器这边的分类损失有个容易看漏的地方:它用假图x_fake送进判别器,但比对的是原始标签c_org,不是目标标签。初读会觉得很反直觉——我不是刚把它变成目标域了吗?逻辑是这样的:x_fake的内容仍然来自原始图像,人物、姿态、背景都是原图的,只是风格属性被改了。判别器对这张图的“来源身份”判断应该还是原始域。这个约束确保了改属性的时候,图像的主体内容没有被破坏。如果这里误写成c_trg,模型会倾向于生成一张完全符合目标域统计分布但与人无关的假脸,这是典型的复现失败案例。

3.3 重建损失:循环一致性在这里怎么落地

重建损失是保证内容保真的核心。流程是:x_real配上目标标签c_trg生成x_fake,再把x_fake配上原始标签c_org生成x_reconst,最终比较x_real和x_reconst的 L1 距离。

为什么用两次生成而不是一次?因为标签翻转之后,中间图像x_fake已经丢失了一部分原始信息(这就是改属性的目的)。要从x_fake还原回去,生成器必须学会在改属性的同时尽量保留那些与属性无关的细节。这个约束让模型不会为了骗过判别器而彻底重画一张图。

这里选择 L1 而不是 L2 是有讲究的。L2 对大误差惩罚更重,会推动模型输出更“平均”的结果,视觉上表现为整体偏糊、细节丢失。L1 的梯度在大误差处是常数,模型更倾向于生成清晰的边缘。图像翻译任务里 L1 基本是默认选择。

重建损失的权重lambda_rec通常设得很大,10 是常见值。理由是它的数值量级天然比对抗损失小——L1 距离在[0, 2]之间(数据归一化到[-1, 1]),而对抗损失动辄几十。不给个 0.1 到 10 的系数,重建项在总损失里几乎没有存在感,模型会迅速退化到只管骗判别器。

3.4 总损失组织与权重配置

把四项拼起来的完整形式:

# 判别器 d_loss_real = -torch.mean(out_src_real) d_loss_fake = torch.mean(out_src_fake) d_loss_cls = 0.5 * (cls_loss_real + cls_loss_fake) d_loss_gp = self.gradient_penalty(out_src_hat, x_hat) d_loss = d_loss_real + d_loss_fake + lambda_cls * d_loss_cls + lambda_gp * d_loss_gp # 生成器 g_loss_fake = -torch.mean(out_src_fake2) g_loss_cls = cls_loss_fake2_to_org g_loss_rec = torch.mean(torch.abs(x_real - x_reconst)) g_loss = g_loss_fake + lambda_cls * g_loss_cls + lambda_rec * g_loss_rec

权重取值上,lambda_cls = 1、lambda_rec = 10、lambda_gp = 10是经过大量实验验证的稳定组合。lambda_gp设 10 而不是 1,是因为梯度惩罚的量级本身很小(梯度范数接近 1 时,平方误差在 0 附近),放大 10 倍才在总损失里占一席之地。我试过调到 1,训练前 1 万次迭代几乎看不出真假分数的收敛,判别器约束太弱。

损失项典型权重作用调大后的表现
对抗(D 真 + D 假)1逼真度纹理锐利但可能出伪影
分类(D 辅助头)1属性可控性属性改变更彻底,易过冲
梯度惩罚10训练稳定性收敛变慢,但更不容易崩
重建(L1)10内容保真细节保留好,属性变化幅度受限

提示:重建权重和分类权重是一对跷跷板。lambda_rec调得过高,模型倾向于“少改”,属性翻转不彻底;lambda_cls调得过高,模型倾向于“乱改”,人物身份都保不住。调参时把这两者当成一对来动,不要单独调。

4. 数据组织与训练循环的完整实现

4.1 标签文件的组织方式

数据侧最简单也最容易出错的是标签。每张图对应一个二值向量,长度等于属性数,每个属性占一位。常见做法是把图像路径和标签写进一个列表文件,每行形如path/to/xxx.jpg 0 1 0,第一位是黑发(0 表示否),第二位是金发,第三位是性别。

加载器读到这一行之后要生成两类张量:图像张量,做 resize、中心裁剪、随机水平翻转(仅训练期)、归一化到[-1, 1];标签张量,直接转 float。

这里有个必须提的细节:水平翻转会破坏某些属性。对于“发色”这类客观属性,翻转不影响标签;但对于“左右分头发型”这类属性,翻转后标签语义就变了。稳妥做法是维护一个“翻转安全属性列表”,只对列表内的属性做随机翻转,或者干脆在翻转时重新计算标签。我见过一个复现项目,翻转之后忘了这回事,训练了三天才发现发色和性别全部错位,生成的图像在两组属性上来回横跳。

另外一个小工程点:把标签保存为 float32 而不是 int,因为后续要参与交叉熵计算和 one-hot 构造,中间换算成 int 再做会多一次转换。

4.2 生成器的前向路径与标签拼接

核心代码一共五行,但每一行都有讲究:

def forward(self, x, c): c = c.view(c.size(0), c.size(1), 1, 1) c = c.repeat(1, 1, x.size(2), x.size(3)) x = torch.cat([x, c], dim=1) return self.main(x)

view把[B, c_dim]变成[B, c_dim, 1, 1],接着repeat在空间维上平铺到和图像一样大。用repeat而不是torch.ones_like(x) * c,是因为前者只做视图复制,实际数据只存一份,显存友好得多。repeat的语义是按维度重复,参数(1, 1, H, W)表示通道维和 batch 维不重复、空间维铺满,这个参数顺序如果写反,拼出来的通道数不对,报错信息会显示为一个莫名其妙的通道数,比如 3+B 之类的。

拼接必须在通道维(dim=1)做,且图像在前、标签在后。顺序其实不影响最终效果,因为后面的卷积会学,但必须与生成器首层卷积的输入通道数3 + c_dim保持一致。有人在推理脚本里忘了加标签通道,直接把 3 通道图喂进去,报错信息会指向卷积的通道不匹配,看起来很基础但确实常见。

4.3 判别器为什么要在一次迭代里跑三次

训练循环里判别器一共被调用了三次,每次的输入都不同:

第一次,判真实图,拿out_src_real和真实标签的分类结果;第二次,判生成图(此时生成图已经 detach 过,不反传梯度到生成器),拿out_src_fake和目标标签的分类结果;第三次,把真假图随机插值作为输入,只为算梯度惩罚。

三次调用之后才把三个损失加起来,一次backward一次step。如果顺序搞反了——比如先optimizer_D.step()再算梯度惩罚——梯度惩罚就用不上当前这一步的判别器状态,起不到约束作用,表现为训练阶段真假分数难以收敛。

生成器的调用也需要两次前向:第一次生成x_fake,第二次把x_fake作为输入生成x_reconst。注意第二步的输入是x_fake而不是x_real,如果这里误用x_real,重建损失恒等于零,模型完全失去内容约束,训练几万次之后输出会越来越抽象。

还有一个训练节奏问题:判别器每更新 1 次,生成器更新 1 次,这个 1:1 的比例在这套结构里是合适的(有些 WGAN 实现用 5:1)。因为判别器这边有梯度惩罚在约束,不需要靠多次更新来强化判别能力。我试过 5:1,判别器过强,生成器梯度消失,损失曲线卡在一个高位纹丝不动。如果在这套代码里加对生成器的更多调用频率,反而会让判别器跟不上,梯度惩罚失去参照。

4.4 超参配置与一次完整训练的时间成本

下面这组配置是我在 128 分辨率、单卡 11 GB 环境下跑通的:

参数取值说明
batch_size16显存占用约 6.5 GB
g_conv_dim64首层通道数
d_conv_dim64首层通道数
g_repeat_num6残差块数量
d_repeat_num6判别器卷积层数
lambda_cls1分类损失权重
lambda_rec10重建损失权重
lambda_gp10梯度惩罚权重
lr1e-4Adam 学习率
beta1 / beta20.5 / 0.999Adam 动量项
n_critic5每轮 D 更新前先跑 5 次 G? 见下方说明

表里n_critic这一项单独说一下,这个参数在不同实现里含义会反。我采用的定义是“每完成 1 次生成器更新,先做 5 次判别器更新”,也就是判别器更激进一些。但前面说过 1:1 在这套结构里更稳,所以我自己的训练脚本里把它设成 5 只是为了观察判别器上限,实际发布的配置是 1。这个参数千万别照着名字抄,先看清楚源码里它是怎么用的。

时间成本上,单卡跑 128 分辨率、20 万次迭代(每次迭代包含 1 次 D 更新 + 1 次 G 更新,即 2 次参数更新),大约需要 40 到 50 小时。前 5000 次迭代基本是在学颜色分布,中间 5 万次左右开始能看出属性变化,10 万次之后趋于稳定。判断收敛不能只看损失曲线,要看生成效果的抽样:每隔 2000 次采样一批固定原图配固定目标标签,拼成网格存盘,人眼看变化趋势,这比盯曲线可靠得多。

5. 复现时踩过的坑与排查思路

5.1 生成图整体发灰、边缘糊

这是最常见也最容易被误判成“模型没训够”的现象。先别急着加迭代次数,按顺序查三件事。

第一,Tanh 输出和归一化区间是否对齐。数据侧如果只除到[0, 1],生成器输出[-1, 1],存盘时再乘 255 就会有一半像素被截断成零,看起来就像整张图糊了一层灰。检查方法很简单:从x_real里取一张,直接存盘,看是否正常,正常说明数据加载没问题,问题在 Tanh 之后的后处理。

第二,InstanceNorm 的track_running_stats。如果设成了True,推理时会用训练期累积的均值方差,而这两个统计量在 batch 很小时估计不准,输出就会整体偏灰。解法是生成器里的 InstanceNorm 全部设成track_running_stats=False,推理时用当前样本自己的统计量。

第三,判别器的梯度惩罚是否真的生效。把d_loss_gp单独打印出来,正常应该在 0.1 到 10 之间波动。如果长期贴着 0 或者长期大于 100,说明插值或二阶导那里出问题了,去查alpha的形状和create_graph参数。

5.2 属性翻转无效,输出几乎没变化

如果生成图看起来只是原图加了点噪声,属性毫无变化,优先怀疑标签没接进去。用最小测试定位:把同一个x_real配上两组差异很大的c_trg(比如全 0 和全 1),分别前向,比较两张输出的 L1 距离。如果距离接近零,说明标签通道根本没起作用。

往上游查,通常是这几个点之一:生成器首层卷积输入通道没改成3 + c_dim;标签拼接时dim写成了 0 或 2;训练循环里c_trg和c_org传反了。

特别提一下第三种。有些实现在构造c_trg时用torch.rand随机生成,结果恰好有概率生成和c_org完全相同的向量,这时候这张图的重建损失和对抗损失会体现出异常小的数值,整体训练看上去“特别顺”,但模型什么也没学到。稳妥做法是在生成c_trg之后做一次断言检查,保证与c_org至少有一位不同。

5.3 训练中途损失突然炸掉

表现为判别器损失在某次迭代突然跳到几千,之后再也回不来。这类崩溃九成和梯度惩罚有关。

第一个可能是插值算错了。alpha的形状必须是[B, 1, 1, 1],如果写成[B],广播之后会按最后一维对齐,得到完全错误的插值结果。这个 bug 特别隐蔽,因为不报错,只是训练慢慢崩。

第二个可能是插值用的x_real和x_fake带了梯度。插值操作必须在.data上做,然后对结果调用requires_grad_(True)。如果保留了原图的梯度,二阶导会一层层往回穿,数值极不稳定,前几千次迭代还能撑住,之后必然爆。

第三个可能是学习率在后期太大。Adam 的beta2=0.999意味着二阶矩估计的窗口很长,如果中途损失已经平稳,学习率没有衰减,一次异常梯度的冲击会被放大。我习惯在 10 万次之后用StepLR把学习率降到 1e-5。

5.4 问题速查表

把上面几类现象和排查顺序整理成表,出问题时按行往下对,比胡乱改参数快得多。

现象最可能的三个原因快速验证
输出发灰归一化区间不匹配 / InstanceNorm 统计量 / Tanh 后处理直接存x_real看是否正常
输出模糊lambda_rec过大 / 判别器过强 / 迭代不足打印重建损失,看是否在 0.05 以下
属性不翻转标签通道没接 / 拼接维度错 /c_trg与c_org相同固定图配两组极端标签比 L1 距离
损失突然爆掉插值形状错 / 插值带梯度 / 学习率未衰减打印d_loss_gp看量级
生成全是同一张脸模式崩塌,lambda_cls过大 / 判别器过强抽样多张不同输入看多样性
显存持续增长计算图未释放 /retain_graph没用对用torch.cuda.memory_allocated监控

注意:这张表里的“直接存x_real看是否正常”是最省时间的排查动作,但它只能排除数据侧问题,不能排除模型侧。数据侧正常之后,第二步是把生成器的权重随机初始化、跑一次前向,输出应该是彩色噪点;如果输出是纯色或者全黑,说明网络结构里某层有问题,和训练无关。

6. 从能跑到好用:几个值得扩展的方向

6.1 多数据集联合训练怎么组织标签

单数据集下标签向量长度固定,加第二个数据集时,两个数据集的属性体系往往不一样——一个管发色性别,另一个管表情类别。合理做法是把标签向量拼接起来,总长度等于两个数据集属性数之和。对某张来自数据集 A 的图,A 对应的那段填真实标签,B 对应的那段全部置零;反之亦然。这个“全零段”就是给模型的信号:这张图不属于那个数据集,不要用它来约束那部分的分类。

生成器在采样时也一样,要生成数据集 B 风格的图,就把 A 对应段置零、B 对应段填目标标签。这套机制不需要改网络结构,只改标签长度。代价是第一个数据集的属性数量变化时,所有已经训好的权重全部要重来,因为首层卷积的输入通道变了。

实际动手时还有一个坑:两个数据集的分辨率往往不同,需要统一到一个尺寸。统一的方式建议用“先缩放到短边再中心裁剪”,而不是直接硬 resize,硬 resize 会把人脸拉变形,属性特征的分布也跟着变,模型学起来更吃力。

6.2 推理阶段的属性插值与批量编辑

训练完成后最实用的能力是批量编辑。给定一张图和一个目标标签,直接前向就得到结果,速度上单张 128 分辨率在普通显卡上是个位数毫秒。批量跑的思路是把同一张图复制 B 份,配上 B 组不同标签,一次前向出 B 张结果,这样能充分利用显卡并行能力,比逐张跑快一个数量级。

更细一点的操作是属性强度插值。因为标签是二值的,模型只见过 0 和 1,直接在中间取值(比如 0.5)会让模型进入没见过的输入分布,输出质量不可控。想要渐变效果,稳妥方案是走隐空间插值:把两个目标标签分别生成结果,在像素或特征层面做线性插值,效果比直接喂小数标签稳定得多。我试过直接喂 0.3 这种值,前几次迭代出来的效果还行,训练越久模型对二值分布越敏感,小数输入反而越容易崩。

6.3 我在复现过程中形成的几条习惯

第一条,先跑通小分辨率再上大的。64×64 跑 2 万次迭代大概两三个小时,能验证标签注入、损失计算、采样存盘整条链路是否通。等 64 上属性翻转正常了,再改到 128,这时候出的问题基本只在分辨率相关的层数上,排查范围小一个数量级。反过来先上 128,一旦属性不翻转,你没法判断是结构问题还是单纯没训够。

第二条,采样日志要存成对比网格,每行一张原图,每列一个目标标签,第一列放原图。这个格式看趋势特别直观——训到某个阶段会发现某一行最开始只有一列有变化,后来逐渐扩散到所有列,这个扩散过程就是模型学会多域翻译的过程。纯数字的损失曲线提供不了这种信息。

第三条,c_trg的构造要留可复现性。用固定随机种子生成,保证每次重跑得到相同的标签序列,这样两次实验的差异才能归因到超参上。我早期偷懒没设种子,两次实验损失曲线不一样,花了半天才发现是标签序列不同导致的。

第四条,别迷信原版超参。原版的lambda_rec=10是在特定数据规模和属性数量下调出来的,你的属性相关性如果更强,这个值可能要往下压到 5 甚至 2。判断依据是抽样对比:如果改发色之后人脸明显换了个人,说明重建约束不够,往上调;如果发色几乎不变,说明重建约束太强,往下调。这个调整过程没有公式,只能靠看。

第五条,判别器最后一层的分类头输出记得做 squeeze。conv2输出是[B, c_dim, 1, 1],如果不 squeeze 成[B, c_dim],交叉熵的输入维度对不上,PyTorch 会给出一个“期望 2 维得到 4 维”的报错。这个错太简单,很多人反而第一时间想不到,会在数据形状上绕一大圈。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询