☰
Python爬虫+人脸检测+DCGAN:从散乱套图到脸部生成模型实战
2026/10/15 12:20:46 网站建设 项目流程

简介:这份资源面向Python机器学习初学者与计算机视觉爱好者,围绕「爬虫采集—人脸识别—DCGAN生成」的完整链路展开,帮助读者理解从写真套图抓取到美女脸部自动生成的实践流程。包内共506个文件,以442张jpg与29张png图片作为训练与识别素材,8个py脚本承载爬虫、人脸检测与DCGAN模型逻辑,另含checkpoint、index、meta等模型权重与索引文件,以及少量txt、xml、bat辅助配置,压缩包约77.58MB,目录结构便于按模块查阅。资源已有1740人学习下载,热度较高。读者可借此了解DCGAN在脸部图像生成中的网络搭建、训练迭代与模型保存方式,观察checkpoint随训练轮次更新的过程,并参考爬虫与识别脚本的衔接思路,为自身的人脸识别或生成式模型项目提供可复用的代码骨架与排错参考。

1. 从一堆散乱套图到能跑的脸部生成模型:这个项目到底在干什么

如果你手头正好有一批来源杂乱的写真套图,想拿它们做点机器学习的事,比如训练一个能自动生成新脸部的模型,那第一道坎往往不是模型本身,而是数据怎么来、怎么清洗、怎么对齐。这个项目就是冲着这条链路去的:用 Python 写爬虫把套图抓下来,再用脸部识别把人脸区域裁出来,最后喂给 DCGAN 训练一个能自动生成脸部图像的模型。它适合两类人:一类是想学爬虫加数据清洗完整流程的新手,另一类是想拿真实图片练 GAN 但苦于没有现成数据集的开发者。整套代码不依赖什么高深框架,核心就是 requests、OpenCV 或者 face_recognition 加 PyTorch,跑起来门槛不高,但中间有几个坑我后面会细说。

2. 技术选型与目录结构:为什么是爬虫 + 人脸检测 + DCGAN 这条线

2.1 三个模块各用什么库,为什么这么选

这个项目的技术栈拆开看其实很朴素,但每一块都有它存在的理由。爬虫部分用 requests 加 BeautifulSoup 或者 lxml,不用 Scrapy 是因为目标站点结构通常不复杂,写个几十行的脚本就能把图片链接和分页逻辑搞定,上 Scrapy 反而要配 settings、middlewares,对一个小规模采集任务来说太重了。脸部识别这块常见做法是两条路:一条是 OpenCV 自带的 Haar 级联或者 DNN 人脸检测器,另一条是 face_recognition 库,底层是 dlib 的 HOG 或者 CNN 模型。前者装起来省事,后者精度高一点但编译 dlib 有时候会让人抓狂。DCGAN 部分用 PyTorch 实现,生成器和判别器都是标准的卷积结构,没有加什么花哨的注意力机制,因为项目定位是跑通流程而不是刷指标。

我一般会建议新手先用 OpenCV 的 DNN 检测器,模型文件小、推理快,等流程跑通了再换 face_recognition 对比效果。DCGAN 这边,PyTorch 的 nn.Conv2d、nn.ConvTranspose2d、nn.BatchNorm2d 这几个模块就够搭出生成器和判别器了,损失函数用 BCELoss,优化器用 Adam,学习率设 0.0002,beta1 设 0.5,这是 DCGAN 论文里的经典配置,别乱改。

2.2 目录结构长什么样,每个文件负责什么

一个能跑的项目,目录结构清晰比代码写得多巧妙更重要。这个项目我拆过之后,典型的组织方式是这样的:

project/ ├── crawler/ │ ├── spider.py # 爬虫主逻辑,负责翻页和图片链接提取 │ └── downloader.py # 图片下载与去重,带重试机制 ├── face_process/ │ ├── detect.py # 人脸检测与裁剪,输出对齐后的人脸图 │ └── align.py # 可选,做眼睛对齐和尺寸归一化 ├── dcgan/ │ ├── model.py # 生成器和判别器定义 │ ├── train.py # 训练循环,含 checkpoint 保存 │ └── generate.py # 加载模型生成新脸部图像 ├── data/ │ ├── raw/ # 爬虫下载的原图 │ └── faces/ # 裁剪后的人脸图,训练集来源 ├── config.py # 路径、超参数、检测阈值集中管理 └── requirements.txt # 依赖清单

