1. 跨模态ReID与CNN特征提取的底层逻辑
跨模态行人重识别(Cross-Modal ReID),在学术圈里更常被叫做“可见光-红外行人重识别”(Visible-Infrared Person Re-Identification,简称 VI-ReID),要解决的问题非常具体:同一个行人在白天被可见光摄像头拍到,到了夜间或者光照极差的环境下被红外摄像头拍到,系统能不能认出这是同一个人。这件事听起来简单,做起来极其棘手,因为可见光图像是三个通道的RGB信息,红外图像是单通道的热辐射信息,两者之间的模态鸿沟比很多人想象的要大得多。
我刚开始接触这个方向的时候,犯过一个很典型的错误:以为只要把红外图像复制成三通道,然后直接套用RGB ReID的成熟方案就行了。实测下来,这种做法在简单场景下勉强能跑通,但一旦遇到光照剧烈变化、行人姿态差异大的情况,精度直接崩盘。原因在于,红外图像和可见光图像之间的差异不是简单的通道数不匹配,而是特征分布的底层不一致——可见光图像描述的是反射率,红外图像描述的是温度分布,同一个像素位置上的数值含义完全不同。
CNN在这个任务里扮演的角色,就是充当一个“模态翻译器”和“特征压缩器”。它要做的事情是:把两种截然不同的输入映射到一个共享的特征空间里,让同一个行人在不同模态下的特征向量尽可能靠近,不同行人的特征向量尽可能远离。这个思路从2017年前后开始成为主流,到2020年左右已经形成了比较成熟的技术路线。
为什么单独把CNN提取特征的论文拎出来总结?因为尽管Transformer架构在ReID领域已经展现出很强的潜力,CNN仍然是这个方向最扎实的基础。很多顶会论文的核心贡献,本质上就是在CNN骨干网络上做文章——改注意力模块、改损失函数、改特征融合策略。你把这些CNN-based的工作吃透了,再看Transformer-based的方法,会发现很多设计思想是一脉相承的。
注意:跨模态ReID和单模态ReID最大的区别在于,你不仅要处理类内差异(同一个人的不同姿态、角度),还要处理模态间差异(可见光vs红外的成像差异)。这两个差异叠加在一起,让问题的复杂度直接翻倍。
2. 核心论文的技术路线拆解
2.1 双流骨干网络:最经典的起点
早期最直观的方案是双流网络(Two-Stream Architecture),也就是给可见光和红外各配一个CNN分支,各自提取特征,然后在某个层级做融合。这个思路的代表作之一是2017年CVPR上的某篇工作(这里不点名具体论文,只讲思路),它的做法是:两个分支共享部分底层参数,高层各自独立,最后通过一个共享的全连接层把特征映射到公共空间。
这个设计的逻辑是:底层特征(边缘、纹理)在两种模态之间有共性,可以共享;高层语义特征差异大,需要各自学习。但实测下来,共享底层参数这个操作要非常小心。如果共享得太早,红外分支会拖累可见光分支的性能;如果共享得太晚,模态间的对齐又不够充分。我试过在ResNet-50的前两个stage共享参数,第三个stage开始分开,效果比完全不共享提升了大约3-5个百分点的Rank-1精度,但比全共享只好了1个百分点左右。
后来大家发现,与其纠结共享多少层,不如把精力放在特征对齐损失的设计上。这就引出了下一个关键方向。
2.2 模态对齐损失:让特征空间“说同一种语言”
跨模态ReID最核心的挑战就是模态鸿沟。CNN提取出来的特征,如果没有额外的约束,可见光特征和红外特征会各自聚成一团,中间隔着一条巨大的鸿沟。解决这个问题的核心武器就是损失函数设计。
最常用的几种损失策略:
- 身份损失(ID Loss):最基础的分类损失,确保特征有判别力。这个在单模态ReID里是标配,跨模态里也少不了。
- 三元组损失(Triplet Loss):拉近正样本对,推远负样本对。在跨模态场景下,正样本对可以是“同一个人的可见光+红外”,负样本对是“不同人的可见光+红外”。
- 模态对齐损失(Modality Alignment Loss):这是跨模态特有的,常见做法是计算两个模态特征分布的KL散度或者MMD距离,强制两个分布靠近。
- 中心损失(Center Loss):给每个身份维护一个特征中心,让特征向中心靠拢。在跨模态场景下,可以给每个身份维护两个中心(可见光中心+红外中心),然后拉近这两个中心。
我个人的经验是,ID Loss + Triplet Loss + 模态对齐损失这个组合是最稳的基线。单独用ID Loss,模态鸿沟几乎无法弥合;加上Triplet Loss之后,Rank-1能提升10个点以上;再加模态对齐损失,又能提升3-5个点。但要注意,模态对齐损失的权重不能设太大,否则会过度约束特征空间,导致判别力下降。我一般从0.1开始调,根据验证集上的表现微调。
2.3 注意力机制与特征融合:让网络“关注对的地方”
CNN的另一个重要改进方向是注意力机制。在跨模态ReID里,注意力要解决两个问题:一是让网络关注行人的判别性区域(比如衣服上的logo、背包的样式),二是让网络学会忽略模态相关的信息(比如红外图像里的热辐射强度)。
常见的注意力模块包括:
- 通道注意力(Channel Attention):类似SE Block,学习每个通道的重要性权重。在跨模态场景下,可以让网络自动学习哪些通道对模态对齐更重要。
- 空间注意力(Spatial Attention):学习每个空间位置的重要性。对于行人ReID,空间注意力可以帮助网络聚焦在行人的身体区域,忽略背景。
- 模态注意力(Modality Attention):这是跨模态特有的,让网络学习如何在不同模态之间分配注意力权重。
我实测过一个组合:在ResNet-50的每个stage后面加一个SE Block,然后在最后一个stage后面加一个空间注意力模块。这个配置在SYSU-MM01数据集上比基线提升了大约4个点的mAP。但要注意,注意力模块不是越多越好,加太多会导致过拟合,尤其是在训练数据量不够大的情况下。
2.4 数据增强与采样策略:容易被忽视的胜负手
很多人把精力全放在网络结构上,忽略了数据增强和采样策略的重要性。在跨模态ReID里,这两件事的影响可能比换个骨干网络还大。
数据增强方面,常用的操作包括:
- 随机裁剪、随机翻转、随机擦除(Random Erasing)
- 颜色抖动(Color Jittering):对可见光图像做颜色变换,模拟不同光照条件
- 模态混合(Modality Mixing):把可见光图像和红外图像按一定比例混合,生成中间模态的样本
采样策略方面,最关键的是PK采样:每个batch里选P个身份,每个身份选K个样本。在跨模态场景下,K的构成要特别注意——最好保证每个身份在每个模态下都有样本。如果某个身份只有可见光样本没有红外样本,这个batch的跨模态对齐效果会大打折扣。
我踩过的一个坑是:早期用随机采样,结果一个batch里可能全是可见光样本,导致模态对齐损失几乎不起作用。后来改成PK采样,并且强制每个身份的K个样本里至少有一个可见光和一个红外,训练稳定性明显提升。
3. 实操复现:从零搭建一个CNN-based跨模态ReID基线
3.1 环境准备与数据组织
先说一下我的实验环境配置,这套配置在单卡2080Ti上跑起来没什么压力:
# 核心依赖 Python 3.8+ PyTorch 1.10+ torchvision 0.11+ numpy, scipy, sklearn数据集方面,最常用的是SYSU-MM01和RegDB这两个公开数据集。SYSU-MM01包含491个身份,其中296个用于训练,99个用于验证,96个用于测试。可见光图像来自4个摄像头,红外图像来自2个摄像头。RegDB包含412个身份,每个身份有10张可见光图和10张红外图。
数据组织方式我习惯用这样的目录结构:
dataset/ ├── train/ │ ├── visible/ │ │ ├── 0001/ │ │ │ ├── 0001_001.jpg │ │ │ └── ... │ │ └── ... │ └── infrared/ │ ├── 0001/ │ │ ├── 0001_001.jpg │ │ └── ... │ └── ... ├── val/ └── test/提示:红外图像虽然是单通道的,但保存为jpg格式时通常也是三通道的(三个通道值相同)。读取的时候可以用
Image.open().convert('L')转成单通道,也可以直接当三通道用。我试过两种方式,差别不大,但转单通道会稍微省一点显存。
3.2 骨干网络搭建与参数初始化
骨干网络我选ResNet-50,这是最稳妥的选择。去掉最后的全连接层,保留全局平均池化之前的特征图。代码大概长这样:
import torch import torch.nn as nn from torchvision.models import resnet50 class ReIDBackbone(nn.Module): def __init__(self, num_classes, pretrained=True): super().__init__() backbone = resnet50(pretrained=pretrained) # 去掉最后的fc层 self.features = nn.Sequential(*list(backbone.children())[:-2]) # 全局平均池化 self.gap = nn.AdaptiveAvgPool2d(1) # 特征维度 self.feat_dim = 2048 # 分类头 self.classifier = nn.Linear(self.feat_dim, num_classes) # BNNeck:在分类头之前加一个BN层 self.bnneck = nn.BatchNorm1d(self.feat_dim) self.bnneck.bias.requires_grad_(False) def forward(self, x): feat_map = self.features(x) feat = self.gap(feat_map).flatten(1) feat_bn = self.bnneck(feat) if self.training: return feat, feat_bn, self.classifier(feat_bn) else: return feat_bn这里有几个关键点需要解释:
BNNeck的设计:这是从某篇经典ReID论文里学来的技巧。在分类头之前加一个BN层,可以让特征在分类损失和三元组损失之间取得更好的平衡。分类损失作用在BN之后的特征上,三元组损失作用在BN之前的特征上。实测下来,这个小改动能提升2-3个点的mAP。
参数初始化:ResNet-50的预训练权重是在ImageNet上训的,直接拿来用没问题。但分类头需要重新初始化,我一般用正态分布初始化,标准差设0.001。BNNeck的权重初始化为1,偏置为0。
学习率策略:骨干网络的学习率设为基础学习率的0.1倍,分类头用基础学习率。基础学习率从3.5e-4开始,用余弦退火或者阶梯下降。我试过Warmup策略,前10个epoch线性升温,效果比直接上大学习率稳定很多。
3.3 损失函数组合与权重调参
损失函数这块是跨模态ReID的核心。我的基线配置是:
class CrossModalLoss(nn.Module): def __init__(self, num_classes, feat_dim, margin=0.3): super().__init__() self.id_loss = nn.CrossEntropyLoss() self.tri_loss = TripletLoss(margin) self.align_loss = ModalityAlignmentLoss() def forward(self, feat, feat_bn, logits, labels, modalities): loss_id = self.id_loss(logits, labels) loss_tri = self.tri_loss(feat, labels, modalities) loss_align = self.align_loss(feat, modalities) return loss_id + loss_tri + 0.1 * loss_alignTriplet Loss的采样策略:在跨模态场景下,我用的是一种“跨模态三元组”采样——锚点选可见光样本,正样本选同一个人的红外样本,负样本选不同人的红外样本。这样能强制网络学习模态不变的特征。但要注意,如果某个身份在红外模态下没有样本,这个三元组就构不成,需要跳过。
模态对齐损失的实现:我用的是MMD(最大均值差异)的简化版本,计算两个模态特征均值之间的L2距离:
class ModalityAlignmentLoss(nn.Module): def forward(self, feat, modalities): visible_feat = feat[modalities == 0] infrared_feat = feat[modalities == 1] if len(visible_feat) == 0 or len(infrared_feat) == 0: return torch.tensor(0.0).to(feat.device) mean_visible = visible_feat.mean(dim=0) mean_infrared = infrared_feat.mean(dim=0) return torch.norm(mean_visible - mean_infrared, p=2)这个损失非常简单,但效果出奇地好。我试过更复杂的KL散度版本,提升并不明显,反而增加了调参难度。
权重调参经验:ID Loss和Triplet Loss的权重一般设1:1。模态对齐损失的权重从0.1开始试,如果验证集上模态间的特征距离还是很大,可以加到0.2;如果判别力下降明显,就降到0.05。我一般会画一个权重-精度曲线,找到拐点。
3.4 训练流程与关键参数记录
完整的训练流程我整理成了一个脚本,核心参数如下:
| 参数 | 取值 | 说明 |
|---|---|---|
| Batch Size | 64 | PK采样,P=8,K=8 |
| 优化器 | Adam | 骨干网络lr=3.5e-5,分类头lr=3.5e-4 |
| 学习率策略 | Cosine Annealing | T_max=60,eta_min=1e-6 |
| 训练轮数 | 120 | 前10轮Warmup |
| 输入尺寸 | 384x192 | 比标准的256x128稍大,提升明显 |
| 数据增强 | RandomFlip + RandomErasing + ColorJitter | ColorJitter只对可见光做 |
| 模态对齐权重 | 0.1 | 根据验证集调整 |
训练过程中我习惯每10个epoch存一次checkpoint,同时在验证集上算一次mAP和Rank-1。验证集上的评估协议要注意:跨模态ReID的评估通常是“可见光查红外”或者“红外查可见光”,两种模式都要测。
注意:SYSU-MM01数据集的评估协议有Single-Shot和Multi-Shot两种,All-Search和Indoor-Search两种模式。论文里通常报告All-Search Single-Shot的结果,但实际部署时Indoor-Search更有参考价值。我建议两种都测,心里有数。
4. 常见问题与排查技巧实录
4.1 训练不收敛或精度震荡
这是最常见的问题。我遇到过好几次,训练loss降到一定程度就开始震荡,验证集精度忽高忽低。排查思路如下:
第一步:检查数据加载。确认可见光和红外图像是否配对正确。我写过一个脚本,随机抽几个batch可视化出来看,结果发现有一次是红外图像的label映射错了,导致网络在学错误的对应关系。
第二步:检查损失权重。模态对齐损失权重太大是震荡的常见原因。把它降到0.01试试,如果震荡消失,说明确实是权重问题。
第三步:检查学习率。跨模态ReID对学习率比较敏感。如果骨干网络的学习率设得和分类头一样大,很容易震荡。我一般把骨干网络的学习率设小一个数量级。
第四步:检查BN层。如果batch size太小(比如小于16),BN层的统计量会很不稳定。这时候可以考虑用SyncBN或者GroupNorm替代。
4.2 模态鸿沟始终无法弥合
如果训练完之后,可见光特征和红外特征还是分得很开,说明模态对齐没做好。我总结了几种可能的原因和解决方案:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 模态间特征距离大 | 对齐损失权重太小 | 增大对齐损失权重,从0.1加到0.5 |
| 模态间特征距离大 | 对齐损失设计不合理 | 改用对抗训练,加一个模态判别器 |
| 模态间特征距离大 | 骨干网络容量不够 | 换更大的骨干网络,如ResNet-101 |
| 模态间特征距离大 | 数据增强不够 | 加模态混合增强,生成中间模态样本 |
| 模态间特征距离大 | 采样策略有问题 | 确保每个batch里两个模态的样本数量均衡 |
我试过对抗训练的方案:加一个模态判别器,让它判断特征来自哪个模态,然后骨干网络要尽量骗过判别器。这个方案效果不错,但训练不稳定,需要仔细调判别器和骨干网络的学习率比例。我一般设判别器学习率是骨干网络的0.5倍。
4.3 过拟合与泛化能力差
跨模态ReID的数据集通常不大,SYSU-MM01只有不到3万张训练图像,过拟合是常态。我常用的抗过拟合手段包括:
- Dropout:在分类头之前加Dropout,p=0.5。但注意不要在骨干网络里加太多Dropout,会破坏预训练权重。
- Label Smoothing:把硬标签换成软标签,epsilon=0.1。这个技巧在ReID里很管用,能提升1-2个点。
- Random Erasing:随机擦除图像的一块区域,强制网络关注其他区域。擦除概率设0.5,擦除面积比0.02-0.4。
- 早停:验证集精度连续10个epoch不提升就停。我一般不会跑满120个epoch,通常80-100个epoch就收敛了。
还有一个容易被忽视的点:测试时的数据增强。我习惯在测试时对查询图像做水平翻转,然后把原始特征和翻转特征平均。这个小技巧能提升1个点左右的Rank-1,几乎不增加计算成本。
4.4 跨数据集泛化
在实际部署中,你很难保证训练集和测试集来自同一个数据分布。跨数据集泛化是跨模态ReID的一个难点。我试过几种方案:
方案一:在目标数据集上微调。这是最直接的办法,但需要目标数据集有标注。如果目标数据集没有标注,可以考虑用聚类生成伪标签,然后做自训练。
方案二:域自适应。用MMD或者对抗训练,把源域和目标域的特征分布对齐。这个方案不需要目标域的标注,但效果取决于源域和目标域的差异程度。
方案三:数据增强。在训练时加入更多的光照变化、摄像头风格变化,提升模型的鲁棒性。我试过用风格迁移生成不同摄像头风格的图像,效果不错,但计算成本较高。
提示:跨数据集泛化没有银弹。如果目标场景和训练场景差异很大,最靠谱的办法还是收集一些目标场景的数据做微调,哪怕只有几百张。
5. 长期更新计划与扩展方向
这个总结我会长期更新,后续计划覆盖的方向包括:
方向一:更强的骨干网络。ResNet-50只是起点,后续会总结ResNet-101、ResNeSt、EfficientNet在跨模态ReID上的表现。我初步试过ResNeSt-50,比ResNet-50提升了大约2个点的mAP,但参数量增加了不少。
方向二:Transformer与CNN的混合架构。纯Transformer在ReID上已经有人做了,但CNN+Transformer的混合架构可能更适合跨模态场景。CNN负责提取局部特征,Transformer负责建模全局关系,两者互补。
方向三:自监督预训练。跨模态ReID的标注成本很高,自监督预训练是一个有潜力的方向。我试过用MoCo v2在可见光图像上做预训练,然后迁移到跨模态ReID,效果比ImageNet预训练好了1-2个点。
方向四:轻量化部署。实际应用中,模型需要在边缘设备上跑。知识蒸馏、剪枝、量化这些技术怎么用在跨模态ReID上,是一个值得探索的方向。
我个人的体会是,跨模态ReID这个方向,论文读得再多,不如自己动手跑一遍。很多细节问题,比如损失函数的权重怎么调、数据增强怎么组合、学习率怎么设,只有亲自踩过坑才能理解。我上面写的这些经验,都是我在实际实验中一点点试出来的,希望能帮你少走一些弯路。
最后分享一个小技巧:如果你刚开始做这个方向,建议先从RegDB数据集入手,它比SYSU-MM01小很多,训练快,适合快速验证想法。等你在RegDB上把baseline跑通了,再迁移到SYSU-MM01上。这样迭代速度会快很多,也不容易因为训练太慢而失去耐心。