18 minute read

Abstract

지난 몇년동안(2013) PASCAL VOC dataset에 관해 객체 탐지의 성능 향상이 더디다. 대화마다 best model은 일반적으로 여러개의 저수준 피처와 고수준의 context를 결합한 복잡한 앙상블 구조이다. 본 연구에서는 단순한 탐지 알고리즘으로 VOC 2012에서 mAP 53.3%를 달성하며 지난 대회 대비 30% 이상 성능을 향상시킨 방법에 대해 제안한다. 본 논문의 두가지 키포인트는 첫째, bottom-up region proposal에 high capacity CNN을 적용해서 localize와 segment objects를 할 수 있는 것과 둘째, 훈련 데이터가 부족한 경우 사전 학습 모델을 fine tuning하여 성능을 크게 높였다는 것이다. Region proposal이 CNN과 결합되었기 때문에 R-CNN이라 부르고, 최근 개발된 sliding window 개념을 활용한 CNN 아키텍처인 OverFeat과 비교했을 때 ILSVRC2013 탐지부문에서 큰 격차로 우승했다.

Details

도입부

  • 지난 10년간 visual recogniton task에서 SIFT, HOG와 같은 방법을 기반으로 많은 진전이 있었다. 하지만, 2010 ~ 2012년까지 일반적인 객체 탐지 부문인 PASCAL VOC에서는 발전이 더디었다.
  • CNN이 ImageNet 데이터뿐만 아니라 PASCAL VOC에도 적합할까?
  • 본 논문은 deep network로 객체를 localize하고, 적은 데이터만 가지고도 high-capacity model을 훈련하는 두 가지 문제에 집중하며 CNN이 HOG에 비해 PASCAL VOC의 객체 탐지 성능을 크게 향상시켰다는 것을 보여주는 첫번째 논문이다.
  • 이미지 분류와는 달리 객체 탐지에서는 localization이라는 회귀 문제가 있기때문에 기존의 CNN 방식으로는 성능이 그리 좋지 않았고, sliding window 개념을 사용하여 공간 정보를 잘 유지하려고했지만, layer가 깊어질수록 입력이미지에 대한 receptive field가 커져서 정밀한 localization을 수행하기에는 어려움이 따른다.
  • 이를 해결하기 위해 약 2000개의 독립적인 RoI(Region of Interest, 물체가 존재할 것이라고 판단하는 영역)를 제시하고 각 영역에 대해 CNN으로 고정된 길이의 특징을 추출한 다음 추출된 결과를 SVM을 통해 클래스로 분류하는 방법을 사용한다.
  • 객체 탐지 시 데이터가 충분하지 않아서 사전학습 된 모델을 fine tuning하여 성능을 8%나 향상시켰다.



Object detection with R-CNN

본 연구의 object detection은 크게 3가지 모듈로 이루어져있다. 첫째는 region proposal을 생성하는 모듈로 객체가 있을 것이라고 판단되는 region의 후보를 추려내는 모듈이다. 둘째는, CNN을 통해 추려낸 모듈 중에 고정된 길이의 특징 벡터를 추출하는 모듈이다. 셋째는, SVM을 통해 분류를 수행하는 모듈이다.


image



1. Module design

Region proposals

  • 최근 많은 논문들이 cateogy-independent region proposals를 생성하는 방법을 제공하고 있다.
  • R-CNN은 selective search(이미지의 얼룩진 부분을 유사한 category로 판단하여 하나의 영역으로 제안하는 알고리즘. 그 후 bottom-up 방식으로 비슷한 영역을 합쳐서 결과적으로 약 2000개의 region proposal을 만듬)를 기반으로 region proposals을 수행한다.

Featrue extraction

  • AlexNet 연구를 기반으로 selective search로 제안한 영역에서 4096의 차원을 갖는 특징을 추출한다. 5개의 합성곱과 2개의 전결합층으로 이루어져있다.
  • 제안된 영역은 CNN의 구성에 맞게 크기가 조정되어야 하므로(227 x 227) bonding box의 크기나 가로 세로 비율에 상관없이 warping(crop, 왜곡 등의 이미지 변형)시킨다.
  • 경계에 bounding box가 있으면 약간 확대시키고 padding 16을 적용한다.
  • Appendix A
    • CNN에 region proposals를 입력하기 전 CNN input에 적합한 형태로 변환이 필요하고 총 3가지 방법을 사용했다. 아래 사진 (A)는 original region proposals이다.
    • 첫번째 방법 (B)는 이미지 사이즈를 줄여서 특정 크기의 정사각형 안에 이미지를 입력하되 객체 외의 주변 context를 조금 포함시키는 방법이다.
    • 두번째 방법 (C)는 정사각형 안에 이미지를 입력하되 객체 외의 주변 context를 포함시키지 않는 방법이다.
    • 세번째 방법 (D)는 정사각형 안에 이미지를 입력하되 가로세로 비율을 고려하지 않고 이미지를 warped 시키는 방법이다.
    • 각 이미지의 위의 행은 padding=0, 아래 행은 padding=16을 사용한 경우이다.
    • 만약 주어진 정사각형이 region proposals보다 크다면, missing data는 image mean으로 대체했다. 대체 된 부분은 CNN 입력전 다시 제거한다.