这个结构的好处是每一层职责单一,爬虫出问题不会影响训练,人脸裁剪效果不好也方便单独调。config.py 把路径和超参数抽出来,换数据集或者调 batch size 不用满项目找变量。requirements.txt 里至少要写清楚 torch、torchvision、opencv-python、requests、beautifulsoup4、numpy、Pillow 这几个,版本号建议锁一下,尤其是 torch 和 opencv,不同版本 API 有差异。

2.3 数据流从爬虫到训练集是怎么串起来的

整个数据流是这样的:spider.py 从列表页拿到详情页链接,再进详情页提取图片地址,交给 downloader.py 下载到 data/raw/。下载的时候要做两件事,一是按 URL 的 hash 或者文件名去重,二是记录失败链接方便重试。raw 目录攒够一定数量后,detect.py 遍历每张图,用人脸检测器找到人脸框,按框裁剪并缩放到统一尺寸,比如 128x128 或者 64x64,存到 data/faces/。这里有个细节,裁剪时可以适当往外扩一点边距,不然脸太贴边,训练出来的生成图边缘会很奇怪。最后 train.py 用 ImageFolder 加载 data/faces/,做归一化到 [-1, 1],因为生成器最后一层是 tanh,输出范围就是 [-1, 1],输入不匹配的话训练会很不稳定。

3. 爬虫与下载模块:从列表页到本地图片的完整实现

3.1 爬虫主逻辑怎么写才不容易翻车

爬虫这块最怕的是写得太脆,站点稍微改个 class 名就全挂。我的习惯是把选择器集中放在 config 或者单独一个 selectors 字典里,方便改。下面是一个简化但能跑的 spider 核心逻辑:

