NIPS 2017
PointNet++: 메트릭 공간에서의 점 집합에 대한 깊은 계층적 특성 학습
Charles R. Qi · Li Yi · Hao Su · Leonidas J. Guibas — Stanford University
ModelNet40 Acc91.9% (vs PointNet 89.2%)
핵심 아이디어계층적 집합 추상화
밀도 처리MSG / MRG
작업분류 & 분할
계층적 집합 추상화 — N → N1 → N2 → 1 SA 계층 1 FPS → N1 중심점 구 쿼리 r₁ PointNet MLP N×3 → N1×d1 SA 계층 2 FPS → N2 중심점 구 쿼리 r₂ > r₁ PointNet MLP N1×d1 → N2×d2 SA 계층 3 Global PointNet 모든 N2 점을 그룹으로 MLP + Max Pool N2×d2 → 1×d3 FC 계층 → k개 클래스 특성 전파 (분할용) N 점 N1 점 N2 점 1 벡터
PointNet++ Hierarchical Architecture

1. PointNet의 문제점

§01

PointNet은 global max-pool을 사용하여 N개의 모든 점을 단일 1024차원 벡터로 집계합니다. 이는 의자와 비행기를 구분하는 데는 효과적이지만 모든 점을 독립적으로 처리하고 모든 국소적인 기하학적 구조를 버립니다. 전체 점의 분포가 비슷하면 평면 표면과 곡면을 구분할 수 없습니다.

더 나쁜 점은 실제 LiDAR 스캔은 불균일한 밀도로 고통받습니다. 센서 근처에는 점이 조밀하게 모여 있고 먼 곳에는 희소합니다. 균일한 샘플로 훈련된 네트워크는 밀도 변화에 대응할 메커니즘이 없기 때문에 밀도 변화 상황에서 일반화가 잘 되지 않습니다.

  • 국소적 구조 없음 — N개의 모든 점에 대한 max pool은 이웃 관계를 완전히 무시하여 세부적인 모양(나사, 경첩)이 보이지 않음.
  • 밀도 무시 — 고정된 global 집계는 밀도가 높은 근거리 영역과 희소한 원거리 영역에 동등한 가중치를 부여함.
  • 계층 구조 없음 — 2D CNN은 점진적으로 수용 필드를 구축(가장자리 → 텍스처 → 부분 → 객체)하지만 PointNet은 한 단계에서 바로 global로 점프함.

2. 집합 추상화 계층

§02

PointNet++는 집합 추상화(SA) 계층을 도입하는데, 이는 점 집합을 다운샘플링하면서 풍부한 국소 특성을 인코딩합니다. 각 SA 계층은 세 가지 부분 연산을 수행합니다:

  • 최원점 샘플링(FPS) — 이미 선택된 모든 중심점으로부터 가장 먼 점을 반복적으로 선택합니다. 점 구름을 최대한 커버하는 M개의 중심점을 생성합니다. 무작위 샘플링보다 더 균등합니다.
  • 구 쿼리(Ball Query) — 각 중심점에 대해 반경 r 내의 모든 점을 수집합니다. K개 이웃으로 제한됩니다. 국소 이웃은 평행이동 불변이며 밀도 적응적입니다.
  • PointNet 미니 네트워크 — K개 이웃의 각 국소 그룹에 작은 PointNet(공유 MLP + max pool)을 적용합니다. 중심점당 d차원 특성 벡터 하나를 출력합니다. 결과: N개 점 → M개 점 및 더 풍부한 특성.
계층적 집합 추상화 — N → N1 → N2 → 1 SA 계층 1 FPS → N1 중심점 구 쿼리 r₁ PointNet MLP N×3 → N1×d1 SA 계층 2 FPS → N2 중심점 구 쿼리 r₂ > r₁ PointNet MLP N1×d1 → N2×d2 SA 계층 3 Global PointNet 모든 N2 점을 그룹으로 MLP + Max Pool N2×d2 → 1×d3 FC 계층 → k개 클래스 특성 전파 (분할용) N 점 N1 점 N2 점 1 벡터
3단계 집합 추상화 계층구조: 각 SA 계층은 FPS를 통해 다운샘플링, 구 쿼리를 통해 그룹화, PointNet 미니 네트워크로 인코딩