image



2. Test-time detection

Test 시에는 제안된 영역마다 특징이 추출되면 해당 특징을 SVM에 입력해 각 영역마다의 점수를 매긴다. 최종 예측을 위해서는 하나의 객체당 하나의 bonding box만 남겨져야하는데 NMS(Non-Maximum Suppression) 방법을 통해 confidence score가 threshold보다 낮은 박스를 모두 제거시키고, 만약 threshold보다 높더라도 confidence score가 높은 bounbding box와 많이 중첩된(IoU가 높은) box(condifence score가 덜 높은)가 있다면 confidence score가 가장 높은 box를 제외하고 모든 box를 제거시켜 각각의 객체마다 하나의 bounding box만 남도록 한다.

NMS(Non-Maximum Supression)

image


R-CNN에서는 selective search 알고리즘을 통해 region proposals을 하고 이 region들을 CNN에 넣어 특징을 추출한다. 추출된 특징은 SVM과 box linear regression을 통해 region 내의 box에 존재하는 객체가 특정 categories에 속할 확률을 예측하고 box의 위치를 ground truth box와 가깝도록 조정한다. 하지만 box에 대해 클래스를 잘 예측하고 box를 잘 그렸다고 할지라도 위의 그림처럼 같은 객체를 여러개의 box가 예측하고 있을 수도 있다. 이는 연산량의 측면에서도 매우 비효율적이기떄문에 가장 객체를 잘 담은 box만 남겨두고 다른 box를 모두 제거하는게 좋을 것이다.

NMS는 가장 좋은 box만을 남겨두기 위한 기법으로 다음과 같은 과정으로 진행된다.

0) 각각의 box마다 계산된 confidence score를 기준으로 confidence score가 threshold를 넘지 않는다면 모두 제거한다. 여기서 말한 confidence score는 물체 존재 확신도로 일반적으로는 box내 객체가 속할 점수들 중 가장 큰 점수로 선택된다(개 0.5, 고양이 0.3, 사자 0.2로 softmax의 예측이 수행되었다면 개의 확률인 0.5가 confidence score로 사용됨). Confidence score를 통한 box 제거는 NMS와 분리된 단계로도 볼 수 있기때문에 0 단계로 표현했다.

1) 모든 box를 confidence score를 기준으로 내림차순 정렬한다.

2) 정렬된 box중 가장 위에 있는 box를 선택하고 이 box와 다른 box와의 IOU를 계산한다.

3) IoU가 threshold보다 높다면 즉, 가장 confidence score가 높은 box와 너무 많이 겹친 박스라면 같은 객체를 담고 있을 확률이 크기때문에 제거한다.

4) 이 과정을 반복하며 객체마다 하나의 box만 남게된다.

Confidence score threshold가 높다면 물체가 존재한다는 기준을 까다롭게 잡은 것이니까 0단계에서 많은 box들이 제거될 것이고, IoU threshold가 낮다면 조금만 겹쳐도 같은 객체를 예측한다고 판단하여 많은 box가 제거될 것이다. 따라서, 적절한 threshold를 선택하는 것도 중요하다.

NMS 이후에는 이미지내의 다른 object들을 기반으로 최종 박스들을 rescoring 하여 결과를 출력한다.

Run-time analysis

  • 모든 CNN 파라미터가 모든 categories에 대해 공유되고, 특징 벡터가 다른 접근 방식들에 비해 저차원이라는 2가지 특징이 detection을 효과적으로 만들었다.
  • Class별로 이루어지는 유일한 계산은 특징 벡터와 SVM 가중치 사이의 연산과 NMS이다. Feature matrix는 일반적으로 2000 x 4096이고, SVM 가중치 행렬은 4096 x N(N은 클래스수)이다.
  • 고차원의 특징을 학습할 때보다 훨씬 더 적은 연산시간을 가진 효과적인 네트워크를 구축했다. 더 좋은 성능을 가지면서 다른 모델에 비해 예측 시간이 빠른 모델이다.

3. Training

Supervised pre-training & Domain-specific fine-tuning

  • Bounding box는 없지만 ILSVRC 2012 classification datsets을 가지고 CNN 모델을 pre-training 시켰다.
  • Detection이라는 새로운 tasks를 수행하기위해 SGD를 사용해 CNN 파라미터를 fine-tuning 시켰고 데이터는 warped region proposals만 사용했다.
  • IoU의 threshold는 bonding box의 IoU가 0.5이상을 positive, 미만을 negative로 했고 pre-trained 모델의 1/10인 0.001을 SGD의 학습률로 사용했다. 각 SGD iteration에는 128(32개의 전경 class, 96개의 배경 class)개의 mini batch가 사용되었다.

