1. Market-1501数据集全景解析
第一次接触行人重识别任务时,我被各种数据集搞得头晕眼花,直到遇到Market-1501这个"标杆级"数据集。这个在清华大学校园采集的数据集,包含了1501个行人在6个摄像头下的32668张图像,就像给每个行人拍了套"多机位写真"。训练集751人共12936张图像,测试集750人19732张图像,这种不对称设计很有意思——测试集图像量反而更多,正是为了模拟真实场景中数据库规模大于查询集的特性。
数据集目录结构看似复杂,其实很有规律。bounding_box_train和bounding_box_test这两个文件夹是核心,分别存放训练和测试图像。query文件夹里的3368张图像都是手工标注的查询样本,而gallery里的图像则是用DPM检测器自动生成的。我刚开始总把query和gallery搞混,后来发现query相当于"问题照片",gallery就是"候选答案库"。
文件命名规则藏着重要信息。比如"0017_c2s1_000976_01.jpg"这个文件名:0017是行人ID,c2表示第2号摄像头,s1是第1段视频序列,000976是帧编号,01表示该帧上的第1个检测框。如果最后是00,就说明是手工标注的框。这种命名方式让我在后续处理时能轻松提取出各种元信息。
2. PyTorch数据加载器设计精髓
构建高效的数据加载器就像给模型打造一条自动化流水线。首先得继承torch.utils.data.Dataset这个基类,重点实现__getitem__和__len__这两个魔法方法。我习惯把数据预处理分成两块:transform_train和transform_test,训练时用随机裁剪、水平翻转等数据增强,测试时只需简单的resize和归一化。
from torchvision import transforms transform_train = transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.Resize((256, 128)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_test = transforms.Compose([ transforms.Resize((256, 128)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])自定义Dataset类时有个坑要注意:图像加载最好用PIL而不是OpenCV,因为PyTorch的transforms对PIL支持最完善。我封装了个Market1501类,初始化时遍历目录结构,构建图像路径列表和对应的行人ID列表。这里ID需要转换成整数标签,从0开始连续编号,方便后续计算损失函数。
3. 高效数据加载的进阶技巧
单纯实现Dataset还不够,DataLoader的参数配置直接影响训练效率。num_workers设置是个经验活——我通常设为CPU核数的2-4倍,但Windows下设为0避免多进程问题。batch_size根据GPU显存来定,16-32是个不错的起点。pin_memory=True能加速GPU数据传输,就像给数据装上了"直通车道"。
采样策略才是真正的魔法所在。默认的随机采样会导致每个batch的ID分布不均匀,我推荐使用RandomIdentitySampler。它能确保每个batch包含固定数量的ID,每个ID又有固定数量的样本,这对行人重识别这种细粒度分类任务特别重要。实测使用这种采样器能让模型收敛更快,识别准确率提升2-3个百分点。
from torch.utils.data.sampler import Sampler class RandomIdentitySampler(Sampler): def __init__(self, data_source, num_instances=4): self.data_source = data_source self.num_instances = num_instances self.index_dic = defaultdict(list) for index, (_, pid) in enumerate(data_source): self.index_dic[pid].append(index) self.pids = list(self.index_dic.keys()) self.length = len(self.pids) * self.num_instances def __iter__(self): indices = [] for pid in self.pids: indices.extend(np.random.choice(self.index_dic[pid], size=self.num_instances)) return iter(indices) def __len__(self): return self.length数据增强方面我有个私藏技巧:除了常规操作外,可以加入RandomErasing。这个增强会随机擦除图像部分区域,强迫模型不只关注局部特征。我还喜欢用ColorJitter轻微调整色调,模拟不同光照条件。这些技巧让模型在真实场景中表现更加鲁棒。
4. 评估协议与指标解读
Market-1501的评估协议很有讲究。测试时要把query图像与gallery库中的所有图像进行比对,按照相似度排序。这里有个关键点:每个query在gallery中可能有多个正样本(同一人在不同摄像头的图像),也可能有干扰项(同一摄像头下的不同图像)。
评估指标主要看mAP和CMC。mAP(平均精度均值)考虑排序中所有正样本的位置,CMC(累积匹配特性)只看前K个结果是否包含正样本。我刚开始只关注Rank-1准确率,后来发现mAP更能反映模型整体性能。好的模型应该在这两个指标上都表现优异。
实现评估代码时要注意去除"junk"图像——这些虽然包含查询行人,但因遮挡等原因质量太差,不计入评估。Market-1501的gt_query文件夹里有标注哪些是good和junk匹配。我建议先把评估流程写成单独脚本,确保结果可复现后再集成到训练流程中。
5. 实战中的避坑指南
第一个大坑是数据泄露。有次我不小心让测试集图像混入了训练过程,模型指标虚高但实际完全不能用。现在我严格分离训练和测试目录,甚至给它们设置不同颜色标签。第二个坑是ID偏移,原始数据集的ID是从1开始的,如果不转为从0开始,可能会引发各种维度错误。
预处理阶段要注意图像尺寸。Market-1501原始图像高宽比各异,我统一resize到256x128,这个尺寸在后续卷积网络中计算比较方便。还有个细节:有些图像文件名包含"-1",表示DPM检测错误的结果,这些样本要特别注意处理。
GPU显存不足时,可以尝试梯度累积技巧。即把一个大batch拆分成几个小batch计算梯度,最后统一更新。虽然训练时间会变长,但能有效缓解显存压力。我在1080Ti上就用这个方法成功跑起了batch_size=64的训练。