12일 안에 세그멘테이션 따라잡기

2021 NIPA Hair-Segmentation 대회 1위 후기

Do not index
Do not index
안녕하세요. 오토피디아 리서치팀에서 일하고 있는 Bo입니다.
오토피디아에서는 운전자 분들이 겪는 다양한 차량 문제의 해결을 위해 닥터차 앱을 운영하고 있는데요. 리서치팀에서는 데이터 웨어하우스 구축, 사내 업무 자동화, 검색 엔진과 같은 머신러닝 프로젝트들을 통해 위와 같은 비전을 달성하는데 기여하고 있습니다. 최근에는 타이어 사진을 찍으면 마모 정도와 수명을 알려주고, 필요한 경우 구매와 장착점 예약까지 손쉽게 할 수 있는 닥터트레드 서비스도 오픈하였으니 많은 관심 부탁드려요!
때는 2021년 작년 초여름, 리서치팀은 세그멘테이션 기반의 차량 부위와, 파손 영역을 인식하는 모델 개발을 앞두고 있었는데요. 마침 NIPA 2021 인공지능 경진대회에서 한국인 헤어스타일 세그멘테이션 문제가 12일 동안 출제된다는 소식을 듣고, 대회 참여를 통해 스파르타 식으로 세그멘테이션 문제의 숙련도를 올릴 수 있을 것 같아 Kevin Jo와 함께 둘이서 참가해 보았습니다.
입력 이미지와 Segmentation GT를 함께 시각화(1열). 대회 측에서 제공된 GT(2열). 후처리가 덜 된 모델 출력(3열). GT-Prediction 에러 시각화(4열)
입력 이미지와 Segmentation GT를 함께 시각화(1열). 대회 측에서 제공된 GT(2열). 후처리가 덜 된 모델 출력(3열). GT-Prediction 에러 시각화(4열)
입력 이미지와 Segmentation GT를 함께 시각화(1열). 대회 측에서 제공된 GT(2열). 후처리가 덜 된 모델 출력(3열). GT-Prediction 에러 시각화(4열)
이번 글에서는 경진대회에 참여하여 최종 1위로 입상하기까지, 12일간의 과정을 간단히 정리해보았습니다. 두 명 모두 Semantic Segmentation 튜토리얼 정도만 돌려보았던터라 최신 모델 Catch-up부터 각종 잔 테크닉들을 대회 기간 내에 밀도 있게 습득하고 대회 문제에 맞게 적용해야 했습니다. 단기간에 빠르게 성능 지표를 올리기 위해 신경썼던 점과 배웠던 점을 소개드립니다.
 

문제 정의

이번에 참여했던 트랙은 주어진 이미지의 각 픽셀이 머리카락/배경 중 어느 클래스에 속하는지를 분류하는 pixel-level binary classification 혹은 1-class segmentation 문제로 512x512 해상도를 갖는 총 196,058개의 학습 데이터, 12,280개의 테스트 데이터로 구성되어 있었습니다. 또한 재현 과정에서의 자원 공평성을 위해 학습은 single-V100 GPU 환경에서 36시간 이내, 추론은 3시간 이내라는 제한 조건을 가집니다.
이 때 한가지 특이사항은 답안을 제출할 때 답안 파일의 용량을 줄이기 위해 각 이미지마다 단일 polygon만 제출을 허용했습니다. 즉, 여러 개의 머리 영역이 검출되더라도 어쨌든 한붓 그리기가 가능한 형태로 후처리를 거쳐 제출해야 했으며 이 규정은 저희 팀으로 하여금 복수 개의 polygon을 어쩔 수 없이 하나로 병합하는 후처리 알고리즘을 개발하게 만들었습니다. (이해는 되지만 상당히 특이하다고 느꼈습니다)

데이터셋 체크

그리고 머리카락의 영역을 육안으로 확인하였을 때 거의 모든 샘플에 대해서 충분히 큼지막한 면적을 가지고 있었기 때문에 class imbalance가 여타 데이터셋에 비해 상대적으로 덜 심각하다고 느꼈고, 사람이 머리카락 영역을 충분히 쉽게 판별할 수 있는 만큼 기본 성능 자체가 높게 나오고 누가누가 더 디테일한 경계를 잘 살려낼 수 있는지의 싸움이 될 것 같다는 느낌이 들었습니다.
  • 머리카락을 염색한 케이스들이 있어 색이 비교적 다양함
  • 남성 샘플의 경우 헤어라인(경계면)이 상당히 심플하나 여성 샘플의 경우 펌을 한 경우 머리 끝 부분의 헤어라인이 상대적으로 복잡했고 제공된 데이터의 라벨이 이를 충분히 반영하지 못한 경우도 많음
  • 머리카락 영역의 면적이 대체로 비슷비슷하여 다양한 사이즈에 대한 어려움은 상대적으로 적다고 판단하였습니다

데이터셋 전처리

