☰
Python爬虫到DCGAN人脸生成:数据清洗决定模型上限的完整链路
2026/10/10 14:45:56 网站建设 项目流程

简介:一份面向Python开发与机器学习初学者的综合实战资源,聚焦写真美女套图爬虫采集、人脸识别定位与DCGAN人脸自动生成三大任务,适合希望将爬虫、OpenCV图像处理、深度学习生成模型串联练习的读者。压缩包共506个文件,约77.58MB,核心包括442张jpg与29张png图像样本、8个py训练与采集脚本,以及DCGAN模型checkpoint、index、meta等训练中间文件,另附create_list.bat等辅助工具,便于复现数据准备、模型训练与生成结果。该资源已有1739人学习浏览,内容兼顾数据、代码与模型产物,可直接对照学习爬虫抓取建库、人脸特征提取、以及基于DCGAN生成人脸图像的完整流程。适合具备基础Python语法、想以真实项目串联图像采集与生成技术的开发者作为练习参考。

1. 从套图爬虫到 DCGAN 人脸生成:这条流水线比想象中更吃数据而不是算力

一提到“Python-写真美女套图爬虫 + 脸部识别 + DCGAN 脸部自动生成”,多数人第一反应是 GPU 要烧钱、模型要调参,但实际跑通这条链路后你会发现,真正的瓶颈在数据:爬虫能不能稳定拿到干净的大图,人脸检测能不能从套图里筛出足够多高质量的正脸样本,决定了 DCGAN 最终是生成出“一张能看的脸”还是一个糊成一团的噪点。这篇笔记面向已经会用 Python 写脚本、想正经把采集→检测→生成三段流程串起来的开发者,给你一套不需要分布式爬虫、单机也能落地的方案,以及我在替换检测器、调 DCGAN 参数时踩过的具体坑。

2. 套图爬虫怎么写:requests + xpath 的采集链路与合规边界

2.1 请求与解析:requests + lxml 的 xpath 取数链路

套图站的页面结构通常很规整:一个专辑页列出几十张图片的地址,图片地址要么直接写在img标签的src里,要么写在某个>import requests from lxml import etree import time # 目标站点仅作技术演示,上线前务必自查 robots.txt 与版权授权 headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", "Referer": "https://example-site.com/" } def fetch_album(url): resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() # 有些站点是 gbk,有些是 utf-8,先探测再解码 if resp.apparent_encoding and resp.apparent_encoding.lower() != "utf-8": resp.encoding = resp.apparent_encoding html = etree.HTML(resp.text) # 图片地址通常在 li > a > img 的>import os from concurrent.futures import ThreadPoolExecutor import hashlib import requests import time def download_image(url, save_dir, referer): # 用 URL 的 MD5 做文件名,天然去重 fname = hashlib.md5(url.encode()).hexdigest() + ".jpg" fpath = os.path.join(save_dir, fname) if os.path.exists(fpath): return False # 已存在,跳过 headers = { "User-Agent": "Mozilla/5.0", "Referer": referer } try: resp = requests.get(url, headers=headers, timeout=15) if resp.status_code == 200 and len(resp.content) > 10_000: with open(fpath, "wb") as f: f.write(resp.content) return True except requests.RequestException: return False finally: time.sleep(0.5) # 限速,避免触发风控 def download_album(img_urls, save_dir, referer): os.makedirs(save_dir, exist_ok=True) with ThreadPoolExecutor(max_workers=4) as pool: pool.map(lambda url: download_image(url, save_dir, referer), img_urls)

这段代码把文件名直接做成 URL 的 MD5,好处是同一个 URL 重复抓取时永远覆盖同一个文件,不需要维护重复队列。len(resp.content) > 10_000这个阈值是过滤掉错误页和占位图的关键——很多反爬机制会返回一个 1KB 左右的空白图,下载下来对人脸检测毫无价值。这里单线程限速 0.5 秒,四线程等效每秒约 8 张,一万张套图大约二十分钟,在可控范围内。

关于合规边界我要多说一句:爬虫只能处理允许匿名访问的公开内容,套图如果涉及人物肖像、版权或平台明确禁止采集的内容,这套代码只能用于本地技术验证。真正的生产流程里,数据授权和来源审查比爬虫效率重要得多,别踩版权红线。

3. 人脸检测与清洗:OpenCV 和 MTCNN 怎么选,样本决定了 DCGAN 的天花板

