본문 바로가기
CV/Coding

ResNet

by BaekDaBang 2024. 4. 22.

1. Information

잔차 신경망(ResNet)은 2015년에 마이크로소프트 리서치 팀에서 제안한 신경망 구조

ResNet에는 잔차 모듈(residual module)과 스킵 연결(skip connection)이라는 새로운 구조가 사용되었으며, 은닉층에도 강한 배치 정규화가 적용되었음
ResNet은 2015년에 ILSVRC에서 top-5 오차율 3.57%을 기록하며 가장 좋은 성능을 기록함
배치 정규화가 강하게 적용된 덕분에 파라미터가 있는 층이 50층, 101층, 152층이나 되는 깊은 신경망(ResNet50, ResNet101, ResNet152)의 복잡도를 훨씬 층수가 적은 VGGNet(19층)보다 낮출 수 있음
신경망이 지나치게 깊어지면 과적합이 발생하기 쉬움
ResNet은 배치정규화를 통해 과적합 문제를 해결하였음

 

LeNet → AlexNet  VGGNet → GoogeLeNet 신경망 구조의 발전을 통해 신경망의 층수가 많을수록 이미지에서 더 좋은 특징을 추출할 수 있다는 것을 발견함
일반적으로 신경망의 층수가 많을수록 신경망 모델의 표현력이 좋아지며, 이는 단순한 모서리(앞쪽층)에서 부터 복잡한 패턴(뒤쪽층)에 이르기까지 다양한 추상화 수준의 특징을 학습할 수 있기 때문임
VGG19(19층)와 GoogLeNet(22층) 구조보다 신경망 층수를 늘리기 위해서는 과적합 문제와 기울기 소실 문제를 해결해야 함
과적합 문제는 드롭아웃, L2규제화, 배치정규화를 통해 해결 가능
기울기 소실 문제는 ResNet 구조가 제안한 스킵 연결을 통해 해결 가능

 

ResNet 연구진은 기울기 소실문제 해결을 위해 스킵 연결을 제안함
스킵 연결이란 뒤쪽 층의 기울기를 앞쪽 층에 직접 전달하는 별도의 경로를 추가하는 것으로 앞쪽 층의 정보가 뒤쪽 층에 전달하는 것을 의미함
스킵 연결은 항등함수를 학습할 수 있어 층이 쌓여도 앞쪽 층보다 성능이 하락하지 않는 역할을 수행함

 

합성곱층에 스킵 연결을 추가한 구조를 잔차 블록(residual block)이라고 함

 

인셉션과 마찬가지로 ResNet 구조도 여러 개의 잔차 블록이 늘어선 형태로 구성됨

  • 특징추출기 : ResNet의 특징추출기 부분은 합성곱층과 풀링층으로 시작해서 그 뒤로 잔차블록이 1개 이상 이어지는 구조로, ResNet은 원하는만큼 잔차 블록을 추가해서 신경망의 층수를 늘릴 수 있음
  • 분류기 : 분류기 부분은 다른 신경망과 마찬가지로 전결합층과 소프트맥스 함수로 구성됨

 

잔차 블록

잔차 블록(residual block)은 다음 2개의 경로로 나뉨

  • 지름길 경로 : 입력을 주경로의 ReLU 입력 전으로 전달함
  • 주경로 : 일련의 합성곱 연산 및 활성화 함수로, 주경로는 ReLU 활성화 함수를 사용하는 3개의 합성곱층으로 구성됨. 과적합 방지와 학습 속도 향상을 위해 각 합성곱층마다 배치 정규화를 적용함. 결과적으로 주 경로의 구조는 [CONV=>BN=>ReLU]x3 와 같음

지름길 경로의 값은 잔차 블록 마지막 합성곱층의 활성화 함수 바로 앞에서 주경로의 값과 합하여 이 값을 ReLU 함수에 통과시킴

 

잔차 블록에는 풀링층이 없음
인셉션 구조와 비슷하게 1x1 합성곱층을 사용하여 다운샘플링을 적용

  • 잔차블록 처음에 1x1 합성곱층을 배치하고, 출력에는 3x3 합성곱층과 1x1 합성곱층을 하나씩 배치해서 두번에 걸쳐 차원을 축소함. 이구조는 여러 층에 걸쳐 입출력의 차원을 제어할 수 있다는 것이 장점
  • 차원축소가 적용된 블록을 병목잔차블록(bottleneck residual block)이라고 함

