많은 논문이 그렇겠지만 PointNet도
코드를 봐야 근본적인 이해가 가능하다.
해당 github 페이지에서 코드를 참조하였다.
https://github.com/yanx27/Pointnet_Pointnet2_pytorch
GitHub - yanx27/Pointnet_Pointnet2_pytorch: PointNet and PointNet++ implemented by pytorch (pure python) and on ModelNet, ShapeN
PointNet and PointNet++ implemented by pytorch (pure python) and on ModelNet, ShapeNet and S3DIS. - GitHub - yanx27/Pointnet_Pointnet2_pytorch: PointNet and PointNet++ implemented by pytorch (pure ...
github.com
PointNet은 Semantic segmentation을 할 때
S3DIS 데이터셋을 사용한다.
이 데이터셋은 6개의 장소에서 총 272개의 장면이 있다.
(PointNet논문에서는 271이라 되있다.)
다음은 S3DIS data 형태다.
각 장면당 1000만개의 point가 있다.
해당 코드의 전처리 코드를 실행하면 다음과 같다.
python collect_indoor3d_data.py
위와같은 형식으로 .npy 파일을 만들어 전처리 한다.
S3DISDataLoader.py파일을 보면
S3DISDataset에서 먼저 data split 인자와 데이터 위치, point 수, test로 사용할 장소 등이 있다.
S3DIS는 각 장면마다 data수가 다르기 때문에 입력 데이터 $N$의 수를 4096으로 정했다.
위 코드를 통해 rooms_split에 room(장소) 리스트를 만든다.
그다음 self.room_points와 self.room_label을 분할아여 저장할 리스트를 만든다.
또 room좌표의 최대 최소값을 저장할 리스트를 만든다.
num_point_all은 각 장면의 point수를 저장하는 리스트다.
labelweights는 각 클래스에 해당하는 point수를 확인하기 위함이다.
for문에서 각 장면의 data_path를 꺼낸 뒤 load한다.
이때 room_data는 주석처럼 xyzrgbl이다.
이것을 points와 labels로 분할한다.
이때 label을 통하여 히스토그램을 뽑고 이것을 tmp에 저장한다.
또 각 point의 x,y,z좌표의 최대 최소값을
coord_min, coord_max에 저장한다.
이전에 만든 room_points와 room_label와
room_coord_min과 room_coord_max와
num_point_all에 해당하는 것을 저장한다.
labelweights를 정규화 시킨 뒤 처리를 하여각 클래스당 가중치 self.labelweights를 만든다.[1.124833 1.1816078 1. 2.2412012 2.340336 2.343587 1.7070498
2.0335796 1.8852289 3.8252103 1.7948895 2.7857335 1.3452303]
또 num_point_all도 정규화를 하여 sample_prob를 만든다.
전체 point수에 샘플링 비율(0.1)을 곱하고 %N%으로나눠서num_iter를 구한다.
room_idxs는 샘플링할 포인트의 인덱스인데,
sample_prob와 num_iter를 고려하여 구한다.
이것을 numpy 배열로 만든게 self.room_idxs다.
따라서 train/test 셋에 샘플이 몇개 있는지 출력한다.
Totally 47623 samples in train set.
__getitem__은 데이터를 꺼내는 부분이다.
dataloader가 idx를 쏴주면 거기에 해당하는 데이터를 출력하면 된다.
idx를 받았을 때, room_idx와 points와 labels를 정하고
해당 장면의 point 수를 N_points를 저장한다.
while문에서 랜덤으로 point(x, y, z) 한개를 선택하여 center라고 한다.
여기를 중심으로 x, y 좌표의 $1m \times 1m$크기 박스를 쳐서
해당하는 영의 point 수가 1024개가 넘을때까지 반복하여 point_idxs를 모은다.
이렇게 모으면 $N$수만큼 point 샘플인 selected_point_idxs를 만든다.
이제 위해서 구한 idxs를 가지고 selected_points를 구한다.
이때 current_points라는것을 만드는데 일단 모두 0값으로 만든다.
current_points의 6,7,8행은 selected_points의 x,y,z값을 최대값으로 정규화 한다.
이후 selected_points의 x, y값은 앞서 찾은 center의 x, y값으로 뺀다.
또 selected_points의 RGB 값은 255를 나눠서 정규화 한다.
이제 current_points의 0~5행은 selected_points값으로 한다.
만약 transform이 적용됐다면 current_points에도 적용하고
이제 data를 load하면 current_points와, current_labels을 반환한다.
__len__은 원래 데이터 전체의 수를 반환하는 것이지만
여기선 장면 개수로 했다.
다시 PointNet 구조를 살펴보면 다음과 같다.
pointnet_utils.py파일을 보면
PointNetEncoder 클래스에 head 아래의 network가 구현되어 있다.
먼저 인자로 global_feat를 받는데 이는 ouput으로 global feature만 필요한
classification task에서 True이다.
feature_transform은 모델 구조에서 feature transform을 적용 유무다.
(논문에서는 사용한다고 했다.)
channel은 input data의 채널을 뜻한다.
(앞서 확인해본 바 저자들이 사용한 채널은 9다)
self.stn은 위 모델 구조의 input transform을 나타낸다.
그리고 shared MLP를 conv1d로 구현한 것을 확인할 수 있다.
kernel size $1 \times 1$을 사용한 것을 shared MLP라 했다는 것을 알 수 있다.
또 모델 구조에 MLP가 (64,64)라 했는데 1 layer다.
(64,128,1024)도 2 layer다.
그리고 각 레이어에 BatchNorm이 적용된다는 것을 알 수 있다.
self.fstn은 feature transform라는 것을 알 수 있다.
모델이 실행될때 forward부분이다.
먼저 입력데이터에서 Batch size, Dimension, point Number(수)를 추출한다.
각각 B, D, N이다.
먼저 모델 구조처럼 input transform(self.stn)을 적용한다.
이를통해 $3 \times 3$ matrix를 얻는다.
이 행렬은 input transform이 얼마나 변형된것인지 추정 후 복구하는 행렬이다.
그다음 행렬 곱을 할 수 있게 x를 transpose한다.
이후 torch.bmm을 통해 input data에 아까 얻은 trans matrix를 곱한다.
그다음 다시 transpose로 원래 형태로 돌아간다.
이제 모델 구조처럼 shared MLP를 적용한다.
앞서 정의한 self.conv1을 활용하여 $n \times 64$로 바꾸고
self.bn1과 ReLU를 사용한다.
feature transform은 사용 여부를 결정할 수 있다.
마찬가지로 이를 사용하여 $64 \times 64$ matrix를 구하고
torch.bmm을 통해 행렬곱을 한다.
이 행렬도 transform이 얼마나 변형된것인지 추정 후 복구하는 행렬이다.
즉, 다양한 feature를 한가지 특정한 space에 매핑하는 것이다.
위의 과정이 모두 처리된 $n \times 64$ feature를 pointfeat이라고 한다.
이제 shared MLP를 2개 적용하면 된다.
self.conv2, self.bn2, self.conv3, self.bn3를 적용한다.
이제 symmetric func.인 max pooling을 하는데
torch.max를 사용하여 max값을 찾아낸다.
이제 return으로 global_feat true 여부에 따라
중에 하나를 출력한다.
feature와 trans, trans_feat을 return한다.
앞서 지나간 transform matrix를 구하는 코드는 다음과 같다.
먼저 Input transform은 3개의 shared MLP와 3개의 MLP이 있다.
각각 Conv1d와 Linear로 정의한다.
또 BatchNorm도 위와같이 정의한다.
먼저 conv1, bn1, conv2, bn2, conv3, bn3을 순서대로 ReLU와 함께 처리한다.
그 다음 max pooling을 적용하기 위해 torch.max를 사용한다.
이제 fc1, bn4, fc2, bn5를 ReLU와 함께 처리하고
fc3를 적용한다.
여기에 [1., 0., 0., 0., 1., 0., 0., 0., 1.] ($I$ 행렬)을 더한 다음
$3 \times 3$ 형태로 만들어 return한다.
(사실 왜 위의 벡터를 더해야 하는지 모르겠다.)
feature transform도 input transform과 똑같이 처리한다.
마지막으로 해당 파일에
를 나타내는 코드가 있는데 그것은 다음과 같다.
이 loss의 역할은 feature transform matrix $64 \times 64$를
matrix $I$ 꼴로 강제하는 loss다.
pointnet_sem_seg.py파일을 보면
PointNet의 head가 구현되어 있다.
self.k 는 해당 데이터의 class 수와 같다.
또 앞서 확인한 PointNetEncoder가 있다는 것을 알 수 있다.
또 shared MLP가 4개 있고 BatchNormalization도 3개 있다.
먼저 batchsize와 point수인 n_pts를 얻는다.
그리고 PointNetEncoder를 통해 $n \times 1088$을 얻는다.
그 다음 conv1, bn1, conv2, bn2, conv3, bn3을 순서대로 ReLU와 함께 처리한다.
또 conv4로 point를 처리한다.
그 후 transpose를 할 때 contiguous()를 사용하는데
이는 아마 F.log_softmax를 처리할때 발생하는 문제 때문이 아닌가 싶다.
contiguous에 대한 설명은 다음 블로그 참조
F.log_softmax를 처리하고 다시 view로 원래 형태로 바꿔준다.
또 아래에 loss를 정의하는 클래스가 있다.
먼저 F.nll_loss를 적용하는데,
이것은 cross_entropy는 내부에 softmax를 내장하고 있기 때문에
nll loss를 사용한다. (역할은 동일)
또 앞서 확인한 식(4) loss를 정의한것을 확인할 수 있다.
둘 loss의 비율은 1 : 0.001이다.
이렇게 PointNet의 구체적인 구조를 코드를 통해 확인하였다.