本文为9月28日~10月4日的学习小结。 本周主要学习了PointNet模型的具体细节,包括其中三个模块的功能及实现:T-Net转换网络、特征组合模块、最大池化层。 程序中使用的数据集大多使用了hdf5这种存储格式,为了解点云的存储以及读取方式,我设计了一个程序实现点云的可视化。 在论文的阅读中,对于文章中遇到了一些神经网络常见的概念(Global feature等)进行了解释。
PointNet的网络结构如下:
网络的输入:由n个点组成的单个点云或经过预先分割的场景点云(这里说明pointnet在处理大规模点云数据时需要先对其进行划分,可能是因为存储能力有限)
网络的输出:根据应用场景,分为两种情况。 对于分类网络:总共输出K个分值(对应K个分类),数值的大小代表了输入点云属于各类别的可能性。 对于分割网络:总共输出N*M个分值(N为点的数量,M为分割数量),对于每个点,M个分值代表该点属于各分割点集的可能性。
点云的格式:点云用一个N×3的数组表示,包含了该点云中每一个点的三个空间坐标,即(x,y,z)。其具体存储形式会在后面的程序部分进行详细分析。
分类网络与分割网络的关系:由图中的结构可以看出,这两部分联系紧密。 分类网络在全局特征(Global Feature)经过多层神经网络(MLP,Multi-Layer Perceptron)后就得到了输出,即分类结果。 而分割网络则是将分类网络浅层得到的局部特征(Local Feature)与全局特征相结合作为输入。后面也会分析二者差异的原因。
为了解决点云的无序性、不变性等种种问题,PointNet模型中设计了三个重要结构:转换网络、特征组合模块、最大池化层。
PointNet网络中设计了两个转换网络,分别用于输入以及特征的转换。对输入点的坐标以及提取的特征进行规范化处理,使得经过刚体变换后的点云输入网络后提取到的特征尽可能的保持不变。 因此,输入点云在经过旋转、平移等变换后,其语义信息并不会因此改变,保证了点云的不变性。 T-Net也是个小型的神经网络,由特征提取,最大池化层以及全连接层等结构构成。
特征组合模块将组合局部特征与全局特征组合为新的点特征。这是因为分割既需要局部的几何信息,也需要全局语义信息来对每个点进行预测。这两种信息的差别在最后一部分进行解释。
最大池化层用于聚合特征,通过使用最大池化层来汇总特征信息能够解决无序性问题。 具体原因如下: 上式中,f为目标函数,输入为点云中的点,输出为分类结果。网络通过MLP来实现函数h:用于提取每个点的特征。并使用对称函数g来聚合这些特征,以达到近似f的效果。 其中对称函数是解决无序性问题的关键。举个简单的例子,现在有一个数组A:{1,2,3,4,5},将其中的元素打乱顺序得到数组B:{2,4,1,5,3}。若使用求和操作,那么数组A和数组B最终得到的结果都为15,可以看到结果与数组中元素的顺序无关。 点云作为点集,点的输入顺序可以随意改变,若使用对称函数对特征进行聚合,那么最终得到的特征也与点的顺序无关,pointnet中就使用了最大池化层实现了对称操作。
为了了解三维点云数据的存储及读取方式,我设计了一个程序实现点云数据的读取以及可视化。 首先读取.h5文件后可以发现数据集中包含四个key:data、faceId、label、normal。其中data用于存放点云数据,label用于存放每个点云的分类。暂时不清楚剩下两类的功能。
data为(2048×2048×3)格式,每个h5文件中的data存储至多2048(0~2047)个点云,每个点云由2048(2~2047)个点组成,每个点包含x,y,z(-1~1)三个坐标。Label为(2048×1)格式,存储了每个点云在40个类别(可在shape_names.txt中查看)中对应的分类标签(0~39)。测试后发现data中第一维存放点云的编号,第二维存放单个点云中的所有点,第三维存放每个点的坐标。因此在确定点云编号后,提取后两维的信息就能绘制出三维散点图。
最终的显示效果如下: 代码如下:
#show_pointcloud.py import os import sys import h5py import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np #读取h5文件 file = h5py.File('data/modelnet40_ply_hdf5_2048/ply_data_train0.h5','r') #查看key print(file.keys()) #获取2048个点云数据 data = file['data'] all_pointclouds = np.array(data) #读取单个点云的数据(2048*3)即:2048个点的三维空间坐标 single = all_pointclouds[2044,::] #获得点云中各点的三个坐标 x = single[:,0] y = single[:,1] z = single[:,2] #此处的fig为2维图像 fig = plt.figure() #二维转为三维 ax = Axes3D(fig) #ax = fig.add_subplot(projection='3d') #绘图,设置每个点的大小为4 ax.scatter(x, y, z,s=4) ax.set_xlabel("X axis") ax.set_ylabel("Y axis") ax.set_zlabel("Z axis") #设置坐标轴的最大最小值,否则得到的图像会自动缩放,注意不要使用plt方法。 #因为plt是针对二维图像的,三维图像需要使用axes的方法。 ax.set_xlim3d(xmin=-1, xmax=1) ax.set_ylim3d(ymin=-1, ymax=1) ax.set_zlim3d(zmin=-1, zmax=1) #显示图像,建议在设置-工具-python scientific中把“在窗口中显示图像”关闭, #使用plt的窗口来查看图像能够鼠标拖动改变观察角度,更加方便。 plt.show() file.close() exit()以下为分类网络的构建
''' n*3 --- 输入转换网络 ---》 n*3 --- mlp(64*64) ---》 n*64 --- 特征转换网络 ---》 n*64 - mlp(64,128,1024)-》 n*1024 --- max pooling ---》 1024 --- mlp(512,256,k)-》 k '''以输入转换网络为例,网络的输入为(batch_size,num_point,3)的形式,首先对输入数据增加一维,经过三层MLP后变为(batch_size,1,1,num_point),reshape后经过全连接层变为(batch_size,256)。使用权重矩阵对其相乘并加上偏置矩阵,最终得到一个(batch_size,3,3)形式的转换矩阵。
''' 输入数据转换网络,输入B*N*3的灰度图像,输出batch_size*3*K的转换矩阵 ''' def input_transform_net(point_cloud, is_training, bn_decay=None, K=3): """ Input (XYZ) Transform Net, input is BxNx3 gray image Return: Transformation matrix of size 3xK """ batch_size = point_cloud.get_shape()[0].value num_point = point_cloud.get_shape()[1].value #输入图像增加维数,数字表示增加位置,-1表示最后一维。 input_image = tf.expand_dims(point_cloud, -1) #input_image(64,1024,3,1) #网络构建,MLP(64,128,1024) net = tf_util.conv2d(input_image, 64, [1,3], padding='VALID', stride=[1,1], bn=True, is_training=is_training, scope='tconv1', bn_decay=bn_decay) #net=(64 1024 1 64) net = tf_util.conv2d(net, 128, [1,1], padding='VALID', stride=[1,1], bn=True, is_training=is_training, scope='tconv2', bn_decay=bn_decay) net = tf_util.conv2d(net, 1024, [1,1], padding='VALID', stride=[1,1], bn=True, is_training=is_training, scope='tconv3', bn_decay=bn_decay) net = tf_util.max_pool2d(net, [num_point,1], padding='VALID', scope='tmaxpool') # net=(64,1,1,1024) net = tf.reshape(net, [batch_size, -1]) #64 1024 #全连接层 net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training, scope='tfc1', bn_decay=bn_decay) #64 512 net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training, scope='tfc2', bn_decay=bn_decay) #64 256 #XYZ转换网络 with tf.variable_scope('transform_XYZ') as sc: assert(K==3) #创建神经元的两个变量:权重weight与偏移bias,例:x1*w1+x2*w2+b0=y weights = tf.get_variable('weights', [256, 3*K], initializer=tf.constant_initializer(0.0), dtype=tf.float32) #256 9 biases = tf.get_variable('biases', [3*K], initializer=tf.constant_initializer(0.0), dtype=tf.float32) #9 #偏移项biases加上一个常数 biases += tf.constant([1,0,0,0,1,0,0,0,1], dtype=tf.float32) #权重参数与网络点乘 transform = tf.matmul(net, weights) #64 9 #最后添加偏置项 transform = tf.nn.bias_add(transform, biases) #64 9 transform = tf.reshape(transform, [batch_size, 3, K]) #64 3 3 return transform万能近似定理(Universal approximation theorem)表明一个至少一层,具有有限个神经元的前馈神经网络可以近似任意连续函数。
在点云分类任务中,我们希望有这样一种函数:它的输入是点云中各个点,输出是这个点云属于某一类。如(1)中的目标函数f,若最大池化层有足够多的神经元,即K足够大,则网络就可以近似函数f。
论文中经常出现局部特征(local feature)与全局特征(global feature)这两个概念,以下是我个人的理解: 在深度神经网络中,前面几层得到的为局部特征,后面几层得到的为全局特征。 浅层网络的感受野小,因此得到的特征包含更多的局部细节,在点云中,local feature可能是一些局部的轮廓,如尖角等,由于同一类的物体间局部结构可能存在差别,因此local feature具有个性。 而随着网络深度增加,感受野增大(尤其是经过全连接层后),此时的global feature是更加抽象的语义信息,同时由于深度的增加,局部的细节也丢失了。在点云中,全局特征可能表现为物体的骨架等。它能够代表输入数据类别(如椅子这一类物体)的共性。