3. MSG — 다중 스케일 그룹화

§03

불균일한 밀도는 고정된 단일 반경을 깨뜨립니다. 희소 영역에서 작은 반경은 이웃이 없고, 밀도 높은 영역에서 큰 반경은 너무 많은 이웃이 생겨 국소 해상도가 부족합니다. MSG는 여러 반경에서 병렬로 여러 구 쿼리를 실행하고 결과 특성 벡터를 연결합니다.

중심점 p 구 r=0.1 → PointNet 구 r=0.2 → PointNet 구 r=0.4 → PointNet 연결 d1+d2+d3 MSG 특성 다중 스케일 인식 SSG는 한 가지 반경만 사용; MSG는 세 가지 모두 병렬로 실행
MSG: 서로 다른 반경의 세 가지 구 쿼리, 각각 자체 PointNet 부분 네트워크; 전파 전 특성 연결

MRG(다중 해상도 그룹화)는 계산상 더 저렴한 대안입니다: 같은 SA 계층에서 여러 반径을 사용하는 대신, 계층 구조의 연속된 두 수준에서 특성을 연결합니다 — 하나는 원본 국소 점에서 계산되고, 하나는 더 거친 수준에서 전파됩니다. 국소 점 집합이 밀도가 높으면 원본 점 분기가 지배적이고, 희소하면 더 거친 특성이 대신합니다. MRG는 같은 수준에서 여러 PointNet을 실행하는 비용을 피합니다.

4. 분할을 위한 특성 전파

§04

분류는 최상위 global 벡터만 필요하지만 점별 분할은 모든 원본 점에서 특성이 필요합니다. PointNet++는 U-Net의 디코더와 유사하게 보간 + 스킵 연결을 사용하여 계층 구조를 따라 특성을 역전파합니다.

더 세밀한 수준의 각 점에 대해, 특성은 더 거친 수준의 k개 최근접 이웃의 역거리 가중치 평균으로 계산됩니다:

f(x) = Σ w_i · f(x_i) / Σ w_i,    w_i = 1 / dist(x, x_i)^p

보간된 특성은 인코더의 같은 수준에서 스킵 연결 특성과 연결된 후 단위 PointNet(점별 MLP)을 통과하여 정제됩니다. 여러 FP 계층을 쌓으면 전체 N점 해상도를 복원합니다.

5. PyTorch 구현

§05
pointnet2_sa.py — 구 쿼리 그룹화를 이용한 집합 추상화 계층
import torch
import torch.nn as nn


def farthest_point_sample(xyz, npoint):
    # 반복적 최원점 샘플링. xyz: B×N×3, B×npoint 인덱스 반환.
    B, N, _ = xyz.shape
    centroids = torch.zeros(B, npoint, dtype=torch.long, device=xyz.device)
    distance  = torch.full((B, N), 1e10, device=xyz.device)
    farthest  = torch.randint(0, N, (B,), device=xyz.device)
    for i in range(npoint):
        centroids[:, i] = farthest
        centroid = xyz[torch.arange(B), farthest].unsqueeze(1)   # B×1×3
        dist = ((xyz - centroid) ** 2).sum(-1)                    # B×N
        distance = torch.min(distance, dist)
        farthest = distance.argmax(-1)
    return centroids


def ball_query(radius, nsample, xyz, new_xyz):
    # 각 중심점에 대해 반경 내의 nsample 이웃 찾기.
    B, N, _ = xyz.shape
    M = new_xyz.shape[1]
    # 쌍별 거리 B×M×N
    dist = ((new_xyz.unsqueeze(2) - xyz.unsqueeze(1)) ** 2).sum(-1)
    # 반경 외부의 점을 마스크; 정렬 후 처음 nsample개 선택
    idx = dist.argsort(-1)[..., :nsample]                         # B×M×nsample
    group_mask = dist.gather(-1, idx) > radius ** 2
    idx[group_mask] = idx[:, :, :1].expand_as(idx)[group_mask]   # 중심점으로 패딩
    return idx                                                     # B×M×nsample