Object category classifier

  • 자동차를 탐지하는 task라면 자동차를 잘 담고 있는 box를 positive, 그 외 배경을 담고 있으면 negative로 취급한다. 하지만, 만약 자동차를 일부만 포함하고 있는 box가 있다면 positive라고 할 수 있을까?
  • 본 연구에서는 이와 같은 overlap 문제를 해결하기 위해 regions 중 IoU overlap threshold가 0.3이상이면 해당 box에 객체가 있다고 판단한다(자동차의 0.3% 정도 담고있으면 해당 부분을 positive로 인정). 0.3이라는 수치는 validation set에서 grid search(0.1, …, 0.5)를 통해 도출했다.
  • 특징이 추출되면 각 클래스마다 하나의 linear SVM을 사용해서 정답과 예측결과를 비교하며 모델을 최적화시키는데 모든 클래스를 한번에 처리하지 않고, 각각의 클래스마다 독립적으로 SVM을 최적화시키기때문에 많은 시간이 소요된다.
  • 이를 해결하기위해 수렴이 빠른 standard hard negative mining methods(negative가 더 많은 불균형 데이터에는 FP오류가 많을 것이기때문에 모델이 FP로 잘못 예측한 데이터를 select하여 학습에 다시 사용해서 모델을 좀 더 강건하게 만드는 방법)를 사용했다.
  • Appendix B
    • CNN에 softmax layer를 추가하여 class 예측을 수행하는 fine-tuning 방법(IoU overlap threshold 0.5)과 기존처럼 SVM(IoU overlap threshold 0.3)을 학습할 때 positive, negative에 대한 기준이 달라진 이유는 fine-tuning과 SVM에서 동일한 IoU overlap threshold 사용했을 때 성능이 오히려 하락했는데 아무래도 fine-tuning에는 과대적합을 방지하기 위해 더 많은 데이터들이 필요하지만 현실적으로 데이터를 추가시키기 어렵기때문에 threshold를 SVM과 통일시키지 못했다.
    • Fine-tuning에서의 분류기를 쓰지 않고 SVM을 굳이 학습시킨 이유는 fine-tuning에서 positive class에 대해 localization이 정확하게 수행되지 않았고, softmax classifier가 SVM처럼의 hard negative mining의 범위가 아닌 전체 범위에서 무작위로 negative class를 추출했기때문이라고 추측된다.

4. Results on PASCAL VOC 2010-12 & ILSVRC2013 detection

  • 다른 여러 모델들과 비교했을 때 본 연구의 SVM은 VOC 2010에서 53.7% mAP를, 2011/12에서 53.3% mAP를 달성하며 가장 좋은 성능을 냈다.
  • ILSVRC2013 detection에서는 OverFeat의 24.3%의 mAP보다 훨씬 더 뛰어난 31.4%의 mAP를 기록하며 이전 대회의 기록보다 더 좋은 성능을 냈다.



Visualization, ablation, and modes of error

1. Visualizaing learned features


image


  • 첫번째 레이어는 선, 점과 같이 저수준의 특징을 학습하기때문에 직관적인 시각화가 가능한데 그 후의 레이어들은 복잡한 특징을 학습해서 시각화하기가 어렵다.
  • 따라서, 각층마다 어떻게 학습이 이루어지는지 파악하기 위해 non-parametric(비모수적 방법. 분석에 대해 사전가정을 포함하지 않는, 즉, 파라미터가 사전에 정해져있지 않은 통계적 기법. 데이터에서 패턴이나 관계를 추정할 때 사용되고 데이터의 순위, 순서, 통계량 등 데이터를 기반으로 분석을 진행)방법을 사용한다.
  • 특정 feature(unit)을 하나의 detector로 취급하고 여러 region proposals에 대해 activation을 적용해서 activation이 높은 순부터 낮은 순까지 정렬한 후 NMS를 적용하고 top score를 시각화한다. 이 방법은 activation이 높은, 즉, top ranking에 위치한 unit은 내가 물체를 가지고 있다는 자신감을 대변한다는 idea다.
  • layer pool5의 units을 시각화한 결과, network는 몇몇 클래스의 feature와, 모양, 텍스처, 색상 등을 결합하여 학습한다는 것을 알 수 있다. 이후, fully connected layer는 이 정보들을 기반으로 더 높은 수준의 특징을 가지고 학습을 진행한다.

2. Ablation studies

Performance layer-by-layer, without fine-tuning

  • 어떤 레이어가 중요한지 파악하기 위해 CNN의 마지막 3개 레이어를 살펴봤다(Layer pool5는 생략).
  • Layer fc6은 4096 x 9216에 pool5의 피처맵을 곱하고, Layer fc7은 4096 x 4096에 fc6의 피처맵을 곱한다.
  • fine tuning을 사용하지 않았을 때는 Layer fc6과 fc7이 없을 때 성능이 더 좋았다. 전결합층으로 인한 많은 연산량이 오히려 성능을 하락시켰다.
  • CNN은 전결합층보다 convolution layer가 중요하다.