대회 측에서 제공해준 레퍼런스 코드 상에서는 Polygon 정보로부터 on-the-fly 방식으로 mask 맵을 생성하고 있었습니다. 테스트 결과 크게 병목은 아니었지만 서버 상에서 학습을 돌리면서 결과 분석용으로 CPU 자원을 사용하고 있었으므로 Mask 맵을 미리 생성하여 npy로 저장하기로 결정하였습니다. 이때 알게 된 점은 numpy.dtype을 np.bool 을 지정하고 np.save 로 저장하더라도 255KB에 달하는 만족스럽지 못한 용량을 가지며 이를 208,338개의 이미지의 Mask 맵 생성에 적용하면 52GB에 육박하여 5.8기가의 이미지 용량에 비해 배보다 배꼽이 큰 상황이 발생합니다.
다행히 스택 오버 플로우에 참고할만한 코드가 있었고 아래와 같은 코드로 Mask 데이터를 bit-string으로 변환하여 저장할 경우 Mask 1장당 용량을 33KB로 줄여 전체 용량이 6.8GB 정도로 크게 줄였습니다. 물론 jpg로 압축 저장하거나 bit-string도 다시 한번 압축 저장했다면 용량이 더 줄었겠지만 이미 데이터 로더의 iteration 과정에서 큰 병목으로 작용하지 않는 것 같아 용량 압축은 이 정도 선에서 끝냈습니다.

Augumentation, Model, Trainer 세팅

본격적인 Baseline을 선택하기에 앞서 이번 문제는 단순 pixel-level binary classification Task이며, ImageNet과 같은 벤치마크용 데이터셋 등에 비해 난이도가 낮다고 생각했기 때문에 Transformer 계열의 Encoder나 HRnet-OCR과 같은 최신 방법론들을 쓰지 않아도 충분한 성능에 도달할 수 있을 것이라 생각하였습니다.
그리고 이번 문제의 경우는 성능 향상에 주요한 포인트가 모델 아키텍처 보다는 다양한 학습방법론의 적용과 하이퍼파라미터 튜닝, 그리고 Post-processing에 있다고 생각하여 빠르게 다양한 실험을 할 수 있도록 개발용이성이 높은 segmentation_models_pytorch 라이브러리를 선택하였습니다.
모든 이미지는 원본해상도인 512*512 사이즈를 그대로 활용하였으며, 위에서 언급하였듯이 mask를 전처리하여 저장해 두는 방식을 통해 데이터 로딩속도를 높혔습니다. Validation 과정에서는 IOU threshold를 0.05부터 0.7까지 0.05단위로 로깅하였으며, 정확도나 F1 score 등도 함께 로깅하며 실험 지표들을 관찰하였습니다. 또한 학습 속도를 높이기 위하여 mix-precision을 활용했으며, 아래와 같은 순서로 하이퍼파라미터를 탐색해나갔습니다.
1. Learning Rate에 따른 성능지표 비교
  • 1e-1 ~ 1e-5까지 다양한 learning rate 에 대하여 실험을 진행하였으며, 1e-4이 가장 좋은 성능을 보여 주었습니다.
  • 이후에 encoder와 decoder의 initial learning rate 를 조절하며 추가적인 실험을 진행 하였습니다. (7. decoder, encoder lr 변화에 따른 성능지표 비교 참고)
2. Loss Function에 따른 성능지표비교
  • Loss Function은 segmentation 관련 모델 아키텍처들이 사용했던 다양한 loss function들을 사용했습니다. Single Loss Function으로는 binary cross entropy(with label smoothing), jaccard, focal, dice , lovasz, smoothl1 loss를 사용하였으며 서로 다른 두 종류의 loss function을 사용자가 지정한 weight로 결합한 형태의 loss도 활용할 수 있도록 코드를 작성하였습니다.
  • 모든 single loss와 BCE * 0.75 + DICE * 0.25 , BCE * 0.5 + smooth L1 * 0.5와 같은 combine loss를 테스트한 결과 smoothl1 loss가 가장 높은 성능을 보였습니다. 벤치마크에 활용한 모델은 efficientnet-b0 backbone에 fpn decoder구조의 모델이었습니다.
  • 문제가 비교적 단순하고 머리카락의 특징점이 명확한 편인데다 class imbalance도 고려할 필요가 없었기 때문에 다른 loss function 보다 직관적인 smoothl1 loss가 성능이 가장 높았던 것으로 생각됩니다.
3. Optimizer에 따른 성능 변화
  • Optimizer의 경우 가장 기본이 되는 adam과 sgdP, adamP에 대한 성능지표를 관찰하였습니다. sgdP와 adamP는 ICLR2021에서 Clova AI가 발표한 optimizer로, 분류문제에서 성능이 높았던 지난 경험을 바탕으로 사용하였으며 세 종류 모두 성능 상의 유의미한 차이가 있지는 않았지만, 그래도 가장 성능이 높았던 adamP를 선택하였습니다.
  • Learning Rate Scheduling의 경우 100번의 LR update 중 처음 5회에 대하여 warm-up을 수행하였으며, 각 update당 약 1만개의 이미지를 학습하였습니다.
4. Data Mixing 기법에 따른 성능 변화
  • data augmentation 보다 Cut-Mix나 Mix-Up 과 같이 여러 이미지를 한 이미지 안에 보여주는 Mixing 방법론이 수렴을 빠르게 하고 한번의 iteration에서 한개 이상의 샘플을 보여주는 효과가 있다고 판단하여 일반적인 augmentation 보다 선행하여 테스트하였습니다.
  • 아무런 mixing을 사용하지 않고 진행한 실험과 배치당 Cut-Mix, Mix-Up을 50%,100% 를 사용한 실험 총 5개를 비교한 결과 Cut-Mix를 배치당 50%의 확률로 적용했을 때 가장 높은 성능을 보여 주었습니다.
