Faster Rcnn 구현 | [논문 읽기] Faster R-Cnn: Towards Real-Time Object Detection With Region Proposal Networks (Nips’ 15) 답을 믿으세요

당신은 주제를 찾고 있습니까 “faster rcnn 구현 – [논문 읽기] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (NIPS’ 15)“? 다음 카테고리의 웹사이트 th.taphoamini.com 에서 귀하의 모든 질문에 답변해 드립니다: th.taphoamini.com/wiki. 바로 아래에서 답을 찾을 수 있습니다. 작성자 동빈나 이(가) 작성한 기사에는 조회수 8,738회 및 좋아요 142개 개의 좋아요가 있습니다.

faster rcnn 구현 주제에 대한 동영상 보기

여기에서 이 주제에 대한 비디오를 시청하십시오. 주의 깊게 살펴보고 읽고 있는 내용에 대한 피드백을 제공하세요!

d여기에서 [논문 읽기] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (NIPS’ 15) – faster rcnn 구현 주제에 대한 세부정보를 참조하세요

[논문 읽기] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (NIPS’ 15)

faster rcnn 구현 주제에 대한 자세한 내용은 여기를 참조하세요.

Faster R-CNN 논문 리뷰 및 코드 구현

논문 제목: Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks Faster R-CNN 개요 SPPnet과 Fast R-CNN은 region …

+ 여기에 더 보기

Source: velog.io

Date Published: 5/3/2022

View: 6600

[Faster R-CNN] 논문 리뷰 & 구현 (Pytorch) – Wolfy Story

R-CNN이랑 Fast R-CNN은 거의 논문리뷰만 하고 구현은 안했는데,. Faster R-CNN은 구현까지 해보았습니다. (근데 오류가 있는것 같음..) …

+ 여기에 표시

Source: wolfy.tistory.com

Date Published: 4/4/2022

View: 4829

객체 탐지 설명과 Faster R-CNN 구현하기 | 얼타고의 개발노트

Two-Stage DetectorPermalink. 모델: R-CNN, Fast R-CNN, Faster R-CNN. Classification, Regional Proposal을 순차적으로 수행하여 결과를 …

+ 여기에 더 보기

Source: panggu15.github.io

Date Published: 11/1/2021

View: 8346

Pytorch로 구현한 Faster R-CNN 모델 – 약초의 숲으로 놀러오세요

이번 포스팅에서는 How FasterRCNN works and step-by-step PyTorch implementation 영상에 올라온 pytorch로 구현한 Faster R-CNN 코드를 분석해 …

+ 여기에 표시

Source: herbwood.tistory.com

Date Published: 4/8/2022

View: 2668

Faster R-CNN 구현

Faster R-CNN 구현. 영은kim 2021. 2. 19. 23:22. wolfy.tistory.com/258 · [Faster R-CNN] 논문 리뷰 & 구현 (Pytorch). 안녕하세요 ! 소신입니다. R-CNN이랑 Fast …

+ 여기에 표시

Source: rladuddms.tistory.com

Date Published: 5/21/2021

View: 8638

(논문리뷰&재구현) Faster R-CNN 설명 및 정리 – 프라이데이

Fast R-CNN에서는 region proposal을 CNN level로 통과시켜 ification, bounding box regression을 하나로 묶었다.

+ 여기에 자세히 보기

Source: ganghee-lee.tistory.com

Date Published: 1/15/2022

View: 2381

Fast R-CNN 구현 질문 – 인프런 | 질문 & 답변

Fast R-CNN 구현중에 가장 난감한 부분이 RoI pooling layer 였는데, 논문에서 보면 mini-batch를 이미지 2개에 roi box를 128개 사용한다고 써 …

+ 여기에 표시

Source: www.inflearn.com

Date Published: 9/20/2022

View: 505

[논문리뷰] Faster R-CNN 이해 – 소소한 블로그

원래는 Mask R-CNN 논문 이해 및 구현이 올해 첫 목표였는데요,. 일단 Faster R-CNN 구현을 먼저 진행해보면 어떨까 고민하고 있습니다.

+ 자세한 내용은 여기를 클릭하십시오

Source: cake.tistory.com

Date Published: 10/23/2021

View: 2441

주제와 관련된 이미지 faster rcnn 구현

주제와 관련된 더 많은 사진을 참조하십시오 [논문 읽기] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (NIPS’ 15). 댓글에서 더 많은 관련 이미지를 보거나 필요한 경우 더 많은 관련 기사를 볼 수 있습니다.

[논문 읽기] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (NIPS' 15)
[논문 읽기] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (NIPS’ 15)

주제에 대한 기사 평가 faster rcnn 구현

  • Author: 동빈나
  • Views: 조회수 8,738회
  • Likes: 좋아요 142개
  • Date Published: 2021. 5. 2.
  • Video Url link: https://www.youtube.com/watch?v=46SjJbUcO-c

Faster R-CNN 논문 리뷰 및 코드 구현

논문 제목: Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks

Faster R-CNN 개요

SPPnet과 Fast R-CNN은 region proposal computation에 많은 시간을 사용한다. 이는 병목현상을 일으킨다. Faster R-CNN은 이런 문제를 해결하기 위해 detection network와 convolutional features를 공유하는 Region Proposal Network(RPN)을 제안한다. RPN은 Fast R-CNN의 detection에 사용되는 region proposals를 제공하기 위해 end-to-end로 학습된다. 논문의 저자들은 RPN과 Fast R-CNN의 convolutional features를 공유하므로써 이들을 하나의 네트워크로 합쳤다.

Faster R-CNN은 VGG-16 모델을 pre-trained로 사용할 때 GPU 환경에서 5fps를 보여줬고, ILSVRC와 COCO2015 competition에서 1등을 차지했다.

Faster R-CNN의 RPN은 동시에 각 위치의 region bounds와 objectness scores를 구하기 위해 pre-trained 된 convolutional layers를 통과한 convolution features에 약간의 추가적인 convolution layers를 추가하므로써 구성했다. 따라서 RPN은 fully convolutional network(FCN)의 한 종류이고, detection proposals를 위해 end-to-end 학습이 가능하다. RPN은 여러 크기와 이미지 비율을 처리하기에 효율적으로 고안되었다. 저자들은 새로운 anchor boxes 방법을 이용했고, 속도를 향상시킬 수 있었다. 또한 RPN과 Fast R-CNN을 통합하기 위해 새로운 training scheme을 제안했다.

Faster R-CNN

사진1. Faster R-CNN Architecture

regions propose를 위한 deep fully convolutional network proposed regions를 이용하는 Fast R-CNN

Faster R-CNN은 두 개의 모듈로 구성된다.

위의 사진1과 같이 전체 구조는 하나의 통합된 네트워크로 object detection을 수행한다. RPN module은 Fast R-CNN module에게 어떤 부분이 중요한지 설명해주는 ‘attention’ mechanisms을 수행한다.

3.1 Region Proposal Networks(RPN)

사진2. RPN network

anchor

기존의 sliding window 방법은 고정된 크기의 window를 이미지에서 sliding하며 각 window에서 객체를 탐지한다. 이때 고정된 고정된 크기의 window를 사용한다는 것은 객체를 탐지할 때, 객체의 크기들이 다 유사하다는 가정을 하는 것과 마찬가지가 된다. 하지만 객체들은 다 제각각의 크기를 갖고 있으므로 이러한 가정을 통해 object detection을 하게 되면 좋은 결과를 얻기 어렵다. (참고: sliding window란?)

anchor를 사용하면 이러한 문제를 해결할 수 있다. 위의 사진2에서 sliding window를 보면 가운데 pixel(파란 점)을 기준으로 k개의 anchor boxes가 있는 것을 확인할 수 있다. 이렇게 k개의 anchor boxes를 이용하면 하나의 window에서 여러 가지의 scale 및 aspect ratio(비율)의 bounding box를 고려할 수 있게 된다. 논문의 저자들은 3가지의 scale과 3가지의 aspect ratio를 이용해 각각의 sliding position 마다 k=9(3×3)개의 achor boxes를 사용했다.

feature map(pre-trained된 conv를 통과한)에 zero-padding을 1 적용하면 각 픽셀이 sliding position의 중심점이 된다. 따라서, feature map의 size가 WxH라면 feature map에서 WxHxk개의 anchor boxes가 있다는 것을 알 수 있다.

RPN의 입력과 출력

사진3. RPN network

anchor에 대한 설명과 위의 사진3을 참고하여 RPN의 입력과 출력을 설명하면 다음과 같다.

pre-trained convolutional networks를 통과해 얻은 feature map(WxHxC)을 입력으로 사용한다. feature map에 3×3 conv 연산을 수행한다. zero-padding을 1 적용하여 입력과 intermediate feature map의 크기를 WxH로 유지한다. intermediate feature map에 대해 유사한 2개의 1×1 conv 연산을 각각 수행한다. 하나는 classification(object의 존재)이고, 다른 하나는 bounding box regression(object의 위치)이다. classification result는 WxHx18이다. WxH에서 각각의 grid는 sliding position의 중심점을 나타낸다. 18은 2×9로 2는 classification score(object vs not object)를 나타내고, 9(=k)는 각각의 anchor boxes를 나타낸다 Bounding box regression result는 WxHx36이다. 18이 아니고 36인 이유는 bounding box를 4개의 좌표를 이용해 regression하기 때문이다.

Loss function and training

Loss function은 다음과 같다

L ( { p i } , { t i } ) = 1 N c l s Σ i L c l s ( p i , p i ∗ ) + λ 1 N r e g Σ i p i ∗ L r e g ( t i , t i ∗ ) L(\{p_i\},\{t_i\}) = \frac{1}{N_{cls}}\Sigma_i L_{cls}(p_i, p_i^*) + \lambda \frac{1}{N_{reg}} \Sigma_i p_i^* L_{reg}(t_i, t_i^*) L ( { p i ​ } , { t i ​ } ) = N c l s ​ 1 ​ Σ i ​ L c l s ​ ( p i ​ , p i ∗ ​ ) + λ N r e g ​ 1 ​ Σ i ​ p i ∗ ​ L r e g ​ ( t i ​ , t i ∗ ​ )

i i i: index of an anchor

p i p_i pi​: predicted probability

t i t_i ti​: parameterized coordinates

N c l s N_{cls} Ncls​: mini-batch size

N r e g N_{reg} Nreg​: the number of anchor locations

λ \lambda λ: balancing parameter (default = 10)

L c l s L_{cls} Lcls​: cross-entropy loss

L r e g L_{reg} Lreg​: L1 smooth loss

윗첨자로 쓰인 *는 ground-truth를 의미한다.

참고) Fast R-CNN의 multi-task loss와 유사하다. 해당 loss에 대한 자세한 설명은 Fast R-CNN 리뷰를 참고

p i ∗ p_i^* pi∗​ = 1인 경우 positive sample이고, p i ∗ p_i^* pi∗​ = 0인 경우 negative sample이다.

positive sample과 negative sample의 정의는 다음과 같다

positive sample: IoU가 가장 높은 anchors 또는 IoU가 0.7 이상인 경우

negative sample: IoU가 0.3이하인 경우

ignore sample: positive 또는 negative가 아닌 anchors로 학습에 사용되지 않는다. (참고: -1로 labeling)

IoU(intersection of Union)는 anchor와 ground-truth box간의 계산으로 얻어진 것이다.

RPN을 학습하기 위한 mini-batch를 구성하는 방법을 알아보자.

일단 mini-batch에 대해 간단히 알아보면, 하나의 이미지로 얻어진 anchor들 중에서 positive 128개+negative 128개 = 256개의 anchor로 구성된다.

mini-batch에 대한 자세한 설명은 다음과 같다.

사진 2에서 conv feature map은 WxH의 size를 갖는다. (W,H)= (50,50), k=9라 해보자. 그러면 하나의 이미지에 anchor box의 개수는 50x50x9 = 22500개이다. 이 중에서 이미지의 경계를 벗어나는 anchor들을 제외한다(-1로 labeling). 그 다음 위의 positive/negative의 기준에 따라 labeling을 한 후 각각 랜덤하게 128개씩 sampling을 수행해서 256개의 anchor box를 얻게 된다. 이렇게 얻은 256개의 anchor box들이 mini-batch가 된다. (나머지 ignore sample들 -1로 labeling)

위의 Loss function과 mini-batch를 backpropagation과 SGD를 이용해 RPN을 학습시킨다.

참고) ignore labeling을 하는 이유??

22500개의 anchor 중에서 256개의 anchor만을 이용해서 loss를 구하지만, loss를 구할 때는 22500개의 모든 anchor를 고려해야 하기 때문이다. 즉, RPN의 output과 ground-truth의 차원을 맞춰줘야 되므로 ignore labeling을 하고, loss를 계산할 때는 무시하도록 한다.

3.2 Sharing Features for RPN and Fast R-CNN

사진4. Faster R-CNN

RPN과 Fast R-CNN의 features 공유를 설명하기 전에 Fast R-CNN module은 Fast R-CNN 논문의 학습 방법으로 학습한다. 한 가지 다른 점은 selective search를 통해 RoI를 받는 것이 아니라 RPN을 통해 RoI를 받는다. 이 때, 모든 anchor를 RoI로 설정하는 것이 아니라 Non-maximum suppression을 사용해 2000개의 RoI들만 이용한다.

4-Step Alternating Training

RPN과 Fast R-CNN을 통합해서 학습하는 방법은 다음과 같다.

사진5. Alternating Training