Performance layer-by-layer, with fine-tuning

  • Fine tuning 결과 성능이 8%나 상승한걸로보아 fine tuning의 효과가 pool5보다 fc6,7에서 더 컸다고 할 수 있다.
  • 이로인해 pool5까지는 ImageNet을 학습하여 일반적인 특징이 추출되고, fine tuning한 분류기에서 해당 도메인에 특화되고 non-linear한 분류기가 구축된 것을 알 수 있다.

Comparison to recent feature learning methods

  • DPM에서 사용한 DPM ST와 DPM HSC 이 두 가지 feature learning methods를 RCNN에 적용하여 DPM과 결과를 비교했다.
  • Feature learning methods를 사용한 RCNN은 최신 DPM과 비교했을 때보다 더 좋은 성능을 가졌다.

3. Network architectures

  • 본 연구에서 구현된 대부분의 아키텍처는 AlexNet을 참고했지만 어떤 아키텍처를 쓰느냐에 따라 탐지 성능이 크게 달라진다는 것을 알게되었다.
  • O-Net(OxfordNet, VGG16)을 pre-trained 모델로 사용하고, 같은 환경에서 pre-trained 시킨 T-Net(TorontoNet)과 비교해본 결과 O-Net의 성능이 더 뛰어났다.
  • 하지만, O-Net은 T-Net에 비해 7배 더 큰 연산 시간을 가진다는 것이 한계로 드러났다.

4. Detection error analysis & Bonding-box regression

  • Hoiem의 Detection analysis를 기반으로 본 모델의 에러 모드와, fine-tuning이 에러 모드를 어떻게 바꾸는지, 에러 type이 DPM과 어떻게 다른지 비교했다.
  • Error anlysis를 기반으로 localization error를 줄이는 방법을 적용했다. DPM의 bounding-box regression에 영감을 받아, selective search region proposal에 대한 pool5의 features가 주어지면 새로운 detection window를 예측하는 linear regression 모델을 학습시킨다.
  • 이 간단한 접근 방식이 mislocaliztion을 줄여서 mAP를 3~4 points 올리는 중요한 방법이 되었다.
  • Appendix C
    • SVM으로 class 분류가 끝나면 class-specific bounding box regressor로 예측을 수행한다.
    • $P$는 pixel, $G$는 ground-truth이다.
    • $P_x, P_y$에 대해서는 점이기때문에 scale은 그대로 한채 변환하고, $P_w,P_h$에 대해서는 log 변환을 수행한다.
    • 위의 $P$들을 얼만큼 이동시킬 것인지에 대한 정보는 $d(P)$를 통해 구할 수 있고, 이 $d(P)$ 정보를 기반으로 ground-truth에 가까운 예측을 수행하게된다. 식은 다음과 같이 표현할 수 있다.


      image


    • $d(P)$들은 pool5 features들을 이용한 linear function이고 여기서 pool5의 features는 $\phi_5(P)$로 표현한다.
    • linear function인 $d(P) = w^T \phi_5(P)$이고 $w$는 학습가능한 파라미터이다. $w$ 파라미터를 최적화시키기 위해서는 다음과 같이 정의한다.


    image


    • 위의 식에서 $t$는 ground-truth G와 예측 P의 차이로 정답에 가까이가기 위해서 P를 얼마나 조정해야하는지 알려준다. $w$의 식을 보면 결국 선형회귀인 MSE와 유사한 손실함수이고, 과적합을 방지하기 위해 regularization parameter인 lambda를 적용했다.


    image


    • 쉽게 말하면, CNN을 통해 추출된 피처와 각 point마다 얼만큼 이동시켜야하는지에 대한 정보인 가중치 $w$를 곱해서 bounding box를 업데이트시키는 방법으로 선형 회귀를 학습하는 것이다.
    • Bounding-box regression을 통해 regularization이 매우 중요하다는 것을 알았고 labmda = 1000으로 설정했다.
    • Region Proposal이 ground-truth와 너무 많이 떨어져있다면 예측이 매우 힘들어지기때문에 IoU overlap threshold를 0.6으로 설정해서 많이 겹치지 않은 proposal은 모두 제거했다.



The ILSVRC2013 detection dataset

1. Dataset overview

  • 학습 약 39만개/검증 약 2만개/테스트 약 4만개
  • 검증과 테스트 데이터에는 라벨링이 잘 되어있지만 학습 데이터에는 모든 라벨들에 대해 라벨링이 잘 되어있진 않다.
  • 주로 검증 데이터에 의존하고, 학습 데이터는 보조로 사용한다. 검증 데이터를 동일한 크기를 갖게 val1(학습에 사용), val2(검증에 사용)로 나누어서 학습과 검증에서 사용한다.
  • 클래스가 불균형하기때문에 검증 데이터셋을 분리할 때 최대한 밸런스하게 분할했다.

