ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GAN+行人重识别:跨域实战与Python源码解析

GAN+行人重识别:跨域实战与Python源码解析 简介一套基于生成对抗网络GAN实现行人重识别的Python源码包面向计算机相关专业的学生、教师与开发者适合用作毕业设计、课程设计或大作业的完整参考项目。压缩包共96个文件整体约36.97MB包含16个Python脚本、16个文本日志、44张JPG结果图、6张PNG训练曲线、4个Markdown说明、4份PDF报告、2个PPT分享及2个YML配置等文件脚本覆盖数据预处理、模型构建、训练测试与结果可视化等环节并配合日志和图表记录不同训练尺寸与学习率下的表现。代码已通过验证可稳定运行直接用于学习和二次开发实验报告与PPT为项目总结和答辩提供了完善素材可帮助理解GAN生成对抗网络的训练机制以及行人重识别的完整流程。资源内还包含操作日志和项目必读说明能够协助规避环境配置与路径设置中的常见问题整体结构完整清晰便于二次开发与扩展。目前已有160人浏览学习。1. GAN 与行人重识别为什么这个组合适合当毕设方向一张行人在摄像头 A 里出现几分钟后在摄像头 B 又被拍了一次算法要能认出这是同一个人——这就是行人重识别ReID。问题在于摄像头的光线、角度、衣着颜色偏差会让同一人的外观差异巨大纯有监督模型换个环境就掉点跨域泛化一直是这个方向的核心痛点。GAN生成对抗网络在这里的角色不是“生成假图像做数据增强”这么简单它要么在像素层面把源域图像翻译成目标域风格要么在特征层面逼着网络忘掉“域”这个不该学的信息。这篇笔记就从原理、工程实现到训练翻车现场把基于 GAN 的行人重识别 Python 源码怎么搭、参数怎么调、结果怎么验证讲透正好对应毕设里“能复现、能讲解、能答辩”的三个硬要求。2. GAN 在行人重识别里的三种身份数据增强、跨域风格迁移与特征级对抗2.1 像素级路线用 CycleGAN 伪造跨摄像头样本行人重识别数据集通常是按摄像头划分的训练集里同一行人的图像可能来自 6 个摄像头推理时 query 来自一个摄像头gallery 来自另一个。摄像头之间的颜色分布差异业内叫“域漂移”。卷积神经网络很容易抓住这种“假线索”看到偏蓝的色调就认为是摄像头 A 拍的看到偏黄的就猜摄像头 B而不是真正去对比行人外观。这就是有监督 ReID 在跨域测试时 Rank-1 掉十几个点的常见原因。像素级 GAN 的思路很直接既然摄像头 A 和摄像头 B 的图像风格不同那就训练一个图像翻译模型把 A 域的图翻译成 B 域风格再用翻译后的图训练重识别模型。最常见的是 CycleGAN 结构——两个生成器加两个判别器配合循环一致性损失保证翻译前后身份内容不变。做行人重识别时还会在 CycleGAN 基础上加一个身份保持损失这就是 SPGAN 的核心生成器把 A 域图翻译成 B 域风格后用预训练的人脸/行人分类器约束输出图仍然是同一个人避免生成器为了骗过判别器把行人的衣服颜色、体型都改掉。这条路线对毕设的好处是可视化强训练完成后可以直接把同一张图在不同域之间的翻译结果打印出来放到论文里评审一眼就能看懂你在做什么。坏处是工程量大两个生成器加两个判别器占显存、训练时间长而且翻译后的图像质量如果不够好反而会污染后面的重识别训练。我一般建议时间紧的队员把像素级路线当作论文里的“动机展示”真正提分靠特征级对抗。2.2 特征级路线让主干特征与域判别器对抗特征级对抗是另一种玩法也是近两年 ReID 跨域工作里更常见的选择。它不生成任何图像而是在主干网络提取出特征向量之后接一个“域判别器”——一个二分类网络输入特征输出该特征来自源域还是目标域。训练时主干网络和判别器互相较劲判别器努力把两个域的特征分开主干网络努力让判别器分不开。这个对抗过程收敛之后特征里关于“域”的信息被洗掉剩下的就主要是身份信息了。为了让这个洗特征的过程更稳定实际工程里有几个常用技巧。判别器的结构要足够小一两层全连接加 LeakyReLU 就够大了会让对抗过程太快收敛到一边。损失函数层面最基本的做法是用 BCE 做二分类但训练中很容易判别器先学会、生成器跟不上出现“模式崩塌”式的不稳定。更稳的做法是用 WGAN-GP 的 Wasserstein 距离替代 BCE或者用谱归一化约束判别器的 Lipschitz 条件。改进技巧里还有一个 feature matching loss不直接拿判别器的最终输出做对抗而是把判别器中间层的特征拿出来让源域和目标域在这些中间特征上距离更近。这个思路来自 Salimans 等人在 2016 年提出的 improved techniques放到行人重识别里本质是让特征在多个尺度上都域无关而不是只看最后一层。2.3 选骨干与损失组合先让基线立住再谈 GAN无论走哪条 GAN 路线ReID 的主干网络都绕不开 ResNet-50。它预先在 ImageNet 上训练过直接去掉最后的全局分类层把倒数第二层输出当特征在 Market-1501 上什么都不做就能跑到 70% 左右的 Rank-1。这个基线必须先跑通再谈 GAN 加成否则模型不收敛时你根本说不清是主干的问题还是对抗的问题。损失函数上标准配置是三件套身份损失CrossEntropy、三元组损失Triplet Loss和对抗损失。主干网络输出特征后接一个 BNNeck——特征先过 BatchNorm 再接分类头这样身份损失和三元组损失各用各的特征表示不会互相拉扯。对抗损失加在哪一层有讲究像素级路线加在图像上特征级路线加在 BNNeck 之后的特征向量上。下表是三种损失的定位和默认权重实际调试时就从这套默认值开始动。损失函数作用默认权重CrossEntropy让特征能区分不同行人 ID收敛最快1.0Triplet Loss拉近同一行人的特征、推开不同行人1.0margin0.3对抗损失让特征对摄像头/域信息不敏感0.1 起步这个 0.1 的对抗损失权重是我反复踩坑后的保守起手值。太大会让主干网络在训练初期就被判别器带着乱跑特征完全学不到身份信息Rank-1 会掉到比纯基线还低太小则对抗几乎没有作用跨域提升不明显。毕设做消融实验时正好可以用这个权重做一组对比表0、0.05、0.1、0.2观察 Rank-1 的变化曲线这组数据放论文里非常加分。3. 搭出一个可跑的 Python 训练工程从目录结构到关键代码3.1 数据准备Market-1501 的目录约定与标注文件先把数据讲清楚。Market-1501 是行人重识别最常用的公开数据集目录结构固定目录作用bounding_box_train/训练集751 个行人 IDbounding_box_test/测试集750 个行人 IDquery/待查询图像3368 张gt_bbox/ 与 gt_query/手绘框用作辅助评估图像命名里带信息0002_c1s2_000451_03.jpg0002是行人 IDc1是摄像头编号s2是场景序号后面是帧号和检测框序号。自定义 Dataset 时我习惯把这个命名解析出来存成(img_path, pid, camid, domain)四个字段domain 字段用来指示当前图属于哪个摄像头域后面的对抗训练全靠它。训练时不能简单用ImageFolder随机采样ReID 要求 PK 采样每个 batch 里随机选 P 个行人 ID每个 ID 再随机取 K 张图组成 P×K 的 batch。这样做是为了保证一个 batch 内有足够的正样本对去计算 Triplet Loss。我常用的设置是 P16、K4batch size 就是 64如果加上 GAN 后显存吃紧可以把 P 降到 8K 保持 4。class ReIDDataset(Dataset): def __init__(self, img_dir, domain_label): self.paths, self.pids, self.camids [], [], [] for name in os.listdir(img_dir): # 文件名如 0002_c1s2_000451_03.jpg pid int(name.split(_)[0]) camid int(name.split(_)[1][1]) self.paths.append(os.path.join(img_dir, name)) self.pids.append(pid) self.camids.append(camid) self.domain_label domain_label # 源域0 目标域1 def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) img transform(img) # 缩放到 256x128随机翻转/擦除 return img, self.pids[idx], self.camids[idx], self.domain_label注意这里domain_label是整个数据集共享的标量它标识的是“这一批图像来自哪个摄像头分区”。跨域训练时通常把源域训练摄像头标成 0目标域测试摄像头标成 1。很多新手会忽略这个字段导致后面实现对抗损失时找不到域标签这是整个工程里最容易错的第一步。3.2 网络骨架主干、生成器与判别器的实现要点主干用 ResNet-50 做特征提取部分关键改动有两处一是把最后的全局平均池化改成自适应池化确保输入尺寸变化时输出维度不变二是接一个 BNNeck把特征分成两条路——原始特征给 Triplet Loss过 BN 后的特征给身份分类头。推理阶段直接返回 BN 后的特征向量这样检索时用余弦距离或欧氏距离都能得到稳定的排序结果。class ReIDBackbone(nn.Module): def __init__(self, num_ids): super().__init__() base torchvision.models.resnet50(pretrainedTrue) self.base nn.Sequential(*list(base.children())[:-2]) self.bnneck nn.BatchNorm1d(2048) self.fc nn.Linear(2048, num_ids, biasFalse) def forward(self, x, trainingTrue): feat self.base(x) # [B, 2048, 8, 4] feat F.avg_pool2d(feat, feat.size()[2:]).view(feat.size(0), -1) bn self.bnneck(feat) if not training: return bn # 推理时直接返回 BN 特征 logits self.fc(bn) # 训练时用 BN 特征做身份分类 return feat, bn, logitsBNNeck 这个设计值得多说一句如果不加它直接用同一个 2048 维特征同时计算 CrossEntropy 和 Triplet Loss两个损失会互相拉扯——分类头希望特征在类别方向上可分Triplet 希望特征在距离上可分最终训练震荡、收敛变慢。加了一个 BN 后identity loss 吃 BN 后的特征Triplet loss 吃原始特征互不干扰这个技巧在 ReID 工程里几乎是标配。判别器就简单多了输入特征向量输出一个标量 logit表示“输入特征来自哪个域”。注意判别器不要做得太复杂两三层线性层足够。如果做到四层以上它学得太快主干怎么骗都骗不过对抗就失去了意义。class DomainDiscriminator(nn.Module): def __init__(self, in_dim2048): super().__init__() self.net nn.Sequential( nn.Linear(in_dim, 512), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(512, 128), nn.LeakyReLU(0.2, inplaceTrue), nn.Linear(128, 1) ) def forward(self, feat): return self.net(feat) # 输出为 logit配合 BCE 或铰链损失3.3 训练主循环一个 batch 里三个 Loss 的先后顺序训练循环是整个源码里最需要小心的地方因为三个 Loss 的更新顺序有讲究。最常见的错法是让主干、判别器同时 backward 一次梯度全部混在一起更新这样对抗双方在同一个优化步里互相抵消Loss 曲线看着很热闹精度一动不动。正确的顺序是先更新判别器固定主干参数再更新主干让它去骗判别器。用一个简单的detach()就能切出边界。for imgs, pids, camids, domain in loader: imgs, pids imgs.cuda(), pids.cuda() # 1. 主干前向 feat, bn, logits model(imgs, trainingTrue) # 2. 身份损失和三元组损失先让模型学会“这是谁” loss_id F.cross_entropy(logits, pids) loss_tri batch_hard_triplet_loss(feat, pids, margin0.3) # 3. 更新判别器输入特征带 detach梯度不回传到主干 real_logit disc(bn.detach()) loss_d F.binary_cross_entropy_with_logits(real_logit, domain) opt_d.zero_grad() loss_d.backward() opt_d.step() # 4. 更新主干对抗 loss 不再 detach让主干反向优化去骗判别器 fake_logit disc(bn) loss_adv F.binary_cross_entropy_with_logits(fake_logit, 1 - domain) # 5. 汇总三个 loss更新主干 loss loss_id loss_tri 0.1 * loss_adv opt.zero_grad() loss.backward() opt.step()几个容易写错的细节第三步里判别器的目标标签是domain即让它学会分辨真实输入来自哪个域第四步里主干的对抗目标变成1 - domain也就是希望判别器预测错。domain本身是 0/1 标量BCE 的 target 需要和 logit 张量形状一致这里要domain.expand_as(fake_logit)或者在构造数据时生成同形状张量。另外三元组损失建议用 batch-hard 策略对每个锚点样本在 batch 内取最难的正样本和负样本计算距离比随机采样收敛快得多。3.4 必须调对的关键参数表参数推荐值说明输入尺寸256×128ReID 惯例长宽比约 2:1PK 采样P16, K4batch size 64主干初始学习率3.5e-4加 cosine 退火warmup epoch10前 10 个 epoch 线性升到 3.5e-4生成器/判别器学习率1e-4比主干小避免对抗震荡Triplet margin0.3太小难收敛太大损失值不敏感对抗损失权重0.1先跑纯基线再逐步调大总训练 epoch60-9040 个 epoch 后每 10 个 epoch 存一个权重学习率这里强调一下主干网络是预训练的可以用相对大的 3.5e-4判别器是随机初始化的给 1e-4 就够了。生成器如果走像素级路线也类似1e-4 起步。一个常见误区是给所有模块统一学习率结果主干还没收敛判别器已经过拟合Loss 曲线看起来像一条被反复拉伸的弹簧。4. 训练与部署避坑GAN 不收敛、精度反降、BN 翻车的 5 种现场4.1 现象判别器一骑绝尘生成器 Loss 半死不活训练日志里最扎心的画面判别器的 loss 前 200 步直线降到 0.1 以下主干的对抗 loss 稳定在 0.7 上下纹丝不动t-SNE 可视化出来两个域的特征被一条线切开。原因判别器学得太快。判别器的任务本质是二分类比主干的 ReID 任务简单太多它很快就能找到区分两个域的“捷径”——可能是背景色、可能是整体亮度这些恰恰是主干想学又该忽略的线索。对抗在数学上只有双方势均力敌才有意义一边倒时主干接收到的梯度几乎为零。解决三个手段按优先级用。第一步把判别器的学习率降到主干的一半第二步给判别器加谱归一化限制它的权重 Lipschitz 常数第三步换损失函数用 WGAN-GP 的 Wasserstein 距离替代 BCEWGAN-GP 的梯度惩罚会强制判别器平滑从根上缓解“瞬移式收敛”。如果三个都做了还是压不住就把对抗损失的权重从 0.1 降到 0.03先让主干把身份信息学扎实。4.2 现象加了 GAN 精度反降比纯基线还低这个现象最打击信心也是最容易让人放弃的方向。你满心期待 GAN 加成能涨 5 个点结果 Market-1501 上 Rank-1 从 72% 掉到 65%。原因对抗损失的更新方向未必和身份分类一致。判别器让主干“忘掉域信息”但特征空间里域信息和身份信息是纠缠在一起的为了骗过判别器主干可能把一部分真正有用的身份线索也丢了。另一个常见原因是生成图像质量太差像素级路线下生成器还没训练好吐出来的图有严重伪影主干吃着这些脏数据自然不会好好学。解决先把对抗损失的系数调低到 0.05 甚至 0.01观察 Rank-1 是否回升同时把训练前 20 个 epoch 的对抗 loss 置零只跑主干20 轮之后再接入对抗训练这相当于带预训练的“后悔药”机制。像素级路线下我会定期把生成器的输出图打印到 tensorboard肉眼看图像是否清晰、行人是否可辨如果全是糊成一团的色块先修生成器再谈提分。4.3 现象训练一切正常验证时 Rank-1 断崖下跌训练 Loss 曲线平滑、收敛训练集上 Rank-1 已经 90%结果切到测试集评估Rank-1 只有 40%比训练精度拦腰斩断。原因这是 BN 的经典翻车现场。训练时 BatchNorm 用的是当前 batch 的均值和方差测试时它要用训练阶段统计好的全局均值/方差。但 ReID 的 PK 采样导致每个 batch 内行人 ID 高度集中BN 统计量被这些采样偏差带偏训练好的全局统计量本身就不稳定。更隐蔽的是有些人评估时忘了model.eval()BN 在推理阶段继续用 batch 统计量整个特征分布全乱了。解决评估前必须调用model.eval()把主干切到推理模式。如果eval()后还是掉点就把 ReID 主干里的 BN 层换成 InstanceNorm 或 GroupNorm这类归一化不依赖 batch 统计对 PK 采样天然免疫。很多开源的 ReID 代码在主干最后几层特意把 BN 改成 GN就是这个原因。4.4 现象显存溢出batch size 降到 16 还爆GAN 一加进来显存就不够用。生成器和判别器各拿一份显存中间特征图在 BP 时要缓存再加上主干网络一张 12GB 的卡根本塞不下 64 的 batch。原因显存账没算通。ResNet-50 主干 判别器 生成器仅模型权重就占用约 300MB但真正吃显存的是反向传播时保存的中间激活值batch size 64 的训练图在 256×128 分辨率下中间激活值轻松超过 6GB。解决三个手段组合用。第一判别器输入用 BN 后的 2048 维向量而不是把整张特征图拉平输入后者会让判别器的第一层线性层就吃掉几百 MB第二用梯度累积batch size 设 32每两步累积一次梯度等效 64第三打开 PyTorch 的 AMP 混合精度训练显存占用直接砍半。像素级路线如果实在跑不动就改成预生成方案先把所有训练图像离线翻译成目标域风格存盘训练时只加载翻译结果丢掉生成器。4.5 现象真图、伪图标签错位梯度像“互相打架”训练能跑通但 Loss 曲线剧烈震荡每隔几十步就跳一次Rank-1 在训练集上也忽高忽低。原因数据管线里域标签和图像没有对齐。最常见的是 DataLoader 的shuffleTrue导致类别标签打了乱序或者对生成图像也做了随机裁剪/翻转增强生成器输出的图像和对应的身份标签错位对抗梯度自然乱套。解决先不要纠结调参直接把训练循环里一个 batch 的imgs、pids、domain全部打印出来人工核对第 i 个样本的图像、身份、域标签能否对上。更稳的做法是生成式路径下对生成器输出图像不做任何随机增强——只在加载真实图像时做裁剪翻转确保伪图与标签严格一一对应。检查完这一层再回去调对抗权重。5. 让结果站得住验证方法、指标基准和一个进阶方向5.1 训练曲线、t-SNE 与跨域测试三张图答辩时最怕“GitHub 上跑通的模型换了自己的 query 就认不出人”。我习惯用三张图证明 GAN 没白加第一张是训练 Loss 曲线纯基线与基线上加 GAN 的对比看对抗 loss 是否平稳收敛第二张是 t-SNE 特征分布把源域和目标域的行人特征投影到二维平面加对抗训练前两个域是两个不相交的团块加完后应该交织在一起同时同一行人的特征仍然聚拢第三张是跨域测试的 Rank-1 曲线按评估轮数画出来看涨点发生在第几个 epoch。这三张图摆出来GAN 是不是真的在起作用一目了然比任何口头解释都有说服力。5.2 用 t-SNE 和跨域测试量化域差异t-SNE 不能只靠眼睛看要量化把训练好的特征按摄像头编号提取出来用 sklearn 计算两个域特征均值之间的欧氏距离这个距离从加对抗前的 2.5 降到 0.8就是一个可以直接写进论文的数值。跨域测试则是更硬的指标训练在 Market-1501 的源域上直接拿去测另一个跨域测试集的 query看 Rank-1 和 mAP 是否比纯基线高。注意跨域测试必须把 GAN 的对抗模块也带上不能只导出主干特征。5.3 值得往下走的一个方向特征级对抗 重排序重排序re-ranking是 ReID 后处理里性价比最高的一步不训练不调参直接在特征检索结果上做 k-reciprocal 编码通常能把 Rank-1 提升 3-8 个点mAP 提升尤其明显。加对抗生成网络之后特征本身已经比较接近域无关再做重排序相当于把最后一点排序噪声也压掉这个组合作为毕设的最终方案很稳妥。我自己做这个方向时有个习惯任何改动先存一份模型权重做后悔药因为 GAN 训练的不确定性太大今天还能跑通的组合明天可能换个随机种子就炸了没有权重备份就只能从头再来。希望这个从原理到踩坑的笔记帮到你顺着这套流程把一个可复现的 GAN-ReID 工程跑通比漫无目的地调参可靠得多。本文还有配套的精品资源点击获取
返回列表