사진 5에서 파란색 네트워크는 업데이트가 되지 않는 네트워크이다.

RPN training 방법을 통해 학습을 한다. 이때, pre-trained VGG도 같이 학습 (1)로 학습된 RPN을 이용해 RoI를 추출하여 Fast R-CNN 부분을 학습시킨다. 이때, pre-trained VGG도 같이 학습 (1)과 (2)로 학습된 pre-trained VGG를 통해 추출한 feature map을 이용해 RPN을 학습시킨다. 이때, pre-trained VGG는 학습시키지 않는다. (1),(2)를 통해 학습된 pre-trained VGG와 (3)을 통해 학습된 RPN을 이용해 Fast R-CNN을 학습한다. 이때, pre-trained VGG와 RPN은 학습시키지 않는다.

참고) 해당 논문에는 다른 학습 방법들도 소개되었으나 논문의 모든 실험은 위의 방법으로 실행되었다. 이외에 Approximate joint training 방법이 있는데, 이는 모든 RPN loss와 Fast R-CNN loss를 더해서 학습시키는 방법이고 꽤 좋은 성능을 보였다. 그리고 위의 방법보다 짧은 학습시간을 보여줬다.

Detection

사진6. Training

사진7. Detection

사진 7은 Faster R-CNN의 detection 작동 방식을 나타낸 사진이다.

사진 6은 training 작동 방식을 나타내고, 사진 7과 다르게 ‘Anchor target layer’와 ‘Proposal target layer’가 존재한다. 이는 학습을 위해 필요햔 ground truth를 만들어 주는 layer들이다.

결론

Faster R-CNN은 RPN을 사용해 기존의 object detection network들 보다 좋은 성능을 보여주는 것과 동시에 거의 실시간의 처리 속도를 갖는 object detection network 이다.

코드 구현

코드는 개인 github에 pytorch를 사용해 구현했다.

chenyuntc님의 깃허브에 pytorch로 구현된 Faster R-CNN repository를 참고해서 코드 구현을 진행했다.

코드 자체를 수정한 부분은 적지만, 학습과정이나 학습에 사용되는 네트워크들의 쓰임세를 주석으로 설명했다.

References

Faster R-CNN 논문: Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks

https://yeomko.tistory.com/17

https://herbwood.tistory.com/10?category=856250

chenyuntc님의 깃허브

https://metar.tistory.com/entry/Sliding-window%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8Cobject-detection

[Faster R-CNN] 논문 리뷰 & 구현 (Pytorch)

반응형

안녕하세요 ! 소신입니다.

R-CNN이랑 Fast R-CNN은 거의 논문리뷰만 하고 구현은 안했는데,

Faster R-CNN은 구현까지 해보았습니다. (근데 오류가 있는것 같음..)

# Faster R-CNN 구조

Faster R-CNN의 구조는 Fast R-CNN에 Region Proposal Network(RPN)를 추가한 모델입니다.

Fast R-CNN의 단점

Selective Search의 CPU로 연산 (연산 시간의 Bottleneck)

Faster R-CNN의 해결 방안

Region Proposal을 모델에서 처리

이로 인해 진정한 end-to-end 학습이 가능한 모델이 되는데요,

근데 학습과정이 생각보다 까다로웠습니다.

# Faster R-CNN 학습 과정

흔히 Backbone이라고 부르는 Conv Layer에서 Feature Map을 추출합니다.

추출한 Feature Map을 RPN Layer에 넣어, region proposal을 생성합니다.

Feature Map과 RPN을 거쳐 얻은 Region Proposal을 통해 Bounding Box Regression과 Classifier를 수행합니다.

RoI Pooling + RoI Head (Fast R-CNN)

여기서 두 가지 Loss를 통해 학습하게 되는데,

RPN layer의 loss와 RoI의 Loss입니다.

이 둘을 합해서 하나의 Total Loss를 계산하고, 이를 통해 전체 네트워크를 업데이트합니다.

# Faster R-CNN 코드 단위 이해

Faster R-CNN을 코드로 이해하는데 꼬박 3일정도 걸린 것 같습니다. (저는 바보인가봐요…)

참고한 코드는

1. Faster RCNN from scratch Github

2. Ganghee-Lee/Faster-RCNN-Tensor Flow Github

3. Simple Faster RCNN pytorch Github

총 세 개 입니다.

1번 코드를 중심으로 설명하자면,

# 패키지 Import, custom utils Load

import numpy as np import torch import torch.nn as nn import torch.nn.functional as F import torchvision from utils import *

utils에는 IoU 계산, non-maximum suppression(NMS), bounding box -> loc 변환, loc -> bounding box 변환

# Sample Data, Feature Extraction

image = torch.zeros((1, 3, 800, 800)).float() image_size = (800, 800) # bbox -> y1, x1, y2, x2 bbox = torch.FloatTensor([[20, 30, 400, 500], [300, 400, 500, 600]]) labels = torch.LongTensor([6, 8]) sub_sample = 16 vgg16 = torchvision.models.vgg16(pretrained=True) req_features = vgg16.features[:30] print(req_features) output_map = req_features(image) print(output_map.shape)

샘플 데이터로 800×800 이미지를 사용했고, bounding box는 2개, labels도 2개입니다.

sub_sample은 down sampling이 얼마나 되는지, 800에서 50으로 줄어듦으로 16입니다. (down sample 4번 2**4)

원본 이미지와 box입니다.

output_map이 Conv Layer (여기선 VGG16)을 지나서 나온 결과입니다.

# Anchor 생성

anchor_scale = [8, 16, 32] ratio = [0.5, 1, 2] # H/W len_anchor_scale = len(anchor_scale) len_ratio = len(ratio) len_anchor_template = len_anchor_scale * len_ratio anchor_template = np.zeros((9, 4)) for idx, scale in enumerate(anchor_scale): h = scale * np.sqrt(ratio) * sub_sample w = scale / np.sqrt(ratio) * sub_sample y1 = -h/2 x1 = -w/2 y2 = h/2 x2 = w/2 anchor_template[idx*len_ratio:(idx+1)*len_ratio, 0] = y1 anchor_template[idx*len_ratio:(idx+1)*len_ratio, 1] = x1 anchor_template[idx*len_ratio:(idx+1)*len_ratio, 2] = y2 anchor_template[idx*len_ratio:(idx+1)*len_ratio, 3] = x2 print(anchor_template)

Anchor scale과 ratio로 box를 판단할 모양을 만들어줍니다. (anchor_template)

anchor 하나당 scale 3, ratio 3으로 총 9개입니다.

template 도식입니다. 크기별로, 모양별로 총 9가지가 나오게 됩니다.

feature_map_size = (50, 50) # The first center coors is (8, 8) ctr_y = np.arange(8, 800, 16) ctr_x = np.arange(8, 800, 16) ctr = np.zeros((*feature_map_size, 2)) for idx, y in enumerate(ctr_y): ctr[idx, :, 0] = y ctr[idx, :, 1] = ctr_x print(ctr.shape)

anchor_template이 위치할 점들을 가져옵니다.

위의 점들을 도식하면 아래와 같이 50×50개의 anchor center들이 생성됩니다.

anchors = np.zeros((*feature_map_size, 9, 4)) for idx_y in range(feature_map_size[0]): for idx_x in range(feature_map_size[1]): anchors[idx_y, idx_x] = (ctr[idx_y, idx_x] + anchor_template.reshape(-1, 2, 2)).reshape(-1, 4) anchors = anchors.reshape(-1, 4) print(anchors.shape) # (22500, 4)

이 점들에 각각 anchor template을 적용해줍니다.

파란색 박스가 800×800 이미지고,

각각의 anchor별로 template을 적용했을 때, 나오는 22500개 (50x50x9)의 anchor box들입니다.

# anchor box labeling for RPN

valid_index = np.where((anchors[:, 0] >= 0) &(anchors[:, 1] >= 0) &(anchors[:, 2] <= 800) &(anchors[:, 3] <= 800))[0] print(valid_index.shape) # 8940 이미지를 넘어가는 박스는 사실상 사용을 못하는 것이기 때문에, 제외해줍니다. valid_labels = np.empty((valid_index.shape[0],), dtype=np.int32) valid_labels.fill(-1) valid_anchors = anchors[valid_index] print(valid_anchors.shape) # (8940,4) print(bbox.shape) # torch.Size([2,4]) RPN에서는 Class는 관심없고, Object가 있는 곳을 Region Proposal하는 것이 중요하므로, label은 1, 0이 됩니다. 또한, 8940개의 proposal 중 겹치는 것도 있고, 불필요한 것들도 있으므로 이를 제거하기 위해 -1이라는 label을 할당해줍니다. ious = bbox_iou(valid_anchors, bbox.numpy()) # anchor 8940 : bbox 2 pos_iou_thres = 0.7 neg_iou_thred = 0.3 # Scenario A anchor_max_iou = np.amax(ious, axis=1) pos_iou_anchor_label = np.where(anchor_max_iou >= pos_iou_thres)[0] neg_iou_anchor_label = np.where(anchor_max_iou < neg_iou_thred)[0] valid_labels[pos_iou_anchor_label] = 1 valid_labels[neg_iou_anchor_label] = 0 # Scenario B gt_max_iou = np.amax(ious, axis=0) gt_max_iou_anchor_label = np.where(ious == gt_max_iou)[0] print(gt_max_iou_anchor_label) valid_labels[gt_max_iou_anchor_label] = 1 valid anchor와 bbox 와의 iou를 계산해줍니다. ious는 행렬이 되는데 행은 각 anchor가 되고 열은 bbox입니다. 8940, 2의 shape로 Anchor별 bbox와의 iou 값들이 들어가게 됩니다. 이 값을 통해 0.7 이상이면 positive, 0.3보다 작으면 negative를 레이블하게 됩니다. 대부분의 경우 0.7이상인 경우가 많지 않기 때문에, Scenario A에서는 논문대로 하고, Scenario B에서 박스별 iou 최대값인 애들로 positive로 레이블합니다. n_sample_anchors = 256 pos_ratio = 0.5 total_n_pos = len(np.where(valid_labels == 1)[0]) n_pos_sample = n_sample_anchors*pos_ratio if total_n_pos > n_sample_anchors*pos_ratio else total_n_pos n_neg_sample = n_sample_anchors – n_pos_sample pos_index = np.where(valid_labels == 1)[0] if len(pos_index) > n_sample_anchors*pos_ratio: disable_index = np.random.choice(pos_index, size=len(pos_index)-n_pos_sample, replace=False) valid_labels[disable_index] = -1 neg_index = np.where(valid_labels == 0)[0] disable_index = np.random.choice(neg_index, size=len(neg_index) – n_neg_sample, replace=False) valid_labels[disable_index] = -1

그리고나서 positive와 negative를 합해 256개만 남기고 제외합니다.

postive가 128개가 되지 않는 경우, 나머지는 negative로 채우게 됩니다.

# Each anchor corresponds to a box argmax_iou = np.argmax(ious, axis=1) max_iou_box = bbox[argmax_iou].numpy() print(max_iou_box.shape) # 8940, 4 print(valid_anchors.shape) # 8940, 4 anchor_loc_format_target = format_loc(valid_anchors, max_iou_box) print(anchor_loc_format_target.shape) # 8940, 4

위의 코드를 보면, ious에서 Anchor별로 어떤 박스가 iou가 높은지 확인합니다.

(0.37312, 0.38272) 이면 1, (0.38272, 0.37312) 이면 0

이렇게하면, 1 0 1 0 0 0 0 1 0, … 이라는 8940개의 배열이 생기게 됩니다.

이 index로 box값들을 하나하나 할당해서 8940, 4의 배열을 만듭니다.

그리고나서, utils에 있는(직접 만든) format_loc함수로 anchor box에 location을 할당해줍니다.

(정확히 이해는 못했는데, Regression을 해준다는 의미인 것 같습니다.)

anchor_target_labels = np.empty((len(anchors),), dtype=np.int32) anchor_target_format_locations = np.zeros((len(anchors), 4), dtype=np.float32) anchor_target_labels.fill(-1) anchor_target_labels[valid_index] = valid_labels anchor_target_format_locations[valid_index] = anchor_loc_format_target print(anchor_target_labels.shape) # 22500, print(anchor_target_format_locations.shape) # 22500, 4

이렇게 하면, 최종적으로 label하고, loc을 계산한 앵커들이 나오게 됩니다.

# RPN

위의 과정이 RPN을 위한 사전 작업이라고 볼 수 있습니다.

mid_channel = 512 in_channel = 512 n_anchor = 9 conv1 = nn.Conv2d(in_channel, mid_channel, 3, 1, 1) reg_layer = nn.Conv2d(mid_channel, n_anchor*4, 1, 1, 0) cls_layer = nn.Conv2d(mid_channel, n_anchor*2, 1, 1, 0)

VGG Net 기준 conv layer의 output channel이 512이라서 in channel은 512가 되고,

box regression은 anchor 9 * 4 (location)

box classification은 anchor 9 * 2 (object or not)

이 됩니다.

x = conv1(output_map) anchor_pred_format_locations = reg_layer(x) anchor_pred_scores = cls_layer(x) print(anchor_pred_format_locations.shape) # torch.Size([1, 36, 50, 50]) print(anchor_pred_scores.shape) # torch.Size([1, 18, 50, 50])

weight 초기화를 거쳐, 추출한 feature map을 conv에 통과시키고,

location과 class를 예측합니다.

이렇게 되면 각 위치별 (50, 50) regression 과 classification 예측 값이 나오게 됩니다.

