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