开源框架PointNet 代码详解——/pointnet/sem_seg/train.py

/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 类似。

 

  • 5
    点赞
  • 64
    收藏
    觉得还不错? 一键收藏
  • 22
    评论
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值