3.1 人脸检测选型:OpenCV Haar Cascade 与 MTCNN 的取舍

爬下来的套图不能直接丢给 DCGAN。写真图集里大量是全身照、侧脸、带墨镜和帽子的构图,直接整图缩放训练,生成器会学出一堆乱七八糟的背景纹理。所以要先用脸部识别把正脸区域裁出来,再做缩放对齐。

我的做法是先跑 OpenCV 的 Haar Cascade 做粗筛,再对粗筛结果用 MTCNN 精修。Haar Cascade 的优势是快,单张 1920×1080 的图在 CPU 上大约 30 毫秒,但它对侧脸、模糊脸、暗光脸的召回率很差;MTCNN 靠三个级联网络(P-Net、R-Net、O-Net)逐步精细定位,召回率明显高,但单张耗时是 Haar 的 10 倍以上。套图数据集如果只有几千张,直接用 MTCNN 没问题,但如果到了十万张量级,Haar 先筛掉明显不是人脸的图,能省下大量 GPU 时间。

import cv2 import numpy as np face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) def haar_detect(img_path): img = cv2.imread(img_path) if img is None: return [] gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(64, 64) ) # faces 是 [ [x, y, w, h], ... ] return img, faces

scaleFactor=1.1表示每次缩放 10%,越小检测越慢但越细致;minNeighbors=5是每个候选矩形至少要有 5 个相邻检测才保留,设低了会出现大量误检,把背景纹理当成人脸。minSize=(64, 64)对 DCGAN 很关键——最终训练图是 64×64,如果检测框本身就小于 64×64,上采样后全是马赛克,这种样本没有训练价值。

Haar 检测完,我会把每个检测框向外扩 20% 再裁剪,因为 Haar 的框往往紧贴脸部,把额头和下巴裁掉一部分。扩边之后再缩放到 64×64,人脸在画面里占比更自然。

3.2 样本清洗与对齐:人脸色块筛选的四个过滤条件

MTCNN 检测完会返回关键点(左眼、右眼、鼻尖、左嘴角、右嘴角),我拿这些关键点做人脸对齐。DCGAN 对对齐非常敏感,同样一套参数,对齐过的数据集能学会清晰五官,没对齐的全是鬼影。

from mtcnn import MTCNN import cv2 import os detector = MTCNN() def align_face(img, keypoints, target_size=64): left_eye = keypoints["left_eye"] right_eye = keypoints["right_eye"] # 计算两眼连线与水平线的夹角 dy = right_eye[1] - left_eye[1] dx = right_eye[0] - left_eye[0] angle = np.degrees(np.arctan2(dy, dx)) center = ((left_eye[0] + right_eye[0]) / 2, (left_eye[1] + right_eye[1]) / 2) rot_mat = cv2.getRotationMatrix2D(center, angle, 1.0) rotated = cv2.warpAffine(img, rot_mat, (img.shape[1], img.shape[0])) # 再按检测框裁切 faces = detector.detect_faces(rotated) if not faces: return None x, y, w, h = faces[0]["box"] # 扩边 20% margin = int(0.2 * w) x = max(0, x - margin) y = max(0, y - margin) w = min(rotated.shape[1] - x, w + 2 * margin) h = min(rotated.shape[0] - y, h + 2 * margin) crop = rotated[y:y+h, x:x+w] return cv2.resize(crop, (target_size, target_size))

人脸的色块筛选是很多人忽略的一步。写真图集里有大量黑白艺术照、重度滤镜图、背景与肤色接近的图,这种样本放进 DCGAN 会让生成器学会“顺着色调画脸”而不是“顺着结构画脸”。我一般对裁剪后的图做三个判断,全部通过才保留:一是灰度方差要大于某个阈值,过滤纯色背景脸;二是 R/G 通道比值要在合理肤色范围,过滤掉明显偏色的图;三是图像熵要大于 5.5,过滤掉模糊图。MTCNN 检测结果里如果confidence低于 0.95,我也直接丢弃,宁可让数据集小一点也不要脏样本。

这些过滤条件在几千张的小数据集上看起来影响不大,但到了十万张规模,它们决定了 DCGAN 的训练是否会在 20 个 epoch 后开始完全崩坏。

4. DCGAN 训练与生成:从旷世到玄学参数的落地

4.1 DCGAN 结构拆解:生成器与判别器的核心层配置

