天天看點

開源架構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 類似。

繼續閱讀