anchor_pred_format_locations = anchor_pred_format_locations.permute(0, 2, 3, 1).contiguous().view(1, -1, 4) anchor_pred_scores = anchor_pred_scores.permute(0, 2, 3, 1).contiguous().view(1, -1, 2) objectness_pred_scores = anchor_pred_scores[:, :, 1]

위에서 ground truth로 만든 anchor와 비교하기 위해, 형태를 맞춰줍니다.

print(anchor_target_labels.shape) print(anchor_target_format_locations.shape) print(anchor_pred_scores.shape) print(anchor_pred_format_locations.shape) gt_rpn_format_locs = torch.from_numpy(anchor_target_format_locations) gt_rpn_scores = torch.from_numpy(anchor_target_labels) rpn_format_locs = anchor_pred_format_locations[0] rpn_scores = anchor_pred_scores[0]

target은 bbox를 통해 만든 ground truth 값들, pred는 RPN으로 예측한 값들입니다.

둘 모두 reg 22500, 4 / cls 22500, 1이 됩니다.

numpy에서 torch로 변환해주고, batch 중 1개만 가져와주는 코드 (여기선 batch가 1)

####### Object or not loss rpn_cls_loss = F.cross_entropy(rpn_scores, gt_rpn_scores.long(), ignore_index=-1) print(rpn_cls_loss) ####### location loss mask = gt_rpn_scores > 0 mask_target_format_locs = gt_rpn_format_locs[mask] mask_pred_format_locs = rpn_format_locs[mask] print(mask_target_format_locs.shape) print(mask_pred_format_locs.shape) x = torch.abs(mask_target_format_locs – mask_pred_format_locs) rpn_loc_loss = ((x<0.5).float()*(x**2)*0.5 + (x>0.5).float()*(x-0.5)).sum() print(rpn_loc_loss)

object인지 아닌지는 cross entropy loss를, location은 실제 object인 것만 masking해서 loss 값을 계산합니다.

rpn_lambda = 10 N_reg = mask.float().sum() rpn_loss = rpn_cls_loss + rpn_lambda / N_reg * rpn_loc_loss print(rpn_loss)

cls loss와 loc loss는 lambda로 적절하게 합쳐주게 됩니다.

# Generating Proposal to Feed Fast R-CNN

RPN에서 구한 Proposal을 Fast R-CNN에서 학습할것만 남기는 과정입니다.

nms_thresh = 0.7 n_train_pre_nms = 12000 n_train_post_nms = 2000 n_test_pre_nms = 6000 n_test_post_nms = 300 min_size = 16

non-maximum suppression (NMS)으로 같은 클래스 정보를 가지는 박스들끼리 iou값을 비교해, 중복되는 것들은 제외해줍니다. 이 때 threshold가 nms_thresh 0.7입니다.

nms이전에 12000개만 우선적으로 남기게 되고,

nms를 하면 2000개의 최종 proposal만 남습니다.

이 2000개의 proposal로 Fast RCNN을 학습하게 됩니다.

box의 width 와 height가 16보다 작으면, 해당 proposal도 제외합니다.

Test에선 6000개, 300개만 남기게 됩니다. (현재 코드에선 사용 X)

print(anchors.shape) # 22500, 4 print(anchor_pred_format_locations.shape) # 22500, 4 rois = deformat_loc(anchors=anchors, formatted_base_anchor=anchor_pred_format_locations[0].data.numpy()) print(rois.shape) # 22500, 4 print(rois) #[[ -37.56205856 -83.65124834 55.51502551 96.9647187 ] # [ -59.50866938 -56.68875009 64.91222143 72.23375052] # [ -81.40298363 -41.99777969 96.39533509 49.35743635] # … # [ 610.35422226 414.3952291 979.0893042 1163.98340092] # [ 538.20066833 564.81064224 1041.29725647 1063.15491104] # [ 432.48094419 606.7697889 1166.24708388 973.39356325]]

이 부분은 좀 헷갈리는데, 예측한 location을 anchors를 통해 rois로 다시 바꿔줍니다. (bounding box)

rois[:, 0:4:2] = np.clip(rois[:, 0:4:2], a_min=0, a_max=image_size[0]) rois[:, 1:4:2] = np.clip(rois[:, 1:4:2], a_min=0, a_max=image_size[1]) print(rois) # [[ 0. 0. 55.51502551 96.9647187 ] # [ 0. 0. 64.91222143 72.23375052] # [ 0. 0. 96.39533509 49.35743635] # … # [610.35422226 414.3952291 800. 800. ] # [538.20066833 564.81064224 800. 800. ] # [432.48094419 606.7697889 800. 800. ]]

그리고 이미지 사이즈를 벗어나는 값들은 이미지 크기에 맞게 조정해줍니다.

h = rois[:, 2] – rois[:, 0] w = rois[:, 3] – rois[:, 1] valid_index = np.where((h>min_size)&(w>min_size))[0] valid_rois = rois[valid_index] valid_scores = objectness_pred_scores[0][valid_index].data.numpy()

그리고 box크기가 16보다 작은 것들은 제외하고

object score를 기준으로 정렬해줍니다.

valid_score_order = valid_scores.ravel().argsort()[::-1] pre_train_valid_score_order = valid_score_order[:n_train_pre_nms] pre_train_valid_rois = valid_rois[pre_train_valid_score_order] pre_train_valid_scores = valid_scores[pre_train_valid_score_order] print(pre_train_valid_rois.shape) # 12000, 4 print(pre_train_valid_scores.shape) # 12000, print(pre_train_valid_score_order.shape) # 12000,

nms를 적용하기 전 12000개만 가져오고

keep_index = nms(rois=pre_train_valid_rois, scores=pre_train_valid_scores, nms_thresh=nms_thresh) post_train_valid_rois = pre_train_valid_rois[keep_index][:n_train_post_nms] post_train_valid_scores = pre_train_valid_scores[keep_index][:n_train_post_nms] print(post_train_valid_rois.shape) # 2000, 4 print(post_train_valid_scores.shape) # 2000,

nms를 적용해 2000개의 roi만 남깁니다.

2000개도 생각보다 많습니다.

# anchor box labeling for Fast R-CNN

n_sample = 128 pos_ratio = 0.25 pos_iou_thresh = 0.5 neg_iou_thresh_hi = 0.5 neg_iou_thresh_lo = 0.0

여기서부턴 RPN에서 ground truth를 만드는 과정과 같습니다.

단지 Fast RCNN을 위한 ground truth를 만드는 것이 차이 (실제 클래스, 실제 bounding box)

ious = bbox_iou(post_train_valid_rois, bbox) print(ious.shape) # 2000, 2

위에서 구한 2000개의 roi와 bbox를 비교해 iou를 계산해줍니다.

RPN에선 8940, 2였는데 2000개만 비교해주면 되니, 2000, 2의 배열이 만들어지게 됩니다.

bbox_assignments = ious.argmax(axis=1) roi_max_ious = ious.max(axis=1) roi_target_labels = labels[bbox_assignments] print(roi_target_labels.shape) # 2000

여기선 anchor에서 큰 값인 애들을 실제 label (6, 8)로 각각 할당해주게 됩니다.

0번째가 크면 6 1번째가 크면 8입니다. (헷갈리신다면 코드 구현 맨 위에서 box label값을 확인해보세요)

6 8 6 6 8 6 6 6과 같은 형태의 배열이 만들어지게 되는데

이게 전부 target일수가 없겠죠?

total_n_pos = len(np.where(roi_max_ious >= pos_iou_thresh)[0]) n_pos_sample = n_sample*pos_ratio if total_n_pos > n_sample*pos_ratio else total_n_pos n_neg_sample = n_sample – n_pos_sample print(n_pos_sample) # 10 print(n_neg_sample) # 118

그래서 positive threshold에 따라 positive인 애들과 negative인 애들을 128개만 sampling합니다. (n_sample)

pos_index = np.where(roi_max_ious >= pos_iou_thresh)[0] pos_index = np.random.choice(pos_index, size=n_pos_sample, replace=False) neg_index = np.where((roi_max_ious < neg_iou_thresh_hi) & (roi_max_ious > neg_iou_thresh_lo))[0] neg_index = np.random.choice(neg_index, size=n_neg_sample, replace=False) print(pos_index.shape) # 10 print(neg_index.shape) # 118

positive index와 negative index를 가져오고, (pos ratio가 0.25이기 때문에 positive box는 최대 32개만 가져옴)

keep_index = np.append(pos_index, neg_index) post_sample_target_labels = roi_target_labels[keep_index].data.numpy() post_sample_target_labels[len(pos_index):] = 0 post_sample_rois = post_train_valid_rois[keep_index]

최종적으로 sampling까지 끝낸 roi들만 남기게 됩니다.

그 중에 positive만 뽑아보면 위의 그래프와 같습니다.

왼쪽 아래 박스가 라벨 6이고, 오른쪽 위 박스가 라벨 8이니

초록색은 6라벨을 위한 roi box가 되고, 빨간색은 라벨 8을 위한 roi box가 됩니다.

post_sample_bbox = bbox[bbox_assignments[keep_index]] post_sample_format_rois = format_loc(anchors=post_sample_rois, base_anchors=post_sample_bbox.data.numpy()) print(post_sample_format_rois.shape)

이를 Fast R-CNN과 비교하기 위한 loc 형태로 변환해주면 target box도 끝

# Fast R-CNN

rois = torch.from_numpy(post_sample_rois).float() print(rois.shape) # 128, 4 # roi_indices = torch.zeros((len(rois),1), dtype=torch.float32) # print(rois.shape, roi_indices.shape) # indices_and_rois = torch.cat([roi_indices, rois], dim=1) # print(indices_and_rois.shape)

roi를 torch로 변환해주고,

밑에 주석처리된 코드는 batch별로 계산해주기 위해

batch별 index를 할당해주고 인덱스, roi로 배열을 만들어주는 코드입니다 (여기선 batch 1)

RoI Pooling

size = (7, 7) adaptive_max_pool = nn.AdaptiveMaxPool2d(size) # correspond to feature map rois.mul_(1/16.0) rois = rois.long()

roi pooling을 통해 고정된 크기로 추출합니다.

그리고 128개의 rois들은 각각 50,50의 공간에 매핑됩니다.

output = [] num_rois = len(rois) for roi in rois: roi_feature = output_map[…, roi[0]:roi[2]+1, roi[1]:roi[3]+1] output.append(adaptive_max_pool(roi_feature)) output = torch.cat(output, 0) print(output.shape) # 128, 512, 7, 7

각각의 roi를 pooling layer를 거쳐, 고정된 크기로 추출해주면 128, 512, 7, 7의 결과가 나오게 됩니다.

이미지 크기에 자유롭기 위해 roi pooling layer를 사용해준 모습이고,

output_ROI_pooling = output.view(output.size(0), -1) print(output_ROI_pooling.shape) # 128, 25088

이를 일자로 펴주게되면 128, 25088의 배열이 나오게 됩니다.

RoI Head & Classifier, BBox Regression

roi_head = nn.Sequential(nn.Linear(25088, 4096), nn.Linear(4096, 4096)) cls_loc = nn.Linear(4096, 21*4) cls_loc.weight.data.normal_(0, 0.01) cls_loc.bias.data.zero_() cls_score = nn.Linear(4096, 21) cls_score.weight.data.normal_(0, 0.01) cls_score.bias.data.zero_() x = roi_head(output_ROI_pooling) roi_cls_loc = cls_loc(x) roi_cls_score = cls_score(x) print(roi_cls_loc.shape, roi_cls_score.shape) # 128, 84 / 128, 21

최종적으로 fully connected layer를 거쳐, 20 (class) + 1 (background)로 분류하게 됩니다.

location은 *4 (x1,y1,x2,y2)

Fast R-CNN Loss

print(roi_cls_loc.shape) # 128, 84 print(roi_cls_score.shape) # 128, 21

예측값

print(post_sample_format_rois.shape) # 128, 4 print(post_sample_target_labels.shape) # 128, gt_roi_cls_loc = torch.from_numpy(post_sample_format_rois).float() gt_roi_cls_label = torch.from_numpy(post_sample_target_labels).long()

실제값 ground truth입니다.

roi_cls_loss = F.cross_entropy(roi_cls_score, gt_roi_cls_label) print(roi_cls_loss)

cls loss는 cross entropy loss를

num_roi = roi_cls_loc.size(0) roi_cls_loc = roi_cls_loc.view(-1, 21, 4) roi_cls_loc = roi_cls_loc[torch.arange(num_roi), gt_roi_cls_label] print(roi_cls_loc.shape) mask = gt_roi_cls_label>0 mask_loc_pred = roi_cls_loc[mask] mask_loc_target = gt_roi_cls_loc[mask] print(mask_loc_pred.shape) # 10, 4 print(mask_loc_target.shape) # 10, 4 x = torch.abs(mask_loc_pred-mask_loc_target) roi_loc_loss = ((x<0.5).float()*x**2*0.5 + (x>0.5).float()*(x-0.5)).sum() print(roi_loc_loss)

Fast R-CNN도 마찬가지로 masking처리해서 label이 background가 아닌 것들만 bounding box regression하게 됩니다.

roi_lambda = 10 N_reg = (gt_roi_cls_label>0).float().sum() roi_loss = roi_cls_loss + roi_lambda / N_reg * roi_loc_loss print(roi_loss)

lambda를 적용해 Fast R-CNN의 Total loss를 구할 수 있습니다.

# Faster R-CNN Total Loss