DCGAN 相比原始 GAN 的核心改动,是把判别器和生成器里的全连接层换成了卷积和转置卷积,并且大量使用 BatchNorm 和 LeakyReLU。我训练 64×64 人脸的常用配置是:生成器输入 100 维高斯噪声,经过一层全连接扩展成 128×8×8 的张量,再通过四层转置卷积逐步上采样到 64×64×3。

import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, latent_dim=100): super().__init__() self.fc = nn.Sequential( nn.Linear(latent_dim, 128 * 8 * 8), nn.BatchNorm1d(128 * 8 * 8), nn.ReLU(True) ) self.deconv = nn.Sequential( nn.ConvTranspose2d(128, 64, 4, 2, 1, bias=False), nn.BatchNorm2d(64), nn.ReLU(True), nn.ConvTranspose2d(64, 32, 4, 2, 1, bias=False), nn.BatchNorm2d(32), nn.ReLU(True), nn.ConvTranspose2d(32, 3, 4, 2, 1, bias=False), nn.Tanh() ) def forward(self, z): x = self.fc(z) x = x.view(-1, 128, 8, 8) return self.deconv(x)

每一层ConvTranspose2d的kernel_size=4, stride=2, padding=1是 DCGAN 原文的标准配置,它能让特征图尺寸精确翻倍:8×8 → 16×16 → 32×32 → 64×64。最后一层用Tanh而不是ReLU,因为 Tanh 输出范围是 -1 到 1,和图片归一化到 [-1, 1] 匹配;如果用 ReLU,生成图像素的分布会被截断在正半轴,训练早期会出现大片灰黑色块。

判别器反过来,输入 64×64×3 的图,通过四层卷积下采样到 8×8,再全连接输出一个实数表示真假概率。注意判别器里不能用 BatchNorm 跑得太激进,容易导致训练不稳定;我一般只在卷积层后面跟 BatchNorm,输入层不接。

4.2 训练脚本与参数表:让 GAN 在 64×64 图上稳定收敛

DCGAN 的“玄学参数”其实大部分都被论文定死了。latent_dim 用 100,学习率固定 0.0002,Adam 的 beta1 必须是 0.5 而不是默认的 0.9。这个细节几乎每个复现 DCGAN 翻车的人都会中招,beta1 太大会让梯度动量过大,判别器 loss 直接冲到负数,生成器跟着崩。

def train_dcgan(loader, gen, disc, epochs=50, device="cuda"): lr = 0.0002 beta1 = 0.5 g_opt = torch.optim.Adam(gen.parameters(), lr=lr, betas=(beta1, 0.999)) d_opt = torch.optim.Adam(disc.parameters(), lr=lr, betas=(beta1, 0.999)) criterion = nn.BCEWithLogitsLoss() fixed_z = torch.randn(64, 100, device=device) # 固定的探针噪声 for epoch in range(epochs): for i, (real_imgs, _) in enumerate(loader): real_imgs = real_imgs.to(device) batch_size = real_imgs.size(0) real_labels = torch.ones(batch_size, 1, device=device) fake_labels = torch.zeros(batch_size, 1, device=device) # ---- 训练判别器 ---- z = torch.randn(batch_size, 100, device=device) fake_imgs = gen(z) d_loss_real = criterion(disc(real_imgs), real_labels) d_loss_fake = criterion(disc(fake_imgs.detach()), fake_labels) d_loss = (d_loss_real + d_loss_fake) / 2 d_opt.zero_grad() d_loss.backward() d_opt.step() # ---- 训练生成器 ---- z = torch.randn(batch_size, 100, device=device) fake_imgs = gen(z) g_loss = criterion(disc(fake_imgs), real_labels) g_opt.zero_grad() g_loss.backward() g_opt.step() # 每个 epoch 保存探针图 with torch.no_grad(): sample = gen(fixed_z).cpu() torchvision.utils.save_image(sample, f"epoch_{epoch}.png", normalize=True, range=(-1, 1))

训练循环里有两个细节比调参更影响稳定性。第一个是判别器每轮先看真图再看假图,两个 loss 各取一半做反向传播,这是原始 GAN 的做法而不是 DCGAN 原文的变体,实践下来在 64×64 人脸上收敛更平滑。第二个是生成器的 loss 用的是判别器对假图的输出和真实标签做 BCE——也就是让假图骗过判别器,而不是让假图和真图的特征距离变近;有人改成 MSE loss 后发现图像更模糊,所以别乱换。

