Goal : Faster R-CNN 이 어떻게 동작하는지 코드로 한 번 알아보고자 함. 전문적인 내용은 담지 않고 있으니 정확한 정보가 필요하다면 논문을 참고하길 바람.
다음은 가장 기본적인 Faster R-CNN 구조이다. 챗봇한테 짜 달라고 해도 코드를 생성해 준다.
import torch
import torchvision
from torchvision.models.detection import FasterRCNN
from torchvision.models.detection.rpn import AnchorGenerator
def create_faster_rcnn(num_classes):
# 백본 모델로 ResNet-50을 사용
backbone = torchvision.models.resnet50(pretrained=True)
# ResNet의 마지막 두 스테이지만 사용
backbone = torch.nn.Sequential(*list(backbone.children())[:-3])
# RPN에서 사용할 앵커 생성기 정의
anchor_generator = AnchorGenerator(
sizes=((32, 64, 128, 256, 512),),
aspect_ratios=((0.5, 1.0, 2.0),)
)
# ROI Pooling 레이어 정의
roi_pooler = torchvision.ops.MultiScaleRoIAlign(
featmap_names=['0'],
output_size=7,
sampling_ratio=2
)
# Faster R-CNN 모델 생성
model = FasterRCNN(
backbone,
num_classes=num_classes,
rpn_anchor_generator=anchor_generator,
box_roi_pool=roi_pooler
)
return model
# 사용 예시
num_classes = 91 # COCO 데이터셋의 클래스 수 (배경 클래스 포함)
model = create_faster_rcnn(num_classes)
model 을 주목해 보면, FasterRCNN 클래스에 속성들을 집어 넣고 있는게 보인다.
항목으로는,
1. 백본
2. num_classes
3. rpn_anchor_generator
4. box_roi_pool
의 4 가지로 구성되어 있는 것을 확인 가능하다.
여기서 num_classes 는 CNN 을 거쳤을 때의 결과 클래스 + 배경 클래스 1을 합친 값이 된다. 따라서 이건 제외하도록 하고
백본, rpn_anchor_generator, box_roi_pool 의 세 가지를 분석해 보도록 한다.
과정을 크게 살펴 보자.
백본의 경우 input image 에서 feature map 을 생성하는 과정이다.
rpn_anchor_generator 의 경우 객체가 있을 만한 곳을 제안하는 데 사용되는 앵커 박스를 생성하는 역할을 한다.
또한 앵커들을 가지고 rpn 이 객체가 있을 만한 영역을 제안한다.
box_roi_pool 은 이 제안된 영역에서 고정 크기의 특징을 추출한다.
추출한 특징은 객체 분류 및 바운딩 박스 생성에 사용된다.
Faster R-CNN 클래스 내부를 한번 보도록 한다.(faster_rcnn.py)
내부 argument 로 사용하는 것들을 적어놓았다.
Args:
backbone (nn.Module): the network used to compute the features for the model.
It should contain an out_channels attribute, which indicates the number of output
channels that each feature map has (and it should be the same for all feature maps).
The backbone should return a single Tensor or and OrderedDict[Tensor].
num_classes (int): number of output classes of the model (including the background).
If box_predictor is specified, num_classes should be None.
min_size (int): minimum size of the image to be rescaled before feeding it to the backbone
max_size (int): maximum size of the image to be rescaled before feeding it to the backbone
image_mean (Tuple[float, float, float]): mean values used for input normalization.
They are generally the mean values of the dataset on which the backbone has been trained
on
image_std (Tuple[float, float, float]): std values used for input normalization.
They are generally the std values of the dataset on which the backbone has been trained on
rpn_anchor_generator (AnchorGenerator): module that generates the anchors for a set of feature
maps.
rpn_head (nn.Module): module that computes the objectness and regression deltas from the RPN
rpn_pre_nms_top_n_train (int): number of proposals to keep before applying NMS during training
rpn_pre_nms_top_n_test (int): number of proposals to keep before applying NMS during testing
rpn_post_nms_top_n_train (int): number of proposals to keep after applying NMS during training
rpn_post_nms_top_n_test (int): number of proposals to keep after applying NMS during testing
rpn_nms_thresh (float): NMS threshold used for postprocessing the RPN proposals
rpn_fg_iou_thresh (float): minimum IoU between the anchor and the GT box so that they can be
considered as positive during training of the RPN.
rpn_bg_iou_thresh (float): maximum IoU between the anchor and the GT box so that they can be
considered as negative during training of the RPN.
rpn_batch_size_per_image (int): number of anchors that are sampled during training of the RPN
for computing the loss
rpn_positive_fraction (float): proportion of positive anchors in a mini-batch during training
of the RPN
rpn_score_thresh (float): only return proposals with an objectness score greater than rpn_score_thresh
box_roi_pool (MultiScaleRoIAlign): the module which crops and resizes the feature maps in
the locations indicated by the bounding boxes
box_head (nn.Module): module that takes the cropped feature maps as input
box_predictor (nn.Module): module that takes the output of box_head and returns the
classification logits and box regression deltas.
box_score_thresh (float): during inference, only return proposals with a classification score
greater than box_score_thresh
box_nms_thresh (float): NMS threshold for the prediction head. Used during inference
box_detections_per_img (int): maximum number of detections per image, for all classes.
box_fg_iou_thresh (float): minimum IoU between the proposals and the GT box so that they can be
considered as positive during training of the classification head
box_bg_iou_thresh (float): maximum IoU between the proposals and the GT box so that they can be
considered as negative during training of the classification head
box_batch_size_per_image (int): number of proposals that are sampled during training of the
classification head
box_positive_fraction (float): proportion of positive proposals in a mini-batch during training
of the classification head
bbox_reg_weights (Tuple[float, float, float, float]): weights for the encoding/decoding of the
bounding boxes
위의 gpt 해석본이다.
- backbone (nn.Module): 모델의 특징을 계산하는 데 사용되는 네트워크입니다. 각 특징 맵(feature map)의 출력 채널 수를 나타내는 out_channels 속성을 포함해야 합니다(모든 특징 맵에 대해 동일해야 합니다). backbone은 하나의 텐서(Tensor) 또는 OrderedDict[Tensor]를 반환해야 합니다.
- num_classes (int): 모델의 출력 클래스 수(배경 포함)입니다. box_predictor가 지정된 경우 num_classes는 None이어야 합니다.
- min_size (int): 백본에 전달되기 전에 이미지의 크기를 조정할 최소 크기입니다.
- max_size (int): 백본에 전달되기 전에 이미지의 크기를 조정할 최대 크기입니다.
- image_mean (Tuple[float, float, float]): 입력 정규화에 사용되는 평균값입니다. 일반적으로 백본이 훈련된 데이터셋의 평균값입니다.
- image_std (Tuple[float, float, float]): 입력 정규화에 사용되는 표준 편차 값입니다. 일반적으로 백본이 훈련된 데이터셋의 표준 편차 값입니다.
- rpn_anchor_generator (AnchorGenerator): 특징 맵 집합에 대한 앵커를 생성하는 모듈입니다.
- rpn_head (nn.Module): RPN에서 객체성(objectness) 및 회귀 델타를 계산하는 모듈입니다.
- rpn_pre_nms_top_n_train (int): 훈련 중 NMS를 적용하기 전에 유지할 제안서 수입니다.
- rpn_pre_nms_top_n_test (int): 테스트 중 NMS를 적용하기 전에 유지할 제안서 수입니다.
- rpn_post_nms_top_n_train (int): 훈련 중 NMS를 적용한 후 유지할 제안서 수입니다.
- rpn_post_nms_top_n_test (int): 테스트 중 NMS를 적용한 후 유지할 제안서 수입니다.
- rpn_nms_thresh (float): RPN 제안서 후처리에 사용되는 NMS 임계값입니다.
- rpn_fg_iou_thresh (float): RPN 훈련 중 앵커와 GT 박스 간의 최소 IoU 값으로, 이를 초과해야 양성으로 간주됩니다.
- rpn_bg_iou_thresh (float): RPN 훈련 중 앵커와 GT 박스 간의 최대 IoU 값으로, 이를 초과하지 않아야 음성으로 간주됩니다.
- rpn_batch_size_per_image (int): RPN 손실을 계산하기 위해 RPN 훈련 중 샘플링되는 앵커 수입니다.
- rpn_positive_fraction (float): RPN 훈련 중 미니 배치에서 양성 앵커의 비율입니다.
- rpn_score_thresh (float): 객체성 점수가 rpn_score_thresh보다 큰 제안서만 반환됩니다.
- box_roi_pool (MultiScaleRoIAlign): 바운딩 박스가 표시된 위치에서 특징 맵을 잘라내고 크기를 조정하는 모듈입니다.
- box_head (nn.Module): 잘라낸 특징 맵을 입력으로 받는 모듈입니다.
- box_predictor (nn.Module): box_head의 출력을 받아 분류 로짓(logits)과 박스 회귀 델타를 반환하는 모듈입니다.
- box_score_thresh (float): 추론 중에 분류 점수가 box_score_thresh보다 큰 제안서만 반환됩니다.
- box_nms_thresh (float): 예측 헤드의 NMS 임계값입니다. 추론 중에 사용됩니다.
- box_detections_per_img (int): 모든 클래스에 대해 이미지당 최대 감지 수입니다.
- box_fg_iou_thresh (float): 분류 헤드 훈련 중 제안서와 GT 박스 간의 최소 IoU 값으로, 이를 초과해야 양성으로 간주됩니다.
- box_bg_iou_thresh (float): 분류 헤드 훈련 중 제안서와 GT 박스 간의 최대 IoU 값으로, 이를 초과하지 않아야 음성으로 간주됩니다.
- box_batch_size_per_image (int): 분류 헤드 훈련 중 샘플링되는 제안서 수입니다.
- box_positive_fraction (float): 분류 헤드 훈련 중 미니 배치에서 양성 제안서의 비율입니다.
- bbox_reg_weights (Tuple[float, float, float, float]): 바운딩 박스의 인코딩/디코딩에 사용되는 가중치입니다.
백본의 경우, 평소에도 사용하던 CNN 을 가져온다.
image 로 (640, 480) 의 rgb 이미지가 있다고 하면, (640, 480, 3)의 이미지 차원을 가지게 된다.
이걸 예시로 Conv2d : kernel = 3, stride = 2, padding = 1, output_channel = 32 를 적용하게 되면
width_new = ((width_old - kernel + 2 * padding) / stride) + 1, 버림 적용
height 에 대해서도 마찬가지이다.
(640, 480, 3) --> (320, 240, 32) 가 된다.
여기에 배치 정규화라던가 activation 함수를 사용한다던가 해서 백본을 구성한다.
내가 궁금한건 이 백본 부분이 아니므로 넘어가도록 한다.
그럼 anchor, rpn, roi 를 한 번 살펴보도록 한다.
rpn = RegionProposalNetwork(
rpn_anchor_generator,
rpn_head,
rpn_fg_iou_thresh,
rpn_bg_iou_thresh,
rpn_batch_size_per_image,
rpn_positive_fraction,
rpn_pre_nms_top_n,
rpn_post_nms_top_n,
rpn_nms_thresh,
score_thresh=rpn_score_thresh,
)
rpn 은 RegionProposalNetwork 클래스에 인자를 넘겨주는 식으로 생성한다.
rpn_anchor_generator 는 아까도 봤었던 anchor generator 에서 생성한다.
anchor_utils.py 내의 AnchorGenerator class
class AnchorGenerator(nn.Module):
"""
Module that generates anchors for a set of feature maps and
image sizes.
The module support computing anchors at multiple sizes and aspect ratios
per feature map. This module assumes aspect ratio = height / width for
each anchor.
sizes and aspect_ratios should have the same number of elements, and it should
correspond to the number of feature maps.
sizes[i] and aspect_ratios[i] can have an arbitrary number of elements,
and AnchorGenerator will output a set of sizes[i] * aspect_ratios[i] anchors
per spatial location for feature map i.
Args:
sizes (Tuple[Tuple[int]]):
aspect_ratios (Tuple[Tuple[float]]):
"""
__annotations__ = {
"cell_anchors": List[torch.Tensor],
}
def __init__(
self,
sizes=((128, 256, 512),),
aspect_ratios=((0.5, 1.0, 2.0),),
):
super().__init__()
if not isinstance(sizes[0], (list, tuple)):
# TODO change this
sizes = tuple((s,) for s in sizes)
if not isinstance(aspect_ratios[0], (list, tuple)):
aspect_ratios = (aspect_ratios,) * len(sizes)
self.sizes = sizes
self.aspect_ratios = aspect_ratios
self.cell_anchors = [
self.generate_anchors(size, aspect_ratio) for size, aspect_ratio in zip(sizes, aspect_ratios)
]
# TODO: https://github.com/pytorch/pytorch/issues/26792
# For every (aspect_ratios, scales) combination, output a zero-centered anchor with those values.
# (scales, aspect_ratios) are usually an element of zip(self.scales, self.aspect_ratios)
# This method assumes aspect ratio = height / width for an anchor.
def generate_anchors(
self,
scales: List[int],
aspect_ratios: List[float],
dtype: torch.dtype = torch.float32,
device: torch.device = torch.device("cpu"),
) -> Tensor:
scales = torch.as_tensor(scales, dtype=dtype, device=device)
aspect_ratios = torch.as_tensor(aspect_ratios, dtype=dtype, device=device)
h_ratios = torch.sqrt(aspect_ratios)
w_ratios = 1 / h_ratios
ws = (w_ratios[:, None] * scales[None, :]).view(-1)
hs = (h_ratios[:, None] * scales[None, :]).view(-1)
base_anchors = torch.stack([-ws, -hs, ws, hs], dim=1) / 2
return base_anchors.round()
def set_cell_anchors(self, dtype: torch.dtype, device: torch.device):
self.cell_anchors = [cell_anchor.to(dtype=dtype, device=device) for cell_anchor in self.cell_anchors]
def num_anchors_per_location(self) -> List[int]:
return [len(s) * len(a) for s, a in zip(self.sizes, self.aspect_ratios)]
# For every combination of (a, (g, s), i) in (self.cell_anchors, zip(grid_sizes, strides), 0:2),
# output g[i] anchors that are s[i] distance apart in direction i, with the same dimensions as a.
def grid_anchors(self, grid_sizes: List[List[int]], strides: List[List[Tensor]]) -> List[Tensor]:
anchors = []
cell_anchors = self.cell_anchors
torch._assert(cell_anchors is not None, "cell_anchors should not be None")
torch._assert(
len(grid_sizes) == len(strides) == len(cell_anchors),
"Anchors should be Tuple[Tuple[int]] because each feature "
"map could potentially have different sizes and aspect ratios. "
"There needs to be a match between the number of "
"feature maps passed and the number of sizes / aspect ratios specified.",
)
for size, stride, base_anchors in zip(grid_sizes, strides, cell_anchors):
grid_height, grid_width = size
stride_height, stride_width = stride
device = base_anchors.device
# For output anchor, compute [x_center, y_center, x_center, y_center]
shifts_x = torch.arange(0, grid_width, dtype=torch.int32, device=device) * stride_width
shifts_y = torch.arange(0, grid_height, dtype=torch.int32, device=device) * stride_height
shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x, indexing="ij")
shift_x = shift_x.reshape(-1)
shift_y = shift_y.reshape(-1)
shifts = torch.stack((shift_x, shift_y, shift_x, shift_y), dim=1)
# For every (base anchor, output anchor) pair,
# offset each zero-centered base anchor by the center of the output anchor.
anchors.append((shifts.view(-1, 1, 4) + base_anchors.view(1, -1, 4)).reshape(-1, 4))
return anchors
def forward(self, image_list: ImageList, feature_maps: List[Tensor]) -> List[Tensor]:
grid_sizes = [feature_map.shape[-2:] for feature_map in feature_maps]
image_size = image_list.tensors.shape[-2:]
dtype, device = feature_maps[0].dtype, feature_maps[0].device
strides = [
[
torch.empty((), dtype=torch.int64, device=device).fill_(image_size[0] // g[0]),
torch.empty((), dtype=torch.int64, device=device).fill_(image_size[1] // g[1]),
]
for g in grid_sizes
]
self.set_cell_anchors(dtype, device)
anchors_over_all_feature_maps = self.grid_anchors(grid_sizes, strides)
anchors: List[List[torch.Tensor]] = []
for _ in range(len(image_list.image_sizes)):
anchors_in_image = [anchors_per_feature_map for anchors_per_feature_map in anchors_over_all_feature_maps]
anchors.append(anchors_in_image)
anchors = [torch.cat(anchors_per_image) for anchors_per_image in anchors]
return anchors
인자로는 size, ratio 를 가진다. init 부분을 보게 되면
self.sizes = sizes
self.aspect_ratios = aspect_ratios
self.cell_anchors = [
self.generate_anchors(size, aspect_ratio) for size, aspect_ratio in zip(sizes, aspect_ratios)
]
이 부분에서 cell_anchors 를 생성하는 것을 볼 수 있다.
forward 부분을 본다.
def forward(self, image_list: ImageList, feature_maps: List[Tensor]) -> List[Tensor]:
grid_sizes = [feature_map.shape[-2:] for feature_map in feature_maps]
image_size = image_list.tensors.shape[-2:]
dtype, device = feature_maps[0].dtype, feature_maps[0].device
strides = [
[
torch.empty((), dtype=torch.int64, device=device).fill_(image_size[0] // g[0]),
torch.empty((), dtype=torch.int64, device=device).fill_(image_size[1] // g[1]),
]
for g in grid_sizes
]
self.set_cell_anchors(dtype, device)
anchors_over_all_feature_maps = self.grid_anchors(grid_sizes, strides)
anchors: List[List[torch.Tensor]] = []
for _ in range(len(image_list.image_sizes)):
anchors_in_image = [anchors_per_feature_map for anchors_per_feature_map in anchors_over_all_feature_maps]
anchors.append(anchors_in_image)
anchors = [torch.cat(anchors_per_image) for anchors_per_image in anchors]
return anchors
일단 어디서 가져왔는지는 모르겠지만 image list 와 feature map 을 받아온다.
.image_list 파일에서 받아오는 것으로 보이는데, 간단해서 한 번 짚고 넘어간다.
class ImageList:
"""
Structure that holds a list of images (of possibly
varying sizes) as a single tensor.
This works by padding the images to the same size,
and storing in a field the original sizes of each image
Args:
tensors (tensor): Tensor containing images.
image_sizes (list[tuple[int, int]]): List of Tuples each containing size of images.
"""
def __init__(self, tensors: Tensor, image_sizes: List[Tuple[int, int]]) -> None:
self.tensors = tensors
self.image_sizes = image_sizes
def to(self, device: torch.device) -> "ImageList":
cast_tensor = self.tensors.to(device)
return ImageList(cast_tensor, self.image_sizes)
이 친구는 동일한 크기로 패딩되어 있는 이미지들을 담은 텐서값을 받고, 원본 이미지의 크기를 담고 있는 리스트를 가진다.
그리고 그래픽카드 사용 시 데이터를 넘겨주기 위해 to 메서드가 있는 모습이다.
다시 forward 부분으로 돌아와서,
feature_maps 는 어디서 가져온 건지는 잘 모르겠다.
grid_sizes = [feature_map.shape[-2:] for feature_map in feature_maps]
image_size = image_list.tensors.shape[-2:]
받아온 feature maps 중 각 feature map 의 size 를 grid_size 로 저장,
입력 이미지 크기를 image_size 로 저장한다.
strides = [
[
torch.empty((), dtype=torch.int64, device=device).fill_(image_size[0] // g[0]),
torch.empty((), dtype=torch.int64, device=device).fill_(image_size[1] // g[1]),
]
for g in grid_sizes
]
stride 는 grid_size 에서 너비, 높이를 받아와서 각각 지정한다.
이후 set_cell_anchors 메서드를 실행하는데,
def set_cell_anchors(self, dtype: torch.dtype, device: torch.device):
self.cell_anchors = [cell_anchor.to(dtype=dtype, device=device) for cell_anchor in self.cell_anchors]
아까 size, ratio 를 지정했던 cell_anchors 를 연산 기기를 다시 지정하는 메서드이다.
anchors_over_all_feature_maps = self.grid_anchors(grid_sizes, strides)
모든 feature map들의 앵커를 지정하는 변수를 하나 선언한다. grid_anchors 라는 메서드를 활용하고, grid size 와 stride parameter 를 넘겨준다.
def grid_anchors(self, grid_sizes: List[List[int]], strides: List[List[Tensor]]) -> List[Tensor]:
anchors = []
cell_anchors = self.cell_anchors
torch._assert(cell_anchors is not None, "cell_anchors should not be None")
torch._assert(
len(grid_sizes) == len(strides) == len(cell_anchors),
"Anchors should be Tuple[Tuple[int]] because each feature "
"map could potentially have different sizes and aspect ratios. "
"There needs to be a match between the number of "
"feature maps passed and the number of sizes / aspect ratios specified.",
)
for size, stride, base_anchors in zip(grid_sizes, strides, cell_anchors):
grid_height, grid_width = size
stride_height, stride_width = stride
device = base_anchors.device
# For output anchor, compute [x_center, y_center, x_center, y_center]
shifts_x = torch.arange(0, grid_width, dtype=torch.int32, device=device) * stride_width
shifts_y = torch.arange(0, grid_height, dtype=torch.int32, device=device) * stride_height
shift_y, shift_x = torch.meshgrid(shifts_y, shifts_x, indexing="ij")
shift_x = shift_x.reshape(-1)
shift_y = shift_y.reshape(-1)
shifts = torch.stack((shift_x, shift_y, shift_x, shift_y), dim=1)
# For every (base anchor, output anchor) pair,
# offset each zero-centered base anchor by the center of the output anchor.
anchors.append((shifts.view(-1, 1, 4) + base_anchors.view(1, -1, 4)).reshape(-1, 4))
return anchors
처음에 cell_anchors 이 비었는지(존재하는지), grid size, stride, cell_anchors 의 길이가 모두 같은지 검사를 한번 한다.
이후 iterable 하게 만들기 위해 zip() 을 사용한다.
grid 의 크기, stride 크기, 연산을 할 device 를 각각 할당해 준다.
torch.arange(start, end, dtype, device) 를 이용해서 시작부터 끝까지 정수 시퀀스를 생성한다.
meshgrid : x축과 y축의 모든 조합을 생성
reshape(-1) --> 1차원으로 내림
shifts : x,y 좌표를 [shift_x, shift_y, shift_x, shift_y) 형태로 스택
anchors.append : 기본 앵커를 시프트된 위치로 이동시켜 최종 앵커 생성
예를 들어 base anchor 의 위치 : [25, 50, 50, 100]
shifts = [30, 20, 30, 20]
최종 앵커의 위치 = [55, 70, 80, 120]
이것을 반복
그리고 최종 앵커의 위치를 반환한다. 결론은 stride 를 참고해 base anchor 의 위치 변경
다시 forward 메서드로 돌아오면 anchor 를 저장하기 위해 함수들이 있다.
anchors: List[List[torch.Tensor]] = []
for _ in range(len(image_list.image_sizes)):
anchors_in_image = [anchors_per_feature_map for anchors_per_feature_map in anchors_over_all_feature_maps]
anchors.append(anchors_in_image)
anchors = [torch.cat(anchors_per_image) for anchors_per_image in anchors]
return anchors
image size 길이 만큼 반복을 한다.
anchors_in_image 는 아까 찾아냈던 앵커들을 anchors_in_image 에 다시 복사한다.
그리고 anchors 변수에 추가한다.
그리고 각 이미지에 대한 앵커를 하나의 텐서로 concatenate 한다.