본문 바로가기
AIML 분야/Segmentation

panoptic-deeplab 코드 리뷰

by 포숑은 맛있어 2021. 2. 4.
반응형

github.com/bowenc0221/panoptic-deeplab

 

bowenc0221/panoptic-deeplab

This is Pytorch re-implementation of our CVPR 2020 paper "Panoptic-DeepLab: A Simple, Strong, and Fast Baseline for Bottom-Up Panoptic Segmentation" (https://arxiv.org/abs/1911.10194) - b...

github.com

 

이 코드를 돌릴거고, COCO dataset에서 시도하고 있다.

 

 

시작하기에 앞서, 데이터셋을 다운받은 후 이 코드에서 말하는 규칙대로 데이터셋 path를 설정해준다.

내 경우에는 데이터셋이 저장된 서버에 sudo권한이 없어서 디렉토리를 못 옮기는 관계로 코드를 조금 바꿔야했다.

 

시도 과정 적어보기

  1. COCO 데이터셋 다운 : 이미 있어서 패스
  2. 자신이 원하는 셋팅으로 config yaml 파일 작성하기
    => 그냥 다른 파일 참고해서 적당히 적어줬다. ResNet 백본 썼다.
  3. json converting 필요
    prepare_coco_panoptic_trainid.py를 실행한다. dataset_dir에 적절하게 위치를 넣어준다.
    이 파일을 실행하면 [1, 200]의 id를 땡겨서 => [0,132] 이렇게 총 133개의 class를 학습하기 좋게 맵핑해준다.
    [1, 200]이지만 중간에 비는 레이블이 있기 때문. (COCO dataset 참고하기)
  4. 데이터는 읽어지는 것 같은데, collate 어쩌구 에러가 뜬다.
    => build.py에 build_train_loader_from_cfg()에 가서 데이터로더에 collate 설정 변경.
  5. 'str obj cannot be interpreted as an int' 에러 발생.
    여기서부터 난감해서 해결을 못하고 있다...

5번에 적은 에러 내용을 쓰려고 이 글을 남기고 있다.

지금 image = data.pop('image')에서 저 에러가 뜨는 이유는, data가 dict{}의 리스트로 되어있기 때문이다.

 

~해결 전 주저리~

input 이미지만 필요하다면 그냥 간단하게 해결할 수 있을 것이다.

tmp = [d['image] for d in data]

image = torch.stack(tmp, dim=0) 이렇게 하면 원래 들어가야할 형태로 들어갈테니까.

 

그런데 이거 말고도 다 바꿔야하는 것 같은데, 그러기엔 코드가 많이 드러워질 것 같다.

왜 이런 문제가 일어났나 대충 생각해보면 distributed 처리 때문인 것 같다.

 

world_size, worker 이런 것들 바꾸면 알아서 dict{}에 제대로 들어가려나?

설정을 어떻게 해야할지를 모르겠다. 이 코드 개발자보다도 원래 model zoo에서 그렇게 되어있는 것 같은데. (역시 model zoo를 안써봤고)

나는 single machine에 multi gpu 환경인데, 그러면 distributed여야하는게 맞나? world size가 1인게 맞는건가?

 

잘 모르겠어서 world size, rank, master addr, master port 이런거 설정 해줬는데 좀 이상하다...

 

model zoo 분산처리 부분 문서를 좀 찾아봐야겠다.

 

 

물론 이런 데이터 문제를 해결해도 모델 사이즈같은거 못맞춰서 세세한 문제가 더 발생할 수 있을 것 같기도 하다.

빡치므로 여기까지 하고 논문을 읽으러 가야겠다. 휴.

 

 

[2021.02.08]

위에서 말한대로, 1. 논문읽기 2. multiGPU와 distributed처리에 대해 조금 알아보기를 끝냈으며, 실험을 정상적으로 돌릴 수 있었다.

 

~ 해결 했다~

왜인지는 모르겠는데 multiGPU가 탐지되면 그냥 distributed 처리해버린다.

난 single machine에 멀티GPU인거니까 자꾸 이상한 에러가 났던 것.

따라서 그 부분 코드를 수정해야하는데, 그냥 귀찮으니 우선 single GPU로 돌려줬다.

 

 

그래서 결국 그냥 single GPU 환경에서 돌렸다.

dictionary 그 이상한것만 하나로 합쳐서 대충 넣으니 학습 제대로 되는걸 확인할 수 있었다.

 

아직 iteration을 몇개 안 돌렸기 때문에 그리 성능이 좋진 않다.

 

인풋

 

predicted
GT


 

코드를 적당히 읽어보려고 한다.

읽고 까먹을 것 같아서 읽으면서 쓰는 글이다.

 

 

나는 학습을 이렇게 실행했다.

그러므로 우선 train_net.py를 보자.

 

[train 명령어]

 

GPUS=0,1,2,3

CUDA_VISIBLE_DEVICES=$GPUS python tools/train_net.py --cfg configs/pan_coco.yaml >> logs.txt

 

 

 

[train_net.py의 일부]

 

image = data.pop('image') 
out_dict = model(image, data)
loss = out_dict['loss']
optimizer.zero_grad()
loss.backward()
optimizer.step()
lr = optimizer.param_groups[best_param_group_id]["lr"]
lr_scheduler.step()

 

 

여기를 보면 모델 인풋은 image, data 이렇게 두가지이다.

image는 dictionary에서 pop해온 (배치단위의) 이미지 tensor이다.

data는 (배치단위의) dictionary이고, 안에 11가지 데이터가 들어있다.

  • tensor type
    'offset_weights', 'center_weights', 'semantic_weights', 'offset', 'center', 'foreground', 'semantic', 'image'
  • 2D list type
    'center_points'
  • Array type
    'raw_size',
    'size'

loss는 model forward()의 output에서 'loss'만 빼온 것이다. 딕셔너리형인가보다.

그러면 각 코드를 보자.

 

 

Model

 

[segmentation/model/build.py]

build_segmentation_model_from_cfg() 함수에서는 config 정보를 받아서 모델을 뱉어준다.

나는 backbone은 resnet, 메타 아키텍쳐는 panoptic_deeplab으로 설정했기 때문에 각각을 볼 예정이다.

코드에서는 backbone으로는 resnet 이외에도 mobilenet v2, mnasnet, xception 등을 지원하며,

메타 또한 panoptic deeplab 뿐만 아니라 deeplab v3, deeplab v3 plus가 지원된다.

 

[segmentation/model/meta_arch/panoptic_deeplab.py, base.py]

PanopticDeeplab 클래스에는 forward 함수가 안보이고, BaseSegmentationModel 클래스를 상속받는다. 아마 여기 정의되어있겠지.

 

base.py를 확인해보자.

 

    def forward(self, x, targets=None):
        input_shape = x.shape[-2:]

        # contract: features is a dict of tensors
        features = self.backbone(x)
        pred = self.decoder(features)
        results = self._upsample_predictions(pred, input_shape)

        if targets is None:
            return results
        else:
            return self.loss(results, targets)

 

백본에서 encoded feature를 가져오고,

이걸 decoder에서 디코딩하고,

디코더에서 얻은 결과를 가지고 _upsample_predictions()을 거쳐 최종 결과를 얻는다.

 

 

encoder는 그냥 모두가 아는 resnet이다. 마지막 레이어 피쳐 그대로 뱉어준다.

segmentation/mode/backbone 폴더를 확인하자.

 

 

decoder의 경우, panoptic deeplab decoder를 사용하도록 구성되어있다.

segmentation/mode/decoder/panoptic_deeplab.py 를 확인하자.

논문 그림 그대로이다. semantic decoder와 head, instance decoder와 head로 구성되어있다.

같은 구조의 모듈 쓴다. 파라미터는 따로.

 

각각의 head에서 output은 pred[key]를 각각 계산하는데, instance의 경우 여기서 key는 'center', 'offset'이 되겠다.

시맨틱 브랜치면 semantic 하나. 그래서 같은 클래스 모듈을 쓸 수 있다.

 

논문 읽었던건 ambitious-posong.tistory.com/90 여기에.

 

 

_upsample_predictions()를 보자.

이 함수는 다시 panoptic_deeplab.py에 정의되어있다.

 

    def _upsample_predictions(self, pred, input_shape):
        """Upsamples final prediction, with special handling to offset.
            Args:
                pred (dict): stores all output of the segmentation model.
                input_shape (tuple): spatial resolution of the desired shape.
            Returns:
                result (OrderedDict): upsampled dictionary.
            """
        # Override upsample method to correctly handle `offset`
        result = OrderedDict()
        for key in pred.keys():
            out = F.interpolate(pred[key], size=input_shape, mode='bilinear', align_corners=True)
            if 'offset' in key:
                scale = (input_shape[0] - 1) // (pred[key].shape[2] - 1)
                out *= scale
            result[key] = out
        return result

디코더의 모든 output에 대해서 bilinear interpolate 한다음에, offset이 있다면 offset을 가지고 scale을 구하여 곱해주고 결과로 뱉는다.

 

 

 

 

Loss

 

model forward()의 output은 원래 이 결과인데, 만약에 target이 같이 들어왔으면 loss를 계산한다. 학습해야하니까.

loss를 보자.

 

base.py에는 구현되어있지 않으며, 이를 상속하는 panoptic segmentation 알고리즘 각각에서 구현하도록 되어있다.

따라서 panoptic_deeplab.py의 loss() 함수를 보자.

총 3가지 로스를 더하는 코드로 되어있다.

 

            if 'semantic_weights' in targets.keys():
                semantic_loss = self.semantic_loss(
                    results['semantic'], targets['semantic'], semantic_weights=targets['semantic_weights']
                ) * self.semantic_loss_weight
            else:
                semantic_loss = self.semantic_loss(
                    results['semantic'], targets['semantic']) * self.semantic_loss_weight
            self.loss_meter_dict['Semantic loss'].update(semantic_loss.detach().cpu().item(), batch_size)
            loss += semantic_loss
            
            
            if self.center_loss is not None:
                # Pixel-wise loss weight
                center_loss_weights = targets['center_weights'][:, None, :, :].expand_as(results['center'])
                center_loss = self.center_loss(results['center'], targets['center']) * center_loss_weights
                # safe division
                if center_loss_weights.sum() > 0:
                    center_loss = center_loss.sum() / center_loss_weights.sum() * self.center_loss_weight
                else:
                    center_loss = center_loss.sum() * 0
                self.loss_meter_dict['Center loss'].update(center_loss.detach().cpu().item(), batch_size)
                loss += center_loss
                
                
            if self.offset_loss is not None:
                # Pixel-wise loss weight
                offset_loss_weights = targets['offset_weights'][:, None, :, :].expand_as(results['offset'])
                offset_loss = self.offset_loss(results['offset'], targets['offset']) * offset_loss_weights
                # safe division
                if offset_loss_weights.sum() > 0:
                    offset_loss = offset_loss.sum() / offset_loss_weights.sum() * self.offset_loss_weight
                else:
                    offset_loss = offset_loss.sum() * 0
                self.loss_meter_dict['Offset loss'].update(offset_loss.detach().cpu().item(), batch_size)
                loss += offset_loss

 

 

self.어쩌구loss로 정의된 것들은 PanopticDeeplab 클래스를 init할때 정해지며, 이건 각자 작성한 config 파일에 따른다.

내 경우에는 yaml 파일에서 아래와 같이 정의했었다.

 

LOSS:
  SEMANTIC:
    NAME: "hard_pixel_mining"
    IGNORE: 255
    TOP_K_PERCENT: 0.2
    WEIGHT: 1.0
  CENTER:
    NAME: "mse"
    WEIGHT: 200.0
  OFFSET:
    NAME: "l1"
    WEIGHT: 0.01

 

 

이런 설정에 맞춰서 아까 segmentation/model/build.py 에서 로스 함수를 할당해준다.

이 loss 각각 또한 segmentation/model/loss 폴더 내부에 각각 정의되어있을테니 궁금한 것은 따로 확인하면 될 것 같다.

 

저번에 논문리뷰할때 봤던대로다.

semantic segmentation에 대한 로스, center point에 대해서 (GT에 가우시안 씌운거랑) MSE loss, 그리고 모든 픽셀에서 본인이 할당된 인스턴스 center와의 offset에 대한 L1 로스.

 

def build_loss_from_cfg(config):
    """Builds loss function with specific configuration.
    Args:
        config: the configuration.

    Returns:
        A nn.Module loss.
    """
    if config.NAME == 'cross_entropy':
        # return CrossEntropyLoss(ignore_index=config.IGNORE, reduction='mean')
        return RegularCE(ignore_label=config.IGNORE)
    elif config.NAME == 'ohem':
        return OhemCE(ignore_label=config.IGNORE, threshold=config.THRESHOLD, min_kept=config.MIN_KEPT)
    elif config.NAME == 'hard_pixel_mining':
        return DeepLabCE(ignore_label=config.IGNORE, top_k_percent_pixels=config.TOP_K_PERCENT)
    elif config.NAME == 'mse':
        return MSELoss(reduction=config.REDUCTION)
    elif config.NAME == 'l1':
        return L1Loss(reduction=config.REDUCTION)
    else:
        raise ValueError('Unknown loss type: {}'.format(config.NAME))

 

 

이정도면 모델 코드는 대략 이해할 수 있을 것 같다.

 


모델 뿐만 아니라 데이터 처리에 대해서도 의문이 든다.

ambitious-posong.tistory.com/91 일단 COCO panoptic dataset의 경우는 저번에 잠깐 봤었다.

 

 

위에서도 언급했듯이 dataloader에서 읽어오는 data 하나의 구조는 이렇다.

offset, center, semantic과 이것의 weights를 각각 뱉어준다. 그리고 image와 foreground 텐서도.

거기다가 center point 좌표와 이미지 사이즈에 대한 정보도 저장되어있다.

 

이중에서 이미지와 사이즈 빼고 나머지 8가지는 segmentation/data/transforms/target_transforms.py/PanopticTargetGenerator 에서 만든다.

  • Tensor
    'offset_weights', 'center_weights', 'semantic_weights', 'offset', 'center', 'semantic', 'image', 'foreground'
  • 2D list type
    'center_points'
  • Array type
    'raw_size', 
    'size'

 

이미지와 시맨틱은 아마 인풋 이미지랑 판옵틱 이미지 그냥 불러왔지 않았나 싶다.

 

forground의 경우는 thing_list에 있는 category_id의 경우에 1로 표기한다.

 

 

가장 궁금한건 center 정보이다.

center annotation이 COCO에서 제공되지는 않으니 직접 만들었을 것이다.

=> center annotation의 경우, panoptic image를 불러와서 만들어준다.

segmentation/data/transforms/target_transforms.py 를 참고하자.

 

대략 보니까 category_id가 그 133개의 아이디이고, id는 색상값에다가 수식으로 처리한 바로 그 panoptic id.

center point의 경우,

    center_y, center_x = np.mean(mask_index[0]), np.mean(mask_index[1])

이렇게 되어있는거 봐서 그냥 이미지를 읽어온 후 해당 클래스인 픽셀들의 평균 좌표인 것 같다. 계산한 것. 난 또 bbox가지고 center값 계산했나 했는데 그건 아니었다.

 

이 center point 좌표를 가지고 gaussian 사용해서 heatmap을 만든 게 'center'이다. (1, H, W) 형태.

그리고 'offset' 또한 center point를 가지고 만든다. center에서 자신의 좌표를 뺀 것. 따라서 (2, H, W) 형태.

 

 

그러면 각각의 weights는 뭘까.

semantic weights의 경우, 나중에 pixel-wise loss를 적용할때 로스를 발생시키지 않을 곳을 마킹한다.

따라서, 먼저 1로 init한 후에 무시할 region 조건 3가지에 부합하는 게 있다면 0으로 바꿔준다. (and 아니고 or 임.)

무시할 region에 대한 조건은 'ignore_label'이거나, 'iscrowd'=1로 레이블이 되어있거나, (optional) stuff class의 경우이다.

center, offset도 각각의 loss가 있으니 이와 같은 의도.

 

 

foreground, 시맨틱, 각종 weights 모두 (H, W)이다.


 

음. 그러면 데이터셋을 커스텀할때,

  • id는 그대로 사용. 컬러링 방법은 그냥 coco와 동일하게 하기.
  • category id는 커스텀 데이터셋에 맞게 결정하기.
    그리고 thing/stuff로 분짓기.
  • 뭔가 복잡한데, 그냥 stuff/thing만 구분하고 'iscrowd' 어노테이션도 버려도 괜찮지 않나 싶다.
    그런데 배경의 맥락에 따라 또 달라지는게 있다면 이에 대한 어노테이션도 필요하다.
    어디서 center, offset, semantic loss를 발생시킬지 의논해봐야겠다.
  • area, bbox annotation은 파일에는 있으나 사용하지 않는다. 굳이 만들필요 없는 데이터.
  • 따라서, json file에는 id (색상), category id (클래스 id)만 저장해주면 될 것 같다. 이미지 어노테이션과 함께.
    iscrowd도 만들어야하나.

 

반응형

댓글