import requests from bs4 import BeautifulSoup from urllib.parse import urljoin import time import os HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0 Safari/537.36" } def get_detail_links(list_url, page_count): """从列表页提取详情页链接,支持翻页""" detail_links = [] for page in range(1, page_count + 1): url = f"{list_url}?page={page}" resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") # 选择器根据实际站点调整,这里用 a.tag 举例 for a in soup.select("a.detail-link"): href = a.get("href") if href: detail_links.append(urljoin(list_url, href)) time.sleep(1) # 控制频率,别把人家站点打挂 return detail_links def get_image_urls(detail_url): """从详情页提取大图地址""" resp = requests.get(detail_url, headers=HEADERS, timeout=10) resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") img_urls = [] for img in soup.select("div.content img"): src = img.get("src") or img.get("data-src") if src: img_urls.append(urljoin(detail_url, src)) return img_urls

这段代码里几个关键点:timeout 必须设,不然网络一卡整个脚本就挂住;resp.encoding 用 apparent_encoding 是为了防止中文乱码;time.sleep(1) 是基本的礼貌,也能降低被封的概率。选择器 a.detail-link 和 div.content img 只是示例,实际用的时候要打开开发者工具看目标站点的结构,换成真实的 class 或标签。

3.2 图片下载与去重:别让重复图和坏图污染数据集

下载环节最容易出的问题是重复下载和半截文件。去重可以用 URL 的 md5 做文件名,这样同一张图不管出现在几个页面都只会存一份。下载时用流式写入,并且检查 Content-Length 和实际写入大小是否一致,不一致就删掉重试。下面是一个带重试的下载函数:

import hashlib from requests.exceptions import RequestException def download_image(img_url, save_dir, retry=3): """下载单张图片,带重试和去重""" if not os.path.exists(save_dir): os.makedirs(save_dir) # 用 URL 的 md5 作为文件名,天然去重 name = hashlib.md5(img_url.encode()).hexdigest() + ".jpg" save_path = os.path.join(save_dir, name) if os.path.exists(save_path): return save_path # 已存在,跳过 for i in range(retry): try: resp = requests.get(img_url, headers=HEADERS, timeout=15, stream=True) if resp.status_code == 200: with open(save_path, "wb") as f: for chunk in resp.iter_content(1024): f.write(chunk) # 简单校验文件大小,太小的可能是坏图 if os.path.getsize(save_path) < 1024: os.remove(save_path) continue return save_path except RequestException as e: print(f"下载失败 {img_url},第 {i+1} 次重试:{e}") time.sleep(2) return None

参数说明:retry 控制重试次数,一般 3 次够了;stream=True 配合 iter_content 避免大图一次性读进内存;文件小于 1KB 的基本是错误页或者占位图,直接删掉。这个函数返回 None 的时候,调用方应该把失败的 URL 记到一个 failed.txt 里,方便后面单独补抓。

3.3 反爬与频率控制:哪些做法是必须的,哪些是过度设计

反爬这块,最基本的就三样:User-Agent 别用默认的、请求间隔别太短、失败别死循环。User-Agent 准备几个轮换就行,不用上什么代理池,小规模采集没必要。请求间隔我一般设 1 到 2 秒,配合随机抖动,比如 time.sleep(1 + random.random())。如果遇到 403 或者 429,先停下来,把间隔调大,而不是换 IP 硬刚。还有一个容易忽略的点是 Referer,有些站点会检查,加上详情页的 URL 作为 Referer 通常能过。至于验证码,如果目标站点有,那这个项目就不适合继续了,换数据源比硬破验证码划算得多。

4. 人脸检测与裁剪:把 raw 图变成 DCGAN 能吃的训练集

4.1 用 OpenCV DNN 做检测的完整代码

人脸检测这一步决定了后面生成质量的上限。如果裁出来的脸歪七扭八,DCGAN 学出来的东西也会是扭曲的。OpenCV 的 DNN 模块加载预训练模型就能用,不需要额外编译。下面是一个批量处理的脚本:

import cv2 import os import glob # 加载 OpenCV DNN 人脸检测模型 prototxt = "deploy.prototxt" model = "res10_300x300_ssd_iter_140000.caffemodel" net = cv2.dnn.readNetFromCaffe(prototxt, model) def detect_and_crop(img_path, output_dir, size=128, conf_threshold=0.6): """检测人脸并裁剪保存""" img = cv2.imread(img_path) if img is None: return False h, w = img.shape[:2] # 构造 blob,注意输入尺寸和均值 blob = cv2.dnn.blobFromImage(cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0)) net.setInput(blob) detections = net.forward() saved = False for i in range(detections.shape[2]): confidence = detections[0, 0, i, 2] if confidence < conf_threshold: continue box = detections[0, 0, i, 3:7] * [w, h, w, h] x1, y1, x2, y2 = box.astype("int") # 适当扩边,避免脸太贴边 pad = int(0.15 * (x2 - x1)) x1 = max(0, x1 - pad) y1 = max(0, y1 - pad) x2 = min(w, x2 + pad) y2 = min(h, y2 + pad) face = img[y1:y2, x1:x2] if face.size == 0: continue face = cv2.resize(face, (size, size)) name = os.path.splitext(os.path.basename(img_path))[0] out_path = os.path.join(output_dir, f"{name}_{i}.jpg") cv2.imwrite(out_path, face) saved = True return saved

参数说明:conf_threshold 设 0.6 是平衡漏检和误检,太低会把背景里的花纹也当脸,太高会漏掉侧脸。pad 取人脸框宽度的 15% 左右,这个比例是我试过比较顺眼的,太小生成图边缘会有硬切感,太大又会引入太多背景。size 要和 DCGAN 的输入尺寸一致,训练用 128 就裁 128,别裁完再在训练时缩放,多一次插值多一次失真。

4.2 裁剪后的清洗:模糊图、多脸图、非人脸图怎么处理

检测完不等于就能直接训练。我一般会再过一遍清洗,主要做三件事。第一,用拉普拉斯方差过滤模糊图,方差低于某个阈值(比如 100)的直接删掉,模糊图会让生成器学到糊成一团的纹理。第二,一张原图里检测出多张脸的,要么只保留置信度最高的那张,要么全部保留但打上不同后缀,看你的数据量决定,数据少就全留。第三,人工抽检一批,把明显不是脸的误检删掉,这一步没法完全自动化,但抽个一两百张看看分布,心里有数。清洗完的 data/faces/ 才是最终训练集,建议再统计一下图片数量,DCGAN 至少需要几千张才能看出点样子,几百张也能跑但生成效果会很有限。

4.3 对齐与归一化:为什么这一步不能省

如果只做裁剪不做对齐,人脸在框里的位置和角度是随机的,DCGAN 要花很多容量去学这些变化,生成质量会下降。简单做法是用眼睛位置做仿射变换,把两只眼睛摆到固定坐标。face_recognition 库的 face_landmarks 能直接拿到眼睛坐标,OpenCV 也可以用级联检测眼睛。对齐之后再做归一化,像素值从 [0, 255] 转到 [-1, 1],用 img / 127.5 - 1.0 这个公式。这一步和生成器最后一层的 tanh 是配套的,输入输出范围一致,训练才稳。归一化后的图存成 npy 或者直接在 Dataset 里做都行,我习惯在 Dataset 的getitem里做,省磁盘空间。

5. DCGAN 模型与训练:生成器和判别器的代码拆解

5.1 生成器结构:从 100 维噪声到 128x128 人脸

DCGAN 的生成器是把一个 100 维的噪声向量逐步上采样成图片。核心是 ConvTranspose2d 加 BatchNorm 加 ReLU,最后一层用 tanh。下面是一个适配 128x128 输出的生成器:

import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, nz=100, ngf=64, nc=3): super(Generator, self).__init__() self.main = nn.Sequential( # 输入 nz x 1 x 1,输出 ngf*8 x 4 x 4 nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # 输出 ngf*4 x 8 x 8 nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # 输出 ngf*2 x 16 x 16 nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # 输出 ngf x 32 x 32 nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf), nn.ReLU(True), # 输出 ngf x 64 x 64 nn.ConvTranspose2d(ngf, ngf, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf), nn.ReLU(True), # 输出 nc x 128 x 128 nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False), nn.Tanh() ) def forward(self, x): return self.main(x)

参数说明:nz 是噪声维度,100 是 DCGAN 论文的默认值,改成 128 或 256 也可以,但别太小,太小生成多样性不够。ngf 是生成器特征图基数,64 起步,显存不够就降到 32。nc 是输出通道,彩色图是 3。每一层 ConvTranspose2d 的 kernel_size 都是 4,stride 是 2,padding 是 1,这样每次上采样尺寸翻倍,从 4x4 一路到 128x128。BatchNorm 在生成器里每一层都加,但输出层不加,因为 tanh 前加 BN 会破坏输出范围。

5.2 判别器结构:为什么用 LeakyReLU 和 Dropout

判别器是生成器的逆过程,用 Conv2d 逐步下采样,最后输出一个标量概率。DCGAN 的判别器有个特点,用 LeakyReLU 而不是 ReLU,斜率 0.2,这是为了防止梯度稀疏。另外可以在中间层加 Dropout,增加一点正则化。代码如下:

class Discriminator(nn.Module): def __init__(self, nc=3, ndf=64): super(Discriminator, self).__init__() self.main = nn.Sequential( # 输入 nc x 128 x 128,输出 ndf x 64 x 64 nn.Conv2d(nc, ndf, 4, 2, 1, bias=False), nn.LeakyReLU(0.2, inplace=True), # 输出 ndf*2 x 32 x 32 nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplace=True), # 输出 ndf*4 x 16 x 16 nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplace=True), # 输出 ndf*8 x 8 x 8 nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplace=True), # 输出 ndf*8 x 4 x 4 nn.Conv2d(ndf * 8, ndf * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplace=True), # 输出 1 x 1 x 1 nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False), nn.Sigmoid() ) def forward(self, x): return self.main(x).view(-1, 1)

判别器第一层不加 BatchNorm,这是 DCGAN 的惯例,因为输入是原始像素,加 BN 会破坏像素间的相对关系。最后一层用 Sigmoid 输出 0 到 1 的概率,配合 BCELoss。view(-1, 1) 是把输出展平成 batch_size x 1,方便算损失。

5.3 训练循环:损失函数、优化器和 checkpoint 保存

训练循环是 GAN 最容易翻车的地方,生成器和判别器的更新要交替进行,而且判别器通常要多更新几次或者用标签平滑。下面是一个稳定的训练循环骨架:

import torch.optim as optim from torchvision.utils import save_image def train(dataloader, netG, netD, epochs=50, lr=0.0002, beta1=0.5, nz=100): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") netG.to(device) netD.to(device) criterion = nn.BCELoss() optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999)) optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999)) fixed_noise = torch.randn(64, nz, 1, 1, device=device) for epoch in range(epochs): for i, (real_imgs, _) in enumerate(dataloader): real_imgs = real_imgs.to(device) batch_size = real_imgs.size(0) # 训练判别器:真图标签为 1,假图标签为 0 netD.zero_grad() label_real = torch.full((batch_size, 1), 1.0, device=device) output_real = netD(real_imgs) lossD_real = criterion(output_real, label_real) noise = torch.randn(batch_size, nz, 1, 1, device=device) fake_imgs = netG(noise) label_fake = torch.full((batch_size, 1), 0.0, device=device) output_fake = netD(fake_imgs.detach()) lossD_fake = criterion(output_fake, label_fake) lossD = lossD_real + lossD_fake lossD.backward() optimizerD.step() # 训练生成器:希望判别器把假图判为真 netG.zero_grad() output_fake = netD(fake_imgs) label_gen = torch.full((batch_size, 1), 1.0, device=device) lossG = criterion(output_fake, label_gen) lossG.backward() optimizerG.step() # 每个 epoch 保存一张生成样本图,方便观察进度 with torch.no_grad(): fake_samples = netG(fixed_noise) save_image(fake_samples, f"output/epoch_{epoch}.png", normalize=True) # 保存 checkpoint torch.save(netG.state_dict(), f"checkpoint/netG_epoch_{epoch}.pth") torch.save(netD.state_dict(), f"checkpoint/netD_epoch_{epoch}.pth")

参数说明:lr 设 0.0002,beta1 设 0.5,这是 DCGAN 论文的推荐值,beta1 比默认的 0.9 小,是为了减少动量,让训练更稳。判别器训练时对假图用了 detach(),防止梯度传到生成器。fixed_noise 是固定的一组噪声,每个 epoch 用同一组噪声生成图片,方便对比生成质量的变化。checkpoint 每个 epoch 都存,GAN 训练经常中间某个 epoch 效果最好,后面反而崩了,存下来可以回头挑。

6. 避坑与排查:训练 GAN 时最常见的五个翻车现场

6.1 生成器输出全是噪点或者同一张脸

现象是训练几十个 epoch 后,生成的图要么是雪花噪点,要么所有噪声输入都生成几乎一样的脸。原因通常是模式崩溃,判别器太强,生成器找不到梯度方向。解决办法是把判别器的学习率调低,或者减少判别器的更新次数,比如每更新两次生成器才更新一次判别器。另外检查一下输入归一化是不是 [-1, 1],如果输入是 [0, 1] 而生成器输出 tanh 的 [-1, 1],判别器很容易区分真假,生成器就学不动。

6.2 损失值震荡剧烈,D loss 掉到 0

现象是判别器损失迅速降到接近 0,生成器损失飙升。原因是判别器过强,把真假分得太开。解决方法是给判别器的标签加噪声,真图标签用 0.9 到 1.0 之间的随机数,假图标签用 0.0 到 0.1 之间的随机数,这叫标签平滑,能缓解判别器过度自信。另外可以给判别器加 Dropout,或者对输入加一点高斯噪声。

6.3 人脸裁剪后大量黑边或者只有半张脸

现象是 data/faces/ 里很多图边缘是黑的,或者脸被切掉一半。原因是检测框坐标越界或者扩边时没做边界裁剪。解决方法是裁剪前把 x1、y1 限制在 0 以上,x2、y2 限制在图像宽高以内,扩边之后也要再限制一次。另外检测置信度阈值别设太低,0.5 以下容易把误检框也裁进来。

6.4 训练时显存爆了,batch size 只能设很小

现象是跑 train.py 时报 CUDA out of memory。原因是 128x128 的图加上生成器和判别器的参数量,显存占用不小。解决办法有三个:把 ngf 和 ndf 从 64 降到 32,把 batch size 降到 16 甚至 8,或者把图片尺寸从 128 降到 64。如果显存实在紧张,可以用混合精度训练,torch.cuda.amp 能省不少显存,但代码要改几行。

6.5 爬虫跑一半被封,或者下载的图全是缩略图

现象是爬了几百张之后请求全部返回 403,或者下载下来的图尺寸很小、带水印。原因是请求频率太高触发了反爬,或者详情页里提取的是缩略图地址而不是原图地址。解决办法是加大请求间隔,加上 Referer 头,检查 img 标签的 src 是不是缩略图,原图可能在 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

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

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

立即咨询