total_loss = rpn_loss + roi_loss

Faster R-CNN의 Total loss는 rpn_loss와 roi_loss를 합친 값이 됩니다.

이 loss를 backward해서 network를 update하면 됩니다.

이 과정을 모듈화하는게 헬포인트

# Pytorch 클래스화

정리되지않아 코드가 길어, 접어놓았습니다. (오류도 좀 있고 이미지 크기나 여러 상황에 일반화가 되지 않았습니다.)

더보기 faster_rcnn.py import torch import torch.nn as nn import torch.nn.functional as F import torchvision from torchvision.ops import RoIPool import numpy as np from utils import * # Backbone from backbone import get_bb_clf # bbox = torch.FloatTensor([[30,20,500,400], [400,300,600,500]]) # labels = torch.LongTensor([6, 8]) import torch import torch.nn as nn import torch.nn.functional as F import torchvision import numpy as np from utils import * from creator_tools import * # bbox = torch.FloatTensor([[30,20,500,400], [400,300,600,500]]) # labels = torch.LongTensor([6, 8]) # RPN class RPN(nn.Module): def __init__( self, in_c=512, mid_c=512, image_size=(800,800), sub_sample=16, anchor_scale=[8,16,32], ratio=[0.5,1,2], ): super(RPN, self).__init__() self.rpn = nn.Conv2d(in_c, mid_c, 3, 1, 1) self.relu = nn.ReLU(inplace=True) n_anchor = len(anchor_scale) * len(ratio) self.reg = nn.Conv2d(mid_c, n_anchor*4, 1, 1, 0) self.cls = nn.Conv2d(mid_c, n_anchor*2, 1, 1, 0) self.anchor_base = generate_anchors(image_size, sub_sample=sub_sample, anchor_scale=anchor_scale, ratio=ratio) self.proposal_layer = ProposalCreator(self) weight_init(self.rpn) weight_init(self.reg) weight_init(self.cls) # x : feature map def forward(self, x, img_size, scale=1.): n, _, h, w = x.shape anchor = self.anchor_base n_anchor = anchor.shape[0] // (h * w) # 9 x = self.rpn(x) x = self.relu(x) pred_loc = self.reg(x) # batch, anchor*4, height, width pred_cls = self.cls(x) # batch, anchor*2, height, width pred_loc = pred_loc.permute(0, 2, 3, 1).contiguous().view(n, -1, 4) # batch anchors (coor) pred_cls = pred_cls.permute(0, 2, 3, 1).contiguous() # batch anchors (obj) pred_sfmax_cls = F.softmax(pred_cls.view(n, h, w, n_anchor, 2), dim=4) pred_fg_cls = pred_sfmax_cls[:,:,:,:,1].contiguous() pred_fg_cls = pred_fg_cls.view(n, -1) pred_cls = pred_cls.view(n, -1, 2) pred_object = pred_cls[:, :, 1] rois = [] roi_indices = [] for i in range(n): roi = self.proposal_layer( pred_loc[i].cpu().data.numpy(), pred_fg_cls[i].cpu().data.numpy(), anchor, img_size,scale=scale) batch_index = i * np.ones((len(roi),), dtype=np.int32) rois.append(roi) roi_indices.append(batch_index) rois = np.concatenate(rois, axis=0) roi_indices = np.concatenate(roi_indices, axis=0) return pred_loc, pred_cls, rois, roi_indices, anchor # target_loc, target_cls = assign_cls_loc로 만든 것 def rpn_loss(pred_loc, pred_cls, target_loc, target_cls, rpn_lamda=10): # cls loss # print(pred_cls.shape) gt_rpn_cls = torch.from_numpy(target_cls).long().to(‘cuda:0’) pred_rpn_cls = pred_cls[0].to(‘cuda:0’) # print(pred_rpn_cls.shape, gt_rpn_cls.shape) rpn_cls_loss = F.cross_entropy(pred_rpn_cls, gt_rpn_cls, ignore_index=-1) # reg loss gt_rpn_loc = torch.from_numpy(target_loc).to(‘cuda:0’) pred_rpn_loc = pred_loc[0].to(‘cuda:0’) mask = gt_rpn_cls > 0 mask_gt_loc = gt_rpn_loc[mask] mask_pred_loc = pred_rpn_loc[mask] x = torch.abs(mask_gt_loc – mask_pred_loc) rpn_loc_loss = ((x<0.5).float()*(x**2)*0.5 + (x>0.5).float()*(x-0.5)).sum() N_reg = mask.float().sum() rpn_loss = rpn_cls_loss + rpn_lamda / N_reg * rpn_loc_loss return rpn_cls_loss, rpn_loc_loss, rpn_loss # class RoIHead(nn.Module): # def __init__(self, n_class, roi_size, spatial_scale, classifier): # super(RoIHead, self).__init__() # self.classifier = classifier # self.cls_loc class FastRCNN(nn.Module): def __init__(self, classifier, n_class=21, size=(7,7), spatial_scale=(1./16)): super(FastRCNN, self).__init__() self.roi = RoIPool(size, spatial_scale) self.roi_pool = nn.AdaptiveMaxPool2d(size) self.classifier = classifier self.reg = nn.Linear(4096, n_class*4) weight_init(self.reg) self.cls = nn.Linear(4096, n_class) weight_init(self.cls) def forward(self, feature_map, rois, roi_indices): # correspond to feature map roi_indices = totensor(roi_indices).float() rois = totensor(rois).float() indices_rois = t.cat([roi_indices[:, None], rois], dim=1).contiguous() pool = self.roi(feature_map, indices_rois) pool = pool.view(pool.size(0), -1) x = self.classifier(pool) roi_loc = self.reg(x) roi_cls = self.cls(x) return roi_loc, roi_cls # gt_loc = torch.from_numpy(final_rois).float() # gt_cls = torch.from_numpy(final_cls).long() def fastrcnn_loss(roi_loc, roi_cls, gt_loc, gt_cls): # [128, 84], [128, 21], [128, 4] torch float, [128, 1] torch long roi_cls = roi_cls.to(‘cuda:0’) gt_cls = gt_cls.to(‘cuda:0’) roi_loc = roi_loc.to(‘cuda:0’) gt_loc = torch.from_numpy(gt_loc).float().to(‘cuda:0’) # print(roi_cls) # print(roi_cls.shape, gt_cls.shape, roi_loc.shape, gt_loc.shape) cls_loss = F.cross_entropy(roi_cls, gt_cls) # print(cls_loss) num_roi = roi_loc.size(0) roi_loc = roi_loc.view(-1, 21, 4) roi_loc = roi_loc[torch.arange(num_roi), gt_cls] mask = gt_cls>0 mask_loc_pred = roi_loc[mask] mask_loc_target = gt_loc[mask] x = torch.abs(mask_loc_pred-mask_loc_target) loc_loss = ((x<0.5).float()*x**2*0.5 + (x>0.5).float()*(x-0.5)).sum() # print(loc_loss) roi_lamda = 10 N_reg = (gt_cls>0).float().sum() roi_loss = cls_loss + roi_lamda / N_reg * loc_loss return cls_loss, loc_loss, roi_loss class FasterRCNN(nn.Module): def __init__(self, backbone, rpn, head): super(FasterRCNN, self).__init__() self.backbone = backbone self.rpn = rpn self.head = head # Fast R-CNN self.proposal_target_creator = ProposalTargetCreator() def forward(self, img, bboxes, labels): b, c, h, w = img.shape ##### backbone feature_map = self.backbone(img) ##### RPN # anchors = generate_anchors((w, h)) # target_cls, target_loc = assign_cls_loc(bboxes, anchors, (w, h)) pred_loc, pred_cls, rois, roi_indices, anchor = self.rpn(feature_map, (w,h), scale=1.) target_cls, target_loc = assign_cls_loc(bboxes, anchor, (w,h)) rpn_loc_loss, rpn_cls_loss, t_rpn_loss = rpn_loss(pred_loc, pred_cls, target_loc, target_cls) # pred_loc, pred_cls, pred_object = sample_roi, gt_roi_loc, gt_roi_label = self.proposal_target_creator(rois, bboxes, labels) sample_roi_index = t.zeros(len(sample_roi)) ##### HEAD – Fast RCNN final_loc, final_cls = self.head(feature_map, sample_roi, sample_roi_index) roi_cls_loss, roi_loc_loss, t_roi_loss = fastrcnn_loss(final_loc, final_cls, gt_roi_loc, gt_roi_label) t_loss = torch.sum(t_roi_loss + t_rpn_loss) return rpn_loc_loss, rpn_cls_loss, roi_cls_loss, roi_loc_loss, t_loss # post_train_rois, post_train_scores = generate_proposal(anchors, pred_loc, pred_cls, pred_object, (w, h)) # final_rois, final_cls = assign_targets(post_train_rois, post_train_scores, bboxes, labels) # final_rois, final_cls = torch.from_numpy(final_rois).float(), torch.from_numpy(final_cls).long() # rois = torch.from_numpy(final_rois).float() # roi_loc, roi_cls = self.fastrcnn(feature_map, final_rois) # gt_loc = final_rois # gt_cls = final_cls # return final_loc, final_cls, rois, roi_indices def fasterrcnn_loss(rpn_loss, roi_loss): return torch.sum(rpn_loss + roi_loss) class FasterRCNNSEMob(FasterRCNN): down_size = 16 def __init__(self, n_fg_class=20, ratios=[0.5, 1, 2], anchor_scales=[8,16,32]): backbone, classifier = get_bb_clf() rpn = RPN() head = FastRCNN(classifier, n_class=n_fg_class+1, spatial_scale=(1./16)) super(FasterRCNNSEMob, self).__init__(backbone, rpn, head) def assign_cls_loc(bboxes, anchors, image_size, pos_thres=0.7, neg_thres=0.3, n_sample=256, pos_ratio=0.5): valid_idx = np.where((anchors[:, 0] >= 0) &(anchors[:, 1] >= 0) &(anchors[:, 2] <= image_size[0]) &(anchors[:, 3] <= image_size[1]))[0] # print(valid_idx.shape) valid_cls = np.empty((valid_idx.shape[0], ), dtype=np.int32) valid_cls.fill(-1) valid_anchors = anchors[valid_idx] ious = bbox_iou(valid_anchors, bboxes.numpy()) # print(ious.shape) # 8940, 2 # valid cls에 positive로 판단하는 것이 총 두 시나리오에 의해 생성됨 # a iou_by_anchor = np.amax(ious, axis=1) # anchor별 최대값 pos_idx = np.where(iou_by_anchor >= pos_thres)[0] neg_idx = np.where(iou_by_anchor < neg_thres)[0] valid_cls[pos_idx] = 1 valid_cls[neg_idx] = 0 # b iou_by_gt = np.amax(ious, axis=0) # gt box별 최대 값 gt_idx = np.where(ious == iou_by_gt)[0] # print(gt_idx) valid_cls[gt_idx] = 1 total_n_pos = len(np.where(valid_cls == 1)[0]) n_pos = int(n_sample*pos_ratio) if total_n_pos > n_sample*pos_ratio else total_n_pos n_neg = n_sample – n_pos # valid label에서 256개 넘는 것은 제외 pos_index = np.where(valid_cls == 1)[0] # print(pos_index, len(pos_index, n_pos)) if len(pos_index) > n_sample*pos_ratio: disable_index = np.random.choice(pos_index, size=len(pos_index)-n_pos, replace=False) valid_cls[disable_index] = -1 neg_index = np.where(valid_cls == 0)[0] disable_index = np.random.choice(neg_index, size=len(neg_index) – n_neg, replace=False) valid_cls[disable_index] = -1 # 최종 valid class (object or not) # print(len(np.where(valid_cls==1)[0]), len(np.where(valid_cls==0)[0])) # valid loc # Anchor별로 iou가 더 높은쪽으로 loc 분배 argmax_iou = np.argmax(ious, axis=1) max_iou_box = bboxes[argmax_iou].numpy() # valid_anchors와 shape 같아야함 valid_loc = format_loc(valid_anchors, max_iou_box) # print(valid_loc.shape) # 8940, 4 dx dy dw dh # 기존 anchor에서 valid index에 지금까지 구한 valid label (pos, neg 18, 238) 할당 target_cls = np.empty((len(anchors),), dtype=np.int32) target_cls.fill(-1) target_cls[valid_idx] = valid_cls # 기존 anchor에서 valid index에 지금까지 구한 dx, dy, dw, dh 할당 target_loc = np.zeros((len(anchors), 4), dtype=np.float32) target_loc[valid_idx] = valid_loc # print(target_cls.shape) # print(target_loc.shape) return target_cls, target_loc # for Fast RCNN def generate_proposal(anchors, pred_loc, pred_cls, pred_object, image_size, n_train_pre_nms=12000, n_train_post_nms=2000, n_test_pre_nms=6000, n_test_post_nms=300, min_size=16, nms_thresh=0.7): rois = deformat_loc(anchors=anchors, formatted_base_anchor=pred_loc[0].cpu().data.numpy()) np.where(rois[:,0]) rois[:, [0,2]] = np.clip(rois[:, [0,2]], a_min=0, a_max=image_size[0]) # x [0 ~ 800] width rois[:, [1,3]] = np.clip(rois[:, [1,3]], a_min=0, a_max=image_size[1]) # y [0 ~ 800] height w = rois[:, 2] – rois[:, 0] h = rois[:, 3] – rois[:, 1] valid_idx = np.where((h>min_size)&(w>min_size))[0] valid_rois = rois[valid_idx] valid_scores = pred_object[0][valid_idx].cpu().data.numpy() order_idx = valid_scores.ravel().argsort()[::-1] pre_train_idx = order_idx[:n_train_pre_nms] pre_train_rois = valid_rois[pre_train_idx] pre_train_scores = valid_scores[pre_train_idx] keep_index = nms(rois=pre_train_rois, scores=pre_train_scores, nms_thresh=nms_thresh) post_train_rois = pre_train_rois[keep_index][:n_train_post_nms] post_train_scores = pre_train_scores[keep_index][:n_train_post_nms] return post_train_rois, post_train_scores def assign_targets(post_train_rois, post_train_scores, bboxes, labels, n_sample = 128, pos_ratio = 0.25, pos_thresh = 0.5, neg_thresh_hi = 0.5, neg_thresh_lo = 0.0): ious = bbox_iou(post_train_rois, bboxes.numpy()) # cls bbox_idx = ious.argmax(axis=1) box_max_ious = ious.max(axis=1) final_cls = labels[bbox_idx] # 2000, Object Class 값 들어감 total_n_pos = len(np.where(box_max_ious >= pos_thresh)[0]) n_pos = int(n_sample*pos_ratio) if total_n_pos > n_sample*pos_ratio else total_n_pos n_neg = n_sample – n_pos pos_index = np.where(box_max_ious >= pos_thresh)[0] pos_index = np.random.choice(pos_index, size=n_pos, replace=False) neg_index = np.where((box_max_ious < neg_thresh_hi) & (box_max_ious >= neg_thresh_lo))[0] neg_index = np.random.choice(neg_index, size=n_neg, replace=False) keep_index = np.append(pos_index, neg_index) final_cls = final_cls[keep_index].data.numpy() final_cls[len(pos_index):] = 0 final_rois = post_train_rois[keep_index] post_sample_bbox = bboxes[bbox_idx[keep_index]] d_rois = format_loc(anchors=final_rois, base_anchors=post_sample_bbox.data.numpy()) return final_rois, final_cls def weight_init(l): if type(l) in [nn.Conv2d]: l.weight.data.normal_(0, 0.01) l.bias.data.zero_() if __name__ == “__main__”: pass backbone.py import torch.nn as nn class SEBlock(nn.Module): def __init__(self, c, r=16): super(SEBlock, self).__init__() self.squeeze = nn.AdaptiveAvgPool2d(1) self.excitation = nn.Sequential( nn.Linear(c, c // r, bias=False), nn.ReLU(inplace=True), nn.Linear(c // r, c, bias=False), nn.Sigmoid() ) def forward(self, x): b, c, _, _ = x.size() se = self.squeeze(x).view(b, c) se = self.excitation(se).view(b, c, 1, 1) return x * se.expand_as(x) def mobile_block(in_dim, out_dim, stride=1): return nn.Sequential( nn.Conv2d(in_channels=in_dim, out_channels=in_dim, kernel_size=3, stride=stride, padding=1, groups=in_dim), nn.BatchNorm2d(in_dim), nn.ReLU(inplace=True), nn.Conv2d(in_channels=in_dim, out_channels=out_dim, kernel_size=1, stride=1, padding=0), nn.BatchNorm2d(out_dim), nn.ReLU(inplace=True), SEBlock(c=out_dim, r=16), ) class SEMobileNet(nn.Module): def __init__(self, width_multi=1, resolution_multi=1, num_classes=1000): super(SEMobileNet, self).__init__() base_width = int(32 * width_multi) self.conv = nn.Sequential( nn.Conv2d(in_channels=3, out_channels=base_width, kernel_size=3, stride=2, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), mobile_block(base_width, base_width*2), mobile_block(base_width*2, base_width*4, 2), mobile_block(base_width*4, base_width*4), mobile_block(base_width*4, base_width*8, 2), mobile_block(base_width*8, base_width*8), mobile_block(base_width*8, base_width*16, 2), # 800×800 -> 50×50 *[mobile_block(base_width*16, base_width*16) for _ in range(5)], # 512 channel mobile_block(base_width*16, base_width*32, 2), mobile_block(base_width*32, base_width*32), nn.AvgPool2d(7), ) self.classifier = nn.Linear(base_width*32, num_classes) def forward(self, x): x = self.conv(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x def get_bb_clf(): model = SEMobileNet() backbone = model.conv[:-3] for p in backbone[:2].parameters(): p.requires_grad = False return backbone, nn.Sequential(nn.Linear(512*7*7, 4096), nn.ReLU(inplace=True)) creator_tools.py (simple faster rcnn code) from utils import * import torch class ProposalCreator: “””Proposal regions are generated by calling this object. The :meth:`__call__` of this object outputs object detection proposals by applying estimated bounding box offsets to a set of anchors. This class takes parameters to control number of bounding boxes to pass to NMS and keep after NMS. If the paramters are negative, it uses all the bounding boxes supplied or keep all the bounding boxes returned by NMS. This class is used for Region Proposal Networks introduced in Faster R-CNN [#]_. .. [#] Shaoqing Ren, Kaiming He, Ross Girshick, Jian Sun. \ Faster R-CNN: Towards Real-Time Object Detection with \ Region Proposal Networks. NIPS 2015. Args: nms_thresh (float): Threshold value used when calling NMS. n_train_pre_nms (int): Number of top scored bounding boxes to keep before passing to NMS in train mode. n_train_post_nms (int): Number of top scored bounding boxes to keep after passing to NMS in train mode. n_test_pre_nms (int): Number of top scored bounding boxes to keep before passing to NMS in test mode. n_test_post_nms (int): Number of top scored bounding boxes to keep after passing to NMS in test mode. force_cpu_nms (bool): If this is :obj:`True`, always use NMS in CPU mode. If :obj:`False`, the NMS mode is selected based on the type of inputs. min_size (int): A paramter to determine the threshold on discarding bounding boxes based on their sizes. “”” def __init__(self, parent_model, nms_thresh=0.7, n_train_pre_nms=12000, n_train_post_nms=2000, n_test_pre_nms=6000, n_test_post_nms=300, min_size=16 ): self.parent_model = parent_model self.nms_thresh = nms_thresh self.n_train_pre_nms = n_train_pre_nms self.n_train_post_nms = n_train_post_nms self.n_test_pre_nms = n_test_pre_nms self.n_test_post_nms = n_test_post_nms self.min_size = min_size def __call__(self, loc, score, anchor, img_size, scale=1.): “””input should be ndarray Propose RoIs. Inputs :obj:`loc, score, anchor` refer to the same anchor when indexed by the same index. On notations, :math:`R` is the total number of anchors. This is equal to product of the height and the width of an image and the number of anchor bases per pixel. Type of the output is same as the inputs. Args: loc (array): Predicted offsets and scaling to anchors. Its shape is :math:`(R, 4)`. score (array): Predicted foreground probability for anchors. Its shape is :math:`(R,)`. anchor (array): Coordinates of anchors. Its shape is :math:`(R, 4)`. img_size (tuple of ints): A tuple :obj:`height, width`, which contains image size after scaling. scale (float): The scaling factor used to scale an image after reading it from a file. Returns: array: An array of coordinates of proposal boxes. Its shape is :math:`(S, 4)`. :math:`S` is less than :obj:`self.n_test_post_nms` in test time and less than :obj:`self.n_train_post_nms` in train time. :math:`S` depends on the size of the predicted bounding boxes and the number of bounding boxes discarded by NMS. “”” # NOTE: when test, remember # faster_rcnn.eval() # to set self.traing = False if self.parent_model.training: n_pre_nms = self.n_train_pre_nms n_post_nms = self.n_train_post_nms else: n_pre_nms = self.n_test_pre_nms n_post_nms = self.n_test_post_nms # Convert anchors into proposal via bbox transformations. # roi = loc2bbox(anchor, loc) roi = deformat_loc(anchor, loc) # Clip predicted boxes to image. roi[:, [0,2]] = np.clip(roi[:, [0,2]], a_min=0, a_max=img_size[0]) # x [0 ~ 800] width roi[:, [1,3]] = np.clip(roi[:, [1,3]], a_min=0, a_max=img_size[1]) # y [0 ~ 800] height w = roi[:, 2] – roi[:, 0] h = roi[:, 3] – roi[:, 1] # Remove predicted boxes with either height or width < threshold. min_size = self.min_size * scale keep = np.where((h >= min_size) & (w >= min_size))[0] roi = roi[keep, :] score = score[keep] # Sort all (proposal, score) pairs by score from highest to lowest. # Take top pre_nms_topN (e.g. 6000). order = score.ravel().argsort()[::-1] if n_pre_nms > 0: order = order[:n_pre_nms] roi = roi[order, :] score = score[order] # Apply nms (e.g. threshold = 0.7). # Take after_nms_topN (e.g. 300). # unNOTE: somthing is wrong here! # TODO: remove cuda.to_gpu keep = nms( torch.from_numpy(roi).cuda(), torch.from_numpy(score).cuda(), self.nms_thresh) if n_post_nms > 0: keep = keep[:n_post_nms] roi = roi[keep] #.cpu().numpy() return roi class ProposalTargetCreator(object): “””Assign ground truth bounding boxes to given RoIs. The :meth:`__call__` of this class generates training targets for each object proposal. This is used to train Faster RCNN [#]_. .. [#] Shaoqing Ren, Kaiming He, Ross Girshick, Jian Sun. \ Faster R-CNN: Towards Real-Time Object Detection with \ Region Proposal Networks. NIPS 2015. Args: n_sample (int): The number of sampled regions. pos_ratio (float): Fraction of regions that is labeled as a foreground. pos_iou_thresh (float): IoU threshold for a RoI to be considered as a foreground. neg_iou_thresh_hi (float): RoI is considered to be the background if IoU is in [:obj:`neg_iou_thresh_hi`, :obj:`neg_iou_thresh_hi`). neg_iou_thresh_lo (float): See above. “”” def __init__(self, n_sample=128, pos_ratio=0.25, pos_iou_thresh=0.5, neg_iou_thresh_hi=0.5, neg_iou_thresh_lo=0.0 ): self.n_sample = n_sample self.pos_ratio = pos_ratio self.pos_iou_thresh = pos_iou_thresh self.neg_iou_thresh_hi = neg_iou_thresh_hi self.neg_iou_thresh_lo = neg_iou_thresh_lo # NOTE:default 0.1 in py-faster-rcnn def __call__(self, roi, bbox, label, loc_normalize_mean=(0., 0., 0., 0.), loc_normalize_std=(0.1, 0.1, 0.2, 0.2)): “””Assigns ground truth to sampled proposals. This function samples total of :obj:`self.n_sample` RoIs from the combination of :obj:`roi` and :obj:`bbox`. The RoIs are assigned with the ground truth class labels as well as bounding box offsets and scales to match the ground truth bounding boxes. As many as :obj:`pos_ratio * self.n_sample` RoIs are sampled as foregrounds. Offsets and scales of bounding boxes are calculated using :func:`model.utils.bbox_tools.bbox2loc`. Also, types of input arrays and output arrays are same. Here are notations. * :math:`S` is the total number of sampled RoIs, which equals \ :obj:`self.n_sample`. * :math:`L` is number of object classes possibly including the \ background. Args: roi (array): Region of Interests (RoIs) from which we sample. Its shape is :math:`(R, 4)` bbox (array): The coordinates of ground truth bounding boxes. Its shape is :math:`(R’, 4)`. label (array): Ground truth bounding box labels. Its shape is :math:`(R’,)`. Its range is :math:`[0, L – 1]`, where :math:`L` is the number of foreground classes. loc_normalize_mean (tuple of four floats): Mean values to normalize coordinates of bouding boxes. loc_normalize_std (tupler of four floats): Standard deviation of the coordinates of bounding boxes. Returns: (array, array, array): * **sample_roi**: Regions of interests that are sampled. \ Its shape is :math:`(S, 4)`. * **gt_roi_loc**: Offsets and scales to match \ the sampled RoIs to the ground truth bounding boxes. \ Its shape is :math:`(S, 4)`. * **gt_roi_label**: Labels assigned to sampled RoIs. Its shape is \ :math:`(S,)`. Its range is :math:`[0, L]`. The label with \ value 0 is the background. “”” n_bbox, _ = bbox.shape roi = np.concatenate((roi, bbox), axis=0) pos_roi_per_image = np.round(self.n_sample * self.pos_ratio) iou = bbox_iou(roi, bbox) gt_assignment = iou.argmax(axis=1) max_iou = iou.max(axis=1) # Offset range of classes from [0, n_fg_class – 1] to [1, n_fg_class]. # The label with value 0 is the background. gt_roi_label = label[gt_assignment] + 1 # Select foreground RoIs as those with >= pos_iou_thresh IoU. pos_index = np.where(max_iou >= self.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) # Select background RoIs as those within # [neg_iou_thresh_lo, neg_iou_thresh_hi). neg_index = np.where((max_iou < self.neg_iou_thresh_hi) & (max_iou >= self.neg_iou_thresh_lo))[0] neg_roi_per_this_image = self.n_sample – pos_roi_per_this_image neg_roi_per_this_image = int(min(neg_roi_per_this_image, neg_index.size)) if neg_index.size > 0: neg_index = np.random.choice( neg_index, size=neg_roi_per_this_image, replace=False) # The indices that we’re selecting (both positive and negative). keep_index = np.append(pos_index, neg_index) gt_roi_label = gt_roi_label[keep_index] gt_roi_label[pos_roi_per_this_image:] = 0 # negative labels –> 0 sample_roi = roi[keep_index] # Compute offsets and scales to match sampled RoIs to the GTs. gt_roi_loc = format_loc(sample_roi, bbox[gt_assignment[keep_index]]) gt_roi_loc = ((gt_roi_loc – np.array(loc_normalize_mean, np.float32) ) / np.array(loc_normalize_std, np.float32)) return sample_roi, gt_roi_loc, gt_roi_label utils.py (faster rcnn from scratch code) import numpy as np “”” tools to convert specified type “”” import torch as t import numpy as np def tonumpy(data): if isinstance(data, np.ndarray): return data if isinstance(data, t.Tensor): return data.detach().cpu().numpy() def totensor(data, cuda=True): if isinstance(data, np.ndarray): tensor = t.from_numpy(data) if isinstance(data, t.Tensor): tensor = data.detach() if cuda: tensor = tensor.cuda() return tensor def scalar(data): if isinstance(data, np.ndarray): return data.reshape(1)[0] if isinstance(data, t.Tensor): return data.item() def generate_anchors(image_size, sub_sample=16, anchor_scale=[8,16,32], ratio=[0.5,1,2]): len_ratio = len(ratio) anchor_base = np.zeros((len(anchor_scale)*len_ratio, 4)) # 9×4 for idx, scale in enumerate(anchor_scale): w = scale / np.sqrt(ratio) * sub_sample h = scale * np.sqrt(ratio) * sub_sample x1, y1, x2, y2 = -w/2, -h/2, w/2, h/2 anchor_base[idx*len_ratio:(idx+1)*len_ratio] = np.c_[x1, y1, x2, y2] feature_map_size = image_size[0] // sub_sample, image_size[1] // sub_sample ctr_x = np.arange(sub_sample//2, image_size[0], sub_sample) ctr_y = np.arange(sub_sample//2, image_size[1], sub_sample) ctr = np.zeros((*feature_map_size, 2)) for idx, y in enumerate(ctr_y): ctr[idx, :, 0] = ctr_x ctr[idx, :, 1] = y anchors = np.zeros((*feature_map_size, *anchor_base.shape)) for idx_x in range(feature_map_size[0]): for idx_y in range(feature_map_size[1]): anchors[idx_x, idx_y] = (ctr[idx_x, idx_y] + anchor_base.reshape(-1, 2, 2)).reshape(-1, 4) return anchors.reshape(-1, 4) # bbox iou 계산, (num_of_boxes1, 4) x (num_of_boxes2, 4) # bboxes_1: anchor, bboxes_2: target box # shape : x1 x2 y1 y2 def bbox_iou(bboxes_1, bboxes_2): len_bboxes_1 = bboxes_1.shape[0] len_bboxes_2 = bboxes_2.shape[0] ious = np.zeros((len_bboxes_1, len_bboxes_2)) for idx, bbox_1 in enumerate(bboxes_1): yy1_max = np.maximum(bbox_1[1], bboxes_2[:, 1]) xx1_max = np.maximum(bbox_1[0], bboxes_2[:, 0]) yy2_min = np.minimum(bbox_1[3], bboxes_2[:, 3]) xx2_min = np.minimum(bbox_1[2], bboxes_2[:, 2]) height = np.maximum(0.0, yy2_min – yy1_max) width = np.maximum(0.0, xx2_min – xx1_max) eps = np.finfo(np.float32).eps inter = height * width union = (bbox_1[3] – bbox_1[1]) * (bbox_1[2] – bbox_1[0]) + \ (bboxes_2[:, 3] – bboxes_2[:, 1]) * (bboxes_2[:, 2] – bboxes_2[:, 0]) – inter + eps iou = inter / union ious[idx] = iou return ious # ious (num_of_boxes1, num_of_boxes2) # (x1, y1, x2, y2) -> (x, y, w, h) -> (dx, dy, dw, dh) ”’ t_{x} = (x – x_{a})/w_{a} t_{y} = (y – y_{a})/h_{a} t_{w} = log(w/ w_a) t_{h} = log(h/ h_a) anchors are the anchors base_anchors are the boxes ”’ def format_loc(anchors, base_anchors): width = anchors[:, 2] – anchors[:, 0] height = anchors[:, 3] – anchors[:, 1] ctr_x = anchors[:, 0] + width*0.5 ctr_y = anchors[:, 1] + height*0.5 base_width = base_anchors[:, 2] – base_anchors[:, 0] base_height = base_anchors[:, 3] – base_anchors[:, 1] base_ctr_x = base_anchors[:, 0] + base_width*0.5 base_ctr_y = base_anchors[:, 1] + base_height*0.5 eps = np.finfo(np.float32).eps height = np.maximum(eps, height) width = np.maximum(eps, width) dx = (base_ctr_x – ctr_x) / width dy = (base_ctr_y – ctr_y) / height dw = np.log(base_width / width) dh = np.log(base_height / height) anchor_loc_target = np.stack((dx, dy, dw, dh), axis=1) return anchor_loc_target # (dx, dy, dw, dh) -> (x, y, w, h) -> (x1, y1, x2, y2) ”’ anchors are the default anchors formatted_base_anchors are the boxes with (dy, dx, dh, dw) ”’ def deformat_loc(anchors, formatted_base_anchor): width = anchors[:, 2] – anchors[:, 0] height = anchors[:, 3] – anchors[:, 1] ctr_x = anchors[:, 0] + width*0.5 ctr_y = anchors[:, 1] + height*0.5 dx, dy, dw, dh = formatted_base_anchor.T base_width = np.exp(dw) * width base_height = np.exp(dh) * height base_ctr_x = dx * width + ctr_x base_ctr_y = dy * height + ctr_y base_anchors = np.zeros_like(anchors) base_anchors[:, 0] = base_ctr_x – base_width*0.5 base_anchors[:, 1] = base_ctr_y – base_height*0.5 base_anchors[:, 2] = base_ctr_x + base_width*0.5 base_anchors[:, 3] = base_ctr_y + base_height*0.5 return base_anchors # non-maximum-suppression def nms(rois, scores, nms_thresh): # print(scores, scores.shape) order = (-scores).argsort().cpu().data.numpy()#[::-1] # x1, y1, x2, y2 = rois.T rois = rois.cpu().data.numpy() keep_index = [] # print(order.size) while order.size > 0: i = order[0] keep_index.append(i) ious = bbox_iou(rois[i][np.newaxis, :], rois[order[1:]]) inds = np.where(ious <= nms_thresh)[1] order = order[inds + 1] return np.asarray(keep_index) 며칠 코드짜고 보고 확인하면서 멘탈이 나가버렸습니다. 저는 AI하기엔 머리가 너무 analog인가봅니다... Ref. Faster R-CNN Article Faster R-CNN pytorch from scratch (KrisHan999) Faster R-CNN tensor flow Github (ganghee-lee) Simple Faster R-CNN Pytorch (chenyuntc) 반응형