2. Region proposals

  • PASCAL과 같은 방법으로 region proposals를 수행했다.
  • 학습 데이터를 제외한 val1, val2, test 데이터에 selective search를 적용했다.
  • ILSVRC image의 사이즈 범위는 매우 넓어서 selective search 전에 이미지 넓이를 500 pixels로 조정했다.
  • 검증 데이터에 selective search를 적용한 결과 이미지당 평균 2403개의 region propsals이 나왔고 이는 91.6%의 recall을 기록했다.
  • PASCAL 데이터에서 98%의 recall에 비해 현저히 낮은 수치이기때문에 이로인해 region proposals 단계가 매우 중요하다는 것을 알 수 있다.

3. Training data

  • 학습 데이터를 구축할 때는 val1과 각 클래스당 N개의 ground-truth boxes만큼의 데이터를 추출했고, 만약 특정 클래스가 minor 클래스라서 N개보다 적다면 해당 클래스의 데이터는 모두 선택했다. $val1 + train_N$ 의 set으로 구성된다.
  • 학습 데이터는 CNN fine-tuning, detector SVM training, bounding-box regressor training이라는 3가지 task에 사용된다.
  • Hard negative mining은 val1으로부터 random하게 5000개의 샘플을 뽑아 진행했다.

4. Validation and evaluation & Ablation study

  • 결과를 제출하기 전에 val1+train, val2와 같은 data usage와 fine tuning, bounding box regression의 효과를 검증했다.
  • 일반화 능력을 판단하기위해 PASCAL과 똑같은 하이퍼파라미터(NMS threshold, SVM C, padding 등)로 실험했다.
  • Bounding box regression이 있는 버전과 없는 버전으로 submission 했다.
  • Fine tuning, val1 데이터셋에 train 데이터 추가, bounding box regression을 도입할 때 위 3가지 중 아무런 방법을 사용하지 않은 모델보다 성능이 더 좋았다.

5. Relationship to OverFeat

  • RCNN과 OverFeat은 구조적으로 굉장히 유사하지만 CNN을 이용한 fine tuning, SVM을 사용했다는 점에서 차이가 존재한다.
  • OverFeat은 속도가 RCNN보다 9배나 빠른데 이 속도는 sliding windows(ex. region proposal)를 진행할 때 이미지가 warp 되지않아서 연산이 훨씬 쉽기 때문이다.
  • RCNN도 다양한 방법을 동원해서 속도 문제를 개선해야한다.



Semantic segmentation

본 연구 당시 Semantic segmentation 분야에서 가장 좋은 모델이라고 평가받는 second order pooling 기법을 활용한 O2P 모델과 성능을 비교하기 위해 그들이 사용한 오픈 소스 프레임워크를 사용했다. O2P는 CPMC 기술을 사용하여 이미지 당 150개의 region proposals을 생성하고 SVR(support vector regression)을 사용하여 localization을 수행했다.

CNN features for segmentation

  • CPMC 알고리즘으로 추출한 regions의 features을 계산하기 위해 3가지 방법을 사용했다.
  • 첫째, region의 형태를 무시하고 warped window에 CNN features를 바로 계산하는 full 전략이다.
  • 둘째, foregound mask에 대해서만 CNN에 넣어 features를 계산하는 fg(foreground) 전략이다. 배경 region은 zero로 만드는 정규화 기법을 사용한다.
  • 셋째, 두 가지 방법을 모두 섞은 full+fg 전략이다.

Results on VOC 2011

  • 모든 feature computation strategy에서 fc6까지 사용하는게 fc7보다 성능이 좋았다.
  • full+fg인 3번째 전략을 사용했을 때 성능이 가장 좋았고, 21개 중 11개의 카테고리에 대한 예측력이 매우 좋았다.
  • RCNN은 R&P(Region & Parts)와 O2P 모델보다 성능이 뛰어났으며, fine tuning을 통해 추가로 성능이 향상될 것으로 기대한다.



Conclusion

  • 최근 성능 향상이 더디던 객체 탐지 분야에서 기존보다 30% 개선된 성능을 낸 모델을 개발했다.
  • Bottom-up region proposals을 CNN에 적용하여 localization과 segments를 수행했고 labeled data가 부족해서 image classification에 사용한 모델을 fine tuning하여 detection 성능을 향상시켰다.
  • Supervised pre-training과 domain specific fine tuning 패러다임이 데이터가 부족한 문제에서 굉장히 큰 효과를 냈다고 추측한다.
  • CV분야의 툴과 딥러닝을 결합(Bottom-up region proposals과 CNN의 결합)하는 방법론은 본 연구에 큰 도움이 됐다.



