본문 바로가기
3D point clouds/Classification

PointNet pytorch 리뷰

by khslab 2023. 6. 23.

 

 

많은 논문이 그렇겠지만 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 형태다.

x, y, z, r, g, b

각 장면당 1000만개의 point가 있다.

 

 해당 코드의 전처리 코드를 실행하면 다음과 같다.

python collect_indoor3d_data.py

0~5열은 data고 6열은 label이다.

위와같은 형식으로 .npy 파일을 만들어 전처리 한다.


S3DISDataLoader.py파일을 보면

S3DISDataset에서 먼저 data split 인자와 데이터 위치, point 수, test로 사용할 장소 등이 있다.

S3DIS는 각 장면마다 data수가 다르기 때문에 입력 데이터 $N$의 수를 4096으로 정했다.

위 코드를 통해 rooms_split에 room(장소) 리스트를 만든다.

그다음 self.room_pointsself.room_label을 분할아여 저장할 리스트를 만든다.

또 room좌표의 최대 최소값을 저장할 리스트를 만든다.

num_point_all은 각 장면의 point수를 저장하는 리스트다.

labelweights는 각 클래스에 해당하는 point수를 확인하기 위함이다.

for문에서 각 장면의 data_path를 꺼낸 뒤 load한다.

이때 room_data는 주석처럼 xyzrgbl이다.

이것을 pointslabels로 분할한다.

이때 label을 통하여 히스토그램을 뽑고 이것을 tmp에 저장한다.

또 각 point의 x,y,z좌표의 최대 최소값을

coord_min, coord_max에 저장한다.

이전에 만든 room_points room_label

room_coord_minroom_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_probnum_iter를 고려하여 구한다.

이것을 numpy 배열로 만든게 self.room_idxs다.

따라서 train/test 셋에 샘플이 몇개 있는지 출력한다.

Totally 47623 samples in train set.

__getitem__은 데이터를 꺼내는 부분이다.

dataloader가 idx를 쏴주면 거기에 해당하는 데이터를 출력하면 된다.

idx를 받았을 때, room_idxpointslabels를 정하고

해당 장면의 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이 있다.

각각 Conv1dLinear로 정의한다.

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의 구체적인 구조를 코드를 통해 확인하였다.