5. Decoder에 따른 성능 변화
  • pytorch segmentation model library가 제공하는 모든 종류의 Decoder(Unet, Unet++, Linknet, FPN, PSPNet, DeepLabV3, DeepLabV3+, PAN)와 EfficientUNet++의 성능을 비교하였습니다. Decoder의 종류마다 학습속도가 굉장히 차이났으나 속도에 비해 최종 성능차이는 크지 않아 학습속도가 비교적 빠르면서도 높은 성능의 Unet을 선택하였습니다. binary classification 문제에서는 Unet의 성능이 경험적으로 매우 높다고 생각합니다.
6. Encoder 및 pre-trained weight 종류(efficientnet 한정)에 따른 성능 변화
  • 이미지 정보를 잘 Encoding하는 것이 최종 성능에 큰 영향을 줄 것이라 생각하여 최대한 다양한 종류의 Encoder를 테스트 하였습니다. EfficientNet의 경우 timm-efficientnet-b0, b3, b5를 테스트하였으며 backbone model의 parameter가 많을 수록 성능이 향상되었습니다. EfficientNet의 경우 pre-trained 옵션을 advprop, imagenet, noisy-student 각각을 제공하고있는데, advprop 옵션이 가장 성능이 높았습니다.
  • EfficientNet이 벤치마크 데이터셋(imagenet)에서는 높은 성능을 보이지만 다양한 도메인의 데이터셋에 대한 성능은 비교적 낮다고 판단하여, RegNet, ResNet, ResNest 등의 Encoder에 대해서도 실험해 보았습니다. 그 결과 timm-resnest101e 이 가장 높은 성능을 보여 주었습니다. EfficientNet-B5모델보다도 0.1% 정도 높았습니다.
7. Decoder, Encoder LR 변화에 따른 성능 변화
  • Pre-trained weight를 활용하는 Encoder보다 Decoder의 learning rate가 높은 것이 학습에 더 용이하다는 캐글 팁을 바탕으로 두 모듈에 각각 다양한 스케일의 learning rate를 실험해 보았습니다. 그 결과 initial learning rate기준 encoder=5e-5, decoder=5e-4가 가장 높은 성능을 보여 주었습니다.
8. Augmentation
  • Augmentation은 크게 한정된 데이터를 증폭하여 더 많은 데이터를 학습시키는 효과와, 문제를 더 어렵게 만들어 학습될 여지를 늘리는 효과가 있다고 생각합니다. 그러나 이번 대회의 경우 주어진 데이터셋이 20만장으로 제한된 GPU 자원과 시간 내에는 전체 데이터를 약 5회 정도 밖에 보지 못하는 상황이었고, train set과 test set의 이미지 분포 차이가 크지 않다고 생각하였습니다. 또한 매우 치열한 점수경쟁으로 픽셀 레벨의 정확한 ouput-mask를 내는 것이 중요하다고 생각했으며 아래 사진과 같이 topology를 유지하며 이미지를 변형시켜주는 GridDistortion 혹은 ElasticTransform이 효과가 있을 줄 알았지만 결국 성능 저하로 이어졌는데요. 이는 Interpolation이 수반되는 Transform이 원인이라고 판단하여 이들을 최대한 배제하는 방식으로 augmentation을 구성하였습니다. 결과적으로 다양한 옵션을 실험해보았지만 flip, rotate, random brightness&contrast 만 있는 augmentation 조합이 가장 높은 성능을 보여주었습니다.
Grid Distortion, Elastic Transform(출처 : Albumentation)
Grid Distortion, Elastic Transform(출처 : Albumentation)
  • 염색한 머리에 대한 데이터가 비교적 부족하다고 생각하여 색상에 변화를 줄 수 있는 augmentation을 실험해 보았으나, 유의미하게 성능이 떨어졌습니다. 인식된 머리영역에 대하여 색변화를 가하거나 생성모델 을 활용한 방법이 아니라면, 전체 이미지에 대하여 일관적인 색변화 적용은 성능향상에 큰 효과가 없는 것으로 보였습니다.
9. Histogram equalization(CLAHE)를 기본 pre-process로 하는 실험
  • 머리의 경계선 영역을 또렷하게 보여줄 수 있는 histogram equalization(clahe) 를 아예 전처리 과정에 넣어 테스트해 보았습니다. Albumentation CLAHE 함수를 활용하였으며, (limit=3, kernel=(32,32) — 강한 적용)와 (limit=2, kernel(4,4) — 약한 적용)을 아무 처리도 하지 않은 실험과 비교하였습니다. 실제로 이미지를 보면 식별에 용이할 것이라고 판단하였으나, 적용 정도가 작을수록 성능이 높았고, 결과적으로 CLAHE를 포함시키지 않는 것이 가장 높은 성능을 보여주었습니다.
10. weight decay에 따른 성능지표비교
  • bias와 batchnorm layer에는 weight decay를 일괄적으로 적용하지 않고 실험하였습니다.
  • 1e-2, 1e-5, 1e-7 을 실험하였으나 유의미한 성능 차이를 관찰하지는 못했으며, 평소에 가장 즐겨쓰던 값인 1e-5을 최종 선택 하였습니다.