개인적인 생각

  • Detection은 classification에 비해 더 복잡한 task인데 데이터 부족 문제를 해결하기위해 image classification model을 사전훈련 모델로 사용했고, fine tuning 결과 성능을 크게 향상시킨 것을 보아 딥러닝이 발전할수록 맨땅에 헤딩하는 심정으로 밑바닥부터 모델을 구현한다기보다 이전에 개발된 모델들을 어떻게 효과적으로 사용하는지가 더 효율적인 시대가 된 것 같다.
  • Features를 처리하는 방법, layer의 깊이, dataset의 크기 등 하나의 연구에 가장 효과적인 case가 무엇인지 수많은 실험을 했고, 이를 통해 본 연구에서 객체 탐지 부문의 성능을 높이기위해 얼마나 많은 시간과 노력을 쏟았는지 알 수 있었다.
  • 하나의 이미지를 처리하는데 약 50초라는 긴 시간이 걸렸는데 향후 개발된 Fast-RCNN, Faster-RCNN에서는 어떻게 이 시간을 크게 단축시켰는지 기대가 된다.
  • 본 연구팀도 각 class마다 SVM 모델을 따로 구축하는 것은 매우 비효율적이라고 생각했을것 같은데 hard negative mining을 포기하고 딥러닝 네트워크에서 softmax를 통한 예측을했다면 예측 시간을 훨씬 단축시키진 않았을까 라는 생각이 들었다. 실험을통해 softmax를 사용했을 때 SVM보다 성능이 떨어졌다고는 했지만, 하나의 분류기를 사용하는 것을 중점적으로 실험을 했다면 조금 더 간단한 분류기가 구축되었을 것이라는 의견이다. 물론, 이미 많은 실험들을 통해 SVM을 선택한 것이지만.



구현

Pytorch로 RCNN을 구현해보자. 데이터는 Roboflow에서 제공하는 soccer dataset을 사용했다. Class는 볼을 소유한 player와 소유하지않은 player 주로 이 2개로 이루어져있지만, coco json 상에서는 배경과 일반 player class가 존재해 총 4개의 클래스(0: players, 1: None, 2: has ball, 3: no ball)로 명시되어있다.

!pip install -q selectivesearch # selective search 라이브러리
import json
import cv2
import numpy as np
import os
import matplotlib.pyplot as plt
from tqdm import tqdm
# 데이터 경로 설정
DATA_PATH = '/content/drive/MyDrive/논문실습/data/'
TR_DATA_PATH = '/content/drive/MyDrive/논문실습/data/coco_format/train/'
VAL_DATA_PATH = '/content/drive/MyDrive/논문실습/data/coco_format/valid/'
TEST_DATA_PATH = '/content/drive/MyDrive/논문실습/data/coco_format/test/'
TR_LAB_PATH = '/content/drive/MyDrive/논문실습/data/coco_format/train/_annotations.coco.json'
VAL_LAB_PATH = '/content/drive/MyDrive/논문실습/data/coco_format/valid/_annotations.coco.json'
TEST_LAB_PATH = '/content/drive/MyDrive/논문실습/data/coco_format/test/_annotations.coco.json'
# annotaions file
with open(TR_LAB_PATH, 'r') as f:
    tr_lab = json.load(f)
#print(json.dumps(tr_lab, indent=4))

with open(TEST_LAB_PATH, 'r') as f:
    test_lab = json.load(f)
# 이미지 파일목록
tr_images = [x for x in sorted(os.listdir(TR_DATA_PATH)) if '.jpg' in x]
val_images = [x for x in sorted(os.listdir(VAL_DATA_PATH)) if '.jpg' in x]
len(tr_images), len(val_images)
# IoU 계산 함수
def get_iou(cand_box, gt_box): # cand_box: selective search box, gt_box: ground_truth
    # cand box는 left, top, right, bottom 순
    assert cand_box[0] < cand_box[2]
    assert cand_box[1] < cand_box[3]
    assert gt_box['x1'] < gt_box['x2']
    assert gt_box['y1'] < gt_box['y2']
    x_left = max(cand_box[0], gt_box['x1']) # 겹치는 구간을 파악하기 위해 left는 더 큰 것
    y_top = max(cand_box[1], gt_box['y1'])
    x_right = min(cand_box[2], gt_box['x2']) # right는 둘 중 더 작은 것
    y_bottom = min(cand_box[3], gt_box['y2'])
    if x_right < x_left or y_bottom < y_top: # 겹치지 않는 경우
        return 0.0
    intersection_area = (x_right - x_left) * (y_bottom - y_top)
    bb1_area = (cand_box[2] - cand_box[0]) * (cand_box[3] - cand_box[1])
    bb2_area = (gt_box['x2'] - gt_box['x1']) * (gt_box['y2'] - gt_box['y1'])
    iou = intersection_area / float(bb1_area + bb2_area - intersection_area)
    assert iou >= 0.0
    assert iou <= 1.0
    return iou
# 파일명을 넣으면 img id가 리턴되도록 dictionary 만들기
img_id_dict = dict()
for x in tr_lab['images']:
    img_id_dict[x['file_name']] = x['id']

# 파일명을 넣으면 test img id가 리턴되도록 dictionary 만들기
test_img_id_dict = dict()
for x in test_lab['images']:
    test_img_id_dict[x['file_name']] = x['id']
