PyTorch实战DCGAN:从原理到代码实现图像生成

📅 发布时间:2026/8/26 21:43:38
PyTorch实战DCGAN:从原理到代码实现图像生成 1. 项目概述从GAN到DCGAN的实战跨越在生成对抗网络GAN的世界里DCGANDeep Convolutional Generative Adversarial Networks绝对算得上是一个里程碑式的存在。几年前当我第一次尝试用原始的GAN生成人脸图片时结果总是一片模糊的噪声模型训练也极不稳定动不动就崩溃。直到DCGAN的出现它通过引入卷积神经网络CNN的结构让生成器Generator和判别器Discriminator都变得“专业”起来终于能稳定地生成清晰、有意义的图像了。今天我们就用PyTorch这个深度学习框架亲手搭建一个DCGAN模型目标很明确让它学会生成手写数字MNIST数据集或者人脸图像CelebA数据集。这不仅仅是跑通一个教程更是理解GAN核心思想、掌握稳定训练技巧、并看到“无中生有”魔力的绝佳实践。无论你是刚入门GAN的新手还是想深化PyTorch工程能力的老兵这个从零到一的搭建过程都会让你对生成式模型有更扎实的把握。2. DCGAN核心原理与架构设计解析2.1 GAN的基本思想与DCGAN的改进要理解DCGAN必须先吃透GAN的基本博弈论思想。你可以把它想象成一个造假币的罪犯生成器G和一个经验老道的警察判别器D。G的目标是造出以假乱真的假币骗过DD的目标是火眼金睛准确分辨出真币和假币。两者在对抗中不断进化最终G能造出连专家都难以辨别的“超级假币”也就是我们想要的生成数据。原始的GAN使用全连接网络处理图像这种高维、具有空间局部相关性的数据效率很低生成的图片质量也差。DCGAN的核心改进就是把G和D都换成了卷积网络生成器G输入是一个随机噪声向量比如100维通过一系列转置卷积Transposed Convolution层逐步“放大”这个向量最终生成一张完整的图像如64x64x3。这个过程可以理解为从一个模糊的概念噪声逐渐描绘出细节。判别器D输入是一张图像真图或假图通过一系列标准的卷积层逐步提取特征并降低空间尺寸最后通过一个全连接层输出一个概率值0到1表示它认为这张图是真实图片的概率。DCGAN论文中提出了几个关键的结构性约束这些是模型稳定的基石去除池化层在D中使用带步长stride的卷积代替池化进行下采样在G中使用转置卷积进行上采样。这让网络可以自己学习最优的采样方式。在G和D中都使用批量归一化BatchNorm这有助于稳定训练缓解梯度问题但论文指出不要在D的输入层和G的输出层使用。去除全连接层除了D的最后一层和G的第一层中间全部使用卷积层减少了参数提升了特征提取能力。激活函数选择G的输出层使用Tanh将像素值映射到[-1, 1]D的输出层使用Sigmoid输出概率。G和D中间层都使用ReLUG和LeakyReLUD。2.2 我们的模型架构设计基于上述原则我们来设计一个生成64x64灰度图像的DCGAN。假设我们的输入噪声向量nz100。生成器网络设计它的工作是把一个100维的随机噪声变成一张64x64的图片。想象一下雕塑的过程先有一块大致的形状低分辨率特征然后不断雕刻出细节。第一层全连接层将100维噪声投影到一个足够大的特征图例如映射成1024个7x7的特征图种子。核心部分4层转置卷积层。每一层都进行上采样尺寸翻倍同时减少通道数最终达到目标尺寸和通道数。第1层转置卷积输入1024x7x7- 输出512x14x14第2层转置卷积输入512x14x14- 输出256x28x28第3层转置卷积输入256x28x28- 输出128x56x56第4层转置卷积输入128x56x56- 输出1x64x64(灰度图所以是1个通道)除最后一层外每个转置卷积层后接BatchNorm2d和ReLU激活。最后一层后接Tanh激活将像素值规范到[-1, 1]。判别器网络设计它的工作是对输入的64x64图片进行“审判”输出它是真接近1还是假接近0。像一个不断聚焦的审查官。输入1x64x64的图像。核心部分4层卷积层。每一层进行下采样尺寸减半同时增加通道数以提取更深层次的特征。第1层卷积输入1x64x64- 输出128x32x32(使用stride2下采样)第2层卷积输入128x32x32- 输出256x16x16第3层卷积输入256x16x16- 输出512x8x8第4层卷积输入512x8x8- 输出1024x4x4将最后的特征图展平送入一个全连接层输出一个标量。除第一层外每个卷积层后接BatchNorm2d和LeakyReLU激活负斜率设为0.2。第一层后只接LeakyReLU。最后一层全连接层后接Sigmoid。注意这里有一个关键细节原始DCGAN论文中判别器最后是一个全连接层接Sigmoid。但在后续实践中很多人发现去掉最后的Sigmoid让判别器输出一个未归一化的分数logits并在计算损失时使用带有Logits参数的BCE损失BCEWithLogitsLoss在数值上更稳定。这是PyTorch实战中一个重要的技巧。3. 环境准备与数据加载3.1 PyTorch与依赖库安装首先确保你的环境已经就绪。推荐使用Anaconda管理Python环境它能很好地处理包依赖。# 创建一个新的conda环境可选但推荐 conda create -n dcgan_pytorch python3.8 conda activate dcgan_pytorch # 安装PyTorch请根据你的CUDA版本前往官网选择对应命令 # 例如对于CUDA 11.8可以使用 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 # 安装其他可能用到的工具库 pip install matplotlib numpy pandas jupyter notebook验证安装import torch print(torch.__version__) print(torch.cuda.is_available()) # 如果支持GPU应返回True3.2 数据集处理与DataLoader配置我们以MNIST数据集为例但需要将其处理成适合DCGAN的格式。原始MNIST是28x28我们需要上采样到64x64并且将像素值归一化到[-1, 1]以匹配生成器Tanh的输出。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义图像预处理变换 image_size 64 batch_size 128 transform transforms.Compose([ transforms.Resize(image_size), # 调整尺寸到64x64 transforms.CenterCrop(image_size), # 中心裁剪确保是正方形 transforms.ToTensor(), # 转换为Tensor范围[0,1] transforms.Normalize((0.5,), (0.5,)) # 将[0,1]归一化到[-1,1]。对于灰度图均值和标准差都是0.5 ]) # 下载并加载训练数据集 dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) # 创建数据加载器 dataloader DataLoader(dataset, batch_sizebatch_size, shuffleTrue, num_workers2, pin_memoryTrue) # 检查一个批次的数据 real_batch next(iter(dataloader)) print(f“图像Tensor形状: {real_batch[0].shape}”) # 应为 torch.Size([128, 1, 64, 64]) print(f“像素值范围: [{real_batch[0].min():.3f}, {real_batch[0].max():.3f}]”) # 应接近[-1, 1]关键点解析Normalize((0.5,), (0.5,))对于单通道灰度图(0.5,)表示均值0.5标准差0.5。计算过程(原始值 - 均值) / 标准差。原始值范围[0,1]减去0.5后变成[-0.5, 0.5]再除以0.5就变成了[-1, 1]。这与生成器输出层Tanh的范围完美匹配。num_workers用于并行数据加载的子进程数可以加快数据读取速度尤其是在使用机械硬盘时。通常设置为CPU核心数。pin_memoryTrue当使用GPU时将数据锁页内存中可以加速从CPU到GPU的数据传输。实操心得如果你使用CelebA等人脸数据集变换管道transform通常会更复杂可能包括随机水平翻转RandomHorizontalFlip来增加数据多样性。对于彩色图像Normalize的参数应为((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))对应RGB三通道。4. 模型搭建生成器与判别器的PyTorch实现现在我们将设计图转化为PyTorch代码。我们会采用模块化的方式先定义生成器再定义判别器。4.1 生成器Generator代码实现生成器的核心是nn.ConvTranspose2d转置卷积。我们需要精心设计每一层的输入/输出通道数和步长以确保尺寸能精确地翻倍到64x64。import torch.nn as nn class Generator(nn.Module): def __init__(self, nz100, ngf64, nc1): 参数: nz: 噪声向量的长度 (输入 latent dim) ngf: 生成器特征图深度的基数 nc: 输出图像的通道数 (灰度图为1 RGB图为3) super(Generator, self).__init__() self.main nn.Sequential( # 输入是 Z, 将其映射到 (ngf*8) x 4 x 4 的特征图 nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, biasFalse), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # 状态尺寸: (ngf*8) x 4 x 4 nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # 状态尺寸: (ngf*4) x 8 x 8 nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # 状态尺寸: (ngf*2) x 16 x 16 nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf), nn.ReLU(True), # 状态尺寸: (ngf) x 32 x 32 nn.ConvTranspose2d(ngf, nc, 4, 2, 1, biasFalse), nn.Tanh() # 输出状态尺寸: (nc) x 64 x 64 ) def forward(self, input): # 输入input的形状应为 (batch_size, nz, 1, 1) # 这是因为ConvTranspose2d期望的输入是4D的: (N, C, H, W) return self.main(input) # 实例化并查看模型结构 netG Generator(nz100, ngf64, nc1) print(netG) # 测试生成器前向传播 test_noise torch.randn(4, 100, 1, 1) # 4个样本100维噪声 fake_images netG(test_noise) print(f“生成的假图像形状: {fake_images.shape}”) # 应为 torch.Size([4, 1, 64, 64])代码细节解读nn.ConvTranspose2d参数(in_channels, out_channels, kernel_size, stride, padding, bias)stride2和padding1是尺寸翻倍的关键组合。输出尺寸公式H_out (H_in - 1) * stride - 2*padding kernel_size。当H_in4, stride2, padding1, kernel_size4时H_out (4-1)*2 -2*1 4 8。biasFalse因为后面紧跟着BatchNorm层BatchNorm本身有可学习的偏移参数所以卷积层可以省略bias以减少参数并可能提升性能。nn.ReLU(True)inplaceTrue参数直接修改输入值以节省少量内存。4.2 判别器Discriminator代码实现判别器就是标准的卷积分类网络但最后一层输出一个标量。class Discriminator(nn.Module): def __init__(self, nc1, ndf64): 参数: nc: 输入图像的通道数 ndf: 判别器特征图深度的基数 super(Discriminator, self).__init__() self.main nn.Sequential( # 输入尺寸: (nc) x 64 x 64 nn.Conv2d(nc, ndf, 4, 2, 1, biasFalse), nn.LeakyReLU(0.2, inplaceTrue), # 状态尺寸: (ndf) x 32 x 32 nn.Conv2d(ndf, ndf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplaceTrue), # 状态尺寸: (ndf*2) x 16 x 16 nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplaceTrue), # 状态尺寸: (ndf*4) x 8 x 8 nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplaceTrue), # 状态尺寸: (ndf*8) x 4 x 4 nn.Conv2d(ndf * 8, 1, 4, 1, 0, biasFalse), # 输出一个标量logits形状: (batch_size, 1, 1, 1) # 我们不在这里加Sigmoid将在损失函数中处理 ) def forward(self, input): # 输入input形状: (batch_size, nc, H, W) output self.main(input) # 将输出从 (batch_size, 1, 1, 1) 压平为 (batch_size, 1) return output.view(-1, 1) # 实例化判别器 netD Discriminator(nc1, ndf64) print(netD) # 测试判别器前向传播 test_images torch.randn(4, 1, 64, 64) output netD(test_images) print(f“判别器输出形状logits: {output.shape}”) # 应为 torch.Size([4, 1])关键设计选择最后一层无Sigmoid如之前所述我们让判别器输出未归一化的分数logits。这允许我们使用BCEWithLogitsLoss它在内部将Sigmoid和BCE损失合并计算数值稳定性更高能有效防止在训练早期因为极端值导致的梯度饱和问题。LeakyReLU的负斜率论文推荐使用0.2。LeakyReLU允许负值有一个小的梯度这有助于缓解D过于强大导致G梯度消失的问题。5. 训练策略损失函数、优化器与训练循环这是DCGAN训练中最核心、最需要技巧的部分。一个不平衡的训练过程很容易导致模式崩溃生成器只生成几种相似的图片或训练发散。5.1 初始化、损失函数与优化器import torch.optim as optim # 设备配置 device torch.device(“cuda:0” if torch.cuda.is_available() else “cpu”) print(f“使用的设备: {device}”) # 将模型移动到设备 netG netG.to(device) netD netD.to(device) # 初始化权重 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) netG.apply(weights_init) netD.apply(weights_init) # 定义损失函数 - 使用带Logits的二元交叉熵 criterion nn.BCEWithLogitsLoss() # 定义优化器 lr 0.0002 # 学习率通常是一个较小的值 beta1 0.5 # Adam优化器的第一个动量衰减率论文推荐值 optimizerD optim.Adam(netD.parameters(), lrlr, betas(beta1, 0.999)) optimizerG optim.Adam(netG.parameters(), lrlr, betas(beta1, 0.999)) # 固定的噪声向量用于在训练过程中可视化生成器的进步 fixed_noise torch.randn(64, 100, 1, 1, devicedevice)初始化的重要性weights_init函数按照DCGAN论文的建议对卷积层权重使用均值为0、标准差为0.02的正态分布初始化对批归一化层的权重使用均值为1、标准差为0.02的正态分布偏置初始化为0。良好的初始化是训练稳定的第一步。fixed_noise在训练过程中定期用同一批噪声生成图片可以直观地看到生成器学习效果的演变而不是被每次不同的随机噪声干扰判断。5.2 核心训练循环详解训练遵循一个迭代过程先更新判别器D再更新生成器G。每一轮epoch遍历整个数据集。num_epochs 50 img_list [] G_losses [] D_losses [] iters 0 print(“开始训练...”) for epoch in range(num_epochs): # 遍历数据加载器 for i, data in enumerate(dataloader, 0): ############################ # (1) 更新判别器 D: 最大化 log(D(x)) log(1 - D(G(z))) ########################### # 用真实图片训练 netD.zero_grad() real_images data[0].to(device) batch_size real_images.size(0) # 创建标签真实图片标签为1平滑标签可设为0.9或1.0 label torch.full((batch_size, 1), 1.0, devicedevice) # 将真实图片输入判别器 output netD(real_images).view(-1, 1) # 计算判别器对真实图片的损失 errD_real criterion(output, label) errD_real.backward() D_x output.mean().item() # 平均判别器输出用于监控 # 用假图片训练 noise torch.randn(batch_size, 100, 1, 1, devicedevice) fake_images netG(noise) label.fill_(0.0) # 假图片标签为0 # 将假图片输入判别器并detach()以阻止梯度传到生成器 output netD(fake_images.detach()).view(-1, 1) errD_fake criterion(output, label) errD_fake.backward() D_G_z1 output.mean().item() # 判别器对假图的平均输出更新前 # 计算判别器总损失并更新其参数 errD errD_real errD_fake optimizerD.step() ############################ # (2) 更新生成器 G: 最大化 log(D(G(z))) ########################### netG.zero_grad() label.fill_(1.0) # 生成器的目标是让判别器认为假图是真的 # 注意这里我们再次将假图输入判别器但没有detach() output netD(fake_images).view(-1, 1) errG criterion(output, label) errG.backward() D_G_z2 output.mean().item() # 判别器对假图的平均输出更新G后 optimizerG.step() # 记录损失 G_losses.append(errG.item()) D_losses.append(errD.item()) # 每100个batch打印一次状态 if i % 100 0: print(f‘[{epoch}/{num_epochs}][{i}/{len(dataloader)}] ’ f‘Loss_D: {errD.item():.4f} Loss_G: {errG.item():.4f} ’ f‘D(x): {D_x:.4f} D(G(z)): {D_G_z1:.4f}/{D_G_z2:.4f}’) iters 1 # 每个epoch结束后用固定噪声生成图片并保存 with torch.no_grad(): fake netG(fixed_noise).detach().cpu() img_list.append(fake) print(“训练完成”)训练逻辑深度解析判别器更新两步第一步真图将真实图片输入D计算D(real)与标签1的损失。我们希望D对真图输出接近1。第二步假图用G生成假图注意这里使用了.detach()。将假图输入D计算D(fake)与标签0的损失。我们希望D对假图输出接近0。.detach()是关键它切断了假图到G的计算图使得这一步的梯度只更新D不更新G。将两步损失相加errD errD_real errD_fake然后optimizerD.step()更新D的参数。D的目标是最大化自己判断正确的概率对应损失函数的最小化。生成器更新一步清零G的梯度。注意这里我们再次将之前生成的fake_images未detach输入D。但这次标签是1这意味着我们“欺骗”D希望它对G生成的假图输出接近1。计算D(fake)与标签1的损失errG然后反向传播。由于fake_images未detach梯度会穿过D一直回溯到G。optimizerG.step()更新G的参数。G的目标是最小化log(1 - D(G(z)))或者说最大化log(D(G(z)))让D把自己生成的假图误判为真图。监控指标D(x)判别器对真实图片输出的平均值。理想情况下训练后期应接近1但不会正好是1。D(G(z))判别器对生成图片输出的平均值。第一个值更新D前应接近0第二个值更新G后应接近1。如果第二个值一直很低说明G没有进步如果两个值都很快趋近0.5说明D太弱或者两者达到了纳什均衡。重要注意事项标签平滑Label Smoothing在上述代码中我们对真实图片使用了硬标签1.0。一个常用的技巧是单边标签平滑即将真实标签设为略小于1的值如0.9而假标签保持为0。这可以防止判别器对真实图片过于自信输出极接近1的logits从而减轻其对生成器的梯度压制有时能带来更稳定的训练。你可以尝试修改label torch.full((batch_size, 1), 0.9, devicedevice)。6. 结果可视化、评估与模型保存训练完成后我们需要看看成果如何并保存模型以备后用。6.1 生成图像可视化import matplotlib.pyplot as plt import numpy as np import torchvision.utils as vutils # 绘制训练过程中的损失曲线 plt.figure(figsize(10,5)) plt.title(“Generator and Discriminator Loss During Training”) plt.plot(G_losses, label“G”) plt.plot(D_losses, label“D”) plt.xlabel(“iterations”) plt.ylabel(“Loss”) plt.legend() plt.show() # 可视化固定噪声生成的图片演变过程 fig, axes plt.subplots(2, 5, figsize(15,6)) axes axes.ravel() for idx in range(10): # 选择第0, 5, 10, … 45个epoch的结果 epoch_idx idx * 5 if epoch_idx len(img_list): break # 取第一个batch的第一张图片 ax axes[idx] ax.imshow(np.transpose(img_list[epoch_idx][0], (1, 2, 0)), cmap‘gray’) ax.set_title(f“Epoch {epoch_idx}”) ax.axis(‘off’) plt.tight_layout() plt.show() # 展示最终生成的一批图片 real_batch next(iter(dataloader)) plt.figure(figsize(8,8)) plt.axis(“off”) plt.title(“Real Images”) plt.imshow(np.transpose(vutils.make_grid(real_batch[0][:64], padding2, normalizeTrue).cpu(),(1,2,0))) plt.show() plt.figure(figsize(8,8)) plt.axis(“off”) plt.title(“Fake Images (Final)”) with torch.no_grad(): fake netG(fixed_noise).detach().cpu() plt.imshow(np.transpose(vutils.make_grid(fake, padding2, normalizeTrue),(1,2,0))) plt.show()6.2 模型评估与保存对于GAN没有像分类准确率那样明确的指标。常用定性目视和定量如FID, IS评估。这里我们先做简单的模型保存。# 保存生成器模型的状态字典 torch.save(netG.state_dict(), ‘./netG_epoch_{}.pth’.format(num_epochs)) # 保存判别器模型的状态字典 torch.save(netD.state_dict(), ‘./netD_epoch_{}.pth’.format(num_epochs)) # 如果需要保存整个模型包含结构但通常不推荐因为代码可能变动 # torch.save(netG, ‘./netG_full.pth’) print(“模型已保存。”) # 加载模型示例在另一个脚本中 def load_model(path, model_class, model_args): model model_class(**model_args) model.load_state_dict(torch.load(path)) model.eval() # 设置为评估模式 return model # 例如加载生成器 # loaded_netG load_model(‘./netG_epoch_50.pth’, Generator, {‘nz’:100, ‘ngf’:64, ‘nc’:1})7. 实战避坑指南与高级技巧经过多次实战我总结了一些让DCGAN训练更稳定、效果更好的关键点这些在官方教程里往往不会细说。7.1 常见问题与排查表问题现象可能原因排查与解决思路生成图片全是噪声或单一颜色1. 梯度消失/爆炸。2. 模型架构错误如层数、通道数。3. 学习率过高。4. 批归一化层在推理时未设为eval模式。1. 检查损失值是否变成NaN。使用梯度裁剪clip_grad_norm_。2. 逐层打印特征图尺寸确保与设计一致。3. 尝试更小的学习率如1e-4。4. 生成时调用netG.eval()。模式崩溃Mode Collapse生成器只产出少数几种图片。1. 判别器太强过早“击败”生成器。2. 损失函数设计问题。3. 优化器或学习率不合适。1. 增加G的训练次数例如每更新一次D更新两次G。2. 尝试WGAN-GP等改进的损失。3. 尝试不同的优化器如RMSprop或调整学习率。损失值震荡剧烈不收敛1. 学习率太大。2. 批次大小Batch Size太小。3. 网络架构不稳定。1. 显著降低学习率。2. 在显存允许范围内增大Batch Size如128, 256。3. 确保遵循DCGAN的架构约束如使用BatchNorm, 不用全连接。判别器损失迅速降为0判别器过于强大生成器学不到任何东西。1. 降低D的学习率或增加G的学习率。2. 在D中添加Dropout层增加噪声。3. 使用标签平滑如前所述。4. 暂时减弱D的能力如减少层数。生成图片有棋盘伪影转置卷积层步长和核大小不匹配导致的“重叠”问题。1. 确保核大小能被步长整除。例如kernel_size4, stride2, padding1是经典组合。2. 考虑使用最近邻上采样卷积代替转置卷积。7.2 提升生成质量的进阶技巧渐进式增长Progressive Growing这是后来提出的非常有效的技巧尤其对于生成高分辨率图像如1024x1024。核心思想是从低分辨率如4x4开始训练G和D稳定后逐步添加新的层来生成更高分辨率的图像。这极大地稳定了训练过程。虽然我们的DCGAN是64x64但这个思想在小模型上也有启发意义可以先训练一个生成32x32的简单模型收敛后再扩展成64x64。历史平均Historical Averaging在损失函数中加入一项惩罚当前模型参数与历史上参数平均值的偏差。这有助于稳定训练防止参数振荡。小批量判别Minibatch Discrimination让判别器不仅看单张图片还能看到整个批次图片的统计特征。这可以有效缓解模式崩溃因为生成器如果只生成一种图片批次内的统计特征会非常明显容易被D识破。实现方式是在D的中间层为每个样本计算一个基于批次内其他样本的特征向量并拼接到原有特征上。谱归一化Spectral Normalization一种更先进的权重归一化方法可以稳定判别器的训练通常能替代或减少对BatchNorm的依赖尤其在WGAN-GP中常用。在PyTorch中可以用torch.nn.utils.spectral_norm包装卷积层。两时间尺度更新规则TTUR为生成器和判别器设置不同的学习率。通常判别器的学习率可以设得比生成器低一些例如D的lr4e-4 G的lr1e-4让两者学习速度更平衡。7.3 从MNIST到CelebA数据与架构调整如果你想尝试生成彩色人脸如CelebA数据集需要做以下调整数据nc3RGB三通道。归一化参数改为transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))。可以加入transforms.RandomHorizontalFlip(p0.5)进行数据增强。模型将Generator和Discriminator初始化参数中的nc改为3。训练CelebA数据量更大、更复杂通常需要更多的epoch200和更深的网络更大的ngf,ndf。可能需要使用上述的进阶技巧来稳定训练。资源生成更高清图像128x128以上对显存要求很高。可能需要减小batch_size使用梯度累积或者尝试在CPU上训练极慢。搭建并训练一个DCGAN模型就像在平衡木上跳舞需要在生成器和判别器之间找到那个微妙的平衡点。这个过程充满了挑战但当你第一次看到模糊的噪声逐渐演变成清晰可辨的数字或人脸时那种成就感是无与伦比的。这次实战不仅仅是敲代码更是对对抗生成思想的一次深刻体验。我个人的体会是耐心调整超参数、仔细观察损失曲线和生成样本的演变比盲目跑更多轮次更重要。如果第一次效果不理想别灰心回头检查数据预处理、模型架构和训练逻辑往往问题就藏在某个细节里。最后生成的模型可以作为一个基础模块尝试去生成你感兴趣的特定类型的图片或者探索Conditional GAN给生成过程加上标签控制那将是另一个有趣的故事了。