Before Fast R-CNN
우선 기존의 R-CNN 방식의 일련의 과정은 위와 같습니다. Image가 있으면 여기서 Selective Search 알고리즘을 통해서 Regional Proposal을 진행해서 후보 Bounding box 영역을 선택합니다. 그리고 Pre-trained된 AlexNet(= CNN) 구조에 넣기 위해 crop을 하고 최종적으로 227x227로 warped image를 생성해 냅니다.
그리고 CNN에 투입된 결과로 Conv features 즉 특징 백터를 생성해 내고 이를 linear SVM 모델에 넣어서 최종 분류를 한 후, 마지막으로 Bounding box regression을 통해 bounding box의 위치를 결정하는 일련의 작업이 R-CNN의 과정이였습니다. 이러한 방법은 이전의 Sliding Window 방법의 Object Detection 방법을 충분히 극복했습니다.
R-CNN의 한계
- CNN에서 warping / crop을 사용해야 해서 그 과정에서 이미 정보 손실이 일어나게 됩니다.
- 2000여개의 영역마다 CNN을 적용해야 하기에 학습 시간이 오래 걸립니다.
- 학습이 여러 단계(Regional Proposal, Classification, Regression)로 이루어지며 이로 인해 긴 학습 시간과 대용량 저장 공간이 요구됩니다.
- Object Detection의 속도 자체도 느립니다.
SPPNet: Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition
crop과 warp은 classification에서는 data augmentation, detection에서는 regional proposal을 입력 사이즈에 맞춰주기 위해서 사용됩니다. 하지만 위와같은 정보의 손실과 같은 문제가 발생했죠.
위와같이 crop을 적용하면 crop된 구역만 CNN을 통과시키기 때문에, 전체 이미지 정보가 손실이 발생합니다. 또 warp을 적용하면 이미지에 변형이 위와같이 일어나는걸 볼 수 있습니다. R-CNN에서는 FC layer가 고정된 입력 크기를 필요로 하기 때문에 강제된 상황이였습니다.
기존의 R-CNN 과는 다르게 SPPNet은 위의 한계점을 보완하고자, image자체를 conv layers에 딱 한번만(R-CNN은 2000번)집어넣어서 feature map을 1차적으로 구합니다. 이렇게 하면 정보의 손실이 최소화 됩니다. 즉 이는 SPP layer를 추가해 줌으로써 임의의 크기에 대해서 항상 같은 길이의 벡터를 만들어 주고자 한 것이라고 볼 수 있겠네요.
Spatial Pyramid Pooling Layer 작동 원리
input image가 고정 사이즈가 아니었기 때문에 최종 Conv를 거친 Feature map은 사이즈가 arbitary합니다. 이때 feature map사이즈가 어떻든간에 피라미드 풀링을 하면 전부 bin x 256 으로 feature를 고정된 사이즈로 압축 가능합니다.
위의 경우에서는 Conv5에서 나온 feature map에 bin=21 인 피라미드 폴링을 적용하면, 위 그림과 같은 fixed-length representation이 나와서 이게 fully-connected layers로 가는 것을 볼 수 있습니다. 이때 폴링 전 feature map은 arbitary size이지만, 최종적으로 완성되는 벡터는 bin * 256(최종 feature map의 채널)으로 고정될 수 있는 것이지요. 참고로 이는 max pooling입니다.
이처럼다양한 사이즈로 폴링을 진항하면, 정보가 더욱 다양해지는 효과가 있습니다 (GoogLeNet도 마찬가지). 다양한 사이즈의 객체를 검출하는 데에 유리한 장점이 있게됩니다. SPP는 그렇게 정교한 구조는 아니지만..,, 나중에 YoLo도 이 부분에 초점을 맞춰서 아키텍처를 발전시켜 나갑니다.
이제 R-CNN에SPP기법이 어떻게 적용되는지 전체적으로 보겠습니다.
- 영상에 대해 Selective Search를 적용해서 RoI 후보군을 추출합니다.
- 영상을 crop & warp를 하지 않고, 그대로 ConvNet에 입력합니다. (2,000 -> 1번의 입력)
- 컨볼루션으로 나온 최종 Feature map에 원본 영상에서 압축된 비율과 동일하게 2,000개의 RoI도 전부 축소시켜(projection) 이를 Feature map에 적용합니다. (이미지 왜곡 문제를 해결)
- feature map으로부터 추출한 RoI feature에 SPP를 적용합니다. (고정된 벡터로 압축 가능)
- 압축한 벡터를 SVM에 입력하여 class 분류
- Bounding box 회귀 진행
- NMS로 객체별 최종 Bounding box 예측
위는 projection 시키는 부분인데, 그냥 원본 RoI에 대한 비율을 유지하는 작업이라고 보면 좋습니다.
RoI porjection의 기본적인 원리
RoI projection에 대해 간단히 톺아보고 가는게 좋을거 같아서 추가하였습니다. 이를 위해 우리는 ResNet을 backbone으로 쓴 상황을 가정해 보겠습니다. 입력 이미지의 크기는 224x224, 출력 feature map은 7x7입니다. 이때 feature map은 입력 image에 비해 32배 작은데 이 점을 이용해서 입력 image의 RoI를 feature map에 대응하는 RoI로 바꿀 것입니다.
RoI를 [52, 106, 117, 206] 이라고 하겠습니다. 첫 두개의 값은 top-left corner, 그 다음은 bottom-right corner. 그리고 feature map상의 RoI는 비율을 통해 [1.625, 3.3125, 3.6525, 6.4375]입니다. pixel을 1보다 작은 수로 쪼갤 수 없으므로 floating point를 그대로 쓸 수 없기 때문에 반올림 해주어 feature map상의 RoI는 [2, 3, 4, 6] 이라고 대략 짐작하는 셈입니다.
SPP-net의 고질적인 문제점
하지만 이 또한 end-to-end가 아니라서 feature들을 저장하여 알고리즘들을 각각 따로 학습시켜야 합니다. 또한 각각의 학습을 하기 때문에 CNN 알고리즘도 SVM과 regressor의 결과와 독립적으로 학습되어 매우 아쉽습니다.
그래서 나온 것이 end-to-end 문제를 해결한 Fast R-CNN입니다!
Fast R-CNN을 통해서
- R-CNN & SPP-net과 비교해서 더 좋은 성능을 획득할 수 있습니다.
- single-stage로 학습이 진행됩니다.
- 이로인해 전체 네트워크를 업데이트 가능해집니다.
- 저장 공간이 따로 필요하지 않게됩니다.
- 더 빠른 시간 (학습시간 9배/3배, 실제 시간 213배/10배 속도 개선)
R-CNN, SPP-net, Fast R-CNN은 모두 Selective Search 방식으로 동작합니다. 하지만 기존의 CNN 라이브러리들(Selective Sarch)이 모두 CPU기반으로 작성되어 있어, 이러한 RoI 후보군을 만들어 내는 과정이 일종의 BottleNeck이 될 수 있다는 게 큰 단점입니다.
Fast R-CNN (ICCV 2015)
일단 제가 mAP(min Average Precision과 관련된 내용을 정확히 언급 안하고 간거 같아 짧게 소개하겠습니다.
위 그래프는 혼동행렬에서 precision과 recall이 반비례함에 따른 단조 감소 함수를 나타냅니다. 이를 통해서 mAP를 계산합니다. mean Average Precision은 이 단조 감소 그래프를 보간(빨간색)하여 넓이를 구하는 것을 말합니다. mAP@0.5는 정답과 예측의 IoU가 50% 이상일 때 정답으로 판정하겠다는 의미입니다.
그리고 이를 통해 NMS도 mAP와 IoU를 통해 서 진행되었던 것이라고 생각하면 되겠습니다. 예를 들어 같은 클래스(class)끼리 IoU가 50%이상일 때 낮은 confidence의 box를 제거하는 예시를 들 수 있겠습니다. 그럼 많은 수의 RoI를 축소시킬 수 있습니다.
그럼 이제 Fast R-CNN (ICCV 2015) 에 대해 본격적으로 시작하겠습니다.
이는 SPP-net과 비슷하게 한번만 이미지를 CNN에 넣습니다. 그리고 RoI를 원본 이미지에서 뽑아내서 feature map상에서 찾을 수 있게 하는 projection 과정도 SPP-net과 같다고 할 수 있습니다. 그리고 기존에 선형 SVM모델이 Softmax로 바뀐것을 볼 수 있습니다. 그럼 Fast R-CNN에서는 뭐가 핵심일까요? 바로 RoI Pooling Layer입니다.
예를 들어 위와같이 실제 RoI상에서 2x2 grid를 RoI를 최대한 같은 비율로 나누어서 Max Pooling을 하는 것입니다. 즉 이는 SPP-net에서의 Spatial Pytamid Pooling layer의 단순한 구조인데요, 하나의 pyramid level 만을 사용하는 형태와 동일합니다.
학습을 위한 변경점
- 이미지넷을 사용한 선학습된 네트워크로 시작합니다.
- 마지막의 max pooling layer가 RoI pooling layer로 대체합니다. (VGG16에서는 H=W=7)
- 신경망의 마지막 fc layer와 softmax단이 두개의 output layer로 대체됩니다. (원래는 이미지넷 1000개 분류)
- 신경망의 입력이 이미지와 RoI를 반영할 수 있도록 변경
- 이는 원본 이미지, RoI 이미지 모두를 신경망에 넣은 것을 말합니다.
- Detection을 위한 Fine-tuning
- SPP-net & R-CNN은 CNN학습에서의 한계점이 있었습니다. (이미지에 대한 학성곱 역전파 학습이 불가능했음 )
- Region-wise sampling -> Hierarchical sampling
- SGD 미니배치가 이미지에 종속됨 ( N개의 이미지를 R/N Rol개로)
- N=2, R=128로도 좋은 학습 가능 / 약 64배 빠른 학습 가능 (N이 작을수록 계산복잡도도 낮아집니다.)
- 이는 2개의 이미지와, 2개 각각에서 뽑아낸 64x2 =128개의 RoI를 통해 미니배치를 구성해서 학습을 진행했다는 의미
- Single stage
- 최종 classifier와 regression 까지 단방향 단계로 이루어집니다.
Multi-task loss
제가 Fast R-CNN에서는 2개의 output layer로 대체된다고 했습니다. 이는 RoI영역을 CNN을 거친 후의 feature map에 투영시킴으로써, 동일 data가 각자 softmax(classification), bbox regressor(localization)으로 들어가기에 연산을 공유한 것이 됩니다. 이는 또 모델이 end-to-end로 한 번에 학습시킬 수 있다는 의미가 됩니다.
- 분류: 각 RoI별 Discrete probability distribution (전체 K+1 카테고리)
- 회귀: bounding box regression
이는 Loss 식을 말합니다. 우선 u같은 경우에는 배경일 경우 0인데 이는 배경일 경우 Regression을 해줄 필요가 없기 때문입니다. 그리고 분류로는 기본적인 크로서 엔트로피를 사용하고, 회귀는 L1-smooth함수를 사용합니다.
Bounding box regression
Predict box
$$P^i = (P^i_x, P^i_y, P^i_w, P^i_h)$$
Ground truth box
$$G = (G_x, G_y, G_w, G_h)$$
가 있다고 하겠습니다. 여기에서 Bounding box regression의 목표는 위의 predict box가 ground truth box와 유사하도록 학습하는 것입니다. ---> P -> G transform
그리고 P의 요소들을 Ground truth와 유사하게 이동시켜주는 함수들이 있는데, 이는 아래와 같이 정의됩니다.
$d_x(P), d_y(P), d_w(P), d_h(P)$ 이 함수들은 학습한 후 아래와 같이 P에 대해서 transformation을 진행합니다.
$$\hat{G}_x = P_{w}d_{x}(P) + P_x$$
$$\hat{G}_y = P_{h}d_{y}(P) + P_y$$
$$\hat{G}_w = P_{w}exp(d_{w}(P))$$
$$\hat{G}_h = P_{w}exp(d_{h}(P))$$
그리고 이때에, $d(P)$는 transformation함수라고 했으며, 아래와같이 학습가능한 가중치 w를 포함하게 됩니다.
$$d_{*}(P) = w_{*}^{T}\phi_{5}(P)$$
우리가 결국 알고자 하는 것은 transformation 함수 d이며, d를 알아야 proposal box와 ground truth box와의 차이를 줄이게 할 수 있습니다.
그리고 이는 t_{i}와의 차이를 i~N sum을 한 것의 argmin을 구하는 것이 최적의 w가 될 것인데, 이 loss함수에서는 ridge regression을 사용하며, t는 그냥 위에 4줄 적은 식에서 d에 대한 식으로 전개한 식이 됩니다. G는 ground truth여서 구할 수 있고, P도 구할 수 있으니까 그냥 회귀를 진행하면 되는 것입니다.
Fast R-CNN 결론
RoI Pooling을 하나 추가함으로써
(1) CNN 후에 region proposal 연산 - 2000xCNN연산 -> 1번의 CNN연산
(2) 변경된 feature vector가 결국 기존의 region proposal을 projection 시킨 후 연산한 것이므로 해당 output으로 classification과 bbox regression도 학습 가능
그러나 여전히 Fast R-CNN에서도 R-CNN에서와 마찬가지로 RoI를 생성하는 Selective search 알고리즘은 CNN 외부에서 진행되므로 이 부분이 속도의 bottleneck입니다.
따라서 이 RoI 생성마저 CNN 내부에서 함으로써 더욱 빠르면서 정확한 region proposal을 생성한 Faster R-CNN이 나오게 됩니다.
Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks
이는 Region proposal을 합성곱 신경망을 활용하여 CPU에서 GPU의 영역으로 전환하게 됩니다. 그리고 이는 Fast R-CNN과 같은 region-based 감지기의 feature map을 region-proposal generating에도 사용하자는 것입니다. 이게 무슨말이냐면 CNN의 결과로 부터 나온 feature map을 region-proposal을 만들때도 사용하는 것을 말합니다. 이를 위해 RPN이 사용되는데, 이는 end-to-end로 학습이 가능하며 object여부와 bounding box를 regress하는 하나의 FCN을 말합니다.
Faster R-CNN의 구조
Selective search가 느린 이유는 cpu에서 돌기 때문이라고 말했습니다. 따라서 Regional proposal을 생성하는 네트워크도 gpu에 넣기 위해서 Conv layer에서 생성하도록 하자는게 아이디어입니다. 즉 이는 RPN + Fast R-CNN이라고 할 수 있습니다.
이는 마치 어텐션처럼 RPN은 Fast R-CNN이 어디에 주목해야 하는지 알려주게 되는거라고 생각하면 편합니다.
Region Proposal Networks (RPN)
RPN 네트워크는 feature map이 주어졌을 때 물체가 있을떄 이것이 object인지 아닌지, 그리고 실제 GT에 맞춰서 좌푯값이 regression이 가능한지를 학습하는 FCN 입니다. 여기에서 RPN은 k개의 앵커 박스(anchor box)를 이용합니다. 또한, 슬라이딩 윈도우(sliding window)를 거쳐 각 위치에 대해 Regression과 Classification을 수행합니다.
위 그림 상에서 conv feature map에서 sliding window를 통해 물체가 있을 법한 intermediate layer를 뽑고, 이것으로 regression과 classification을 진행합니다. 이때 RPN은 그저 물체가 있을 법한 곳을 proposal하기 위한 목적으로 사용되므로, 전체 class 결과가 아닌 물체가 있는지 아니면 없는지에 대한 여부만 2개의 output으로 알려줍니다. 그리고 물체가 있는 정확한 위치를 찾기 위해 regression layer를 거쳐, bounding box의 중간 위치, weight, height를 더욱 잘 예측할 수 있게 합니다.
Faster R-CNN에서 제안된 RoI는 CNN에서 생성된 feature map에 기반하여 RoI를 제안하긴 하지만, 이 RoI는 원본 이미지 공간에 대한 참조를 유지하고 있음을 의미합니다. 즉 이로 인해, 나중에 이 RoI를 사용하여 원본 이미지에서 객체를 정확히 localize하거나 segment할 수 있습니다.
정리해보겠습니다.
RPN은 원본 이미지에서 regional proposals를 추출하는 네트워크입니다. 원본 이미지에서 anchor box를 생성하면 수많은 region proposals가 만들어집니다. RPN은 region proposals에 대하여 class score를 매기고, bounding box coefficient를 출력하는 기능을 합니다. RPN은 전체적인 동작 과정을 간략하게 다시 보겠습니다.
- 원본 이미지를 pre-trained된 VGG 모델에 입력하여 feature map을 얻습니다.
- 원본 이미지의 크기가 800x800이며, sub-sampling ratio가 1/100이라고 했을 때 8x8 크기의 feature map이 생성됩니다. (channel의 수는 512개입니다).
- 위에서 얻은 feature map에 대하여 3x3 conv 연산을 적용해줍니다. 이떄 feature map의 크기가 유지될 수 있도록 padding을 추가합니다.
- 8x8x512 feature map에 대하여 3x3 conv 연산을 적용하여 8x8x512 개의 feature map이 출력됩니다.
- class score를 매기기 위해서 feature map에 대하여 1x1 conv 연산을 적용합니다. 이 떄 출력하는 feature map의 channel 수가 2x9 가 되도록 설정합니다. RPN에서는 후보 영역이 어떤 class 에 해당하는지까지 구체적인 분류를 하지 않고 객체가 포함되어 있는지 여부만을 분류하기 때문입니다. 또한 ahchor box를 각 grid cell마다 9개가 되도록 설정했습니다. 따라서 channel 수는 2(object 여부) x 9(anchor box 9개)가 되는 것입니다.
- 8x8x512 크기의 feature map을 입력받아 8x8x2x9 크기의 feature map을 출력합니다.
- bounding box regressor를 얻기 위해 feature map에 대하여 1x1 conv 연산을 적용합니다. 이 떄 출력하는 feature map의 channel 수가 4(bounding box regressor) x 9(anchor box 9개)가 되도록 합니다.
- 8x8x512 크기의 feature map을 입력받아 8x8x4x9 크기의 feature map을 출력합니다.
좌측 표는 anchor box의 종류에 따라 객체 포함 여부를 나타낸 feature map이며, 우측 표는 anchor box의 종류에 따라 bouding box regressor를 나타낸 feature map입니다. 이를 통해 8x8 grid cell 마다 9개의 anchor box가 생성되어 576(=8x8x9)개의 region proposals가 추출되며, feature map을 통해 각각에 대한 객체 포함 여부와 bounding box regressor를 파악할 수 있습니다. 이후 class score에 따라 상위 N개의 region proposals만을 추출하고, Non maximum suppression을 적용하여 최적의 region proposals만을 Fast R-CNN에게 전달하게 되는데, 그 과정은 아래에 자세히 기술하겠습니다.
Multi-task loss
RPN과 Fast R-CNN을 학습시키기 위해 Multi-task loss를 똑같이 사용합니다. 하지만 RPN에서는 객체의 존재 여부만을 분류하는 반면, Fast R-CNN에서는 배경을 포함한 N+1개의 class를 분류한다는 점에서 차이가 있습니다.
그리고 위에서 정규화 파라미터는 크게 상관이 없다고 실험결과에서 보여주고 있습니다.
그리고 회귀에서 RPN의 regressor는 일반적으로 회귀 작업에 널리 사용되는 Fully Connected layer를 사용합니다. 학습 데이터로는 각 anchor box와 해당 anchor box와 가장 IoU가 큰 GT bounding box와의 상대적인 위치를 사용합니다. 이를 통해 모델은 anchor box를 보정하여 실제 객체를 더 잘 포함하는 bounding box를 예측하게 됩니다.
Training Faster R-CNN
1) feature extraction by pre-trained VGG16
pre-trained된 VGG16 모델에 800x800x3 크기의 원본 이미지를 입력하여 50x50x512크기의 feature map을 얻습니다. 여기서 sub-sampling ratio는 1/16입니다.
2) Generate Anchors by Anchor generation layer
regional proposals를 추출하기에 앞서 원본 이미지에 대하여 anchor box를 생성하는 과정이 필요합니다. 원본 이미지의 크기에 sub-sampling ratio를 곱한 만큼의 grid-cell이 생성되며, 이를 기준으로 각 grid-cell 마다 9개의 anchor box를 생성합니다. 즉, 원본 이미지에 50x50(=800x1/16 x 800x1/16)개의 grid-cell이 생성되고, 각 grid-cell 마다 9개의 anchor box를 생성하므로 총 22500(=50x50x9)개의 anchor box가 생성됩니다.
3) Class score and Bounding box regressor by RPN
RPN은 VGG16으로부터 feature map을 입력받아 anchor에 대한 class score, bounding box regressor를 반환하는 역할을 합니다. 동작과정은 위에있으므로 참고하시면 될 것 같습니다.
- Input: 50x50x512 sized feature map
- Process: Region proposal by RPN
- Output: class score(50x50x2x9 sized feature map) and bounding box regressors(50x50x4x9 sized feature map)
4) Region proposal by Proposal layer
Proposal layer에서는 2)번 과정에서 생성된 anchor boxes와 RPN에서 반환된 class scores와 bounding box regressor를 사용하여 region proposals를 추출하는 작업을 수행합니다. 먼저 Non maximum suppression(=NMS)를 적용하여 부적절한 객체를 제거한 후, class score 상위 N개의 anchor box를 추출합니다. 이후 regression coefficient를 anchor box에 적용하여 anchor box가 객체의 위치를 더 잘 detect하도록 조정합니다.
- Input:
- 25000(=50x50x9) anchor boxes
- class score(50x50x2x9 sized feature map) and bounding box regressors(50x50x4x9 sized feature map)
- Process: region proposal by proposal layer
- Output: top-N ranked region proposals
5) Select anchors for training RPN by (Anchor target layer)
Anchor target layer의 목표는 RPN이 학습하는데 사용할 수 있는 anchor를 선택하는 것입니다. 먼저 2)번 과정에서 생성한 anchor box 중에서 원본 이미지의 경계를 벗어나지 않는 anchor box를 선택합니다. 그 다음 positive / negative 데이터를 sampling 해줍니다. 여기 positive sample은 객체가 존재하는 foreground, negative sample은 객체가 존재하지 않는 background를 의미합니다.
실제로 IoU가 0.7보다 높은건 positive, 0.3보다 낮은건 negative로 하는데, mini-batch를 구성할 때에는 50%/50% 비율을 맞추도록 합니다. 당연히 detection에서는 negative가 훨씬 더 많을 것이기 때문이죠. 그리고 실제로 IoU 값이 0.3~0.7인 anchor box는 무시합니다. 이러한 과정을 통해 RPN을 학습시키는데 사용할 데이터셋을 구성하게 됩니다.
- Input: anchor boxes, ground truth boxes
- Process: select anchors for training RPN
- Output: positive / negative samples with target regression coefficients
6) Select anchors for training Fast R-CNN by Proposal Target layer
Proposal target layer의 목표는 proposal layer에서 나온 region proposals 중에서 Fast R-CNN 모델을 학습시키기 위한 유용한 sample을 선택하는 것입니다. 여기서 선택된 region proposal는 1)번 과정을 통해 출력된 feature map에 RoI Pooling을 수행하게 됩니다. 먼저 region proposals와 ground truth box와의 IoU를 계산하여 0.5 이상일 경우 positive, 0.1~0.5 사이일 경우 negative sample로 label됩니다.
- Input: top-N ranked region proposals, ground truth boxes
- Process: select region proposals for training Fast R-CNN
- Output: positive / negative samples with target regression coefficients
7) Max pooling by RoI pooling
원본 이미지를 VGG16 모델에 입력하여 얻은 feature map과 6)과정을 통해 얻은 sample을 사용하여 RoI sampling을 수행합니다. 이를 통해 고정된 크기의 feature map이 출력됩니다.
- Input:
- 50x50x512 sized feature map
- positive / negative samples with target regression coefficients
- Process: RoI pooling
- Output: 7x7x512 sized feature map
8) Train Fast R-CNN by Multi-task loss
나머지 과정은 Fast R-CNN 모델의 동작 순서와 동일합니다. 입력받은 feature map을 fc-layer에 입력하여 4096 크기의 feature vector를 얻습니다. 이후 feature vector를 Classifier와 Bounding box regressor에 입력하여 (class의 수가 K라고 할 때) 각각 (K + 1), (K + 1) x 4 크기의 feature vector를 출력합니다. 출력된 결과를 사용하여 Multi-task loss를 통해 Fast R-CNN 모델을 학습시킵니다.
- Input: 7x7x512 (fixed) sized feature map
- Process:
- feature extraction by fc-layer
- classification by Classifier (softmax)
- bounding box regression by Bounding box regressor (Multi-class regression)
- Train Fast R-CNN by Multi-task loss
- Output: loss(loss + Smooth L1 loss)
*Alternating Training
그리고 논문의 저자는 Faster R-CNN 모델을 학습시키기 위해 RPN과 Fast R-CNN을 번갈아가며 학습시키는 Alternating Training 방법을 사용합니다. 학습 과정은 아래와 같습니다.
1) 먼저 Anchor generation layer에서 생성된 anchor box와 원본 이미지의 ground truth box를 사용하여 Anchor target layer에서 RPN을 학습시킬 positive / negative 데이터 셋을 구성합니다. 이를 활용하여 RPN을 학습시킵니다. 이 과정에서 pre-trained 된 VGG16 역시 학습됩니다.
2) Anchor generation layer에서 생성한 anchor box와 학습된 RPN에 원본 이미지를 입력하여 얻은 feature map을 사용하여 proposals layer에서 region proposals를 추출합니다. 이를 Proposal target layer에 전달하여 Fast R-CNN 모델을 학습시킬 positive / negative 데이터 셋을 구성합니다. 이를 활용하여 Fast R-CNN을 학습시킵니다. 이 때 pre-trained된 VGG16 역시 학습됩니다.
3) 앞서 학습된 RPN과 Fast R-CNN에서 RPN에 해당하는 부분만 학습(fine tune)시킵니다. 세부적인 학습과정은 1)과 같습니다. 이 과정에서 두 네트워크끼리 공유하는 convolutional layer, 즉 pre-trained된 VGG16은 고정(freeze) 합니다.
4) 학습시킨 RPN(3번 과정)을 활용하여 추출한 region proposals를 활용하여 Fast R-CNN을 학습(fine tune)시킵니다. 이 때 RPN과 pre-trained된 VGG16은 고정(freeze)합니다.
쉽게 생각하면 RPN과 Fast R-CNN을 번갈아가며 학습시키면서 공유된 convolutional layer를 사용한다고 보면 됩니다. 하지만 실제 학습 절차가 상당히 복잡하여 이후 두 네트워크를 병합하여 학습시키는 Approximate Joint Training 방법으로 대체된다고 합니다.
Detection
실제 detection(=inference) 시에는 Anchor target layer와 Proposal target layer는 사용되지 않습니다. 두 layer 모두 네트워크를 학습시키기 위한 데이터 셋을 구성하는데 사용되었었기 떄문이죠. Fast R-CNN은 Proposal layer에서 추출한 region proposals을 활용하여 detection을 수행합니다. 그리고 최종적으로 얻은 predict box에 Non maximum suppression을 적용하여 최적의 bounding box만을 결과로 출력합니다.
Conclusion
Faster R-CNN 모델은 PASCAL VOC 2012 데이터셋에서 mAP 값이 75.9를 보이면서 Fast R-CNN모델보다 더 높은 detection 성능을 보였습니다. 또한 Fast R-CNN 모델이 0.5fps인 반면 Faster R-CNN모델은 17fps를 보이며, 이미지 처리 속도 면에서 발전한 결과를 보였습니다. 또한 feature extraction에 사용하는 convolutional layer의 feature를 공유하면서 end-to-end로 학습시키는 것이 가능해졌습니다. 하지만 논문의 저자는 detection 속도에 대해 "near real-time"이라고 언급하여, 실시간 detection에는 여전히 한계가 있음을 인정했습니다.
Implementation
https://herbwood.tistory.com/11?category=867198
위는 실제로 코드로 구현한 블로그 내용을 첨부합니다. 이를 참고하며 간단히 분석해보겠습니다,,!
model = torchvision.models.vgg16(pretrained=True).to(DEVICE)
features = list(model.features)
# only collect layers with output feature map size (W, H) < 50
dummy_img = torch.zeros((1, 3, 800, 800)).float() # test image array
req_features = []
output = dummy_img.clone().to(DEVICE)
for feature in features:
output = feature(output)
# print(output.size()) => torch.Size([batch_size, channel, width, height])
if output.size()[2] < 800//16: # 800/16=50
break
req_features.append(feature)
out_channels = output.size()[1]
faster_rcnn_feature_extractor = nn.Sequential(*req_features)
output_map = faster_rcnn_feature_extractor(imgTensor)
먼저 원본 이미지에 대하여 feature extraction을 수행할 pre-trained 된 VGG16모델을 정의합니다. 그 다음 전체 모델에서 sub-sampling ratio에 맞게 50x50 크기가 되는 layer까지만 feature extractor로 사용합니다 (VGG16 레이어를 자름). 이를 위해 원본 이미지와 크기가 같은 800x800 크기의 dummy 배열을 입력하여 50x50 크기의 feature map을 출력하는 layer를 찾습니다. 이후 faster_rcnn_feature_extractor 변수에 전체 모델에서 해당 layer까지만 저장합니다. 이후 원본 이미지를 faster_rcnn_feature_extractor에 입력하여 50x50x512 크기의 feature map을 얻습니다.
feature_size = 800 // 16
ctr_x = np.arange(16, (feature_size + 1) * 16, 16)
ctr_y = np.arange(16, (feature_size + 1) * 16, 16)
ratios = [0.5, 1, 2]
scales = [8, 16, 32]
sub_sample = 16
anchor_boxes = np.zeros(((feature_size * feature_size * 9), 4))
index = 0
for c in ctr: # per anchors
ctr_y, ctr_x = c
for i in range(len(ratios)): # per ratios
for j in range(len(scales)): # per scales
# anchor box height, width
h = sub_sample * scales[j] * np.sqrt(ratios[i])
w = sub_sample * scales[j] * np.sqrt(1./ ratios[i])
# anchor box [x1, y1, x2, y2]
anchor_boxes[index, 1] = ctr_y - h / 2.
anchor_boxes[index, 0] = ctr_x - w / 2.
anchor_boxes[index, 3] = ctr_y + h / 2.
anchor_boxes[index, 2] = ctr_x + w / 2.
index += 1
Anchor generation layer에서는 말 그대로 anchor box를 생성하는 역할입니다. 이미지의 크기가 800x800이며, sub-sampling ratio=1/16이므로, 총 22500(=800x1/16 x 800x1/16 x 9)개의 anchor box를 생성해야 합니다. 이를 위해 16x16 간격의 grid 마다 anchor를 생성해준 후, anchor를 기준으로 서로 다른 scale과 aspect ratio를 가지는 9개의 anchor box를 생성해주었습니다. anchor_boxes 변수에 전체 anchor box의 좌표 (x1, y1, x2, y2)를 저장합니다. 이의 shape은 (22500, 4)입니다.
index_inside = np.where(
(anchor_boxes[:, 0] >= 0) &
(anchor_boxes[:, 1] >= 0) &
(anchor_boxes[:, 2] <= 800) &
(anchor_boxes[:, 3] <= 800))[0]
valid_anchor_boxes = anchor_boxes[index_inside]
Anchor Target layer에서는 RPN을 학습시키기 위해 적절한 anchor box를 선택하는 작업을 수행합니다. 먼저 위와같이 이미지 경계(=800x800) 내부에 있는 anchor box만을 선택합니다.
label = np.empty((len(index_inside),), dtype=np.int32)
label.fill(-1)
pos_iou_threshold = 0.7
neg_iou_threshold = 0.3
label[gt_argmax_ious] = 1
label[max_ious >= pos_iou_threshold] = 1
label[max_ious < neg_iou_threshold] = 0
n_sample = 256
pos_ratio = 0.5
n_pos = pos_ratio * n_sample
pos_index = np.where(label == 1)[0]
if len(pos_index) > n_pos:
disable_index = np.random.choice(pos_index,
size = (len(pos_index) - n_pos),
replace=False)
label[disable_index] = -1
그 다음 전체 anchor box에 대하여 ground truth box와의 값을 구합니다. 그리고 각 ground truth box와의 IoU가 가장 큰 anchor box와 IoU가 가장 큰 anchor box와 IoU 값이 0.7 이상인 anchor box를 positive sample로, IoU값이 0.3 미만인 anchor box는 negative sample로 저장합니다. 여기에서 label변수에 positive sample일 경우 1, negative sample일 경우 0으로 저장합니다.
그리고 그 아래에서는 positive/negative sample의 비율이 1:1이 되도록 구성합니다. 각각의 개수는 128개로 말이죠.
in_channels = 512
mid_channels = 512
n_anchor = 9
conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1).to(DEVICE)
conv1.weight.data.normal_(0, 0.01)
conv1.bias.data.zero_()
# bounding box regressor
reg_layer = nn.Conv2d(mid_channels, n_anchor * 4, 1, 1, 0).to(DEVICE)
reg_layer.weight.data.normal_(0, 0.01)
reg_layer.bias.data.zero_()
# classifier(object or not)
cls_layer = nn.Conv2d(mid_channels, n_anchor * 2, 1, 1, 0).to(DEVICE)
cls_layer.weight.data.normal_(0, 0.01)
cls_layer.bias.data.zero_()
RPN을 정의합니다. 1) 과정을 통해 생성된 feature map에 3x3 conv 연산을 적용하는 layer를 정의합니다. 이후 1x1 conv 연산을 적용하여 9x4(anchor box의 수 x bounding box coordinates)개의 channel을 가지는 feature map을 반환하는 Bounding box regressor를 정의합니다. 마찬가지로 1x1 conv 연산을 적용하여 9x2(anchor box의 수 x object 여부)개의 channel을 가지는 feature map을 반환하는 Classifier를 정의합니다.
x = conv1(output_map.to(DEVICE)) # output_map = faster_rcnn_feature_extractor(imgTensor)
pred_anchor_locs = reg_layer(x) # bounding box regresor output
pred_cls_scores = cls_layer(x) # classifier output
pred_anchor_locs = pred_anchor_locs.permute(0, 2, 3, 1).contiguous().view(1, -1, 4)
print(pred_anchor_locs.shape)
pred_cls_scores = pred_cls_scores.permute(0, 2, 3, 1).contiguous()
print(pred_cls_scores.shape)
objectness_score = pred_cls_scores.view(1, 50, 50, 9, 2)[:, :, :, :, 1].contiguous().view(1, -1)
print(objectness_score.shape)
pred_cls_scores = pred_cls_scores.view(1, -1, 2)
print(pred_cls_scores.shape)
bounding box coefficients(=pred_anchor_locs)와 objectness score(=pred_cls_scores)를 얻습니다. 이를 target값과 비교하기 위해 적절하게 resize해줍니다.
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_score.long().to(DEVICE), ignore_index = -1)
# only positive samples
pos = gt_rpn_score > 0
mask = pos.unsqueeze(1).expand_as(rpn_loc)
print(mask.shape)
# take those bounding boxes whick have positive labels
mask_loc_preds = rpn_loc[mask].view(-1, 4)
mask_loc_targets = gt_rpn_loc[mask].view(-1, 4)
print(mask_loc_preds.shape, mask_loc_targets.shape)
x = torch.abs(mask_loc_targets.cpu() - mask_loc_preds.cpu())
rpn_loc_loss = ((x < 1).float() * 0.5 * x ** 2) + ((x >= 1).float() * (x - 0.5))
print(rpn_loc_loss.sum())
rpn_lambda = 10
N_reg = (gt_rpn_score > 0).float().sum()
rpn_loc_loss = rpn_loc_loss.sum() / N_reg
rpn_loss = rpn_cls_loss + (rpn_lambda * rpn_loc_loss)
print(rpn_loss)
그 다음으로 RPN의 loss를 계산합니다. Classification loss로는 cross entropy loss를 활용합니다. Bounding box regression loss는 오직 positive에 해당하는 sample에 대해서만 loss를 계산하므로, positive/negative 여부를 저장하는 배열인 mask를 생성해줍니다. 이를 활용하여 Smooth L1 Loss를 구합니다. Classification loss와 Bounding box regression loss 사이를 조정하는 balancing parameter $\lambda = 10$으로 지정해주고 두 loss를 더해 multi-task loss를 구합니다.
nms_thresh = 0.7 # non-maximum supression (NMS)
n_train_pre_nms = 12000 # no. of train pre-NMS
n_train_post_nms = 2000 # after nms, training Fast R-CNN using 2000 RPN proposals
n_test_pre_nms = 6000
n_test_post_nms = 300 # During testing we evaluate 300 proposals,
min_size = 16
order = score.ravel().argsort()[::-1]
order = order[:n_train_pre_nms]
roi = roi[order, :]
order = order.argsort()[::-1]
keep = []
while (order.size > 0):
i = order[0] # take the 1st elt in roder and append to keep
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= nms_thresh)[0]
order = order[inds + 1]
keep = keep[:n_train_post_nms] # while training/testing, use accordingly
roi = roi[keep]
그리고 Proposal layer에서는 Anchor generation layer에서 생성된 anchor boxes와 RPN에서 반환된 class scores와 bounding box regressor를 사용하여 region proposals를 추출하는 작업을 수행합니다. 먼저 score 변수에 저장된 objectness score를 내림차순으로 정렬한 후 objectness score 상위 12000개의 anchor box에 대하여 NMS알고리즘을 수행합니다. 남은 anchor box중 상위 2000개의 region proposals를 Fask R-CNN 학습에 사용합니다.
n_sample = 128 # number of samples from roi
pos_ratio = 0.25 # number of positive examples out of the n_samples
# min iou of region proposal with any ground truth object
# to consider as positive sample
pos_iou_thresh = 0.5
neg_iou_thresh_hi = 0.5 # iou 0~0.5 is considered as negative (0, background)
neg_iou_thresh_lo = 0.0
(...)
gt_assignment = ious.argmax(axis=1)
max_iou = ious.max(axis=1)
print(gt_assignment)
print(max_iou)
# assign the labels to each proposal
gt_roi_label = labels[gt_assignment]
print(gt_roi_label)
pos_roi_per_image = 32
pos_index = np.where(max_iou >= pos_iou_thresh)[0]
pos_roi_per_this_image = int(min(pos_roi_per_image, pos_index.size))
if pos_index.size > 0:
pos_index = np.random.choice(
pos_index, size=pos_roi_per_this_image, replace=False)
Proposal target layer의 목표는 proposal layer에서 나온 region proposals 중에서 Fast R-CNN 모델을 학습시키기 위한 유용한 sample을 선택하는 것입니다. 학습을 위해 128개의 sample을 mini-batch로 구성합니다. 이 때 Proposal layer에서 얻은 anchor box중 ground truth box와의 IoU값이 0.5이상인 box를 positive로 0.5미만은 negative로 지정합니다.
위에서 실제로 초록색이 ground truth box, 흰색이 predicted bounding box입니다. 왼쪽은 positive sample, 오른쪽은 negative sample에 해당하는 box입니다.
rois = torch.from_numpy(sample_roi).float()
roi_indices = 0 * np.ones((len(rois),), dtype=np.int32)
roi_indices = torch.from_numpy(roi_indices).float()
indices_and_rois = torch.cat([roi_indices[:, None], rois], dim=1)
xy_indices_and_rois = indices_and_rois[:, [0, 2, 1, 4, 3]]
indices_and_rois = xy_indices_and_rois.contiguous()
size = (7, 7)
adaptive_max_pool = nn.AdaptiveMaxPool2d(size[0], size[1])
output = []
rois = indices_and_rois.data.float()
rois[:, 1:].mul_(1/16.0) # sub-sampling ratio
rois = rois.long()
num_rois = rois.size(0)
for i in range(num_rois):
roi = rois[i]
im_idx = roi[0]
im = output_map.narrow(0, im_idx, 1)[..., roi[1]:(roi[3]+1), roi[2]:(roi[4]+1)]
tmp = adaptive_max_pool(im)
output.append(tmp[0])
output = torch.cat(output, 0)
Featur extractor를 통해 얻은 feature map과 Proposal Target layer에서 추출한 region proposals을 활용하여 RoI pooling을 수행합니다. 이때 output feature map의 크기가 7x7이 되도록 설정합니다.
roi_head_classifier = nn.Sequential(*[nn.Linear(25088, 4096), nn.Linear(4096, 4096)]).to(DEVICE)
cls_loc = nn.Linear(4096, 2 * 4).to(DEVICE) # 1 class, 1 background, 4 coordiinates
cls_loc.weight.data.normal_(0, 0.01)
cls_loc.bias.data.zero_()
score = nn.Linear(4096, 2).to(DEVICE) # 1 class, 1 background
k = roi_head_classifier(k.to(DEVICE))
roi_cls_loc = cls_loc(k)
roi_cls_score = score(k)
마지막으로 RoI pooling을 통해 얻은 7x7 feature map을 입력받을 fc-layer를 정의합니다. 첫 fc-layer의 크기는 25088(7x7x512) x 4096입니다. 그리고 class 별로 bounding box coefficients를 예측하는 Bounding box regressor와 class score를 예측하는 classifier를 정의합니다. 그 후에 Multi-task를 구하는 부분은 RPN과 비슷합니다.