Pytorch로 구현한 Faster R-CNN 모델

이번 포스팅에서는 How FasterRCNN works and step-by-step PyTorch implementation 영상에 올라온 pytorch로 구현한 Faster R-CNN 코드를 분석해보도록 하겠습니다. Faster R-CNN은 여러 코드 구현체가 있었지만, 살펴볼 코드가 RPN 내부에서 동작하는 여러 과정들을 직관적으로 잘 보여준 것 같아서 선정하게 되었습니다. 단일 이미지를 입력하여 Faster R-CNN 모델의 각 모듈의 입출력 데이터와 동작 과정을 쉽게 확인할 수 있습니다. 해당 모델에 대한 설명은 Faster R-CNN 논문 리뷰 포스팅 을 참고하시기 바랍니다.

zebras

저는 입력 이미지로 위의 얼룩말 이미지를 사용했습니다. 편의를 위해 원본 이미지를 800×800 크기로 resize해주었습니다. 실제 sub-sampling ratio=1/16으로 지정하여 feature extractor를 거친 feature map의 크기는 50×50이 됩니다. 코드는 제 github repository 에 정리해두었습니다.

Faster R-CNN

1) Feature extraction by pre-trained VGG16

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로 사용합니다. 이를 위해 원본 이미지와 크기가 같은 800x800 크기의 dummy 배열을 입력하여 50x50 크기의 feature map을 출력하는 layer를 찾습니다. 이후 faster_rcnn_feature_extractor 변수에 전체 모델에서 해당 layer까지만 저장합니다. 이후 원본 이미지를 faster_rcnn_feature_extractor에 입력하여 50x50x512 크기의 feature map을 얻습니다. 2) Anchor generation layer Anchor boxes 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(=50x50)개의 anchor box를 생성해야 합니다. 이를 위해 16x16 간격의 grid마다 anchor를 생성해준 후, anchor를 기준으로 서로 다른 scale과 aspect ratio를 가지는 9개의 anchor box를 생성해줍니다. anchor_boxes 변수에 전체 anchor box의 좌표(x1, y1, x2, y2)를 저장합니다(anchor_boxes 변수의 크기는 (22500, 4)입니다). 3) Anchor Target layer 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를 선택하는 작업을 수행합니다. 먼저 위와 같이 이미지 경계(=800x80) 내부에 있는 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와의 IoU값을 구합니다(이 부분에 대한 설명은 생략했습니다. 원본 코드 를 참고하시기 바랍니다). 그리고 각 ground truth 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으로 저장합니다.

