결국 network의 깊이가 성능에 중요한 영향을 미친다.
기존에도 layer를 여러번 쌓으면 모든 함수가 표현이 가능하다. 그러나 여기에서는 명시적으로 F(x)=H(x)-x를 표현한다. 명시적으로 표현하는 것과 암묵적으로 표현하는 것에는 학습의 어려움 정도가 다르다.
결국 layer에서 H(x)=F(x)+x가 되고 layer가 엄청나게 깊어져도 F(x)=0이 되면 H(x)=x 즉 identity mapping이 됨으로써 기존의 20 layer 보다 56layer가 training error이 더 높은 상황을 방지한다. 왜냐하면 최소한 identity mapping으로 나머지 26 layer를 채우면 20layer와 동일하게 만들 수 있기 때문이다. 결국 degradation 문제를 방지할 수 있다.
결국 degradation은 일반 multiple non-linear layers에서 identity mapping으로의 수렴이 잘 이루어지지 않는 것을 의미하기도 한다. 만약 identity mapping이 optimal이라면 identity로 갔을 것이기 때문.
실제 문제에서는 identity mapping이 최적일 가능성이 낮지만 이러한 H(x)=F(x)+x의 변환이 도움을 준다.
만약 최적 함수가 identity mapping에 가깝다면 새로운 함수를 만들어내는 것보다 identity mapping을 참조해서 변동을 만들어내기 쉽기 때문이다.
실제로 학습된 결과를 분석한 위 이미지를 보면 plain보다 resnet에서 output의 표준편차가 적다. 즉 identity mapping을 참조해서 변동이 생기기 때문에 기존 흐름에서 큰 변화가 생기지 않는 것이다.
논문에서는 VGG net을 참고하였는데 여기에서 영감을 받아 2가지 형태의 3x3 Conv layer가 들어간다.
또한 feature map의 down sampling은 cnn을 stride 2로 설정하여 적용.
shortcut의 down sampling은 2가지 방법으로 구현이 가능
A. identtiy mapping인데 dim을 늘리기 위해 나머지 공간을 0으로 채우고 feature map을 절반으로 줄이기 위해 stride 2로 이동
B. dim을 늘리는 1x1 cnn을 stride 2로 적용
224x224 이미지 기준
1.각각 conv layer, activation 사이에 BN(batch normalization) layer 삽입
2. weight는 He initialization으로 적용
3. 미니배치 256
4. learning rate는 0.1에서 시작해서 고원(plateaus)에 도달하면 10을 나누는 식으로 진행
5. 학습 60x10^6 iteration 진행
6. SGD에 weight decay 0.0001, momentum 0.9 적용
7. no dropout
주로 실험에 관한 내용이다. imagenet classifier에서
위와 같은 결과가 나왔는데 이는 plain network에서는 degradation problem이 나타난다 (38 layer가 18layer보다 error이 더 높음) 반면에 resnet은 해결이 되었다.
degradation 문제는 gradient exploding, vanishing 등의 문제가 아니다. 왜냐하면 직접 forward, backward 연산이 정상적인 것을 확인하였고 plain network에도 BN(batch normalization)이 적용이 되었기 때문에 어느정도 gradient 문제에 robust 하기 때문.
resnet에는 identity mapping을 사용해서 extra parameter가 없는데도 오히려 error가 줄어든 것을 볼 수 있음. 이는 간단한 shortcut 만으로도 degradation을 처리할 수 있는 것을 보여줌. 그리고 이제 layer의 depth를 더 늘려서 성능을 얻을 수 있음을 의미함.
18 layer와 같은 얕은 resnet에서는 큰 성능의 증가가 없었지만 plain network와 비교해서 빠른 loss 감소를 보임
이는 resnet의 형태가 더 쉬운 학습을 보여준다는 것을 의미함.
이 부분은 위의 내용과 조금 차이가 있는 실험이라서 따로 나누었다. 이전에 identity mapping 즉 parameter가 없이 그냥 매핑만 해주는 경우에 우리는 성능 증가를 볼 수 있었다. 그러나 shortcut에는 3가지의 옵션이 존재한다.
A. dim을 증가시키는 shortcut 에는 zero padding을 적용하고 나머지에는 identity mapping
B. dim을 증가시키는 shortcut에 projection weight적용(1x1 conv 등) 나머지에는 identity mapping
C. 모든 shortcut에 projection을 적용
위 내용은 A, B, C의 테스트 결과인데 보면 A<B<C의 순서이다.
오히려 정확도가 증가하는 모습을 보여주는데 논문에서는 이를 단순히 weight param 증가의 효과로 보고있다.
그리고 큰 차이가 없는 것을 통해 degradation 문제에서 shortcut의 weight는 필수가 아닌 것을 보여준다는 것을 알 수있다.
class ResBlock(nn.Module):
def __init__(self, in_channel, out_channel, is_downsample=False):
super().__init__()
self.is_downsample = is_downsample
stride = 2 if is_downsample else 1
# conv2d 기본적으로 he uniform initialization을 사용하기에 따로 설정 x
self.seq = nn.Sequential(
nn.Conv2d(in_channel, out_channel, kernel_size=3,
stride=stride, padding=1, bias=False,),
nn.BatchNorm2d(out_channel),
nn.ReLU(),
nn.Conv2d(out_channel, out_channel, kernel_size=3,
stride=1, padding=1, bias=False,),
nn.BatchNorm2d(out_channel),
)
# option B: downsample -> shortcut을 cnn으로 구현
if self.is_downsample:
self.downsample = nn.Sequential(
nn.Conv2d(in_channel, out_channel, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(out_channel),
)
else:
self.downsample = nn.Identity()
def forward(self, x):
return F.relu(self.seq(x) + self.downsample(x))
class Resnet18(nn.Sequential):
def __init__(self, class_num=10):
super().__init__(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
ResBlock(64, 64), # 64, 32x32유지
ResBlock(64, 64), # 64, 32x32유지
ResBlock(64, 128, is_downsample=True), # 128, 16x16
ResBlock(128, 128),
ResBlock(128, 256, is_downsample=True), # 256, 8x8
ResBlock(256, 256),
ResBlock(256, 512, is_downsample=True), # 512, 4x4
ResBlock(512, 512),
nn.AvgPool2d(4), # 512, 1x1
nn.Flatten(),
nn.Linear(512, class_num),
)
bottle neck 모델
class ResBottleNeckBlock(nn.Module):
def __init__(self, in_channel, out_channel, is_downsample=False, stride=None):
super().__init__()
if stride is None:
self.stride = 2 if is_downsample else 1
else:
self.stride = stride
self.in_channel = in_channel
self.out_channel = out_channel
mid_channel = out_channel // 4
self.seq = nn.Sequential(
nn.Conv2d(in_channel, mid_channel, kernel_size=1, bias=False),
nn.BatchNorm2d(mid_channel),
nn.ReLU(),
nn.Conv2d(mid_channel, mid_channel, kernel_size=3,
stride=self.stride, padding=1, bias=False),
nn.BatchNorm2d(mid_channel),
nn.ReLU(),
nn.Conv2d(mid_channel, out_channel, kernel_size=1, bias=False),
nn.BatchNorm2d(out_channel),
)
if is_downsample:
self.downsample = self._identitymap
else:
self.downsample = nn.Identity()
def forward(self, x):
return F.relu(self.seq(x) + self.downsample(x))
def _identitymap(self, x):
x = x[:, :, ::self.stride, ::self.stride]
dim = self.out_channel//self.in_channel # in_channel을 out_channel로 만들어야함
for _ in range(dim//2): # 2배 -> 1번 4배 -> 2번
x = torch.concat((x, torch.zeros_like(x)), dim=1)
return x
class ResBottleNecknet18(nn.Sequential):
def __init__(self, class_num=10):
super().__init__(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
ResBottleNeckBlock(64, 256, is_downsample=True, stride=1),
ResBottleNeckBlock(256, 256),
ResBottleNeckBlock(256, 512, is_downsample=True),
ResBottleNeckBlock(512, 512),
ResBottleNeckBlock(512, 1024, is_downsample=True),
ResBottleNeckBlock(1024, 1024),
ResBottleNeckBlock(1024, 2048, is_downsample=True),
ResBottleNeckBlock(2048, 2048),
nn.AvgPool2d(4),
nn.Flatten(),
nn.Linear(2048, class_num),
)