赞
踩
本篇博文主要是对 PointNet,PointNet++ 论文的要点进行梳理和总结。认真阅读本博文后,不仅能够深刻理解论文的核心算法思想,而且对模型训练数据、模型的训练流程也能了然于胸。如果想阅读原论文以及翻译,参考下面的链接资源,是我对原论文的翻译。
- PointNet 论文翻译,中英文对照以及关键点详细解读,请参考 https://blog.csdn.net/kxh123456/article/details/120901487。
- PointNet++ 论文翻译,https://blog.csdn.net/kxh123456/article/details/121021971。
- PointNet,PointNet++ 论文作者的公开课链接为:https://www.shenlanxueyuan.com/channel/8hQkB6hqr2/detail(大佬的课必须去感受下啊~~)
解决了什么问题?
a. 由于卷积网络在2D图像上的兴起,很多研究者开始将神经网络应用于3D点云数据。但是,大部分工作都是将3D点云体素化或者转换为多个视角的2D图像,然后应用常规的卷积神经网络。因此,PointNet 主要解决了如何将2D卷积神经网络【直接】处理3D点云本身,即使点云出现波动,噪声或者缺失的情况,也能稳定的提取点集特征。
本篇论文的亮点?
a. 如何解决点云无序性问题? 3D点云一个重要的特征是无序性,对于 N N N 个点的点集,它有 N ! N! N! 种输入顺序。那么,针对每一种顺序,如何保证网络的学习结果保持不变?为此,本文提出对称函数(最大池化层,Max-Pooling)来解决无序性的问题。同时,最大池化层将独立学习的点特征聚合为全局点集特征,进而进行后续的3D识别任务。
b. 输入点云和特征对齐模块的具体网络结构是什么,有什么作用?网络的预测结果应该对特定的变换具有不变性,比如刚性变换。为此,本文提出 T-Net 变换矩阵,将输入以及不同点的特征进行对齐,使得网络学习得到的表达也具有这种特性。下面的图片是 T-Net 的具体结构,包括每一层的输入和输出。【注意,输入点集对齐模块和特征对齐模块的差别在于红色标识部分的尺寸的差别。】
输入点集对齐模块如下图所示,
错误修正:上图的第三个卷积核修改为 1x1(1x3是错误的)
特征对齐模块如下图所示,
两个变换模块的训练表现如下图所示,
单独使用任何一个模块,网络的精度提升非常有限。由于特征对齐模块的维度较大,需要添加正则化,才能保证训练效果有所提升。同时使用连个模块以及正则化,精度提升相对较大。
c. 网络的稳定性 网络对输入点云的波动,添加一定的噪声点,以及删除点集部分点等干扰操作,网络能够保证学习能力的鲁棒性,也可以达到良好的预测效果。那么,为何网络具有一定的【稳定性】?参考下面的解释。
给定一个无序的点集 { x 1 , x 2 , . . . , x n } , x i ∈ R d \lbrace{x_1,x_2,...,x_n\rbrace},x_i\in R^d {x1,x2,...,xn},xi∈Rd,可以定义一个函数集合 f : → R f: \rightarrow R f:→R,将点集映射为一个向量:
f ( x 1 , x 2 , . . . , x n ) = γ ( M A X i = 1 , . . . , n { h ( x i ) } ) (1) f(x_1,x_2,...,x_n)=\gamma(MAX_{i=1,...,n}\lbrace h(x_i) \rbrace) \tag1 f(x1,x2,...,xn)=γ(MAXi=1,...,n{h(xi)})(1)
这里, γ a n d h \gamma ~ and ~h γ and h 通常是多层感知机网络(MLP).
集合函数 f f f 对输入点集的顺序具有不变性,并且可以拟合任何连续函数集(论文 PointNet 论述)。对于连续函数而言,微小的变动不会影响函数值。 h h h 被认为是单独一个点(a point)的空间编码,学习每一个点的特征。
详细的网络结构
论文中大致的网络的结构如下,分类和分割任务的主干网络基本一样,差别在于后面网络的处理,
PointNet 分类网络如下图所示,
PointNet Part分割网络如下图所示,应注意以下几点:(a)特征变换矩阵后的两层卷积的核输出分别为512,2048,论文是128,512;(b)concate 模块中的16,指示着数据集目标总共的类别,有助于提升效果;(c)倒数第三、第四层网络添加了 Dropout层。(d)特征转换矩阵的大小为128,大于分类的转换矩阵64.
损失函数
(A) 分类任务
损失函数包括两个部分,分别是常规的交叉熵(cross_entropy)分类损失,以及特征变换矩阵的损失函数。因为特征变换矩阵的维度较大,为了能够保证网络的训练效果,需要添加正则项(上文有相关介绍)。损失函数的代码实现如下代码片段所示,
def get_loss(pred, label, end_points, reg_weight=0.001):
""" pred: B*NUM_CLASSES, label: B, """
print('classify loss compute================>\n')
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=pred, labels=label)
classify_loss = tf.reduce_mean(loss)
tf.summary.scalar('classify loss', classify_loss)
# Enforce the transformation as orthogonal matrix
transform = end_points['transform'] # BxKxK
K = transform.get_shape()[1].value
mat_diff = tf.matmul(transform, tf.transpose(transform, perm=[0,2,1]))
mat_diff -= tf.constant(np.eye(K), dtype=tf.float32)
mat_diff_loss = tf.nn.l2_loss(mat_diff)
tf.summary.scalar('mat loss', mat_diff_loss)
return classify_loss + mat_diff_loss * reg_weight
按照论文的陈述,约束生成的特征矩阵为正交矩阵,相应的损失函数设计如下,
L
r
e
g
=
∣
∣
I
−
A
A
T
∣
∣
F
2
L_{reg}=||I-AA^T||^2_F
Lreg=∣∣I−AAT∣∣F2
正交矩阵的性质1:如果 A A T = I AA^T=I AAT=I,或者 A T A = I A^TA=I ATA=I, I I I是单位矩阵,那么是矩阵 A A A是正交矩阵。
正交矩阵的性质2:设 A A A为 n n n阶实矩阵,则 A A A为正交矩阵的充分必要条件是其列(行)向量组是标准正交向量组。
对于性质2可以推断,如果特征变换矩阵是正交矩阵,它的行或列为正交向量组,那么不同的输入点集经过特征转换后,更能表达学习的特征空间,也就是表达性更强。
(B) 分割任务
Part分割的损失函数同样包含两个部分,第一部分是分类损失,这一部分损失被置零,不参与训练。第二部分是Part类别损失,其实计算的仍是输入点集中的每一个点的语义分类。第三部分是关于特征变换矩阵的损失(分类任务已陈述,两个特征矩阵仅仅是尺寸的差别)。
def get_loss(l_pred, seg_pred, label, seg, weight, end_points):
per_instance_label_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=l_pred, labels=label)
label_loss = tf.reduce_mean(per_instance_label_loss)
# size of seg_pred is batch_size x point_num x part_cat_num(50)
# size of seg is batch_size x point_num
per_instance_seg_loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=seg_pred, labels=seg), axis=1)
print('per_instance_seg_loss: ', per_instance_seg_loss)
seg_loss = tf.reduce_mean(per_instance_seg_loss)
per_instance_seg_pred_res = tf.argmax(seg_pred, 2)
# Enforce the transformation as orthogonal matrix
transform = end_points['transform'] # BxKxK
K = transform.get_shape()[1].value
mat_diff = tf.matmul(transform, tf.transpose(transform, perm=[0,2,1])) - tf.constant(np.eye(K), dtype=tf.float32)
mat_diff_loss = tf.nn.l2_loss(mat_diff)
total_loss = weight * seg_loss + (1 - weight) * label_loss + mat_diff_loss * 1e-3
return total_loss, label_loss, per_instance_label_loss, seg_loss, per_instance_seg_loss, per_instance_seg_pred_res
数据集
ModelNet40:用于分类的CAD模型,总共40类。12311个CAD模型,9843个用于训练,2468个用于测试。下载官方的开源代码,如下图所示,运行【train.py】脚本,会自动下载数据(~416M,可能需要翻墙才能下载)。
ShapeNet Part:该数据集用于Part分割,总共有16881个分块形状,总共16类,50个不同的分块,每一个对象标注了大概2-5个分块。如下图所示,所有的资源均在文件夹【./part_seg]中,
Part分割任务的训练流程:按照下图中官网的步骤就可以训练了,大概要下载2个数据。这个数据可能也得翻墙才能下载。
训练过程,参数设置
具体的训练设置,可以参考开源代码,都是常规的设置,这里不赘述了。
论文不足
论文最大的不足是不能很好的提取局部点云特征。从分割网络的设计而言,特征合并模块(直接 concate)也过于简单。
论文主要为了解决什么问题?
a. 由于 PointNet 不能很好地提取局部精细的特征,那么无法应用于需要识别精细特征的识别任务,比如语义分割。为此,本文主要是为了解决直接提取3D点云特征的过程中,如何能够更好的提取【不同尺度下局部】的精细特征。
b. 相比于之前的 PointNet 网络,本文的 PointNet++ 网络添加了分层次的结构,进而提取不同尺度下的特征。类似于图像中的特征金子塔,尺度增大,感受野不断增大。
论文的关键点有哪些?
下图是 PointNet++ 网络的一部分结构,也是最为核心的结构——分层次的点集抽象层(Hierarchical Set Abstraction)。所谓的分层,也就是使用多个 SA (Set Abstraction)模块进行特征提取,差别在于每一个 SA 模块的采样点数量(
K
)和采样半径(R
)不一样。随着层次加深,K
和R
会随之增大。
SA 模块是该论文最为核心的部分,它包含三个关键层:Sampling Layer,Grouping Layer,PointNet Layer。每一个模块的作用,请参考下面【a,b,c】的分析。【d】介绍了如何解决密度不均匀的特征学习问题。
a. 如何划分相互重叠的局部邻域?
Sampling Layer:首先,通过最远点采样算法(Furthest Point Sample,FPS),在输入点集中均匀采样固定数量的点,记为C
。
Grouping Layer:然后,以点集C
中的点作为中心点,在一定半径R
内,取出K
个点,此时完成了对输入点集的单尺度局部邻域划分。那么输入点集就被划为为空间上很多个相互重叠的球邻域,每一个球邻域就是一个局部点集。输出的形状为 B × N × K × 3 B\times N\times K\times3 B×N×K×3,其中 B 是输入的 batch,N 为中心点的数量,K 是每一个邻域球中点的数量,3表示x,y,z通道的坐标。
下面的代码片段给出了一个SA
模块的相关参数设置,包括中心点个数,局部邻域的半径大小,局部邻域内采样点数量,以及 PointNet 层的参数等等。
# 单尺度 SA 模块 l1_xyz, l1_points, l1_indices =pointnet_sa_module(l0_xyz, l0_points, # 输入点集以及特征 npoint=512, # 中心点的采样数量 radius=0.2, nsample=32, # 邻域划分参数 mlp=[64,64,128], # pointnet 输出通道数量 mlp2=None, group_all=False, is_training=is_training, bn_decay=bn_decay, scope='layer1', use_nchw=True)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
b. 如何学习局部邻域特征?
PointNet Layer:输入点集经过前面两层的划分,得到N
个空间球邻域,然后将其输入到 PointNet Layer,学习局部邻域的点集特征。以【a】中的代码片段的SA的参数为例(第一个SA模块),经过MLP
,Max-Pooling
的处理,得到相应的点集特征。点集特征提取的代码片段如下所示,
# new_points of group layer: Tensor("layer1/sub:0", shape=(16, 512, 32, 3), # dtype=float32, device=/device:GPU:0) for i, num_out_channel in enumerate(mlp): new_points = tf_util.conv2d(new_points, num_out_channel, [1,1], padding='VALID', stride=[1,1], bn=bn, is_training=is_training, scope='conv%d'%(i), bn_decay=bn_decay, data_format=data_format) # output: # new_points after mlp: Tensor("layer1/transpose_1:0", shape=(16, 512, 32, 128), # dtype=float32, device=/device:GPU:0) if pooling=='max': new_points = tf.reduce_max(new_points, axis=[2], keep_dims=True, name='maxpool') print('new_points after of max-pooling: ', new_points) # output # new_points after of max-pooling: Tensor("layer1/maxpool:0", shape=(16, 512, 1, 128), # dtype=float32, device=/device:GPU:0)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
还需注意的是,在 Grouping Layer 通常会将上一层的特征与当前的局部邻域划分点集进行连接(concate),组成新的特征(也即是将xyz通道融合进特征向量),进而作为 PointNet Layer 的输入,参考下面的代码片段中的tf.concat()
,
if points is not None: print('kkkkkkk') grouped_points = group_point(points, idx) # (batch_size, npoint, nsample, channel) if use_xyz: # (batch_size, npoint, nample, 3+channel) new_points = tf.concat([grouped_xyz, grouped_points], axis=-1) else: new_points = grouped_points else: print('xxxxxxx') new_points = grouped_xyz
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
c. PointNet 用于提取局部邻域的点集特征,没有特别复杂的结构,具体结构可以参考下面【3. 详细的网络结构】部分的结构图。在这一层,输入为 N ′ N' N′ 个局部邻域点集,大小为 N ′ × K × ( d + C ) N'\times K\times (d+C) N′×K×(d+C)。每一个局部邻域的输出是被其中心和局部特征(编码了中心的局部邻域特征)抽象,输出大小为 N ′ × ( d + C ′ ) N'\times(d+C') N′×(d+C′).
局部邻域的点坐标首先被转换为相对于中心点的局部结构:
x i ( j ) = x i ( j ) − x ^ ( j ) , i = 1 , 2 , . . . , K a n d j = 1 , 2 , . . . , d x^{(j)}_i=x^{(j)}_i-\hat x^{(j)},i=1,2,...,K ~ and~j=1,2,...,d xi(j)=xi(j)−x^(j),i=1,2,...,K and j=1,2,...,d
这里, x ^ \hat x x^ 是中心点的坐标。我们使用 PointNet 作为学习局部特征的基础模块。通过将点的相对坐标以及点特征连接而形成新的特征,并作为 PointNet 的输入,我们可以提取局部区域内点与点之间的相互关系。
d. 如何解决密度变化对特征学习的影响? 在3D点云中,不同区域的密度变化时非常普遍的现象,为了保证特征学习的有效性。本文提出两种密度自适应层来解决该问题,分别为 MSG 和 MRG 方法。
MSG(Multi-Scale Grouping) 该方法思想很简单,就是在一个局部邻域中心点添加多个尺度,然后将不同尺度下的局部邻域特征连接为一个特征,作为当前SA模块提取的点集特征。
# 单个SA模块的多尺度参数 l1_xyz, l1_points = pointnet_sa_module_msg(l0_xyz, l0_points, # 输入点集和特征 512, # 中心点数量 [0.1,0.2,0.4], # 邻域的半径的大小(多尺度) [16,32,128], # 对应三个尺度(半径)下的局部采样点数量 [[32,32,64], [64,64,128], [64,96,128]], # 输出通道数量 is_training, bn_decay, scope='layer1', use_nchw=True)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
MRG(Multi-Resolution Grouping) 上面的MSG方法的计算代价会非常高。为此,提出MRG方法, L i L_i Li 层的特征由两个向量组成:第一个向量为 L i − 1 L_{i-1} Li−1 的子区域的特征,另一向量是当前区域的SA特征,然后将两个向量连接。作者没有提供这一方法的代码实现。
详细的网络结构
a. 分类网络结构
ModelNet-40分类的网络结构如图所示,网络结构图为单尺度版本(每个SA
只设置了一个半径值)。各种颜色的的框表示模块的类别,前后箭头之上分别都标示了输入和输出的形状。由于绘图空间有限,输入和输出只标出了SA
输出的特征向量形状。实际上,每个SA
模块的输入和输出有2个向量,分别是中心点的坐标(BxNx3)以及相应的点特征向量。要注意的是,这里的点特征向量不再是单独一个中心点的特征,而是以该点为中心的邻域特征。
b. 分割网络的一些细节
相比于 PointNet 简单将浅层网络和深层网络特征进行连接,PointNet++ 提出新的融合特征的方法(或者说更为有效的方法),逆距离权重法(IDW,Inverse Distance Weight)。
IDW:该方法是一种插值算法,简言之,就是将已知点的特征通过插值的形式传递到目标点。具体在本文中,就是将 N l × ( d + C ) N_l\times (d+C) Nl×(d+C) 个点特征传递给 N l − 1 N_{l-1} Nl−1 个点,由于 SA模块降采样,所有 N l < N l − 1 N_l < N_{l-1} Nl<Nl−1. 该插值算法的具体公式如下图所示,
上图的公式中, w i w_i wi是计算点权重,它与距离成反比,距离越近,影响越大。通常, f ( j ) f^{(j)} f(j) 称为未知点的值, f i ( j ) f^{(j)}_i fi(j)是已知点的值,k 表示在已知点集中取 k 个点进行插值计算,p 表示距离对权重的影响程度。
具体到本文的算法中,取 p=2,k=3,也就是从已知点集中取出最近的3(参数 k)个点进行插值计算。比如, L i − 1 L_{i-1} Li−1 层有1024个点, L i L_i Li 采样后剩下 512个点,那么如何将512个点的特征传递给1024个点呢?简单说,就是1024中的每一个点,从512个点中选取最近的三个点,然后根据最近的3个点进行插值,得到新的特征。计算方式是依照上面的公式,作者的开源代码中也实现了该算法,具体细节可以参考代码。
网络结构参考如下:
与分类网络的差别在于FP之后的结果,其它均是一样的结构,更为详细的结构,可以参考代码,这里不在赘述。
上图分割网络的3个FP模块用于上采样,那么具体实现细节是什么样的呢?下面,我们将单独解析该模块:
作用:由于分割需要确定每个点的分类,所以就需要得的特征,
损失函数
两个任务都是分类问题,所以用的是交叉熵损失函数,这里不在赘述,我之前的博文也详细介绍了此损失函数的理论和应用细节。博文参考链接:交叉熵损失函数理论以及tensorflow应用。
数据集介绍
数据集与 PointNet 使用的是一样的数据,并且
ModelNet40
和ShapeNet
并不是实际场景扫描数据,不在具体叙述。
训练流程
a. 训练环境配置
由于 PointNet++ 的开源代码中添加了使用C++、Cuda编写的采样层,分组层,插值层,因此,需要单独编译这几个接口。编译步骤如下:首先,如果是用conda安装开发环境,那么需要激活深度学习环境(tensorflow)。其次,如下图所示,进入相应的【3d_interpolation,grouping,sampling】文件夹,更改sh文件的配置,见下面的代码片段,主要是更改tenssorflow的相关路径。最后,运行sh **_compile.sh
脚本,如果没有问题则会生成相应的【.so】文件。
编译过程中,修改tensorflow路径如下代码所示,如果找不到tensorflow的【include,lib】路径,可以执行相关代码,打印路径即可,代码如下【import tensorflow as tf; print(tf.sysconfig.get_include()),import tensorflow as tf; print(tf.sysconfig.get_include())
】,
#/bin/bash /usr/local/cuda-10.2/bin/nvcc tf_grouping_g.cu -o tf_grouping_g.cu.o -c -O2 -DGOOGLE_CUDA=1 -x cu -Xcompiler -fPIC # TF1.2 # g++ -std=c++11 tf_grouping.cpp tf_grouping_g.cu.o -o tf_grouping_so.so -shared -fPIC -I /usr/local/lib/python2.7/dist- packages/tensorflow/include -I /usr/local/cuda-8.0/include -lcudart -L /usr/local/cuda-8.0/lib64/ -O2 -D_GLIBCXX_USE_CXX11_ABI=0 # TF1.4 g++ -std=c++11 tf_grouping.cpp tf_grouping_g.cu.o -o tf_grouping_so.so -shared -fPIC -I /home/slam/anaconda3/envs/TF1.4/lib/python3.6/site-packages/tensorflow/include -I /usr/local/cuda-10.2/include -I /home/slam/anaconda3/envs/TF1.4/lib/python3.6/site-packages/tensorflow/include/external/nsync/public -lcudart -L /usr/local/cuda-10.2/lib64/ -L/home/slam/anaconda3/envs/TF1.4/lib/python3.6/site-packages/tensorflow -ltensorflow_framework -O2 -D_GLIBCXX_USE_CXX11_ABI=0
- 1
- 2
- 3
- 4
- 5
- 6
我的编译环境,tensorflow-1.4,python3.6,Ubuntu-18,cuda-10.2,编译过程中,遇到如下图所示的问题。从错误提示,明显可以知道是系统的【gcc,g++】版本太高,因此需要安装更低版本的编译器(如何安装,自行百度,比较简单),
根据上述错误提示,建立如下的软链接,
sudo ln -s gcc-5 gcc(将系统的gcc版本改为gcc-5) sudo ln -s g++-5 g++
- 1
- 2
按照上述更改,依然会报同样的错误,为此按照下面的代码片段的操作,继续更改软链接。之所以进行下面的更改,应该是当初系统安装cuda的时候,gcc、g++默认使用的系统的gcc、g++版本太高导致。
sudo ln -s /usr/bin/gcc-5 /usr/local/cuda/bin/gcc(更改cuda中gcc的版本) sudo ln -s /usr/bin/g++-5 /usr/local/cuda/bin/g++
- 1
- 2
b. 训练流程 参考开源代码得流程,准备好数据,直接运行训练脚本,即可正常训练。
论文不足
暂时略
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。