参数表通常照着这个基准调就够用。

参数推荐值说明
latent_dim100高斯噪声维度,过小容易模式崩塌
lr0.0002生成器与判别器共用,别单独调高
beta10.5Adam 动量因子,默认 0.9 会导致训练发散
batch_size64单卡 8G 显存可以稳定跑
图像尺寸64×64数据量和算力的平衡点
训练轮数50~80超过 100 轮后判别器通常会过强

我一般会在训练到 20、40、60 轮时各停一次,用 matplotlib 把探针图拼成一张大图看趋势。如果 20 轮时还是一团糊,正常;但如果 40 轮时依然完全没有人脸轮廓,就去查数据集里是不是混入了大量非人脸样本,而不是继续加轮数。

5. 避坑与排查:采集、清洗、训练三段最常见的翻车点

5.1 爬虫阶段的翻车与限速策略

现象一:同一个 IP 爬半小时后,所有请求都返回 403,连首页都打不开。原因是请求频率过高,站点做了基于 IP 的访问计数限流。解决方法是把并发线程降到 2,并在每次请求后随机 sleep 0.3 到 1 秒。有人第一反应是建代理池,但对套图站来说,单机限速比代理池可靠得多,代理池里大量匿名代理的延迟和连接失败率反而会让你的下载脚本充满异常分支。

现象二:下载下来的图片全是 1KB 左右的空白图。原因是图片 URL 需要带 Referer 才能访问,漏了 Referer 时服务器会返回一张占位图。解决方式是把下载请求的headers["Referer"]设为专辑页 URL,并且把len(resp.content) > 10000作为过滤条件。这也是我前面代码里把这两个防御写死的原因。

现象三:xpath 匹配不到任何节点,但浏览器里明明能看到图片。原因是站点是前端 JS 渲染的图片列表,requests 拿到的 HTML 里只有空壳 div。这时的常见做法是用 Selenium 或 Playwright 做渲染后再解析,但更省事的替代方案是直接在浏览器开发者工具里搜m3u8或.jpg,往往能在接口请求里找到后端返回的 JSON 图片地址;直接请求那个 JSON 接口通常比渲染整个页面快得多。

5.2 人脸检测与数据集构建的坑

现象一:Haar Cascade 把背景里的镂空花纹、车窗反光当成人脸框出来了。原因是minNeighbors太低,默认 5 还是压不住某些纹理。解决方法是把minNeighbors提到 8~10,同时把minSize提到 80×80,因为 DCGAN 训练图是 64×64,检测框小于 80×80 的样本即使裁剪出来,上采样后也缺少有效五官信息。

现象二:MTCNN 检测结果很好,但裁剪后的人脸很多是斜的、眼睛不在同一水平线。原因是直接按检测框裁剪,忽略了关键点旋转。解决方法是先按左右眼连线角度做warpAffine旋转,再裁剪。这一步不做,DCGAN 训练出来的五官永远是歪的,且生成器会把这种歪斜当成一种默认风格,无法通过调参修正。

现象三:训练集里出现了大量重复图片。原因是爬虫下载阶段用 URL 的 MD5 去重,但同一个图在站点上有多个 URL(比如缩略图和原图地址不同),下载到了同一张图的不同尺寸版本。解决方式是对裁剪后的人脸图再次做 perceptual hash(感知哈希)去重,两张图的 pHash 汉明距离小于 5 就删掉一张。这个坑几乎不影响小数据集,但数据集到了五万张以上,重复样本会让 DCGAN 训练曲线看起来正常、生成结果却总是那几张面孔。

5.3 DCGAN 训练阶段的稳定与崩溃问题

现象一:训练到第 10 个 epoch 左右,判别器 loss 突然变成 0.000,生成器 loss 飙升。原因是学习率偏大或者 beta1 用了默认 0.9,判别器快速收敛到完美区分真假,梯度直接消失。解决方法是把 lr 调回 0.0002、beta1 调成 0.5,并且给判别器训练加 label smoothing(真标签用 0.9 而不是 1.0)。记着:DCGAN 的判别器不是越强越好,强到无敌的时候生成器就废了。

