/pointnet/sem_seg/train.py 是PointNet 中用来训练点云语义分割(Semantic Segmentation in Scenes)的文件。当需要训练自己的数据集的时候,可能需要对这个训练文件做一些修改,因此有必要看懂其中的代码。记录下来,也是捋清思路。
parser = argparse.ArgumentParser()
parser.add_argument('--gpu', type=int, default=0, help='GPU to use [default: GPU 0]')
parser.add_argument('--log_dir', default='log', help='Log dir [default: log]')
parser.add_argument('--num_point', type=int, default=4096, help='Point number [default: 4096]')
parser.add_argument('--max_epoch', type=int, default=50, help='Epoch to run [default: 50]')
parser.add_argument('--batch_size', type=int, default=24, help='Batch Size during training [default: 24]')
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=300000, help='Decay step for lr decay [default: 300000]')
parser.add_argument('--decay_rate', type=float, default=0.5, help='Decay rate for lr decay [default: 0.5]')
parser.add_argument('--test_area', type=int, default=6, help='Which area to use for test, option: 1-6 [default: 6]')
FLAGS = parser.parse_args()
命令行解析。num_point默认4096,这与所用数据集有关。
BATCH_SIZE = FLAGS.batch_size
NUM_POINT = FLAGS.num_point
MAX_EPOCH = FLAGS.max_epoch
NUM_POINT = FLAGS.num_point
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
从命令行接受的全局变量。DECAY_STEP 和 DECAY_RATE用来计算learning_rate的衰减。具体计算方法下面介绍。
LOG_DIR = FLAGS.log_dir
if not os.path.exists(LOG_DIR): os.mkdir(LOG_DIR)
os.system('cp model.py %s' % (LOG_DIR)) # bkp of model def
os.system('cp train.py %s' % (LOG_DIR)) # bkp of train procedure
LOG_FOUT = open(os.path.join(LOG_DIR, 'log_train.txt'), 'w')
LOG_FOUT.write(str(FLAGS)+'\n')
记录训练日志文件。以及备份 model.py 和 train.py。
MAX_NUM_POINT = 4096
NUM_CLASSES = 13
BN_INIT_DECAY = 0.5
BN_DECAY_DECAY_RATE = 0.5
#BN_DECAY_DECAY_STEP = float(DECAY_STEP * 2)
BN_DECAY_DECAY_STEP = float(DECAY_STEP)
BN_DECAY_CLIP = 0.99
HOSTNAME = socket.gethostname()
全局变量。每个样本(此处每个样本为Block块,详见我记录的数据集结构:https://blog.csdn.net/shaozhenghan/article/details/81087024)点数为4096,一共13个语义分类标签。BN_ 开头的4个变量用来计算 Batch Normalization 的Decay参数,即decay参数也随着训练逐渐decay。具体计算方法下面解介绍。 socket 涉及网络编程,不懂==
ALL_FILES = provider.getDataFiles('indoor3d_sem_seg_hdf5_data/all_files.txt')
room_filelist = [line.rstrip() for line in open('indoor3d_sem_seg_hdf5_data/room_filelist.txt')]
因为论文中原作者所用数据集划分为了24个h5格式的文件,名字存在all_files.txt 中,上面第一行是获取所有数据文件名。第二行获取每个样本(Block)所对应的room。
# ply_data_all_0.h5 至 ply_data_all_23.h5 一共 23×1000+585=23585行,每行一个物体,每个物体4096点,每点9个维度
# room_filelist.txt 一共 23585 行; 即对应每个物体在哪个room采集的
# Load ALL data
data_batch_list = []
label_batch_list = []
for h5_filename in ALL_FILES:
data_batch, label_batch = provider.loadDataFile(h5_filename)
data_batch_list.append(data_batch)
label_batch_list.append(label_batch)
data_batches = np.concatenate(data_batch_list, 0)
label_batches = np.concatenate(label_batch_list, 0)
print(data_batches.shape)
print(label_batches.shape)
依次从每个数据文件中加载数据。这里首先将数据定义为list,然后在循环中依次append,循环结束后再将list转换为numpy数组。这样会比较浪费内存,因为要重新在内存中开辟一块与list一样大但是连续的内存,然后将list的内容复制过去。实际自己训练的时候可以用直接用numpy数组。但numpy数组不支持动态扩展,即np.append()每次都会重新为新数组分配内存,然后copy。这样效率很低。那么可以根据自己的训练集的大小预先np.zeros(),然后修改数据即可。
Python 中使用动态数组的方法参见:http://blog.chinaunix.net/uid-23100982-id-3164530.html
test_area = 'Area_'+str(FLAGS.test_area)
train_idxs = []
test_idxs = []
# enumerate(): 用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标
for i,room_name in enumerate(room_filelist):
if test_area in room_name:
test_idxs.append(i)
else:
train_idxs.append(i)
train_data = data_batches[train_idxs,...]
train_label = label_batches[train_idxs]
test_data = data_batches[test_idxs,...]
test_label = label_batches[test_idxs]
print(train_data.shape, train_label.shape)
print(test_data.shape, test_label.shape)
分配训练集和测试集。test_area 为从命令行解析的参数,原文数据集从6个区域中采样而得,训练时需指定哪一个区域的数据用来测试。
def log_string(out_str):
LOG_FOUT.write(out_str+'\n')
# flush() 刷新缓冲区,将缓冲区中的数据立刻写入文件,同时清空缓冲区
LOG_FOUT.flush()
print(out_str)
log_string(out_str)函数用来log训练日志。
def get_learning_rate(batch):
learning_rate = tf.train.exponential_decay(
BASE_LEARNING_RATE, # Base learning rate.
# decayed_learning_rate = learning_rate *
# decay_rate ^ (global_step / decay_steps)
batch * BATCH_SIZE, # Current index into the dataset.
DECAY_STEP, # Decay step.
DECAY_RATE, # Decay rate.
staircase=True)
learning_rate = tf.maximum(learning_rate, 0.00001) # CLIP THE LEARNING RATE!!
return learning_rate
计算指数衰减的学习率。训练时学习率最好随着训练衰减。函数tf.train.exponential_decay(learning_rate, global_step, decay_steps, decay_rate, staircase=False, name=None)为指数衰减函数。计算公式如下:
decayed_learning_rate = learning_rate *
decay_rate ^ (global_step / decay_steps)
此处 global_step = batch * BATCH_SIZE
if the argument staircase
is True
, then global_step /decay_steps
is an integer division and the decayed learning rate follows a staircase function.
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
计算衰减的Batch Normalization 的 decay。基本同上。
接下来是训练函数 train():
def train():
with tf.Graph().as_default():
with tf.device('/gpu:'+str(GPU_INDEX)):
pointclouds_pl, labels_pl = placeholder_inputs(BATCH_SIZE, NUM_POINT)
is_training_pl = tf.placeholder(tf.bool, shape=())
# Note the global_step=batch parameter to minimize.
# global step 参数 初始化 为0, 每次自动加 1
# That tells the optimizer to helpfully increment the 'batch' parameter for you every time it trains.
batch = tf.Variable(0)
bn_decay = get_bn_decay(batch)
tf.summary.scalar('bn_decay', bn_decay)
这一段主要是placeholder,以及batch初始化为0。没看出batch是怎么每次增加的??????
# Get model and loss
pred = get_model(pointclouds_pl, is_training_pl, bn_decay=bn_decay)
loss = get_loss(pred, labels_pl)
tf.summary.scalar('loss', loss)
# tf.argmax(pred, 2) 返回pred C 这个维度的最大值索引
# tf.equal() 比较两个张量对应位置是否想等,返回相同维度的bool值矩阵
correct = tf.equal(tf.argmax(pred, 2), tf.to_int64(labels_pl))
accuracy = tf.reduce_sum(tf.cast(correct, tf.float32)) / float(BATCH_SIZE*NUM_POINT)
tf.summary.scalar('accuracy', accuracy)
预测值为pred,调用model.py 中的 get_model()得到。由get_model()可知,pred的维度为B×N×13,13为Channel数,对应13个分类标签。每个点的这13个值最大的一个的下标即为所预测的分类标签。
# 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)
获得衰减后的学习率,以及选择优化器optimizer。
# Create a session
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
config.allow_soft_placement = True
config.log_device_placement = True
sess = tf.Session(config=config)
配置session 运行参数。
config.gpu_options.allow_growth = True:让TensorFlow在运行过程中动态申请显存,避免过多的显存占用。
config.allow_soft_placement = True:当指定的设备不存在时,允许选择一个存在的设备运行。比如gpu不存在,自动降到cpu上运行。
config.log_device_placement = True:在终端打印出各项操作是在哪个设备上运行的。
# 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()
train_one_epoch(sess, ops, train_writer)
eval_one_epoch(sess, ops, test_writer)
# Save the variables to disk.
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)
初始化参数,开始训练。train_one_epoch 函数用来训练一个epoch,eval_one_epoch函数用来每运行一个epoch后evaluate在测试集的accuracy和loss。每10个epoch保存1次模型。
训练函数 train() 结束
train_one_epoch(sess, ops, train_writer) 函数
def train_one_epoch(sess, ops, train_writer):
""" ops: dict mapping from string to tf ops """
is_training = True
log_string('----')
current_data, current_label, _ = provider.shuffle_data(train_data[:,0:NUM_POINT,:], train_label)
file_size = current_data.shape[0]
num_batches = file_size // BATCH_SIZE # // 除完后对结果进行自动floor向下取整操作
provider.shuffle_data 函数随机打乱数据,返回打乱后的数据。 num_batches = file_size // BATCH_SIZE,计算在指定BATCH_SIZE下,训练1个epoch 需要几个mini-batch训练。
for batch_idx in range(num_batches):
if batch_idx % 100 == 0:
print('Current batch/total batch num: %d/%d'%(batch_idx,num_batches))
start_idx = batch_idx * BATCH_SIZE
end_idx = (batch_idx+1) * BATCH_SIZE
feed_dict = {ops['pointclouds_pl']: current_data[start_idx:end_idx, :, :],
ops['labels_pl']: current_label[start_idx:end_idx],
ops['is_training_pl']: is_training,}
summary, step, _, loss_val, pred_val = sess.run([ops['merged'], ops['step'], ops['train_op'], ops['loss'], ops['pred']],
feed_dict=feed_dict)
train_writer.add_summary(summary, step)
pred_val = np.argmax(pred_val, 2)
correct = np.sum(pred_val == current_label[start_idx:end_idx])
total_correct += correct
total_seen += (BATCH_SIZE*NUM_POINT)
loss_sum += loss_val
在一个epoch 中逐个mini-batch训练直至遍历完一遍训练集。计算总分类正确数total_correct和已遍历样本数total_senn,总损失loss_sum.
log_string('mean loss: %f' % (loss_sum / float(num_batches)))
log_string('accuracy: %f' % (total_correct / float(total_seen)))
记录平均loss,以及平均accuracy。
train_one_epoch(sess, ops, train_writer) 函数 结束
eval_one_epoch(sess, ops, test_writer)
用来在测试集上评估evaluate。与train_one_epoch 类似。