简介:面向Python机器学习爱好者与图像生成方向开发者,这份资源围绕“爬虫采集写真图片—人脸识别定位—DCGAN自动生成”完整流程展开,将数据获取、预处理与生成式模型训练串联成一个可参考的实战案例。压缩包共506个文件,以442张jpg写真图、29张png图片为主要数据素材,附带8个py脚本用于爬虫、人脸检测与DCGAN训练,同时包含模型checkpoint、index/meta等TensorFlow模型文件以及txt、xml说明文件,整体包体约77.58MB,结构上兼顾了数据集、训练代码与模型参数。目前资源已被1736人学习,适合希望从零搭建人脸生成项目、理解数据准备到生成效果的初学者。通过这套资料,可以直观看到写真套图爬虫的采集逻辑、基于人脸识别的裁剪与对齐方式,以及DCGAN迭代生成人脸的过程,并可借助已有模型文件快速验证生成效果或继续调参训练。 这个项目不是那种纯练习题,而是一条真正能跑通的数据闭环:从公开图库采集人像图片,用OpenCV把人脸检测并裁剪出来,再丢给DCGAN训练出一个能自动生成人脸图像的网络。输入一个随机噪声向量,模型就能吐出一张看起来还挺像样的全新人脸。别人听了可能觉得不就是把爬虫和深度学习拼在一起嘛,对,结果确实不复杂,但中间串起来的过程,才是真正涨经验的地方。
这类组合非常适合作为进阶练手项目,因为一个项目同时覆盖了网络数据采集、图像预处理、深度学习训练三块硬技能。如果你已经掌握基础Python语法、想把爬虫往实际方向进阶,或者刚开始接触生成对抗网络、想亲手跑一次GAN训练,这篇应该能给你一份比较完整的参考。我按自己实际操作顺序,把几个环节的选型逻辑、代码、参数和踩坑记录全部写出来。
1. 项目整体设计与技术选型
1.1 一条完整的数据到生成流水线
整个项目本质上是一条“数据原料采集 → 数据清洗 → 模型训练 → 内容生成”的流水线。先用爬虫从合法的公开图库或开放数据集页面抓取原始图片,然后对每张图做人脸检测,把脸部区域单独裁剪出来并统一缩放尺寸,最后把这些经过对齐的人脸图片整理成训练集,输入DCGAN进行训练。
有人可能会问,为什么不直接拿原始图片训练?原因很直接:DCGAN生成的图片分辨率通常在64x64或128x128,如果训练图里有人脸、有全身、有背景,模型会学得乱七八糟,生成结果大概率是一堆模糊的“鬼影”。所以人脸检测这一步不是锦上添花,而是决定模型最终效果的关键前置环节。数据质量决定模型上限,这个原则在这个项目里体现得特别明显。
1.2 技术栈选型及其取舍逻辑
选型上我没有追求最新最猛的工具,而是选了每个环节最“皮实”的方案:
- 爬虫使用 requests + BeautifulSoup。目标如果是静态网页,这两个库就够用了,不需要一上来就上 Scrapy 那样的大框架,先把请求、解析、存储这套逻辑跑顺再说。
- 人脸检测使用 OpenCV 内置的 Haar Cascade。虽然检测精度和 MTCNN、RetinaFace 这些深度学习方案有差距,但零额外模型加载成本,CPU 上也能跑得很快,对入门阶段来说性价比最高。后面如果想提升准确率,可以无缝换成 MTCNN。
- 生成模型使用 DCGAN。相比原始 GAN,它把全连接层换成了卷积结构,训练稳定性好很多,在64x64分辨率下已经能生成轮廓清晰的人脸,是最适合理解生成对抗网络运作逻辑的框架。
这套组合还有一个好处是环境依赖简单,基本只涉及 requests、beautifulsoup4、opencv-python、torch、torchvision 这几个库,新手照着装也不容易出问题。
提示:如果机器没有独立显卡,CPU 训练 DCGAN 跑 64x64 分辨率、几百张图片,单轮时间也在可接受范围内,只是训练轮数会受些影响。个人学习场景下,CPU跑通整个流程完全可行。
2. 数据采集:爬虫部分的完整实现
2.1 基础爬虫框架:requests 搭配 BeautifulSoup
爬虫部分是整条流水线的“原材料采购员”,职责是稳定的抓取图片并保存到本地。我用的基础框架很简单:requests 负责发 HTTP 请求,BeautifulSoup 负责解析 HTML。核心代码大致是下面这个样子。
import requests from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", "Referer": "https://example.com/" } resp = requests.get("https://example.com/gallery", headers=headers, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "html.parser") img_tags = soup.select("img.photo") for img in img_tags: src = img.get("src") or img.get("data-src") # 有些站点懒加载,真实地址在data-src里 if not src: continue if src.startswith("//"): src = "https:" + src elif src.startswith("/"): src = "https://example.com" + src print(src)这里有几个小地方值得注意。resp.encoding = resp.apparent_encoding 是为了避免中文页面乱码,很实用。用 img.get("src") or img.get("data-src") 是因为很多图集站对图片做了懒加载,真实地址藏在>import time import random USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/120.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Firefox/121.0", "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ... Mobile Safari", ] def safe_get(url, retries=3): for i in range(retries): try: headers = {"User-Agent": random.choice(USER_AGENTS)} resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp except requests.RequestException as e: print(f"[retry {i+1}] {e}") time.sleep(random.uniform(1, 3)) return None
随机延迟是最笨但最有效的办法,把请求间隔控制在 1 到 3 秒之间随机浮动,能躲过绝大多数简单的频率检测。重试机制也很重要,网络请求偶发超时太正常了,不加重试的话整个采集过程很容易中途挂掉。高并发场景下才需要上代理池,个人学习项目用上面的逻辑足够了。
2.3 数据合规:这条红线先说清楚
数据采集的合规性必须放在最前面。做这个项目的默认前提是:只能采集明确公开且允许抓取的图片资源,必须遵守目标网站的 robots.txt 和服务条款,采集的数据仅限于个人技术学习,不能直接对外传播,更不能用于商业用途。
人脸数据还有一层额外的肖像权问题。如果爬取的是真实人物照片,即便只是用于本地训练,模型生成的图片若与真实人物相似,公开传播也可能带来法律风险。所以我的建议是:把爬虫练习目标和训练数据来源分开。爬虫部分可以用任何公开、无版权的图片站来练手;而训练DCGAN使用的数据,建议直接选用学术领域公开的人脸数据集,比如 CelebA、FFHQ 的开放版本。这样既能保证模型效果,又不用背着肖像权包袱。
注意:合规数据源的优先级永远高于模型效果。没有合适数据就换数据源,不要冒着合规风险去抓取受保护的内容。
3. 脸部识别与数据集制作
3.1 人脸检测方案选型与参数调整
采集到原始图片后,下一步就是用脸部识别技术把人脸区域从图片中切出来。OpenCV 的 Haar Cascade 检测器在 CPU 上很快,而且不需要额外下载模型,代码也非常简洁。
import cv2 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) img = cv2.imread("photo.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(48, 48) ) for (x, y, w, h) in faces: face = img[y:y+h, x:x+w] cv2.imwrite(f"faces/{x}_{y}.jpg", face)detectMultiScale 里几个参数值得多解释两句。scaleFactor 是每次检测时图片缩小的比例,值越接近 1 检测越精细但耗时越长,1.1 是精度和速度的平衡点。minNeighbors 表示每个候选区域至少要被多少个相邻窗口命中才算真的检测到人脸,数值越大误检越少,但可能漏掉侧面或小尺寸人脸,5 左右比较合适。minSize 是检测目标的最小尺寸,如果图片本身包含大量小脸,把它设低一些能提高召回率,不过误检也会变多。
3.2 批量对齐、裁剪与数据存储
检测过程跑完后,得到的是大小不一的人脸图,不能直接拿去训练,需要统一处理成固定尺寸。一般 DCGAN 生成 64x64 图像,我就把所有检测到的人脸 resize 到 64x64。这里必须强调一个实用细节:直接 resize 会把脸拉伸变形,最好先把人脸裁成正方形区域,再做缩放,避免比例失真影响训练效果。
def extract_face(img, x, y, w, h, target_size=64): margin = 0.2 # 四周扩展20%,减少刘海和下巴被切掉的情况 nx = max(0, int(x - w * margin)) ny = max(0, int(y - h * margin)) nw = min(img.shape[1] - nx, int(w * (1 + 2 * margin))) nh = min(img.shape[0] - ny, int(h * (1 + 2 * margin))) cropped = img[ny:ny+nh, nx:nx+nw] square = cv2.resize(cropped, (target_size, target_size)) return squaremargin 扩展是我在实际操作里加进去的,因为人脸检测框经常只包含眉毛到下巴的区域,刘海和部分额头会被切掉,留出 20% 的余量后,裁剪出来的人脸更完整,训练效果明显更好。处理完所有图片后,保存时我建议直接存成 numpy 的 .npy 文件,或者打包成 .h5,这样训练时一次性加载到内存,比训练过程中反复读图片快得多,也避免了频繁 I/O 带来的卡顿。
import numpy as np faces_data = [] for f in sorted(all_files): img = cv2.imread(f) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = img / 255.0 * 2 - 1 # 归一化到[-1, 1] faces_data.append(img) np.save("faces_64.npy", np.array(faces_data)) print("完成,共处理", len(faces_data), "张人脸")如果你发现检测到的人脸数量明显少于图片数量,大概率是明暗对比、侧脸角度或遮挡问题导致漏检。可以考虑换用 MTCNN 或 RetinaFace 替代 Haar Cascade。它们的检测率更高,代价是需要多安装一个库和模型文件,处理速度会慢一些,但对后续模型训练来说是值得的。
4. DCGAN 人脸生成:原理与实战
4.1 先理解生成对抗网络的博弈逻辑
DCGAN 是生成对抗网络(GAN)的卷积版本。它由两个网络组成:生成器 G 和判别器 D。简单理解,G 像一个造假画的画师,给它一个随机噪声向量,它尽量画出以假乱真的人脸;D 像一个鉴定师,负责判断一张图是真实照片还是 G 造出来的“假画”。两人不断博弈,G 越来越擅长画脸,D 也越来越擅长挑破绽,最后收敛时,G 生成的人脸已经能骗过训练集以外的新样本。
这个思路听着抽象,但实际跑起来其实挺直观。训练早期,G 生成的是五颜六色的噪点图,D 一眼就能甄别;经过几十个 epoch 的对抗之后,生成图开始出现人脸轮廓、五官雏形,那种“模型真的在学会画脸”的成就感特别强。
4.2 生成器和判别器的网络结构
PyTorch 里实现 DCGAN 非常简洁。生成器输入一个 100 维的随机高斯噪声,经过全连接层扩大尺寸后 reshape 成 4x4 的特征图,再用四层转置卷积逐步上采样,最终输出 64x64x3 的图片。
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, 512 * 4 * 4), nn.BatchNorm1d(512 * 4 * 4), nn.ReLU(inplace=True), ) self.deconv = nn.Sequential( nn.ConvTranspose2d(512, 256, 4, 2, 1), # 4x4 -> 8x8 nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.ConvTranspose2d(256, 128, 4, 2, 1), # 8x8 -> 16x16 nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.ConvTranspose2d(128, 64, 4, 2, 1), # 16x16 -> 32x32 nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.ConvTranspose2d(64, 3, 4, 2, 1), # 32x32 -> 64x64 nn.Tanh(), ) def forward(self, z): out = self.fc(z).view(-1, 512, 4, 4) return self.deconv(out)转置卷积的尺寸变化可以用公式算:输出尺寸 = (输入尺寸 - 1) * stride - 2 * padding + kernel_size。以第一层为例,输入 4,stride=2,padding=1,kernel=4,输出就是 (4-1)*2 - 2 + 4 = 8,正好翻倍。这个计算很重要,每一层输出尺寸必须和注释里标的一致,接错的话会直接报 shape 不匹配的错。
判别器则是完全对称的结构,用普通卷积逐步下采样,最后输出一个概率值。
class Discriminator(nn.Module): def __init__(self): super().__init__() self.conv = nn.Sequential( nn.Conv2d(3, 64, 4, 2, 1), # 64x64 -> 32x32 nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(64, 128, 4, 2, 1), # 32x32 -> 16x16 nn.BatchNorm2d(128), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(128, 256, 4, 2, 1), # 16x16 -> 8x8 nn.BatchNorm2d(256), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(256, 1, 4, 1, 0), # 8x8 -> 1 nn.Sigmoid(), ) def forward(self, x): return self.conv(x).view(-1, 1)判别器里用 LeakyReLU 而不是普通 ReLU,是因为负半轴的小斜率能避免神经元死亡,这个对训练稳定性影响很大。生成器里用 ReLU 加 BatchNorm 则能加快收敛。
4.3 训练循环细节与关键参数
训练部分是整个项目最磨人的环节。损失函数用 BCEWithLogitsLoss 或者 BCE Loss,优化器统一使用 Adam,学习率建议设为 0.0002,beta1 设为 0.5 而不是默认的 0.9。这个改动很关键,因为Adam 的默认参数在 GAN 训练中容易震荡,beta1 取 0.5 会让动量衰减更快,训练更稳定。
训练循环的核心逻辑是:每次迭代先训练判别器 D,用真实图片和生成图片各算一次损失;再训练生成器 G,目标是让生成图片被 D 判断为真实的概率尽量高。实际代码中,常见做法是每更新一次 G 就更新一次 D,但如果你发现 D 的损失降得太快而 G 跟不上,可以反过来每个 epoch 让 G 多更新几次。
输入图片的数值范围也要注意。数据准备阶段我把图片归一化到 [-1, 1],因为生成器最后用 Tanh 输出,输出范围正好是 [-1, 1],两边的区间一致,模型更容易拟合。
criterion = nn.BCELoss() opt_G = torch.optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999)) opt_D = torch.optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999)) for epoch in range(epochs): for real_img in dataloader: batch_size = real_img.size(0) real_label = torch.ones(batch_size, 1) fake_label = torch.zeros(batch_size, 1) # 训练判别器 opt_D.zero_grad() pred_real = D(real_img) loss_D_real = criterion(pred_real, real_label) z = torch.randn(batch_size, latent_dim) fake_img = G(z) pred_fake = D(fake_img.detach()) loss_D_fake = criterion(pred_fake, fake_label) loss_D = loss_D_real + loss_D_fake loss_D.backward() opt_D.step() # 训练生成器 opt_G.zero_grad() pred_fake = D(fake_img) loss_G = criterion(pred_fake, real_label) loss_G.backward() opt_G.step()我一般 batch size 取 64,训练 100 个 epoch 左右,每 5 个 epoch 存一次生成样例图片。如果你发现生成的人脸非常模糊,可能是分辨率低,可以把数据提高到 128x128,但代价是显存占用和训练时间都会显著上升。个人项目在 64x64 这个档位先跑通就够了。
5. 训练过程中的常见问题与排查
5.1 模式崩溃
模式崩溃是所有 GAN 新手最容易遇到的现象:训练一段时间后,生成的图总是那么两三张脸,甚至完全一样,多样性消失了。原因是生成器找到了判别器目前的“盲区”,只要生成某几种特定样式就能稳定骗过 D,它就不再努力拓宽绘画范围。
针对模式崩溃,我实测有效的办法有几个。第一,降低学习率,别让 G 更新步子迈得太大。第二,给判别器的真实标签做标签平滑,把 1 改成 0.9,让 D 不要对自己的判断过分自信。第三,优先从数据上解决,检查训练集中人脸角度、肤色、光照分布是否太单一,数据多样性不足也会加剧模式崩溃。
5.2 损失函数忽高忽低、图像模糊不清
训练不收敛也是家常便饭。最常见的情况是 D 的 loss 一路掉到接近 0,同时 G 的 loss 一直涨,这说明判别器太强了,生成器完全骗不过它。解决办法包括降低 D 的学习率、减少 D 的卷积层通道数,或者在每个训练迭代中让 G 更新 2 次、D 只更新 1 次。
还有一种情况是训练了好几十个 epoch,生成图始终是一团模糊的色块,轮廓都没有。这通常不是模型结构问题,而是人脸对齐没做好。回到第 3 节检查一下:裁剪出来的人脸是否包含太多背景、是否缩放到同一尺寸、是否做了归一化。我在项目里至少有一半的“模型翻车”最后都定位到数据预处理环节。
下面这个表可以当成一个速查表用:
| 现象 | 可能原因 | 优先排查项 |
|---|---|---|
| 生成图全是同一种脸 | 模式崩溃 | 降低学习率、标签平滑、检查数据多样性 |
| D的loss趋近0、G的loss飙高 | 判别器过强 | 降低D学习率、减少D容量、增加G更新次数 |
| 图像模糊无轮廓 | 数据对齐差/分辨率低 | 检查人脸裁剪质量、尝试更高分辨率 |
| 训练到一半显存溢出 | batch_size过大 | 减小batch_size或降低图片分辨率 |
5.3 保存训练中间态与效果评估方法
保存模型也是需要从一开始就做好的习惯。我建议每个 epoch 都保存一次生成器生成的网格图,并至少每 10 个 epoch 保存一次完整的模型 checkpt。这里有个比较实用的评价技巧:不要只看 loss,而要盯住生成图的实际效果。GAN 的 loss 本身并不直接反映生成质量,有时候 loss 还在正常波动,但你保存的样例图已经在明显变好了。
if epoch % 5 == 0: with torch.no_grad(): sample = G(fixed_noise).detach().cpu() torchvision.utils.save_image(sample, f"samples/epoch_{epoch}.png", nrow=8) torch.save({"G": G.state_dict(), "D": D.state_dict()}, f"checkpoints/ckpt_{epoch}.pt")另外建议 fixed_noise 固定下来,每次都用同一个随机向量做生成,这样不同 epoch 之间的对比才直观。你还能看到同一个人脸“逐渐长出来”的过程,特别治愈。如果条件允许,给模型加一个 BatchNorm eval 的注意事项:生成器做推理时要切换到 eval 模式,否则 BatchNorm 在训练和推理状态下统计量的差异会影响输出。
6. 写在最后:一些实操体会
这个项目我反复跑了多轮,感受最深的其实不是某个算法细节,而是“数据中间层”在整个链路里的分量。爬虫写起来半天就搞定,人脸检测代码也不长,真正花时间的是想清楚怎么处理数据:人脸框要不要扩展、图片怎么对齐、训练样例什么时候该重新清洗一遍。任何一个环节的数据脏了,最后都会在生成结果上以一种诡异的方式“还给你”。
最后分享一个实战小技巧:在你保存的 epoch 样例图里,翻车最严重的那些图其实最值钱,不要删。它们能直观地告诉你某一轮数据或训练参数到底出了什么问题。我第一次训练时,发现每隔 3 个 epoch 就会蹦出一张极端扭曲的脸,排查了半天才发现是某些人脸图片本身包含了大面积遮挡,检测框又切进去一块背景,模型被这些“坏样本”反复刺激才产生了周期性崩坏。
如果把这条链路继续往下延伸,下一步可以从 DCGAN 升级到 StyleGAN,生成分辨率能做到 1024x1024,效果基本看不出破绽;也可以把数据采集端从图片换成视频流,做实时人脸采集和生成;再或者给爬虫端接入增量更新逻辑,让训练集可以持续扩充。总之这个项目的天花板不在某一个环节的深度,而在你把多个环节串起来之后,能产生多少种变化。
本文还有配套的精品资源,点击获取