现象二:生成图像从第 5 个 epoch 开始就长一个样,同一批探针噪声生成出来的脸基本没差异,换了噪声还是类似五官。这是典型的 mode collapse,原因通常不是超参,而是数据集本身太单一——套图站同一个摄影师的图集,光线、角度、构图高度相似,DCGAN 学到的分布被压缩到少数几个模式。解决方法是把训练集扩展到至少两个不同摄影师的图集,并加入随机水平翻转、±5 度旋转的数据增强。翻转对 DCGAN 尤其重要,因为写真图集里人脸本就对称。

现象三:训练到 80 轮,生成图依然带着明显网格纹理或者颜色条纹。原因是生成器最后一层ConvTranspose2d的步长设置为 2,转置卷积会留下棋盘伪影。解决方法是把最后一层换成分数步长卷积(nn.Upsample+ 普通卷积),或者接受 64×64 的训练尺寸里伪影可以被 3×3 卷积掩盖。这是 DCGAN 的先天毛病,不算 bug,但如果你的目标是生成 128×128 甚至更高分辨率,必须换用带残差结构的生成器。

现象四:训练显存占用一直在涨,第 30 轮时直接 Out of Memory。原因是 DataLoader 的num_workers设得太大或者开了pin_memory=True却没把 batch_size 调小。解决方式是 batch_size 从 64 降到 32,num_workers=4,并确认每张图加载后已经 resize 到 64×64 而不是原图尺寸。

6. 用探针图与 FID 验证生成效果:别让 DCGAN 成为黑匣子

训练好 DCGAN 之后,最忌讳的做法是看一眼最后几个 epoch 的探针图觉得“还行”就收工。探针图只能看出生成的人脸有没有五官、整体是否自然,看不出多样性好不好、有没有真实感。我的习惯是同时做三层验证:固定探针图对比、生成图多样性统计、FID 指标估算。

第一层是固定探针法。训练脚本里固定了一个 64×100 的噪声矩阵,每个 epoch 存一次生成图。把这些图按行拼成动画或长图,你能清楚看到训练从噪声到人脸的演化过程。如果演化在第 20 轮后突然停滞,但判别器 loss 还在下降,说明生成器已经学不动了,该提前停止,而不是硬跑满 100 轮。

第二层是多样性和真实感量化。生成 5000 张 64×64 人脸图,先计算它们的像素标准差——如果标准差小于全训练集的 1/3,说明生成器在“模仿均值脸”,多样性不够。再把这 5000 张图重新喂给 MTCNN,统计人脸检出率;如果检出率低于 60%,说明生成的人脸在检测器眼中不像人脸,模型大概率还没收敛。这套方法不需要额外依赖,比 FID 更容易先跑起来。

第三层才是 FID。FID 需要用到 InceptionV3 在 ImageNet 上的预训练权重,虽然它并不是专门为人脸设计的,但作为分布距离的度量依然有效。我通常会算 FID 时把测试集限定在 5000 张随机采样的人脸上,避免全量计算拖慢速度。

import torchvision from torchvision import transforms from scipy.linalg import sqrtm import numpy as np def calculate_fid(real_loader, gen, device, num_samples=5000): # 这里用简化的特征提取:把图片先缩放到 299x299,再走 InceptionV3 # 生产环境直接用 pytorch_fid 库的 FID 类更省事 inception = torchvision.models.inception_v3( weights=torchvision.models.Inception_V3_Weights.IMAGENET1K_V1, transform_input=False ).to(device) inception.eval() # 省略中间特征层输出,实际使用可换成 penultimate_layer ...

FID 数值在 64×64 人脸上,训练集内部一般能到 30 以下,跨数据集对比没有绝对标准,只看同一个数据集下不同训练方案的相对大小。我在实际项目中吃过这个亏:第一次训完 FID 算出来 55,觉得高得离谱,后来发现是 FID 计算时把真实图和生成图都做了中心裁剪,把本来就不大的 64×64 图裁成了 48×48,特征提取质量全丢了。从那以后我养成了一个习惯——先随机抽 8 张真实图和 8 张生成图,把它们拼成同一张对比图看一遍再算指标,指标是给人做决策的,但眼睛永远比指标先发现问题。

这套流水线真正落地时,爬虫、检测、生成三段每个环节都有各自的成熟工具,但把它们串起来后,大部分翻车点其实是数据质量而不是模型参数。希望你跑通后记住:DCGAN 不是黑匣子,它只是对输入数据的分布极其敏感;当你对生成结果不满意,先回看数据集,再动学习率。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询