mini-batch의 수는 256개로, positive/negative sample의 비율이 1:1이 되도록 구성합니다. 만약 positive sample의 수가 128개 이상인 경우, 남는 positive sample에 해당하는 sample은 label 변수에 -1로 지정합니다. negative sample에 대해서도 마찬가지로 수행합니다. 하지만 일반적으로 positive sample의 수가 128개 미만일 경우, 부족한만큼의 sample을 negative sample에서 추출합니다.

4) RPN(Region Proposal Network)

Region Proposal Network

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(Region Proposal Network)를 정의합니다. 1)번 과정을 통해 생성된 feature map에 3×3 conv 연산을 적용하는 layer를 정의합니다. 이후 1×1 conv 연산을 적용하여 9×4(anchor box의 수 x bounding box coordinates)개의 channel을 가지는 feature map을 반환하는 Bounding box regressor를 정의합니다. 마찬가지로 1×1 conv 연산을 적용하여 9×2(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)

1)번 과정에서 얻은 50x50x512 크기의 feature map을 3×3 conv layer에 입력합니다. 이를 통해 얻은 50x50x512 크기의 feature map을 Bounding box regressor, Classifier에 입력하여 각각 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를 구합니다.

5) Proposal layer

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 상위 N(n_train_pre_nms=12000)개의 anchor box에 대하여 Non maximum suppression 알고리즘을 수행합니다. 남은 anchor box 중 상위 N(n_train_post_nms=2000)개의 region proposals를 학습에 사용합니다. 6) Proposal Target layer 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 sample로, 0.5 미만인 box를 negative sample로 지정합니다(IoU를 구하는 과정은 코드 를 참고하시기 바랍니다). 전체 mini-batch sample 중 1/4, 즉 32개가 positive sample이 되도록 구성합니다. positive sample이 32개 미만인 경우 부족한 sample은 negative sample에서 구합니다(위의 코드에서 positive sample의 수는 21개, negative sample의 수는 107개입니다).

positive samples and negative samples

위의 그림에서 초록색 box는 ground truth box, 흰 색 box는 predicted bounding box입니다. 왼쪽 그림은 positive sample에 해당하는 box를, 오른쪽 그림은 negative sample에 해당하는 box를 시각화한 결과입니다. 왼쪽 그림에서는 얼룩말의 위치를 상대적을 잘 맞춘 반면, 오른쪽 그림에서는 다수의 box가 배경을 포함하고 있는 모습을 확인할 수 있습니다.

7) RoI pooling

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)

Feature extractor를 통해 얻은 feature map과 Proposal Target layer에서 추출한 region proposals을 활용하여 RoI pooling을 수행합니다. 이 때 output feature map의 크기가 7×7이 되도록 설정합니다.

8) Fast R-CNN

Fast R-CNN

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을 통해 얻은 7×7 크기의 feature map을 입력받을 fc layer를 정의합니다(첫 fc layer의 크기는 25088(7x7x512) x 4096입니다). class별로 bounding box coefficients를 예측하는 Bounding box regresor와 clas score를 예측하는 Classifier를 정의합니다. Multi-task를 구하는 부분은 RPN과 비슷하기에 생략했습니다.

지금까지 pytorch로 구현한 Faster R-CNN 모델을 살펴봤습니다. 단일 이미지를 입력받아 각 모듈별 입출력 데이터와 처리 과정을 상대적으로 쉽게 살펴볼 수 있어 모델 내부 구조를 파악하는데 도움이 되었습니다. 하지만 전체 학습 과정이나 detection 과정에 대한 구현 과정이 살짝 부족하여 조금 더 매운맛 버전인 jwyang님의 Faster R-CNN 구현 코드 를 분석해볼 계획입니다.

Reference

How FasterRCNN works and step-by-step PyTorch implementation

pytorch Faster R-CNNgithub repository

Faster R-CNN 논문 리뷰

jwyang님의 Faster R-CNN 구현 코드

Faster R-CNN 구현

[Faster R-CNN] 논문 리뷰 & 구현 (Pytorch)

안녕하세요 ! 소신입니다. R-CNN이랑 Fast R-CNN은 거의 논문리뷰만 하고 구현은 안했는데, Faster R-CNN은 구현까지 해보았습니다. (근데 오류가 있는것 같음..) # Faster R-CNN 구조 Faster R-CNN의 구조는 Fa..