11. SWA
  • 제한된 학습 시간 내에 여러 개의 모델을 학습시켜 앙상블하기에는 주어진 시간이 부족하다고 판단하였고, 일반적으로 모델 앙상블을 통한 성능 향상이 크다는 것은 각각의 개별 모델이 충분히 학습되지 못했다는 것을 의미한다고 생각하였기 때문에 각 개별 모델의 성능을 끝까지 끌어올는 것이 중요하다고 판단하였습니다. 대신에 모델 앙상블과 비슷한 효과를 볼 수 있는 SWA(Stochastic Weight Averaging)를 적용해 보았는데요. 전체 100번의 LR update 중 k번째 update 부터 LR을 고정하고 n frequency로 weight를 저장하여 최종 모델에서 SWA 모델을 확보하였습니다. (torchcontrib의 swa optimizer를 활용하였습니다)
  • 하지만 k와 n을 조절해가며 실험했음에도 불구하고 SWA를 적용한 모델이 best model이었던 적이 한 번도 없었고, 결국 최종적으로 사용하지 않았습니다.
12. Two-Stage로의 실험과정 분리 실험
  • 위의 방법들을 활용하여 best모델을 선정하고, 해당 모델의 output 결과를 보았을 때, 몇몇 짧은 머리에 대한 성능이 낮은 것을 볼 수 있었습니다. 머리 영역 자체가 작은 경우, 적은 오차에도 IOU 성능이 크게 저하되는 것이 그 원인이었습니다.
  • 저희 팀은 인공지능 그랜드 챌린지 4차 대회 생활폐기물 인식대회에서 two-stage 추론과정을 통해 우승할 수 있었는데요. 당시 이미지에서 물체가 차지하는 영역이 작을 때 인식정확도를 높이는데 큰 효과가 있었습니다. segmentation에서도 이 방법이 효과적일 수 있을 것 같아, 전반적인 학습과 추론과정을 수정하여 Two-Stage 방법론을 활용해 보았습니다.
  • Two-Stage 추론과정은 아래와 같습니다.
  1. 첫번째 stage 에서 모델은 먼저 이미지를 받아 머리영역을 추출합니다.
  1. 추출된 머리 영역에서 margin을 k만큼 준 정사각형 crop을 생성합니다. (k는 hyperparameter)
  1. 잘린 이미지를 다시 input으로 활용하여 mask output을 생성해냅니다.
  1. 원래의 이미지에 결과를 붙여넣고 최종적인 mask를 생성합니다.
  • 해당 과정을 위해 모델은 확대된 이미지에 대해서도 머리영역을 추출해낼 수 있도록 학습되어야 합니다. 저희는 first stage에서의 성능이 어느정도 확보되는 것이 확인했다고 생각했기 때문에 모델의 output을 처리하여 활용하는 방식이 아닌 데이터 로더 단에서 label정보를 활용하여 crop 된 이미지를 생성하고, 원본이미지와 함께 학습에 활용하였습니다. 이때 random으로 margin을 주도록 구성하였으며, 나머지 값들은 위의 11가지 hyperparameter 세팅에서 찾은 최적값들을 활용하였습니다.
13. TTA
  • 주어진 추론시간을 충분히 활용하기 위하여 TTA(Test Time Augmentation)을 수행하였습니다. N개의 서로 다른 augmentation transform으로 구성된 transform set으로부터 validation set에 대해 가장 성능이 좋은 k개(k ≤ N)의 transform을 구성된 조합을 brute-force 방식으로 찾았습니다. 결과적으로 총 9개의 augmentation transform set으로부터 가능한 모든 조합의 수를 시도해보았을 때 최종적으로 아래 5개의 transform을 augumentation 과정에 적용할 때 성능이 가장 높았습니다.
  • Origin image, 90 degree rotation, 180 degree rotation, 270 degree rotation, Horizontal flip
  • TTA 수행 도중 Albumentation Rotation 메소드가 성능 하락을 발생시키는 것을 확인하였는데요. 이는 아마도 Albumentation의 Rotation 메소드가 90도를 회전시키더라도 내부적으로는 interpolation이 수반되는 형태로 구현되어 있는 있기 때문이라고 추정합니다. 때문에 최종적으로 pytorch의 rot90flip 을 활용하여 interpolation이 발생하지 않는 형태로 transform을 구현하였습니다.

Error Analysis를 통한 성능 개선

대회 중반에 이르러 기초적인 하이퍼파라미터 튜닝이 이루어져 단순 하이퍼파라미터 값 조정으로 인한 성능 향상이 정체되었을 때에는, 우리가 만든 모델이 어떤 유형의 애러를 겪게 되는지 파악하고 각 애러 유형별로 해결할 수 있는 방법론에 대한 아이디어를 얻기 위해 Validation Set에서 ground truth mask와 model prediction mask 간의 IOU 차이가 큰 순으로 샘플들로 정렬하여 육안으로 확인해보기로 했습니다.
이를 위해서는 모델 prediction 결과에 대한 시각화가 필수적인데요. 단순하게는 주피터 상에서 plot을 하거나 디스크에 출력 결과를 이미지로 저장해서 확인할 수 있지만 데이터셋의 규모가 일정 수준을 넘어가게 되면 수천, 수만개의 이미지 샘플과 다양한 체크포인트와 후처리 코드에서 생성되는 여러 버전의 추론 결과를 매끄럽게 비교하고, 정렬하고, 시각화하기 어려워집니다. 결과적으로는 적절한 error analysis 플랫폼을 갖추지 않을 경우, 연구자들이 정성적인 error analysis에 소극적이게 되고 육안으로 충분히 많은 샘플들을 확인하지 않으며, 정량적인 metric 지표에만 의존하는 현상이 발생합니다.
저희 팀도 여러 프로젝트에서 위와 같은 이유로 정성적인 분석을 적극적으로 진행하지 못 해왔던 점을 이번 대회를 통해 개선해보고자 평소 눈여겨보던 weight&bias (a.k.a wandb) 서비스의 Table Artifact이미지 오브젝트 기능을 활용하여 이미지 샘플, ground truth, neural net의 mask map, contour map, mask-diff map, IOU 정보를 로깅해보았습니다. 해당 기능을 활용할 때의 장점으로는 1차적으로 웹 브라우저 환경에서 연구자가 편하게 모델의 예측 결과를 확인할 수 있다는 점이며 (IOU가 낮은 순으로 정렬도 가능!) 2차적으로는 이미지 파일명을 id로 삼아 join key로 활용하여 다른 모델 체크포인트 혹은 다른 버전의 후처리 코드로 파생된 예측 Table Artifact와 JOIN하여 비교할 수 있다는 점입니다. 따라서 연구자가 가한 변경이 이전 버전과 비교하였을 때 성능 개선을 이루어냈는지 샘플 별로 확인하기에 무척 편리해집니다.
그리고 이번 대회 문제는 클래스 수가 하나였기 때문에 아래 사진과 같이 한 이미지 내에 각 픽셀의 에러 유형별로 색깔을(true-positive → 연두색, false-positive → 파란색, false-negative → 빨간색, true-negative → 검은색) 부여하여 모델이 범한 애러 상황을 직관적이 이해할 수 있었습니다. 즉, 빨간색은 모델이 놓친 영역이고 파란색은 모델이 초과하여 머리카락으로 인식한 부분입니다.
600여개의 시각화 결과를 보며 크게 4가지의 애러 유형으로 정리해볼 수 있었습니다.

