尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从爬虫到DCGAN:完整人脸图像生成项目实战指南

发布时间:2026/9/29 18:59:51

资讯中心
01
ARTICLE

从爬虫到DCGAN:完整人脸图像生成项目实战指南

从爬虫到DCGAN:完整人脸图像生成项目实战指南
简介这是一份以爬虫、图像识别与生成对抗网络为主线将写真套图下载、美女脸部识别和DCGAN自动生成面孔整合在一起的Python实战资源适合具备一定Python基础、想深入GAN应用或人脸图像处理的开发者。压缩包共506个文件约77.58MB442张jpg和29张png构成原始图片与生成结果数据集8个py脚本覆盖数据采集、人脸裁剪、模型训练与生成链路index/meta/checkpoint等文件提供多阶段DCGAN模型权重可直接加载继续迭代。已有1737人学习下载。配合md说明、txt列表和批处理脚本可完整理解从批量抓图、脸部检测对齐到生成新面孔的项目工程组织方式是一份兼顾代码、数据与训练产物的成套实践资料。1. 爬虫 人脸识别 DCGAN 的组合为什么这个资源值得动手跑一遍先给结论这个项目不是单纯的爬虫源码包也不是一个孤立的人脸识别 demo而是一条完整的「数据采集 → 数据清洗 → 模型训练 → 图像生成」流水线。你需要先写爬虫把写真套图按页拉下来再用 OpenCV 的人脸检测器把每一张图里的人脸框出来、裁剪成统一尺寸最后喂给 DCGAN 去生成新的人脸。整个过程踩过一遍你就能同时摸清 requests 反爬、Haar 级联检测、GAN 训练调参这三块硬骨头而且每一块的产出都是可视化的——图片下载进度、人脸裁剪预览、生成器输出的假脸出了问题一眼就能看出来。这套资源适合两类人一类是刚学完 Python 基础、想找个综合性项目练手的另一类是想跑通一次 DCGAN 但手头没有干净人脸数据集的人。前者可以顺着爬虫和人脸裁剪把工程基础补扎实后者可以跳过第一段直接使用整理好的人脸数据子集来训练模型。但请注意爬取公开图片应遵守目标站点的 robots 协议和相关法律规定本项目代码仅限学习交流不要用于任何商业用途或非法抓取。下面我从爬虫讲起一步步拆到 DCGAN 生成每一章的代码都是我试过能跑的版本参数也是实际调出来的。2. 写真套图爬虫请求、解析与断点续传爬虫在整个项目里的角色是「数据入口」。你要做的不是把整站图片全下下来而是按栏目分页抓取套图列表再进入每套图的详情页拿到原图地址。这里最核心的坑不是解析逻辑而是请求频率和会话保持。直接用 requests.get 裸抓多半会被站点拦截原因不是你不带 header而是缺少 cookie 和 referer 的配合。2.1 选型与请求头伪装写爬虫前先选工具链。我一般只用 requests BeautifulSoup不用 scrapy因为套图站的结构不复杂scrapy 的框架反而让调试变得啰嗦。requests 负责发送 HTTP 请求BeautifulSoup 用 select 或 find_all 抽取 HTML 里的链接和图片地址。注意这里不涉及任何绕过访问限制的脚本只讲常规的请求头伪装和频率控制。import requests from bs4 import BeautifulSoup import time SESSION requests.Session() SESSION.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Connection: keep-alive, }) def fetch_page(url): for attempt in range(3): try: resp SESSION.get(url, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except Exception as e: print(f第 {attempt 1} 次请求失败: {e}) time.sleep(2) return None这段代码的关键点是requests.Session()它会自动维持 cookie 状态很多站点第一次访问会下发一个会话 cookie后续请求带上它才不会被重定向到验证页。resp.apparent_encoding用于从 HTML 里的 meta 标签推断编码避免中文乱码导致解析失败。timeout10是硬性要求没有超时控制的爬虫一旦遇到挂起的请求整个队列会卡死。2.2 页面解析与图片批量下载拿到 HTML 之后下一步是解析套图列表和详情页。写真类站点的页面结构通常是列表页每张缩略图包在a标签里链接指向详情页详情页里.content img下的src或>from bs4 import BeautifulSoup import os def parse_list(html, base_url): soup BeautifulSoup(html, html.parser) detail_links [] for a in soup.select(a[href*/detail/]): href a.get(href) if href and href not in detail_links: detail_links.append(requests.compat.urljoin(base_url, href)) return detail_links[:20] # 每页只取前20个避免请求过载 def download_images(detail_html, save_dir): soup BeautifulSoup(detail_html, html.parser) img_tags soup.select(.content img) os.makedirs(save_dir, exist_okTrue) for i, img in enumerate(img_tags): src img.get(src) or img.get(data-original) if not src: continue ext src.split(?)[0].split(.)[-1] if ext not in (jpg, jpeg, png): ext jpg path os.path.join(save_dir, f{i:04d}.{ext}) try: with SESSION.get(src, streamTrue, timeout15) as r: r.raise_for_status() with open(path, wb) as f: for chunk in r.iter_content(chunk_size8192): f.write(chunk) time.sleep(0.5) except Exception as e: print(f图片下载失败: {src} - {e})requests.compat.urljoin用来拼接相对地址很多详情页的链接是相对路径不拼会直接变成错误 URL。streamTrue配合iter_content(chunk_size8192)是按块写盘比r.content一次性读入内存更适合下载大图写真套图的原图普遍在 1MB 以上一次性读入容易把内存打爆。time.sleep(0.5)是下载间隔不是每张都等半秒而是给对端一个缓冲避免连续高并发触发封 IP。2.3 使用 sqlalchemy 储存爬虫数据爬虫一旦跑起来进度记录就成了刚需。我不建议存 JSON 文件因为断点续传时你要反复读整个文件再修改效率低而且容易写坏。用 sqlalchemy 建一张表记录每个详情页的状态比手写文件靠谱得多。from sqlalchemy import create_engine, Column, Integer, String, Boolean from sqlalchemy.orm import declarative_base, sessionmaker Base declarative_base() class CrawlRecord(Base): __tablename__ crawl_records id Column(Integer, primary_keyTrue) page_url Column(String(512), uniqueTrue) status Column(String(32), defaultpending) # pending/downloaded/failed image_count Column(Integer, default0) engine create_engine(sqlite:///crawl_state.db, echoFalse) Base.metadata.create_all(engine) SessionLocal sessionmaker(bindengine) def mark_done(url, count): with SessionLocal() as session: record session.query(CrawlRecord).filter_by(page_urlurl).first() if record: record.status downloaded record.image_count count session.commit()用 sqlite 做库是因为单机爬虫不需要网络数据库sqlite:///crawl_state.db一行就能创建本地文件库。uniqueTrue防止同一详情页被重复爬配合filter_by(page_urlurl).first()能实现天然的去重。注意 sqlalchemy 的 ORM 在写完后要session.commit()否则数据不会落盘这是最常见的翻车点——代码不报错但数据库里永远是空的。提示断点续传的核心逻辑就是在download_images前先查库如果记录状态是downloaded就跳过该详情页如果是failed则重新爬但要把之前下了一半的图片目录清掉否则会出现图片序号错位。3. 人脸检测与裁剪把套图变成可训练的人脸数据集图片下载只是原始素材DCGAN 需要的是对齐的人脸图像。这里涉及两个概念人脸检测是找出图片中「哪一块」是人脸人脸对齐是把这块人脸归一化到统一尺寸和大致位置。OpenCV 自带的 Haar 级联分类器虽然老但速度快、不依赖 GPU、安装即用非常适合批量处理几千张图。dlib 的 68 点模型更精确但配置繁琐而且很多人脸被头发遮挡时反而容易失败。我的经验是先 Haar 粗检再统一裁剪最后人工抽查性价比最高。3.1 用 OpenCV Haar 级联定位人脸Haar 级联的原理是滑动窗口 多尺度检测OpenCV 已经把训练好的模型封装成CascadeClassifier。使用时最需要注意的是minSize参数——写真套图里经常有远处的人脸或者半张脸minSize设太大漏检设太小会产生大量误检框。import cv2 haar_path cv2.data.haarcascades haarcascade_frontalface_default.xml face_cascade cv2.CascadeClassifier(haar_path) def detect_faces(image_path): img cv2.imread(image_path) if img is None: return [] gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 缩小图片加速检测检测框坐标按比例映射回去 scale 0.5 small_gray cv2.resize(gray, (0, 0), fxscale, fyscale) faces face_cascade.detectMultiScale( small_gray, scaleFactor1.1, minNeighbors5, minSize(48, 48), ) faces [(int(x / scale), int(y / scale), int(w / scale), int(h / scale)) for (x, y, w, h) in faces] return facesscaleFactor1.1是每次缩小窗口比例越小检测越慢但越准确minNeighbors5表示一个区域至少被 5 个相邻窗口命中才算人脸这个值调低了会框出很多背景纹理调高了会漏掉侧面脸。我把图片缩到 0.5 倍再检测是因为 Haar 在长宽超 2000 的套图原图上滑窗极慢缩半张图能提速 4 倍检测框坐标再乘回去就行。注意cv2.imread读不到文件时返回None必须提前判断否则cvtColor直接抛异常导致整个批量程序中断。3.2 批量裁剪与归一化缩放填充与保存策略检测出人脸框之后不能直接crop就完事。DCGAN 的输入是固定尺寸比如 64x64 或 128x128而人脸框是高矮不一的矩形直接 resize 会把脸拉变形。正确做法是先按中心点把框扩成正方形然后保持宽高比缩放到目标尺寸最后用黑色填充不足部分。import numpy as np import os TARGET_SIZE 64 def crop_and_resize_face(img, face, targetTARGET_SIZE): x, y, w, h face cx, cy x w // 2, y h // 2 side max(w, h) int(0.2 * max(w, h)) # 多留20%边距 half side // 2 x0, y0 max(cx - half, 0), max(cy - half, 0) x1, y1 min(cx half, img.shape[1]), min(cy half, img.shape[0]) face_crop img[y0:y1, x0:x1] if face_crop.size 0: return None # 等比例缩放到target-2再居中放到target* target的画布中 h, w face_crop.shape[:2] scale (target - 2) / max(h, w) new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(face_crop, (new_w, new_h), interpolationcv2.INTER_AREA) canvas np.zeros((target, target, 3), dtypenp.uint8) offset_x (target - new_w) // 2 offset_y (target - new_h) // 2 canvas[offset_y:offset_y new_h, offset_x:offset_x new_w] resized return canvas def process_directory(src_dir, dst_dir): os.makedirs(dst_dir, exist_okTrue) for fname in os.listdir(src_dir): img_path os.path.join(src_dir, fname) faces detect_faces(img_path) if not faces: continue img cv2.imread(img_path) for idx, face in enumerate(faces): face_img crop_and_resize_face(img, face) if face_img is not None: out_name f{os.path.splitext(fname)[0]}_{idx}.jpg cv2.imwrite(os.path.join(dst_dir, out_name), face_img)max(w, h) 0.2 * max(w, h)是给脸周围留出耳朵和额头上方的空间太贴边会让 DCGAN 学到的都是下巴纹理。cv2.INTER_AREA在缩小图片时能保留边缘信息比默认的双线性插值更不容易产生锯齿。最后的中置填充不是多余操作GAN 训练时如果图片里有大量黑色边缘生成器会学着把脸画在图像正中央这是好事——生成结果会更加规整。3.3 清洗脏数据模糊检测与重复去重裁剪完的人脸不等于都是好数据。套图里常见三种脏数据对焦不准导致的模糊脸、重复出现的同一张图可能只是尺寸不同、以及误检框出来的非人脸区域。模糊检测最简单的方法是拉普拉斯方差方差越低代表图像越模糊重复去重用感知哈希两张图的相似度超过阈值就只保留一张。def is_blurry(face_img, threshold50): gray cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY) return cv2.Laplacian(gray, cv2.CV_64F).var() threshold def phash(img, hash_size16): gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) resized cv2.resize(gray, (hash_size, hash_size), interpolationcv2.INTER_AREA) mean resized.mean() return (resized mean).astype(np.uint8).flatten() def hamming(a, b): return np.count_nonzero(a ! b) def deduplicate(image_list): kept [] hashes [] for path in image_list: img cv2.imread(path) if img is None: continue h phash(img) if all(hamming(h, old) 8 for old in hashes): hashes.append(h) kept.append(path) return keptthreshold50不是绝对值需要根据你的相机画质调整。如果套图本身来自专业摄影人物皮肤纹理细腻拉普拉斯方差通常能到 100 以上如果是压缩严重的缩略图40 都算清晰。我一般先随机抽 30 张裁剪结果看一遍定一个能让 90% 正常图通过的阈值再跑全量。感知哈希的hash_size16意味着把图缩到 16x16 再比较亮度分布汉明距离小于 8 就视为重复这个值对于同一张脸的不同裁切已经够用误杀率不高。注意清洗这一步不能省。DCGAN 对噪声数据极其敏感如果训练集里混进了模糊脸、侧面脸和误检背景生成器会在训练中期开始「糊弄」专门生成模糊图像来骗过判别器这就是后面要讲的训练塌缩的常见诱因。4. 训练 DCGAN 生成人脸结构设计与参数配置数据准备好之后进入整个项目最核心的部分——DCGAN。DCGAN 是卷积版 GAN它用转置卷积把随机噪声一步步放大成图像用普通卷积做判别。相比原始的 GANDCGAN 出了一套很实用的网络结构规范生成器里不用池化层改用步长卷积不带全连接层除了输出层用 Tanh其余层全部用 ReLU生成器或 LeakyReLU判别器。这套规范我今天还在用因为它是经过大量实验验证的稳定结构改起来有章可循。4.1 生成器与判别器网络结构生成器的输入是一个 100 维的随机噪声向量经过四个转置卷积块最终输出 3x64x64 的 RGB 图像。每个转置卷积层的通道数从 512 逐步降到 3每次上采样尺寸翻倍。判别器正好相反输入 3x64x64 图像输出一个标量真实概率。import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, latent_dim100, ngf64): super().__init__() self.model nn.Sequential( nn.ConvTranspose2d(latent_dim, ngf * 8, 4, 1, 0, biasFalse), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, biasFalse), nn.BatchNorm2d(ngf), nn.ReLU(True), nn.ConvTranspose2d(ngf, 3, 4, 2, 1, biasFalse), nn.Tanh(), ) def forward(self, z): z z.unsqueeze(2).unsqueeze(3) return self.model(z) class Discriminator(nn.Module): def __init__(self, ndf64): super().__init__() self.model nn.Sequential( nn.Conv2d(3, ndf, 4, 2, 1, biasFalse), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf, ndf * 2, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, biasFalse), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(ndf * 8, 1, 4, 1, 0, biasFalse), nn.Sigmoid(), ) def forward(self, img): return self.model(img).view(-1, 1)ConvTranspose2d(100, 512, 4, 1, 0)是把 100 维向量从 1x1 放大到 4x4 的第一层这是 DCGAN 的经典起始配置。后面每层stride2, padding1都会让特征图尺寸翻倍4→8→16→32→64。注意生成器的输出层用了Tanh所以训练前要把人脸图像归一化到 [-1, 1] 区间这个细节常被忽略——如果输入图像是 [0, 1]生成器的输出范围就对不上损失会一直震荡不收敛。4.2 训练循环与损失函数DCGAN 的训练本质是极小极大博弈判别器想让真实图片判定为 1、假图片判定为 0生成器想让假图片被判定为 1。实现时用二元交叉熵即可但两个网络的更新节奏和优化器参数要单独设置。生成器使用 Adam 学习率 0.0002判别器也是 0.0002但每轮要先冻住生成器更新判别器再冻住判别器更新生成器。import torch.optim as optim from torch.utils.data import DataLoader, Dataset import torchvision.transforms as transforms def train_gan(gen, disc, dataloader, epochs200, devicecuda): # 初始化权重DCGAN 要求所有权重从均值为0标准差0.02的正态分布采样 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) gen.apply(weights_init) disc.apply(weights_init) opt_gen optim.Adam(gen.parameters(), lr0.0002, betas(0.5, 0.999)) opt_disc optim.Adam(disc.parameters(), lr0.0002, betas(0.5, 0.999)) criterion nn.BCELoss() for epoch in range(epochs): for real_imgs, _ in dataloader: real_imgs real_imgs.to(device) batch_size real_imgs.size(0) label_real torch.full((batch_size, 1), 1.0, devicedevice) label_fake torch.full((batch_size, 1), 0.0, devicedevice) # 1. 训练判别器 disc.zero_grad() output_real disc(real_imgs) loss_real criterion(output_real, label_real) z torch.randn(batch_size, 100, devicedevice) fake_imgs gen(z) output_fake disc(fake_imgs.detach()) loss_fake criterion(output_fake, label_fake) loss_disc loss_real loss_fake loss_disc.backward() opt_disc.step() # 2. 训练生成器 gen.zero_grad() output_fake2 disc(fake_imgs) loss_gen criterion(output_fake2, label_real) # 希望假图被判为真 loss_gen.backward() opt_gen.step() if (epoch 1) % 10 0: print(fEpoch [{epoch1}/{epochs}] D损失: {loss_disc.item():.4f} G损失: {loss_gen.item():.4f})disc(fake_imgs.detach())中的detach()很关键它切断了判别器反传时到生成器的梯度确保这一步只更新判别器。betas(0.5, 0.999)是 DCGAN 官方实现里的设定默认的 Adam 用0.9在 GAN 训练里容易震荡改成0.5能抑制动量过大带来的不收敛。损失打印不需要每个 batch 都做每 10 个 epoch 看一眼即可重点是趋势如果判别器损失长期趋近 0说明生成器已经完全掉队了。4.3 图像输出与模型保存训练过程中如果不定期看图你根本不知道模型是在学人脸还是已经在生成噪点。一般每 5 个 epoch 生成一批固定噪声的假图保存成一张网格图。固定噪声很重要——只有输入相同的 z你才能清晰对比每个 epoch 的生成效果变化。import torchvision.utils as vutils fixed_noise torch.randn(64, 100, devicecuda) def save_samples(gen, noise, epoch, save_path): gen.eval() with torch.no_grad(): fake gen(noise).detach().cpu() grid vutils.make_grid(fake, nrow8, normalizeTrue, value_range(-1, 1)) vutils.save_image(grid, f{save_path}/epoch_{epoch:03d}.png) gen.train() def save_checkpoint(gen, disc, opt_gen, opt_disc, epoch, path): torch.save({ epoch: epoch, gen_state: gen.state_dict(), disc_state: disc.state_dict(), opt_gen: opt_gen.state_dict(), opt_disc: opt_disc.state_dict(), }, path)make_grid的normalizeTrue, value_range(-1, 1)会把生成器的 Tanh 输出映射到 [0, 1]不然保存出来的图片会黑乎乎一片。eval()和train()的切换不能省因为生成器里的 BatchNorm 在推理和训练时行为不同不切换会导致 validation 结果不稳定。checkpoint 建议连优化器状态一起保存这样中断训练后能精确恢复到之前的 Adam 动量而不是从零再来。5. DCGAN 训练避坑指南从塌缩到 NaN 的常见问题这部分是血泪经验。我用 DCGAN 跑过人脸、车、动漫头像踩过的坑基本集中在四类训练塌缩、损失 NaN、生成图全是噪点、显存不足。每个坑我都按「现象 → 原因 → 解决」写清楚你照着排查能省至少两天的调参时间。5.1 训练塌缩判别器太强导致生成器梯度消失现象训练到 200 轮左右判别器损失降到 0.01生成器损失涨到 5 以上生成的图片变成一张固定的模糊脸无论输入什么噪声都输出同一张图。原因判别器学会了只靠个别特征比如肤色分布就能区分真假生成器找不到可学的梯度开始输出一个能骗过判别器的均值图像。解决最有效的是降低判别器能力例如把判别器的通道数减半或者每训练一次判别器就训练两次生成器。# 在训练循环里改为生成器更新2次判别器更新1次 for _ in range(2): gen.zero_grad() z torch.randn(batch_size, 100, devicedevice) fake_imgs gen(z) output_fake disc(fake_imgs) loss_gen criterion(output_fake, label_real) loss_gen.backward() opt_gen.step()这个方法的核心思想是给生成器更多「说话机会」让它在判别器还没完全站稳时先跟上去。另一个技巧是给判别器的真实标签加少量噪声比如用 0.9 而不是 1.0 当真实标签这会迫使判别器决策边界不那么尖锐间接帮助生成器。5.2 损失变成 NaN学习率与初始化现象某个 epoch 开始D 损失和 G 损失突然变成nan然后一直无法恢复。原因训练过程发散常见于学习率过大或者 BatchNorm 在 batch size 太小时统计量不稳定。解决先把学习率从0.0002降到0.0001同时检查 batch size至少别小于 64。如果仍然 NaN就把所有卷积层的权重重新初始化一次。def reset_model(gen, disc): gen.apply(weights_init) disc.apply(weights_init)注意weights_init必须重新给每个子模块执行一遍不能只调用model.reset_parameters()因为 Conv 和 BatchNorm 的初始化函数不同。还有一种隐蔽原因是数据里有 NaN 值——图片下载后有些损坏的 JPEG 能被cv2.imread读出但像素是异常值建议裁剪时加上np.isfinite(img).all()检查。5.3 生成图全是噪点人脸没有对齐现象生成器不塌缩损失也正常下降但生成的图片里看不到完整人脸轮廓只有一团团有皮肤纹理的色块。原因训练图像里的人脸位置没有对齐有的脸在图像中央有的在左上角有的脸占整张图有的只占三分之一。DCGAN 的卷积核没有平移不变性不是 CNN 的共享权重能解决的问题它学到的是「脸必须在固定位置出现」这一强先验。解决回炉第三节把裁剪的边距比例调小强制人脸占图像面积 70% 以上。注意人脸对齐是 DCGAN 项目里最重要的一步比调网络结构还重要。我试过用 3000 张没有裁剪对齐的图训练最后生成的永远是一堆色块把同样数据重新裁剪到 64x64 并让人脸居中后同一套代码在 100 个 epoch 就能看出清晰的五官轮廓。5.4 显存不足批量大小与图像分辨率权衡现象程序启动后直接报CUDA out of memory甚至把显存占用到 12GB 还不够。原因生成器和判别器各占一份显存64 张 64x64 的图对计算图的要求已经很高如果切成 128x128 尺寸单 batch 32 张都会爆。解决先固定 batch size 64如果爆了再降 batch或者用torch.cuda.amp混合精度训练把显存占用降一半。from torch.cuda.amp import autocast, GradScaler scaler GradScaler() with autocast(): output_fake disc(fake_imgs.detach()) loss_fake criterion(output_fake, label_fake) scaler.scale(loss_fake).backward() scaler.step(opt_disc) scaler.update()注意混合精度不是万能的它会让 BN 层的计算有轻微精度损失但人脸生成任务对精度不敏感。另一个实用技巧是关掉训练时的torch.backends.cudnn.benchmark默认开启的 autocast 在某些显卡上会自动优化算法选择反而导致显存碎片化加剧。如果不确定手动设torch.backends.cudnn.benchmark False更省心。6. 让生成结果更接近真人预处理增强与后期筛选最后这一章不聊网络结构了聊两个让你超出「能跑通」水平、真正把画质做上去的手段数据增强和生成结果筛选。训练到后期你会发现单纯加大 epoch 数并不能提升细节锐度DCGAN 的瓶颈往往在数据多样性上。6.1 调整裁剪策略偏移与旋转增强通用的人脸数据集在送入 DCGAN 前通常没有做随机裁剪但写真套图里的人物姿势、表情、拍摄角度高度相似连续多张图几乎是一个模子刻出来的。我建议把裁剪逻辑加一点「随机扰动」每次从人脸框的位置随机偏移 5 个像素、旋转 ±5 度再缩放到目标尺寸。def crop_with_augment(img, face, target64, jitter5, angle5): x, y, w, h face cx, cy x w // 2, y h // 2 side int(max(w, h) * 1.2) # 随机偏移 cx np.random.randint(-jitter, jitter 1) cy np.random.randint(-jitter, jitter 1) half side // 2 x0, y0 max(cx - half, 0), max(cy - half, 0) x1, y1 min(cx half, img.shape[1]), min(cy half, img.shape[0]) crop img[y0:y1, x0:x1] # 随机旋转并缩放 M cv2.getRotationMatrix2D((crop.shape[1] // 2, crop.shape[0] // 2), angle, 1.0) crop cv2.warpAffine(crop, M, (crop.shape[1], crop.shape[0])) # 继续走等比例缩放和居中填充 ...偏移量jitter5对应 64x64 的图约 8% 的平移量不会导致脸跑到画布外。旋转角度超过 10 度就会引入黑边所以控制在 5 度以内。这套增强不是官方标准而是我从实际效果里试出来的——它能显著增加 DCGAN 看到的样本分布范围尤其在训练集只有两三千张的时候生成器更容易学到姿态变化而不是死记硬背。6.2 用 Fréchet Inception Distance 评估生成质量平时我们看生成图全凭主观但两个人脸相似度很高不代表 DCGAN 训练得好。为了量化效果我习惯用 FIDFréchet Inception Distance指标。FID 衡量真实图集和生成图集在 Inception 特征空间的分布距离数值越低越好。这个指标也能当早期停止依据FID 降到相对低位后继续训练通常只会过拟合或者塌缩。# 伪代码实际使用需要安装 torchvision 的 inception 特征提取 from torchvision.models import inception_v3 import numpy as np def compute_fid(real_features, fake_features): mu_real, sigma_real real_features.mean(0), np.cov(real_features, rowvarFalse) mu_fake, sigma_fake fake_features.mean(0), np.cov(fake_features, rowvarFalse) diff mu_real - mu_fake covmean sqrtm(sigma_real.dot(sigma_fake)) fid diff.dot(diff) np.trace(sigma_real sigma_fake - 2 * covmean) return fidsqrtm来自scipy.linalg计算两个协方差矩阵的平方根这一步如果矩阵不可逆会报错可以用np.linalg.eig手动求。FID 不需要每个 epoch 都算一般每 20 个 epoch 算一次计算时随机抽 1000 张真实图和 1000 张生成图就够稳定了。注意 FID 对图像分辨率敏感统一都用 64x64 的输入算特征不要拿原始大图对比。6.3 我的筛选习惯与最终建议训练跑完后直接把fixed_noise生成的所有图挑出来是远远不够的。我现在会专门写一段筛选脚本先对生成图片做模糊检测把低于阈值的扔掉再用 OpenCV 的CascadeClassifier检测生成图里有没有人脸如果没有就说明这一轮生成的完全不可用。def filter_generated_images(gen, noise, threshold0.3): gen.eval() with torch.no_grad(): fake gen(noise).cpu() good_indices [] for i, img in enumerate(fake): img_np (img.permute(1, 2, 0).numpy() * 0.5 0.5) * 255 img_np img_np.astype(np.uint8) faces face_cascade.detectMultiScale(img_np, 1.1, 5, minSize(24, 24)) blur_score cv2.Laplacian(cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY), cv2.CV_64F).var() if len(faces) 1 and blur_score 20: good_indices.append(i) gen.train() return good_indices注意检测用的人脸 cascade 参数minSize(24, 24)和筛选取材形的(48, 48)不一样因为生成图的清晰度本来就不高检测框太大容易漏检。训练完的生成器保存好 checkpoint 后我习惯把每次生成的筛选结果按 FID 排序只保留排名前 20% 的图像作为后续人工挑选的候选集。这个过程很笨但有效——DCGAN 是生成模型它输出的所有图不可能张张都好你要做的是选出它的上限而不是平均质量。说实话跑通这套流程后最大的感受是爬虫代码再烂也能靠中断重跑救回来但 DCGAN 训练一旦塌缩回滚到之前的 checkpoint 往往也救不回来只能重新调节学习率和数据清洗策略。从那以后我每次开始训练 GAN 之前都会强制自己先花一小时检查三件事训练集里有没有模糊脸、人脸是否对齐、生成器和判别器初始权重是否合规。这三关过了模型大概率能跑到 epoch 200 不翻车。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。