1. 从图像检索的痛点说起:为什么需要NetVLAD
先聊聊我自己的经历。前几年做城市级视觉重定位项目时,经常要面对几万张街景图的检索任务。当时试过直接用CNN提取全局特征(比如AlexNet的fc7输出)跑暴力匹配,也试过用传统局部特征SIFT加BOW(词袋模型)搞检索,效果都不理想。CNN特征虽然语义强,但缺乏空间几何信息的显式建模;传统BOW呢,量化误差大,召回率上不去。
后来接触到NetVLAD,一个很直观的感受是:它把传统VLAD的“硬量化”变成了一种可学习的“软分配”,并且嵌进了CNN网络里端到端训练。这个思路一下就解决了两个问题——一是描述子的判别力不再依赖手工设计的编码规则,二是检索的精度上限可以得到明显提升。在Pittsburgh数据集上,当时跑出来的mAP比直接用CNN特征高了十来个点,这个差距在检索任务里已经是质变了。
很多人第一次听NetVLAD,会以为它只是一个“把VLAD向量接在CNN后面”的插件。实际上,它改变了特征聚合的整个逻辑基础:不再是先提局部特征再做聚类编码,而是让网络自己去学什么局部特征值得被聚合、哪些聚类中心是有区分度的。
这篇内容我会从原理开始,逐步拆解数学推导,再给出一份可以直接复现的PyTorch实现,最后聊聊我在实际项目中踩过的坑。无论你是做图像检索、视觉重定位、还是点云全局描述子,这套思路都有很高的参考价值。
1.1 传统VLAD是怎么工作的
要理解NetVLAD,必须先理解VLAD(Vector of Locally Aggregated Descriptors)。它和BOW一样,是特征编码领域的老牌方法。BOW的思路是:把局部特征(比如SIFT)量化为视觉单词,然后统计每个单词出现的次数,形成一个直方图。VLAD则更进一步,不统计次数,而是累加局部特征与对应聚类中心的残差。
具体来说,假设有K个聚类中心,每个局部描述子维度是D维。对于每个聚类中心,把所有属于它的局部特征求和后减去聚类中心,得到一个D维残差向量。把K个残差向量拼起来,就得到了KD维的VLAD向量。
公式上看,VLAD的第j个聚类中心对应的输出块可以写成:
[ V(j,k) = \sum_{i=1}^{N} a_j(x_i) \cdot (x_i(k) - c_j(k)) ]
其中(x_i)是第i个局部描述子,(c_j)是第j个聚类中心,(a_j(x_i))是一个指示函数:当(x_i)属于第j类时为1,否则为0。传统做法里,这个归属关系通过最近邻来确定。
这样做的好处是,它保留了一阶统计信息(残差),比BOW的零阶统计(计数)表达能力强不少。但问题也出在指示函数上——它是离散的、不可导的,所以整个编码过程没法端到端训练。
1.2 传统VLAD到底哪里不够好
我在实际项目中体会最深的一点是:传统VLAD的聚类中心是预先用K-Means在大量局部特征上训练得到的,而这个聚类过程完全独立于最终任务(比如图像检索)的优化目标。
也就是说,聚类中心的好坏只反映“特征空间的分布结构”,不代表“对检索任务最有判别力的划分方式”。同时量化的硬边界会导致接近聚类边界的特征被强制归到某一类,引入较大的量化噪声。反映到检索结果上,就是召回精度不稳定,换个数据集效果波动很大。
另一个痛点在于VLAD向量通常需要和PCA、白化之类的降维手段配合使用。因为KD维度太高(比如D=128,K=64时维度是8192),直接做内积相似度计算成本很高,而且高维稀疏向量里有很多冗余信息。这些后处理手段同样是任务无关的,无法保证降维后保留的维度一定是有判别力的。
1.3 NetVLAD的整体思路
NetVLAD的核心贡献,是把VLAD中的硬分配(a_j(x_i))替换成可微的软分配。网络不再通过最近邻来决定归属,而是通过一个小型神经网络学习每个局部特征对每个聚类中心的“软响应值”。
这个软分配本质上就是Softmax输出。每个局部特征会同时作用于所有的聚类中心,只是权重不同。这样输出向量对输入特征是可微的,梯度就能从损失函数一路回传到卷积层。
这样一来,聚类中心不再依赖离线K-Means,而变成了网络中的可训练参数。整个系统可以端到端训练,聚类中心和CNN骨干都能根据检索任务的需求被联合优化。
结构上,NetVLAD层包含三个分支,分别负责:
- 计算分配权重:将局部特征通过一个1x1卷积映射到K维,再过Softmax得到软分配矩阵。
- 计算残差:对每个聚类中心做残差计算。
- 累加残差并归一化,输出最终的全局描述子。
这三个分支各有各的职责,下面逐个来拆解。
2. NetVLAD网络结构核心拆解
NetVLAD的提出论文是《NetVLAD: CNN Architecture for Weakly Supervised Place Recognition》,2016年发表在CVPR上。它用了一个类似AlexNet或VGG的CNN作为前置特征提取器,后面接上NetVLAD层作为全局特征编码器。
先把整体结构串起来:
输入图像 -> CNN卷积层(多个卷积+池化)-> 得到W×H×D的特征图 -> 把特征图视为N=W×H个局部描述子(每个维度D)-> 送入NetVLAD层 -> 输出K×D维全局描述子 -> 归一化 -> 用于检索/识别。
这个流程里最关键的设计,是把整张图片的特征图当成局部描述子集合来处理。每个空间位置的特征向量相当于一个“局部特征”,而NetVLAD要做的事情就是把这些局部特征聚合成一个全局描述子。
2.1 特征提取主干网络:从AlexNet到VGG再到更现代的骨架
NetVLAD最初使用AlexNet结构,但后来的实践表明,VGG-16作为主干效果更稳定,尤其是去掉全连接层、只保留卷积部分后,特征图的空间分辨率更高,局部位置信息保留更完整。
在选择主干网络时,有几点经验可以分享:
- 输入图像尺寸不能太小。NetVLAD需要一定的空间尺寸来保证有足够多的局部描述子参与聚合。如果输入224×224,输出特征图可能是14×14,也就是196个局部特征,这个数量够用;如果输入尺寸降到96×96,局部特征数量大幅下降,描述子质量会明显变差。
- 主干网络最后几层的感受野要适中。感受野太小,特征偏局部、缺乏语义;感受野太大,局部特征的独立性又会被弱化。VGG-16的conv5层感受野大约是196像素,对于224输入来说覆盖了多半张图,算是比较合适的平衡点。
- 如果条件允许,可以用ResNet-50或ResNet-101替换VGG。实测下来,ResNet系列收敛更快,检索精度也略高,但要注意BatchNorm在弱监督训练时对batch size比较敏感,batch太小会不稳定。
2.2 NetVLAD层的三个关键分支
NetVLAD层的输入是形状为N×D的矩阵(N个局部描述子,每个D维),输出是K×D维向量。在具体实现中,它分成三个分支来协同工作。
第一个分支计算软分配权重。把N×D的输入通过一个1x1卷积,输出N×K,然后在N这个维度上做Softmax。为什么要用1x1卷积?因为它本质上就是对每个局部特征做一次线性变换,把D维映射到K维,等价于对每个局部特征学习一个“属于哪个聚类中心”的得分。Softmax之后,每一行的K个值加起来等于1,表示这个局部特征在各个聚类中心上的软分配比例。
第二个分支计算加权残差。对每个聚类中心,计算局部特征与该聚类中心的差,然后乘上对应的软分配权重,再做累加。这一步其实和传统VLAD中的残差累加类似,只不过权重从硬性的0/1变成了软性的0到1之间的连续值。
第三个分支做归一化。常见的做法是先用带符号的平方根归一化(signed square rooting),再做L2归一化。这个操作能有效抑制大数值维度的影响,让描述子更均匀。注意这里有个细节:归一化操作只在前向传播时生效,反向传播时根号系数的梯度通过链式法则自动计算,不需要手工处理。
2.3 聚类中心是可学习参数
在传统VLAD中,聚类中心是通过K-Means离线训练得到的,一旦训练完成就固定不动了。NetVLAD把聚类中心作为网络参数,参与端到端的梯度反传。
这意味着聚类中心的初始值仍然可以用K-Means的结果初始化,但在训练过程中它们会随着任务目标不断调整。我在实验中发现,用K-Means初始化比随机初始化好很多,尤其是K值比较大(比如64以上)时,随机初始化的收敛速度会很慢,还容易陷入局部最优。
更关键的是,因为是端到端训练,聚类中心不再仅仅描述局部特征的分布,而是学会用“对最终任务最有利”的方式来划分特征空间。该靠近的特征会靠得更近,该拉开的会被强制拉远,整个特征空间的判别力就能得到有效增强。
3. NetVLAD数学原理与公式推导
很多博客在讲NetVLAD时,只是把软分配公式贴出来,然后说“用Softmax近似硬分配就好了”,这其实是不够的。我尽量把推导过程拆细一些,大家可以沿着这条线自己推一遍,理解会透彻很多。
3.1 传统VLAD的数学表达
传统VLAD对第j个聚类中心和第k个特征维度的输出可以写为:
[ V(j,k) = \sum_{i=1}^{N} a_j(x_i) (x_i(k) - c_j(k)) ]
其中:
[ a_j(x_i) = \begin{cases} 1, & \text{if } j = \arg\min_j |x_i - c_j|_2 \ 0, & \text{otherwise} \end{cases} ]
这个(a_j(x_i))就是硬分配。问题很明显:它不可导,无法放进神经网络做反向传播。
3.2 硬分配到软分配的转换
为了让整个式子可导,NetVLAD把(a_j(x_i))替换成了连续函数:
[ \bar{a}j(x_i) = \frac{\exp(w_j^T x_i + b_j)}{\sum{j'} \exp(w_{j'}^T x_i + b_{j'})} ]
这就是一个基于权重的Softmax。可以看到,它不再需要聚类中心来计算归属了,而是通过学习一组权重(w_j)和偏置(b_j)来决定归属。
这里有一个细节值得注意:为什么不用(-|x_i - c_j|^2)来作为Softmax的输入?理论上这也是可行的,而且更贴近原始VLAD的形式。论文在前几个版本中对比过,使用(w_j^T x_i + b_j)的效果更好。原因是这种线性形式给网络更多自由,不需要让(w_j)和(c_j)之间有任何约束关系,训练起来更灵活。
同时,如果采用距离形式的软分配,由于x_i和c_j都在不断更新,Softmax输出的分布变化会非常剧烈,训练稳定性差。线性形式配合合适的初始化,能有效避免这种情况。
3.3 软分配后残差项的处理
软分配和残差项放在一起,此时的NetVLAD输出公式为:
[ V(j,k) = \sum_{i=1}^{N} \frac{\exp(w_j^T x_i + b_j)}{\sum_{j'} \exp(w_{j'}^T x_i + b_{j'})} (x_i(k) - c_j(k)) ]
残差项(x_i(k) - c_j(k))在计算时使用的是聚类中心(c_j),这些聚类中心同样是可训练参数。
那么在反向传播时,梯度如何传导?其实不需要手工推导——PyTorch、TensorFlow的自动微分会自动处理。但如果你要自己实现底层算子,或者需要调试梯度相关问题,就需要注意:梯度会通过软分配权重流向(w_j)和(b_j),也会通过残差项流向(c_j),同时还会直接流向输入(x_i)。
如果输入来自CNN骨干网络,那么梯度就会继续回传到卷积层,实现端到端更新。
3.4 从模型角度重新理解:为什么这行得通
如果把NetVLAD拆开看,它的软分配机制可以理解成一个多路注意力模型。每个聚类中心实际上定义了一个“注意力头”,它决定哪些局部特征对某个子空间贡献更大。然后每个头内部做加权残差累加,相当于在特征子空间内做了一次“带注意力的聚合”。
这种设计的好处是:
- 不同聚类中心可以学出不同的语义含义。比如某个聚类中心可能专门编码建筑风格信息,另一个编码道路结构信息。
- 可学习的权重让每个聚类中心不再是固定的“平均特征”体现,而是根据任务动态调整。
- Softmax的归一化天然保证了竞争机制,不同的聚类中心会往不同方向分化,减少了冗余。
3.5 数学推导小练习
如果你想知道从输入到输出的完整梯度流,可以自己尝试推导一下简化版本(设K=1时的Softmax分母)。假设K=1,那么:
[ V(1,k) = \sum_{i=1}^{N} 1 \cdot (x_i(k) - c_1(k)) ]
这等价于计算所有局部特征在第k维上与聚类中心1的残差总和。此时聚类中心会学成所有局部特征的均值减去一个偏置项,以便让输出向量最大化地编码有区分度的信息。虽然这个简化版本没什么实际用途,但能帮助理解梯度流方向。
4. 训练工程细节与实操要点
NetVLAD本身结构不复杂,真正影响效果的是训练策略。这一块我踩过的坑比较多,挑几个重要的写出来。
4.1 训练数据组织形式
NetVLAD最早是为视觉地点识别设计的,训练数据通常以三元组(Query,Positive,Negative)形式组织。Query是当前图像,Positive是和Query在同一地点的另一张图(视角、时间不同),Negative是不同地点的图像。
在代码层面,通常会构建一个Batch,里面包含多个Query,每个Query配一个Positive和一组合适的Negative。负样本的选择对训练效果影响很大。如果负样本和Query太相似(比如同一街道的不同门牌号),模型会被迫学习非常细微的差异,可能导致过拟合;如果负样本太简单(完全不同的场景),模型很快就学会了,loss下降很快但检索能力提升有限。
实际训练中,比较有效的做法是难例挖掘(Hard Negative Mining)。先训练几个epoch,然后用模型对训练集做一次推理,找出那些距离很近但是地点不同的图像对,作为困难负样本加入后续训练。
4.2 损失函数与训练设定
NetVLAD原论文使用三元组损失(Triplet Loss):
[ L = \sum_{i} \max(0, d(q_i, p_i) - d(q_i, n_i) + m) ]
其中(d(\cdot, \cdot))表示L2距离,(m)是margin值,论文中取0.1。因为模型输出的描述子经过L2归一化,特征都落在单位球面上,所以距离范围在0到2之间,margin取0.1已经足够拉开差距。
后来很多改进工作采用了更激进的损失函数,比如Multi-Similarity Loss、Circle Loss等。但实测下来,在三元组基础上加难例挖掘,效果不会比复杂损失函数差太多。对大多数场景,先把三元组损失和负样本挖掘做好,就已经能拿到相当不错的基准线了。
4.3 归一化与白化的工程影响
NetVLAD输出的是KD维向量,K一般取16到64,D是主干网络输出特征图的通道数。比如VGG-16的conv5层输出D=512,K=16时,描述子维度是8192。
这个维度的描述子可以直接做检索,但存储和计算开销比较大。工程上通常的做法是:
- 先做带符号平方根归一化: [ V \leftarrow \text{sign}(V) \cdot \sqrt{|V|} ] 这步能把值域压缩到根号尺度,降低极端大值的影响。
- 再做L2归一化。
- 需要降维时,用PCA把维度压到256或512维。注意PCA要用训练集的描述子来拟合,不能用测试数据。
关于是否要学PCA而非直接用固定PCA,我的经验是:如果你的数据分布比较稳定(比如都是城市街景),固定PCA就够了;如果数据来源多样,可以尝试用可学习的线性投影层代替PCA,每次训练时一起优化。方法上这个投影层可以用1x1卷积或全连接层实现。
4.4 训练超参数的实践参考值
以VGG-16主干为例,整理一组中等规模数据集的常用配置:
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| 输入图像尺寸 | 224×224 或 320×320 | 更大尺寸效果更好,但显存开销增加 |
| 聚类中心数K | 16~64 | 小数据集用16,大规模用32或64 |
| 学习率 | 0.0001(骨干网络为0.00001) | 骨干网络层的学习率调低,防止破坏预训练特征 |
| 优化器 | SGD,momentum=0.9 | Adam效果也可以,但SGD+StepLR更稳 |
| Learning rate decay | 每隔10个epoch乘0.5 | 需要根据训练曲线调整 |
| Margin(三元组) | 0.1~0.3 | 太小难收敛,太大容易不收敛 |
| Batch Size | 4~8个Query,每个Query配4~8个负样本 | 总的图像数在32到64张左右 |
4.5 数据增强对描述子鲁棒性的影响
NetVLAD训练时,数据增强策略对最终描述子的鲁棒性影响很大。除了常规的随机裁剪、水平翻转之外,这里特别推荐加入颜色抖动(Color Jitter)和亮度对比度调整。原因在于,地点识别任务中,同一地点在不同天气、不同光照条件下拍摄的图像,颜色差异可能很大。如果网络过于依赖颜色统计信息,泛化能力会明显下降。
但要注意:别做过于剧烈的几何增强。随机旋转超过15度可能造成方向信息混乱;拉伸类增强也会扭曲建筑等刚体结构的几何特征。我在实验中比较合理的组合是:随机裁剪(尺度0.7到1.0)+ 水平翻转 + 颜色抖动 + 轻微亮度对比度调整。
5. 代码实现:从零搭建NetVLAD层
写了这么多原理,直接上代码。这里给出一个PyTorch实现,核心模块简洁,方便看懂和改造。
5.1 NetVLAD层核心模块
import torch import torch.nn as nn import torch.nn.functional as F class NetVLAD(nn.Module): def __init__(self, num_clusters, dim, alpha=100.0, normalize_input=True): super(NetVLAD, self).__init__() self.num_clusters = num_clusters self.dim = dim self.alpha = alpha self.normalize_input = normalize_input # 聚类中心,可训练参数 self.conv = nn.Conv2d(dim, num_clusters, kernel_size=(1, 1), bias=True) # 初始化聚类中心 self.clusters = nn.Parameter(torch.randn(num_clusters, dim) * 0.1) # 初始化softmax前的卷积层权重 nn.init.xavier_uniform_(self.conv.weight) self.conv.bias.data.fill_(0.0) def forward(self, x): # x: (batch_size, dim, height, width) N, C, H, W = x.shape if self.normalize_input: x = F.normalize(x, p=2, dim=1) # 对通道维度做L2归一化 # 展平成 (N, C, L),其中 L = H * W,看作 N*L 个局部描述子 x_flat = x.view(N, C, -1) # (N, C, L) # 计算软分配权重 (N, K, L) assignment = self.conv(x) # (N, K, H, W) assignment = assignment.view(N, self.num_clusters, -1) # (N, K, L) assignment = F.softmax(assignment, dim=1) # 计算残差累加 # clusters: (K, C) -> (1, K, C, 1) clusters = self.clusters.unsqueeze(0).unsqueeze(-1) # (1, K, C, 1) # x_flat: (N, C, L) -> (N, 1, C, L) x_flat = x_flat.unsqueeze(1) # (N, 1, C, L) # 残差项: (N, K, C, L) residual = x_flat - clusters # 加权累加: 对 L 维度求和 vlad = residual * assignment.unsqueeze(2) # (N, K, C, L) vlad = vlad.sum(dim=-1) # (N, K, C) # 展平成 (N, K*C) vlad = vlad.view(N, -1) # 归一化 vlad = self._normalize(vlad) return vlad def _normalize(self, vlad): # 带符号平方根归一化 + L2归一化 vlad = torch.sign(vlad) * torch.sqrt(torch.abs(vlad) + 1e-6) vlad = F.normalize(vlad, p=2, dim=1) return vlad这个实现有几个注意点说明一下:
self.conv的卷积核大小是1x1,等价于对特征图的每个空间位置做一次线性变换,输出K个得分。它和使用全连接层本质上没有区别,但用1x1卷积更高效,代码也更简洁。alpha参数在原论文中是用来调节Softmax温度(即把assignment乘以alpha再算Softmax)的,上面代码里没有显式做。如果你发现软分配输出过于平滑(所有权重都接近均匀分布),可以加上assignment = F.softmax(self.alpha * assignment, dim=1)。- 通过
nn.Parameter直接定义聚类中心,梯度会自动传到该参数上。
5.2 接入CNN骨干网络
下面是完整的模型封装,把VGG-16的卷积部分作为骨干,接上NetVLAD层:
import torchvision.models as models class NetVLADModel(nn.Module): def __init__(self, num_clusters=32, dim=512, pretrained=True): super(NetVLADModel, self).__init__() # 以VGG-16为例,提取到conv5_3 backbone = models.vgg16(pretrained=pretrained) self.backbone = nn.Sequential(*list(backbone.features.children())[:30]) self.vlad = NetVLAD(num_clusters=num_clusters, dim=dim) def forward(self, x): x = self.backbone(x) # (N, 512, H', W') vlad = self.vlad(x) # (N, K*512) return vlad登录后复制
如果你的输入图像是224×224,VGG-16经过5次池化后,特征图尺寸是7×7,也就是说有49个局部描述子参与聚合。数量不多,但实测效果依然够用。想要更多局部描述子,可以把输入尺寸提高为320×320或更大的尺寸,对应特征图尺寸会变成10×10、14×14。
5.3 注意:数值稳定性与Batch Size的影响
在工程实现中有个容易忽略但又影响训练稳定性的细节:NetVLAD中的聚类中心和卷积权重在训练初期如果更新幅度过大会导致输出向量范数突变,进而反向影响骨干网络的学习稳定性。建议给NetVLAD层的参数设置独立且偏小的学习率,骨干网络用基础学习率,聚类中心可以用基础学习率乘0.1左右。
另一个重要经验是Batch Size不能太小。因为三元组损失依赖Query和Positive之间的对应关系,Batch Size小于8张图时,难例挖掘基本无法使用,因为挖掘范围太窄,找到的负样本难度不够。
5.4 弱监督训练对比强监督
如果做了密集标注(每个局部特征属于哪个位置),那么NetVLAD可以走全监督的训练方式,即对每个局部特征都确定归属后再聚合残差。但这种标注成本太高,实际项目中基本不会用。
弱监督训练反而是NetVLAD论文的最大亮点:只需要图像级别的标签(它们是否来自同一地点),无需知道具体位置对应关系。这样Site-Loss(地点损失)能够给NetVLAD层传回梯度,而层内通过自动学习找到合理的聚类中心分布。这也是NetVLAD能被广泛落地到不同领域的关键原因。
6. 常见问题与排查技巧实录
这个部分全是实操中容易遇到的问题,我按“现象—排查思路—解决方案”的方式整理成速查表。
6.1 聚类中心数K怎么选
K是NetVLAD中最重要的超参数之一。K太小,描述子区分度不够,聚类粒度过粗;K太大,训练容易过拟合,且内存开销增加。
以VGG-16主干(D=512)为例:
- K=16,输出维度8192,适合中小规模数据集(几万张图的级别)
- K=32,输出维度16384,是大多数场景的默认选择
- K=64,输出维度32768,大规模数据集和精细检索时使用,但需要更多显存和正则化手段
我在Pittsburgh数据集上对比过K=16和K=32,后者mAP大约高出1.5到2个点;但K=64反而只比K=32高不到0.5个点,训练时间却长了将近一倍。如果你刚开始做,建议直接从K=32起步。
6.2 训练loss不降或者发散
这是很多人遇到的第一个坎。常见原因有三个:
一是学习率设置不合理。NetVLAD层的参数更新幅度应该和骨干网络区分对待。我常用的做法是骨干网络学习率0.0001,NetVLAD层0.001。如果整体用同一个学习率,骨干网络容易把预训练特征破坏掉,训练曲线表现为先降后升。
二是Softmax温度设置不当。如果alpha太大,软分配趋近于one-hot,训练初期容易震荡;如果太小,软分配过于平滑,不同聚类中心学不出差异。可以先固定alpha=100,观察loss变化,如果震荡严重再调小到30或50。
三是负样本选择不当。如果负样本全是简单负样本,loss会快速下降但学不到有判别力的特征;如果全选困难负样本,训练初期模型能力不足会导致loss居高不下。建议先用简单负样本训练几个epoch,再逐步引入困难负样本。
6.3 描述子维度再降到底够不够
我在实际部署时经常需要把描述子降到128维或256维来节省存储。直接用PCA从8192维压到128维,mAP下降大约8%到12%;如果用可学习的线性投影层端到端训练,下降能控制在5%以内。
这里有个实操技巧:降维的目标维度不宜低于64。因为大多数检索任务至少需要一些基本的特征维度空间来承载不同地点的区分信息。压到32维时,即使端到端训练,mAP也会大幅下降,此时不如直接换更轻量的模型(比如MobileNet骨干)做描述子。
6.4 不同数据集间的泛化问题
NetVLAD在训练集上表现很好,但换一个完全不同风格的测试集(比如室内场景训练、室外场景测试)时,描述子质量会明显下降。原因在于CNN骨干学到的特征存在域偏移(Domain Shift),NetVLAD层聚类中心学到的划分方式也可能不再适用。
常用的解决方案是领域适配(Domain Adaptation):
- 用少量目标域数据做微调(几百张就行),只更新骨干网络的后两层和NetVLAD层。
- 训练时加入域混淆损失,让网络提取的特征域差异越小越好。
- 主干网络换用更鲁棒的架构(比如ResNet-50),对域偏移的耐受力稍好一些。
6.5 显存爆炸与训练速度优化
NetVLAD训练时显存占用往往集中在assignment矩阵上。以Batch=32,输入特征图尺寸14×14(196个位置),K=64为例,assignment矩阵的形状是(32,64,196),这个不算大。但如果输入尺寸是608×608,特征图可能是19×19或更高,再配合大Batch和聚类中心,内存和显存压力会明显上升。
优化手段有三个方向:
- 训练时用小尺寸(如224×224),测试时再用大尺寸(如320×320),效果差异不大。
- 梯度累积:梯度累积能让有效Batch Size扩大的同时不增加显存占用,但NetVLAD层的BatchNorm(如果有的话)需要特殊处理。
- 使用混合精度训练(AMP),NetVLAD层的矩阵运算在FP16下没有明显精度损失,值得一试。
7. 典型应用场景盘点
NetVLAD不只是在视觉地点识别上取得了成功,后续很多领域的工作都借鉴了它的软分配聚合思路。这里挑几个我实际接触过的方向。
7.1 图像检索与去重
大规模以图搜图(比如商品检索、街景去重)是NetVLAD最直接的应用场景。流程是把所有库图像经过骨干+NetVLAD提取全局描述子,建索引;查询图像用同一网络提取描述子,做最近邻搜索即可。
工程上需要注意索引构建的效率。描述子维度高时,暴力搜索耗时严重。可以采用倒排索引+乘积量化(PQ)的方式压缩描述子,同时用NetVLAD描述子做粗排,再用其他细粒度特征做精确重排,效果非常理想。
7.2 视觉重定位与SLAM回环检测
在视觉SLAM系统中,NetVLAD被广泛应用于回环检测。当相机运动到曾经到过的位置时,需要快速找到历史帧,通过全局描述子的相似度来唤醒回环候选帧。相比基于局部特征匹配的方法,NetVLAD的检索速度快得多,而且鲁棒性好,能适应光照、季节变换。
实际部署时,我一般用NetVLAD做粗定位,拿到Top-10候选帧后,再用RANSAC+几何校验做精确位姿估计。这套组合在室内外场景都能达到很高的准确率和召回率。
7.3 点云全局描述子
NetVLAD的思路也被迁移到了3D点云领域。对于激光雷达扫描得到的点云,先通过PointNet等结构提取逐点特征,然后将NetVLAD层用于特征聚合,得到点云全局描述子。这在自动驾驶的地图匹配和重定位中比较常见。
我自己试过在KITTI数据集上做点云描述子提取,效果比直接用PointNet++的全局特征好不少。关键在于局部特征的数量要够多、分布要够分散,NetVLAD才能充分发挥聚合能力。
7.4 视频检索与摘要
视频场景下,每帧画面通过NetVLAD提取全局描述子,然后按时序聚合(平均或加权)得到视频级描述子。由于NetVLAD对帧内的空间几何信息建模较好,这样的视频描述子不仅对画面内容变化敏感,也能在不同镜头切换时保持一定的稳定性。
不过要提醒一点:视频描述子之间的时序关系,仅靠NetVLAD是建模不出来的。如果业务上需要识别动作或事件,建议在NetVLAD之后再接一个时序编码器,效果会好很多。
7.5 跨模态检索的聚合层
最近两年做图文检索时,我也借鉴了NetVLAD的思想,把图像区域的局部特征通过软分配聚合成全局表示。具体做法是:先通过目标检测器提取区域特征,然后定义若干“聚类中心”用类似NetVLAD的学习方式做聚合。相比直接用CLS token输出,这种方法的可解释性更强,而且聚类中心能对应到某些语义概念。
8. 写在最后的一些实操心得
NetVLAD的整个发展历程,从我第一次在论文里读到它,到亲手复现,再到后来在多个项目里落地,给我最大的感慨是:一个看似简单的“把硬分配换成软分配”的思想,能带来如此广泛的影响。它的成功不只是数学上的可微化改造,更是特征工程思路的转变——让数据的编码方式去适配任务目标,而不是让任务去适应人为设计的编码规则。
如果你尝试复现本文代码并训练自己的数据集,这里有几个发自肺腑的建议:
第一,先建立基准线再去调参。刚跑通时不要一上来就加各种技巧,先用小数据集和基础配置跑一遍,确认loss能正常下降、检索指标能出来。再逐步加难例挖掘、数据增强、降维层等等。
第二,可视化软分配结果。训练完成后,把某张图片送入网络,提取assignment矩阵并把它映射回原图,看哪些区域被分配到了哪个聚类中心。经常会发现聚类中心会集中在建筑轮廓、道路边缘、地标物体等区域,这种可解释性对调试非常有帮助。
第三,注意不同版本PyTorch对数值精度的影响。我在一次从PyTorch 1.x升级到2.x后,发现同样的模型、同样的数据,mAP差了0.3个点。排查了半天,发现是F.normalize在FP16模式下的计算精度发生了变化。换成FP32后,差异消失。这种问题不致命,但会让你在调参时产生困惑。
另外,如果任务场景变化较大,比如从室外街景迁到室内房间识别,建议重新初始化聚类中心而不是沿用预训练模型的聚类中心。因为聚类中心的分布和特征的分布强相关,直接沿用旧的聚类中心,会导致初始阶段软分配权重的分布非常不均衡,拖慢收敛速度。
最后,分享一个提高训练效率的小技巧:训练初期固定骨干网络只更新NetVLAD层,训练10到20个epoch后解冻骨干网络,并调低学习率进行联合微调。这样做既能快速让NetVLAD层先适配特征空间,又能避免早期训练不稳定对骨干网络的破坏。我在多个数据集上验证过,这个方法能显著提升最终的收敛精度。