wolfy.tistory.com

(논문리뷰&재구현) Faster R-CNN 설명 및 정리

이전글 : (논문리뷰) Fast R-CNN 설명 및 정리

R-CNN에서는 3가지 모듈 (region proposal, classification, bounding box regression)을 각각 따로따로 수행한다.

(1)region proposal 추출 → 각 region proposal별로 CNN 연산 → (2)classification, (3)bounding box regression

Fast R-CNN에서는 region proposal을 CNN level로 통과시켜 classification, bounding box regression을 하나로 묶었다.

(1)region proposal 추출 → 전체 image CNN 연산 → RoI projection, RoI Pooling

→ (2)classification, bounding box regression

그러나 여전히 region proposal인 Selective search알고리즘을 CNN외부에서 연산하므로 RoI 생성단계가 병목이다.

따라서 Faster R-CNN에서는 detection에서 쓰인 conv feature을 RPN에서도 공유해서

RoI생성역시 CNN level에서 수행하여 속도를 향상시킨다.

“Region Proposal도 Selective search 쓰지말고 CNN – (classification | bounding box regression)

이 네트워크 안에서 같이 해보자!”

Selective search가 느린이유는 cpu에서 돌기 때문이다.

따라서 Region proposal 생성하는 네트워크도 gpu에 넣기 위해서 Conv layer에서 생성하도록 하자는게 아이디어이다.

Faster R-CNN은 한마디로 RPN + Fast R-CNN이라할 수 있다.

Faster R-CNN은 Fast R-CNN구조에서 conv feature map과 RoI Pooling사이에 RoI를 생성하는

Region Proposal Network가 추가된 구조이다.

RPN + Fast R-CNN

그리고 Faster R-CNN에서는 RPN 네트워크에서 사용할 CNN과

Fast R-CNN에서 classification, bbox regression을 위해 사용한 CNN 네트워크를 공유하자는 개념에서 나왔다.

CNN Sharing (RPN & Detector)

결국 위 그림에서와 같이 CNN을 통과하여 생성된 conv feature map이 RPN에 의해 RoI를 생성한다.

주의해야할 것이 생성된 RoI는 feature map에서의 RoI가 아닌 original image에서의 RoI이다.

(그래서 코드 상에서도 anchor box의 scale은 original image 크기에 맞춰서 (128, 256, 512)와 같이 생성하고

이 anchor box와 network의 output 값 사이의 loss를 optimize하도록 훈련시킨다.)

따라서 original image위에서 생성된 RoI는 아래 그림과 같이 conv feature map의 크기에 맞게 rescaling된다.

feature map에 투영된 RoI

이렇게 feature map에 RoI가 투영되고 나면 FC layer에 의해 classification과 bbox regression이 수행된다.

위 그림에서 보다시피 마지막에 FC layer를 사용하기에 input size를 맞춰주기 위해 RoI pooling을 사용한다.

RoI pooling을 사용하니까 RoI들의 size가 달라도 되는것처럼 original image의 input size도 달라도된다.

그러나 구현할때 코드를 보면 original image의 size는 같은 크기로 맞춰주는데 그 이유는

“vgg의 경우 244×224, resNet의 경우 min : 600, max : 1024 등.. 으로 맞춰줄때 성능이 가장 좋기 때문이다”

original image를 resize할때 손실되는 data가 존재하듯이

feature map을 RoI pooling에서 max pooling을 통해 resize할때 손실되는 data 역시 존재한다.

따라서 이때 손실되는 data와 input image 자체를 resize할때 손실되는 data 사이의 Trade off가 각각

vgg의 경우 224×224, resNet은 600~1024이기에 input size를 고정시킨 것이다.

“따라서 요즘에는 이 FC layer 대신 GAP(Global Average Pooling)을 사용하는 추세이다”

GAP를 사용하면 input size와 관계없이 1 value로 average pooling하기에 filter의 개수만 고정되어있으면 되기 때문이다.

따라서 input size를 고정시킬 필요없기에 RoI pooling으로 인해 손실되는 data역시 없어서 original image의 size역시

고정시킬 필요가 없는 장점이 있다.

더욱 자세한 이야기는 따로 게시글로 다루도록 하겠다.

GAP(Global Average Pooling) vs FCN(Fully Convolutional Network)

그럼먼저 RPN 네트워크에 대해서 살펴보자

RPN의 input 값은 이전 CNN 모델에서 뽑아낸 feature map이다.

Region proposal을 생성하기 위해 feature map위에 nxn window를 sliding window시킨다.

이때, object의 크기와 비율이 어떻게 될지모르므로 k개의 anchor box를 미리 정의해놓는다.

이 anchor box가 bounding box가 될 수 있는 것이고 미리 가능할만한 box모양 k개를 정의해놓는 것이다.

여기서는 가로세로길이 3종류 x 비율 3종류 = 9개의 anchor box를 이용한다.

이 단계에서 9개의 anchor box를 이용하여 classification과 bbox regression을 먼저 구한다. (For 학습)

먼저, CNN에서 뽑아낸 feature map에 대해 3×3 conv filter 256개를 연산하여 depth를 256으로 만든다.

그 후 1×1 conv 두개를 이용하여 각각 classification과 bbox regression을 계산한다.

* 1×1 convolution 이란?

input 차원이 nxnx4라고 할때 1×1 convolution 2개를 이용하면 결국 input nxn짜리 4개에 대해

convolution 2개랑 아래와 같은 연산을 하는 것이다.

각 A는 nxn 채널 / weight가 1x1conv 2개

따라서 1×1 convolution은 fully connected layer와 같다고 한다.

다시 돌아와서 RPN에서 이렇게 1×1 convolution을 이용하여 classification과 bbox regression을 계산하는데

이때 네트워크를 가볍게 만들기 위해 binary classification으로 bbox에 물체가 있나 없나만 판단한다.

무슨 물체인지 classification하는 것은 마지막 classification 단계에서 한다.

RPN단계에서 classification과 bbox regression을 하는 이유는 결국 학습을 위함이다.

위 단계로부터 positive / negative examples들을 뽑아내는데 다음 기준에 따른다.

IoU가 0.7보다 크거나, 한 지점에서 모든 anchor box중 가장 IoU가 큰 anchor box는 positive example로 만든다.

IoU가 0.3보다 작으면 object가 아닌 background를 뜻하므로 negative example로 만들고

이 사이에 있는 IoU에 대해서는 애매한 값이므로 학습 데이터로 이용하지 않는다.

Faster R-CNN에 대한 학습이 완료된 후 RPN모델을 예측시키며 하마 한 객체당 여러 proposal값이 나올 것이다.

이 문제를 해결하기 위해 NMS알고리즘을 사용하여 proposal의 개수를 줄인다.

NMS알고리즘은 다음과 같다.

1. box들의 score(confidence)를 기준으로 정렬한다.

2. score가 가장 높은 box부터 시작해서 다른 모든 box들과 IoU를 계산해서 0.7이상이면 같은 객체를 detect한 box라고

생각할 수 있기 때문에 해당 box는 지운다.

3. 최종적으로 각 object별로 score가 가장 높은 box 하나씩만 남게 된다.

NMS 전 NMS 후

학습과정

최종 Loss = (Classification Loss + Regression Loss)

Bounding box regression Loss

이상으로 Faster R-CNN 논문에 대한 리뷰를 마쳤습니다.

해당 논문의 재구현 혹은 자세한 코드를 보고싶다면 아래 링크에서 확인하실 수 있습니다.

https://github.com/Kanghee-Lee/Faster-RCNN_TF-RPN-

코랩을 통해서 구현하였으며 쉽게 이해할 수 있도록 최대한 자세하게 line-by-line으로 주석달아 설명하고 있습니다.

Faster R-CNN의 Region Proposal Network를 구현하였습니다.

혹시나 이해가 안되는 부분이나 틀린 부분이 있다면 댓글로 알려주시면 감사하겠습니다.

다음글 : (논문리뷰) Mask R-CNN 설명 및 정리

Fast R-CNN 구현 질문 – 인프런

Fast R-CNN 구현중에 가장 난감한 부분이 RoI pooling layer 였는데, 논문에서 보면 mini-batch를 이미지 2개에 roi box를 128개 사용한다고 써있더라구요. 그러면 학습할 때 입력 데이터로 이미지 2개 + roi box(region proposal된 roi)가 들어간다고 이해했습니다.

그런데 여기서 이해가 안되는 점이 여러개가 있습니다 .

1. 이론상 fast rcnn은 입력 크기가 정해지지 않아도 되는데, tensorflow 에서 pre-trained vgg16 모델을 사용하려면 입력 크기가 224x224x3으로 고정되어 있어서 결국 mini-batch를 구성할 때 입력 사이즈를 모두 224x224x3으로 맞춰줘야 하는건가요?

2. 이미지 + roi box가 입력으로 들어가면 multi input으로 인풋값을 동시에 두 가지를 분리해서 넣어줘야 하나요?

3. 마지막에 box regressor와 classifier에서 box regressor는 x,y, w,h 좌표를 output으로 / classifier는 K(클래스 개수) + 1(배경) 총 K+1개 output이 도출되어야 하는데 입력 데이터 구성이 이미지 데이터 + roi box에 주어진 target 데이터는 ground truth box여서 mini-batch 구성할 때 Pascal IoU 기준에 따라 ground truth box와 이미지당 roi box 64개를 roi 비교하여 0.5이상인 경우는 1 0.2이상 0.5 미만을 0으로 구성한 후에 학습을 해야 하는건가요?

여기서 예를 들어 클래스가 4개라고 가정했을 때 target 데이터는 [N, 64, 4] (N은 데이터 개수) 크기로 구성하는게 맞을까요? (64개는 roi 개수 4개는 클래스입니다)

추가 설명 : [1, 64, 4]라고 했을 때 1개 이미지에 해당하는 64개 roi 각각 4개 클래스 중 overlap되는 roi가 0.5이상 되는 경우 1을 추가하고 클래스가 강아지, 고양이, 사람, 배경이라고 했을 때 강아지가 2마리 사람이 1명 이면 [2, 0, 1, 0] 이렇게 target 데이터가 구성되는게 맞나요..? ㅠㅠ

4. Roi pooling layer에 들어가는 input값이, 14x14x256(input 크기가 224x224x3이라고 가정했을 때) feature map + roi 인걸로 이해 했는데, tensorflow 구현시 roi는 input으로 어떻게 넘겨 줘야 할지 모르겠습니다..ㅠㅠ

5. Pre-trained model로 imagenet 데이터로 학습된 vgg16모델을 쓴다고 했을 때, 만약에 한식 이미지를 detection 해야 하는 문제가 있으면 fine tuning 한 후에 적용해야 정확도가 올라가겠죠? pre trained 된 이미지와 새로 detection할 이미지가 유사한지 유사하지 않은지는 추론해보고 결과가 좋지 못하면 fine tuning을 해야겠다 판단하는 건가요 아니면 사전에 학습된 이미지와 새로 학습하거나 추론할 이미지와 유사성을 분석 해보고 fine tuning을 하나요..? 당연히 추론해보고 결과가 좋지 못하면 할것 같지만, fine tuning을 해야 겠다 판단하는 기준이 혹시 따로 있을지 궁금합니다

논문보고 이해가 안가는 부분은 블로그도 보고 이것 저것 참조하다 보니 엄청 헷갈리는 상태가 돼서 질문이 정갈하지 않는 점 양해부탁드립니다 ㅠㅠ

[논문리뷰] Faster R-CNN 이해

오늘은 Faster R-CNN에 대해 간단히 리뷰해보려 합니다.

논문링크↓

Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks

원래는 Mask R-CNN 논문 이해 및 구현이 올해 첫 목표였는데요,

일단 Faster R-CNN 구현을 먼저 진행해보면 어떨까 고민하고 있습니다.

지난 Fast R-CNN 게시물을 보고 싶으시면 아래를 참고하시길 바랍니다.

2021.03.31 – [이론/이미지인식] – Fast R-CNN 이해

Faster R-CNN은 R-CNN, Fast R-CNN과 마찬가지로 Object Detection model입니다.

Fast R-CNN이 R-CNN의 상위버전이라면, Faster R-CNN은 Fast R-CNN의 상위버전입니다.

지난 리뷰를 돌아보면 R-CNN에서는 모든 region proposal의 convolution 과정으로 인해

training, test 시간이 오래 걸린다는 문제점이 있었는데,

Fast R-CNN에서는 이를 해결했습니다.

하지만 Fast R-CNN에서는 region proposal에서 여전히 많은 시간이 소요된다는 문제점이 있었는데요,

Faster R-CNN이 바로 이 문제점을 해결했습니다.

본론으로 들어가기 전에 이 논문에서는 object detection을 크게 2단계로 나누고 있다는 것을 알아두시면 좋습니다.

하나는 region proposal 단계이고,

다른 하나는 이를 바탕으로 bounding-box의 클래스를 판단하고 위치를 regression하는 detection 단계입니다.

이런 관점은 R-CNN과 Fast R-CNN과 비슷하니 낯설지는 않으실 듯 합니다.

이런 방법을 Two-stage(proposal + detection) 이라고 부르는 듯 하네요.

Faster R-CNN은 region proposal 하는 단계에서 detection 단계에서 쓰이는 convolutional features를 공유하게 함으로써, region proposal 하는 시간을 크게 감소시켰습니다.

