ARTICLE DETAIL

资讯详情

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

基于Parser的车辆ReID源码解析:配置驱动训练与复现

基于Parser的车辆ReID源码解析:配置驱动训练与复现 简介车辆重识别Vehicle ReID是智慧交通与安防监控中的关键任务旨在跨摄像头检索同一车辆。这份源码包面向具备一定深度学习基础的研究者或算法工程师基于Parser参数解析方式组织车辆ReID项目包含Python源码、预训练权重与项目说明可直接用于训练、验证与推理流程。资源共59个文件以49个Python脚本为核心覆盖模型构建、损失函数、评估指标、数据预处理与工具模块6个YAML文件用于配置实验参数另有README、依赖清单和项目说明文档压缩包仅2.07MB结构清晰便于按模块修改扩展。目前已有41人浏览/学习。借助预训练权重和模块化代码可快速复现车辆重识别基线效果并针对具体场景进行数据适配与算法调优适合入门学习与二次开发。1. 车辆ReID为何绕不开Parser这份Python源码包解决了什么车辆重识别Vehicle ReID在智慧交通、停车场管理、园区安防里都是刚需很多人以为难点只在模型涨点真正把开源代码跑起来才发现最磨人的是参数管理。同一套模型换个数据集、改个Batch Size、调一下学习率都要去硬翻脚本里的全局常量改错了还容易把训练跑崩。这份源码包的思路是“基于Parser解析”所有路径、模型结构、训练策略都收敛到配置文件和命令行argparse里改实验不用动核心代码预训练权重也一并打包适合想快速复现论文、或者在真实视频上验证效果的人。这套源码最大的价值不只是提供一个模型而是把车辆ReID的完整链路——数据加载、Backbone、损失函数、评估指标、训练与测试脚本——用一套可控的配置方式串起来。新手按项目说明整理数据、改配置就能跑通老手也能在Parser基础上快速做消融省去重复造轮子。接下来我从解析器设计讲起再落到训练、评估和排错最后给一个实验管理技巧。2. Parser解析器设计用一套配置驱动数据、模型与训练流程车辆ReID的实验变量非常多数据集路径、图片分辨率、backbone类型、预训练权重路径、损失超参、训练轮数、学习率策略、随机种子……如果这些变量散落在各个脚本里复制一份实验就要改上十个地方而且很容易改漏。这份源码把变量全部收编到Parser层主训练脚本只认一个config对象这也是它叫“基于Parser解析”的原因。我在拆包时第一件事就是找配置文件目录看它到底支持哪些参数。2.1 配置文件长什么样YAML argparse 双轨源码包通常包含一个configs目录里面每个YAML文件对应一种实验配置。比如VehicleID.yml和VeRi.yml一个跑一个数据集互不干扰。YAML把参数按块组织我摘一段典型配置来拆解# configs/VehicleID.yml data: root: ./data/VehicleID image_size: [256, 256] batch_size: 64 num_workers: 4 model: backbone: resnet50 pretrained: ./weights/vehicle_reid_pretrained.pth feature_dim: 2048 loss: triplet_margin: 0.3 weight_ce: 1.0 weight_triplet: 1.0 train: epochs: 120 base_lr: 0.00035 lr_scheduler: warmup_multi_step milestones: [40, 80] warmup_epochs: 5 seed: 42 test: batch_size: 128 dist_metric: cosine dataset: VehicleID query_list: query_list.txt gallery_list: gallery_list.txtdata.root是数据集根目录源码内部会在这个路径下找图片子目录和列表文件。image_size是训练时统一的缩放尺寸车辆ReID常用256×256它直接影响计算量和显存占用不要随便改成特别大的值否则BN的batch统计容易不稳定。model.backbone支持切换主干但要注意同步改feature_dim——resnet50是2048resnet34是512改错维度的话后面的BN层和损失函数会直接报错。loss块里的两个权重我一般保持1:1起步调试时可以调大Triplet权重让特征推拉更激进。train块中milestones是学习率衰减的轮次节点warmup_epochs是预热轮数这两个参数直接决定收敛曲线。YAML只写值、不写逻辑方便对比不同实验。但光有YAML不够因为调试时经常想临时减小batch_size或者跑一个短训练如果每次都去编辑YAML容易留下手滑导致的伪造记录。所以源码包通常还会用argparse接收命令行覆盖参数两个来源合并到一起命令行优先级更高。这也是“Parser解析”名字的由来所有参数在启动时被解析成一份统一配置而不是散落在代码里。2.2 解析器的核心实现从命令行到全局参数对象核心模块一般叫parser.py或config.py。它的任务是三步读取YAML文件、解析命令行参数、合并后输出一个全局config对象。我见过很多ReID项目都是这个套路源码包里实现得也比较典型# config.py import argparse import yaml def load_config(): parser argparse.ArgumentParser(descriptionVehicle ReID Config) parser.add_argument(--config, typestr, defaultconfigs/VehicleID.yml, helpPath to yaml config) parser.add_argument(--batch_size, typeint, defaultNone, helpOverride batch_size from CLI) parser.add_argument(--epochs, typeint, defaultNone, helpOverride epochs from CLI) parser.add_argument(--backbone, typestr, defaultNone, helpOverride backbone from CLI) parser.add_argument(--tag, typestr, default, helpExperiment tag for log/checkpoint naming) args, unknown parser.parse_known_args() with open(args.config, r, encodingutf-8) as f: cfg yaml.safe_load(f) if args.batch_size is not None: cfg[data][batch_size] args.batch_size if args.epochs is not None: cfg[train][epochs] args.epochs if args.backbone is not None: cfg[model][backbone] args.backbone # 如果改了backbonefeature_dim也要跟着改 cfg[model][feature_dim] { resnet50: 2048, resnet34: 512, mobilenet_v2: 1280, }[args.backbone] if args.tag: cfg[tag] args.tag return cfg这段代码里parse_known_args()用unknown接收不认识的参数好处是即使某个版本新增了参数旧配置也不会因为多传了参数而崩掉。cfg是一个嵌套字典后续通过cfg[model][backbone]访问。我在写第二个参数时通常会偷懒直接用config.py里的if分支但大批量扩展时这个方式很烦。更优雅的做法是支持点路径覆盖比如命令行传--model.backbone resnet50然后在load_config里递归把model.backbone拆分键再写进字典这样每加一层配置不用改Parser函数。源码包里常见的是YAML默认值加少数几个命令行覆盖参数比如--batch_size、--epochs、--resume。如果你要加自己的参数建议优先在YAML里加然后只给最活跃的几个变量配命令行入口保持Parser简洁。另外--tag这个参数非常有用我后面跑实验矩阵时靠它区分日志文件如果你的源码包没带这个参数可以在load_config里自己拼一个时间戳字符串。2.3 参数优先级与改造建议这套设计的优先级链是YAML默认值 命令行覆盖 代码内强制校验。比如feature_dim在命令行传了--backbone之后会被强制映射成对应值这就是代码内强制防止你只改backbone忘记改维度。我在复现时特别看重这个优先级因为车辆ReID的配置项互相牵连比如image_size会影响模型输入维度pretrained路径错了会影响加载milestones设置不对会导致学习率降不下来。对想做二次开发的人我的建议是不要改它原来的configs/VehicleID.yml而是新增一个自己的配置文件。比如你想对比“resnet50 vs mobilenet_v2”在同样数据上的效果就复制一份configs/compare_backbone.yml把backbone改成mobilenet_v2feature_dim改成1280。这样原配置还能留着做基准出问题也好回滚。如果你发现Parser没有暴露某些参数优先在load_config里补充而不是跑到训练脚本里去写死。血泪教训很多所谓“复现结果不一致”的问题最后查出来是有人不小心把实验配置写死在代码里Parser再强大也救不回来。3. 车辆ReID主干实现特征提取、损失函数与评价指标Parser只管分发参数真正干活的是模型定义和训练评估脚本。这一章把主干流程拆开backbone怎么选、预训练权重怎么用、损失函数怎么组合、评估指标怎么算。理解这一层你改Parser参数时才不会“改了不知道影响哪里”。很多初学者拿到代码直接跑测出来分数不对又不知道怎么调就是因为没搞懂这条链路里每个参数的作用。3.1 Backbone选择与预训练权重加载车辆ReID最常用的backbone是ResNet系列源码包里默认resnet50。选它的原因很实在ImageNet预训练权重好找特征表达能力强而且PyTorch官方直接支持。车辆纹理、颜色、车窗形状这些特征ImageNet能提供很好的底层先验所以初始化权重一般不会用随机值。源码在加载权重时通常这样做import torch import torchvision.models as models def build_model(cfg): backbone_name cfg[model][backbone] feature_dim cfg[model][feature_dim] if backbone_name resnet50: backbone models.resnet50(pretrainedFalse) backbone.fc torch.nn.Identity() elif backbone_name resnet34: backbone models.resnet34(pretrainedFalse) backbone.fc torch.nn.Identity() elif backbone_name mobilenet_v2: backbone models.mobilenet_v2(pretrainedFalse).features backbone torch.nn.Sequential(backbone, torch.nn.AdaptiveAvgPool2d(1)) backbone torch.nn.Flatten()(backbone) # 注意这里实际用法需要包装 else: raise ValueError(fUnknown backbone: {backbone_name}) # 实际使用时backbone的输出会先过一个BN层 bn torch.nn.BatchNorm1d(feature_dim) model torch.nn.Sequential(backbone, bn) pretrained_path cfg[model].get(pretrained, None) if pretrained_path: state_dict torch.load(pretrained_path, map_locationcpu) # 去掉不需要的key state_dict {k: v for k, v in state_dict.items() if fc not in k} model.load_state_dict(state_dict, strictFalse) return model代码里故意用了pretrainedFalse然后手动加载本地权重。为什么因为torchvision在pretrainedTrue时会自动下载官方权重但服务器经常处于离线环境或者你想加载的并不是ImageNet权重而是别人在VehicleID上训好的权重。手动加载更可控。strictFalse意味着允许某些key缺失或不匹配常见情况就是预训练权重里带module.前缀来自DataParallel或者最后的分类层维度不一样。所以我会先把fc相关的key过滤掉只加载backbone的卷积层和BN层权重。feature_dim这个参数就是Backbone输出的特征维度resnet50是2048resnet34是512mobilenet_v2是1280。源码包里很多地方依赖这个值比如BN层、分类头、距离计算。改backbone时如果不同步改维度会出现维度不匹配的报错或者更隐蔽的——数据流数值错乱但程序不报错。我建议你改配置时把feature_dim和backbone当做一个整体来改。3.2 损失函数组合Triplet Loss CrossEntropy车辆ReID的难点在于“同类不同款、同款不同色”都能被认成同一辆车所以需要度量学习。纯分类交叉熵只能让模型记住ID映射但特征空间里同类距离不一定近。源码包通常采用双损失CrossEntropy做ID分类Triplet Loss做特征推拉。部署时删除分类头只保留特征输出。训练循环核心片段如下# train_one_epoch.py 片段 from losses import CrossEntropyLoss, TripletLoss ce_loss CrossEntropyLoss(ignore_index-1) triplet_loss TripletLoss(margincfg[loss][triplet_margin]) for images, ids in dataloader: images, ids images.cuda(), ids.cuda() features, logits model(images) loss_ce ce_loss(logits, ids) loss_triplet triplet_loss(features, ids) loss cfg[loss][weight_ce] * loss_ce \ cfg[loss][weight_triplet] * loss_triplet optimizer.zero_grad() loss.backward() optimizer.step()这里的model不是一个简单的Sequential通常是双头结构backbone输出2048维特征分出一支输入分类器得到logits分类器的类别数等于训练集车辆ID数量另一支直接用特征算Triplet。ignore_index-1用来跳过那些没有ID标签的样本如果你在数据准备阶段把部分图片打上-1标签这句就能保证它们不参与交叉熵计算。Triplet Loss的margin很关键。它表示“正样本对距离”要比“负样本对距离”至少小多少。取值太小比如0.1模型容易偷懒太大比如1.0会让特征被过度拉开造成类间距离饱和、不好收敛。源码默认0.3是个比较平衡的起点。我在调参时如果发现rank1上不去会先把margin降到0.2配合更长的warmup看效果而不是盲目加大。还有一个至关重要的细节是采样器。ReID训练不能随机抽图否则一个batch里可能全是不同IDTriplet根本构造不出正样本对。源码包里通常实现一个“PK采样器”每个batch选P个ID每个ID选K张不同图片保证batch里存在足够的正样本。比如batch_size64, P16, K4。这个采样逻辑绑定在DataLoader里如果你为了调试直接改torch.utils.data.DataLoader的samplerTriplet Loss基本就废了。我一般会先打印一个batch的ID标签确认每个ID的图片数量≥2。3.3 评估流程CMC与mAP的计算细节车辆ReID评估不像分类任务只看准确率而是要看排序能力。源码包里test.py会先提特征再算距离矩阵最后计算CMC和mAP。核心流程如下# eval.py 片段 from metrics import compute_cmc_and_map query_feats extract_features(model, query_loader) # [Nq, dim] gallery_feats extract_features(model, gallery_loader) # [Ng, dim] dist_mat euclidean_dist(query_feats, gallery_feats) if cfg[test][dist_metric] cosine: # 归一化后使用余弦距离 dist_mat cosine_dist(query_feats, gallery_feats) cmc, mAP compute_cmc_and_map(dist_mat, query_ids, gallery_ids, query_cams, gallery_cams, topk[1, 5, 10])query是待查图片集合比如从某摄像头拍到的车gallery是候选库比如停车场所有卡口拍到过的车。系统给每张query车返回一组排序结果CMC看前k位命中率mAP看整体排序质量。很多人忽略的是“同源过滤”如果query里的车和gallery里的车来自同一个摄像头那么它们很可能本来就长得很像检索它会让分数虚高。源码里compute_cmc_and_map通常会根据camera_id排除“query_id相同且camera_id相同”的样本但你自己写评估代码时很容易漏。dist_metric也很关键。欧氏距离和余弦距离在特征归一化后的排序结果可能只有细微差别但如果模型没有BNNeck两者差异会被放大。我在实际比测时会把dist_metric配置成可选项分别跑一遍看rank1和mAP而不是拍脑袋选一个。有些模型训练时用了Triplet它在欧氏距离下定义margin那评估用欧氏距离更贴合理论有些模型用分类损失为主余弦距离往往表现更好。源码包默认cosine但如果你手上的预训练权重来自只看Triplet的模型建议改成euclidean重测。4. 从源码到复现数据准备、训练与测试的完整操作这一章是给准备“一键复现”的人写的。我按实际动手顺序从数据集整理讲到跑通训练再讲到用预训练权重测试。每一步都给出我常用的命令和参数你照着做就能把这份Python源码用起来。这里不是贴官方README而是我自己踩过一遍后整理的最短路径。4.1 数据集整理VehicleID / VeRi 的目录规范车辆ReID公开数据集最常用的是VehicleID和VeRi-776两者目录结构略有不同。源码包的项目说明里会写清楚支持哪个我拆包看到的多半是下面的VehicleID布局data/ └── VehicleID/ ├── image/ # 所有车辆图片 │ ├── 00001.jpg │ ├── 00002.jpg │ └── ... ├── train_test_split/ # 官方划分文件 │ ├── train_list.txt │ ├── test_list_800.txt │ ├── test_list_1600.txt │ └── test_list_2400.txt ├── query_list.txt # 由项目说明生成 └── gallery_list.txtimage/存放所有原始图片文件名一般是00001.jpg这样。官方划分文件里train_list.txt每行是“图片路径 车辆ID”但这里的ID是从0开始的连续编号和文件名没有直接关系。query_list.txt和gallery_list.txt可能需要你根据测试集自己生成源码包里通常附带了生成脚本。如果你是下载VeRi-776目录里会多一个image_query/和image_gallery/。无论哪种data.root都指向这个外层目录源码内部会拼接出完整路径。我一般会写一个小脚本把原始数据整理成上述格式顺便检查图片是否能正常打开。下面这段脚本可以帮你生成训练列表# prepare_data.py import os data_root data/VehicleID image_dir os.path.join(data_root, image) train_list [] # 从官方划分文件读取训练图片和ID with open(os.path.join(data_root, train_test_split, train_list.txt)) as f: for line in f: img_name, vehicle_id line.strip().split() img_path os.path.join(image_dir, img_name) if not os.path.exists(img_path): print(f[WARN] missing: {img_path}) continue train_list.append((img_path, int(vehicle_id))) # 写入格式绝对路径 空格 ID with open(os.path.join(data_root, train_list.txt), w) as f: for img_path, vid in train_list: f.write(f{img_path} {vid}\n) print(f[INFO] train samples: {len(train_list)})注意这里生成的train_list.txt是带完整路径的如果你的data.root在配置文件里写的是相对路径可能和这里的绝对路径冲突。我习惯在配置里使用相对路径在脚本里用os.path.join(data_root, ...)拼接这样换机器只是改data.root一个值。另外一个很容易被忽略的坑是必须过滤掉那些在官方test split中出现的图片。如果训练集混入测试样本mAP会虚高你部署时立刻露馅。官方划分文件里会严格区分train和test你照着读就不会混。4.2 训练一张显卡跑通的命令与关键参数确认数据没问题后进入源码包根目录先看requirements.txt和project说明安装依赖。然后是训练命令假设你已经装好PyTorch、yaml、opencv等包python train.py --config configs/VehicleID.yml --batch_size 64 --epochs 120如果显存不够把batch_size调成32python train.py --config configs/VehicleID.yml --batch_size 32 --epochs 120这里改动只通过命令行传参不碰YAML文件。train.py内部会读取--config指定的YAML然后用命令行参数覆盖batch_size。我特别强调这个是因为很多新手会直接编辑VehicleID.yml去改batch_size结果测试对比时忘了自己改过什么导致实验无法复现。用命令行覆盖至少运行历史里还留着一条命令。如果你需要从断点继续训练源码包通常提供--resume参数python train.py --config configs/VehicleID.yml --resume weights/checkpoint_last.pth训练时日志里会打印loss、rank1、mAP。我一般会盯着这三个值的变化趋势如果loss在降但rank1不动先检查验证集的图片是否做了和训练集一致的预处理比如归一化、resize如果loss根本没降再检查学习率是不是被warmup压得太低。车辆ReID里warmup_epochs设为5到10比较常见尤其你加载ImageNet预训练权重时前期直接大步更新会把权重冲坏。源码包里默认warmup_multi_step调度器milestones[40, 80]表示在第40轮和第80轮各乘一次0.1的学习率衰减。训练过程中我还有一个习惯每5个epoch手动跑一次快速验证而不是傻等到120轮全部结束。这样能早期发现数据加载异常比如图片通道顺序反了、ID编号错乱、某些图片解码失败而不是等了半天才看到模型崩了。具体做法是改一个--eval_every 5的参数如果源码包没有这个参数就自己写一个wrapper脚本定期调test.py。4.3 测试阶段用预训练权重输出特征并可视化源码包里自带的预训练权重应该是已经在某个数据集上训练好的。拿到权重后执行测试命令python test.py --config configs/VehicleID.yml --weights weights/vehicle_reid_pretrained.pth --save_feature true--save_feature true会输出一个包含query和gallery特征的pickle文件这个文件可以离线反复计算指标不用每次都重新提特征。如果源码包有visualize.py它会把每辆query车的前5个检索结果画出来python visualize.py --query 00001.jpg --gallery_dir ./data/VehicleID/image没有可视化脚本时自己写也不难。核心步骤是加载特征文件、按距离排序、把gallery图片用OpenCV拼在一起。但有一点要提醒可视化保存时注意颜色通道OpenCV默认BGR用PIL保存RGB图会颜色不对。我在修这个坑时浪费了一个下午最后发现只是保存时少了一行cv2.cvtColor。测试阶段还要注意--weights路径是不是真的存在。很多人下载权重后解压放错目录运行时报FileNotFoundError然后开始怀疑源码有bug。其实只要torch.load之前打印一下路径就清楚了。5. 避坑指南Parser配置、权重加载与ReID复现的六个常见坑这部分是血泪经验。我从源码包的结构和常见复现问题里挑出几个最容易让人卡住又最不好定位的坑按“现象-原因-解决”写清楚帮你省下几天排查时间。这里没有理论全是实操叠了好几层buff才总结出来的。5.1 坑一Parser配置正确但加载图片全部黑屏现象训练能跑loss也在降但验证集上rank1几乎为0打印图片发现全是黑屏或单色块。原因图片路径拼接错误导致加载了空数组或者读取图片时用了cv2.imread返回BGR图但代码按RGB处理颜色通道反转。更隐蔽的情况是data.root后面多了一个/和image_dir拼出了data/VehicleID//imageLinux下能忍但Windows下会歧义。解决在Dataset的__getitem__里临时加print(self.data_paths[index])确认路径真实存在如果是颜色通道问题在图像增强前强制cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。我一般会先跑一个batch做预处理回显把图片保存到本地看一眼而不是直接开始全量训练。第一次回显花了半小时之后再也没有因为图片问题浪费过训练时间。5.2 坑二torch.load加载预训练权重报错“unexpected key”现象加载预训练权重时提示Missing key(s)或Unexpected key(s)程序中断。原因权重文件来自不同框架或不同模型定义。最常见的是多卡训练保存的权重带有module.前缀而当前模型是单卡定义或者原始模型的分类头类别数和你的训练集ID数不一致导致分类层的权重维度对不上。解决先打印state_dict.keys()和模型state_dict().keys()找出差异。如果只是因为module.用{k.replace(module., ): v for k, v in state_dict.items()}统一清洗。如果只是分类头维度不同load_state_dict(state_dict, strictFalse)跳过即可。记得不要整个权重直接load要先清洗再过滤否则即使不报错也会把不该加载的层污染进去。5.3 坑三Batch Size改小之后训练指标剧烈抖动现象把batch_size从64改成32loss曲线震荡严重验证集指标反而下降。原因ReID依赖batch内样本构成PK采样下batch size越小每个batch包含的ID数量越少三元组采样难度变大。另外BN在32的小batch上统计量不稳定尤其前几个epochwarmup没结束抖动更明显。解决如果显存只能支持32可以保持batch_size64做梯度累积每两个batch再更新一次参数。或者改用SyncBN让多个卡共享BN统计量单卡场景更简单的方案是显式降低学习率比如从3.5e-4降到2e-4再拉长warmup。不要一上来归咎于模型先看采样器和学习率。5.4 坑四评估时mAP分数虚高但真实场景效果很差现象在测试集上mAP能到90%部署到新视频上却检索不出来和测试结果明显不匹配。原因评估时没有排除同源样本query和gallery来自同一摄像头甚至同一时刻的截图被重复检索导致分数虚高。还有一种可能是你在生成query_list时把训练集的车也放了进去模型见过它们当然能认出来。解决检查compute_cmc_and_map里是否排除了相同(query_id, camera_id)组合如果源码包没有排除逻辑自己加两行过滤。如果数据集没有官方query/gallery划分不要自己随机切最好下载官方reid划分文件。评估前可以用一个小工具检查query的ID是否在训练集中出现出现就是数据泄漏。5.5 坑五YAML文件里写路径Windows与Linux分隔符不一致现象Windows上跑得好好的放到Linux服务器上就报FileNotFoundError。原因Windows路径用了\分隔符在Linux眼里是转义字符或者yaml里写了绝对路径换机器就失效。比如root: D:\data\VehicleIDLinux会解析成D:dataVehicleID完全错乱。解决所有路径统一用os.path.join拼接yaml里只写相对根目录的路径。在load_config里加一段校验如果data.root不存在直接报错并提示当前工作目录。我在自己的使用习惯里会在配置里额外加一个comment字段写清楚“路径相对项目根目录不要带盘符”。这样给别人传递配置时少很多无效沟通。5.6 坑六相同配置重启训练最终指标对不上原日志现象用完全一样的命令再跑一次最终rank1差了好几个点怀疑是源码随机性。原因没有固定随机种子。ReID里有随机采样、随机数据增强、shuffle、PyTorch的CUDA算子这些都不固定的话两次训练结果不可能完全一致。还有可能是你忘了设置PYTHONHASHSEED导致Python字符串哈希随机影响数据加载顺序。解决在train.py最开头设置torch.manual_seed(cfg[train][seed])、np.random.seed(seed)并在DataLoader里设置固定generator。如果追求严格可复现再设置torch.use_deterministic_algorithms(True)但注意这会限制某些操作比如某些插值算法普通实验不必开。我的习惯是固定种子后至少跑两次确认指标差异在0.1以内再谈调参。6. 进阶用法用Parser做实验矩阵一次跑完所有消融形而下的坑讲完了最后给你一个能立刻用上的进阶技巧怎么把Parser配置变成你的实验管理工具。车辆ReID论文里经常说“ablation study”实际就是同一份数据上一堆不同参数组合的对比。如果你不想每改一次配置就手动编辑YAML可以写一个简单的实验矩阵脚本自动生成多条训练命令并记录日志。这个习惯帮我把消融实验的周期从两周压缩到三天。我常用的做法是写一个run_experiments.sh内容类似#!/bin/bash for backbone in resnet50 resnet34 mobilenet_v2; do for lr in 0.00035 0.0005; do python train.py \ --config configs/VehicleID.yml \ --backbone $backbone \ --base_lr $lr \ --epochs 120 \ --tag backbone_${backbone}_lr_${lr} done done注意脚本里加了--tag参数如果源码包支持日志和checkpoint文件名都会自动带上这个标记比如VehicleID_backbone_resnet50_lr_0.00035.log。如果不支持你可以在load_config里自动加入时间戳但这就不方便对比了。我更推荐改源码包支持--tag改动很小收益很大。另外--base_lr这个参数如果你的源码包不叫这个名字需要先去load_config里确认否则命令行覆盖不上。跑完所有实验后再用一个汇总脚本读取日志里的rank1、mAP按backbone和lr维度整合成对比表。这一步不需要TensorBoardPython标准库就能做import re import glob results [] for log in glob.glob(logs/*.log): with open(log) as f: text f.read() # 按实际日志格式调整正则 rank1 float(re.search(rrank1[\s:]([\d.]), text).group(1)) mAP float(re.search(rmAP[\s:]([\d.]), text).group(1)) results.append((log, rank1, mAP)) for log, rank1, mAP in sorted(results, keylambda x: x[1], reverseTrue): print(f{log}: rank1{rank1:.2f} mAP{mAP:.2f})这个脚本最需要注意的是正则表达式一定要先tail一条日志看rank1和mAP到底是怎么打印的。我看到过很多日志打印的是best_rank1而不是当前epoch的rank1如果没看清楚就汇总数值会重复或错位。建议在日志格式里就统一写rank1: 0.8610 mAP: 0.7230脚本则用同一个正则避免自己给自己挖坑。当你熟练用这个流程后你可能会发现Backbone对rank1的影响其实比Loss权重更明显而image_size和batch_size往往交织在一起——小图配大batch反而比大图配小batch更稳。这些结论只有在你把Parser用起来、批量实验后才会得到而不是靠感觉拍脑袋。从那以后我每次拿到一个ReID源码包都强制自己先在配置层把变量列清楚再动手训练跑任何实验都通过命令行传参而不是改脚本这样对比实验的时间至少省一半。希望帮到你。本文还有配套的精品资源点击获取
返回列表