ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用GAN做行人重识别:CycleGAN跨摄像头检索实战与避坑指南

2026/10/1 13:00:54 拓冰建站 浏览量
用GAN做行人重识别:CycleGAN跨摄像头检索实战与避坑指南 简介基于生成对抗网络GAN的行人重识别毕业设计工程定位清晰适合深度学习或计算机视觉方向的学生、研究者用于课题落地与答辩展示。资源以Python源码为核心完整覆盖行人重识别任务的模型构建、训练、评估与可视化流程配套实验报告与答辩PPT便于梳理方法原理、实验设计和结果分析。在此基础上还提供项目说明、依赖配置及相关文本记录方便复现运行与二次开发。压缩包共计48个文件主要包含8个Python脚本、22张jpg示例图片、3张png图表、PDF格式的实验报告、PPT演示文稿及多个txt说明文档整体约18.49MB目录结构整理清晰便于按代码、结果、文档分类查阅。已有409人学习下载资源经过调试运行完成度较高可帮助使用者快速理解GAN在行人重识别中的具体应用也能作为毕业设计、课程设计或技术分享的参考模板。1. 用GAN做行人重识别一个能写进毕设也能落地讲清楚的深度学习方向训练正常生成的图像看着也像那么回事可ReID模型的Rank-1反而掉了两个点——这是我第一次跑基于GAN的行人重识别踩到的坑。基于GAN深度学习生成对抗网络做行人重识别本质是用生成网络去缓解不同摄像头之间的外观差异把摄像头A的图转成摄像头B的风格让特征提取网络在训练时就见过更多换摄像头之后长什么样的样本。这个方案能解决监控场景里跨摄像头找人难的问题也能给毕业论文和课程项目提供一条清晰的创新主线。适合正在做行人重识别入门、准备答辩汇报或者想把GAN真正用在视觉检索任务上的工程师。2. 生成对抗网络在行人重识别里的三个入口先想清楚要解决什么2.1 行人重识别的痛点是跨摄像头域差异GAN恰好长在图像生成上行人重识别要解决的是跨摄像头找人给一张摄像头A里的行人图像去另一个摄像头B的图像库里检索同一个人。看似简单难在同一个人的外观在不同摄像头下会完全变样——白平衡偏差、光照角度、背景干扰、遮挡都会让外观漂移。Market1501里同一个ID在c1摄像头下穿深蓝色外套到了c6摄像头下可能被白平衡拉成偏紫色。如果训练数据覆盖不到这种变化模型就会把颜色误当成身份特征。常见的深度学习方法会用随机裁剪、颜色抖动这类数据增强来缓解但这些增强是盲目的并不知道目标摄像头的图像分布长什么样。生成对抗网络不一样它能显式学习两个摄像头域的图像分布然后按需生成换了一个摄像头之后的样本。这是GAN做迁移任务的底层逻辑不是靠运气增强而是靠对抗让生成器的输出分布贴近目标域。ReID网络在训练时看到更多目标域风格的样本学到的特征就不容易被摄像头差异带偏。这里有个容易被忽略的认知源码工程里GAN不是替代ReID检索网络的模型而是数据侧和特征侧的增强手段。整个项目最难的不是理解对抗训练本身而是搞清楚生成的图像以什么方式进入检索任务。想清楚这一层你再看那些开源的训练脚本就不会被为什么这里有个生成器、那里有个判别器带偏。2.2 三套做法像素级风格迁移、样本生成、特征级对抗第一套是像素级风格迁移最常见的就是CycleGAN。把摄像头A的全部训练图作为源域摄像头B的全部训练图作为目标域训练一个G_A2B完成风格转换。训练好之后有两种用法一是直接扩充ReID训练集把A域图转成B域风格再参与训练二是推理前做图像归一化把query和gallery都转到同一个风格域再算相似度。第一种用法对精度提升更直接因为网络在训练时已经见过两种风格。CycleGAN的循环一致性损失在这里特别关键它保证生成的图在结构上和原图是同一个人的姿态而不是随意改内容。第二套是条件样本生成。用姿态关键点、视角标签作为条件输入让GAN生成同一个人的不同姿态图像。严格说这个方案解决的不是摄像头域差异而是姿态变化问题。它会引入姿态估计器做条件标注整个链路长、训练成本高生成质量稍差就会误导ReID网络。更适合竞赛加分的场景不太适合当毕设主方案。我在实际项目里用这招较少因为它投入产出比不如风格迁移。第三套是特征级对抗常见的是DANN思路ReID特征提取器后面接一个域判别器判别器从特征向量里判断图像来自哪个摄像头而特征提取器的目标反过来让判别器判断失败。对抗平衡之后特征向量里的摄像头域信息被压制留下的是更纯粹的身份信息。这一套数值指标往往最好因为全程在特征空间操作不产生像素伪影。缺点是要写清楚对抗损失和梯度反转层的原理答辩时得能讲明白为什么判别器分不出来反而更好。三套不是互斥的。我在工程里见过把第一套和第三套串起来用的先用CycleGAN做图像级风格转换再加特征级对抗mAP比单用一整套高2到3个点代价是训练时间接近两倍。时间紧张的话先做第一套把链路跑通后面再加第三套做消融对比这是性价比最高的路线。2.3 网络结构怎么搭CycleGAN加ResNet50的组合与两阶段训练生成器我优先用U-Net编解码结构。U-Net有跳连接能把输入图像的轮廓直接传到输出端行人轮廓不容易丢。判别器用PatchGAN它把输入图像切成若干个Patch分别判断真假输出一个概率矩阵。相比整图判别器PatchGAN在256x128的行人图像上参数更少训练更稳还能保留局部纹理约束生成的图不容易出现整块颜色对了但细节全糊的问题。ReID主干常见做法是ResNet50加全局平均池化加载ImageNet预训练权重。这里有一个关键决策GAN和ReID要不要端到端联合训练。多数复现工程选择两阶段——第一阶段只训CycleGAN第二阶段加载收敛的生成器并冻结再去训ReID。为什么不联合训练因为GAN的对抗损失和ReID的交叉熵损失同时反传两个优化目标会打架训练曲线一会儿好一会儿坏出了问题很难定位是生成器的问题还是检索网络的问题。两阶段虽然慢但指标异常时能快速分清是哪一段的责任。CycleGAN训练还有一个关键细节循环一致性损失。生成器把A域图转成B域风格后要再用另一个生成器转回A域要求还原图和原图尽量一致。这个约束保证转换过程中身份信息、姿态和背景结构被保留。有些变体会在前景区域加mask让生成器只对背景和光照做修改行人区域尽量保持原样进一步降低身份信息丢失的风险。ResNet50直接用PyTorch官方ImageNet权重即可CycleGAN没有现成预训练一般从零开始。数据集小时生成器容易过拟合输入侧做随机水平翻转和随机擦除这两种增强对行人图像都很有效。训练进度管理也值得提前规划CycleGAN阶段每5个epoch把A域原图、生成B域图并排存一次训练结束后回看这些图能直观看到风格迁移质量的变化ReID阶段每10个epoch跑一次验证集的Rank-1并记录下来。这些日志会成为实验报告里的可视化素材也是判断训练是否正常的重要依据。3. 跑通源码Market1501数据集、GAN训练主循环与关键参数3.1 数据集准备Market1501的目录结构、文件名与训练测试清单做行人重识别最绕不开的数据集是Market1501训练集约751个身份测试集约1500个身份图像来自校园多个摄像头。下载后目录结构一般是这样的Market-1501-v15.09.15/ ├── bounding_box_train/ # 训练集 │ ├── 0001_c1s1_000051_00.jpg │ ├── 0002_c2s1_000101_00.jpg ├── bounding_box_test/ # 测试候选池 gallery ├── query/ # 测试查询图 query └── gt_query/ # 手工标注的匹配关系文件名编码规则很关键0001是行人IDc1s1表示摄像头1下的第1个片段000051是帧号最后的00是连拍序号。训练时用bounding_box_train测试时用query作为检索目标、bounding_box_test作为候选池。源码工程里最好写一个生成清单的脚本把图像路径 ID 摄像头编号输出到一个txt避免每次改路径都要改代码。下面这个脚本片段是我常用的数据清单生成方式# prepare_list.py —— 生成训练清单和测试清单 import os from collections import defaultdict def parse_market1501_filename(fname): # 0001_c1s1_000051_00.jpg - id0001, cam1 parts fname.split(_) pid int(parts[0]) cam int(parts[1][1]) return pid, cam def build_list(img_dir, save_path): lines [] for fname in os.listdir(img_dir): if not fname.endswith(.jpg): continue pid, cam parse_market1501_filename(fname) lines.append(f{os.path.join(img_dir, fname)} {pid} {cam}) with open(save_path, w) as f: f.write(\n.join(lines)) build_list(Market-1501-v15.09.15/bounding_box_train, train_list.txt)脚本的逻辑很简单遍历图像目录从文件名解析出行人ID和摄像头编号写成模型训练时可以直接读取的三列清单。摄像头编号一定要保留因为后面按摄像头分域、做评测协议时的同摄像头排除都需要这个字段。如果后面要喂给CycleGAN做A/B域风格迁移还要在加载器里按cam字段把数据分成两个子集这一步常常被人漏掉。3.2 第一阶段CycleGAN训练循环生成器与判别器如何交替更新第一阶段训CycleGAN核心是两组生成器G_A2B和G_B2A加两组判别器D_A和D_B。我习惯把A域设为摄像头列表里编号较小的那一组B域设为编号较大的那一组避免每次重跑都换方向。# train_cyclegan.py —— 第一阶段训练循环核心 import torch import torch.nn.functional as F G_A2B CycleGenerator().cuda() # A域 - B域 G_B2A CycleGenerator().cuda() # B域 - A域 D_A PatchDiscriminator().cuda() # 判别A域真假 D_B PatchDiscriminator().cuda() # 判别B域真假 opt_g torch.optim.Adam( list(G_A2B.parameters()) list(G_B2A.parameters()), lr2e-4, betas(0.5, 0.999)) opt_d torch.optim.Adam( list(D_A.parameters()) list(D_B.parameters()), lr2e-4, betas(0.5, 0.999)) l1 torch.nn.L1Loss() bce torch.nn.BCEWithLogitsLoss() for real_a, real_b in paired_loader: # 前向风格转换与还原 fake_b G_A2B(real_a) rec_a G_B2A(fake_b) fake_a G_B2A(real_b) rec_b G_A2B(fake_a) # 判别器损失 loss_d bce(D_B(real_b), torch.ones_like(D_B(real_b))) \ bce(D_B(fake_b.detach()), torch.zeros_like(D_B(fake_b))) \ bce(D_A(real_a), torch.ones_like(D_A(real_a))) \ bce(D_A(fake_a.detach()), torch.zeros_like(D_A(fake_a))) # 生成器损失对抗损失 循环一致性损失 loss_g bce(D_B(fake_b), torch.ones_like(D_B(fake_b))) \ bce(D_A(fake_a), torch.ones_like(D_A(fake_a))) \ l1(rec_a, real_a) * 10.0 \ l1(rec_b, real_b) * 10.0参数说明生成器和判别器的初始学习率都用2e-4beta1用0.5这是CycleGAN论文里验证过的组合比默认的0.9更稳。循环一致性损失的权重10.0是论文里的常用值调低到1会让生成图随意变色调高到50会压制风格迁移让生成图几乎等于原图。这里的paired_loader是同一个行人在A/B两个摄像头下的样本对不需要像素级对齐这也就是CycleGAN常说的无配对图像翻译。判别器更新时对fake_b和fake_a做detach()是必须的否则梯度会穿过判别器进入生成器生成器会被拉向一个错误的目标。3.3 第二阶段ReID训练循环生成图怎么参与分类第一阶段结束后把G_A2B的权重存下来。第二阶段加载进来并冻结然后把原图和生成图混合一起训练ReID网络。这是整个源码工程里最关键的承上启下部分。# train_reid_with_gan.py —— 第二阶段训练循环核心 import torch import torch.nn.functional as F G_A2B CycleGenerator().cuda().eval() # 加载第一阶段权重后冻结 for p in G_A2B.parameters(): p.requires_grad False reid_net ResNet50ReID(num_classes751).cuda() opt_r torch.optim.Adam(reid_net.parameters(), lr3e-4, weight_decay5e-4) ce torch.nn.CrossEntropyLoss() for epoch in range(epochs): for imgs_a, imgs_b, ids in train_loader: with torch.no_grad(): fake_b G_A2B(imgs_a) # 生成B域风格图 reid_net.zero_grad() logits reid_net(torch.cat([imgs_a, fake_b], dim0)) loss_r ce(logits, torch.cat([ids, ids], dim0)) loss_r.backward() opt_r.step()这里需要注意两个细节。一是imgs_a和fake_b拼在一起过ReID网络batch维度翻倍让分类器同一轮同时见到原始风格和迁移风格这个设计比单独训完再混合更稳。二是生成器用torch.no_grad()推理fake_b不参与梯度计算只当数据增强用。如果显存紧张可以把fake_b先存到硬盘做成离线增强代价是生成图在多个epoch里固定不变ReID网络容易过拟合到生成伪影上。我一般用在线生成每轮都用重新采样的batch去生成相当于自带随机性。如果你后面想改成端到端联合训练只需要把G_A2B从eval切回train、把requires_grad恢复为True然后在loss_r里加上判别器损失和循环一致性损失。但这条路调参成本会成倍上升我建议把联合训练作为消融实验的对比项而不是主方案。3.4 必调参数表学习率、batch size、判别器更新频率GAN做行人重识别有很强的玄学成分但下面这张参数表是我反复跑过之后沉淀下来的起步值单卡12GB显存可以照抄。参数推荐值说明生成器/判别器初始学习率2e-4beta10.5AdamReID网络学习率3e-4weight_decay5e-4输入分辨率256x128超过384x192容易爆显存ReID batch size32加上生成图后总batch为64判别器更新频率1:1 或 1:3判别器过强时降到1:3CycleGAN阶段epoch100~200看生成图质量是否稳定ReID阶段epoch60左右后半段线性衰减学习率循环一致性损失权重10.0CycleGAN论文基准值调参的核心观察点是判别器损失和生成器损失的走势。如果判别器损失一路降到接近0生成器梯度会消失生成图开始出现绿色噪点这就是判别器太强的信号把更新频率从1:1改成1:3。如果生成器损失下降但生成图一直模糊往往是循环一致性权重太大。这套排查思路比盲改学习率更可靠也是源码工程里最容易抄走的经验。4. 实验报告与答辩PPT把源码成果变成能验收的交代源码实验报告答辩PPT三个交付物里真正决定分数的是后两个。模型跑得再好讲不清楚就亏了。实验报告的核心不是贴代码而是回答三个问题你解决什么问题、你的方案比基线好在哪、GAN带来的增益是不是真实可靠。4.1 实验报告五段式从问题定义写到消融实验一份合格的ReID实验报告我一般按五个小节组织摘要、问题定义与相关工作、方法设计、实验与讨论、结论。摘要控制在300字以内写清楚用什么生成对抗网络结构、在哪个数据集上、比什么基线提升了多少Rank-1和mAP。问题定义部分重点讲清楚行人重识别为什么难最好配两张同一个行人在不同摄像头下的对比图这张图比任何文字都直接。方法设计部分放一张网络结构图画清楚CycleGAN生成器如何接入ReID主干这是答辩时被问得最多的区域。实验部分的表格至少要包含四行BaselineResNet50直接训练、加CycleGAN风格迁移、加特征级对抗、两者都加。每行都要列Rank-1、mAP、参数量、推理时间。不要把负结果藏起来比如增加特征级对抗后mAP下降0.8个百分点推测原因是域判别器与分类器的对抗目标存在冲突。答辩现场这类诚实的写法比全篇显著提升可信得多。报告最后附上生成图对比和检索结果Top10截图注意截图要选有代表性的同一个query下基线检索错了而你的方法检索对了的那个case比挑一张顺眼的更有说服力。4.2 答辩PPT 12页链路从痛点讲到Rank-1曲线答辩PPT要有一条完整的逻辑链页面控制在10到12页每页只放一个核心信息。我惯用的结构是第1页标题写清基于GAN的行人重识别方法研究。第2页背景与痛点放跨摄像头外观变化图。第3页相关工作列出CycleGAN、StyleGAN和已有的GAN-ReID论文。第4页方法总览一张图讲清楚两阶段链路。第5到第7页分别展开生成器、判别器、ReID主干的结构细节。第8页数据集与实验设置。第9页结果表格标出最优值。第10页可视化原图、生成图、检索结果Top10。第11页消融实验。第12页结论与未来工作。每页标题尽量用断言句不要用方法介绍这种模板话。比如第5页标题写用CycleGAN把摄像头A的风格转成摄像头B第9页写Rank-1从82.3提升到87.1mAP提升4.6个百分点。在讲结果那页把评价指标的定义放在页脚方便老师随时回看。PPT里放生成图对比时记得标注清楚哪些是真实拍摄、哪些是GAN生成老师问起来你能立刻指出生成图像的特征这个细节很能体现你对方案的理解深度。4.3 算清楚Rank-1与mAP评估脚本与评测协议陷阱行人重识别的评价指标有三个常用口径Rank-1表示检索结果第一位的正确率mAP表示所有正确匹配在检索结果中位置的平均精度CMC曲线是Rank-1到Rank-k的累积曲线。三个指标里mAP更严格因为它惩罚了正确结果排得靠后的情况。计算时有一个协议陷阱query里的每一张图在候选池里必须排除同摄像头同ID的图像否则mAP会虚高。这个错误在刚上手的人里出现率极高指标高得离谱时首先要怀疑这里。下面是一个可直接使用的评估脚本核心片段# eval.py —— 计算Rank-1与mAP import numpy as np import torch import torch.nn.functional as F def evaluate(query_feats, gallery_feats, query_ids, gallery_ids, query_cams, gallery_cams): # 余弦相似度矩阵 q F.normalize(query_feats, dim1) g F.normalize(gallery_feats, dim1) sim torch.mm(q, g.t()).cpu().numpy() mAP 0.0 rank1 0.0 n_query sim.shape[0] for i in range(n_query): # 协议排除同摄像头且同ID的gallery图像 valid ~((gallery_ids query_ids[i]) (gallery_cams query_cams[i])) scores sim[i].copy() scores[~valid] -1e5 order np.argsort(-scores) matches gallery_ids[order] query_ids[i] rank1 matches[0] pos_idx np.where(matches)[0] ap sum((np.arange(1, len(pos_idx) 1)) / (pos_idx 1)) / max(len(pos_idx), 1) mAP ap return rank1 / n_query, mAP / n_query这段代码先算query特征和gallery特征的余弦相似度矩阵然后逐条计算每个query的正确匹配位置。关键在这两行valid把同摄像头同ID的图像排除掉被排除的位置分数改成-1e5而不是0。设成零的坏处是如果一张错误图恰好没被排除、得分也是负值排序时可能出现在被排除图前面干扰Rank-1。用-1e5能保证被排除图永远排在最后。计算AP时pos_idx是正确匹配在排序结果中的位置索引公式本质上是对每个正确匹配的精度做平均。5. 避坑GAN做行人重识别的四个翻车现场与排查方法5.1 生成图像失真Rank-1不升反降现象加载预训练的CycleGAN生成器把A域图像转换到B域之后ReID的Rank-1从82掉到79生成的图有明显色块和斑点行人边缘发糊。 原因生成器训练不充分或者A/B域划分不干净。很多复现工程直接把所有训练ID的图像丢给CycleGAN没有按摄像头分域生成器学到的不是摄像头风格而是身份和场景的混杂风格。 解决先确认文件名里c1、c2这些字段是有效的按摄像头编号把训练集切成A、B两个域再训CycleGAN。生成图像仍有伪影时把输出分辨率降到和训练一致输入前再做一次归一化校准。还有一个容易被忽略的点CycleGAN训练集里的图像如果包含大量背景区域生成器会把背景风格迁移到行人身上解决方法是训练前做行人检测裁剪只保留bbox区域。5.2 判别器收敛太快生成器梯度消失现象训练几千步后判别器损失降到0.01以下生成图像出现绿色噪点ReID损失也突然升高。 原因PatchGAN判别器能力比生成器强小数据集上很快就分清真伪生成器收到几乎为0的梯度训练陷入停滞。这是所有GAN训练里的经典问题行人重识别数据集规模不大尤其容易触发。 解决把判别器更新频率从1:1改成1:3给生成器更多学习空间。我一般还会在判别器输入上叠加标准差0.05的高斯噪声阻止判别器把真伪判断建立在对噪声的捕捉上。改完参数后回看生成图如果绿色噪点消失、图像恢复连续纹理说明对症了。5.3 数据泄漏同一个行人在训练集和测试集里同时出现现象训练时mAP很高Rank-1到了90但测试集上的表现掉到70训练和评估差距大。生成器训练时损失的下降曲线也异常平滑。 原因复现工程里如果只按文件夹简单划分没有严格按Market1501官方协议走就会出现同一个行人的图像既进训练集又进测试集。GAN尤其怕这个因为生成器会把测试域的分布提前学进训练数据里ReID网络看到过答案泛化自然崩塌。 解决严格使用官方划分文件训练清单一律从bounding_box_train生成测试用query和bounding_box_test。在工程里放一个断言脚本检查训练集和测试集ID集合的交集必须为空。每次跑实验前先过一遍这个检查能拦住绝大多数低级错误。我吃过一次亏后把这一条写进了工程模板后续所有复现都默认执行。5.4 显存溢出与训练中断batch size与显存回收现象单卡12GB显存batch size设64跑两步就OOM改成32能跑但训练到一半进程被杀报错显示CUDA out of memory。 原因循环里持有生成图、判别器输出、特征向量等中间变量的引用反向传播后显存没有完全释放。CycleGAN的生成器在256x128下不算大但如果把A域和B域的图像拼在一起过生成器显存压力会直接翻倍。 解决不用的中间变量用del加torch.cuda.empty_cache()手动回收。生成器推理时用torch.no_grad()包住省掉自动求图占用的显存。ReID阶段如果batch size上不去可以开梯度检查点torch.utils.checkpoint对ResNet50的某些层做重计算用时间换显存。最保险的做法是先把batch size调到16把流程跑通确认整个循环没有显存泄漏后再往上加。6. 从复现到改进用三步验证你的GAN-ReID方案值不值得投入6.1 三步验证同域对比、跨域对比与数据泄漏检查第一步先跑数据泄漏断言确认训练集和测试集的ID集合交集为空这一步不通过后面所有指标都是黑匣子里的幻觉。第二步做同域对比和三组消融只用原始图像、原始图像加随机图像扰动、原始图像加GAN生成图像。第三种明显高于第二种说明GAN确实带来了新的有效信息如果三种差不多说明生成样本没有信息增益问题大概率出在生成器多样性不够。第三步做跨域验证把在Market1501训练的模型直接放到另一个数据集上评估看Rank-1的掉点幅度。掉点小说明学到的是身份鲁棒特征而不是数据集特有风格。6.2 可预见的三个进阶方向遮挡、噪声标签与轻量化这个方向往后走有三个具体做法。第一是遮挡ReID用GAN对待遮挡的行人图像做局部修复这个方向和图像修复任务同源在真实监控场景里有明确需求。第二是噪声标签处理真实监控数据的标注本身有错误可以用置信度筛选出不可靠样本交给生成器重绘后再参与训练降低错误标签的干扰。第三是轻量化把ReID主干从ResNet50换成MobileNetV3配合蒸馏让移动端设备也能在线跑检索。这三个方向任何一个做深都比继续堆一个更高的Rank-1更有说服力也适合写在答辩PPT的未来工作页面。我现在每次跑完训练都会强制自己先过一遍数据泄漏断言和消融对比再去看Rank-1数字。吃过一次指标虚高但上线翻车的亏之后我明白了在这个方案里GAN和ReID任何一个环节出问题最终都会表现为指标异常但指标异常背后的原因从来不会自己浮现出来。希望帮到你。本文还有配套的精品资源点击获取