이러한 region proposal 방법을 RPN이라고 정의하고 있습니다.

이제 본론으로 들어가봅시다!

[기존 region based CNN의 문제점 및 해결책]

앞서 언급했듯이 Fast R-CNN과 같은 region based CNN을 쓰는 모델은

region proposal 단계에서 시간이 많이 소요됩니다.

region proposal 하는 방법에는 Selective search나 EdgeBoxes등이 있습니다.

(자세한 내용은 여기서 다루지 않겠습니다.)

region proposal은 CPU연산이고 region-based CNN은 GPU 연산이기 때문에

둘의 시간소요를 비교하면 안된다고 생각할 수 있습니다.

그렇다고해서 region proposal을 GPU 자원을 쓸 수 있게 다시 구현하게 된다면,

여기서 쓰이는 sharining computation(region proposal과 detection사이의 sharing)을 하지 못한다고 하네요.

논문의 저자들은 region-based detector에서 쓰는 convolutional feature maps가

region proposal을 하는데에도 쓰일 수 있다는 것을 알아냈습니다.

따라서 이 논문에서는 기본 region proposal 방법을 활용하지 않고

RPN이라는 새로운 network를 소개하고 있습니다.

convolution network를 쓰기 때문에 기존의 방법보다 시간측면에서 성능이 좋습니다.

또한 정확도 측면에서도 더 성능이 좋다고 합니다.

convolutional features의 위에다가 몇개의 convolutional layers를 추가하여 RPN을 구현했습니다.

______________

[RPN 이란]

앞서 언급했듯이 RPN은 region proposal을 뽑아내는 network입니다.

region proposal은 직사각형모양인데 물체에 따라 다양한 비율을(가로:세로) 가져야 할 것입니다.

이를 위해 RPN은 아래에서 보듯이

여러 크기와 여러 비율을 가지는 “anchor” boxes(그림에서 파란색 박스)를 이용합니다.

image에다가 conv feature map을 뽑아낸 뒤, 해당 map에서 window가 sliding 합니다.

(이것을 filter라고도 합니다.)

그리고 하나의 filter가 여러개의 anchor boxes로 변화하게 됩니다

Faster R-CNN의 Figure1. (c) pyramids of reference boxes

사실 여러 사이즈의 region proposal을 제안하기 위해

이미지 자체의 크기를 줄이면서 학습시키는 방법도 있고

filter의 사이즈를 다양하게 변경하면서 학습시키는 방법도 있습니다.(아래 사진 참고)

Faster R-CNN의 Figure1. (a), (b)

이 두개의 방법이 아닌 다양한 비율을 가지는 anchor box를 이용하여 학습시키면

스피드 측면에서 이점이 있다고 하네요.

Faster R-CNN은 오직 한개의 image와 한개의 filter(feature map에서 sliding window)를 사용합니다.

대신 하나의 filter에서 여러개의 anchor box를 쓰는 것이죠.

그러면 RPN에서 다양한 비율을 가지는 proposal을 생성하는 방법을 알아봤고,

RPN이 전체적으로 어떤 과정인지 알아야 겠죠.

RPN의 구조는 아래와 같습니다.

Faster R-CNN 논문 Figure 3

위의 사진에서 보듯이 각 sliding window는 작은 차원의 feature로 변하게 됩니다. (그림에서는 256-d)

그 후 이 feature은 2가지 분기로 나뉘게 됩니다.

하나는 각 proposal들이 물체인지 아닌지 점수를 매기는 부분(왼쪽)이며

다른 하나는 각 proposal들의 위치를 regression하는 부분(오른쪽)입니다.

여기서 k는 하나의 sliding window에서 나오는 anchor box의 개수입니다.

논문에서는 k를 9로 설정했습니다.

3개의 크기와 3개의 비율(가로:세로를 각 1:1, 2:1, 1:2)로 anchor box의 9개의 크기를 정했습니다.

Convolutional feature map이 W*H라면 전체 이미지에서 anchor box의 개수는 W*H*k가 됩니다.

따라서 RPN의 결과로 여러 사이즈의 region proposal box와 이 box가 object일 확률을 return합니다.

______________

[Faster R-CNN 구조]

Faster R-CNN은 2가지 모듈로 나눠져 있습니다.

첫번째는 region proposal을 구하는 fully convolutional network

두번째는 앞서 추출한 region proposal을 사용하여 object detection을 하는 Fast R-CNN detector 모듈

이 두개는 아래 사진 처럼 하나의 network로 되어 있습니다.

Faster R-CNN 논문 Figure 2

______________

[Loss Function]

RPN을 훈련시키기 위해서 RPN의 loss function이 당연히 정의되어 있어야겠죠?

위의 글을 상기시키면 일단 RPN에서는 anchor box가 물체가 아닌지 맞는지

binary class label을 추측해야 합니다.

binary class가 true인 경우는

(1) ground-truth box와의 IoU가 제일 높은 anchor box인 경우

(2) ground-truth box와의 IoU가 0.7이상인 anchor box인 경우

입니다.

따라서 하나의 ground-truth box에 대해 여러 anchor box의 binary class가 true일 수 있습니다.

그리고 위에서 (1)조건이 있는 이유는 경우에 따라 (2)조건을 성립하는 anchor box가 없을 수 있기 때문입니다.

그리고 binary class가 false인 경우는

ground-truth box와의 IoU가 0.3미만인 경우입니다.

따라서 binary class가 true도 아니고 false도 아닐 수 있습니다. 이 경우는 훈련에 영향을 끼치지 않습니다.

loss 식은 아래와 같습니다.

Faster R-CNN논문 RPN loss

우변의 첫번째 항은 2개의 class(물체인지 아닌지)에 대한 classification loss이고,

우변의 두번째 항은 regression loss입니다.

regression loss는 해당 anchor box가 truth일 때만 더해집니다.

(true면 pi*가 1이 되고 false면 pi*가 0)

classification loss는 log loss이고, regression loss는 robust loss입니다.

변수의 정의는 아래와 같습니다.

i := anchor box의 index

pi := i번째 anchor box가 object일 확률

ti := bound box의 추측값 (4개의 벡터로 이뤄져 있음)

또한 *가 붙은 것은 ground-truth이고 붙지 않은 것은 추측값입니다.

RPN을 훈련 시킬때는 하나의 이미지에서 256개의 anchors를 무작위로 고르는데,

여기서 positive anchor과 negative anchor의 비율을 1:1로 맞췄다고 합니다.

만약 하나의 이미지에서 positive anchor의 개수가 128보다 작다면

negative anchor의 개수도 positive anchor 개수에 맞춰 뽑습니다.

______________

[RPN과 Fast R-CNN을 연결하는 방법 ]

Faster R-CNN에서는 detection network로 Fast R-CNN을 선택했습니다.

논문에서는 Faster R-CNN에서 RPN과 Fast R-CNN을 어떻게 연결했는지 소개하는데

이에 앞서 RPN과 Fast R-CNN을 연결할 수 있는 후보 선택지 3가지를 소개합니다.

1) Alternating training

이 학습 방법은 먼저 RPN만 학습시키고 그 다음 Fast R-CNN의 detection 부분을 fine-tuning하는 방법입니다.

실질적으로 Faster R-CNN에서 썼던 방법입니다.

2) Approximate joint training

이 방법은 RPN loss와 Fast R-CNN loss를 하나로 합쳐 RPN과 Fast R-CNN을 한꺼번에 학습시키는 것입니다.

이는 구현하기는 쉬울 지 몰라도 proposal box에 대한 coordinate에 대한 derivate값을 무시한다고 하네요.

(왜 무시하는지는 여기서 다루지 않겠습니다.)

첫번째 방법을 썼을 때 이 방법을 썼을 때보다 학습시간이 25-50%로 줄게 된다고 하네요.

3) Non-approximate joint training

두번째 방법이 RPN의 bounding boxes의 coordinate의 derivate를 무시한다고 했는데요,

이 방법에서는 “RoI warping” layer를 사용하여 이 문제점을 해결할 수 있다고 합니다.

논문에서도 이것을 설명하는건 이 논문의 범위를 벗어난다고 설명을 생략했으니,

여기에서도 다루지 않겠습니다.

그래서 결론만 언급하자면 Faster R-CNN은 alternating training을 씁니다.

이 training 과정은 크게 4가지로 나뉩니다.

1) ImageNet에서 사전학습된 network로 파라미터들을 초기화 한후 RPN 학습

2) 위에서 학습시킨 RPN을 이용해 Fast R-CNN의 detection network 학습

(이때 detection network 또한 ImageNet에서 사전학습된 모델입니다.)

3) 위에서 학습시킨 detection network와 shared convolutional layer는 고정시키고,

오직 RPN에 대한 layer들만 fine-tuning

4) 위에서 학습시킨 RPN과 convolution layer는 고정시키고, Fast R-CNN에 대한 layer만 fine-tuning

자세한 내용은 구현을 하면서 알아봐야 할 듯 합니다.

______________

[기타]

몇 RPN proposal들은 다른 것들과 overlap될 수 있습니다.

이런 redundancy를 줄이기 위해 Faster R-CNN에서도 non-maximum suppression(NMS)를 썼다고 하네요.

또한 논문에서는 Faster R-CNN이 Translation-Invariant Anchors 성질을 가지고 있다고 소개합니다.

처음에 translation-invariant가 무슨 뜻인지 감이 안잡혀 구글링해보니

대략 입력이 바뀌어도 출력은 동일하다는 뜻인듯 하더군요.

여기에서는 이미지상에서 어떤 물체를 옮겨도

옮긴 후 위치에서 옮기기 전과 동일하게 proposal을 뽑아내는 성질을 의미합니다.

그런데 Faster R-CNN은 이런 성질을 지니고 있다고 합니다.

이 부분에서 MultiBox와 비교하며 설명하고 있습니다.

MultiBox의 경우는 k-means를 사용하여 anchor box를 800개(<-> Faster R-CNN의 경우 9개) 뽑아냅니다.

(여기서는 제 의견.. k-means라는 것은 주변 노드들의 영향을 받는 것이니

MultiBox는 translation-invariant anchor성질이 없다는 것 같고,

반면 Faster R-CNN은 sliding window로 anchor box를 뽑아내기 때문에

Faster R-CNN은 translation-invariant anchor 성질을 가진다는 듯 합니다.

어디까지나 저의 의견입니다!)

그리고 MultiBox에서 proposal을 추출하는데 필요한 파라미터 수보다

Faster R-CNN에서 필요한 파라미터 수가 더 작기 때문에

작은 dataset에서 overfitting의 위험을 줄일 수 있다고 소개하고 있습니다.

(사실 논문을 읽으면서 왜 translation-invariant인지에 대한 설명은 부족하다고 느꼈습니다.

이런 이유를 설명하기보다는 그냥 MultiBox와의 비교를 소개한다는 느낌을 받았어요.

Faster R-CNN은 detection에서 쓰이는 convolutional features를 region proposal에서도 활용함으로써

기존 proposal 단계에서 걸리는 시간을 크게 줄였다는 측면에서 의의가 있습니다.

개인적을 이번 논문의 서술방식은 뭔가 저에게는 맞지 않았어요.

앞에서 설명했던 것을 뒤에서도 몇번 더 반복해서 설명하는 느낌이라

음… 저한테는 설명이 왔다갔다 하는 느낌이랄까요?

그리고 뭔가 각 챕터의 순서도 저에게는 꼬인것 같은 느낌.

그래서 블로그에 적어진 설명 순서도 논문의 순서와는 다를 거에요.

저는 차근차근 순차적으로 넘어가는 설명방식을 좋아하는 것 같아요.

하지만 Faster R-CNN에서의 RPN방법은 참신한 것은 분명합니다.

이전 R-CNN, Fast R-CNN에 대한 게시물 내용은 짧았던 것 같은데,

이번 논문은 비교적 설명이 길어진 것 같아요.

왜냐하면 이 Faster R-CNN 구현을 해보고 싶은 마음때문이죠ㅎㅎ

글을 시작하기에 앞서 Mask R-CNN을 다뤄보기전에

Faster R-CNN을 구현해보고 싶다고 했는데

음 아마도 다음 게시물은 Faster R-CNN 구현이 되지 않을까 싶어요..!

그럼 이만~

키워드에 대한 정보 faster rcnn 구현

다음은 Bing에서 faster rcnn 구현 주제에 대한 검색 결과입니다. 필요한 경우 더 읽을 수 있습니다.

이 기사는 인터넷의 다양한 출처에서 편집되었습니다. 이 기사가 유용했기를 바랍니다. 이 기사가 유용하다고 생각되면 공유하십시오. 매우 감사합니다!

사람들이 주제에 대해 자주 검색하는 키워드 [논문 읽기] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (NIPS’ 15)

  • Faster R-CNN
  • R-CNN
  • 객체 검출
  • Object Detection
  • Detection
[논문 #읽기] #Faster #R-CNN: #Towards #Real-Time #Object #Detection #with #Region #Proposal #Networks #(NIPS’ #15)


YouTube에서 faster rcnn 구현 주제의 다른 동영상 보기

주제에 대한 기사를 시청해 주셔서 감사합니다 [논문 읽기] Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks (NIPS’ 15) | faster rcnn 구현, 이 기사가 유용하다고 생각되면 공유하십시오, 매우 감사합니다.

See also  밥 먹는 고양이 | 아기 길고양이에게 밥을 줬는데 냠냠냠 소리를 내며 허겁지겁 먹어요 Feat 골골송 69 개의 자세한 답변

Leave a Comment