기존 네트워크의 한계 및 Residual Network의 등장
기존에 우리가 살펴본 머신 러닝 모델에는 여러 레이어를 배열해서 데이터가 그 레이어들을 통과하도록 설계하였다.
하지만 이런 모델은 레이어의 깊이가 깊어질수록 몇 가지 문제가 발생한다.

위의 예를 살펴보자. (출처: https://arxiv.org/abs/1512.03385)
20개 및 56개의 레이어가 있는 vgg 스타일의 네트워크에 대한 트레이닝 커브를 보자.
56개의 레이어가 있는 모델은 capacity가 더 큼에도 불구하고 20 레이어보다 나쁜 성능을 보여주고 있다.
이것은 네트워크의 깊이가 깊어지면 발생하기 쉬운 현상으로 Vanishing Gradient Problem(기울기 소실 문제) 등의 문제들이 원인이다.
Vanishing Gradient Problem(기울기 소실 문제)는 역전파(backpropagation) 과정에서 입력층과 가까운 레이어의 가중치에서 나타날 수 있다.
가중치의 그래디언트에 0에 가까운 값을 곱하는 과정과 부동 소수점 오류가 누적되면 그래디언트가 부정확해지고 작아지면서 결국 모델의 학습이 원활하게 이루어지지 않게 된다.
오늘은 이러한 문제를 해결해준 좀더 최신 모델(2015)을 살펴보자.
Residual Block

위의 왼쪽 그림에서 입력 x는 일련의 레이어를 통과하여 출력 f(x)가 되어 전달된다.
오른쪽의 레이어도 마찬가지로 x을 입력받아 출력 f(x)가 되는데 Residual Block의 개념을 적용하면 출력 f(x)가 입력 x와 결합하여 최종 출력을 생성하여 다음으로 전달된다.
즉, 두 경로를 통해 입력을 전달하는 것이다.
분명 결합되는 x는 레이어를 통과하지 않고 "skip"되어서 그래디언트가 한 레이어의 출력에서 이전 레이어의 입력으로 직접 흐를 수 있게 되어 네트워크가 깊어질 때 발생할 수 있는 기울기 소실 문제를 방지해준다.
Residual Block의 사용 방법
PyTorch의 텐서는 객체로 저장하고 조작할 수 있기 때문에 residual block을 만드는 것이 아주 쉽다.
입력과 그 입력이 레이어를 통과하여 나온 출력을 별도로 저장한 다음, 마지막에 합하면 된다.
직접 코드를 통해 알아보자.
import torch
import torch.nn as nn
class ResBlock(torch.nn.Module):
def __init__(self):
super(ResBlock, self).__init__()
# torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0 ... )
self.c1 = torch.nn.Conv2d(3,3,3,padding=1) # 3x3 filter, 3 channels -> 3 channels
self.c2 = torch.nn.Conv2d(3,3,3,padding=1) # 3x3 filter, 3 channels -> 3 channels
def forward(self, x):
fx = self.c1(x)
fx = torch.relu(fx) # we could also add a batchnorm layer, etc.
fx = self.c2(fx)
x = x + fx
return torch.relu(x)
x을 입력받아 레이어를 통과한 출력 fx을 만들고 단순히 더하면 residual block을 간단히 만들 수 있다.
입력과 출력의 모양을 확인해보자.
r = ResBlock()
x = torch.randn((1,3,4,4))
print(x.shape,r(x).shape)
torch.Size([1, 3, 4, 4]) torch.Size([1, 3, 4, 4])
만약 입력과 출력의 채널 수가 달라진다면 다음과 같은 방법을 사용할 수 있다.
class ResBlock(torch.nn.Module):
def __init__(self):
super(ResBlock, self).__init__()
self.c1 = torch.nn.Conv2d(3,6,3,padding=1) # 3x3 filter, 3 channels -> 6 channels
self.c2 = torch.nn.Conv2d(6,6,3,padding=1) # 3x3 filter, 6 channels -> 6 channels
self.cc = torch.nn.Conv2d(3,6,1) # 1x1 filter 3 channels -> 6 channels
def forward(self, x):
fx = self.c1(x)
fx = torch.relu(fx) # we could also add a batchnorm layer, etc.
fx = self.c2(fx)
x = self.cc(x) # convolve to more channels
x = x + fx # so we can add together
return torch.relu(x)
r = ResBlock()
x = torch.randn((1,3,4,4))
print(x.shape,r(x).shape)
torch.Size([1, 3, 4, 4]) torch.Size([1, 6, 4, 4]) (채널이 3에서 6으로 늘어났다.)
위의 방법 처럼 1x1 커널 사이즈를 가지는 cc레이어를 통해 채널 수만 증가시켜서 출력과 입력을 합칠 수 있다.
만약 크기도 변한다면 어떨까?
입력을 그냥 더하지 말고 풀링(pooling) 레이어를 통해 출력과 합칠 수 있다.
class ResBlock(torch.nn.Module):
def __init__(self):
super(ResBlock, self).__init__()
self.c1 = torch.nn.Conv2d(3,3,3,padding=1) # 3x3 filter, 3 channels -> 3 channels
self.c2 = torch.nn.Conv2d(3,6,3,padding=1) # 3x3 filter, 3 channels -> 6 channels
self.cc = torch.nn.Conv2d(3,6,1,stride=2) # 1x1 filter 3 channels -> 6 channels, padding to match pooling
self.pool = torch.nn.MaxPool2d(2)
def forward(self, x):
fx = self.c1(x)
fx = self.pool(torch.relu(fx))
fx = self.c2(fx)
x = self.cc(x) # includes the padding->shrinks the image
x = x + fx
return torch.relu(x)
r = ResBlock()
x = torch.randn((1,3,4,4))
print(x.shape,r(x).shape)
torch.Size([1, 3, 4, 4]) torch.Size([1, 6, 2, 2]) (사이즈가 4x4에서 2x2으로 줄었다.)
이런 방법으로 입출력의 사이즈가 달라도 간단한 방법으로 residual network을 만들 수 있다.
성능을 비교하고자 여러 모델을 만들어 테스트를 진행했는데, 네트워크를 웬만큼 복잡하게 만들지 않는 이상 유의미한 성능 차이가 나지 않았다.
ResNet에 대한 좀더 자세한 설명이나 다른 모델과의 비교가 궁금하다면 arXiv:1512.03385을 읽어보자.(표나 그래프만 봐도 된다.)
'머신 러닝 > Machine Learning' 카테고리의 다른 글
| [Machine Learning] PyTorch로 LSTM(Long Short-term Memory) 구현하기 (0) | 2023.03.14 |
|---|---|
| [Machine Learning] PyTorch로 RNN(순환 신경망) 구현하기 (0) | 2023.03.09 |
| [Machine Learning] PyTorch로 Regularization(정규화) 직접 해보기 (1) | 2023.03.06 |
| [Machine Learning] PyTorch로 CNN 구현하기 (합성곱 신경망, Convolutional Neural Network) (0) | 2023.03.04 |
| [Machine Learning] PyTorch로 다중 로지스틱 회귀 구현하기 (Multinomial Logistic Regression) (0) | 2023.03.03 |