
简介基于Python实现的生成对抗网络人脸生成与矫正实践资源面向深度学习初学者与GAN入门者。资源以GAN核心训练流程为主线演示了固定生成器训练判别器、从噪声采样并生成数据、计算二分类概率与损失函数等关键环节可帮助读者理解对抗训练原理并快速跑通人脸生成与矫正的简易示例。zip压缩包共4个文件包含2个Python脚本用于模型训练与推理、1个Markdown文档说明实现思路以及1份License授权文件整体体积仅9KB代码精简、便于逐行阅读和修改。已有1395人学习下载适合希望在实战中掌握生成对抗网络基础操作、并进一步尝试人脸生成方向的研究者或学生。1. 从「十张脸九张歪」说起GAN人脸生成为什么离不开矫正第一次用GAN跑人脸生成十有八九会遇到这种场面轮廓出来了、肤色也像那么回事但眼睛一高一低、嘴巴糊成一片、耳朵长在奇怪的位置。这不是模型没训练好而是GAN在低分辨率、有限数据下对人脸结构先验把握不足生成器学会了「像脸」的像素统计却没学会「人脸应当长什么样」。整条方案要解决的就是这件事先把脸大致生成出来再用一个独立的矫正模块把五官位置扶正、把模糊细节修干净。这个「生成 矫正」两阶段思路比单纯堆一个更大的GAN要省事得多也更容易定位问题是出在生成阶段还是矫正阶段。这篇教程面向两类人想快速跑通GAN人脸项目的新手以及已经训出模糊人脸但不知道怎么继续调的工程师。下面从选型讲起一路写到可运行的完整代码和避坑记录。2. GAN人脸生成与矫正的技术选型先想清楚要解决什么问题2.1 生成器与判别器的分工谁来画脸、谁来验收GAN的核心是两套网络互相较劲。生成器G拿一个随机噪声向量 z 作为输入输出一张图像判别器D拿一张图像作为输入输出一个「真/假」的置信度。训练时D要努力分辨出哪些是真实数据集里的脸、哪些是G生成的假脸G则要努力骗过D让它把假脸也判成真。两者对抗的结果是G生成的图像分布逐渐逼近真实数据分布。用人脸场景来说G是一个画师D是一个验收员。画师每画一张脸验收员就挑毛病眼睛间距不对、肤色不均匀、纹理像塑料。画师根据验收员的反馈不断改直到验收员挑不出毛病。这个机制在数学上对应一个最小最大博弈min_G max_D E[log D(x)] E[log(1 - D(G(z)))]其中 x 是真实人脸图像z 是采样噪声。训练过程会交替更新D和G而不是先训完一个再训另一个。理解这个博弈的关键是「信号」D给G的梯度是唯一的学习信号。如果D太弱G得不到有效反馈生成结果会一直糊如果D太强G的梯度会迅速消失或爆炸直接导致训练震荡。人脸又是所有图像生成任务里结构约束最强的类别之一两眼连线、鼻子中轴、嘴的位置都有固定的相对关系人类对脸部的细微不对称极其敏感。这也是为什么人脸生成比风景、纹理生成更容易暴露GAN的缺陷。2.2 矫正的两种路线几何对齐与像素修复选哪个「矫正」这个动作在下文里被拆成两个层面。第一个层面是几何矫正解决「五官位置错」的问题检测出眼睛、鼻子、嘴等关键点计算当前人脸与标准模板之间的旋转、缩放和位移然后用相似变换把人脸扳回正位。这只做结构上的对齐不做像素修补。第二个层面是像素级修复解决「局部糊和不对称」的问题用一个残差自编码网络学习生成图到理想人脸图之间的修正量把模糊的眼睛、发虚的嘴唇、塑料感的皮肤纹理修干净。这两个层面并不是二选一的关系。几何矫正做的是「宏观扳正」但它对模糊和伪影无能为力像素修复做的是「微观修补」但如果输入的人脸本身歪着修复网络还得额外学好几何变换负担会大很多。常见做法是先几何矫正把人脸统一送到标准姿态再用修复网络只学纹理和细节的修正。这样两个模块各管一段问题定位也清晰输出歪了就先查关键点检测和仿射变换输出糊了再查修复网络的结构和损失。2.3 为什么第一版用DCGAN而不是StyleGAN人脸生成领域现在质量最高的方案基本都基于StyleGAN系列架构但第一版项目我仍然建议用DCGAN。原因有三一是DCGAN结构最简单生成器就是全连接加转置卷积的堆叠判别器就是卷积下采样代码量在一个文件里就能写完二是显存和训练时长友好64×64分辨率下普通显卡十几分钟就能看到初步结果三是DCGAN的失败模式已经被反复记录过出了问题时社区里能查到的排查经验最多。StyleGAN类的架构引入映射网络、风格注入、噪声输入等机制调参空间大新手很难判断是数据问题还是参数问题。等DCGAN这套「生成 矫正」的pipeline跑通、你也知道了损失曲线长什么样、生成结果哪里容易崩再迁移到高分辨率架构时矫正模块可以基本不动只换生成器。这个顺序能避免「一上来就调StyleGAN调了两周还不知道问题在哪」的常见窘境。3. 数据准备与训练环境把数据集喂给GAN之前的三件事3.1 数据集目录组织与划分脚本人脸生成最常用的公开数据集是CelebA包含二十多万张明星人脸图图像经过初步对齐五官基本居中这对「生成后再矫正」的pipeline非常友好。下载解压后目录里是一张大图裁剪好的jpg序列文件名是数字编号。第一步是把它按训练集和验证集划分开并且在代码里用路径对象统一管理别用硬编码字符串。from pathlib import Path import shutil import random data_root Path(data/celebA/img_align_celeba) train_dir Path(data/celebA/train) val_dir Path(data/celebA/val) train_dir.mkdir(parentsTrue, exist_okTrue) val_dir.mkdir(parentsTrue, exist_okTrue) images sorted(data_root.glob(*.jpg)) random.seed(42) random.shuffle(images) split_idx int(len(images) * 0.95) for img in images[:split_idx]: shutil.copy(str(img), str(train_dir / img.name)) for img in images[split_idx:]: shutil.copy(str(img), str(val_dir / img.name))这段脚本把95%的图分到训练目录5%分到验证目录。要注意的是random.shuffle之前先设置随机种子否则每次划分结果不一样后续对比实验的基线就乱了。验证集在GAN训练里不参与损失计算它的主要用途是训练结束后检查生成结果的多样性以及用来算FID分数。3.2 图像预处理中心裁剪、归一化与数据增强GAN对输入图像的预处理比分类任务更敏感。分类网络通常用ImageNet的均值和标准差做标准化但GAN生成器的输出层一般用Tanh输出值域是[-1, 1]所以判别器的输入也必须落在[-1, 1]区间否则判别器看到的真实图像和生成图像分布不一致训练压根起不来。from torchvision import transforms transform transforms.Compose([ transforms.CenterCrop(128), transforms.Resize(64), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ])CenterCrop(128)先把原图中心区域裁成128×128去掉边缘可能出现的背景干扰再Resize到64×64这是DCGAN一个常见配置。Normalize用0.5均值、0.5标准差作用是把[0,1]区间的像素值映射到[-1,1]和生成器Tanh输出对齐。RandomHorizontalFlip是唯一的数据增强人脸基本左右对称翻转后的样本依然真实相当于数据量翻倍但要注意翻转会交换左右眼的位置如果后续关键点检测用到眼别信息推理时要关闭这个增强。3.3 DataLoader参数批次大小与worker怎么设数据加载器直接影响训练速度和稳定性。批次大小不仅决定显存占用还在GAN训练里影响判别器的梯度质量——批次太小判别器每个batch估计的分布偏差大损失容易剧烈抖动批次太大显存吃紧单步迭代时间变长。from torch.utils.data import DataLoader, Dataset from PIL import Image import torch class FaceDataset(Dataset): def __init__(self, root_dir, transformNone): self.paths sorted(Path(root_dir).glob(*.jpg)) self.transform transform def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) if self.transform: img self.transform(img) return img train_dataset FaceDataset(train_dir, transformtransform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue, drop_lastTrue)drop_lastTrue是很容易被忽略的参数。如果数据量不能被批次大小整除最后一个batch只剩一二十张图用它更新判别器和生成器会让损失曲线出现周期性的跳动点给排查训练震荡制造干扰。pin_memoryTrue在GPU训练时能减少数据从内存拷贝到显存的开销配合num_workers4基本能保证数据加载不是瓶颈。4. 完整训练代码生成器、判别器与人脸矫正模块的一次跑通4.1 生成器结构五层转置卷积与输出层激活生成器输入一个100维的噪声向量输出一张64×64×3的图像。DCGAN的典型做法是用转置卷积逐层把空间尺寸翻倍通道数逐层减半最后用Tanh把输出值域压到[-1,1]。import torch.nn as nn class Generator(nn.Module): def __init__(self, z_dim100): super().__init__() self.fc nn.Linear(z_dim, 512 * 4 * 4) self.net nn.Sequential( nn.ConvTranspose2d(512, 256, 4, 2, 1), nn.BatchNorm2d(256), nn.ReLU(True), nn.ConvTranspose2d(256, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.ReLU(True), nn.ConvTranspose2d(128, 64, 4, 2, 1), nn.BatchNorm2d(64), nn.ReLU(True), nn.ConvTranspose2d(64, 3, 4, 2, 1), nn.Tanh(), ) def forward(self, z): x self.fc(z).view(z.size(0), 512, 4, 4) return self.net(x)结构逻辑是从4×4的512通道特征图开始经过四次转置卷积放大到64×64通道数从512降到3。每一次ConvTranspose2d的kernel_size4、stride2、padding1正好让空间尺寸翻倍。中间接BatchNorm2d和ReLU是DCGAN生成器的标配BatchNorm能稳定训练ReLU负责引入非线性。最后一层用Tanh而不是ReLU因为输出图像需要完整的[-1,1]值域。fc层把100维噪声映射到4×4×512的初始特征这个初始特征图是生成人脸的最小骨架它的质量直接影响最终面部结构。4.2 判别器结构卷积下采样与LeakyReLU判别器是生成器的镜像输入一张64×64×3的图像输出一个标量logit。与生成器不同判别器用带泄露的LeakyReLU激活避免负区间梯度全零导致神经元死亡。class Discriminator(nn.Module): def __init__(self): super().__init__() self.net nn.Sequential( nn.Conv2d(3, 64, 4, 2, 1), nn.LeakyReLU(0.2, True), nn.Conv2d(64, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, True), nn.Conv2d(128, 256, 4, 2, 1), nn.BatchNorm2d(256), nn.LeakyReLU(0.2, True), nn.Conv2d(256, 1, 4, 1, 0), ) def forward(self, x): return self.net(x).view(-1)最后两层值得单独说明Conv2d(256, 1, 4, 1, 0)配合kernel_size4、stride1、padding0正好把256×8×8的特征图收缩成1×1×1等价于一个全局判断。输出不加Sigmoid直接返回logit配合BCEWithLogitsLoss使用数值上更稳定。判别器的Conv2d都没有加padding补边空间尺寸从64一路降到8再降到1卷积核始终覆盖有效区域不会出现边缘特征被零填充污染的问题。4.3 训练循环与损失判别器先行、生成器后补GAN的训练顺序有讲究每个迭代里先更新判别器让D尽量区分真图和假图再更新生成器让G骗过刚更新完的D。每一步都只做一次反向传播不追求单步内D完全收敛否则G的梯度信号会消失。import torch import torch.nn.functional as F device torch.device(cuda if torch.cuda.is_available() else cpu) G Generator(z_dim100).to(device) D Discriminator().to(device) corrector FaceCorrector().to(device) # 4.4节定义 lr_g, lr_d 2e-4, 1e-4 opt_g torch.optim.Adam(G.parameters(), lrlr_g, betas(0.5, 0.999)) opt_d torch.optim.Adam(D.parameters(), lrlr_d, betas(0.5, 0.999)) criterion torch.nn.BCEWithLogitsLoss() for epoch in range(120): for real_imgs in train_loader: real_imgs real_imgs.to(device) batch real_imgs.size(0) z torch.randn(batch, 100, devicedevice) # 更新判别器 fake_imgs G(z).detach() real_logits D(real_imgs) fake_logits D(fake_imgs) d_loss criterion(real_logits, torch.ones_like(real_logits)) \ criterion(fake_logits, torch.zeros_like(fake_logits)) opt_d.zero_grad() d_loss.backward() opt_d.step() # 更新生成器 fake_imgs G(z) fake_logits D(fake_imgs) g_loss criterion(fake_logits, torch.ones_like(fake_logits)) opt_g.zero_grad() g_loss.backward() opt_g.step()几个细节值得注意。生成器输入z每次都重新采样但同一个迭代内更新D和G时用的是同一个z这是标准做法保证两次更新面对的是同一批样本。fake_imgs在更新判别器时要detach切断生成器的梯度回传否则D的反向传播会把梯度同时流进G破坏先后逻辑。BCEWithLogitsLoss内部自带Sigmoid所以判别器输出logit即可。4.4 人脸矫正模块仿射对齐 残差修复网络这是整个方案的核心。生成器训完后直接输出的图大概率存在结构歪斜。矫正模块第一步是几何矫正借助现成的关键点检测器拿到两眼中心坐标计算旋转角和缩放比例用相似变换把脸扳正。import cv2 import numpy as np def geometric_correction(img_np, left_eye, right_eye, output_size64): # img_np: 0-255范围的RGB图 # left_eye, right_eye: 检测器返回的左右眼坐标形状为 (2,) eye_center (left_eye right_eye) / 2.0 dx right_eye[0] - left_eye[0] dy right_eye[1] - left_eye[1] angle np.degrees(np.arctan2(dy, dx)) # 目标模板里双眼距离占图宽的35% target_dist output_size * 0.35 cur_dist np.sqrt(dx**2 dy**2) scale target_dist / max(cur_dist, 1e-6) M cv2.getRotationMatrix2D(tuple(eye_center), angle, scale) # 平移让眼睛中心落在目标位置 M[0, 2] output_size * 0.5 - eye_center[0] M[1, 2] output_size * 0.4 - eye_center[1] aligned cv2.warpAffine(img_np, M, (output_size, output_size), flagscv2.INTER_LINEAR) return aligned这里用getRotationMatrix2D构造的是相似变换矩阵只包含旋转、等比例缩放和平移不会引入非均匀拉伸。如果改用getAffineTransform做完整仿射变换双眼距离和鼻梁角度会被强迫贴合模板脸型反而会变形——这是矫正模块最常见的翻车点。scale用当前双眼距离和目标距离的比值计算保证不同脸型的人脸被缩放到接近一致的尺度。第二步是像素修复。几何矫正解决了「歪」但解决不了「糊」。这里用一个残差自编码网络输入矫正后的图像输出修正量残差连接让网络只需要学习「哪里需要改」而不是重新画一张脸。class FaceCorrector(nn.Module): def __init__(self, in_channels3): super().__init__() self.encoder nn.Sequential( nn.Conv2d(in_channels, 64, 4, 2, 1), nn.LeakyReLU(0.2, True), nn.Conv2d(64, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.LeakyReLU(0.2, True), nn.Conv2d(128, 256, 4, 2, 1), nn.BatchNorm2d(256), nn.LeakyReLU(0.2, True), ) self.decoder nn.Sequential( nn.ConvTranspose2d(256, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.ReLU(True), nn.ConvTranspose2d(128, 64, 4, 2, 1), nn.BatchNorm2d(64), nn.ReLU(True), nn.ConvTranspose2d(64, in_channels, 4, 2, 1), nn.Tanh(), ) def forward(self, x): residual self.decoder(self.encoder(x)) return x residualencoder三次下采样把64×64降到8×8提取全局结构信息decoder再三次上采样回64×64。残差连接是关键如果直接让网络回归整张图它得同时记住五官位置和肤色纹理训练难度大改成回归残差后网络只需要输出一个0附近的小修正量收敛快得多。修复网络的训练数据不需要额外标注——用真实人脸图做输入随机加一点噪声、轻微旋转或模糊模拟生成器的缺陷然后让网络把退化后的图还原成原图。# 修复网络损失重建为主对抗为辅 recon_loss F.l1_loss(corrected, real_imgs) # 如果环境里有预训练特征提取器可以加感知损失权重调到0.1左右 # perceptual_loss F.mse_loss(feature_extractor(corrected), feature_extractor(real_imgs)) adv_loss F.binary_cross_entropy_with_logits(D_corrected, torch.ones_like(D_corrected)) corrector_loss recon_loss 0.05 * adv_lossL1损失是重建质量的主力它比L2对模糊的容忍度更低能逼着网络把边缘修锐。对抗损失权重只用0.05作用是防止修复结果过于平滑、失去皮肤纹理细节。感知损失是可选项它比对像素更关注结构但如果环境里没有预训练特征网络跳过也不影响主流程。4.5 关键超参数与训练时长参考参数推荐值说明z_dim100潜变量维度太小容易模式崩塌batch_size6464×64分辨率下显存占用约4GBlr_g / lr_d2e-4 / 1e-4判别器略低防止D碾压Gbeta1 / beta20.5 / 0.999DCGAN标配的Adam动量epochs120前50轮看结构后70轮抠细节矫正网络权重recon 1.0 adv 0.05对抗权重过大会让修复结果出现伪影学习率是GAN训练里最容易影响成败的参数。批大小64、学率2e-4是DCGAN论文里验证过的组合如果你数据量小可以先把批大小降到32但学习率也要相应降到1e-4否则单个batch的梯度噪声变大损失曲线会出现毛刺。如果训练时显存不够不要先动网络结构优先降批大小和开混合精度。5. 避坑与排查人脸生成矫正的五个高频翻车现场5.1 训练震荡、损失不收敛现象判别器损失一路降到接近0生成器损失飙升到几十甚至上百生成结果全是带颜色的噪点。这是GAN新手最容易撞上的问题。原因判别器太强超前于生成器收敛。判别器看一眼就能分辨真假生成器拿到的梯度不是0就是极大值没有中间地带可以学习。具体诱因通常是学习率偏高或者判别器迭代次数多于生成器。解决学习率降到1e-4甚至5e-5把判别器学习率设为生成器的一半训练循环里严格保证每一轮先更新D再更新G且G只更新一次。还可以给标签做平滑——真实样本目标值设0.9而不是1.0生成样本设0.1而不是0给判别器留出梯度余量。5.2 模式崩塌生成结果全是同一张脸现象训练后期生成的人脸质量不错但采样几十次全是同一张脸或者只有两三种表情姿态。多样性消失判别器不再能区分生成器停在了一个稳定的「欺骗点」。原因生成器发现某个特定的z区间能稳定骗过判别器于是把所有噪声都映射到那个区域。z维度太小、判别器容量太大、训练数据本身太单一都会诱发这个问题。解决把z_dim从100调到128或256给判别器加Dropout层挡住过拟合检查训练集是不是混入了太多重复图。判断是否崩塌的一个笨办法每轮训练结束固定采样50个不同的z生成后拼图保存肉眼看重复率。一旦发现连续几十轮生成结果几乎不变立刻回退到之前的checkpoint调低生成器学习率重训。5.3 生成人脸模糊、五官糊成一团现象人脸轮廓基本成立但眼睛、牙齿、发丝边缘全是糊的皮肤像磨了皮拉大看有颗粒状的网格伪影。这在低分辨率GAN里非常常见。原因转置卷积的「棋盘效应」——ConvTranspose2d在某些位置重复叠加权重形成规则的像素网格加上64×64分辨率本身信息量有限眉毛睫毛这类高频细节很难被建模。解决生成器里把ConvTranspose2d替换成Upsample Conv2d组合插值上采样后接普通卷积棋盘效应基本消失修复网络的输出再接一层轻量的UNet结构细化边缘。如果不想改架构至少把修复网络的对抗损失权重从0.05提高到0.1让对抗信号逼着输出保留更多高频细节。5.4 矫正后反而更歪三点教训现象几何矫正前人脸只是轻微歪斜矫正后眼睛间距异常、脸被拉长甚至出现双眼不在同一水平线的情况。原因最常见的是直接用完整仿射变换六个自由度把脸型任意拉扯其次是关键点检测器在生成图上失效检测出的眼睛中心偏了几个像素旋转角自然不准。解决改用getRotationMatrix2D的相似变换只保留旋转、等比例缩放和平移检测器输出置信度低于阈值时跳过矫正直接走修复网络别硬矫正。还有个习惯值得养成矫正模块必须单独抽样验证——随机选20张生成图同一张分别存矫正前和矫正后的版本拼成对比图看。这样能快速判断是旋转角度的计算问题还是缩放比例的问题。5.5 显存溢出与训练速度过慢现象loss还没开始下降就报CUDA out of memory或者不报错但每轮训练耗时越来越长严重时batch_size只有以前的一半。原因批次过大、特征图缓存累积、数据加载成了瓶颈。GAN的判别器和生成器都要各保存一份中间激活显存消耗是同等分类网络的两倍以上。解决先把批次降到32或16确认能跑通再逐步上调PyTorch的自动混合精度能省近一半显存且几乎不掉精度DataLoader的num_workers调大到8、pin_memory保持True观察GPU利用率是否接近100%。如果这些都做了还是慢就别硬撑——先64×64跑通再换128×64的中等分辨率强行上高分辨率只会把时间耗在排错上。6. 进阶验证从64×64到128×128的矫正效果检查6.1 用FID量化生成质量肉眼判断有主观性FID是GAN项目里最常用的量化指标。计算方式是把真实图和生成图各抽一批送入特征提取网络比较两组特征分布的均值和协方差差异。训练完用验证集里随机抽的3000张真实图、3000张生成图算一次FID记为基线。之后每调整一次矫正模块或生成器结构都重新算一次FID和基线对比。FID下降说明整体分布更接近真实人脸但要注意FID对模糊并不敏感它衡量的是分布相似度不是单张清晰度所以不能完全替代肉眼检查。6.2 迁移到128×128的四个调整从64×64升到128×128需要动四个地方生成器和判别器各增加一层卷积/转置卷积把特征图从64×64逐步过渡到128批大小从64降到32否则显存大概率溢出z_dim可以考虑从100提到128给生成器更多的表达空间修复网络的输入输出尺寸同步调整残差结构的其他部分不需要改动。迁移之后先别指望一次到位前20轮重点观察是否出现震荡如果出现就把生成器学习率降到1e-4。6.3 推理脚本与看样例的习惯训练和推理应当用同一套预处理参数。推理时把生成器的输出从[-1,1]反归一化回[0,1]再转成numpy交给OpenCV做几何矫正矫正后再转回张量输入修复网络。最关键的习惯是每轮训练结束固定保存一批带噪声z的生成结果拼成4×4的网格图命名带上epoch编号。我现在的习惯是训练时不看损失曲线只看这批样例图的直观变化——损失曲线能骗人样例图不会。等到样例图连续几十轮看不出变化才回头翻损失曲线找原因。希望这套「生成 矫正」的方案能帮你少走些弯路也欢迎在对照复现时把踩到的坑记下来它们往往比参数表更有价值。本文还有配套的精品资源点击获取