GAN学习

复制代码
    # 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 的图像。生成器和判别器的网络结构也是为这个尺寸设计的(如使用 ConvTranspose2dConv2d 层逐步上采样/下采样)。

📸 日志与可视化参数

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

⚙️ 设计细节

  1. 第一层不加 BN

    DCGAN 原文建议:第一层直接接 LeakyReLU,避免"早期震荡"+"模式崩溃"。

  2. LeakyReLU(0.2)

    负斜率 0.2,比 ReLU 更稳健,防止判别器"过度自信"导致梯度消失。

  3. 通道数"逐级翻倍"

    128→256→512→1024,与生成器"逐级减半"对称,容量匹配。

  4. 最后一层 Sigmoid

    输出可解释为概率,配合 BCELossBCEWithLogitsLoss(若去掉 Sigmoid)均可。

  5. 展平 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 迭代次数

⚠️ 常见踩坑提醒

  1. 忘记 .to(device)

    运行直接报 Expected object of device type cuda but got cpu

  2. 漏掉 detach()

    会导致 G 被"顺带"更新,判别器变成生成器的"助教",训练崩掉。

  3. BN + generator.eval() 忽略

    如果可视化阶段不关 train(),BN 会拿当前 batch 统计量,生成图出现诡异噪点

  4. 标签平滑值写反

    real_label=0.9 误设成 0.1,D 会反向学习,损失爆炸。

  5. 学习率太高

    若出现 D loss=0.000, G loss 飙升 → 把 lr 降到 0.0001 或 每轮 D/G 更新比例从 1:1 改成 1:2。

✅ 一句话总结

这段循环把"真/假采样 → 损失计算 → 参数更新 → 指标监控 → 可视化快照 "全打包,打印的三个置信度 就是 DCGAN 的"体检报告":只要 D(x)≈0.9D(G(z)) 从 0 慢慢涨到 0.5 左右,说明 G 正在"骗"成功,训练就健康。

相关推荐
ziwu10 小时前
【岩石种类识别系统】Python+TensorFlow+Django+人工智能+深度学习+卷积神经网络算法
人工智能·深度学习·图像识别
AI即插即用10 小时前
即插即用系列 | CVPR SwiftFormer:移动端推理新王者!0.8ms 延迟下 ImageNet 78.5% 准确率,吊打 MobileViT
图像处理·人工智能·深度学习·目标检测·计算机视觉·cnn·视觉检测
CHANG_THE_WORLD10 小时前
Python 学习三 Python字符串拼接详解
开发语言·python·学习
其美杰布-富贵-李11 小时前
SpaceClaim流体域创建学习笔记
笔记·学习
ziwu11 小时前
【中草药识别系统】Python+TensorFlow+Django+人工智能+深度学习+卷积神经网络算法
人工智能·深度学习·图像识别
冬夜戏雪11 小时前
【java学习日记】【2025.12.6】【6/60】
学习
行云流水200011 小时前
青少年编程学习:考级与竞赛结合提升能力的方法
人工智能·学习·青少年编程
Blossom.11811 小时前
基于多智能体强化学习的云资源调度系统:如何用MARL把ECS成本打下来60%
人工智能·python·学习·决策树·机器学习·stable diffusion·音视频
Coding茶水间11 小时前
基于深度学习的苹果病害检测系统演示与介绍(YOLOv12/v11/v8/v5模型+Pyqt5界面+训练代码+数据集)
图像处理·人工智能·深度学习·yolo·目标检测·计算机视觉