유형1 — 가장 큰 contour를 제외한 나머지 contour 영역들이 false-negative가 되는 경우 (빨간색)

notion image
notion image

유형2 — 머리카락 사이 틈이 있거나 얼굴이 존재해서 구멍이 파져야 되는데 Polygon Output으로 변환되는 과정에서 false-positive가 생기는 경우 (파란색)

notion image
notion image
notion image

유형3 - 유형1 + 유형2인 경우 (빨간색 + 파란색)

notion image

유형4 - Ground Truth의 Annotation 자체가 디테일하지 못한 경우

notion image
notion image
notion image

Polygon Combining 알고리즘

유형 1,2,3의 애러는 대회 초반부터 어느 정도 예상하고 있었지만 막상 눈으로 보니 생각보다 전체 점수를 깎아먹는 샘플들이 꽤 비중 있게 존재한다는 것을 느낄 수 있었습니다. 대회 초반에는 리더보드에 스코어가 찍히는 것을 확인하기 위해 cv2.findContours() 로 찾은 각 contour의 면적 중 면적이 가장 큰 contour를 선택하여 polygon으로 제출하는 간단한 방법을 선택하였는데요. 첫 3가지 유형의 애러를 해소하기 위해서는 결국 복수개의 contour를 한붓 그리기가 가능한 형태로 합치는 후처리 모듈이 필요했으며 취할 수 있는 방법은 각각의 contour들을 이어주는 다리(bridge) 역할을 하는 선분을 그려서 (pixel 값을 반전시켜서) cv2.findCountours() 를 다시 수행했을 때 하나의 contour로 출력되게 하는 것이었습니다. 이때 아래와 같은 목표들을 달성할 필요가 있었습니다.
  • channel을 그리는 행위가 기존에 멀쩡한 출력값을 반전시켜서 애러로 만드는 일이기 때문에 channel을 이루는 선분의 길이 합을 최소화시킬 것
  • 하나의 contour 내에는 임의 횟수만큼의 포함 관계가 존재할 수 있음 (흰색 영역 안에 검은색 영역 안에 다시 흰색 영역…)
  • 두 contour를 이어주는 다리(bridge)를 만들기 위해서는 다리 선분의 색이 배경색과 반대색이어야 됨. 이때 contour tree의 깊이에 따라서 배경색이 흰색이 될 수도 검은색이 될 수도 있기 때문에 다리가 그려질 위치의 배경색이 둘 중 무엇인지 자동으로 인식할 수 있어야 함
  • 위 과정을 수행하는데 너무 오랜시간이 걸려선 안됨 (추론 제한 시간)
충분히 고민한 끝에 크게 5가지 포인트를 신경썼는데요,
  1. 이 글을 통해서 cv2.findContours()cv2.RETR_TREE 옵션 활용시 트리 구조의 contour 인식 결과를 제공한다는 것을 알 수 있었고 이를 바탕으로 contour 간의 계층 구조를 만든 후에 재귀적으로 가장 상위 contour부터 시작하여 leaf contour까지 내려간 다음 자기 자신과 같은 depth에 있는 contour와 부모 contour를 함께 병합하는 channel을 생성하고 위로 올라가는 형태로 알고리즘을 작성하여 목표 2번에 대응할 수 있었습니다.
  1. 1번 목표를 달성하기 위해서 우선 부모 contour와 연결하는데 필요한 선분이 가장 짧은 자식 contour를 먼저 병합하고 병합된 contour와 나머지 자식 contour에 대해 다시 길이를 계산해서 연결해야 되는 cost가 적은 순으로 병합되도록 했습니다. 그 결과 모든 자식이 부모와 연결되는 가장 짧은 channel을 형성하는 것에서 발전하여 만약 근처에 부모와 연결되어 있는, 부모보다 더 가까운 자식이 있을 경우 이 자식 contour에 channel을 놓는 것을 확인할 수 있습니다.
  1. 3번 목표를 달성하기 위해 생성하고자 하는 channel 선분에 해당하는 픽셀 값들의 평균을 취함으로써 현재 이 선분이 검은색으로 그려져야 할지, 흰색으로 그려져야 할지 쉽게 판단할 수 있었습니다.
  1. cv2.findContours() 를 진행할 때 cv2.CHAIN_APPROX_TC89_KCOS 옵션을 사용함으로써 어느 정도의 정확성을 유지하면서 point 개수가 절약된 polygon을 얻을 수 있었고 덕분에 4번의 목표를 달성하며 두 contour 사이의 최단 연결 포인트를 찾을 수 있었습니다.
  1. cv2.findContours() 테스트 결과 흰색 channel은 두께가 1픽셀이어도 뭉개지지 않지만 검은색 channel은 두께를 2픽셀 이상 주어야지 topology가 유지됩니다.
