PointNet设想的由来
说到如何设计PointNet网络的,那我们首先就要从输入数据的特性说起。点云数据是一种不规则的数据,在空间上和数量上可以任意分布,由于其特性而不能直接适用于传统CNN。以往的研究者想出了很多种处理点云方式:
-
将点云数据转化为规则的数据,转化为栅格使其均匀分布,再用3DCNN来处理栅格数据
缺点:3D cnn 复杂度相当的高,三次方的增长,所以分辨率不高303030 相比图像是很低的,带来了量化的噪声错误,限制识别的错误。 -
有的学者将点云数据从3D投影到2D平面,再进行2DCNN,这样会损失3D空间的信息。
吸取前者的经验,根据点云数据的特性着手设计PointNet网络
点云数据的三个特性
- 点云具有无序性
点云数据是一种对顺序很不敏感的数据,点云是无序的,点与点之间的顺序可以任意变换,但其代表的还是同一个物体。故此在设计网络的时候,就需要使网络能够针对不同顺序下的点云数据都能够提取到同一种空间特征,具有置换不变性。
数学上提供了对称函数的理论指出:输出结果与输入顺序无关的函数
而在网络上能实现对称函数功能的是最大池化层:Maxpooling
三维点云数据不管以什么顺序输入,经过maxpool后输出的总是最大的特征,这也有效的解决了点云无序性的问题。
如想具体了解可点击点云深度学习的3D场景理解
- 点与点之间的存在的信息连接
这点我认为在pointnet中没有很好的体现出来,在进行零件细分和语义分割的过程中,只是将局部特征和全局特征进行了串联拼接,该架构不能获取点附近的底层局部结构,这也是PointNet最主要的缺点,而在Pointnet++中改善此缺点。
- 点云的旋转不变性
点云具有旋转不变性,平移不变性,缩放不变性。针对该问题,网络增加了一个基于数据本身的变换函数模块T-Net,我在看别的博客,大多都是解释,生成点云旋转矩阵与输入点云数据相乘,使得与特征空间对齐来保证不变性。
对于特征空间对齐这个概念我一直不是很理解,在观看其他博客的时候,我得到了一种说法,实质上T-net得作用就是保留原始点云的部分特征,为后面的concat操作提供更多特征。在原文中,作者也对T-Net做了实验,发现在分类过程中,加入T-Net网络,使用输入变换可以提高0.8%的性能。但是T-Net没法获取点附近之间的局部特征,故此对于分割而言是没有帮助的,在语义分割中也弃用了T-Net。
最终得到PointNet框架如下:
分类代码详解
先贴个图,看分类代码的分支,用到的其实就是transform_nets.py,pointnet_cls,py,外加训练测试加预测了
T-Net
def input_transform_net(point_cloud, is_training, bn_decay=None,K=3):
"""
输入(XYZ)转换网络
输入:B x N x 3灰度图像
返回:转换矩阵尺寸 3 x K
"""
batch_size = point_cloud.get_shape()[0].value #批次大小为B的值 32
num_point = point_cloud.get_shape()[1].value #每个批次点云数据量N大小 2048
input_image = tf.expand_dims(point_cloud,-1) #扩展数组的维度[B,N,3,1] (32,2048,3,1)
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)
#将(32,2048,3,1)-->(32,2048,1,1)
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)
#(32,2048,1,1)-->(32,2048,1,128)
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)
#(32,2048,1,128)-->(32,2048,1,1024)
net = tf_util.max_pool2d(net, [num_point,1],
padding='VALID', scope='tmaxpool')
#maxpooling (32,2048,1,1024)-->(32,1,1,1024)
#利用1024维特征生成256维度的特征
#(32,1,1,1024)-->(32,1024)
net = tf.reshape(net,[batch_size,-1])
#(32,1024)-->(32.512)
net = tf_util.fully_connected(net,512,bn=True,is_training=is_training,
scope='tfc1',bn_decay=bn_decay)
#(32,512)-->(32,256)
net = tf_util.fully_connected(net,256,bn=True, is_training=is_training,
scope='tfc2', bn_decay=bn_decay)
#生成点云旋转矩阵 T=3*3
with tf.variable_scope('transform_XYZ') as sc:
assert (K==3)
weights = tf.get_variable('weights',[256,3*K],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
biases = tf.get_variable('biases',[3*K],
initializer=tf.constant_initializer(0.0),
dtype=tf.float32)
biases += tf.constant([1,0,0,0,1,0,0,0,1],dtype=tf.float32)
transform =tf.matmul(net,weights)
transform = tf.nn.bias_add(transform,biases)
"""tf.nn.bias_add(value,bias,name=None)
一个叫bias的向量加到value的矩阵,是向量与矩阵的每一行相加,
得到的结果和value矩阵大小相同
将偏差项bias加到value上,加到每一个数上
"""
transform = tf.reshape(transform,[batch_size,3,K])
return transform
KK的旋转矩阵和33的旋转矩阵代码基本一样就不细说了
train.py代码
import argparse
"""argpare是python自带的命令行参数解析包,可以方便读取命令行参数"""
import math
import h5py
import numpy as np
import tensorflow as tf
import socket
import importlib
import os
import sys
BASE_dir = os.path.dirname(os.path.abspath(__file__))#获取当前文件的绝对路径
sys.path.append(BASE_dir)
sys.path.append(os.path.join(BASE_dir,'models'))
sys.path.append(os.path.join(BASE_dir,'utils'))
import preproesess
import tf_util
parser = argparse.ArgumentParser()
parser.add_argument('--gpu',type=int,default=0, help='GPU to use [default:GPU 0]')
parser.add_argument('--model',default='pointnet_cls',help='Model name: pointnet_cls or pointnet_cls_basic [default: pointnet_cls]')
parser.add_argument('--log_dir',default='log',help='Log dir [default : log]')
parser.add_argument('--num_point',type=int,default=1024,help='Point Number [256/512/1024/2048] [default:1024]')
parser.add_argument('--max_epoch',type=int,default=50,help='Epoch to run [default: 250]')
parser.add_argument('--batch_size',type=int,default=32,help='Batch Size during training [default: 32]')
parser.add_argument('--learning_rate',type=float,default=0.001,help='Initial learning rate [default:0.001]')
parser.add_argument('--momentum',type=float,default=0.9,help='Initial learning rate [default :0.9]')
parser.add_argument('--optimizer',default='adam',help='adam or momentum [default:adam]')
parser.add_argument('--decay_step',type=int,default=200000,help='Decay step for lr decay [default: 200000]')
parser.add_argument('--decay_rate',type=float,default=0.7,help='Decay rate for lr decay [default:0.7]')
FLAGS = parser.parse_args()
BATCH_SIZE =FLAGS.batch_size
NUM_POINT = FLAGS.num_point
MAX_EPOCH = FLAGS.max_epoch
BASE_LEARNING_RATE = FLAGS.learning_rate #基础学习率
GPU_INDEX = FLAGS.gpu
MOMENTUM = FLAGS.momentum
OPTIMIZER = FLAGS.optimizer
DECAY_STEP = FLAGS.decay_step
DECAY_RATE = FLAGS.decay_rate
MODEL = importlib.import_module(FLAGS.model) #导入网络模块
"""importlib:
python提供importlib包作为标准库的一部分,提供python中import语句的实现
importlib允许创建自定义的对象,用于引入过程"""
MODEL_FILE = os.path.join(BASE_dir,'models',FLAGS.model+'py')
LOG_DIR = FLAGS.log_dir
if not os.path.exists(LOG_DIR): os.mkdir(LOG_DIR)
os.system('cp %s %s' % (MODEL_FILE,LOG_DIR))#cp为copy,复制文件和目录
os.system('cp train.py %s' % (LOG_DIR))
LOG_FOUT = open(os.path.join(LOG_DIR,'log_train.txt'),'w')
LOG_FOUT.write(str(FLAGS)+'\n')
MAX_NUM_POINT = 2048
NUM_CLASSES = 40
"""????BN 代表什么"""
BN_INIT_DECAY = 0.5
BN_DECAY_DECAY_RATE = 0.5
BN_DECAY_DECAY_STEP = float(DECAY_STEP)
BN_DECAY_CLIP = 0.99
HOSTNAME = socket.gethostname()
"""socket插口
socket模块提供函数用于使用主机名和地址来工作
socket.gethostname()返回运行程序所在的计算机的主机名"""
#ModelNet40 official train/test
TRAIN_FILE =preproesess.getDataFiles(os.path.join(BASE_dir,'data/modelnet40_ply_hdf5_2048/train_files.txt'))
TEST_FILE =preproesess.getDataFiles(os.path.join(BASE_dir,'data/modelnet40_ply_hdf5_2048/test_files.txt'))
def log_string(out_str): #写日志
LOG_FOUT.write(out_str+'\n')
LOG_FOUT.flush()
print(out_str)
"""write()方法用于向文件中写入指定字符串
1.fileObject.write([str]) str:要写入文件的字符串
2.flush()方法是用来把文件从内存buffer(缓冲区)中强制刷新到硬盘中,
同时清空缓冲区,一般情况下文件关闭后会自动刷新到硬盘中,但有时
需要在关闭前刷新到硬盘中,可使用flush()
fileObject.flush() //刷新缓冲区
在使用流时,都会有一个缓冲区,把要发的数据先放到缓冲区,缓冲区
放满以后再一次性发过去,而不是分开一次一次发
flush()表示强制将缓冲区中的数据发送出去,不必等到缓冲区满"""
def get_learning_rate(batch): #获取学习率
learning_rate = tf.train.exponential_decay(
BASE_LEARNING_RATE, #事先设定的初始学习率
batch * BATCH_SIZE, #当前的数据集索引(总的迭代次数)
DECAY_STEP, #衰减速度(在迭代到该次数时学习率衰减为 lr*decay_rate)
DECAY_RATE, #衰减系数,通常介于0-1之间
staircase=True) #通过staircase选择不同的衰减方式,若为True(global_step/decay_steps)则被转化为整数
learning_rate = tf.maximum(learning_rate,0.00001)
return learning_rate
"""tf.train.exponential_decay()函数实现指数衰减学习率
1.首先使用较大学习率(目的:为快速得到一个比较优的解)
2.然后通过迭代逐步减小学习率(目的:为使模型再训练后期更加稳定)
如果staircase为True,则global_step / decay_steps始终取整数,
也就是说衰减是突变的,每decay_steps次变化一次,变化曲线是阶梯状。
decayed_learning_rate = learning_rate * decay_rate^(global_step/decay_step)"""
def get_bn_decay(batch):
bn_momentum = tf.train.exponential_decay(
BN_INIT_DECAY, #初始衰减
batch*BATCH_SIZE, #总的迭代次数
BN_DECAY_DECAY_STEP, #衰减步数
BN_DECAY_DECAY_RATE, #衰减率
staircase=True)
bn_decay = tf.minimum(BN_DECAY_CLIP,1-bn_momentum)
return bn_decay
"""衰减也是不断衰减的
tf.minimum(a,b):返回的是a,b之间的最小值_
tf.maximun(a,b):返回的是a,b之间的最大值"""
def train():
with tf.Graph().as_default():
with tf.device('/gpu:'+str(GPU_INDEX)):
pointclouds_pl, labels_pl = MODEL.placeholder_inputs(BATCH_SIZE,NUM_POINT)
is_training_pl = tf.placeholder(tf.bool,shape=())
print(is_training_pl)
#注意将global_step = batch参数最小化
#告诉优化器在每次训练时都为您增加batch参数
batch = tf.Variable(0)
bn_decay = get_bn_decay(batch)
tf.summary.scalar('bn_decay',bn_decay)
# 获取model和loss
pred, end_points = MODEL.get_model(pointclouds_pl,is_training_pl,bn_decay=bn_decay)
loss = MODEL.get_loss(pred,labels_pl,end_points)
tf.summary.scalar('loss',loss)
correct = tf.equal(tf.argmax(pred,1),tf.to_int64(labels_pl))
"""1.tf.argmax(input,axis):根据axis取值的不同返回每行或每列最大值的索引
tf.argmax(array,0) tf.argmax(array,1)
axis = 0:比较每一列的元素,将每一列最大元素所在的索引记录下来,
输出最大元素所在的索引数组
axis = 1:将每一行最大元素的索引记录下来
2.tf.to_int64:张量变换,将张量转换为int64类型,
返回一个类型为int64的Tensor或者SparseTensor,与x具有相同的形状
3.tf.equal(x,y,name=None)对比两个矩阵或者向量的相等的元素,若相等
那就返回True,反则False,返回的值的矩阵维度和x,y一样。
由于是逐个元素对比,所以x,y的维度也要相同"""
accuary = tf.reduce_sum(tf.cast(correct,tf.float32)) / float(BATCH_SIZE)
tf.summary.scalar('accuary',accuary)
"""tf.reduce_sum(input_tensor,axis=None,keepdims=None)
计算张量tensor沿着某一维度的和,可在求和后降维
input_tensor:待求和的tensor
axis:指定的维度,不指定则计算所有元素的总和
keepdims:是否保持原有张量维度,True保持,False降维,默认降维"""
#get training operator
learning_rate = get_learning_rate(batch)
tf.summary.scalar('learning_rate',learning_rate)
if OPTIMIZER =='momentum':
optimizer = tf.train.MomentumOptimizer(learning_rate,momentum=MOMENTUM)
elif OPTIMIZER == 'adam':
optimizer = tf.train.AdamOptimizer(learning_rate)
train_op = optimizer.minimize(loss,global_step=batch)
"""tf.train.Optimizer.minimize(loss,global_step=None,var_list=None,
gate_gradients=1,aggregation_method=None)
添加操作通过更新var_list来最大程度减少loss
是将compute_gradients()和apply_gradients()组合在一起
变量
loss:包含最小化值的张量
global_step:可选变量,再变量更新后增加一
var_list:可选的可变对象列表,可进行更新以最大程度减少损失
gate_gradients:如何控制梯度的计算
"""
# 添加操作以保存和还原所有变量。
saver = tf.train.Saver()
# Create a session
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
config.allow_soft_placement = True
config.log_device_placement = False
sess = tf.Session(config= config)
"""tf.ConfigProto()函数在创建session时候,对session进行参数配置
1.tf.ConfigProto(log_device_placement = Ture)
记录设备指派情况,获取Tensor指派到哪个设备(几号CPU几号GPU),
会在终端打印出各项操作在哪个设备上运行
2.tf.ConfigProto(allow_soft_placement=True)
在tf中,通过命令"with tf.device('/cpu:0')",允许手动设置操作运行的设备
设置该函数允许tf自动选择一个存在并且可用的设备运行。
3.动态申请显存 tf.ConfigProto(gpu_options.allow_growth=True)
Session是为了控制,输出文件的执行语句
"""
# Add summary writers
merged = tf.summary.merge_all()
train_writer = tf.summary.FileWriter(os.path.join(LOG_DIR, 'train'),
sess.graph)
test_writer = tf.summary.FileWriter(os.path.join(LOG_DIR, 'test'))
# Init variables
init = tf.global_variables_initializer()
sess.run(init, {is_training_pl: True})
ops = {'pointclouds_pl': pointclouds_pl,
'labels_pl': labels_pl,
'is_training_pl': is_training_pl,
'pred': pred,
'loss': loss,
'train_op': train_op,
'merged': merged,
'step': batch}
for epoch in range(MAX_EPOCH):
log_string('**** EPOCH %03d ****' % (epoch))
sys.stdout.flush()
"""sys.stdout是print的一种默认输出格式
print默认调用了sys.stdout.write()方法"""
train_one_peoch(sess,ops,train_writer)
eval_one_epoch(sess,ops,test_writer)
#保存变量到磁盘
if epoch % 10 == 0:
save_path = saver.save(sess, os.path.join(LOG_DIR, "model.ckpt"))
log_string("Model saved in file: %s" % save_path)
博主也是入门,初看到代码的时候头晕眼花,故此通过搜索将很多自己没有用过的函数都进行了注释,希望能帮助到像博主一样刚入门基础不是很好的初学者。
代码分类结果
由于电脑配置低,所以只进行了50epoch的训练,跑了差不多十多个小时。
原文分类正确率