# selective search 결과를 전달받아 iou를 계산하고 positive, negative로 분류하여
# 결과를 리턴해주는 함수. RCNN은 이미지당 2000개의 region을 제안하지만 GPU의 한계로
# positive, negative 각각 30개씩만 제안받도록함.
def pos_neg_region(image, ssresults, bboxes, labels, threshold):
    train_imgs = []
    train_labs = []
    train_cands = []

    p_num = 0
    n_num = 0
    cands = [cand['rect'] for cand in ssresults if cand['size'] < 10000] # selective search box 중에 크기가 10000이하인 box 좌표만
    cand_rects = []
    for x in cands:
        if x not in cand_rects:
            cand_rects.append(x) # 영역 중복 제거

#    print(len(cand_rects))

    for _, cand in enumerate(cand_rects):
        cand = list(cand)
        if cand[2] == 0: # 넓이, 높이가 0이면 박스가 그려지지않기때문에 1로 바꿈
            cand[2] = 1

        elif cand[3] == 0:
            cand[3] = 1
        cand[2] += cand[0] # width -> x2 형식.
        cand[3] += cand[1] # height -> y2 형식.

        if p_num > 30 and n_num > 30: # gpu의 한계로 positive, negative 각각 30개씩만. region이 2000개면 너무 많음 
            break

        for i in range(len(labels)):
            bbox = bboxes[i]
            label = labels[i]
            img = cv2.resize(image[cand[1]:cand[3], cand[0]:cand[2]], (224, 224), interpolation = cv2.INTER_CUBIC) # 원본에서 후보 부분 잘라서 저장
            iou = get_iou(cand, {"x1":bbox[0],"x2":bbox[0]+bbox[2],"y1":bbox[1],"y2":bbox[1] + bbox[3]})

            if iou > threshold:
                if p_num < 30:
                    #print(cand, bbox)
                    train_imgs.append(img)
                    train_labs.append(int(label))
                    train_cands.append(cand)
                    p_num += 1

            else:
                if n_num < 30:
                    train_imgs.append(img)
                    train_labs.append(0)
                    train_cands.append(cand)
                    n_num += 1

    return train_imgs, train_labs, train_cands

import selectivesearch

def region_proposal(image, mode):
    train_images = []
    train_labels = []
    train_cands = []

    if mode == 'finetuning':
        threshold = 0.3 # 논문처럼 0.5로 했더니 positive region이 잘 탐색되지 않음
        img = cv2.imread(TR_DATA_PATH + image)
        id = img_id_dict[image]
        bboxes = [x['bbox'] for x in tr_lab['annotations'] if x['image_id'] == id]
        labels = [x['category_id'] for x in tr_lab['annotations'] if x['image_id'] == id]
    elif mode == 'classify':
        threshold = 0.3
    elif mode == 'test':
        threshold = 0.3
        img = cv2.imread(TEST_DATA_PATH + image)
        id = test_img_id_dict[image]
        bboxes = [x['bbox'] for x in test_lab['annotations'] if x['image_id'] == id]
        labels = [x['category_id'] for x in test_lab['annotations'] if x['image_id'] == id]

    # 이미지당 gt box 만들기
    _, regions = selectivesearch.selective_search(img, scale=100, min_size=100)
    imgs, labs, cands = pos_neg_region(img, regions, bboxes, labels,  threshold)

    train_images += imgs # 한 이미지당 selectivesearch 결과가 저장된 리스트
    train_labels += labs
    train_cands += cands

    if mode == 'test':
      return train_images, train_labels, train_cands # test시에는 시각화를 위해 후보 영역의 좌표 정보도 같이 리턴
    return train_images, train_labels

# 모델 정의
from torchvision.models import mobilenet_v3_small
import torch.nn as nn
import torch

# Mobilenet을 CNN으로 사용
model = mobilenet_v3_small(num_classes=4) # 배경, player, 공을 소유한 player, 공을 소유하지 않은 player
# coco 형식 상에서는 4개의 클래스지만 사실상 공을 소유한 player와 공을 소유하지 않은 player인 2개의 클래스로 구분된다.

params = [p for p in model.parameters() if p.requires_grad]

# linear SVM을 직접구현하는대신 편의상 pytorch에서 제공하는 선형 분류기 사용
model.classifier = nn.Sequential(nn.Linear(576, 4096), # mobilenet 마지막 layer의 output576
                    nn.Linear(4096, 4))  # 배경포함
device = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
model.to(device)
# 손실함수 및 optimizer 정의
import torch.optim as optim

criterion = nn.CrossEntropyLoss().cuda()
optimizer = optim.SGD(params,lr=0.001, momentum=0.9, weight_decay=0.0005)
# 정규화 및 데이터 증강
# 정규화를 하지않으면 gradient exploding 일어날 수 있음
from torchvision import transforms
train_transform = transforms.Compose([
                                      transforms.ToTensor(), # 0과 1 사이값을 가지도록 정규화
                                      transforms.RandomVerticalFlip(p=0.5),
                                      transforms.RandomHorizontalFlip(p=0.5),
])
# 학습
import time

num_epochs = 50
print('----------------------train start--------------------------')