그 결과 아래 사진들과 같이 channel에 의한 정확도 희생을 최소화하며 한붓 그리기가 가능한 단일 polygon을 생성할 수 있었습니다.
notion image
Polygon Combining 적용 전(위). 적용 후(아래)
Polygon Combining 적용 전(위). 적용 후(아래)
notion image
Polygon Combining 적용 전(위). 적용 후(아래)
Polygon Combining 적용 전(위). 적용 후(아래)
그리고 위의 알고리즘을 적용한 후에도 IOU가 떨어지는 샘플들을 정렬하며 아래 사진과 같이 아주 작은 영역의 polygon들을 위한 channel 이 생성되면서 득보다 실이 많은 경우도 있다는 점을 인지했습니다.
notion image
따라서 일정 크기 이하의 contour들은 병합 과정에서 제외하기 위해 Validation Set 내의 contour들의 면적 루트 값의 히스토그램을 그려본 결과 아래 사진과 같이 0~50 사이의 반지름을 갖는 contour 분포와 50~400 반지름 사이의 정규 분포가 공존하는 모습을 볼 수 있었습니다.
notion image
이러한 경우를 최소화하고자 병합 알고리즘 내에 min_area 옵션을 만들어 0~50 사이의 값을 테스트해본 결과 영역의 루트 값이 27 이하인 contour 들은 무시되도록 할 때 가장 큰 폭의 추가 성능 향상을 만들 수 있었습니다.

CRF (Conditional Random Field)

Polygon Combining을 적용하여 유형 1,2,3번의 문제는 상당 부분 해결되었고 유형 4를 해결하기 위해서 레퍼런스를 찾던 중 쏘카의 기술 블로그를 통해 CRF의 존재에 대해 알게 되고 pydensecrf을 참조하여 CRF를 시도해보았습니다.
주피터에서 이리저리 돌려보았을 때 직접적으로 개선을 이루진 못하더라도 CRF는 뉴럴넷이 생성한 mask map을 조금 더 sharp 하게 만들어주고 적절한 하이퍼파라미터를 적용했을 때는 육안으로 보아도 훨씬 더 정확한 mask map으로 변환됨을 느꼈으나, 리더보드 상에서의 성능 개선으로는 모두 이어지지 못해 적용하지 않았습니다.
아무래도 본 대회는 결국 에러가 포함된 인간이 생성한 어노테이션을 바탕으로 평가가 이루어졌기 때문에 픽셀 레벨의 개선이 오히려 일치율을 떨어트린 것이 아닌가 하는 생각이 들었습니다. 만약 대회가 아닌 real-world problem이었다면 다음에도 활용해보고 싶었습니다.

Mask Probability의 STD Normalization

CRF가 실패하고 나서 막연히 들었던 가설은 모델 출력 결과인 Probability Map이 Binary Mask Map으로 변환되는 과정에서 threshold 값에 따른 Binary Mask 생성이 robustness를 떨어트리는 큰 요인이라고 생각하였습니다. 왜냐하면 모델이 학습될 때는 0.5를 기준으로 loss가 계산되지만 추론 시에 threshold 값을 튜닝하면 0.27과 같은 임계값에서 리더보드 성능이 더 올라가는 현상이 발생했기 때문입니다. 다행히 smooth L1 loss를 적용한 이후로 0.5 근방에서 항상 가장 좋은 리더보드 점수를 기록하는 일관성을 확보할 수 있었습니다.
그러나 여전히 더 개선할 여지가 남아있다는 막연한 희망을 가지고 각 이미지 샘플에서 모델이 출력한 확률맵의 std 값과 mean 값을 계산하여 scatter plot을 그냥 한번 그려봤습니다. 그 결과 아래 사진과 같이 std가 높을 수록 mean 값도 증가하는 원호와 같은 분포를 띄고 있음을 보았고 이때 Ground Truth와 IOU가 97% 이상인 샘플들에 대해 주황색으로 색칠해보니 오른쪽 하단 영역에 주로 분포하는 것을 볼 수 있었습니다.
이어서 “적절한 normalization을 통해 Probability Map의 std 값과 mean 값을 마사지 해줌으로써 파랑색 친구들도 주황색 친구들 근처로 보내준다면 IOU가 올라가지 않을까?" 라는 막연한 생각이 들었습니다. 그래서 주어진 파란색 친구에 대해 가까운 주황색 친구를 찾고 해당 주황색 친구의 std 값과 mean 값을 갖도록 normalization을 해주는 코드를 야심차게 작성하였지만 안타깝게도 성능 저하만 있었습니다.
x축 (이미지별 mask probability의 STD값) — y축 (이미지별 mask probability의 mean값). 주황색 : (IOU 97% 이상) / 파란색 : (IOU 97% 이하)
x축 (이미지별 mask probability의 STD값) — y축 (이미지별 mask probability의 mean값). 주황색 : (IOU 97% 이상) / 파란색 : (IOU 97% 이하)
하지만 신기하게도 STD 값이 0.2 미만의 파랑색 친구들에 한하여 STD를 0.2 수준으로 올려주는 간단한 Normalization만 적용해도 IOU 스코어가 유의미하게 증가하였습니다. 시간이 부족하여 시각화를 통한 검증은 못했지만 std 가 낮은 샘플들은 대체로 mean 값도 낮기 때문에 0.5를 기준으로 Binarization 하는 과정에서 모두 가짜 배경색(false-negative)으로 전락해버리고 마는데, std 를 높여주는 것이 모델의 Probability Map의 대비(Contrast)를 올려주는 행위로 작용하는 것 같습니다. 따라서 일부 픽셀들이 임계값 이상의 확률 값을 갖는데 성공하여 추가 성능 개선이 이루어진 것이 아닐까 추측해봅니다.
이 기법은 간단한 차트로부터 의식의 흐름을 타고 만들어진 트릭이라고도 볼 수 있는데요. 만들어 놓고도 성능 향상이 생기는게 신기할 따름이며 분명 더 발전된 형태의 후처리 방식이 존재할 것이라 믿습니다. 관련된 연구 결과나 논문을 아시는 분들께서는 페북 댓글을 통해 언급해주시면 정말 큰 도움이 될 것 같습니다.

