从爬虫到DCGAN:人脸数据集构建与生成实战
简介本资源面向Python机器学习初学者与计算机视觉爱好者提供一套从数据采集到人脸生成的完整实战案例。内容围绕写真美女套图展开先用爬虫批量获取图片素材再通过人脸检测完成脸部区域提取与筛选最后基于DCGAN训练模型实现人脸图像的自动生成帮助读者理解生成对抗网络在图像任务中的落地流程。压缩包共506个文件约77.58MB其中442个jpg与29个png为训练与测试图像样本8个py脚本承载爬虫、识别与训练逻辑另有checkpoint、index、meta等模型文件及少量txt、xml、bat辅助配置目录结构清晰便于按模块查阅。目前已有1740人学习下载适合想动手复现爬虫加人脸识别加DCGAN生成全流程的读者参考可从中获取数据准备、模型训练与生成效果验证的完整思路。1. 从一套写真套图到可训练人脸集这条链路到底在做什么很多人第一次看到“写真套图爬虫 人脸识别 DCGAN 生成”这种组合第一反应是三个不相干的东西硬凑在一起。其实它是一条完整的图像数据流水线先用爬虫把散落在网页上的写真套图批量抓下来再用检测模型把人脸区域裁出来、对齐、清洗成统一规格的人脸数据集最后拿这批数据去训练一个 DCGAN让模型自己生成不存在的人脸图。整条链路解决的核心问题是——你手上没有现成的人脸数据集但你需要一个能跑通训练、能出图、能验证效果的闭环。这套资源适合两类人一类是刚学完 Python 基础、想找一个“有真实数据、有模型、有产出”的练手项目另一类是做图像处理或机器学习方向需要一套可复现的人脸采集到生成流程做实验基线。它不涉及任何线上部署也不要求你有 GPU 集群一台带独显的机器或者能跑 Colab 的环境就够。下面我按实际拆包的顺序把每一步的参数、代码和翻车点讲清楚。2. 爬虫层写真套图批量下载与目录规整2.1 目标站点结构分析与请求构造写真套图类站点的页面结构通常分三层列表页缩略图 详情页链接、详情页大图 分页、图片直链。爬虫要做的就是按这个层级逐层解析。常见做法是用requests拿 HTML用BeautifulSoup或lxml提取链接再对图片直链发二次请求下载。这里不讨论具体站点只讲通用结构。请求头里最关键的是User-Agent和Referer。很多站点会校验Referer是否来自本站详情页缺失就直接返回 403。另一个容易被忽略的是Accept和Accept-Language部分站点会根据这两个字段返回不同分辨率的图片。我一般会先手动打开开发者工具复制一条真实图片请求的完整头信息再精简成最小可用集合。import requests from bs4 import BeautifulSoup import os import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://example.com/, # 替换为目标站点根域名 Accept: image/avif,image/webp,image/apng,image/*,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, } def fetch_html(url, retry3): for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except Exception as e: print(f第{i1}次请求失败: {e}) time.sleep(2) return None def parse_detail_links(list_url): html fetch_html(list_url) if not html: return [] soup BeautifulSoup(html, lxml) links [] for a in soup.select(a.album-link): # 选择器按实际页面调整 href a.get(href) if href: links.append(href) return linksfetch_html里的retry参数控制重试次数timeout10是单次请求超时超过就抛异常进入重试。resp.apparent_encoding比resp.encoding更可靠能自动推断编码避免中文乱码。parse_detail_links里的 CSS 选择器a.album-link是占位实际要替换成目标站点的真实类名或路径规则。2.2 图片下载与去重、命名规范下载环节最容易出问题的是三件事文件名冲突、重复下载、以及下载到 HTML 错误页而不是图片。我的做法是每张图用md5(图片URL)作为文件名扩展名从Content-Type推断同时在本地维护一个downloaded.txt记录已完成的 URL断点续传时先读这个文件。import hashlib def download_image(img_url, save_dir, done_set): if img_url in done_set: return False try: resp requests.get(img_url, headersHEADERS, timeout15, streamTrue) if resp.status_code ! 200: return False content_type resp.headers.get(Content-Type, ) if image not in content_type: return False # 过滤掉错误页 ext content_type.split(/)[-1].split(;)[0] if ext not in (jpeg, jpg, png, webp): ext jpg name hashlib.md5(img_url.encode()).hexdigest() . ext path os.path.join(save_dir, name) with open(path, wb) as f: for chunk in resp.iter_content(8192): f.write(chunk) done_set.add(img_url) return True except Exception as e: print(f下载失败 {img_url}: {e}) return FalsestreamTrue配合iter_content(8192)是分块写入避免大图一次性占满内存。Content-Type校验能挡掉大部分反爬返回的 HTML 页面。done_set在程序启动时从downloaded.txt加载每下载成功一张就追加一行这样中断后重跑不会重复劳动。提示下载频率控制在每秒 12 张并在每张之间time.sleep(random.uniform(0.5, 1.5))否则很容易触发站点限流。3. 人脸检测与对齐从杂乱套图到规范人脸集3.1 检测器选型与批量裁剪爬下来的图尺寸不一、构图各异直接拿去训练 DCGAN 效果会很差。必须先做人脸检测和裁剪。常见方案有三种OpenCV 的 Haar 级联、Dlib 的 HOG 检测器、以及基于深度学习的 MTCNN 或 RetinaFace。Haar 最快但误检率高Dlib HOG 居中MTCNN 精度最好但速度慢。我一般用 MTCNN 做第一遍因为人脸数据集的质量直接决定生成效果宁可慢一点。from mtcnn import MTCNN import cv2 import os detector MTCNN() def extract_faces(img_path, out_dir, min_size80): img cv2.imread(img_path) if img is None: return 0 rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results detector.detect_faces(rgb) count 0 for i, res in enumerate(results): x, y, w, h res[box] if w min_size or h min_size: continue x, y max(0, x), max(0, y) face img[y:yh, x:xw] face cv2.resize(face, (128, 128)) base os.path.splitext(os.path.basename(img_path))[0] cv2.imwrite(os.path.join(out_dir, f{base}_face{i}.jpg), face) count 1 return countmin_size80是过滤掉太小的脸小于这个尺寸的裁剪图放大到 128 会严重模糊。res[box]返回的是(x, y, w, h)注意 MTCNN 的坐标可能为负所以要max(0, ...)截断。统一resize到 128×128 是为了后续 DCGAN 输入固定尺寸。3.2 对齐、去重与数据集划分裁剪完的人脸还需要做对齐否则侧脸、歪头会拉低生成质量。对齐的常见做法是用两眼坐标计算旋转角度把眼睛拉到水平线。MTCNN 返回的keypoints里就有left_eye和right_eye可以直接用。import numpy as np def align_face(img, left_eye, right_eye): dx right_eye[0] - left_eye[0] dy right_eye[1] - left_eye[1] angle np.degrees(np.arctan2(dy, dx)) center ((left_eye[0] right_eye[0]) // 2, (left_eye[1] right_eye[1]) // 2) M cv2.getRotationMatrix2D(center, angle, 1.0) return cv2.warpAffine(img, M, (img.shape[1], img.shape[0]), flagscv2.INTER_CUBIC)去重我用的是感知哈希pHash把每张人脸图转成 64 位指纹汉明距离小于 5 的视为重复。这一步能去掉连拍或相似角度带来的冗余样本。最后按 8:1:1 划分训练集、验证集、测试集目录结构保持train/val/test三个文件夹每个文件夹下直接放图片方便ImageFolder直接读取。注意如果爬下来的套图里同一个人出现多次去重后仍可能残留同一人的多张不同角度图。DCGAN 训练时这会导致模型偏向生成某张脸建议按人物维度再做一次粗聚类或者直接接受这种偏差作为实验观察点。4. DCGAN 训练把 128×128 人脸喂进生成器4.1 生成器与判别器结构设计DCGAN 的核心约束是生成器用转置卷积上采样判别器用步长卷积下采样两者都加 BatchNorm生成器输出层和判别器输入层除外激活函数生成器用 ReLU、最后一层用 tanh判别器统一用 LeakyReLU。输入噪声维度一般取 100生成器最终输出 128×128×3。import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, nz100, ngf64, nc3): super().__init__() self.net nn.Sequential( nn.ConvTranspose2d(nz, 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, nc, 4, 2, 1, biasFalse), nn.Tanh() ) def forward(self, x): return self.net(x) class Discriminator(nn.Module): def __init__(self, nc3, ndf64): super().__init__() self.net nn.Sequential( nn.Conv2d(nc, 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, x): return self.net(x).view(-1)nz100是噪声向量长度ngf和ndf控制特征图基数显存不够就降到 32。生成器最后一层Tanh把输出压到 [-1,1]所以数据预处理时要把图片归一化到同一范围。判别器最后用Sigmoid输出概率配合BCELoss使用。4.2 训练循环、超参与显存控制训练循环里最关键的是生成器和判别器的更新节奏。标准做法是每步先更新判别器一次再更新生成器一次。判别器学习率一般设 0.0002生成器可以稍低或相同。优化器用 Adambetas(0.5, 0.999)是 DCGAN 论文里的推荐值比默认的 0.9 更稳。from torch.utils.data import DataLoader from torchvision import datasets, transforms transform transforms.Compose([ transforms.Resize(128), transforms.CenterCrop(128), transforms.ToTensor(), transforms.Normalize((0.5,0.5,0.5), (0.5,0.5,0.5)) ]) dataset datasets.ImageFolder(faces/train, transformtransform) loader DataLoader(dataset, batch_size64, shuffleTrue, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) G Generator().to(device) D Discriminator().to(device) criterion nn.BCELoss() optG torch.optim.Adam(G.parameters(), lr0.0002, betas(0.5,0.999)) optD torch.optim.Adam(D.parameters(), lr0.0002, betas(0.5,0.999)) for epoch in range(50): for i, (real, _) in enumerate(loader): real real.to(device) bs real.size(0) # 更新判别器 D.zero_grad() out_real D(real) loss_real criterion(out_real, torch.ones(bs, devicedevice)) noise torch.randn(bs, 100, 1, 1, devicedevice) fake G(noise) out_fake D(fake.detach()) loss_fake criterion(out_fake, torch.zeros(bs, devicedevice)) loss_D loss_real loss_fake loss_D.backward() optD.step() # 更新生成器 G.zero_grad() out D(fake) loss_G criterion(out, torch.ones(bs, devicedevice)) loss_G.backward() optG.step() print(fEpoch {epoch} | D: {loss_D.item():.4f} | G: {loss_G.item():.4f})batch_size64在 8G 显存上跑 128×128 基本够用不够就降到 32 或 16。num_workers4是数据加载线程数Windows 下如果报错就改成 0。fake.detach()在更新判别器时切断生成器梯度这是标准操作漏掉会导致生成器被意外更新。每轮打印两个 loss正常情况是 D 在 0.51.0 之间波动G 在 1.03.0 之间如果 D 掉到 0.1 以下说明判别器太强生成器学不动。提示训练中途每隔 5 个 epoch 存一次生成器权重和一批生成图方便回看质量变化。DCGAN 的收敛很看运气同一套参数换一批数据可能完全不同的表现。5. 避坑与排查这条链路上最容易翻车的五个点5.1 爬虫返回 200 但下载的是空图或错误页现象文件下载成功但打开是 1KB 的空白图或站点错误提示图。原因站点对图片直链做了防盗链或者请求头里Referer不对。解决用开发者工具抓一条真实图片请求完整复制头信息同时在下载后校验文件大小小于 5KB 的直接删除并记录 URL 待重试。5.2 MTCNN 检测不到人脸或框错位置现象裁剪出来的图全是背景或半张脸。原因原图分辨率太低、人脸太小或者图片本身是插画而非真实照片。解决在检测前先过滤掉宽度小于 300px 的图对检测结果加置信度阈值res[confidence] 0.9如果整批图都是插画风格换 RetinaFace 或直接放弃这批数据。5.3 训练 loss 震荡剧烈、生成图全是噪点现象D loss 在 0.01 和 0.9 之间跳G loss 持续上升生成图看不出人脸。原因判别器太强或学习率不匹配。解决把判别器学习率降到生成器的一半给判别器输入加高斯噪声均值 0、方差 0.1或者把判别器更新频率降到每两步一次。另一个常见原因是数据没归一化到 [-1,1]检查Normalize的均值和方差是否都是 0.5。5.4 显存溢出CUDA out of memory现象训练几步后报显存不足。原因batch_size太大、生成器特征图基数ngf太高、或者没有及时释放中间变量。解决先把batch_size减半再把ngf和ndf从 64 降到 32在训练循环里对loss_D和loss_G用del手动释放如果还不行把图片尺寸从 128 降到 64。5.5 生成结果千篇一律、模式崩溃现象不管输入什么噪声生成器都输出几乎一样的脸。原因生成器找到了一个能骗过判别器的“万能样本”这是 GAN 的经典模式崩溃。解决在判别器里加标签平滑真实标签用 0.9 而不是 1.0对生成器加特征匹配损失或者最简单粗暴的办法——换一批更多样化的训练数据模式崩溃很多时候是数据太单一导致的。6. 进阶技巧用 FID 和插值验证生成质量训练完不能只看几张图就下结论。我一般会做两件事算 FIDFréchet Inception Distance和做潜在空间插值。FID 衡量生成分布和真实分布的差距数值越低越好但需要至少几千张生成图和真实图对比。插值则是取两个随机噪声向量在它们之间做线性插值生成一系列过渡图观察是否平滑——如果中间出现突变或崩坏说明潜在空间不连续模型还没训好。import numpy as np from scipy.linalg import sqrtm from pytorch_fid import fid_score # 方式一直接用 pytorch-fid 库 fid fid_score.calculate_fid_given_paths( [faces/val, generated], batch_size32, devicecuda, dims2048 ) print(fFID: {fid:.2f}) # 方式二手动做潜在空间插值 z1 torch.randn(1, 100, 1, 1, devicedevice) z2 torch.randn(1, 100, 1, 1, devicedevice) alphas np.linspace(0, 1, 10) with torch.no_grad(): for a in alphas: z z1 * (1 - a) z2 * a img G(z) # 保存 img 为图片观察过渡是否平滑fid_score.calculate_fid_given_paths第一个参数是真实图目录第二个是生成图目录两个目录下的图片数量最好一致。dims2048是 Inception 特征维度显存不够可以降到 768。插值那段的alphas从 0 到 1 取 10 个点生成的图按顺序排列如果第 3 张到第 7 张之间出现明显跳变说明模型对潜在空间的映射还不够连续。我自己的习惯是每训完一个版本先跑 FID再抽 10 组插值图最后人工扫一遍生成图里有没有明显畸变。这三步走完才决定要不要继续调参。从那以后我每次训 GAN 都强制走一遍 FID 插值 人工抽检再也没出现过“看着还行、一算指标就崩”的情况。希望帮到你。本文还有配套的精品资源点击获取