서론
이 장은 MobileNetV2 모델의 코드를 리뷰한다.
목차
_make_divisible()
위 함수는 주어진 숫자를 특정 수로 나눌 수 있는 가장 가까운 수로 바꾸는 역할을 합니다. 이 함수는 모델의 계층에서 채널 수가 특정 값(예를 들어 8)의 배수가 되도록 하는데 사용된다.

먼저 아래코드에 대해서 설명해보겠다

int(v + divisor / 2)에서 v에 divisor/2 를 더해주는 이유는 v를 //divisor를 해줄 때 v의 소수점 자리수가 0.5보다 큰 경우에 대해서 반올림을 해주기 위해서다.
예를 들어, int(3.7)은 3을 반환한다. 이렇게 단순히 버림을 하면 반올림 효과를 얻을 수 없다.
하지만 divisor/2를 더한 뒤 int() 함수를 적용하면 반올림 효과를 얻을 수 있다.
divisor/2를 더하면 소수 부분이 0.5 이상일 때, 몫이 1증가하게 됩니다. 즉, 이는 실수를 가장 가까운 정수로 반올림하는 효과를 가져온다.
좀 더 자세히 설명하면 v가 5.6이고 divisor가 2라면 v + divisor/2는 5.6 + 2/2 = 6.6이 됩니다. 여기에 int() 함수를 적용하면 6을 얻는다. 이후 6 //divisor를 하고 * divisor를 하면 최종적으로 5.6은 2의 배수와 가장 가까운 6을 얻게 된다.
만약 divisor/2를 더하지 않고 int(v)를 사용했다면 결과는 5가 될 것이고 이후 5 //divisor * divisor를 하게되면 4를 얻게된다. 따라서 v + divisor/2를 사용함으로써 divisor의 배수에 가까운 수를 구하는 반올림 효과를 얻을 수 있다.
이렇게 하는 이유는 하드웨어 최적화를 위해서이다.
다음 분석해볼 코드는 아래와 같다.
if new_v < 0.9 * v:
new_v += divisor
return new_v
new_v는 원래의 값 v의 90% 이상의 값을 가져야 한다.
- 90% 이상인 경우(
divisor=8,v=15)v + divisor / 2는15 + 8 / 2이므로15 + 4인 19가 된다.- 이 값을
divisor로 나눈 후 다시 곱하면,19 // 8 * 8이므로 16이 됩니다. 이것이new_v의 초기값이다. - 원래의 값
v인 15의 90%는13.5입니다. 이 경우new_v인 16은v의 90%인 13.5보다 크므로new_v는 그대로 16이 된다.
- 90% 이상이 아닌 경우(
divisor=8,v=18)v + divisor / 2는18 + 8 / 2이므로18 + 4인 22가 된다.- 이를
divisor로 나눈 후 다시 곱하면22 // 8 * 8이므로 16이 됩니다. 이것이new_v의 초기값이다. - 원래의 값
v인 18의 90%는16.2이다.new_v인 16은v의 90%인16.2보다 작으므로, 이 조건문이 참이 된다. 따라서new_v에divisor인 8을 더하게 되어new_v는24가 된다. 이런 식으로new_v는 원래 값v의 90% 이상이 되도록 보장된다.
MobileNetV2
다음은 Conv + BN + ReLU를 만들어 주기 위한 클래스다. 3x3 kernel_size를 사용함에 따라 depthwise conv의 부분이다.

hidden_dim 에 in_channels*expansion factor를 하여 채널을 확장해준다.
그러나 86번줄에 if expand_ratio != 1: 가 있는 것을 볼 수 있다. 초기에 expansion factor가 1이기 때문에 kernel_size=1인 point-wise conv가 적용되지 않고 이후부터는 쭉 exapnsion factor가 6이기 때문에 이후에는 쭉 적용이 된다.

최종 클래스는 1000으로 두고, layer의 채널을 축소하는 width_mult는 1로 설정한다. 그리고 channel이 8의 배수가 되기 위해 round_nearest=8로 설정한다.

이후 아래 논문 처럼 t, c, n ,s 를 설정한다 → (expansion factor, channels, iteration, stride) 의 순서로 의미를 갖는다. input_channel과 self.last_channel이 8의 배수로 떨어지도록 _make_divisible로 점검해준다. 이후 features라는 변수에 nn.Module로 계속 레이어들을 리스트안에 추가한다.
논문에서는 초기에 일반 conv2d를 사용하여 output_size를 절반으로 줄이는 부분이 있다. 그 첫번째 부분에 해당하는 ConvBNReLU를 리스트에 추가해준다.


output_channel에 해당되는 c를 이 8의 배수가 되도록 점검해주는 _make_divisible를 통해서 대입해준다.

이 부분을 봐보면 n이 반복횟수인데 처음에만 stride에 대해서 s를 적용시켜주고, 이후부터는 s=1로 고정하는 것을 볼 수 있다.
이후 177번로 feature변수에 InvertedResidual 인스턴스인 block을 append시켜준다.
input_channel = output_channel 부분은 계속 입력의 채널을 convolution과 맞춰주기 위해서 넣어준다.
이후 마지막 channel이 1280인 conv2d를 만들어주고 self.features = nn.Sequential(*features)에 담는것을 볼 수 있다.

다음은 classifier에 대한 블록을 형성하고 이후 아래는 module이 Conv2d, BatchNorm2d, GroupNorm 등에 해당하는 layer를 가질때 각각의 weight를 초기화하는 방법이다.

다음은 forward에 해당하는 부분인데 x = self.features(x)의 output이 (1, 1280, 7, 7)을 가지게 되는데 이때 adaptive_avg_pool2d를 적용하여 (1, 1280, 1, 1)이 되고 reshape을 통해 최종적으로 (1, 1280)이 된다. 이후 classifier에 해당하는 fc layer와 이어진다.

이미 foward함수가 있는데도 _forward_impl이라는 함수를 사용하는 이유는 고성능 런타임의 torchscript의 코드와 호환될 수 있게 하기 위해따로 다음과 같이 foward를 구현한 것이라 생각한다.
Conclusion
MobileNetV2의 논문에서는 stride=1인 경우에 대해서 residual connection이 이루어진다고 나와있다. 따라서 논문에서 보여주는 t, c, n, s부분의 표만 보면 s=1인 경우는 3가지 밖에 없어 residual connection이 3번있을 것이라 추측할 수 있으나 실질적으로 n이 iteration될 때 처음에만 s가 달라지고 이후부터는 1로 고정되어 꾀나 많은 residual connection이 형성됨을 알 수 있다.
Comment