대회 결과 & Ablation Study

12일 동안 열심히 달린 끝에 최종적으로 Public, Private, Final 리더보드에서 모두 1위를 달성할 수 있었습니다. 그리고 대회 종료 마지막 5시간 전부터는 더 이상의 튜닝이 의미가 없어 Ablation Study를 진행해 보았습니다.
아래 차트에서 A는 Encoder, Decoder, LR에 대한 하이퍼파라미터 서칭이 완료되었을 때의 순수한 모델 성능입니다. 이후 B에서 Polygon Combining을 적용하여 큰 폭으로 성능 향상이 있었구요 (하지만 2,3위에는 미치지는 못했습니다). C에서 min_area와 TTA를 적용함으로써 1위로 진입할 수 있었고 최종적으로 D에서 STD normalization 까지 적용하여 더욱 확실하게 1위를 달성할 수 있었습니다.
그리고 Y축의 스케일에서 알 수 있듯이 순위권 팀들 간의 성능 차이가 소수 첫째자리에서 결정될 만큼 치열했으며 대회 평가 방식의 특성 상, 후처리 트릭이 미치는 영향이 유독 컸던 것 같습니다.
주요 성능 기법 Ablation Study 및 Public 리더보드 상위권 점수 비교
주요 성능 기법 Ablation Study 및 Public 리더보드 상위권 점수 비교

Key Takeaway 겸 후기

(보성)
  • 해결하려는 문제와 데이터셋 특성에 맞는 방법론을 잘 선택하는 것이 중요하다는 것을 느꼈습니다. Imagenet과 같은 Benchmark dataset에서 높은 성능을 보이는 방법론도 적용하려는 문제에 따라 오히려 성능을 떨어트릴 수 있다는 것을 다시 한 번 느꼈고, 풀고 있는 문제에 대한 명확한 이해와 고민을 바탕으로 여러 방법론들이 적용되어야 한다는 것을 느꼈습니다.
  • Post-processing이 순위 상승의 큰 역할을 하였는데, 실제 현실의 문제를 풀 때에도 후처리에 노력을 많이 기울여야 겠다는 생각을 했습니다. CRF같은 경우 성능향상에는 도움이 되지 못했지만 Label 이 정확했다면 훨씬 더 높은 정확도를 보여주었을 것이라 생각합니다. 후처리를 통한 성능 개선을 이끌어낸 좋은 경험이었습니다.
  • 여러 대회들에 출전하였지만, 케빈과 팀을 이루어 짧은 대회 기간동안 이론학습부터 실험 계획, 전략 논의와 구현을 가장 체계적으로 이뤄낸 대회였습니다. 앞으로 실제 현실 문제를 풀면서도 비슷한 적용들을 많이 할 수 있을거라 기대하고, 대회 자체도 너무 즐거웠습니다👍
(케빈)
  • 기본적인 모델 성능 튜닝도 무척 중요하지만 그 이상의 성능 향상을 내기 위해서는 귀찮고 번거롭더라도 Error-Analysis 혹은 Model Analysis를 실천해서 공통적인 성능 하락 유형을 파악하며 이를 해결하기 위한 전략들을 하나씩 빠르게 시도해 보는게 정말 중요하다는 것을 다시 한번 느꼈습니다..!
  • 이번에 Hydra와 Pytorch-Ligthning 스택을 야심차게 공부했는데 결국 적용하지 못하고 Back-compatability가 깨진게 참 비통합니다. 다음 프로젝트를 진행할 때는 조금 더 알흠다운 (DL as a Code)를 실현하기 위해서 열심히 공부해야 봐야겠습니다.

기타 팁

아래는 다른 분들에게도 도움이 될까하여 사소하지만 공유하고 싶은 작은 포인트입니다.

Project Configuration

