简介:面向生成对抗网络初学者与PyTorch实践者的DCGAN完整实现包,围绕64×64人脸图像生成任务,提供从数据读取、模型搭建到训练与结果观察的可复现实例。压缩包共31个文件、约17.35MB,含8个py源码与14个pyc缓存文件,另有4张png结果图、1个gif训练效果图、3个txt日志/列表及1个md说明文档,覆盖生成器、判别器、主训练脚本和数据处理模块。已有3780人学习下载。读者可依据README梳理运行环境与训练流程,结合日志和分阶段生成图判断收敛效果,并通过Generator、Discriminator及read_data等代码理解卷积GAN的损失优化与交替训练思路,适合课程实验、项目复现及GAN入门研究参考。
1. 这不是又一个 DCGAN 玩具:从数据集到可训练的 PyTorch 工程
把 DCGAN 当成“生成几张假图”的玩具,基本会在第二次改 batch size 时翻车。这份 DCGAN 的 PyTorch 实现带数据集和说明文件,价值不在模型定义那几十行,而在于把数据目录、训练入口、模型保存和固定噪声评估串成了一条能复现的线。它适合已经能跑通简单 CNN、想亲手看生成器从噪声里长出图像的人,也适合带课程设计或实验项目、需要一套结构清楚又不至于从零搭环境的从业者。说明文件负责目录和启动命令,代码负责训练循环和检查点,两者对着看,能少走很多“环境对了但形状不对”的弯路。
2. 把生成器和判别器拆开:DCGAN 的结构选型与张量形状核对
DCGAN 的结构不复杂,难在每一层的张量形状要严丝合缝。生成器从 100 维噪声往上采样,判别器从 64×64 图像往下采样,中间任何一处通道数或步长写错,报错信息往往只给一个不匹配的维度,不告诉你是哪一层。下面按生成器、判别器、形状核对三段拆开,代码可以直接放进models.py,也可以按说明文件里的文件组织方式拆成单独模块。
2.1 生成器从 100 维噪声到 64×64 图像:转置卷积的通道阶梯
生成器的常见做法是让噪声先变成 1×1 的特征图,再用转置卷积逐级放大到 4×4、8×8、16×16、32×32、64×64。每放大一次,通道数减半,直到输出 3 通道彩色图或 1 通道灰度图。转置卷积的参数一般取kernel_size=4, stride=2, padding=1,这个组合刚好让特征图边长翻倍;第一层从 1×1 到 4×4 要用kernel_size=4, stride=1, padding=0,不要顺手写成 stride=2,否则一开始尺寸就对不上。
import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, z_dim=100, base_channels=64, out_channels=3): super().__init__() self.net = nn.Sequential( # 输入 z: (N, 100, 1, 1) -> (N, 512, 4, 4) nn.ConvTranspose2d(z_dim, base_channels * 8, 4, 1, 0, bias=False), nn.BatchNorm2d(base_channels * 8), nn.ReLU(inplace=True), # (N, 512, 4, 4) -> (N, 256, 8, 8) nn.ConvTranspose2d(base_channels * 8, base_channels * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(base_channels * 4), nn.ReLU(inplace=True), # (N, 256, 8, 8) -> (N, 128, 16, 16) nn.ConvTranspose2d(base_channels * 4, base_channels * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(base_channels * 2), nn.ReLU(inplace=True), # (N, 128, 16, 16) -> (N, 64, 32, 32) nn.ConvTranspose2d(base_channels * 2, base_channels, 4, 2, 1, bias=False), nn.BatchNorm2d(base_channels), nn.ReLU(inplace=True), # (N, 64, 32, 32) -> (N, 3, 64, 64) nn.ConvTranspose2d(base_channels, out_channels, 4, 2, 1, bias=False), nn.Tanh(), ) def forward(self, z): # z 的形状必须是 (N, z_dim, 1, 1) return self.net(z)这段代码里base_channels=64是常见起点,通道阶梯是 512→256→128→64→3。z_dim=100是 DCGAN 常用的噪声维度,不是硬性规定,但改小到 32 时生成多样性会明显下降。out_channels要根据数据集来:彩色图填 3,灰度图填 1。生成器所有层都用了 BatchNorm,唯独最后一层没有,因为输出层接 Tanh,BatchNorm 会把输出分布拉回中心附近,反而限制颜色范围。Tanh 的输出范围是 [-1,1],所以数据预处理里的 Normalize 必须配套写成均值 0.5、标准差 0.5,否则生成器学到的颜色和真实图对不上。
bias=False也是有意为之:转置卷积后面接了 BatchNorm,偏置会被归一化抵消,省掉可以少一点参数。ReLU 用inplace=True在训练时省显存,但如果你后面要做梯度检查或可视化中间激活,建议先关掉,避免原地操作改掉前向结果。生成器第一层从z_dim直接到base_channels*8,不要中间再加全连接层,DCGAN 的原始做法就是把噪声 reshape 成(N, z_dim, 1, 1)直接送进转置卷积,这样结构更干净,也方便后续换分辨率。
2.2 判别器为什么用步长卷积替代池化:LeakyReLU 与 Dropout 的取舍
判别器的任务是把 64×64 图像压成一个分数。常见做法是不用池化层,直接用stride=2的卷积下采样,让网络自己学降采样方式。池化是固定的取最大值或平均值,卷积下采样保留可学习参数,对生成对抗训练更友好。判别器第一层通常不加 BatchNorm,因为真实图像和生成图像的分布差异在第一层最明显,加了归一化反而把这种差异抹平;后面的层可以加 BatchNorm 稳定训练。激活函数用 LeakyReLU,负半轴斜率 0.2,避免 ReLU 在负区直接死掉。
class Discriminator(nn.Module): def __init__(self, in_channels=3, base_channels=64): super().__init__() self.net = nn.Sequential( # (N, 3, 64, 64) -> (N, 64, 32, 32) nn.Conv2d(in_channels, base_channels, 4, 2, 1, bias=False), nn.LeakyReLU(0.2, inplace=True), # (N, 64, 32, 32) -> (N, 128, 16, 16) nn.Conv2d(base_channels, base_channels * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(base_channels * 2), nn.LeakyReLU(0.2, inplace=True), # (N, 128, 16, 16) -> (N, 256, 8, 8) nn.Conv2d(base_channels * 2, base_channels * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(base_channels * 4), nn.LeakyReLU(0.2, inplace=True), # (N, 256, 8, 8) -> (N, 512, 4, 4) nn.Conv2d(base_channels * 4, base_channels * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(base_channels * 8), nn.LeakyReLU(0.2, inplace=True), # (N, 512, 4, 4) -> (N, 1, 1, 1) nn.Conv2d(base_channels * 8, 1, 4, 1, 0, bias=False), ) def forward(self, x): # 输出 logits,不接 Sigmoid;训练时用 BCEWithLogitsLoss return self.net(x).view(-1, 1)最后一层输出(N,1,1,1),用view(-1,1)拉成(N,1),每个样本一个 logit。注意这里没有接 Sigmoid,训练时用BCEWithLogitsLoss,它内部会把 Sigmoid 和 BCELoss 合并,数值更稳。如果先把判别器输出过 Sigmoid 再用 BCELoss,遇到极端置信度时梯度容易饱和,生成器收不到有效信号,这是很多人第一次训练 DCGAN 时觉得“判别器 loss 降得很快但生成器不动”的原因之一。
Dropout 在判别器里是可选项。原始 DCGAN 在判别器中间加过 Dropout,但后来的实践里很多人去掉也能训。我的建议是:数据量小、判别器过拟合明显时,在最后两个卷积块之间加nn.Dropout(0.3);数据量够、训练稳定时先不加,减少一个变量。权重初始化也要统一,卷积层用均值 0、标准差 0.02 的正态分布,BatchNorm 的权重初始化为 1、偏置为 0,这套初始化对 GAN 的起步稳定性有实际影响。
def weights_init(m): classname = m.__class__.__name__ if classname.find('Conv') != -1: nn.init.normal_(m.weight.data, 0.0, 0.02) elif classname.find('BatchNorm') != -1: nn.init.normal_(m.weight.data, 1.0, 0.02) nn.init.constant_(m.bias.data, 0)把weights_init在创建模型后分别apply到生成器和判别器上,不要只初始化其中一个。有人只初始化生成器,判别器用 PyTorch 默认初始化,训练前几轮判别器打分特别极端,生成器梯度被压得很小,看起来像“生成器不学习”,其实是两边起点不一致。
2.3 用形状核对脚本锁死每一层:避免 32→64 拼接处翻车
在碰真实数据之前,先用一个小批量随机张量把生成器和判别器各跑一遍,确认输出形状。这一步花不了两分钟,但能省掉后面在训练循环里查形状报错的时间。尤其是你把 64×64 改成 128×128 时,生成器要多加一层转置卷积,判别器要多加一层步长卷积,层数不对时错误往往出现在很靠后的位置。
import torch from models import Generator, Discriminator # 小批量 8 张,先跑通形状,不要一上来就全量数据 z = torch.randn(8, 100, 1, 1) G = Generator(z_dim=100, base_channels=64, out_channels=3) D = Discriminator(in_channels=3, base_channels=64) fake = G(z) print('生成器输出:', fake.shape) # 期望 torch.Size([8, 3, 64, 64]) print('判别器输出:', D(fake).shape) # 期望 torch.Size([8, 1])如果生成器输出是[8, 3, 64, 64],判别器输出是[8, 1],说明主干形状对上了。再把out_channels改成 1,确认灰度分支也能跑。常见错误是生成器最后输出 64×64 但判别器第一层按 128×128 写,卷积下采样四次后得到 2×2,最后一层 kernel 4 直接报负尺寸。另一个高频问题是转置卷积的output_padding没设,尺寸差 1 像素,肉眼看不出来,但拼回图像时边缘会对不齐。
| 模块 | 输入形状 | 关键参数 | 输出形状 |
|---|---|---|---|
| 生成器第一层 | (N,100,1,1) | k=4,s=1,p=0 | (N,512,4,4) |
| 生成器中间层 | (N,512,4,4) | k=4,s=2,p=1 | (N,256,8,8) |
| 生成器输出层 | (N,64,32,32) | k=4,s=2,p=1 | (N,3,64,64) |
| 判别器第一层 | (N,3,64,64) | k=4,s=2,p=1 | (N,64,32,32) |
| 判别器输出层 | (N,512,4,4) | k=4,s=1,p=0 | (N,1,1,1) |
这张表建议直接写进说明文件的备注里。以后改分辨率时,先对着表数层数,再动代码。生成器和判别器的通道数不要求完全对称,但 64×64 输入下判别器四次下采样到 4×4 是常见配置;如果你把base_channels从 64 改成 32,生成器和判别器要同时改,不要只改一边。
3. 训练循环的四个关键参数:批次、学习率、优化器与标签平滑
结构跑通只是第一步,DCGAN 训练是否稳定,很大程度取决于数据预处理、批次大小、学习率和优化器动量这几个参数。下面按数据加载、交替训练、损失与优化器、日志与保存四段写,代码可以直接拼成train.py。说明文件里如果给了启动命令,优先按它的参数入口走,下面的值作为默认起点。
3.1 数据集加载与预处理:把图片统一到 64×64 的坑
用ImageFolder读图片时,目录结构要保留一层类别文件夹。即使你的数据集只有一类,也建议建一个子目录,例如data/train/images/xxx.jpg,否则ImageFolder会把图片当成类别名,直接报错。预处理链一般写Resize(64)、CenterCrop(64)、ToTensor()、Normalize()。Resize(64)只保证短边是 64,长边可能大于 64,所以后面要接CenterCrop(64)把长宽都裁到 64;如果直接用Resize((64,64)),长宽比会被强行拉伸,人脸或物体变形,生成器学到的边缘会歪。
from torchvision import datasets, transforms from torch.utils.data import DataLoader transform = transforms.Compose([ transforms.Resize(64), transforms.CenterCrop(64), transforms.ToTensor(), # 彩色图用三通道均值方差;灰度图改成 (0.5,), (0.5,) transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ]) dataset = datasets.ImageFolder(root='data/train', transform=transform) loader = DataLoader( dataset, batch_size=64, shuffle=True, num_workers=2, drop_last=True, # 避免最后一个 batch 只有 1 张,BatchNorm 会报错 )batch_size=64是 64×64 分辨率下的常见起点。显存不够时先降到 32 或 16,不要急着改模型通道数;批次太小会让 BatchNorm 的统计量波动很大,生成图像颜色会一跳一跳。drop_last=True很关键:如果数据集大小不是 batch size 的整数倍,最后一个批次可能只剩 1 张图,生成器和判别器里的 BatchNorm 在训练模式下对单样本会直接报错。num_workers在 Windows 上设 0 更稳,Linux 上可以设 2 或 4,设太大反而可能因为磁盘读取成为瓶颈。
归一化参数必须和生成器最后一层匹配。生成器用 Tanh 输出 [-1,1],数据就 Normalize 到 [-1,1];如果你把生成器最后一层换成 Sigmoid,数据要改成 [0,1] 归一化,同时损失函数和判别器输入也要跟着调。最忌讳的是生成器用 Tanh、数据却只除以 255 没做 Normalize,训练初期生成器输出全灰,判别器很容易区分,生成器梯度几乎为零,看起来像“模型不收敛”。
3.2 判别器与生成器的交替训练:一步 D 一步 G 的代码骨架
训练循环的核心是每个 batch 先更新判别器,再更新生成器。更新判别器时,真实图像标签为 1,生成图像标签为 0;生成图像要.detach(),避免梯度传回生成器。更新生成器时,重新采样一组噪声,生成图像送进判别器,标签设成 1,意思是“让判别器把假图判成真图”。两个优化器要分开,不要共用一个,否则zero_grad()会把另一边的梯度也清掉。
import torch import torch.nn as nn import torch.optim as optim device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') G = Generator(z_dim=100, base_channels=64, out_channels=3).to(device) D = Discriminator(in_channels=3, base_channels=64).to(device) G.apply(weights_init) D.apply(weights_init) criterion = nn.BCEWithLogitsLoss() lr = 0.0002 beta1 = 0.5 optimizer_G = optim.Adam(G.parameters(), lr=lr, betas=(beta1, 0.999)) optimizer_D = optim.Adam(D.parameters(), lr=lr, betas=(beta1, 0.999)) fixed_z = torch.randn(64, 100, 1, 1, device=device) for epoch in range(50): for i, (real_imgs, _) in enumerate(loader): real_imgs = real_imgs.to(device) batch_size = real_imgs.size(0) # 训练判别器:真图标签接近 1,假图标签为 0 D.zero_grad() real_logits = D(real_imgs) real_labels = torch.ones_like(real_logits) * 0.9 # 标签平滑,可选 loss_D_real = criterion(real_logits, real_labels) z = torch.randn(batch_size, 100, 1, 1, device=device) fake_imgs = G(z).detach() # 分离,避免更新 G fake_logits = D(fake_imgs) fake_labels = torch.zeros_like(fake_logits) loss_D_fake = criterion(fake_logits, fake_labels) loss_D = loss_D_real + loss_D_fake loss_D.backward() optimizer_D.step() # 训练生成器:希望判别器把假图判为真 G.zero_grad() z = torch.randn(batch_size, 100, 1, 1, device=device) fake_imgs = G(z) fake_logits = D(fake_imgs) gen_labels = torch.ones_like(fake_logits) loss_G = criterion(fake_logits, gen_labels) loss_G.backward() optimizer_G.step() if i % 50 == 0: print(f'epoch {epoch} step {i} loss_D {loss_D.item():.4f} loss_G {loss_G.item():.4f}')这段骨架里,判别器更新时用了fake_imgs = G(z).detach(),.detach()把生成图像从计算图里摘出来,判别器反向传播不会传到生成器。生成器更新时重新采样z,而不是复用判别器那一步的噪声,这样每次生成器看到的样本不同,训练更充分。如果你显存比较紧,可以复用同一个z,但生成器更新前要重新前向一次,不能直接用判别器阶段的fake_imgs.detach()。
标签平滑把真实标签从 1 改成 0.9,目的是防止判别器对真图过度自信。这个技巧不是必须的,但在数据量不大时能缓解判别器 loss 迅速降到 0。注意平滑只作用于真实标签,假标签仍然用 0。有人把假标签也改成 0.1,那相当于告诉判别器“假图有一点真”,训练目标会变味,不建议新手一上来就这么改。
3.3 损失函数与优化器参数:BCEWithLogitsLoss、Adam 的 lr 和 betas
损失函数用BCEWithLogitsLoss,判别器输出 logits,不要在外面加 Sigmoid。学习率常用 0.0002,Adam 的betas设成(0.5, 0.999),第一个动量 0.5 比默认的 0.9 小,目的是降低判别器更新的惯性,让生成器和判别器不至于一边倒。这个组合来自 DCGAN 的常见实践,不是唯一解,但作为起点比较稳。
如果训练几轮后判别器 loss 一直在 0.01 附近,生成器 loss 却很高,说明判别器太强。常见调整是把判别器学习率降到 0.0001,生成器保持 0.0002;或者把判别器每步更新一次改成每两步更新一次。反过来,如果判别器 loss 一直在 0.7 左右降不下去,生成图像又很模糊,可能是生成器学习率太低,或者数据预处理有问题。不要一上来就同时改学习率、batch size 和网络层数,一次只动一个参数,否则出了问题不知道是谁的锅。
优化器要分别建,optimizer_G只装生成器参数,optimizer_D只装判别器参数。有人图省事用一个优化器装两边参数,训练时一次step()同时更新,生成器和判别器会互相拖累,loss 曲线看着像心电图。优化器状态在保存模型时也可以一起存,断点续训时能接上动量,但新手先把模型权重存明白更重要。
3.4 训练日志与模型保存:怎么判断这次不是“玄学”
日志至少记录loss_D、loss_G和每个 epoch 的生成图。只看 loss 数值容易被误导:判别器 loss 低不一定好,生成器 loss 低也不一定生成质量高。比较实用的做法是每个 epoch 用同一组固定噪声生成一张 8×8 网格图,按 epoch 编号保存,训练结束后翻着看生成图像从噪声到有轮廓的变化。如果连续几个 epoch 生成图都是同一张脸或同一团颜色,基本可以判断模式崩溃,继续跑意义不大。
import os from torchvision.utils import save_image os.makedirs('samples', exist_ok=True) os.makedirs('checkpoints', exist_ok=True) with torch.no_grad(): fake = G(fixed_z) save_image(fake, f'samples/epoch_{epoch:03d}.png', nrow=8, normalize=True) torch.save({ 'G_state_dict': G.state_dict(), 'D_state_dict': D.state_dict(), 'z_dim': 100, 'base_channels': 64, 'out_channels': 3, }, f'checkpoints/dcgan_epoch_{epoch:03d}.pth')保存字典比只存state_dict()更稳,因为加载时知道z_dim、base_channels、out_channels这些结构参数。只存权重的话,下次换一台机器、忘了当时用的通道数,加载会直接报形状不匹配。normalize=True会把 [-1,1] 的图像映射到 [0,1] 再保存,否则图片打开一片黑。模型文件按 epoch 存,不要每个 step 都写盘,否则训练还没结束磁盘先满了。日志文件建议单独写一个train.log,把每个 epoch 的平均 loss 和保存路径记下来,后面排查时不用翻终端滚动记录。
4. 跑起来之后先别急着调参:五个高频翻车点排查
DCGAN 的训练不稳定是常态,但大部分“玄学”问题都能落到具体原因上。下面五条是实际复现时最容易遇到的,每条按现象、原因、解决写。遇到问题时先对照这五条排查,不要一上来就改网络结构。
4.1 生成器输出全黑或全灰:检查归一化与激活函数
现象:训练几个 epoch 后,保存的生成图要么全黑,要么全灰,判别器 loss 很低,生成器 loss 几乎不动。
原因:最常见的是数据归一化和生成器输出层不匹配。生成器最后一层用 Tanh,输出范围 [-1,1],但数据只做了ToTensor()没做Normalize,真实图像在 [0,1],判别器很容易区分真假,生成器梯度被压得很小。另一个原因是生成器最后一层误用了 ReLU,输出只有非负值,颜色永远偏亮。
解决:确认transforms.Normalize((0.5,), (0.5,))或三通道版本已经加上,并且生成器最后一层是 Tanh。如果数据集是灰度图,out_channels=1,归一化参数也要改成单通道。改完先跑 5 个 epoch,看生成图是否从纯灰变成有明暗变化,再继续调学习率。
4.2 判别器 loss 一路降到 0:模式崩溃的前兆
现象:判别器 loss 很快降到 0.01 以下,生成器 loss 持续升高,生成图在几个 epoch 内变成同一张脸或同一团纹理。
原因:判别器太强,生成器找不到有效梯度。常见诱因是判别器学习率比生成器高,或者判别器层数/通道数明显多于生成器,或者没有做标签平滑。
解决:先把判别器学习率降到 0.0001,生成器保持 0.0002;再加标签平滑,真实标签用 0.9;如果还不行,把判别器更新频率降到每两步一次。不要通过删判别器层来“让生成器赢”,那只是把问题往后拖。模式崩溃一旦发生,继续训练很难自己恢复,建议回退到崩溃前的检查点,改参数重跑。
4.3 显存不够:batch size 与 64×64 的边界
现象:训练一开始就报CUDA out of memory,或者跑几十步后显存缓慢上涨最后爆掉。
原因:batch_size=64在 64×64 分辨率下对 8GB 显存比较紧,尤其是生成器和判别器同时驻留显存、又保存了计算图。另一个常见原因是日志里把loss张量累加进列表却没.item(),计算图一直挂着,显存越跑越大。
解决:先把batch_size降到 32 或 16,确认能跑通再逐步加。打印 loss 时用loss.item(),不要直接存张量。验证阶段用with torch.no_grad(),并且不要把验证图的张量留在列表里。如果改完 batch size 后 BatchNorm 报单样本错误,确认drop_last=True还在。
4.4 数据集路径和图像通道数不对:报错信息里的线索
现象:ImageFolder报FileNotFoundError,或者生成器输出通道数和数据对不上,训练时卷积输入维度报错。
原因:数据目录少了一层类别文件夹,或者图片是灰度图但out_channels写了 3,再或者图片是 RGBA 四通道,ToTensor()后变成 4 通道,判别器第一层按 3 通道写就会报错。
解决:先打印len(dataset)和dataset[0][0].shape,确认读进来的张量是(3,64,64)还是(1,64,64)。如果是四通道图,在预处理里加transforms.Lambda(lambda img: img.convert('RGB')),或者用transforms.Grayscale(num_output_channels=3)统一成三通道。目录结构按data/train/类别名/图片组织,类别名用英文或数字,避免路径里有空格和特殊字符。
4.5 保存的模型加载后输出乱码:模型结构与权重顺序要一致
现象:训练时生成图正常,加载保存的.pth后生成图变成噪声或纯色。
原因:加载时只传了state_dict,但生成器的z_dim、base_channels、out_channels和保存时不一致;或者保存时用了torch.save(G, path)整个模型,加载环境里缺少自定义类定义,反序列化失败但被静默处理。
解决:统一用字典保存结构参数和权重,加载时先按参数重建模型,再load_state_dict。加载后先用固定噪声跑一张图,和训练时的samples目录对比,确认输出一致再继续训练。不要在加载时随手改base_channels,通道数一变,权重形状全对不上。
5. 从能跑到能看:用固定噪声和指标卡住随机性
DCGAN 的训练随机性很强,同一个脚本跑两次,生成图可能差很多。想把调参变成可比较的实验,先把随机种子和评估噪声固定下来。常见做法是在训练脚本开头设 Python、NumPy、PyTorch 的种子,并把cudnn.deterministic打开。这个设置会牺牲一点速度,但对比实验时值得。
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False固定噪声向量要在训练开始前生成一次,保存成.pt文件,之后每次评估都加载同一组,不要每个 epoch 重新torch.randn。这样不同 epoch 的生成图才有可比性:如果第 10 轮和第 20 轮用的是不同噪声,你很难判断变化来自模型进步还是噪声不同。
评估不能只靠肉眼。一个简单的粗筛是统计生成图和真实图的像素均值、方差,或者用判别器对生成图的平均打分。下面这段代码只做演示,不依赖额外库:
@torch.no_grad() def quick_stats(G, fixed_z, real_loader): fake = G(fixed_z) fake_mean = fake.mean().item() fake_std = fake.std().item() real_batch = next(iter(real_loader))[0].to(next(G.parameters()).device) real_mean = real_batch.mean().item() real_std = real_batch.std().item() return { 'fake_mean': round(fake_mean, 4), 'fake_std': round(fake_std, 4), 'real_mean': round(real_mean, 4), 'real_std': round(real_std, 4), }| 指标 | 观察重点 | 异常信号 |
|---|---|---|
| 生成图均值 | 是否接近真实图均值 | 长期偏离 0 附近,颜色整体偏亮或偏暗 |
| 生成图标准差 | 是否有足够对比度 | 标准差持续变小,生成图越来越平 |
| 判别器平均打分 | 生成图是否被一刀切判假 | 接近 0 且不回升,判别器过强 |
| 固定噪声生成图 | 每个 epoch 的变化 | 连续多轮几乎一样,模式崩溃 |
一个具体技巧:每轮把同一组 64 个噪声生成的图按 8×8 拼好,文件名带 epoch 编号,训练结束后用图片查看器快速翻一遍。比只看 loss 曲线直观得多,也方便截取中间过程放进报告。我第一次跑 DCGAN 时没固定噪声,每次看生成图都像开盲盒,后来把fixed_z存成文件、每个 epoch 复用,才真正看出学习率从 0.0002 调到 0.0001 后生成图边缘变稳了。从那以后我每次跑 DCGAN 都先把种子和固定噪声写进脚本,再谈调参。希望帮到你。
本文还有配套的精品资源,点击获取