class SetAbstraction(nn.Module):
    # 단일 집합 추상화 계층 (SSG 변형).

    def __init__(self, npoint, radius, nsample, in_ch, mlp_channels):
        super().__init__()
        self.npoint   = npoint
        self.radius   = radius
        self.nsample  = nsample
        layers = []
        last = in_ch + 3          # 상대 xyz 포함
        for out_ch in mlp_channels:
            layers += [nn.Conv2d(last, out_ch, 1),
                       nn.BatchNorm2d(out_ch), nn.ReLU()]
            last = out_ch
        self.mlp = nn.Sequential(*layers)
        self.out_ch = last

    def forward(self, xyz, features=None):
        # xyz: B×N×3, features: B×N×C or None → new_xyz: B×M×3, new_feat: B×M×out_ch
        B, N, _ = xyz.shape
        # 1. FPS
        idx      = farthest_point_sample(xyz, self.npoint)         # B×M
        new_xyz  = xyz[torch.arange(B).unsqueeze(1), idx]          # B×M×3
        # 2. 구 쿼리
        grp_idx  = ball_query(self.radius, self.nsample, xyz, new_xyz)  # B×M×K
        grp_xyz  = xyz[torch.arange(B).view(B,1,1).expand_as(grp_idx),
                       grp_idx]                                     # B×M×K×3
        grp_xyz -= new_xyz.unsqueeze(2)                             # 상대 좌표
        if features is not None:
            grp_feat = features[torch.arange(B).view(B,1,1).expand_as(grp_idx),
                                grp_idx]                            # B×M×K×C
            grp_in = torch.cat([grp_xyz, grp_feat], dim=-1)        # B×M×K×(3+C)
        else:
            grp_in = grp_xyz                                        # B×M×K×3
        # 3. PointNet 미니 네트워크
        grp_in  = grp_in.permute(0, 3, 2, 1)                      # B×(3+C)×K×M
        grp_out = self.mlp(grp_in)                                  # B×out_ch×K×M
        new_feat = grp_out.max(dim=2)[0].permute(0, 2, 1)         # B×M×out_ch
        return new_xyz, new_feat


# 빠른 테스트
if __name__ == "__main__":
    xyz  = torch.randn(4, 1024, 3)
    sa1  = SetAbstraction(npoint=512, radius=0.2, nsample=32,
                           in_ch=0, mlp_channels=[64, 64, 128])
    sa2  = SetAbstraction(npoint=128, radius=0.4, nsample=64,
                           in_ch=128, mlp_channels=[128, 128, 256])
    xyz1, f1 = sa1(xyz)
    xyz2, f2 = sa2(xyz1, f1)
    print(xyz1.shape, f1.shape)    # torch.Size([4, 512, 3]) torch.Size([4, 512, 128])
    print(xyz2.shape, f2.shape)    # torch.Size([4, 128, 3]) torch.Size([4, 128, 256])

6. 결과

§06
작업데이터셋방법지표점수
분류ModelNet40PointNet정확도89.2%
분류ModelNet40PointNet++ (MSG)정확도91.9%
부분 분할ShapeNet PartPointNetmIoU83.7%
부분 분할ShapeNet PartPointNet++mIoU85.1%
의미론적 분할S3DISPointNetmIoU47.6%
의미론적 분할S3DISPointNet++mIoU54.5%

7. 해설

§07

PointNet++는 PointNet의 자연스러운 후속작입니다. CNN이 합성곱 구조에서 "무료로" 얻는 계층적 국소-대역 처리를 추가합니다. 집합 추상화 계층은 개념적으로 3D 합성곱 계층의 아날로그입니다 — 중심점을 샘플링, 국소 이웃을 풀, 더 거친 표현을 출력 — 하지만 불규칙한 점 집합에 맞게 조정되었습니다.

MSG 밀도 처리는 우아하지만 비쌉니다: 각 SA 계층에서 K개의 독립적인 PointNet 부분 네트워크를 실행합니다. 실제로 많은 최신 아키텍처(PointConv, KPConv, VoteNet)는 FPS + 구 쿼리 골격을 차용하지만 내부 PointNet을 더 저렴한 연산으로 대체합니다.

유산: 거의 모든 후속 3D 탐지 또는 분할 네트워크(PointRCNN, VoteNet, 3DETR)는 PointNet++을 백본으로 사용합니다. FPS와 구 쿼리는 3D 심층 학습 라이브러리(PyTorch3D, MinkowskiEngine)의 표준 기본 단위가 되었습니다.