Python机器学习数据流水线:爬虫采集+人脸识别+DCGAN生成
简介把写真图片爬虫、美女脸部识别和DCGAN人脸自动生成整合在一起面向希望掌握图像数据采集、人脸检测预处理与生成对抗网络训练的Python开发者。压缩包共506个文件约77.58MB主体是442张JPG与29张PNG原始图像另有8个PY脚本完成爬虫、人脸检测和DCGAN训练模型检查点、索引和元数据文件支撑训练中途恢复与效果评估XML与TXT等文档补充说明。当前已有1739人浏览学习适合需要完整项目案例来对照复现的初学者和进阶者。资源里既包含从爬虫采集到模型生成的全套代码也保留了不同训练步数的生成模型中间产物可直接观察DCGAN逐步生成的图像变化还能通过调整爬虫目标或网络结构做进一步实验。这份材料能帮助读者把机器学习理论落地为可运行的人脸生成Demo。1. 从写真套图到 DCGAN 自动生成人脸一条能跑通的 Python 数据流水线用 Python 爬虫把写真套图批量抓下来再靠脸部识别把图里的人脸裁出来最后丢给 DCGAN 自动生成不存在的脸——这三段听起来是三个独立项目拼起来却是完整的机器学习数据链路。我拆过一套以“套图爬虫 脸部识别 DCGAN 生成”为主线的 Python 机器学习资源正好覆盖了“数据从哪里来、数据怎么洗干净、模型怎么训出来”三个最现实的问题。适合已经有 Python 基础、想拿真实数据练 GAN 的开发者也适合想搞懂图片数据集构建流程的初学者。全文按实际执行顺序展开每步的参数和坑都摊开讲照着跑能少走弯路。2. 先把数据抓下来requests XPath 写一个不吃防盗链的爬虫写这套资源的第一步不是搭模型而是拿到足够多的真实图片。写真套图站通常结构不复杂没有登录墙单机爬取足够关键是别让请求被服务器识破。2.1 为什么选 requests 而不是 scrapy轻量爬虫的适用边界爬虫选型先看目标。套图站页面结构简单主要工作是“翻列表页 → 进详情页 → 拿图片地址 → 下载”用 scrapy 做分布式属于杀鸡用牛刀调试还麻烦。requests XPathlxml 解析这个组合最适合requests 负责网络请求lxml 负责 HTML 解析代码量小出问题一行行看得明白。请求头是关键。很多站点返回 403 不是因为你没登录而是请求头里少了 Referer。Referer 表示图片页面的来源有些资源服务器会校验它少了直接拒绝User-Agent 伪装成浏览器也能避开最基本的反爬。我一般在模块顶部定义一份默认头所有请求复用省得每次写。import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://example.com/detail/12345, # 改成实际套图站图片所在页 } session requests.Session() session.headers.update(HEADERS) def fetch(url: str, timeout: int 10) - requests.Response: 带重试的请求函数网络抖动时自动重试一次。 try: resp session.get(url, timeouttimeout) resp.raise_for_status() return resp except (requests.Timeout, requests.ConnectionError) as e: print(f[retry] {url} - {e}) return session.get(url, timeouttimeout)说明一下这个函数的逻辑Session 会复用 TCP 连接比直接 requests.get 快不少几万张图下载时差距很明显遇到超时或连接错误就自动重试一次避免单次网络抖动直接把任务打挂。timeout 我通常设 10 秒太短容易误判超时太长会卡住整个爬取流程。HEADERS 里的 Referer 要填图片所在详情页的地址而不是站点首页否则部分图片来源校验直接 403。2.2 列表页解析XPath 的 text() 和 src 真正要抠的细节拿到列表页 HTML 后用 lxml 转成 Element 对象XPath 定位套图链接。这里最容易翻车的不是表达式本身而是页面的懒加载机制很多图站在列表页不直接给img src而是把真实地址塞进>from lxml import html def parse_list(page_html: str): doc html.fromstring(page_html) links doc.xpath(//a[contains(href, detail)]/href) titles doc.xpath(//div[contains(class,title)]) result [] for a, t in zip(links, titles): # 兼容懒加载属性img 标签里 src/data-src/data-original 哪个有值用哪个 img t.xpath(.//img[1]) if not img: continue src (img[0].get(data-src) or img[0].get(data-original) or img[0].get(src) or ) result.append({url: a, title: t.xpath(string(.)).strip(), cover: src}) return result这段代码有两个值得注意的点string(.)会把节点内所有文本拼起来包括子节点的比text()稳得多图片地址的优选顺序是>import os import time from urllib.parse import urljoin def download_images(page_url: str, img_urls: list, save_dir: str): os.makedirs(save_dir, exist_okTrue) for idx, img_url in enumerate(img_urls): full_url urljoin(page_url, img_url) resp session.get(full_url, streamTrue, timeout15) if resp.status_code ! 200: print(f[skip] {full_url} - {resp.status_code}) continue ext os.path.splitext(img_url)[1] or .jpg fname os.path.join(save_dir, f{idx:04d}{ext}) with open(fname, wb) as fp: for chunk in resp.iter_content(chunk_size8192): fp.write(chunk) time.sleep(0.5) # 限速避免短时间打爆对方服务器下载函数里的细节status_code 非 200 直接跳过而不是重试因为图片站对不存在的资源会稳定返回 404重试纯属浪费时间ext 取不到时默认.jpg防止有些链接没有后缀导致文件打不开每张图之间 sleep 0.5 秒是限速保命手段既是对服务器的尊重也是避免 IP 被封。真实爬取时保存目录按“站点名/套图名”分层建方便后面对应到脸部识别阶段。3. 把套图变成人脸数据集OpenCV 检测与四步清洗图片下回来不能直接喂 GAN——套图里大量是全身照、半身照、侧脸和遮挡直接缩放训练出来的东西全是废的。这一步的目的是把人脸区域裁出来做成干净数据集。3.1 haarcascade 还是 dlib选错工具会让你多洗三天数据主流的检测方案有两个OpenCV 自带的 haarcascade 和 dlib 的 HOG 关键点。这个项目里我选了 haarcascade理由很朴素速度足够快CPU 上单张图几毫秒几千张图跑完也就几分钟dlib 对侧脸和大角度人脸召回更好但要装 dlib 库和下载模型文件环境问题多。两者的取舍其实看数据形态。如果套图全部是正脸自拍haarcascade 完全够用如果图里一半是转身、侧脸dlib 的召回率更值。下表是实际对比。方案单张耗时侧脸召回误检率环境依赖haarcascade3~8ms低中opencv-python 即可dlib HOG30~100ms高低需要 dlib、模型文件我这套图源是写真集多为正面自拍和摆拍侧脸比例不高用 haarcascade 换更快的迭代速度是划算的。如果你要处理上万张混合角度的图且 CPU 慢这个决策的差距会非常明显——dlib 跑一万张图可能要一小时haarcascade 几分钟搞定。3.2 批量裁剪脚本scaleFactor 和 minNeighbors 是玄学参数但必须调OpenCV 的detectMultiScale有四个核心参数scaleFactor 控制每次缩放比例minNeighbors 控制候选框最少保留的邻居数minSize 控制最小检测尺寸。这几个参数调不好要么检测不到人脸要么框里全是噪点。我的默认值是 scaleFactor1.1、minNeighbors5、minSize(64, 64)比较稳。import cv2 import os def detect_and_crop(img_path: str, out_dir: str, min_size(64, 64)): img cv2.imread(img_path) if img is None: return 0 gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSizemin_size, flagscv2.CASCADE_SCALE_IMAGE) cnt 0 for (x, y, w, h) in faces: # 适当外扩 10%避免人脸顶到框边缘 margin int(max(w, h) * 0.1) x0 max(0, x - margin) y0 max(0, y - margin) x1 min(img.shape[1], x w margin) y1 min(img.shape[0], y h margin) face img[y0:y1, x0:x1] out_path os.path.join(out_dir, f{os.path.basename(img_path)}_{cnt}.jpg) cv2.imwrite(out_path, face) cnt 1 return cnt逻辑是先转灰度再检测OpenCV 的人脸检测模型是灰度训练的直接喂彩色图没用这是新手最容易忽略的点。外扩 10% 是因为默认框太贴脸后期缩放后容易把额头和下巴切掉。minSize 设置 64 是因为训练目标就是 64×64 的 DCGAN小于这个尺寸裁出来拿到模型里全是糊的。如果检测召回率太低把 minNeighbors 降到 3代价是误检变多要靠下面清洗步骤兜底。这里注意一个细节minNeighbors调低后水印和复杂背景会被当成脸框出来后面清洗会多花时间。3.3 清洗四步尺寸过滤、灰度去重、清晰度筛选、人工抽检检测裁完数据集里通常混着误检框、重复脸和糊图。不洗直接训练生成器会把噪点当特征学进去。我的清洗流程固定四步顺序不能乱尺寸过滤只保留长宽都 ≥ 96 的图小于这个值的裁块要么是误检要么是远距离小脸学不出五官细节。灰度去重把图缩小到 8×8 灰度后算平均哈希汉明距离小于 5 的视为重复删掉其中一张。同一套图里连拍帧重复率极高不去重会把某张脸反复训练几十遍。清晰度筛选用 Laplacian 算子算方差方差小于阈值比如 50的直接删。这种图几乎都是运动模糊或压缩过度放进去会让生成器学会模模糊糊的脸。人工抽检每 500 张随机抽 20 张肉眼扫一遍发现疑似非脸部就调上一节的 minNeighbors。import cv2 import numpy as np def is_duplicate(img_a: np.ndarray, img_b: np.ndarray, thresh: int 5) - bool: def avg_hash(img): small cv2.resize(img, (8, 8), interpolationcv2.INTER_AREA) gray cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) return gray.flatten() gray.mean() ha, hb avg_hash(img_a), avg_hash(img_b) return np.count_nonzero(ha ! hb) thresh def is_blurry(img: np.ndarray, thresh: float 50.0) - bool: gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) lap cv2.Laplacian(gray, cv2.CV_64F).var() return lap thresh平均哈希的缺点是只对亮度结构敏感但对这个场景已经够用对付连拍帧和同图不同尺寸绰绰有余。Laplacian 方差就是公认的清晰度代理指标值越大代表边缘梯度越强。做批量清洗时建议把“保留”的图先拷到一个clean/目录而不是在原目录上删方便后面重新调整阈值——这是后悔药的思路。清洗完统计数量一般能剩 30%~50% 就不错了剩下的就是 DCGAN 的输入数据集。4. DCGAN 生成脸部从 64×64 开始把生成器训到不崩数据集准备到位接下来进入正题用 DCGAN 生成人脸。这一步的核心不是堆算力而是控制好生成器和判别器的平衡。4.1 DCGAN 为什么能生成人脸全卷积、BN 和 LeakyReLU传统 GAN 训人脸特别容易崩核心原因在于生成器用全连接层直接映射隐向量到图像特征空间没有空间结构。DCGAN 的关键改动是去掉全连接层换成转置卷积每一层都加 BatchNorm生成器激活函数用 ReLU、判别器用 LeakyReLU。这些改动让网络更容易收敛到稳定状态而不是在判别器太强或太弱之间来回震荡。判别器用 LeakyReLU 是为了避免负半轴梯度死亡生成器用 ReLU 则是因为输出层之前不太容易出现死神经元。BatchNorm 的作用是稳定每层的输入分布训练时配合小 batch 效果更明显。下面是我常用的结构代码latent_dim 保持 100。import torch.nn as nn def conv_block(in_ch, out_ch, kernel4, stride2, padding1, bnTrue): layers [nn.Conv2d(in_ch, out_ch, kernel, stride, padding, biasFalse)] if bn: layers.append(nn.BatchNorm2d(out_ch)) layers.append(nn.LeakyReLU(0.2, inplaceTrue)) return nn.Sequential(*layers) class Generator(nn.Module): def __init__(self, latent_dim100): super().__init__() self.model nn.Sequential( nn.ConvTranspose2d(latent_dim, 512, 4, 1, 0, biasFalse), nn.BatchNorm2d(512), nn.ReLU(True), conv_block(512, 256, 4, 2, 1), conv_block(256, 128, 4, 2, 1), conv_block(128, 64, 4, 2, 1), nn.ConvTranspose2d(64, 3, 4, 2, 1), nn.Tanh() ) def forward(self, z): return self.model(z.unsqueeze(-1).unsqueeze(-1))这段生成器从 100 维隐向量出发经过 4 次转置卷积把特征图从 4×4 逐步放大到 64×64输出层用 Tanh 把像素压到 [-1, 1]对应数据预处理时也要把图片归一化到 [-1, 1]这步最容易漏。latent_dim100 是 DCGAN 论文默认值别改改了要重新调收敛曲线。判别器结构是生成器的镜像输入 3×64×64 图输出 1 个 logitBN 层加在除第一层外的所有卷积层最后一层不用 BN。4.2 训练主循环与超参表照着跑能收敛的默认值DCGAN 训练本质是交替优化每步先训判别器再训生成器。判别器的目标是把真实图判为 1、把生成图判为 0生成器的目标是骗过判别器。这个对抗过程没有“正确”的 loss 绝对值关键是让两边保持均势。criterion nn.BCEWithLogitsLoss() opt_g torch.optim.Adam(generator.parameters(), lr2e-4, betas(0.5, 0.999)) opt_d torch.optim.Adam(discriminator.parameters(), lr2e-4, betas(0.5, 0.999)) for epoch in range(epochs): for real_imgs, _ in loader: batch real_imgs.size(0) # ---------- 训练判别器 ---------- noise torch.randn(batch, latent_dim) fake generator(noise) loss_d criterion(discriminator(real_imgs), real_label) loss_d criterion(discriminator(fake.detach()), fake_label) opt_d.zero_grad(); loss_d.backward(); opt_d.step() # ---------- 训练生成器 ---------- fake generator(torch.randn(batch, latent_dim)) loss_g criterion(discriminator(fake), real_label) opt_g.zero_grad(); loss_g.backward(); opt_g.step()循环里有两个常被忽略的细节一是训练判别器时生成器必须 detach否则梯度会穿过生成器把两个网络搅在一起二是生成器每步都用全新的噪声向量不能复用判别器那一步的 fake否则生成器只需要学会针对当前 batch 的噪声布线。BCEWithLogitsLoss内部自带 sigmoid不要在判别器输出层再手动加 sigmoid双重计算会导致梯度消失。关键超参参考下表参数默认值调整方向lr2e-4不收敛时降到 1e-4别调高batch_size64显存小用 32但 BN 效果会变差betas(0.5, 0.999)0.9 会让训练震荡用 DCGAN 推荐值image_size64数据集量大可上 128训练时间翻倍epochs200看生成图质量决定不必死磕Adam 的 beta1 要特别强调GAN 训练默认的 beta10.9 会让 loss 波动很大DCGAN 论文专门改成 0.5这是为对抗训练设计的。图集数量在 5000 张以下时 batch 用 32数据增强只做 RandomHorizontalFlip 就够了别加裁剪或旋转会破坏人脸结构。4.3 看 loss 曲线和生成图哪种状态是好状态训练时每 100 步把 G loss 和 D loss 记下来画曲线但别指望 loss 一直下降。正常健康的曲线是两者在某个区间内小幅度震荡如果 D loss 长时间趋近 0 而 G loss 一直涨说明判别器完全压制了生成器此时生成器无论怎么学都骗不过对方梯度基本没用了。反过来如果 G loss 长期在 0 附近说明生成器在瞎猫撞死耗子做出来的图千篇一律。靠 loss 曲线判断不够直观更可靠的是每 10 个 epoch 固定用同一批隐向量 z 生成一张 8×8 网格图存盘然后快速翻看变化轨迹。正常训练时同一批噪声生成的图会逐渐从噪点变成轮廓、变成五官第 100 epoch 左右能看到逼近真实人脸的形状。翻看时发现某一阶段开始所有图变得一模一样就是 mode collapse 的前兆要回退到崩溃前的 checkpoint 重新来而不是硬着头皮继续训。5. 避坑与排查环境、数据、训练三层的翻车记录这套资源跑下来我在三个层面都翻过车。挑五个最有代表性的记在这里都是按“现象→原因→解决”的逻辑写的遇到类似问题可以直接对号入座。5.1 cv2 装不上或 import 报错现象pip install opencv-python 之后 import cv2 直接报错或者装完提示 DLL load failed。这是 Windows 上最经典的坑多数是 Python 版本与 wheel 不匹配。原因opencv-python 的 wheel 对 Python 3.8~3.12 都有对应版本但如果你用 conda 环境且混合了非官方源容易装到不带 GUI 的 opencv-headless 变体DLL 报错则多半是 numpy 版本过新opencv 预编译的二进制对 numpy 有版本上限。解决先pip uninstall opencv-python opencv-headless全部卸掉再pip install opencv-python4.8.1.78 numpy1.24.4这套组合在 Python 3.9/3.10 上验证过基本不会冲突。装完跑一下cv2.CascadeClassifier读取自带 haarcascade 验证能读到就说明环境没问题。5.2 爬下来全是 403 或验证码现象requests 请求列表页正常但下载图片时大量 403有时候整个 IP 被拉黑需要输入验证码才能继续访问。原因图片服务器做了双重校验仅带 UA 不够还校验 Referer另一个是请求频率太高触发了站点的访问频控。解决所有图片请求带上详情页 URL 作为 Referer而不是站点首页下载函数里每张图之间加 0.5 秒 sleep如果还是被拉黑换代理 IP 是唯一办法。血泪经验先拿 10 张图试水确认没有 403 再上全量否则脚本跑十分钟才发现全废白白浪费时间。5.3 脸部检测召回率低或误检一堆现象明显的人脸没框出来或者把水印、logo、背景纹理当成脸框了一堆清洗时删到手软。原因前者是 minNeighbors 设太高或 minSize 太大后者是图片本身带了大量文字水印和复杂背景haarcascade 会把纹理误判为人脸。解决一张图多个误检时提高 minNeighbors 到 8~10 清除小框侧面人脸漏检时先翻转图片再检测一次两次结果合并去重。检测前把图缩放到不超过 800px 宽能提速好几倍检测完的框坐标记得按缩放比例映射回原图。这个技巧对大图非常友好缩放后细节损失不多但速度翻倍。5.4 GAN 训练不收敛或判别器一边倒现象D loss 快速掉到 0.1 以下G loss 却不断上升或者反过来 D loss 居高不下生成图全是噪点怎么看都不像人脸。原因通常不是网络结构问题而是学习率太高或 BN 层在判别器里效果不稳定。另一个常见原因是真实图片没有归一化到 [-1, 1]而生成器输出是 Tanh 范围判别器输入分布不匹配导致它很容易区分真假图。解决先检查数据预处理环节是否做了(img / 127.5) - 1确认 Adam 的 betas 是 (0.5, 0.999)再把 lr 从 2e-4 降到 1e-4。如果依然崩检查判别器是否加了 Dropout判别器训练时 dropout 会加剧不稳定性建议去掉。这套检查顺序我已经养成肌肉记忆十次里有八次是归一化漏了。5.5 生成的图全是同一张脸模式崩塌现象生成网格里 64 张图看起来都是同一张脸只是位置或色调略有差异。这是 GAN 最经典也最难缠的问题我在这上面浪费过一整个周末。原因生成器发现把隐向量映射到训练集里某一张或某几张占比高的图上就能稳定骗过判别器于是停止了探索输出退化成一个固定映射。解决先用 t-SNE 可视化隐向量 z 的分布看是否挤在一起然后加大训练数据多样性把重复图彻底删干净最后把 batch_size 降低到 32并给判别器加 label smoothing真实标签从 1 变成 0.9削弱判别器的过自信。这套组合能大概率把模型从崩塌边缘拉回来但如果数据集本身只有几百张图崩塌是迟早的事加数据才是根治方案。6. 给训练结果吃后悔药隐向量插值验证生成器真的学到了GAN 训完别急着收工先做一次隐向量插值latent space interpolation。做法是取两个随机噪声 z1 和 z2在它们之间均匀插 10 个点依次生成图片按顺序拼成一行。如果生成器真的学到了人脸流形这个序列里的人脸五官和姿态会平滑过渡从第一张脸持续改变到第二张脸中间没有突变、没有闪烁成另一个人。如果插值时出现画面突然跳到另一张脸或者过程中脸崩碎成噪点说明生成器只是死记了训练集的若干映射没有学到人脸的连续分布。插值代码很简单核心是 torch 的 lerp 操作import torch def interpolate(generator, z1: torch.Tensor, z2: torch.Tensor, steps: int 10): alphas torch.linspace(0, 1, steps).view(-1, 1) z z1.unsqueeze(0) * (1 - alphas) z2.unsqueeze(0) * alphas with torch.no_grad(): imgs generator(z) return imgs # shape: [steps, 3, 64, 64]这里 z1 和 z2 都是 1×100 的随机向量。用no_grad省显存torch.linspace在 CPU 上生成插值系数然后按系数加权求和得到中间隐向量。拼图时把每张图 normalize 回 [0,255] 再横向拼接存成一张长条图肉眼直接看出过渡是否平滑。我现在的习惯是每次训练结束都强制生成两张插值图一张横跨 z1→z2一张走折线 z1→z3→z2。折线图能更敏锐地暴露隐空间里的空洞如果某一段生成图明显变糊或者突变就是该段隐空间采样稀疏。再用 PSNR 算相邻两张生成图的峰值信噪比如果中间两步的 PSNR 突然掉到 20dB 以下基本可以确认发生了跳变说明训练还没充分。从那以后我每次训完 GAN 都强制走一遍插值验证再决定要不要回滚 checkpoint 继续调参。这比只看 loss 曲线靠谱得多希望帮到你。本文还有配套的精品资源点击获取