레즈넷은 한 개의 입력 줄기(input stem)와 네 개의 스테이지(stage)로 구성돼 있다.

입력 줄기는 7X7 합성곱 계층, 배치 정규화, ReLU, 3X3 최댓값 풀링으로 구성돼 있으며, 스테잊는 여러 개의 잔차 블록(Residual Block)으로 구성된다.

Untitled

레즈넷은 스테이지를 구성하는 블록 유형에 따라 나뉜다. 비교적 계층의 수가 적은 ResNet-18의 경우 스테이지별로 2개의 기본 블록이 존재하며, ResNet-34의 경우 3, 4, 6, 3개의 기본 블록으로 스테이지를 구성한다. ResNet-50부터는 깊은 구조로 인해 앞서 설명한 병목 블록을 사용한다. ResNet-50은 3, 4, 6, 3개의 병목 블록을 사용하고, ResNet-101과 ResNet-152는 각각 3, 4, 23, 3개와 3, 8, 36, 3개의 병목 블록으로 스테이지를 구성한다.

기본 블록

ResNet-18과 ResNet-34에서 사용하는 기본 블록을 구현해본다.

기본 블록은 3X3 합성곱 계층, 배치 정규화, ReLU를 두 번 반복해 연결한 구조를 갖는다.

from torch import nn

class BasicBlock(nn.Module):
    expansion = 1
#inplanes: 입력 특징 맵의 차원 수, planes: 출력 특징 맵의 차원 수
    def __init__(self, inplanes, planes, stride=1):
        super().__init__()
        #첫번째 합성곱 레이어
        self.conv1 = nn.Conv2d(
            inplanes, planes,
            kernel_size=3, stride=stride, padding=1, bias=False
        )
        #첫번째 배치 정규화 레이어
        self.bn1 = nn.BatchNorm2d(planes)
        #활성함수
        self.relu = nn.ReLU(inplace=True)
        #두번째 합성곱 레이어
        self.conv2 = nn.Conv2d(
            planes, planes,
            kernel_size=3, stride=1, padding=1, bias=False
        )
        #두번째 배치 정규화 레이어
        self.bn2 = nn.BatchNorm2d(planes)
        
				#비어 있는 시퀀셜을 생성해 잔차 연결 변수로 사용
        self.shortcut = nn.Sequential()
        #W_s를 구현: 차원을 맞추기 위한 연산
        if stride != 1 or inplanes != self.expansion*planes:
            self.shortcut = nn.Sequential(
                nn.Conv2d(
                    inplanes, self.expansion*planes,
                    kernel_size=1, stride=stride, bias=False
                ),
                nn.BatchNorm2d(self.expansion*planes)
            )

    def forward(self, x):
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        #잔차 연결을 통해 입력값과 출력값을 더한 다음 활성화
        out += self.shortcut(x)
        out = self.relu(out)
        return out

레즈넷은 두 번째 스테이지부터 마지막 스테이지까지 첫 번째 블록의 첫 번째 합성곱 계층에만 간격(stride)을 2로 사용하는 지점이 존재한다. 간격을 2로 키워 이미지의 크기를 줄이고 모델의 깊이를 증가시켜 연산량을 감소시킨다.

병목 블록

더 많은 합성곱 계층과 확장(expansion) 변수를 사용

병목 블록에서 사용되는 확장값은 병목 블록의 세 번째 합성곱 계층에서 사용된다. 이 값은 출력 차원 수를 늘려 더 많은 특징을 학습하고 성능을 높이기 위해 사용된다.

class BottleneckBlock(nn.Module):
#깊은 레즈넷에서는 확장 값을 4로 사용
#세 번째 합성곱 연산에서 출력 차원 수를 높여 모델의 목잡도와 매개변수의 수를 조절
    expansion = 4

    def __init__(self, inplanes, planes, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(
            inplanes, planes,
            kernel_size=1, bias=False
        )
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(
            planes, planes,
            kernel_size=3, stride=stride, padding=1, bias=False
        )
        self.bn2 = nn.BatchNorm2d(planes)
        self.conv3 = nn.Conv2d(
            planes, self.expansion*planes,
            kernel_size=1, bias=False
        )
        self.bn3 = nn.BatchNorm2d(self.expansion*planes)
        self.relu = nn.ReLU(inplace=True)

        self.shortcut = nn.Sequential()
        if stride != 1 or inplanes != self.expansion*planes:
            self.shortcut = nn.Sequential(
                nn.Conv2d(
                    inplanes, self.expansion*planes,
                    kernel_size=1, stride=stride, bias=False
                ),
                nn.BatchNorm2d(self.expansion*planes)
            )

    def forward(self, x):
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)
        out = self.conv3(out)
        out = self.bn3(out)
        out += self.shortcut(x)
        out = self.relu(out)
        return out

레즈넷 모델

블록이 여러 번 반복해 하나의 스테이지를 구성한다.

import torch

class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=1000):
        super().__init__()

        self.inplanes = 64
        self.stem = nn.Sequential(
            nn.Conv2d(3, self.inplanes, kernel_size=7, stride=2, padding=3, bias=False),
            nn.BatchNorm2d(self.inplanes),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        ) 
        #스테이지 시작점을 정하고, 레즈넷 유형에 따라 반복 횟수를 모듈화: 두 번째 스테이지부터 첫 간격은 2
        self.stage1 = self._make_layer(block, 64, layers[0], stride=1) #첫 번째 스테이지
        self.stage2 = self._make_layer(block, 128, layers[1], stride=2) #이후 스테이지의 첫 번째 블록 간격
        self.stage3 = self._make_layer(block, 256, layers[2], stride=2)
        self.stage4 = self._make_layer(block, 512, layers[3], stride=2)
				
				#적응형 평균값 풀링
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        #선형 변환
        self.fc = nn.Linear(512 * block.expansion, num_classes)

    def _make_layer(self, block, planes, num_blocks, stride):
        layers = []
        layers.append(block(self.inplanes, planes, stride))
        self.inplanes = planes * block.expansion
        #두 번째 블록부터는 블록 수 -1만큼 반복해 간격을 1로 생성
        for _ in range(num_blocks - 1):
            layers.append(block(self.inplanes, planes, 1))
        
        return nn.Sequential(*layers)
		#순전파
    def forward(self, x):
        out = self.stem(x)
        out = self.stage1(out)
        out = self.stage2(out)
        out = self.stage3(out)
        out = self.stage4(out)
        #평균값 풀링
        out = self.avgpool(out)
        #배열을 1차원으로 평탄화
        out = torch.flatten(out, 1)
        out = self.fc(out)
        return out

ResNet 클래스를 생성해 stem과 stage를 구성한다.

입력 줄기의 합성곱 계층은 3채널 이미지를 전달받으므로 입력 데이터 차원의 크기는 3, 출력 데이터 차원의 크기는 64로 전달한다.

레즈넷에서 합성곱 계층을 사용하는 경우, 배치 정규화와 ReLU 함수가 함께 사용된다. 이후 최댓값 풀링을 적용한다.

적응형 평균값 풀링