딥러닝 프로젝트를 진행하다 보면 시간에 따라서 변화하는 코드 아키텍쳐와 무관하게 Back-compatability가 보장되어야 함을 항상 느낍니다. 가장 이상적인 것은 과거 실험의 설정(Configuration)만을 불러오더라도 현재 상태의 코드로도 재현이 가능해야 한다고 생각하는데요. 이를 위해서는 딥러닝 프로젝트를 구성하는 각 컴포너트들이 모듈화되고 코드 내에 하드코딩 되는 요소 없이 단일의 설정 값만으로 생성되어야 합니다.
딥러닝 프로젝트에서 이러한 목표 달성을 어렵게 하는 점은 단순히 오브젝트의 설정값만 존재하는 것이 아닌 실험이 진행되는 절차에 대한 설정값도 존재한다는 점입니다. 이를테면 과거의 실험에서는 단순히 N 에폭을 돌리고 종료하는 형태의 실험이었고 따라서 몇 에폭을 돌릴지, N값만 설정하면 되었다면 이후에는 M 에폭을 돌리면서 x 스텝 마다 LR 값을 y 배 감소시키고… 등등의 실험 과정 자체가 초기에는 점점 예상치 못한 방향으로 흘러가게 됩니다. 이럴 때 서로 다른 실험 과정에 따라 메인 학습 코드를 변경하게 된다면 설정 값 간의 compatiablilty를 충족하지 못하는 문제가 발생합니다.
따라서 단일 configuration 오브젝트로부터 전처리 → 학습 → 후처리를 자동으로 구성되도록 하기 위해 요즘 핫한 Hydra + Pytorch-ligthning 조합을 1~3일차에 시도하였지만, Hydra의 이점을 모두 누리기 위한 학습 시간이 생각보다 많이 소요되었고 괴랄한 아이디어를 적용하는데에 Pytorch-lightning의 틀이 오히려 발목을 잡는 경우가 생겨 결국 어느 정도의 back-compatability를 포기하고 직접 trainer 코드를 작성하고 코드 스냅샷과 설정값이 담긴 yaml 파일을 아티팩트로 저장하는 방식으로 재현성을 확보했습니다.

Albumentation as a Code

Albumentation을 통한 transform 오브젝트는 다양한 transform의 조합으로 구성되기 때문에 이를 설정값 형태로 남기기 참 애매합니다. 미리 몇가지의 transform 조합을 preset으로 만들어두고 yaml 파일 상에 preset의 이름을 지정함으로써 Configurable 하게 만들 수는 있지만 실험을 하다보면 preset이 점점 많아지고 코드의 가독성이 매우 떨어질 수 밖에 없습니다.
이때, python의 eval() 메소드를 사용하면 원래 preset 파이썬 코드에 입력할 TTA 생성 코드를 그대로 yaml 파일로부터 불러와 실행할 수 있습니다. 예를 들자면 yaml 파일에 아래와 같이 TTA시 이미지에 적용할 Albumentation transform과 mask 데이터에 적용할 transform의 문자열을 정의해놓고
파이썬 코드 내에서 config를 통해 불러들인 tta 설정 문자열을 eval() 함수 안에 넣어주면 문자열 대로 파이썬 인터프리터가 통해 코드를 실행하여 Albumentation transform 오브젝트를 다이내믹하게 생성할 수 있습니다. 이런 방식은 안전하지는 않지만 yaml → Albumentation Transform 오브젝트 생성 과정을 아주 쉽고 직관적으로 만들어 줍니다. 혹시 Configurable한 Augumentation Transform을 만드는데 조금 더 나은 방법이 있다면 댓글을 통해서 꼭 공유 부탁드리겠습니다.

타임라인

끝으로, 혹시나 대회 기간에는 하루 단위로 어떤 일들이 일어나는지 궁금하신 분이 있을까 하여 날짜별 일지도 남겨놓습니다.
(1일차~10일차) : Baseline pre-trained 모델, Loss function, Learning Rate 등 상기 기술한 다양한 하이퍼파라미터 및 방법론 테스트
(5일차) : 복수 contour를 하나의 contour로 합치는 polygon combining 알고리즘을 고안하여 큰 폭의 성능 향상
(6일차) : min_area 옵션 추가 + 튜닝으로 추가 성능 향상
(7~8일차) : CRF를 시도했지만 성능이 하락됨
(10일차) : two-stage training + inference 시도했지만 성능 저하만 발생
(11일차) : 모델의 probability map의 STD가 낮은 샘플들에 대해 일정 값 이상으로 올려주었을 때 성능 향상
(12일차) : TTA를 통한 성능 향상 발견

Reference

  1. https://stackoverflow.com/questions/5602155/numpy-boolean-array-with-1-bit-entries
  1. https://github.com/qubvel/segmentation_models.pytorch
  1. https://github.com/HRNet/HRNet-Semantic-Segmentation
  1. https://github.com/clovaai/AdamP
  1. https://github.com/clovaai/CutMix-PyTorch
  1. https://albumentations.ai/docs/api_reference/augmentations/transforms/#albumentations.augmentations.transforms.GridDistortion
  1. https://albumentations.ai/docs/api_reference/augmentations/geometric/transforms/#albumentations.augmentations.geometric.transforms.ElasticTransform
  1. https://pytorch.org/blog/stochastic-weight-averaging-in-pytorch/
  1. https://tech.socarcorp.kr/data/2020/02/13/car-damage-segmentation-model.html
  1. https://github.com/lucasb-eyer/pydensecr

오토피디아 채용에 관한 모든 것을 준비했어요

첨단기술을 통한 모빌리티 혁신, 함께 하고 싶다면?

채용 둘러보기

글쓴이

0 comments