for epoch in range(num_epochs):

  start = time.time()
  model.train()
  epoch_loss = 0
  prog_bar = tqdm(train_data, total=len(train_data))

  for _, t in enumerate(prog_bar):
      image_data, label_data = t[0], t[1] # 원래는 box regression으로 region을 조정 후 조정된 region 입력값이 들어와야함
      # 논문에서는 positive 32, negative 96개를 포함하는 128의 batch size사용. 편의상 하나의 이미지 내 region의 갯수를 batch size로 사용.
      inputs = torch.cat(tuple(train_transform(id).cuda().reshape(-1, 3, 224, 224) for id in image_data)) # input을 (?,3,224,224) 사이즈로 바꿈
      labels = torch.Tensor(label_data).cuda()
      labels = labels.type(torch.long)

      outputs = model(inputs) # (region, 각 클래스에 속하는 출력값)의 크기를 갖는 텐서

      # 만약 outputs이 0과 1사이인 확률값이 아니라면 crossentropy를 계산할 때 자동으로 확률값으로 변환 후 손실 계산
      # 각 region마다 4개의 클래스에 대한 출력값을 확률값으로 변환시켜 손실 계산
      loss = criterion(outputs, labels) # loss에러나면 모델 빌딩부터 다시
#      print(loss)
      loss.backward()
      optimizer.step()
      epoch_loss += loss.item()
  print(f'epoch : {epoch+1}, Loss : {epoch_loss}, time : {time.time() - start}')
# model test
test_images = [x for x in sorted(os.listdir(TEST_DATA_PATH)) if '.jpg' in x]

import torch.nn.functional as F

candidate_predict = []
candidate_score = []

model.eval()
with torch.no_grad():
      # 시각화를 위해 region의 좌표정보도 함께 가져옴
      # test용으로 이미지 하나만 실행
      image_data, label_data, regions = region_proposal(test_images[0], mode = 'test')
      inputs = torch.cat(tuple(train_transform(id).cuda().reshape(-1, 3, 224, 224) for id in image_data)) # input을 (?,3,224,224) 사이즈로 바꿈
      labels = torch.Tensor(label_data).cuda()
      labels = labels.type(torch.long)

      outputs = model2(inputs)
      for x in outputs:
        predict_class = torch.argmax(x).to(device).item() # 예측한 클래스. 출력값이 가장 높은 값
        candidate_predict.append(predict_class)
        candidate_score.append(F.softmax(x)[predict_class].item()) # 해당 클래스의 확률값
# 여러 region이 겹칠 수 있으니까 가장 정답과 가까우면서 다른 region과 겹치지않는 박스만 남겨두기
# test시에는 nms가 적용되어 정리된 결과만을 출력해야한다.

from torchvision.ops import nms

# IoU가 0.2이상이면 겹치는 것으로 간주하고 제거해서 가장 score가 높은 박스만 남겨두기
selected_idx = nms(torch.tensor(regions).float(), torch.tensor(candidate_score), iou_threshold = 0.2)
selected_boxes = torch.tensor(regions)[selected_idx]
# 원본 이미지 시각화
img_test = cv2.imread(TEST_DATA_PATH + test_images[0])
img_test = cv2.cvtColor(img_test, cv2.COLOR_BGR2RGB)

for idx, x in enumerate(outputs):
  if torch.argmax(x) != 0:
    x1,y1,x2,y2 = regions[idx]
    cv2.rectangle(img_test, (x1,y1), (x2,y2), color = (0,255,0), thickness=2)
    cv2.putText(img_test, str(torch.argmax(x).tolist()), (x1,y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color = (255,0,0), thickness= 3)
plt.imshow(img_test)


image


# nms로 선택된 이미지 시각화
img_test_nms = cv2.imread(TEST_DATA_PATH + test_images[0])
img_test_nms = cv2.cvtColor(img_test_nms, cv2.COLOR_BGR2RGB)

for idx, x in enumerate(selected_boxes):
    x1,y1,x2,y2 = x.tolist()
    cv2.rectangle(img_test_nms, (x1,y1), (x2,y2), color = (0,255,0), thickness=2)
    cv2.putText(img_test_nms, str(torch.argmax(x).tolist()), (x1,y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color = (255,0,0), thickness= 3)
plt.imshow(img_test_nms)


image



위의 이미지를 보면 localization이 잘 수행되지않아 box가 객체를 잘 감싸지 못하고 있다. RCNN은 위의 코드와 같이 region proposals과 classification을 수행할 수 있고, 더 정확한 localization을 위해 box regression을 독립적으로 수행해야하지만 일단 다음을 기약한다… Box regression 후에는 더 정확한 예측을 수행할 것이다.

실제로해보니 region proposals, classification, box regression이 모두 독립적으로 수행되니까 굉장히 비효율적이고 한 stage마다 너무 많은 시간이 소요된다. 이후에 등장한 one-stage model이나 Faster RCNN과 같은 모델에 감사하자.

이미지 출처

Categories:

Updated: