2017년 7월 2일 일요일

딥러닝 기반 3차원 비전 객체 인식 PointNet 분석

이 글은 최근 발표된 딥러닝 기반 3차원 비전 객체 인식 기술인 PointNet을 분석해 본다. 이 글은 아래 레퍼런스를 참고한다.
PointNet 분류 결과

1. 머리말
PointNet은 CVPR 2017 컨퍼런스에서 발표된 arXiv 기술을 기반으로 한다. 스캔 결과로 얻어지는 3차원 점군을 인식하기 위해서, 많은 연구자들은 점군을 복셀(voxel) 형식으로 변환한다. 복셀은 점군을 요약하기에는 좋으나, 빈공간이 많이 발생하여, 비효율적인 부분이 있다. PointNet은 입력 시 포인트 순열의 불변성을 이용한다. PointNet을 통해, 객체 분류, 세그먼테이션, 스캔 장면의 의미 해석 등에 필요한 아키텍처를 제공한다. PointNet은 간단하지만, 매우 효율적으로 3차원 점군에서 객체를 인식한다. PointNet은 ShapeNet Part Dataset을 기반으로 훈련을 하였다.


2. 설치하기
PointNet은 텐서플로우 1.01, h5py, CUDA 8.0, cuDNN 5.1, 우분투 14.04를 사용한다.
만약, h5py 가 없다면, 다음과 같이 패키지를 설치한다. PointNet은 HDF5 를 사용하며, 관련된 상세 내용는 여기를 참고하길 바란다.
sudo apt-get install libhdf5-dev
sudo pip install h5py
참고로, 아나콘다에서는 다음과 같이 설치하면 된다.
conda install h5py

3. 신경망 훈련하기
다음과 같이 실행한다.
python train.py
로그 파일은 log폴더에 저장된다. 점군은 HDF5 파일로 ModelNet40 형식으로 다운로드 된다(416MB). 각 점군은 2048 포인트들을 담고 있다.
다음과 같이 학습 결과를 텐서보드를 통해 확인할 수 있다.
tensorboard --logdir log
훈련후에는 다음과 같이 정확도를 평가할 수 있다.
python evaluate.py --visu
만약, 미리 준비한 점군이 있다면, utils/data_prep_util.py 유틸리티 함수를 통해 HDF5 파일을 읽고 쓸 수 있다.
참고로, 훈련을 위해서는 ShapeNetPart 데이터(약 1.08GB)를 다음과 같이 다운로드해야 한다.
cd part_seg
sh download_data.sh
훈련과정 및 결과는 다음과 같다.

다음은 인식한 객체 정확도이다.
참고로, 훈련과정에서 사용된 포인트 클라우드는 이미지로 저장되어 확인할 수 있다.



4. 데이터 구조 분석
훈련용 데이터 파일은 총 6개이며, (3 x 2048 x 2048) 데이터셋으로 포인트클라우드(점군)와 라벨 데이터셋(2048)이 저장되어 있다. 테스트용 데이터는 총 2개이다. 파일포맷은 HDF5이다.
훈련용 데이터셋(3 x 2048 x 2048) HDF5파일

5. 신경망 구조 분석
PointNet 신경망 구조는 다음과 같다. n 개 포인트 클라우드 (점군)을 입력한다. 입력 및 특징 변환을 수행 하고, max pooling을 통해 특징을 일반화한다. 출력으로 m 개 클래스 스코어가 분류된다. 신경망 구조는 분류(classification), 세그먼테이션(segmentation) 네트워크로 구성되어 있다. 세그먼트 네트워크는 분류 네트워크를 확장하였다. Batchnorm(Batch Normalization)은 ReLU 함수를 적용한다. Dropout은 분류 네트워크의 마지막 mlp(multi layer perception. 다층 레이어 퍼셉트론)에만 적용하였다.
PointNet 구조

분류 네트워크 구조는 다음과 같다.
1. n개의 3차원 포인트 좌표값이 input points로 입력
   1) T-Net 으로 3x3 텐서 변환
   2) matrix multiply 연산 처리
2. 변환된 n x 3 데이터가 mlp 64x64로 전달되어, n x 64 텐서로 출력됨
3. feature transform을 통해 계산된 n x 64 텐서 출력
4. mlp 64x128x1024 로 변환된어 n x 1024 텐서로 출력
5. max pooling 을 통해 일반화된 특징 벡터 1024 출력
6. mlp 512 x 256 x k 로 출력해 score 벡터 k 계산

세그먼트 네트워크 구조는 다음과 같다.
1. 분류 네트워크의 1 ~ 4번까지 그래프 구조는 재활용됨
2. n x 1088 텐서가 mlp 512x256을 통해 point feature 텐서 n x 128 로 출력
3. n x 128 텐서가 mlp 128 x m 을 통해 n x m 텐서로 출력

6. 소스코드 분석
텐서플로우를 사용하고 파이썬으로 코딩된 전체 소스코드는 261라인이다. 좀 더 깊은 이해를 위해, 소스코드의 주요 부분을 확인해 보자. 

MAX_NUM_POINT = 2048   # 입력 포인트 갯수
NUM_CLASSES = 40       # 클래스 갯수
BN_INIT_DECAY = 0.5    # 학습 속도 

# 훈련용, 테스트용 포인트 클라우드 데이터 준비
TRAIN_FILES = provider.getDataFiles( \
    os.path.join(BASE_DIR, 'data/modelnet40_ply_hdf5_2048/train_files.txt'))
TEST_FILES = provider.getDataFiles(\
    os.path.join(BASE_DIR, 'data/modelnet40_ply_hdf5_2048/test_files.txt'))

def train():
    with tf.Graph().as_default():
        with tf.device('/gpu:'+str(GPU_INDEX)):   # 신경망 계산 시 GPU 사용
            pointclouds_pl, labels_pl = MODEL.placeholder_inputs(BATCH_SIZE, NUM_POINT) 

            # prediction 모델 구성
            pred, end_points = MODEL.get_model(pointclouds_pl, is_training_pl, bn_decay=bn_decay)
            loss = MODEL.get_loss(pred, labels_pl, end_points)

            # 정확도 모델 구성
            correct = tf.equal(tf.argmax(pred, 1), tf.to_int64(labels_pl))
            accuracy = tf.reduce_sum(tf.cast(correct, tf.float32)) / float(BATCH_SIZE)

            # 최적화 옵션에 따른 모델 정의
            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)   # 오차 최소화 모델
            
        sess.run(init, {is_training_pl: True})     # 세션 시작

        for epoch in range(MAX_EPOCH):     # 세대별 학습
            train_one_epoch(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"))   # 모델 저장

def train_one_epoch(sess, ops, train_writer):
    np.random.shuffle(train_file_idxs)   # 임의로 서플링함
    
    for fn in range(len(TRAIN_FILES)):
        current_label = np.squeeze(current_label)  # 라벨값
        
        for batch_idx in range(num_batches):       # 배치 갯수만큼 루프
            rotated_data = provider.rotate_point_cloud(current_data[start_idx:end_idx, :, :])  # 회전 변환
            jittered_data = provider.jitter_point_cloud(rotated_data)  # 지터 처리
            feed_dict = {ops['pointclouds_pl']: jittered_data,
                         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)   # 세션 실행

def eval_one_epoch(sess, ops, test_writer):
    for fn in range(len(TEST_FILES)):
        current_label = np.squeeze(current_label)
        num_batches = file_size // BATCH_SIZE
        for batch_idx in range(num_batches):
            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['loss'], ops['pred']], feed_dict=feed_dict)
            pred_val = np.argmax(pred_val, 1)
            correct = np.sum(pred_val == current_label[start_idx:end_idx])

실제 pointnet 신경망 정의는 다음 폴더 아래에 있다.

이 중에 pointnet_cls, pointnet_seg 가 분류, 세그먼테이션 신경망을 구성하는 모듈이다. pointnet_cls.py는 전체 98라인이고, pointnet_seg.py는 115라인이다. 설명은 주석으로 처리하였으니, 앞의 신경망 구조와 비교해 확인해 보자.

# pointnet_cls.py
def placeholder_inputs(batch_size, num_point):
    pointclouds_pl = tf.placeholder(tf.float32, shape=(batch_size, num_point, 3))
    labels_pl = tf.placeholder(tf.int32, shape=(batch_size))
    return pointclouds_pl, labels_pl


def get_model(point_cloud, is_training, bn_decay=None):
    """ Classification PointNet, input is BxNx3, output Bx40 """
    with tf.variable_scope('transform_net1') as sc:
        transform = input_transform_net(point_cloud, is_training, bn_decay, K=3)
    point_cloud_transformed = tf.matmul(point_cloud, transform)  # 점군 변환
    input_image = tf.expand_dims(point_cloud_transformed, -1)    # 차원 확장

    net = tf_util.conv2d(input_image, 64, [1,3],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv1', bn_decay=bn_decay)   # conv1. 64 1 x 3 컨볼류션 적용
    net = tf_util.conv2d(net, 64, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv2', bn_decay=bn_decay)   # conv2. 64 1 x 1 컨볼루션 적용

    with tf.variable_scope('transform_net2') as sc:
        transform = feature_transform_net(net, is_training, bn_decay, K=64)  # 변환
    end_points['transform'] = transform
    net_transformed = tf.matmul(tf.squeeze(net, axis=[2]), transform)  # matmul 계산
    net_transformed = tf.expand_dims(net_transformed, [2])             # 텐서 확장

    net = tf_util.conv2d(net_transformed, 64, [1,1],           
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv3', bn_decay=bn_decay)    # conv3. 64 1 x 1 컨볼루션 적용
    net = tf_util.conv2d(net, 128, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv4', bn_decay=bn_decay)    # conv4. 128 1 x 1 컨볼루션 적용
    net = tf_util.conv2d(net, 1024, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv5', bn_decay=bn_decay)    # conv5. 1024 1 x 1 컨볼루션 적용

    # Symmetric function: max pooling
    net = tf_util.max_pool2d(net, [num_point,1],                   # max pooling
                             padding='VALID', scope='maxpool')

    net = tf.reshape(net, [batch_size, -1])                            # reshape
    net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
                                  scope='fc1', bn_decay=bn_decay)                     # fc1 512
    net = tf_util.dropout(net, keep_prob=0.7, is_training=is_training,            # dp1. dropout 0.7
                          scope='dp1')
    net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,  
                                  scope='fc2', bn_decay=bn_decay)                     # fc2 256
    net = tf_util.dropout(net, keep_prob=0.7, is_training=is_training,
                          scope='dp2')                                                       # dp2. dropout 0.7
    net = tf_util.fully_connected(net, 40, activation_fn=None, scope='fc3')    # fc3. 40 

def get_loss(pred, label, end_points, reg_weight=0.001):
    """ pred: B*NUM_CLASSES,
        label: B, """
    loss = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=pred, labels=label)  # softmax 
    classify_loss = tf.reduce_mean(loss)                                                           # reduce mean

    transform = end_points['transform']  # B x K x K
    K = transform.get_shape()[1].value    # transform
    mat_diff = tf.matmul(transform, tf.transpose(transform, perm=[0,2,1]))
    mat_diff -= tf.constant(np.eye(K), dtype=tf.float32)
    mat_diff_loss = tf.nn.l2_loss(mat_diff)                           # loss 계산


# pointnet_seg.py
def get_model(point_cloud, is_training, bn_decay=None):
    """ Classification PointNet, input is BxNx3, output BxNx50 """
    with tf.variable_scope('transform_net1') as sc:
        transform = input_transform_net(point_cloud, is_training, bn_decay, K=3)
    point_cloud_transformed = tf.matmul(point_cloud, transform)
    input_image = tf.expand_dims(point_cloud_transformed, -1)

    net = tf_util.conv2d(input_image, 64, [1,3],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv1', bn_decay=bn_decay)
    # 여기까지는 분류 네트워크와 구조 동일

    net = tf_util.conv2d(concat_feat, 512, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv6', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 256, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv7', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 128, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv8', bn_decay=bn_decay)
    net = tf_util.conv2d(net, 128, [1,1],
                         padding='VALID', stride=[1,1],
                         bn=True, is_training=is_training,
                         scope='conv9', bn_decay=bn_decay)

    net = tf_util.conv2d(net, 50, [1,1],
                         padding='VALID', stride=[1,1], activation_fn=None,
                         scope='conv10')
    net = tf.squeeze(net, [2]) # B x N x C


    return net, end_points


레퍼런스



댓글 없음:

댓글 쓰기