# 1. 配置参数
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
batch_size = 128
lr = 0.0002
epochs = 50
z_dim = 100 # 噪声维度
image_size = 64 # 图像尺寸(DCGAN默认64x64)
sample_interval = 100 # 每多少个batch保存一次生成的图像
fixed_noise = torch.randn(16, z_dim).to(device) # 固定噪声,用于观察生成效果的稳定性
📦 训练过程参数
batch_size = 128
-
作用:每次训练迭代中使用的样本数量。
-
解释:DCGAN 通常使用较大的 batch_size(如 64~256),这有助于训练更稳定,生成图像质量更高。
batch_size(批次大小)是深度学习 / 机器学习中梯度下降优化过程 的核心超参数,指每次迭代(iteration)中模型同时处理的样本数量。
简单来说:
- 假设数据集有 1000 个样本,若
batch_size=100,则模型每轮(epoch)需要 10 次迭代才能看完所有样本; - 若
batch_size=1,则是「随机梯度下降(SGD)」(每次只看 1 个样本就更新参数); - 若
batch_size=1000(等于总样本数),则是「批量梯度下降(BGD)」(看完所有样本才更新参数); - 介于 1 和总样本数之间的情况,称为「小批量梯度下降(Mini-batch GD)」,也是最常用的方式。
lr = 0.0002
-
作用:学习率(learning rate)。
-
解释:DCGAN 原文中推荐的学习率是 0.0002,这个值比标准 GAN 的 0.001 更小,能让训练更稳定,避免模式崩溃(mode collapse)。
epochs = 50
-
作用:训练轮数。
-
解释:整个数据集将被遍历 50 次。DCGAN 在简单数据集(如 MNIST、CIFAR-10)上 50 轮通常足够,但在复杂数据集(如 ImageNet)上可能需要更多。
🧠 模型结构参数
z_dim = 100
-
作用:生成器输入的噪声向量维度。
-
解释:从标准正态分布中采样一个 100 维的向量,作为生成器的输入。这个向量决定了生成图像的"潜在语义"。
image_size = 64
-
作用:生成图像的分辨率。
-
解释 :DCGAN 默认生成 64×64 的图像。生成器和判别器的网络结构也是为这个尺寸设计的(如使用
ConvTranspose2d和Conv2d层逐步上采样/下采样)。
📸 日志与可视化参数
sample_interval = 100
-
作用:每训练多少个 batch 就保存一次生成图像。
-
解释:用于监控训练过程中的生成效果,观察生成器是否逐渐学会生成逼真图像。
fixed_noise = torch.randn(16, z_dim).to(device)
-
作用:固定一组噪声向量。
-
解释 :每次保存图像时都使用这同一个噪声输入,这样可以直观对比生成器在不同训练阶段的输出变化,观察生成效果的稳定性和改进过程。
# 3. 生成器(转置卷积:上采样,从噪声→64x64图像) class Generator(nn.Module): def __init__(self): super(Generator, self).__init__() self.model = nn.Sequential( # 输入:z_dim x 1 x 1 → 1024 x 4 x 4 nn.ConvTranspose2d(z_dim, 1024, 4, 1, 0, bias=False), nn.BatchNorm2d(1024), nn.ReLU(True), # 1024 x 4 x 4 → 512 x 8 x 8 nn.ConvTranspose2d(1024, 512, 4, 2, 1, bias=False), nn.BatchNorm2d(512), nn.ReLU(True), # 512 x 8 x 8 → 256 x 16 x 16 nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False), nn.BatchNorm2d(256), nn.ReLU(True), # 256 x 16 x 16 → 128 x 32 x 32 nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False), nn.BatchNorm2d(128), nn.ReLU(True), # 128 x 32 x 32 → 1 x 64 x 64(MNIST单通道) nn.ConvTranspose2d(128, 1, 4, 2, 1, bias=False), nn.Tanh() # 输出[-1,1] ) def forward(self, z): return self.model(z.view(-1, z_dim, 1, 1)) # 噪声reshape为4D张量
🎯 总体目标
把形状为 (B, 100) 的噪声 → (B, 1, 64, 64) 的图像,像素值范围 [-1, 1]。
| 层 | 输入尺寸 | 输出尺寸 | 关键参数 | 说明 |
|---|---|---|---|---|
ConvT2d(100→1024, 4, 1, 0) |
(B,100,1,1) |
(B,1024,4,4) |
kernel=4, stride=1, padding=0 | 把 1×1 直接"拉"成 4×4,通道数暴增,相当于"学会"如何把 100 维向量映射到高维空间。 |
ConvT2d(1024→512, 4, 2, 1) |
(B,1024,4,4) |
(B,512,8,8) |
stride=2, padding=1 | 经典"上采样×2",宽高翻倍,通道减半。 |
ConvT2d(512→256, 4, 2, 1) |
(B,512,8,8) |
(B,256,16,16) |
同上 | 继续×2。 |
ConvT2d(256→128, 4, 2, 1) |
(B,256,16,16) |
(B,128,32,32) |
同上 | 继续×2。 |
ConvT2d(128→1, 4, 2, 1) |
(B,128,32,32) |
(B,1,64,64) |
同上 | 最后一层把通道压到 1,得到灰度图。 |
Tanh() |
把像素压到 [-1,1],与归一化到 [-1,1] 的真实图像对齐。 |
🧮 输出尺寸速算公式(ConvTranspose2d)
对于 kernel=4, stride=2, padding=1 的层:
H_out = (H_in − 1) × stride − 2 × padding + kernel
= (H_in − 1) × 2 − 2 + 4
= H_in × 2
# 4. 判别器(卷积:下采样,从图像→概率)
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
# 输入:1 x 64 x 64 → 128 x 32 x 32
nn.Conv2d(1, 128, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# 128 x 32 x 32 → 256 x 16 x 16
nn.Conv2d(128, 256, 4, 2, 1, bias=False),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2, inplace=True),
# 256 x 16 x 16 → 512 x 8 x 8
nn.Conv2d(256, 512, 4, 2, 1, bias=False),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2, inplace=True),
# 512 x 8 x 8 → 1024 x 4 x 4
nn.Conv2d(512, 1024, 4, 2, 1, bias=False),
nn.BatchNorm2d(1024),
nn.LeakyReLU(0.2, inplace=True),
# 1024 x 4 x 4 → 1 x 1 x 1(概率)
nn.Conv2d(1024, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, x):
return self.model(x).view(-1, 1) # 展平为batch_size x 1
这段判别器就是生成器的"镜像"------用普通卷积一步步把 64×64 的图像压成 1 个概率值,告诉你"这张图有多真"。下面逐层拆给你看。
🎯 总体目标
把 (B, 1, 64, 64) 的图像 → (B, 1) 的标量,值域 [0, 1],越大越真。
| 层 | 输入尺寸 | 输出尺寸 | 关键参数 | 说明 |
|---|---|---|---|---|
Conv2d(1→128, 4, 2, 1) |
(B,1,64,64) |
(B,128,32,32) |
stride=2, padding=1 | 第一层不带 BN,避免"早期震荡";LeakyReLU(0.2) 防止梯度死亡。 |
Conv2d(128→256, 4, 2, 1) |
(B,128,32,32) |
(B,256,16,16) |
同上 | 宽高再×½,通道翻倍。 |
Conv2d(256→512, 4, 2, 1) |
(B,256,16,16) |
(B,512,8,8) |
同上 | 继续×½。 |
Conv2d(512→1024, 4, 2, 1) |
(B,512,8,8) |
(B,1024,4,4) |
同上 | 最后一层下采样,得到 4×4 空间维度。 |
Conv2d(1024→1, 4, 1, 0) |
(B,1024,4,4) |
(B,1,1,1) |
kernel=4, stride=1, padding=0 | "全局卷积"直接把 4×4 压成 1×1,等价于全连接但参数量更少。 |
Sigmoid() |
把 logits 压到 [0, 1],得到"真伪概率"。 |
🧮 输出尺寸速算公式(Conv2d)
对于 kernel=4, stride=2, padding=1 的层:
H_out = floor((H_in + 2×padding − kernel) / stride) + 1
= floor((H_in + 2 − 4) / 2) + 1
= floor((H_in − 2) / 2) + 1
= H_in / 2
⚙️ 设计细节
-
第一层不加 BN
DCGAN 原文建议:第一层直接接 LeakyReLU,避免"早期震荡"+"模式崩溃"。
-
LeakyReLU(0.2)
负斜率 0.2,比 ReLU 更稳健,防止判别器"过度自信"导致梯度消失。
-
通道数"逐级翻倍"
128→256→512→1024,与生成器"逐级减半"对称,容量匹配。
-
最后一层
Sigmoid输出可解释为概率,配合
BCELoss或BCEWithLogitsLoss(若去掉 Sigmoid)均可。 -
展平
view(-1, 1)把
(B,1,1,1)→(B,1),方便与标签计算二元交叉熵。# 5. 初始化模型、损失函数和优化器 generator = Generator().to(device) discriminator = Discriminator().to(device) criterion = nn.BCELoss() # 二分类交叉熵损失 optimizer_g = optim.Adam(generator.parameters(), lr=lr, betas=(0.5, 0.999)) # DCGAN推荐的betas optimizer_d = optim.Adam(discriminator.parameters(), lr=lr, betas=(0.5, 0.999)) # 标签平滑(可选,提高稳定性) real_label = 0.9 # 真实样本标签设为0.9而非1.0,避免判别器过于自信 fake_label = 0.0 # 记录损失,用于后续可视化 d_losses = [] g_losses = []
1. 模型搬到 GPU
generator = Generator().to(device)
discriminator = Discriminator().to(device)
-
作用:把网络参数、中间激活全部放到显存,加速训练。
-
注意 :后续输入/标签/噪声也要
.to(device),否则会在 CPU 上报错。
2. 损失函数
criterion = nn.BCELoss()
-
全称:Binary Cross Entropy,二元交叉熵。
-
数学形式:
L = − [y·log(p) + (1−y)·log(1−p)]-
y=1(真图)时,希望p→1; -
y=0(假图)时,希望p→0。
-
-
配套要求 :网络最后一层必须是
Sigmoid,否则数值不稳定(也可用BCEWithLogitsLoss省去 Sigmoid)。
3. 优化器
optimizer_g = optim.Adam(generator.parameters(), lr=lr, betas=(0.5, 0.999))
optimizer_d = optim.Adam(discriminator.parameters(), lr=lr, betas=(0.5, 0.999))
-
lr=0.0002:DCGAN 原文经验值,比常见 0.001 小,防止更新过猛。
-
betas=(0.5, 0.999):
-
β₁=0.5(默认 0.9)→ 让动量更"慢",减少震荡,对抗训练更稳。
-
β₂=0.999 保持默认即可。
-
-
两个独立优化器:生成器和判别器各自更新,互不干扰。
4. 标签平滑(Label Smoothing)
real_label = 0.9
fake_label = 0.0
-
目的:让判别器不要"过度自信",缓解梯度消失、模式崩溃。
-
原理:真图不再给硬标签 1.0,而是 0.9;假图保持 0.0。
-
等价操作:也可以在 [0.7, 1.2] 区间随机采样,但 0.9 已足够有效。
5. 损失记录
d_losses = []
g_losses = []
作用 :每轮把 loss.item() 追加进去,训练结束后可画图:
plt.plot(d_losses, label='D')
plt.plot(g_losses, label='G')
plt.legend()
# 6. 完整训练循环
print(f"开始训练,使用设备:{device}")
for epoch in range(epochs):
epoch_d_loss = 0.0
epoch_g_loss = 0.0
# 使用tqdm显示进度条
for i, (real_imgs, _) in enumerate(tqdm(train_loader, desc=f"Epoch {epoch + 1}/{epochs}")):
batch_size = real_imgs.size(0)
real_imgs = real_imgs.to(device)
#######################################
# 第一步:训练判别器(最大化D(real)=1,D(fake)=0)
#######################################
discriminator.zero_grad()
# 1. 训练真实样本
label = torch.full((batch_size, 1), real_label, device=device) # 真实标签
output = discriminator(real_imgs) # D(real)
d_loss_real = criterion(output, label) # 真实样本损失
d_loss_real.backward() # 反向传播计算梯度
d_x = output.mean().item() # 记录D(real)的平均值(理想接近1)
# 2. 训练生成的假样本
noise = torch.randn(batch_size, z_dim, device=device) # 生成随机噪声
fake_imgs = generator(noise) # G(noise)生成假样本
label.fill_(fake_label) # 假样本标签
output = discriminator(fake_imgs.detach()) # D(fake),detach()避免更新G的梯度
d_loss_fake = criterion(output, label) # 假样本损失
d_loss_fake.backward() # 反向传播计算梯度
d_g_z1 = output.mean().item() # 记录D(fake)的平均值(理想接近0)
# 3. 总判别器损失 + 更新参数
d_loss = d_loss_real + d_loss_fake
optimizer_d.step()
#######################################
# 第二步:训练生成器(最大化D(G(z))=1)
#######################################
generator.zero_grad()
label.fill_(real_label) # 生成器希望D(fake)被判断为真实样本(标签=1)
output = discriminator(fake_imgs) # D(G(z))
g_loss = criterion(output, label) # 生成器损失
g_loss.backward() # 反向传播计算梯度
d_g_z2 = output.mean().item() # 记录D(G(z))的平均值(理想接近1)
# 更新生成器参数
optimizer_g.step()
#######################################
# 记录损失和日志输出
#######################################
epoch_d_loss += d_loss.item()
epoch_g_loss += g_loss.item()
# 每sample_interval个batch,保存生成的图像
if (i + 1) % sample_interval == 0:
# 生成固定噪声的图像(便于观察训练稳定性)
generator.eval() # 切换到评估模式
with torch.no_grad():
fixed_fake = generator(fixed_noise)
# 反归一化:从[-1,1]转回[0,1]
fixed_fake = fixed_fake.cpu().detach().numpy() * 0.5 + 0.5
# 绘制16张生成的图像
plt.figure(figsize=(4, 4))
for j in range(16):
plt.subplot(4, 4, j + 1)
plt.imshow(fixed_fake[j].squeeze(), cmap='gray')
plt.axis('off')
plt.suptitle(f"Epoch {epoch + 1}, Batch {i + 1}")
print(f"---- 准备保存:epoch_{epoch + 1}_batch_{i + 1}.png ----")
plt.savefig(os.path.join(save_dir, f"epoch_{epoch + 1}_batch_{i + 1}.png"))
plt.close()
generator.train() # 切换回训练模式
# 计算每个epoch的平均损失
avg_d_loss = epoch_d_loss / len(train_loader)
avg_g_loss = epoch_g_loss / len(train_loader)
d_losses.append(avg_d_loss)
g_losses.append(avg_g_loss)
# 输出每个epoch的日志
print(f"[Epoch {epoch + 1}] D Loss: {avg_d_loss:.4f}, G Loss: {avg_g_loss:.4f}")
print(f" D(real): {d_x:.4f}, D(fake before G update): {d_g_z1:.4f}, D(fake after G update): {d_g_z2:.4f}")
| 阶段 | 代码片段 | 作用 & 关键细节 |
|---|---|---|
| 0. 准备 | real_imgs = real_imgs.to(device) |
千万别忘了,否则 GPU/CPU 混用直接报错。 |
| 1. 训练判别器 | discriminator.zero_grad() |
清空旧梯度,避免累积。 |
label.fill_(real_label) → criterion(output, label) |
真图希望 D 输出 ≈ 0.9(标签平滑)。 | |
fake_imgs.detach() |
关键:截断生成器梯度,只更新 D,不然 G 也会被"拉"向假图。 | |
d_loss_real + d_loss_fake |
一次反向就能同时更新,省一次 optimizer_d.step()。 |
|
| 2. 训练生成器 | generator.zero_grad() |
同样清空梯度。 |
label.fill_(real_label) |
精髓:G 希望 D 把假图当成真图,所以标签是 1! | |
不再 .detach() |
这次要让梯度流回 G,才能更新生成器。 | |
| 3. 记录指标 | d_x / d_g_z1 / d_g_z2 |
三个"置信度"瞬时快照,打印出来就能一眼看出有没有崩 : - d_x 真图置信度,理想 0.9 左右; - d_g_z1 假图置信度(G 更新前),理想 0.0 左右; - d_g_z2 假图置信度(G 更新后),应该比 d_g_z1 高,否则 G 没学到东西。 |
| 4. 可视化 | generator.eval() + no_grad() |
推断模式:关闭 Dropout/BN 的训练行为,省显存、加速。 |
*0.5+0.5 |
把 [-1,1] 的 Tanh 输出拉回 [0,1] 才能 imshow。 |
|
plt.savefig(...) |
每 sample_interval 存一次,天然就是训练动画帧 ,后期可 ffmpeg 合成 gif。 |
|
| 5. epoch 日志 | avg_d_loss / avg_g_loss |
画损失曲线用,若 D 损失迅速降到 0 而 G 损失飙升 → 判别器太强,要减小 lr 或降低 D 迭代次数。 |
⚠️ 常见踩坑提醒
-
忘记
.to(device)运行直接报
Expected object of device type cuda but got cpu。 -
漏掉
detach()会导致 G 被"顺带"更新,判别器变成生成器的"助教",训练崩掉。
-
BN +
generator.eval()忽略如果可视化阶段不关
train(),BN 会拿当前 batch 统计量,生成图出现诡异噪点。 -
标签平滑值写反
把
real_label=0.9误设成0.1,D 会反向学习,损失爆炸。 -
学习率太高
若出现
D loss=0.000, G loss 飙升→ 把lr降到 0.0001 或 每轮 D/G 更新比例从 1:1 改成 1:2。
✅ 一句话总结
这段循环把"真/假采样 → 损失计算 → 参数更新 → 指标监控 → 可视化快照 "全打包,打印的三个置信度 就是 DCGAN 的"体检报告":只要 D(x)≈0.9、D(G(z)) 从 0 慢慢涨到 0.5 左右,说明 G 正在"骗"成功,训练就健康。