개요
ResNet은 2015년 ILSVRC(Image Large Scale Visual Recognition challenge) 대회에서 우승을 한 CNN 모델로 원 논문명은 “Deep Residual Learning for Image Recognition”이다.
2024년 7월 27일 현재 논문 인용수가 약 23만 회에 달할 정도로 딥러닝 이미지 분야에서 많이 사용되고 있는 모델이다.
ResNet의 레이어 수는 152개의 층에 달하는데, 직전 2014년 대회에서 우승한 GoogleNet의 레이어 수가 22개 층으로 구성된 것을 감안하면 약 7배나 깊어진 수치다.
단순히 레이어가 깊어졌기에 성능이 향상됐다고 볼수도 있겠지만, CIFAR-10 데이터셋을 이용해 layer-depth에 따른 에러율을 확인한 결과 56-layer에서 에러율이 더 높은 것을 확인할 수 있다.
이는 레이어가 깊어질수록 역전파(back propagation) 과정에서 gradient vanishing / exploding이 일어날 확률이 높아지고, training error가 증가할 확률이 높아지기 때문인데 그렇다면 ResNet은 이런 문제들을 어떻게 극복한 것인지 지금부터 알아보도록 하겠다!
Residual block 제안
왼쪽은 Plane layer, 즉 기존 방식을 설명하는 그림이고 오른쪽은 Residual block을 설명하는 그림이다. 식을 보고 유추해보면 단순히 기존 함수에 x를 더한 새로운 함수를 정의한 것이라고 볼수도 있겠다.
네트워크의 관점에서는 단순히 입력값을 출력값에 더해줄 수 있도록 지름길, 즉 shortcut을 추가했을 뿐이다.
이해를 돕기 위해 오른쪽 그림을 .y=f(x)+x라는 식으로 정의해보면 x는 기존에 학습한 정보, f(x)는 추가적으로 학습하는 정보를 의미한다.
여기서 ResNet은 추가적으로 학습해야 하는 정보인 F(x)를 최소화하는 것을 목표로 하게 되는데, F(x)=H(x)−x 를 residual이라고 한다.
ResNet의 구조
위 그림을 보면 알 수 있듯 34층의 ResNet은 처음을 제외하고는 3x3의 convolution filter을 사용했으며, 2x2 pool을 사용할 때마다 channel을 2배씩 늘렸다.
위 사진은 다양한 층의 ResNet들이 어떻게 구성되어 있는지를 보여준다.
18-layer의 경우 입력층의 Conv layer, 출력층의 FC layer을 포함하여 총 18개의 레이어를 가지고 있다.
이 글은 https://velog.io/@lighthouse97/ResNet의-이해 해당 링크를 참고하였으며 더 세부적인 내용을 해당 링크를 참고하기를 바란다.
구현 코드
데이터셋 불러오기
transform = transforms.Compose([ transforms.ToTensor() ]) trainset = torchvision.datasets.CIFAR10(root='./cifar10', train=True, download=True, transform=transform) print(trainset.data) train_data_mean = trainset.data.mean( axis=(0,1,2) ) train_data_std = trainset.data.std( axis=(0,1,2) ) print(train_data_mean) print(train_data_std) train_data_mean = train_data_mean / 255 train_data_std = train_data_std / 255 print(train_data_mean) print(train_data_std)
transform_train = transforms.Compose([ transforms.RandomCrop(32, padding=2), transforms.ToTensor(), transforms.Normalize(train_data_mean, train_data_std) ]) transform_test = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(train_data_mean, train_data_std) ]) trainset = torchvision.datasets.CIFAR10(root='./cifar10', train=True, download=True, transform=transform_train) trainloader = torch.utils.data.DataLoader(trainset, batch_size=256, shuffle=True, num_workers=0) testset = torchvision.datasets.CIFAR10(root='./cifar10', train=False, download=True, transform=transform_test) testloader = torch.utils.data.DataLoader(testset, batch_size=256, shuffle=False, num_workers=0) classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
정규화를 해준 후, transforms.RandomCrop 함수를 이용해 이미지의 일부만 잘라와 학습을 진행하도록 하였다.
여담으로 padding 값은 이미지의 테두리 부분에 여백의 공간을 주는 것으로 없어도 무방하다. (위 코드에서는 학습의 용이성을 위해 넣었다.)
모델 선언
import torchvision.models.resnet as resnet
# 밑에서 ResNet 클래스를 재정의하는 과정에서 에러를 방지하기 위함 conv1x1=resnet.conv1x1 Bottleneck = resnet.Bottleneck BasicBlock= resnet.BasicBlock
class ResNet(nn.Module): def __init__(self, block, layers, num_classes=1000, zero_init_residual=False): super(ResNet, self).__init__() self.inplanes = 16 self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(16) self.relu = nn.ReLU(inplace=True) #self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) self.layer1 = self._make_layer(block, 16, layers[0], stride=1) self.layer2 = self._make_layer(block, 32, layers[1], stride=1) self.layer3 = self._make_layer(block, 64, layers[2], stride=2) self.layer4 = self._make_layer(block, 128, layers[3], stride=2) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(128 * block.expansion, num_classes) for m in self.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0) # Zero-initialize the last BN in each residual branch, # so that the residual branch starts with zeros, and each residual block behaves like an identity. # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677 if zero_init_residual: for m in self.modules(): if isinstance(m, Bottleneck): nn.init.constant_(m.bn3.weight, 0) elif isinstance(m, BasicBlock): nn.init.constant_(m.bn2.weight, 0) def _make_layer(self, block, planes, blocks, stride=1): downsample = None if stride != 1 or self.inplanes != planes * block.expansion: downsample = nn.Sequential( conv1x1(self.inplanes, planes * block.expansion, stride), nn.BatchNorm2d(planes * block.expansion), ) layers = [] layers.append(block(self.inplanes, planes, stride, downsample)) self.inplanes = planes * block.expansion for _ in range(1, blocks): layers.append(block(self.inplanes, planes)) return nn.Sequential(*layers) def forward(self, x): x = self.conv1(x) #x.shape =[1, 16, 32,32] x = self.bn1(x) x = self.relu(x) #x = self.maxpool(x) x = self.layer1(x) #x.shape =[1, 128, 32,32] x = self.layer2(x) #x.shape =[1, 256, 32,32] x = self.layer3(x) #x.shape =[1, 512, 16,16] x = self.layer4(x) #x.shape =[1, 1024, 8,8] x = self.avgpool(x) x = x.view(x.size(0), -1) x = self.fc(x) return x
resnet50 = ResNet(resnet.Bottleneck, [3, 4, 6, 3], 10, True).to(device)
PyTorch에서 ResNet은 torchvision.models.resnet을 import해 사용할 수 있다.
위 코드에서는 우리가 학습할 이미지가 3x32x32로 비교적 작기 때문에 각 layer들의 채널 수를 축소시켰고, 첫번째 레이어에서의 maxpool을 삭제했다.
학습 결과
epoch을 40번 정도 진행했더니 정확도가 89% 정도 나왔다. 40번만 진행한 이유는 대충 10번째 정도부터 정확도에 큰 차이가 없었기 때문이다.