주 경로와 지름길 경로로 전달되는 정보의 차원이 동일해야 연산(행렬덧셈)이 가능

  • 주경로에 차원축소층을 적용하여 정보의 차원을 축소했다면, 지름길 경로에도 병목층(1x1합성곱층+배치정규화)을 적용하여 정보의 차원을 축소해야 함
  • 이러한 구조를 축소 지름길 경로(reduce shortcut)라고 함

 

잔차 블록에는 지름길 경로와 주경로가 있음

주경로는 3개의 합성곱층에 각각 배치정규화가 적용됨

  • 1x1 합성곱층
  • 3x3 합성곱층
  • 1x1 합성곱층

지름길 경로를 구현하는 1x1합성곱층 방법은 두가지

  • 일반 지름길 경로 : 주경로와입력을더하는일반적인지름길경로
  • 축소 지름길 경로 : 주경로와입력을더하는지점이전에합성곱층이하나배치된 지름길 경로

 

하이퍼파라미터 설정

미니배치 경사하강법
모멘텀 : 0.9

학습률 초기값 : 0.1 (검증오차 감소 없을시 학습률 1/10 감소)

가중치 감소율 : 0.0001
L2규제화 함께 사용 (단, 아래 구현코드에서는 생략)

 

2. Code

class BottleNeck(nn.Module):

    def __init__(self, in_planes, out_planes, stride=1):
        super(BottleNeck, self).__init__()
        
        self.main1 = nn.Sequential(
            nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride, bias=False),
            nn.BatchNorm2d(out_planes),
            nn.ReLU()
        ) 
        self.main2 = nn.Sequential(
            nn.Conv2d(out_planes, out_planes, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(out_planes),
            nn.ReLU()
        )
        self.main3 = nn.Sequential(
            nn.Conv2d(out_planes, out_planes*4, kernel_size=1, stride=1, bias=False),
            nn.BatchNorm2d(out_planes*4)
        )
        
        self.shortcut = nn.Sequential()
        
        if (stride!=1) or (in_planes!=out_planes*4):
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_planes, out_planes*4, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_planes*4)
            )
        
    def forward(self, x):
        
        out = self.main1(x)
        out = self.main2(out)
        out = self.main3(out)
        
        out += self.shortcut(x)
        out = F.relu(out)
        
        return out
class ResNet(nn.Module):
    # CIFAR-10의 class: 10 -> num_classes=10으로 
    def __init__(self, BottleNeck, num_blocks=[3, 4, 6, 3], num_classes=10):
        super(ResNet, self).__init__()
        self.in_planes = 64
        
        self.stage1 = nn.Sequential(
            nn.Conv2d(3, self.in_planes, kernel_size=7, stride=2, padding=3),
            nn.BatchNorm2d(self.in_planes),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        )
        self.stage2 = self.make_layer(BottleNeck, 64, num_blocks[0], stride=1)
        self.stage3 = self.make_layer(BottleNeck, 128, num_blocks[1], stride=2)
        self.stage4 = self.make_layer(BottleNeck, 256, num_blocks[2], stride=2)
        self.stage5 = self.make_layer(BottleNeck, 512, num_blocks[3], stride=2)
        self.avgpool = nn.AdaptiveAvgPool2d((1,1))
        
        self.linear = nn.Linear(512 * 4, num_classes)
        
    def make_layer(self, block, out_planes, num_blocks, stride):
        strides = [stride] + [1] * (num_blocks-1)
        layers = []
        for i in range(num_blocks):
            layers.append(block(self.in_planes, out_planes, strides[i]))
            self.in_planes = 4 * out_planes
            
        return nn.Sequential(*layers)
    
    def forward(self, x):
        
        out = self.stage1(x)
        out = self.stage2(out)
        out = self.stage3(out)
        out = self.stage4(out)
        out = self.stage5(out)
        out = self.avgpool(out)
        out = torch.flatten(out,1)
        out = self.linear(out)
        
        return out

'CV > Coding' 카테고리의 다른 글

GoogLeNet  (0) 2024.04.22
VGG  (0) 2024.04.22
AlexNet  (0) 2024.04.22
LeNet  (0) 2024.04.17