行人属性识别实战:数据集、模型训练与推理避坑指南
简介面向计算机视觉领域的Python开发者与行人属性识别研究者这套资源将数据集与预训练模型整合在一起可直接用于行人性别、年龄、着装、携带物等多属性识别任务避免从零训练模型的高门槛和大规模数据标注成本。压缩包共6个文件整体约375MB包含3个Python脚本、1个tar格式预训练模型、1份使用说明txt和1个mp4演示视频。脚本覆盖行人跟踪、属性预测与标签字典映射等核心环节使用说明与演示视频能帮助用户快速完成环境配置和结果验证。目前已有469人学习下载。借助预训练模型开发者无需标注新数据只需将待识别图像输入即可输出对应属性标签便于在智能监控、行人重识别、人机交互及自动驾驶等场景中进行快速验证、算法对比与二次开发同时也可作为学术研究的基线参考。1. 行人属性识别不是新鲜事但“拿来就能用”的少做监控视频分析或者智慧零售的人大概率都碰过这种需求客户问能不能把画面里的行人筛一下比如“只要穿红色上衣的女性”或者“统计一下背着双肩包的人数”。目标检测只能给你一个框框里是谁、长什么样、穿了什么这些信息得靠另一个任务来回答这就是行人属性识别。标题里说的“数据集合训练好的模型可直接使用”解决的正是“从零开始攒数据集、调参、训模型”这一整套麻烦。现实情况是公开可复现的行人属性识别方案远没有目标检测那么多很多团队想上手卡在数据格式不统一和权重文件难以获取这两个坎上。这篇文章就是顺着这条线把数据集怎么组织、模型怎么加载、训练和推理要注意什么讲清楚让新手能跟着把代码跑起来让熟手能少走几步弯路。2. 先把“属性识别”这件事拆清楚多标签分类不等于多分类2.1 为什么不能用softmax解决行人属性两张标签图的差别不少人第一次接触行人属性识别会拿它跟图像分类类比然后直接套一个ResNet加softmax。这个思路会翻车。分类任务是一张图对应一个类别而行人属性识别里一张图同时对应性别男/女、年龄段年轻/中年/老年、服饰短袖/外套/裙子、携带物背包/手提包/无等多组标签。每一组内部是互斥的但组与组之间是并存的。这意味着神经网络输出的不是一个概率分布而是多个二分类得分。最后解码的时候对每一个属性分别用阈值通常取0.5判断“有”或“没有”而不是用argmax挑一个最大得分。数据标注的形式也能看出来这种差异普通分类的标注是一个整数索引而属性识别的标注是一个向量比如[1, 0, 1, 0, 1, 0]每一位代表一个属性是否存在。理解了这一点你就明白为什么损失函数要用BCEWithLogitsLoss而不是CrossEntropyLoss。前者把每个输出节点当成独立的伯努利分布来算损失天然支持一张图同时有多个正标签。常见的开源实现里网络最后一层通常是一个nn.Linear(feature_dim, num_attributes)后面不接nn.Sigmoid因为损失函数内部已经包含了sigmoid操作。如果在输出层手动加了sigmoid再传给BCEWithLogitsLoss数值上会出问题导致训练初期loss不下降这是一个非常隐蔽的坑。2.2 数据集选型PETA、RAP、PA100K和标题方案的差异选数据集之前要确认两件事属性定义是否跟业务场景一致以及数据分布能不能覆盖你的应用场景。比如PETA数据集包含超过19000张行人图像涵盖但不限于性别、年龄段、服饰类型、携带物等几十种属性类别比较全但图片分辨率参差不齐很多是街拍或监控截图。RAP数据集则偏向于室内环境采集自某购物中心属性定义更细。PA100K数据集的优势在于属性数量适中且划分好了固定的训练集和验证集复现实验结果比较方便。标题里提到的“行人属性识别数据集”常见的组织方式是仿照PA100K的风格一个list_attr.txt文件第一行写图片总数和属性总数第二行是属性名列表后面的每一行由“图片路径 一串正负标签”组成。我一般会先看一眼标注文件的格式再决定怎么写数据加载代码。常见的坑是属性定义里的“无关项”也就是该属性在当前图上无法判断的情况比如看不清是不是背了包。有些数据集的标注里用-1表示“未标注”训练时要把这些样本的损失贡献mask掉。直接拿-1当0训练的话模型的输出会偏向于预测“无”因为负样本数量被人为增多了。提示PETA和RAP数据集经常被用于学术论文但如果你做的是工业落地建议先单独整理一份自己场景的验证集哪怕只有几百张图也能比较客观地评估模型效果。2.3 一个可复用的PyTorch数据集加载类解析属性列表下面这段代码是我常用的行人属性数据集加载类兼容PETA、RAP、PA100K这类list_attr.txt风格的文件。核心逻辑是解析标注文件然后把图片路径和标签一一对应起来。import os from PIL import Image from torch.utils.data import Dataset class AttrDataset(Dataset): def __init__(self, root_dir, list_file, transformNone): self.root_dir root_dir self.transform transform self.attr_names [] self.images [] self.labels [] with open(list_file, r) as f: lines f.readlines() # 第一行图片数 属性数但有些数据集第一行只有图片数 parts lines[0].strip().split() self.num_attrs int(parts[1]) if len(parts) 1 else None # 第二行是属性名用空格分隔 self.attr_names lines[1].strip().split() for line in lines[2:]: items line.strip().split() if len(items) self.num_attrs 1: img_path items[0] # 标签里的-1需要处理要么跳过要么转为0 labels [] for val in items[1:]: v int(val) if v -1: labels.append(0) else: labels.append(v) self.images.append(os.path.join(root_dir, img_path)) self.labels.append(labels) def __len__(self): return len(self.images) def __getitem__(self, idx): image Image.open(self.images[idx]).convert(RGB) if self.transform: image self.transform(image) label self.labels[idx] return image, { labels: torch.tensor(label, dtypetorch.float32), img_path: self.images[idx] }这段代码的逻辑说明一下构造函数的入参list_file指向属性标注文件读取时先处理头部两行元信息再逐行解析图片路径和标签。最关键的细节在于对-1的处理我这里是直接转成0因为BCEWithLogitsLoss要求target必须是0或1。如果你想精细化处理可以在loss计算时引入mask对标注为-1的位置跳过梯度回传。__getitem__返回图片张量和一个包含标签及路径的字典而不是只返回一个张量是因为后续打印bad case时需要知道图片来自哪里。参数方面transform建议统一做Resize((224, 224))、ToTensor()和Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])这套数值是ImageNet的均值和标准差。不用自己算数据集统计量除非图片的分布和ImageNet差异极大否则沿用ImageNet参数即可。训练阶段可以加RandomHorizontalFlip()做数据增强但要注意翻转会导致属性“左右”这类概念错乱比如一部分数据集的属性里包含“左肩背包”和“右肩背包”翻转会改变标签语义。这种情况要么不翻转要么在翻转时同步交换左右属性标签非常麻烦我一般干脆不翻转。3. 用预训练模型快速跑通推理从加载权重到输出属性列表3.1 模型结构选择为什么ResNet系比ViT更省心在行人属性识别这个方向常见的模型主干是ResNet50或ResNet101。选ResNet而不是ViT最现实的原因有两个一是参数量相对可控显存占用低推理速度快适合视频流场景二是预训练权重好找PyTorch官方仓库直接下载不需要像ViT那样自己折腾训练配置。如果你显存不够但想换更强的特征提取器可以考虑swin_transformer但要接受一个现实行人属性识别数据集的规模普遍不大几万张图用大数据集预训练出的ViT权重微调效果不一定比ResNet好反而容易过拟合。网络结构上常见做法是把ResNet的最后一层全连接拿掉换成自己的属性分类头。分类头一般是一个nn.Linear(2048, num_attrs)。这里一个值得注意的点是要不要在分类头前面加一个Dropout加一个p0.3的Dropout对缓解过拟合是有帮助的尤其在训练数据只有一两万张的时候。这不属于玄学而是多标签分类任务的通用经验。3.2 加载权重并推理完整脚本和输出解析如果你拿到的是某个开源方案里“训练好的模型”文件格式通常是.pth但保存的内容有两种可能一种是model.state_dict()另一种是完整的torch.save(model)。我建议你拿到一个.pth文件后先不要急着加载用Python看一眼它的dict结构import torch ckpt torch.load(model.pth, map_locationcpu) if isinstance(ckpt, dict) and state_dict in ckpt: state_dict ckpt[state_dict] else: state_dict ckpt # 打印前几个key确认是state_dict还是完整模型 for k in list(state_dict.keys())[:10]: print(k)如果打印出来的key是fc.weight、layer4.2.conv1.weight这种说明是state_dict形式。如果看到model、optimizer这样的key说明是整包保存的checkpoint需要再剥一层。识别清楚以后加载权重的完整推理脚本如下import torch import torch.nn as nn from torchvision import models, transforms from PIL import Image num_attrs 26 # 以PA100K为例 model models.resnet50(pretrainedFalse) model.fc nn.Sequential( nn.Dropout(p0.3), nn.Linear(model.fc.in_features, num_attrs) ) state_dict torch.load(model.pth, map_locationcpu) if state_dict in state_dict: state_dict state_dict[state_dict] # 兼容键名带module.前缀的情况 new_state_dict {} for k, v in state_dict.items(): if k.startswith(module.): k k[7:] new_state_dict[k] v model.load_state_dict(new_state_dict) model.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img_path test.jpg input_tensor transform(Image.open(img_path).convert(RGB)).unsqueeze(0) with torch.no_grad(): logits model(input_tensor) probs torch.sigmoid(logits).squeeze().numpy() attr_names [glass, hat, longhair, shorts, backpack, ...] # 从标注文件第二行复制 for name, prob in zip(attr_names, probs): print(f{name}: {prob:.3f} - {prob 0.5}) predicted_attrs [attr_names[i] for i, p in enumerate(probs) if p 0.5] print(识别结果:, predicted_attrs)这段代码里有一个细节值得专门说明pretrainedFalse因为我们要加载的是自己训练好的权重不是ImageNet预训练的。如果你写成pretrainedTrue再覆盖权重浪费下载不说严格来说模型结构也一致所以无所谓但习惯上还是写成False。加载时处理了module.前缀这是用DataParallel或者分布式训练保存后的常见现象不处理会报unexpected key in state_dict。推理时用with torch.no_grad()包起来省显存。解码的时候判断阈值直接取0.5这是一个不错的起点。不要用argmax去把每个属性组挑一个出来前面已经解释过属性之间不是互斥的。如果你发现输出的概率普遍偏高或者偏低可以看一下probs的分布动态调阈值。比如某个业务里“背包”的准确率要求高可以把阈值调到0.6让概率超过0.6才判定为有精确率会上去但召回率会掉。这是一个权衡。3.3 数据预处理参数与模型输入的强绑定关系这里必须单独提一个高频翻车点推理时的预处理必须和训练时保持一致包括尺寸、归一化均值和标准差、是否BGR等。很多人在Python里用PIL读图得到的是RGB通道顺序而训练时如果用的OpenCV读图BGR顺序那么推理时也要转成BGR或者统一在训练阶段就用PIL读图。通道顺序错误不会报错但会显著降低模型在真实场景里的识别率尤其是颜色属性如“红色上衣”深受其害。我自己的血泪经验是调试这种问题花了整整一天最后发现是cv2.cvtColor和Image.open混用导致的通道序错乱。你可以在代码里加一句断言或者把图片存下来看一眼比对着颜色通道猜有效率得多。另一个参数是输入尺寸。ResNet50默认输入224x224但有些开源方案用的是256x256比如直接在训练时把Resize((256, 256))和CenterCrop(224)组合起来。如果你在推理脚本里只写了Resize((224, 224))而训练时是Resize((256, 256)) CenterCrop((224, 224))输入分布和训练时不一致效果同样会打折。所以拿到一套权重时最好先看一眼训练代码里的transforms是什么照着抄一遍。不要只依赖README里的一句话描述。4. 从头训练自己的属性识别模型手动“造轮子”全流程4.1 一个可直接运行的训练循环从数据加载到模型保存如果你手里拿到了数据集并且有预训练好的模型可以先用着但还是想自己训一版适配业务场景的模型可以参照下面这个精简的训练脚本。它假设你已经用AttrDataset类把训练集和验证集定义好了。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import models from torch.optim import AdamW from tqdm import tqdm def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss 0.0 for images, targets in loader: images, labels images.cuda(), targets[labels].cuda() optimizer.zero_grad() logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) def validate(model, loader, criterion): model.eval() val_loss 0.0 with torch.no_grad(): for images, targets in loader: images, labels images.cuda(), targets[labels].cuda() logits model(images) loss criterion(logits, labels) val_loss loss.item() return val_loss / len(loader) model models.resnet50(pretrainedTrue) model.fc nn.Sequential( nn.Dropout(p0.3), nn.Linear(2048, num_attrs) ) model.cuda() criterion nn.BCEWithLogitsLoss() optimizer AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4, weight_decay5e-4) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4, pin_memoryTrue) best_loss float(inf) for epoch in range(30): train_loss train_one_epoch(model, train_loader, optimizer, criterion) val_loss validate(model, val_loader, criterion) print(fEpoch {epoch}: train_loss {train_loss:.4f}, val_loss {val_loss:.4f}) if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), best_model.pth)这段脚本的要点如下。第一pretrainedTrue表示加载ImageNet预训练权重然后在数据集上微调。第二优化器建议用AdamW而不是SGD因为属性识别任务的收敛速度对学习率不太敏感AdamW对新手更友好。第三学习率设1e-4是一个安全范围weight_decay设5e-4防止过拟合。最后保存模型时只保存state_dict()不要存整个模型对象方便后续做版本管理。4.2 学习率调度与训练轮数一个不会翻船的配置参考训练轮数epoch取决于数据量和任务难度。以PA100K为例大约一万张训练图片30个epoch能收敛得不错。如果你在PETA或RAP这种几万张的数据集上训练50个epoch也是正常的要看验证集loss有没有继续下降。常见做法是配合ReduceLROnPlateau调度器当验证loss连续两三个epoch不降时把学习率砍半。代码片段scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience2, verboseTrue ) # 每个epoch结束后 scheduler.step(val_loss)这个调度器非常实用。训练前期用1e-4大步快跑后期loss平台期自动降学习率不必手动调整。如果你复现的某个开源方案里用了CosineAnnealingLR也完全可以但ReduceLROnPlateau的优点是天然适配验证集loss的变化不容易把学习率降得过快。4.3 类别不均衡问题单个属性样本悬殊怎么办行人属性里有些属性天然非常稀疏。比如“佩戴帽子”在普通商业街的数据里可能只占5%不到“骑摩托车”更是少见。如果不做处理模型最后会输出全0也就是所有属性都判定为“不存在”。这是有实际后果的精确率看起来高但召回率一塌糊涂业务上“漏检”不可接受。处理方法有三种。第一种是在损失函数里给每个属性按样本数加权把正样本少的属性权重调高。PyTorch的BCEWithLogitsLoss自带pos_weight参数用法是criterion nn.BCEWithLogitsLoss(pos_weightpos_weight_tensor)。pos_weight的计算方式是负样本数除以正样本数每个属性一个值。第二种是在标签层面做重采样但这在多标签场景里比较复杂容易搞乱批次内部的标签关系一般不推荐。第三种是调低推理时的判断阈值比如从0.5降到0.3可以提升稀有属性的召回率但需要配合业务规则做取舍。我个人更倾向于用pos_weight。它在实现上最简单且收敛稳定不破坏数据分布。计算pos_weight的代码可以在训练前一次性算好注意要基于训练集的标签来统计不要混入验证集。如果某些属性完全没有正样本工业上这种属性干脆去掉留着一个永远学不出来的属性没有意义。5. 避坑行人属性识别最常见的一线“翻车”记录5.1 图像尺寸不一致导致模型推理全乱现象用训练好的模型跑单张图片很正常但跑一个视频流发现识别结果时好时坏甚至同一帧连续推理两次结果不同。原因去查一下你的预处理流程。如果是先对全图做检测截取行人框再把不同尺寸的行人框直接Resize((224, 224))送进模型而训练数据里所有行人图像本身已经是裁剪好的、构图居中推理时如果框得太大包含大量背景或太小截到了下半身输出概率分布会明显偏移。深度学习模型对输入上下文的分布极其敏感。解决核对一下训练样本的裁剪方式。如果训练时用的是检测模型的框标注直接裁剪推理时也要用同一个检测模型的输出裁剪不要手动改框。另外在推理脚本里固定Resize((224, 224))的同时可以打印一张预处理后的图看一眼确认行人在画面中的比例和训练集样本差不多。5.2 属性定义顺序错位导致“张冠李戴”现象模型输出的属性名和实际内容对不上比如把“女性”识别成“背包”或者全部输出跟训练时对调的属性。原因训练用的属性名列表和推理脚本里写的attr_names列表顺序不一致。有些数据集文件里第二行的属性名是用某种固定顺序排列的但有人会在训练前对属性做一次“去除无效属性”的筛选导致索引号重排。如果你在推理脚本里直接写了attr_names [glass, hat, ...]而训练时用的是另一个顺序结果当然错位。解决不要手打属性名单从list_attr.txt文件里读取。把训练和推理脚本里对属性名的读取逻辑封装成同一个函数从第二次开始不依赖手写列表。如果已经翻车了用验证集跑一遍比对输出和标注能立刻看出错位规律。5.3 模型输出的概率分布整体偏移现象验证集的mA指标不低但业务场景里模型把大量负样本判成正样本或者反过来需要靠调阈值来救。原因这通常是数据分布差异导致的。训练集是街拍行人业务场景是低头弯腰的推车顾客或者是俯视摄像头角度行人的姿态和遮挡比例完全不一样。模型学到的属性特征跟场景绑定过紧。解决如果调阈值能接受快速上线先用着。但更推荐的做法是把业务场景里的图片积攒下一批标注几百张在现有模型基础上再做一次小规模的微调fine-tune。微调学习率设1e-5只跑5到10个epoch就能把模型的场景分布拉回来。不要在冷启动时指望一个通用数据集覆盖所有落地场景这是一些供应商提供的模型在真实场景里效果打折的核心原因。5.4 模型训练早期loss下降随即卡住不动现象训练第一个epoch loss正常下降但后面几个epoch基本不变val_loss也不降。原因学习率设置过高导致loss在鞍点附近震荡或者分类头和特征提取器之间的学习率不匹配。如果整个模型用同一个学习率从1e-4起步问题不算大但有人在微调时只把分类头的学习率调大特征提取器的lr设为0这时候特征提取器还是ImageNet的分布分类头很难真正拟合。解决检查是否对主干调用了requires_grad_(False)。常见的迁移学习套路是冻结主干只训练分类头但前提是训练数据量极少且和ImageNet分布接近。行人属性识别属于细粒度特征主干特征也需要微调不建议冻结。如果想要更快收敛可以对分类头单独设大一点的学习率比如1e-3主干仍用1e-4。用param_groups可以实现optimizer AdamW([ {params: model.layer4.parameters(), lr: 1e-4}, {params: model.fc.parameters(), lr: 1e-3}, ], weight_decay5e-4)5.5 验证集指标高但线上可用性差现象mAmean Accuracy达到90%以上业务方试用后依然反馈识别不准要求返工。原因mA这个指标是多标签任务最常用的全局评估指标但它对每个属性一视同仁。如果样本里“女性”占绝大多数“男性”预测错了几十次在mA里可能只会让这个属性的准确率掉两三个点全局分数依然好看。但业务场景往往只关注少数几个属性比如“戴帽子”“背包”这些属性训练样本少本身就更容易错。解决不要只看mA单独统计每个属性的Precision和Recall输出一张属性级别的明细表。如果某些关键属性的精确率低于80%这个模型在业务上就还需要打磨。另外可以在验证集里专门构造一个“困难样本子集”比如只有行人侧身、背面、遮挡严重的图片看模型在这些样本上的表现比全局指标更有参考价值。6. 用模型的中间层特征做属性间的关联分析找“软标签”一个我认为值得进阶尝试的技巧是利用fc层之前的特征向量做属性间的关联分析。很多属性并不是独立的比如“穿裙子 长发”和“女性”之间存在强相关“光头 男性”也是如此。模型在训练时其实学到了这些隐式关联只是最后输出的概率是独立解码的。你可以把最后一层pool5输出的2048维特征向量取出来投影到二维空间可视化。操作方法是在推理脚本里临时注册一个hook或者干脆把模型截断到最后一层卷积输出然后再接自己的分类头。这个特征向量如果做聚类你会发现模型自动把“女性行人”和“男性行人”分成了两堆且每一堆内部的服饰属性也具有相似性。更实际的应用是“软标签”或者说“伪标签修正”。当你拿到一批没有标注的行人图片要快速扩充训练集时可以让现成模型预测出一组概率把概率大于0.9的作为正标签、小于0.1的作为负标签概率在中间段的置为“不确定”不参与训练。这种半监督方法在属性识别上是能明显提升精度的。我做过一个粗算在PETA数据子集上用这种方法额外加了约两万张无标注图片几个稀有属性的召回率提升了大约5个百分点。当然伪标签也不是无限扩张的随着加入的数据变多提升会放缓而且如果模型的初始错误率太高伪标签会把错误放大所以只能选择概率极端接近0或1的样本。还有一个我自己的习惯推理代码里永远保留一个“打印bad case”的逻辑。每次模型更新权重后把验证集里预测错得离谱的图片和概率单独存到一个目录里定期翻一翻。这个习惯帮我发现了很多数据标注错误也看到了数据分布迁移的早期信号。模型做出来不是放在服务器里就完了它是要持续迭代的。希望这篇内容能帮你在行人属性识别这个方向上少踩一些我踩过的坑愿你的模型一次跑通。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →