Slow is better than NOTHING

Computer Science/5. Deep learning

[Object Detection] Object Detection 튜토리얼 - SSD300 Implementation

Jeff_Kang 2021. 1. 11. 17:37
반응형

지금까지, Object detection 모델 중 SSD 에 대한 기본적인 이해와 어떻게 학습이 되는지에 대해 알아보았습니다.
이번 포스트에서는 실제 Object detection model 인 SSD를 pytorch 를 이용해 구현해보고 내부 프로세스에 대해 자세히 알아보도록 하겠습니다.


 

Dataset

dataset 으로는 Pascal Visual Object Classes (VOC) data를 사용합니다. 2007, 2012 모델 모두 사용가능합니다. 
VOC dataset은 다음과 같은 20개의 class로 이루어져있습니다.

 

{'aeroplane', 'bicycle', 'bird', 'boat', 'bottle', 'bus', 'car', 'cat', 'chair', 'cow', 'diningtable', 'dog', 'horse', 'motorbike', 'person', 'pottedplant', 'sheep', 'sofa', 'train', 'tvmonitor'}

각각의 Object는 절대좌표를 기준으로 bbox가 labeling되어 있으며, 각 label은 위 20개의 label 중 1개입니다. 또한 이 Dataset에는 difficulty라는 항목이 있는데, 이는 탐지하기 어려운 객체임을 표시해둔 것입니다. (0 = not difficult, 1=difficult). 모델을 evaluation 하는데 있어, 간혹 difficult examples에 대한 검증을 따로 하거나 빼고하는 경우도 있습니다.

Dataset은 아래의 링크에서 다운받으실 수 있습니다. Linux 환경을 사용하시는 경우 다음과 같은 명령어로 Local에 다운로드 받으실 수 있습니다. 또는, wget 이하 링크에 접속하셔서 로컬 파일을 다운로드 받으실수도 있습니다.

cd ~/workspace # moving to own workspace folder
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2007.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2012/VOCtrainval_11-May-2012.tar
wget http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtest_06-Nov-2007.tar

tar -xvf VOCtrainval_06-Nov-2007.tar # VOC2007 trainval
tar -xvf VOCtrainval_11-May-2012.tar # VOC2012 trainval 
tar -xvf VOCtest_06-Nov-2007.tar     # VOC2007 test

SSD author는 VOC2007 과  VOC2012 dataset을 동시에 사용합니다. 따라서 이번 Implementation에서도 두 가지  trainval dataset을 모두 사용할것입니다.


Inputs of the model

SSD300을 구현할 예정이므로, 모델의 Input size는 (300,300) RGB image입니다. base conv로 이용하는 VGG16 pre-trained network는 torchvision에서 제공하는 모델을 이용합니다. 

Normalization

이미지를 model에 입력하기 전 모든 이미지는 [0,1] 사이로 되어있어야합니다. 따라서, preprocessing으로 mean, std값을 적용하여 이미지의 스케일을 [0,1] 사이로 Normalize 시켜줍니다.

mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]

또한, Pytorch 에서는 (N,C,H,W) 형식의 RGB format을 사용하기 때문에, (C) channel dimension은 size dimension보다 앞에 위치해야합니다. 즉, 300x300x3 RGB 이미지를 Pytorch 형태로 표현하려면, (N, 3, 300, 300) 형태의 Tensor가 됩니다.

Bounding Boxes 

각 이미지에 대해 ground truth object 에 해당하는 boundary coordinates (x_min, y_min, x_max, y_max) 를 제공해야합니다. 이미지마다 존재하는 객체의 수가 다르기 때문에, N개의 sample에 대해 Bounding box를 저장하기 위한 고정된 크기의 텐서를 사용할 수 없습니다.  따라서, 모델에 feeding되는 ground truth bounding boxes는 그 List의 길이를 같이 제공해주어야 합니다. List의 각 요소는 (N_o, 4) 형태이며 N_o는 그 이미지에 들어있는 Object의 갯수입니다.

Labels

인간이 객체를 판별하기 위해, 그 이미지의 객체에 해당하는 ground truth label을 제공해주어야 합니다. (e.g., cat, person etc.) 각 Label은 1~20 으로 나타내며 20가지의 다른 클래스가 존재합니다. 또한, Index 0를 갖는 Background 를 위한 클래스까지, 총 21가지의 클래스가 존재합니다. 
Label 역시 한 이미지에 담겨있는 Object의 수를 알 수 없기 때문에, Label 값을 저장하기 위한 고정된 크기의 텐서를 사용할 수 없습니다. Bounding box 와 마찬가지로 feeding 되기 전 ground truth label의 길이를 제공해주어야 하고 N_o형태로 나타냅니다. N_o는 이미지에 나타난 object의 갯수입니다.


Base Conv 

SSD300은 VGG16 network를 base conv net으로 활용합니다. 여기서는 torchvision에서 제공하는 vgg16(pretrained=True) 모델을 base net으로 사용할 예정입니다.

앞서 포스트한 내용에서 언급했든, 기본 VGG16 network 를 일부 수정(fc6, fc7 layer --> conv6, conv7) 한 VGGBase 모듈입니다. conv6 에서 사용된 "dilation" 개념은 자세히 설명하지 않도록 하겠습니다. SSD author 는 연산량이 많아지는것을 막기 위해 atrous convlution을 통해 연산량을 줄이고 훈련 속도를 증가시켰습니다. 

새롭게 수정된 VGGBase module은 "self.load_pretrained_layers()" 에 의해 torchvision 에서 제공하는 vgg16 network를 ImageNet에서 pre-train 시킨 모델의 weight값으로 초기화됩니다. 단, fc layer 를 conv 로 변환시켜주었기에 일부 연산 shape이 바뀌지만 parameter 수는 갖기 때문에 연산 shape 변환을 통해 weight초기화가 가능합니다. 

Fig 1. Modified VGG-16 network for SSD300

class VGGBase(nn.Module):
    """
    VGG base convolutions to produce lower-level feature maps.
    """

    def __init__(self):
        super(VGGBase, self).__init__()

        # Standard convolutional layers in VGG16
        self.conv1_1 = nn.Conv2d(3, 64, kernel_size=3, padding=1)  # stride = 1, by default
        self.conv1_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)

        self.conv2_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.conv2_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)

        self.conv3_1 = nn.Conv2d(128, 256, kernel_size=3, padding=1)
        self.conv3_2 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.conv3_3 = nn.Conv2d(256, 256, kernel_size=3, padding=1)
        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2, ceil_mode=True)  # ceiling (not floor) here for even dims

        self.conv4_1 = nn.Conv2d(256, 512, kernel_size=3, padding=1)
        self.conv4_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv4_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)

        self.conv5_1 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.conv5_3 = nn.Conv2d(512, 512, kernel_size=3, padding=1)
        self.pool5 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)  # retains size because stride is 1 (and padding)

	#########################################
        # Replacements for FC6 and FC7 in VGG16 #
        #########################################
        self.conv6 = nn.Conv2d(512, 1024, kernel_size=3, padding=6, dilation=6)  # atrous convolution
        self.conv7 = nn.Conv2d(1024, 1024, kernel_size=1)

        # Load pretrained layers
        self.load_pretrained_layers()

    def forward(self, image):
        """
        Forward propagation.

        :param image: images, a tensor of dimensions (N, 3, 300, 300)
        :return: lower-level feature maps conv4_3 and conv7
        """
        out = F.relu(self.conv1_1(image))  # (N, 64, 300, 300)
        out = F.relu(self.conv1_2(out))  # (N, 64, 300, 300)
        out = self.pool1(out)  # (N, 64, 150, 150)

        out = F.relu(self.conv2_1(out))  # (N, 128, 150, 150)
        out = F.relu(self.conv2_2(out))  # (N, 128, 150, 150)
        out = self.pool2(out)  # (N, 128, 75, 75)

        out = F.relu(self.conv3_1(out))  # (N, 256, 75, 75)
        out = F.relu(self.conv3_2(out))  # (N, 256, 75, 75)
        out = F.relu(self.conv3_3(out))  # (N, 256, 75, 75)
        out = self.pool3(out)  # (N, 256, 38, 38), it would have been 37 if not for ceil_mode = True

        out = F.relu(self.conv4_1(out))  # (N, 512, 38, 38)
        out = F.relu(self.conv4_2(out))  # (N, 512, 38, 38)
        out = F.relu(self.conv4_3(out))  # (N, 512, 38, 38)
        conv4_3_feats = out  # (N, 512, 38, 38)
        out = self.pool4(out)  # (N, 512, 19, 19)

        out = F.relu(self.conv5_1(out))  # (N, 512, 19, 19)
        out = F.relu(self.conv5_2(out))  # (N, 512, 19, 19)
        out = F.relu(self.conv5_3(out))  # (N, 512, 19, 19)
        out = self.pool5(out)  # (N, 512, 19, 19), pool5 does not reduce dimensions

        out = F.relu(self.conv6(out))  # (N, 1024, 19, 19)

        conv7_feats = F.relu(self.conv7(out))  # (N, 1024, 19, 19)

        # Lower-level feature maps
        return conv4_3_feats, conv7_feats

    def load_pretrained_layers(self):
        """
        As in the paper, we use a VGG-16 pretrained on the ImageNet task as the base network.
        There's one available in PyTorch, see https://pytorch.org/docs/stable/torchvision/models.html#torchvision.models.vgg16
        We copy these parameters into our network. It's straightforward for conv1 to conv5.
        However, the original VGG-16 does not contain the conv6 and con7 layers.
        Therefore, we convert fc6 and fc7 into convolutional layers, and subsample by decimation. See 'decimate' in utils.py.
        """
        # Current state of base
        state_dict = self.state_dict()
        param_names = list(state_dict.keys())

        # Pretrained VGG base
        pretrained_state_dict = torchvision.models.vgg16(pretrained=True).state_dict()
        pretrained_param_names = list(pretrained_state_dict.keys())

        # Transfer conv. parameters from pretrained model to current model
        for i, param in enumerate(param_names[:-4]):  # excluding conv6 and conv7 parameters
            state_dict[param] = pretrained_state_dict[pretrained_param_names[i]]

        # Convert fc6, fc7 to convolutional layers, and subsample (by decimation) to sizes of conv6 and conv7
        # fc6
        conv_fc6_weight = pretrained_state_dict['classifier.0.weight'].view(4096, 512, 7, 7)  # (4096, 512, 7, 7)
        conv_fc6_bias = pretrained_state_dict['classifier.0.bias']  # (4096)
        state_dict['conv6.weight'] = decimate(conv_fc6_weight, m=[4, None, 3, 3])  # (1024, 512, 3, 3)
        state_dict['conv6.bias'] = decimate(conv_fc6_bias, m=[4])  # (1024)
        # fc7
        conv_fc7_weight = pretrained_state_dict['classifier.3.weight'].view(4096, 4096, 1, 1)  # (4096, 4096, 1, 1)
        conv_fc7_bias = pretrained_state_dict['classifier.3.bias']  # (4096)
        state_dict['conv7.weight'] = decimate(conv_fc7_weight, m=[4, 4, None, None])  # (1024, 1024, 1, 1)
        state_dict['conv7.bias'] = decimate(conv_fc7_bias, m=[4])  # (1024)

        # Note: an FC layer of size (K) operating on a flattened version (C*H*W) of a 2D image of size (C, H, W)...
        # ...is equivalent to a convolutional layer with kernel size (H, W), input channels C, output channels K...
        # ...operating on the 2D image of size (C, H, W) without padding

        self.load_state_dict(state_dict)

        print("\nLoaded base model.\n")

Auxiliary Convolutions

SSD 는 VGG network 의 마지막 Layer에서 추출한 Feature map 만을 활용하는 것이 아닌, 여러가지 Layer에서 다양한 Feature map을 추출할 수 있는 Auxiliary Conv를 활용합니다. 각 Layer에서는 stride를 통해 Pooling을 하고, 총 6개의 다른 scale을 가진 conv4_3, conv7, conv8_2, conv9_2, conv10_2, conv11_2 Feature map 을 활용합니다.
참고, initializer로는 Xavier 를 사용합니다. 

Fig 2. Auxiliary Convolutions on base-network to extract multiple feature map

class AuxiliaryConvolutions(nn.Module):
    """
    Additional convolutions to produce higher-level feature maps.
    """

    def __init__(self):
        super(AuxiliaryConvolutions, self).__init__()

        # Auxiliary/additional convolutions on top of the VGG base
        self.conv8_1 = nn.Conv2d(1024, 256, kernel_size=1, padding=0)  # stride = 1, by default
        self.conv8_2 = nn.Conv2d(256, 512, kernel_size=3, stride=2, padding=1)  # dim. reduction because stride > 1

        self.conv9_1 = nn.Conv2d(512, 128, kernel_size=1, padding=0)
        self.conv9_2 = nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1)  # dim. reduction because stride > 1

        self.conv10_1 = nn.Conv2d(256, 128, kernel_size=1, padding=0)
        self.conv10_2 = nn.Conv2d(128, 256, kernel_size=3, padding=0)  # dim. reduction because padding = 0

        self.conv11_1 = nn.Conv2d(256, 128, kernel_size=1, padding=0)
        self.conv11_2 = nn.Conv2d(128, 256, kernel_size=3, padding=0)  # dim. reduction because padding = 0

        # Initialize convolutions' parameters
        self.init_conv2d()

    def init_conv2d(self):
        """
        Initialize convolution parameters.
        """
        for c in self.children():
            if isinstance(c, nn.Conv2d):
                nn.init.xavier_uniform_(c.weight)
                nn.init.constant_(c.bias, 0.)

    def forward(self, conv7_feats):
        """
        Forward propagation.

        :param conv7_feats: lower-level conv7 feature map, a tensor of dimensions (N, 1024, 19, 19)
        :return: higher-level feature maps conv8_2, conv9_2, conv10_2, and conv11_2
        """
        out = F.relu(self.conv8_1(conv7_feats))  # (N, 256, 19, 19)
        out = F.relu(self.conv8_2(out))  # (N, 512, 10, 10)
        conv8_2_feats = out  # (N, 512, 10, 10)

        out = F.relu(self.conv9_1(out))  # (N, 128, 10, 10)
        out = F.relu(self.conv9_2(out))  # (N, 256, 5, 5)
        conv9_2_feats = out  # (N, 256, 5, 5)

        out = F.relu(self.conv10_1(out))  # (N, 128, 5, 5)
        out = F.relu(self.conv10_2(out))  # (N, 256, 3, 3)
        conv10_2_feats = out  # (N, 256, 3, 3)

        out = F.relu(self.conv11_1(out))  # (N, 128, 3, 3)
        conv11_2_feats = F.relu(self.conv11_2(out))  # (N, 256, 1, 1)

        # Higher-level feature maps
        return conv8_2_feats, conv9_2_feats, conv10_2_feats, conv11_2_feats

Prediction Convolutions

Prediction Conv는 Aux conv로 부터 추출된 6개의 다른 scale을 갖는 Feature map을 활용하여 bounding box의 offset을 구하는 "Localization task", 탐지 된 객체의 라벨을 분류하는 "Classification task"를 담당합니다.
Localization은 4개의 Offset을 구해야 하기 때문에, $(g^c_x, g^c_y, g_w, g_h)$ prior box 에 4를 곱해준 output을 반환하고, Classification은 각 Class별 score를 계산해야하므로 데이터셋에 존재하는 전체 라벨의 갯수를 곱해주어야합니다.  

참고적으로, forward pass에서 이루어지는 "contiguous()" function 에 대한 이해를 보충하자면, view 함수를 통해 실제 (8732, 4) 또는 (8732, n_classes) 형태로 텐서 모양을 변경해주어야하는데 Pytorch 에서는 항상 continuous한 메모리 연산을 보장하지 않습니다. 이 말인 즉, 연산을 하는 메모리의 chunk의 배열이 항상 일정하지 않을 수 있기 때문에(permute, transpose, expand, narrow 등의 함수) 이를 stacking 해서 아래의 Fig 3과 같은 연속적인 값들이 하나의 grid를 나타내는 속성을 표현할 수 없을 수도 있습니다. 이때, Pytorch에서는 연속적이지 않은 메모리를 참조했다고 Runtime Error 를 주긴 합니다. 따라서, contiguous() 함수를 호출하게 되면, 변형된 Tensor 가 원본 Element의 순서가 같은 모양의 Tensor 가 만들어집니다(Permutation Equivalent).

Fig 3. Location prediction output

class PredictionConvolutions(nn.Module):
    """
    Convolutions to predict class scores and bounding boxes using lower and higher-level feature maps.

    The bounding boxes (locations) are predicted as encoded offsets w.r.t each of the 8732 prior (default) boxes.
    See 'cxcy_to_gcxgcy' in utils.py for the encoding definition.

    The class scores represent the scores of each object class in each of the 8732 bounding boxes located.
    A high score for 'background' = no object.
    """

    def __init__(self, n_classes):
        """
        :param n_classes: number of different types of objects
        """
        super(PredictionConvolutions, self).__init__()

        self.n_classes = n_classes

        # Number of prior-boxes we are considering per position in each feature map
        n_boxes = {'conv4_3': 4,
                   'conv7': 6,
                   'conv8_2': 6,
                   'conv9_2': 6,
                   'conv10_2': 4,
                   'conv11_2': 4}
        # 4 prior-boxes implies we use 4 different aspect ratios, etc.

        # Localization prediction convolutions (predict offsets w.r.t prior-boxes)
        self.loc_conv4_3 = nn.Conv2d(512, n_boxes['conv4_3'] * 4, kernel_size=3, padding=1)
        self.loc_conv7 = nn.Conv2d(1024, n_boxes['conv7'] * 4, kernel_size=3, padding=1)
        self.loc_conv8_2 = nn.Conv2d(512, n_boxes['conv8_2'] * 4, kernel_size=3, padding=1)
        self.loc_conv9_2 = nn.Conv2d(256, n_boxes['conv9_2'] * 4, kernel_size=3, padding=1)
        self.loc_conv10_2 = nn.Conv2d(256, n_boxes['conv10_2'] * 4, kernel_size=3, padding=1)
        self.loc_conv11_2 = nn.Conv2d(256, n_boxes['conv11_2'] * 4, kernel_size=3, padding=1)

        # Class prediction convolutions (predict classes in localization boxes)
        self.cl_conv4_3 = nn.Conv2d(512, n_boxes['conv4_3'] * n_classes, kernel_size=3, padding=1)
        self.cl_conv7 = nn.Conv2d(1024, n_boxes['conv7'] * n_classes, kernel_size=3, padding=1)
        self.cl_conv8_2 = nn.Conv2d(512, n_boxes['conv8_2'] * n_classes, kernel_size=3, padding=1)
        self.cl_conv9_2 = nn.Conv2d(256, n_boxes['conv9_2'] * n_classes, kernel_size=3, padding=1)
        self.cl_conv10_2 = nn.Conv2d(256, n_boxes['conv10_2'] * n_classes, kernel_size=3, padding=1)
        self.cl_conv11_2 = nn.Conv2d(256, n_boxes['conv11_2'] * n_classes, kernel_size=3, padding=1)

        # Initialize convolutions' parameters
        self.init_conv2d()

    def init_conv2d(self):
        """
        Initialize convolution parameters.
        """
        for c in self.children():
            if isinstance(c, nn.Conv2d):
                nn.init.xavier_uniform_(c.weight)
                nn.init.constant_(c.bias, 0.)

    def forward(self, conv4_3_feats, conv7_feats, conv8_2_feats, conv9_2_feats, conv10_2_feats, conv11_2_feats):
        """
        Forward propagation.

        :param conv4_3_feats: conv4_3 feature map, a tensor of dimensions (N, 512, 38, 38)
        :param conv7_feats: conv7 feature map, a tensor of dimensions (N, 1024, 19, 19)
        :param conv8_2_feats: conv8_2 feature map, a tensor of dimensions (N, 512, 10, 10)
        :param conv9_2_feats: conv9_2 feature map, a tensor of dimensions (N, 256, 5, 5)
        :param conv10_2_feats: conv10_2 feature map, a tensor of dimensions (N, 256, 3, 3)
        :param conv11_2_feats: conv11_2 feature map, a tensor of dimensions (N, 256, 1, 1)
        :return: 8732 locations and class scores (i.e. w.r.t each prior box) for each image
        """
        batch_size = conv4_3_feats.size(0)

        # Predict localization boxes' bounds (as offsets w.r.t prior-boxes)
        l_conv4_3 = self.loc_conv4_3(conv4_3_feats)  # (N, 16, 38, 38)
        l_conv4_3 = l_conv4_3.permute(0, 2, 3,
                                      1).contiguous()  # (N, 38, 38, 16), to match prior-box order (after .view())
        # (.contiguous() ensures it is stored in a contiguous chunk of memory, needed for .view() below)
        l_conv4_3 = l_conv4_3.view(batch_size, -1, 4)  # (N, 5776, 4), there are a total 5776 boxes on this feature map

        l_conv7 = self.loc_conv7(conv7_feats)  # (N, 24, 19, 19)
        l_conv7 = l_conv7.permute(0, 2, 3, 1).contiguous()  # (N, 19, 19, 24)
        l_conv7 = l_conv7.view(batch_size, -1, 4)  # (N, 2166, 4), there are a total 2116 boxes on this feature map

        l_conv8_2 = self.loc_conv8_2(conv8_2_feats)  # (N, 24, 10, 10)
        l_conv8_2 = l_conv8_2.permute(0, 2, 3, 1).contiguous()  # (N, 10, 10, 24)
        l_conv8_2 = l_conv8_2.view(batch_size, -1, 4)  # (N, 600, 4)

        l_conv9_2 = self.loc_conv9_2(conv9_2_feats)  # (N, 24, 5, 5)
        l_conv9_2 = l_conv9_2.permute(0, 2, 3, 1).contiguous()  # (N, 5, 5, 24)
        l_conv9_2 = l_conv9_2.view(batch_size, -1, 4)  # (N, 150, 4)

        l_conv10_2 = self.loc_conv10_2(conv10_2_feats)  # (N, 16, 3, 3)
        l_conv10_2 = l_conv10_2.permute(0, 2, 3, 1).contiguous()  # (N, 3, 3, 16)
        l_conv10_2 = l_conv10_2.view(batch_size, -1, 4)  # (N, 36, 4)

        l_conv11_2 = self.loc_conv11_2(conv11_2_feats)  # (N, 16, 1, 1)
        l_conv11_2 = l_conv11_2.permute(0, 2, 3, 1).contiguous()  # (N, 1, 1, 16)
        l_conv11_2 = l_conv11_2.view(batch_size, -1, 4)  # (N, 4, 4)

        # Predict classes in localization boxes
        c_conv4_3 = self.cl_conv4_3(conv4_3_feats)  # (N, 4 * n_classes, 38, 38)
        c_conv4_3 = c_conv4_3.permute(0, 2, 3,
                                      1).contiguous()  # (N, 38, 38, 4 * n_classes), to match prior-box order (after .view())
        c_conv4_3 = c_conv4_3.view(batch_size, -1,
                                   self.n_classes)  # (N, 5776, n_classes), there are a total 5776 boxes on this feature map

        c_conv7 = self.cl_conv7(conv7_feats)  # (N, 6 * n_classes, 19, 19)
        c_conv7 = c_conv7.permute(0, 2, 3, 1).contiguous()  # (N, 19, 19, 6 * n_classes)
        c_conv7 = c_conv7.view(batch_size, -1,
                               self.n_classes)  # (N, 2166, n_classes), there are a total 2116 boxes on this feature map

        c_conv8_2 = self.cl_conv8_2(conv8_2_feats)  # (N, 6 * n_classes, 10, 10)
        c_conv8_2 = c_conv8_2.permute(0, 2, 3, 1).contiguous()  # (N, 10, 10, 6 * n_classes)
        c_conv8_2 = c_conv8_2.view(batch_size, -1, self.n_classes)  # (N, 600, n_classes)

        c_conv9_2 = self.cl_conv9_2(conv9_2_feats)  # (N, 6 * n_classes, 5, 5)
        c_conv9_2 = c_conv9_2.permute(0, 2, 3, 1).contiguous()  # (N, 5, 5, 6 * n_classes)
        c_conv9_2 = c_conv9_2.view(batch_size, -1, self.n_classes)  # (N, 150, n_classes)

        c_conv10_2 = self.cl_conv10_2(conv10_2_feats)  # (N, 4 * n_classes, 3, 3)
        c_conv10_2 = c_conv10_2.permute(0, 2, 3, 1).contiguous()  # (N, 3, 3, 4 * n_classes)
        c_conv10_2 = c_conv10_2.view(batch_size, -1, self.n_classes)  # (N, 36, n_classes)

        c_conv11_2 = self.cl_conv11_2(conv11_2_feats)  # (N, 4 * n_classes, 1, 1)
        c_conv11_2 = c_conv11_2.permute(0, 2, 3, 1).contiguous()  # (N, 1, 1, 4 * n_classes)
        c_conv11_2 = c_conv11_2.view(batch_size, -1, self.n_classes)  # (N, 4, n_classes)

        # A total of 8732 boxes
        # Concatenate in this specific order (i.e. must match the order of the prior-boxes)
        locs = torch.cat([l_conv4_3, l_conv7, l_conv8_2, l_conv9_2, l_conv10_2, l_conv11_2], dim=1)  # (N, 8732, 4)
        classes_scores = torch.cat([c_conv4_3, c_conv7, c_conv8_2, c_conv9_2, c_conv10_2, c_conv11_2],
                                   dim=1)  # (N, 8732, n_classes)

        return locs, classes_scores

Training Model

Base/Auxiliary/Prediction Convolution에 대한 설계과 완료되었다면, 이를 이용하여 SSD300 을 훈련시키기 위한 모듈을 디자인합니다. 아래의 SSD300 class에서는 Base Conv-> Auxiliary Conv -> Prediction Conv 순서로 Training 이 이루어집니다. 여기서 주목할 점은, self.base 에서 나온 conv4_3_feats 의 output의 feature value는 aux_conv output(conv8_2, conv9_2, conv10_2, conv11_2) 에 비해 scale적인 측면에서 차이값이 크기 때문에, predict_conv로 입력 전 이를 L2-normalize를 적용하도록 Paper에서 안내하고있습니다. 이를 통해 값을 (0,1) 사이로 scaling 할 수 있기 때문에, 비정상적인 feature value 차이로 인한 loss exploding 을 방지할 수 있습니다. 

class SSD300(nn.Module):
    """
    The SSD300 network - encapsulates the base VGG network, auxiliary, and prediction convolutions.
    """

    def __init__(self, n_classes):
        super(SSD300, self).__init__()

        self.n_classes = n_classes

        self.base = VGGBase()
        self.aux_convs = AuxiliaryConvolutions()
        self.pred_convs = PredictionConvolutions(n_classes)

        # Since lower level features (conv4_3_feats) have considerably larger scales, we take the L2 norm and rescale
        # Rescale factor is initially set at 20, but is learned for each channel during back-prop
        self.rescale_factors = nn.Parameter(torch.FloatTensor(1, 512, 1, 1))  # there are 512 channels in conv4_3_feats
        nn.init.constant_(self.rescale_factors, 20)

    def forward(self, image):
        """
        Forward propagation.

        :param image: images, a tensor of dimensions (N, 3, 300, 300)
        :return: 8732 locations and class scores (i.e. w.r.t each prior box) for each image
        """
        # Run VGG base network convolutions (lower level feature map generators)
        mean = [0.485, 0.456, 0.406]
        std = [0.229, 0.224, 0.225]
        image = data_normalize(image, mean, std)
        conv4_3_feats, conv7_feats = self.base(image)  # (N, 512, 38, 38), (N, 1024, 19, 19)

        # Rescale conv4_3 after L2 norm
        norm = conv4_3_feats.pow(2).sum(dim=1, keepdim=True).sqrt()  # (N, 1, 38, 38)
        conv4_3_feats = conv4_3_feats / norm  # (N, 512, 38, 38)
        conv4_3_feats = conv4_3_feats * self.rescale_factors  # (N, 512, 38, 38)
        # (PyTorch autobroadcasts singleton dimensions during arithmetic)

        # Run auxiliary convolutions (higher level feature map generators)
        conv8_2_feats, conv9_2_feats, conv10_2_feats, conv11_2_feats = \
            self.aux_convs(conv7_feats)  # (N, 512, 10, 10),  (N, 256, 5, 5), (N, 256, 3, 3), (N, 256, 1, 1)

        # Run prediction convolutions (predict offsets w.r.t prior-boxes and classes in each resulting localization box)
        locs, classes_scores = self.pred_convs(conv4_3_feats, conv7_feats, conv8_2_feats, conv9_2_feats, conv10_2_feats,
                                               conv11_2_feats)  # (N, 8732, 4), (N, 8732, n_classes)

        return locs, classes_scores

 


본 포스트는 모델 아키텍처에 대한 이해를 위한 내용으로, MultiBox loss에 대한 내용은 생략되어있습니다. 보다 자세한 Implementation내용은 아래 깃헙 주소에서 확인 가능합니다. 로컬 GPU가 있으시다면 직접 훈련을 시켜보며 그 Output을 보시고 fine-tunning하는 과정을 해보시길 추천드립니다. 

github.com/sgrvinod/a-PyTorch-Tutorial-to-Object-Detection

github.com/Jeffkang-94/pytorch-SSD300

 

Jeffkang-94/pytorch-SSD300

Pytorch Implementation for SSD300. Contribute to Jeffkang-94/pytorch-SSD300 development by creating an account on GitHub.

github.com

 

sgrvinod/a-PyTorch-Tutorial-to-Object-Detection

SSD: Single Shot MultiBox Detector | a PyTorch Tutorial to Object Detection - sgrvinod/a-PyTorch-Tutorial-to-Object-Detection

github.com

 

반응형