이미지 로딩 중...
AI Generated
2025. 11. 23. · 0 Views
ResNet과 Skip Connection 완벽 가이드
딥러닝 모델이 깊어질수록 성능이 떨어지는 문제를 해결한 혁신적인 기법, ResNet과 Skip Connection을 초급자도 이해할 수 있도록 쉽게 설명합니다. 실제 구현 코드와 함께 배워보세요.
목차
- Skip Connection의 개념 - 정보 손실 없이 깊은 네트워크 구축하기
- ResNet 기본 블록 - 잔차 학습의 핵심 구조
- Bottleneck 블록 - 효율적인 깊은 네트워크 구축
- ResNet 전체 구조 - 완전한 네트워크 구축하기
- Identity Mapping - Skip Connection의 수학적 원리
- Pre-activation ResNet - 더 나은 기울기 흐름
- ResNeXt - 다중 경로로 표현력 높이기
- Gradient Highway - 극도로 깊은 네트워크 학습하기
1. Skip Connection의 개념 - 정보 손실 없이 깊은 네트워크 구축하기
시작하며
여러분이 딥러닝 모델을 만들 때 이런 상황을 겪어본 적 있나요? "레이어를 더 쌓으면 성능이 좋아질 거야!"라고 생각했는데, 오히려 레이어를 많이 쌓을수록 성능이 떨어지는 신기한 현상을 경험한 적 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 신경망이 깊어질수록 기울기가 사라지거나(Vanishing Gradient), 정보가 왜곡되면서 학습이 제대로 되지 않는 거예요.
마치 전화기로 메시지를 전달하는 게임에서, 사람이 많아질수록 원래 메시지가 왜곡되는 것과 비슷합니다. 바로 이럴 때 필요한 것이 Skip Connection입니다.
이 기법은 정보를 "지름길"로 전달해서, 깊은 네트워크에서도 원본 정보를 잃지 않고 학습할 수 있게 해줍니다.
개요
간단히 말해서, Skip Connection은 신경망의 중간 레이어를 건너뛰어 입력을 직접 출력에 더해주는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 딥러닝 모델이 깊어질수록 학습이 어려워지는데, Skip Connection을 사용하면 100층 이상의 매우 깊은 네트워크도 안정적으로 학습할 수 있습니다.
예를 들어, 이미지 분류나 객체 인식 같은 복잡한 작업에서 높은 정확도를 얻을 때 매우 유용합니다. 전통적인 방법과의 비교를 해보면, 기존에는 레이어를 거칠 때마다 정보가 변형되고 손실되었다면, 이제는 원본 정보를 그대로 보존하면서도 새로운 특징을 학습할 수 있습니다.
Skip Connection의 핵심 특징은 첫째, 기울기 소실 문제를 해결하고, 둘째, 항등 함수(identity function)를 쉽게 학습할 수 있게 하며, 셋째, 모델의 깊이를 크게 늘릴 수 있다는 점입니다. 이러한 특징들이 현대 딥러닝에서 매우 깊은 네트워크를 성공적으로 학습시킬 수 있는 핵심 열쇠가 됩니다.
코드 예제
import torch
import torch.nn as nn
class SkipConnectionBlock(nn.Module):
def __init__(self, in_channels):
super().__init__()
# 주석: 메인 경로 - 실제 학습이 일어나는 부분
self.conv1 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(in_channels, in_channels, kernel_size=3, padding=1)
def forward(self, x):
# 주석: 원본 입력을 저장 (Skip Connection의 핵심!)
identity = x
# 주석: 메인 경로를 거쳐 변환된 출력
out = self.conv1(x)
out = self.relu(out)
out = self.conv2(out)
# 주석: 원본과 변환된 출력을 더함 - 이것이 Skip Connection!
out = out + identity
out = self.relu(out)
return out
설명
이것이 하는 일: Skip Connection은 신경망의 레이어를 거치면서 변형되는 정보와 원본 정보를 함께 사용하여, 더 효과적으로 학습할 수 있게 합니다. 첫 번째로, forward 메서드에서 identity = x로 원본 입력을 저장합니다.
이것이 매우 중요한데, 나중에 변환된 출력과 더해지는 "지름길" 역할을 하기 때문입니다. 마치 건물에서 계단과 엘리베이터를 동시에 제공하는 것과 비슷하다고 생각하면 됩니다.
그 다음으로, 메인 경로에서 두 개의 Convolution 레이어와 ReLU 활성화 함수를 거치면서 입력이 변환됩니다. 이 과정에서 네트워크는 "무엇을 추가로 학습해야 하는지"를 배웁니다.
내부에서는 필터들이 이미지의 특징을 추출하고 변형하는 작업이 일어납니다. 세 번째 단계로, out = out + identity에서 변환된 출력과 원본 입력을 더합니다.
이것이 Skip Connection의 핵심입니다! 최종적으로 ReLU를 한 번 더 거쳐서 음수 값을 제거하고 출력을 만들어냅니다.
여러분이 이 코드를 사용하면 깊은 네트워크를 만들어도 학습이 잘 되는 효과를 얻을 수 있습니다. 실무에서의 이점으로는 첫째, 100층 이상의 깊은 모델도 안정적으로 학습 가능하고, 둘째, 학습 속도가 빨라지며, 셋째, 최종 정확도가 향상됩니다.
실전 팁
💡 Skip Connection을 추가할 때는 입력과 출력의 차원(shape)이 같아야 합니다. 만약 차원이 다르다면 1x1 Convolution으로 차원을 맞춰주세요.
💡 초기에는 Skip Connection의 가중치를 0으로 초기화하면 학습 초반에 안정적입니다. 네트워크가 항등 함수부터 시작해서 점진적으로 학습하게 됩니다.
💡 배치 정규화(Batch Normalization)를 사용한다면, Convolution 이후 ReLU 이전에 넣어주세요. 이렇게 하면 학습이 더 안정적이고 빨라집니다.
💡 디버깅할 때는 Skip Connection을 끄고 켰을 때의 성능 차이를 비교해보세요. 일반적으로 5-10% 이상의 성능 향상이 있어야 정상입니다.
💡 Skip Connection 블록을 여러 개 쌓을 때는 3-4개 정도를 하나의 Stage로 묶어서 관리하면 코드가 깔끔해집니다.
2. ResNet 기본 블록 - 잔차 학습의 핵심 구조
시작하며
여러분이 이미지 분류 모델을 만들 때 이런 고민을 해본 적 있나요? "네트워크를 20층으로 만들었는데, 30층으로 늘리면 성능이 더 좋아질까?" 하지만 막상 레이어를 추가하면 오히려 성능이 나빠지는 좌절감을 느끼셨을 겁니다.
이런 문제는 2015년 이전까지 딥러닝의 큰 난제였습니다. 깊은 네트워크가 얕은 네트워크보다 못한 성능을 보이는 "퇴화 문제(Degradation Problem)"가 발생했거든요.
단순히 기울기 소실 문제만이 아니라, 최적화 자체가 어려워지는 현상이었습니다. 바로 이럴 때 필요한 것이 ResNet의 기본 블록입니다.
이 블록은 "잔차(residual)"를 학습하는 방식으로 문제를 해결해서, 152층의 초깊은 네트워크도 성공적으로 학습할 수 있게 만들었습니다.
개요
간단히 말해서, ResNet 기본 블록은 "입력을 그대로 출력하는 것"을 기본값으로 두고, 추가로 배워야 할 "차이"만 학습하는 구조입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 네트워크가 깊어질 때 "아무것도 하지 않는 것(항등 함수)"조차 학습하기 어려운데, ResNet 블록은 이를 자동으로 보장하면서도 필요한 변환만 추가로 학습합니다.
예를 들어, ImageNet 대회에서 우승한 152층짜리 ResNet-152 모델 같은 경우에 이 블록이 핵심 역할을 했습니다. 전통적인 방법과의 비교를 해보면, 기존 네트워크는 H(x) = 원하는 출력을 직접 학습했다면, ResNet은 F(x) = H(x) - x, 즉 "잔차"만 학습하고 나중에 x를 더해줍니다.
수학적으로는 작은 차이지만, 실제 학습에서는 엄청난 차이를 만듭니다. ResNet 기본 블록의 핵심 특징은 첫째, 두 개의 3x3 Convolution 레이어로 구성되고, 둘째, 각 Convolution 후에 Batch Normalization이 있으며, 셋째, Skip Connection으로 입력이 직접 출력에 더해진다는 점입니다.
이러한 특징들이 깊은 네트워크의 학습을 가능하게 만드는 핵심 설계입니다.
코드 예제
import torch
import torch.nn as nn
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
# 주석: 첫 번째 Convolution - 특징 추출
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(out_channels)
# 주석: 두 번째 Convolution - 특징 정제
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
# 주석: 차원이 다를 때 Skip Connection 경로 조정
self.downsample = None
if stride != 1 or in_channels != out_channels:
self.downsample = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
identity = x
# 주석: 메인 경로 - 잔차 F(x) 학습
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
# 주석: 필요시 identity 차원 조정
if self.downsample is not None:
identity = self.downsample(x)
# 주석: 잔차 더하기 - H(x) = F(x) + x
out += identity
out = self.relu(out)
return out
설명
이것이 하는 일: ResNet 기본 블록은 두 개의 Convolution 레이어로 "잔차(residual)"를 학습하고, 이를 원본 입력에 더해서 최종 출력을 만듭니다. 첫 번째로, __init__ 메서드에서 두 개의 Convolution과 Batch Normalization 레이어를 정의합니다.
여기서 중요한 점은 bias=False로 설정한 것인데, Batch Normalization이 bias 역할을 대신하기 때문에 중복을 피하는 겁니다. 또한 downsample 모듈은 입력과 출력의 차원이 다를 때 사용되는 특별한 경로입니다.
그 다음으로, forward 메서드에서 실제 계산이 일어납니다. 메인 경로에서는 Conv → BN → ReLU → Conv → BN 순서로 처리되며, 이 과정에서 네트워크는 F(x), 즉 "잔차"를 학습합니다.
내부적으로 필터들이 입력 이미지에서 유용한 특징을 추출하고 변환합니다. 세 번째 단계로, stride가 1이 아니거나 채널 수가 변경되는 경우, downsample 모듈이 identity를 적절한 크기로 조정합니다.
이것은 마치 서로 다른 크기의 퍼즐 조각을 맞추기 위해 한 쪽을 잘라내는 것과 비슷합니다. 마지막으로, out += identity에서 학습된 잔차와 원본 입력(또는 조정된 입력)을 더한 후, 최종 ReLU를 거쳐 출력을 만들어냅니다.
여러분이 이 블록을 여러 개 쌓으면 ResNet-18, ResNet-34 같은 강력한 모델을 만들 수 있습니다. 실무에서의 이점으로는 첫째, ImageNet에서 3.57% 에러율로 인간 수준을 뛰어넘는 성능, 둘째, Transfer Learning의 강력한 백본, 셋째, 다양한 작업에 쉽게 적용 가능하다는 점이 있습니다.
실전 팁
💡 Batch Normalization을 사용할 때는 Convolution의 bias를 False로 설정하세요. BN의 beta 파라미터가 bias 역할을 하므로 메모리를 절약할 수 있습니다.
💡 학습 초기에 불안정하다면 "Warm-up" 전략을 사용하세요. 처음 몇 epoch은 낮은 learning rate로 시작해서 점진적으로 올리면 수렴이 안정적입니다.
💡 downsample 경로는 학습 가능한 파라미터가 적으므로, 여기에는 Dropout을 적용하지 마세요. 메인 경로에만 정규화를 적용하는 것이 효과적입니다.
💡 실전에서는 ReLU 대신 Mish나 GELU 같은 최신 활성화 함수를 시도해보세요. 일부 케이스에서 1-2% 성능 향상을 얻을 수 있습니다.
3. Bottleneck 블록 - 효율적인 깊은 네트워크 구축
시작하며
여러분이 50층 이상의 매우 깊은 네트워크를 만들 때 이런 문제에 직면한 적 있나요? "ResNet 기본 블록으로 50층을 쌓으니 학습 시간이 너무 오래 걸리고 메모리도 부족해!" 이런 문제는 실무에서 큰 모델을 다룰 때 항상 따라다닙니다.
3x3 Convolution은 계산량이 많아서, 채널이 많고 레이어가 깊어지면 GPU 메모리가 금방 가득 차고 학습 속도도 크게 느려집니다. 예를 들어, 256 채널로 3x3 Convolution을 하면 589,824개의 파라미터가 필요합니다.
바로 이럴 때 필요한 것이 Bottleneck 블록입니다. 이 블록은 "차원 축소 → 처리 → 차원 복원" 전략으로 계산량을 크게 줄이면서도 성능은 유지하거나 오히려 향상시킵니다.
개요
간단히 말해서, Bottleneck 블록은 1x1 Convolution으로 채널을 줄였다가, 3x3 Convolution으로 처리하고, 다시 1x1 Convolution으로 채널을 늘리는 3단계 구조입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, ResNet-50 이상의 깊은 모델에서는 계산 효율이 매우 중요한데, Bottleneck 블록을 사용하면 기본 블록 대비 약 70% 적은 연산으로 비슷하거나 더 좋은 성능을 낼 수 있습니다.
예를 들어, ResNet-50, ResNet-101, ResNet-152 같은 산업 표준 모델들이 모두 Bottleneck 블록을 사용합니다. 전통적인 방법과의 비교를 해보면, 기본 블록은 256 채널에서 3x3 Convolution을 두 번 하지만, Bottleneck은 256 → 64 → 64 → 256으로 처리해서 파라미터 수를 약 4분의 1로 줄입니다.
Bottleneck 블록의 핵심 특징은 첫째, 1x1 Conv로 채널을 1/4로 줄이고(차원 축소), 둘째, 3x3 Conv로 공간적 특징을 추출하며, 셋째, 다시 1x1 Conv로 원래 채널로 복원한다는 점입니다. 이러한 특징들이 메모리와 연산량을 크게 절약하면서도 표현력은 유지하게 해줍니다.
코드 예제
import torch
import torch.nn as nn
class Bottleneck(nn.Module):
expansion = 4 # 주석: 출력 채널이 입력의 4배로 확장됨
def __init__(self, in_channels, mid_channels, stride=1):
super().__init__()
# 주석: 1x1 Conv - 차원 축소 (Bottleneck 시작)
self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(mid_channels)
# 주석: 3x3 Conv - 공간적 특징 추출 (좁은 채널에서 효율적으로)
self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(mid_channels)
# 주석: 1x1 Conv - 차원 복원 (원래 채널의 4배로 확장)
self.conv3 = nn.Conv2d(mid_channels, mid_channels * self.expansion,
kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(mid_channels * self.expansion)
self.relu = nn.ReLU(inplace=True)
# 주석: Skip Connection 경로 조정
self.downsample = None
if stride != 1 or in_channels != mid_channels * self.expansion:
self.downsample = nn.Sequential(
nn.Conv2d(in_channels, mid_channels * self.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(mid_channels * self.expansion)
)
def forward(self, x):
identity = x
# 주석: 차원 축소 → 처리 → 복원 경로
out = self.relu(self.bn1(self.conv1(x)))
out = self.relu(self.bn2(self.conv2(out)))
out = self.bn3(self.conv3(out))
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
설명
이것이 하는 일: Bottleneck 블록은 3단계 Convolution 구조로 계산 효율성을 극대화하면서도 표현력을 유지합니다. 첫 번째로, expansion = 4는 클래스 변수로, 출력 채널이 중간 채널의 4배가 된다는 것을 의미합니다.
예를 들어, mid_channels가 64라면 최종 출력은 256 채널이 됩니다. 이것은 ResNet의 표준 설계 패턴입니다.
그 다음으로, forward 메서드에서 세 번의 Convolution이 순차적으로 실행됩니다. 첫 번째 1x1 Conv는 256 채널을 64 채널로 줄여서 "병목(bottleneck)"을 만듭니다.
이렇게 채널을 줄이면 다음 3x3 Conv의 계산량이 크게 감소합니다. 예를 들어, 256→256의 3x3 Conv는 589,824 파라미터인데, 64→64는 단지 36,864 파라미터만 필요합니다.
세 번째 단계로, 두 번째 3x3 Conv는 줄어든 64 채널에서 공간적 패턴을 추출합니다. 채널은 적지만 3x3 필터로 주변 픽셀 정보를 충분히 활용할 수 있습니다.
그 다음 세 번째 1x1 Conv가 64 채널을 다시 256 채널로 확장해서 풍부한 표현력을 복원합니다. 마지막으로, Skip Connection을 통해 identity가 더해지고 최종 ReLU를 거칩니다.
여러분이 이 블록을 사용하면 ResNet-50처럼 깊은 모델을 훨씬 적은 메모리와 빠른 속도로 학습할 수 있습니다. 실무에서의 이점으로는 첫째, 기본 블록 대비 약 30% 빠른 학습 속도, 둘째, GPU 메모리 절약으로 더 큰 배치 사이즈 사용 가능, 셋째, 더 깊은 네트워크 구축이 가능하다는 점이 있습니다.
실전 팁
💡 mid_channels는 보통 최종 출력 채널의 1/4로 설정하세요. 이것이 ResNet 논문에서 검증된 최적 비율입니다.
💡 메모리가 부족하면 expansion을 4 대신 2로 줄여보세요. 성능은 약간 떨어지지만 메모리를 크게 절약할 수 있습니다.
💡 Transfer Learning할 때는 마지막 1x1 Conv의 가중치를 처음 1x1 Conv보다 작게 초기화하세요. 학습 초기에 안정성이 높아집니다.
💡 프로파일링 도구로 각 레이어의 실행 시간을 측정해보세요. Bottleneck 블록은 기본 블록보다 약 2-3배 빨라야 정상입니다.
💡 양자화(Quantization)를 적용할 때는 1x1 Conv가 3x3 Conv보다 정밀도 손실에 민감하므로, Per-Channel Quantization을 사용하세요.
4. ResNet 전체 구조 - 완전한 네트워크 구축하기
시작하며
여러분이 지금까지 배운 ResNet 블록들을 실제로 사용하려면 어떻게 해야 할까요? "블록은 이해했는데, 이걸 어떻게 조합해서 ResNet-50을 만들지?" 하는 고민이 생기실 겁니다.
이런 문제는 실무에서 새로운 아키텍처를 구현할 때 항상 겪는 일입니다. 블록 하나하나는 이해했지만, 전체 네트워크를 어떻게 구성하고, 각 Stage에서 채널을 어떻게 관리하며, 입력에서 출력까지 흐름을 어떻게 설계하는지가 명확하지 않을 수 있습니다.
바로 이럴 때 필요한 것이 ResNet의 전체 구조 이해입니다. ResNet은 명확한 설계 원칙을 따라서, 누구나 쉽게 18층부터 152층까지 다양한 깊이의 모델을 만들 수 있게 구조화되어 있습니다.
개요
간단히 말해서, ResNet 전체 구조는 초기 Convolution → 4개의 Stage(각 Stage는 여러 ResNet 블록) → Global Average Pooling → Fully Connected 레이어로 구성됩니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 표준화된 구조를 이해하면 ResNet-18, 34, 50, 101, 152를 모두 같은 코드로 만들 수 있고, 나만의 커스텀 ResNet 변형도 쉽게 만들 수 있습니다.
예를 들어, 의료 영상 분석이나 위성 이미지 처리처럼 특수한 도메인에서도 ResNet 구조를 쉽게 적용할 수 있습니다. 전통적인 방법과의 비교를 해보면, VGGNet 같은 이전 모델들은 단순히 레이어를 쌓기만 했다면, ResNet은 Stage별로 채널을 체계적으로 증가시키고 해상도를 줄이는 명확한 설계 패턴을 가지고 있습니다.
ResNet 전체 구조의 핵심 특징은 첫째, 각 Stage마다 채널이 2배씩 증가하고(64 → 128 → 256 → 512), 둘째, 공간 해상도는 절반씩 감소하며(stride=2 사용), 셋째, 마지막에 Global Average Pooling으로 공간 정보를 완전히 제거한다는 점입니다. 이러한 특징들이 효율적이면서도 강력한 특징 추출을 가능하게 합니다.
코드 예제
import torch
import torch.nn as nn
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000):
super().__init__()
self.in_channels = 64
# 주석: 초기 Convolution - 7x7로 큰 특징 추출
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 주석: 4개의 Stage - 각각 다른 깊이와 채널
self.layer1 = self._make_layer(block, 64, layers[0], stride=1)
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
# 주석: 분류를 위한 마지막 레이어
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
def _make_layer(self, block, out_channels, num_blocks, stride):
# 주석: Stage를 만드는 헬퍼 함수
layers = []
# 첫 블록은 stride 적용 (해상도 감소)
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels * block.expansion
# 나머지 블록들은 stride=1
for _ in range(1, num_blocks):
layers.append(block(self.in_channels, out_channels, stride=1))
return nn.Sequential(*layers)
def forward(self, x):
# 주석: 224x224x3 → 112x112x64 → 56x56x64
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x)
# 주석: 각 Stage 통과 (56→28→14→7)
x = self.layer1(x) # 56x56x256
x = self.layer2(x) # 28x28x512
x = self.layer3(x) # 14x14x1024
x = self.layer4(x) # 7x7x2048
# 주석: 7x7x2048 → 1x1x2048 → 2048 → num_classes
x = self.avgpool(x)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
# ResNet-50 생성 예시
def resnet50(num_classes=1000):
# 주석: [3, 4, 6, 3]은 각 Stage의 블록 수
return ResNet(Bottleneck, [3, 4, 6, 3], num_classes)
설명
이것이 하는 일: ResNet은 입력 이미지를 점진적으로 고수준 특징으로 변환하고, 최종적으로 분류 결과를 출력하는 완전한 파이프라인입니다. 첫 번째로, __init__ 메서드에서 초기 7x7 Convolution과 MaxPooling이 입력 이미지를 빠르게 다운샘플링합니다.
224x224 이미지가 두 단계를 거쳐 56x56으로 줄어들면서, 계산량을 크게 절약하고 넓은 receptive field를 확보합니다. 이것은 초기에 전체적인 패턴을 빠르게 파악하기 위한 전략입니다.
그 다음으로, _make_layer 메서드가 각 Stage를 생성합니다. 여기서 중요한 점은 각 Stage의 첫 블록만 stride=2로 해상도를 줄이고, 나머지 블록들은 stride=1로 같은 해상도를 유지한다는 것입니다.
예를 들어, layer2는 첫 블록에서 56x56을 28x28로 줄이고, 나머지 3개 블록은 28x28을 유지하면서 특징을 정제합니다. 세 번째 단계로, forward 메서드에서 실제 데이터가 흐릅니다.
4개의 layer를 거치면서 채널은 64 → 256 → 512 → 1024 → 2048로 증가하고, 해상도는 56 → 28 → 14 → 7로 감소합니다. 이것은 "공간 정보는 줄이고, 의미 정보는 늘린다"는 CNN의 핵심 원리를 따릅니다.
마지막으로, AdaptiveAvgPool2d가 7x7 feature map을 1x1로 만들고, flatten으로 1차원 벡터로 변환한 후 FC layer가 최종 분류 결과를 출력합니다. 여러분이 이 구조를 이해하면 ResNet-18(layers=[2,2,2,2]), ResNet-34(layers=[3,4,6,3] with BasicBlock), ResNet-101(layers=[3,4,23,3]) 등을 쉽게 만들 수 있습니다.
실무에서의 이점으로는 첫째, ImageNet pretrained 모델을 쉽게 로드하여 Transfer Learning 가능, 둘째, 모듈화된 구조로 실험과 수정이 간편, 셋째, 산업 표준 아키텍처로 다른 개발자와 협업이 쉽다는 점이 있습니다.
실전 팁
💡 처음부터 학습할 때는 초기 learning rate를 0.1로 시작해서 30, 60, 90 epoch마다 1/10씩 줄이는 것이 표준 방법입니다.
💡 Transfer Learning할 때는 마지막 FC layer만 교체하고 나머지는 freeze한 상태로 시작하세요. 몇 epoch 후에 전체를 fine-tuning하면 효과적입니다.
💡 입력 이미지 크기를 224x224 대신 384x384로 늘리면 약 1-2% 성능 향상이 있지만, 메모리는 3배 이상 필요합니다. Trade-off를 고려하세요.
💡 각 Stage의 출력을 저장해두면 FPN(Feature Pyramid Network) 같은 고급 구조로 쉽게 확장할 수 있습니다. 객체 검출이나 세그멘테이션에 유용합니다.
💡 모델을 저장할 때는 state_dict만 저장하세요. 전체 모델을 pickle로 저장하면 버전 호환성 문제가 생길 수 있습니다.
5. Identity Mapping - Skip Connection의 수학적 원리
시작하며
여러분이 ResNet을 사용하면서 이런 궁금증을 가져본 적 있나요? "왜 단순히 더하기만 했는데 이렇게 효과가 좋을까?" Skip Connection이 왜 작동하는지 수학적으로 이해하고 싶으셨을 겁니다.
이런 궁금증은 딥러닝을 깊이 이해하려는 사람이라면 누구나 가지는 자연스러운 질문입니다. 표면적으로는 단순한 덧셈이지만, 역전파(backpropagation) 관점에서 보면 기울기 흐름을 근본적으로 바꾸는 강력한 메커니즘입니다.
바로 이럴 때 필요한 것이 Identity Mapping의 수학적 이해입니다. 이것은 왜 ResNet이 깊은 네트워크에서도 잘 학습되는지, 왜 기울기 소실 문제가 해결되는지를 명확하게 설명해줍니다.
개요
간단히 말해서, Identity Mapping은 입력 x가 변형 없이 출력으로 "직통"할 수 있게 하여, 역전파 시 기울기가 감쇠 없이 전달되도록 만드는 수학적 구조입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 네트워크가 깊어질수록 역전파 시 기울기가 곱셈으로 누적되면서 사라지는데(vanishing gradient), Identity Mapping은 덧셈 구조를 만들어서 기울기가 최소 1의 값을 유지하게 합니다.
예를 들어, 100층짜리 네트워크에서도 첫 번째 층까지 기울기가 살아서 전달될 수 있습니다. 전통적인 방법과의 비교를 해보면, 일반 네트워크는 역전파 시 ∂L/∂x = ∂L/∂y × ∂y/∂x 형태로 기울기가 곱해지면서 지수적으로 감소하지만, ResNet은 ∂L/∂x = ∂L/∂y × (1 + ∂F/∂x) 형태로 항상 1이라는 "기본값"을 보장합니다.
Identity Mapping의 핵심 특징은 첫째, y = F(x) + x 구조에서 미분 시 ∂y/∂x = ∂F/∂x + 1이 되고, 둘째, 역전파 시 기울기가 분산되지 않고 합산되며, 셋째, 네트워크가 항등 함수를 학습하기 매우 쉬워진다는 점입니다. 이러한 특징들이 깊은 네트워크의 학습을 수학적으로 보장해줍니다.
코드 예제
import torch
import torch.nn as nn
# 주석: Identity Mapping의 효과를 시각화하는 예제
class IdentityMappingDemo(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.ModuleList([
nn.Linear(128, 128) for _ in range(10)
])
def forward_without_skip(self, x):
# 주석: Skip Connection 없는 일반 네트워크
for layer in self.layers:
x = torch.relu(layer(x))
return x
def forward_with_skip(self, x):
# 주석: Skip Connection이 있는 ResNet 스타일
for layer in self.layers:
identity = x
x = torch.relu(layer(x))
x = x + identity # Identity Mapping!
return x
# 주석: 기울기 흐름 비교 실험
def compare_gradients():
model = IdentityMappingDemo()
x = torch.randn(1, 128, requires_grad=True)
# 주석: Skip Connection 없이
out1 = model.forward_without_skip(x)
loss1 = out1.sum()
loss1.backward()
grad_without_skip = x.grad.clone()
# 주석: Skip Connection 있을 때
x.grad = None # 기울기 초기화
out2 = model.forward_with_skip(x)
loss2 = out2.sum()
loss2.backward()
grad_with_skip = x.grad.clone()
# 주석: 기울기 크기 비교 - Skip이 있을 때 훨씬 큼!
print(f"Without skip: {grad_without_skip.norm():.4f}")
print(f"With skip: {grad_with_skip.norm():.4f}")
설명
이것이 하는 일: Identity Mapping은 수학적으로 기울기 흐름을 보장하여, 아무리 깊은 네트워크도 학습 가능하게 만듭니다. 첫 번째로, forward pass에서 y = F(x) + x라는 구조를 만듭니다.
여기서 F(x)는 ResNet 블록의 변환 함수이고, x는 원본 입력입니다. 이 간단한 덧셈이 역전파에서 놀라운 효과를 만들어냅니다.
그 다음으로, backward pass에서 chain rule에 의해 ∂y/∂x = ∂F/∂x + 1이 됩니다. 여기서 중요한 점은 "+1" 항입니다.
F(x)가 아무리 복잡한 변환이어도, 최소한 1이라는 값이 항상 보장되므로 기울기가 0으로 사라질 수 없습니다. 내부적으로 이것은 기울기가 "곱셈"이 아닌 "덧셈"으로 누적된다는 것을 의미합니다.
세 번째 단계로, 다층 네트워크에서 이 효과가 누적됩니다. 예를 들어, 100개의 ResNet 블록을 거쳐도 기울기는 (1 + ∂F₁/∂x) + (1 + ∂F₂/∂x) + ...
형태로 합산되어, 최소 100이라는 값을 유지합니다. 반면 일반 네트워크는 ∂F₁/∂x × ∂F₂/∂x × ...
형태로 곱해져서 0.99^100 = 0.366처럼 지수적으로 감소합니다. 마지막으로, compare_gradients 함수는 이 차이를 실제로 확인할 수 있게 해줍니다.
여러분이 이 코드를 실행하면 Skip Connection이 있을 때 기울기 크기가 10배 이상 큰 것을 볼 수 있습니다. 실무에서의 이점으로는 첫째, 1000층 이상의 극도로 깊은 네트워크도 이론적으로 학습 가능, 둘째, learning rate를 더 크게 설정 가능, 셋째, 학습 초기부터 안정적인 수렴이라는 점이 있습니다.
실전 팁
💡 Identity Mapping의 효과를 최대화하려면 활성화 함수를 덧셈 이후에 배치하세요. ResNet v2 논문에서는 "pre-activation" 구조를 제안합니다.
💡 기울기를 모니터링할 때는 각 층의 기울기 norm을 로깅하세요. Skip Connection이 제대로 작동하면 모든 층에서 비슷한 크기의 기울기가 관찰됩니다.
💡 Identity 경로에 Dropout을 적용하면 안 됩니다! 이것은 기울기 흐름을 방해하여 ResNet의 핵심 이점을 없앱니다.
💡 실험적으로 Skip Connection의 가중치를 학습 가능하게 만들 수도 있습니다 (y = α×F(x) + β×x). 일부 작업에서 성능 향상이 있습니다.
6. Pre-activation ResNet - 더 나은 기울기 흐름
시작하며
여러분이 ResNet을 사용하면서 이런 생각을 해본 적 있나요? "ResNet도 좋지만, 혹시 더 개선할 방법은 없을까?" 원래 ResNet 논문 이후에도 연구자들은 계속 개선 방법을 찾았습니다.
이런 노력은 실제로 결실을 맺었습니다. 2016년 ResNet v2 논문에서 활성화 함수와 Batch Normalization의 위치를 바꾸는 간단한 수정만으로 성능과 학습 안정성을 더 높일 수 있다는 것을 발견했습니다.
바로 이럴 때 필요한 것이 Pre-activation ResNet입니다. 이 구조는 "BN → ReLU → Conv" 순서로 처리해서, Identity Mapping을 더 순수하게 유지하고 기울기 흐름을 더욱 개선합니다.
개요
간단히 말해서, Pre-activation ResNet은 Convolution 전에 Batch Normalization과 ReLU를 먼저 적용하는 구조로, 원래 ResNet의 "Conv → BN → ReLU" 순서를 뒤집은 것입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 원래 ResNet은 Skip Connection 경로가 ReLU를 거치면서 음수 값이 잘려나가는데, Pre-activation에서는 Skip Connection이 완전히 깨끗한 Identity Mapping이 되어 기울기가 더 잘 흐릅니다.
예를 들어, ResNet-200 같은 매우 깊은 모델에서 특히 효과가 큽니다. 전통적인 ResNet과의 비교를 해보면, 원래는 y = ReLU(F(x) + x)로 마지막에 비선형성이 적용되었다면, Pre-activation은 y = F'(x) + x 형태로 덧셈 자체에는 아무런 변형이 없습니다.
이것이 수학적으로 더 순수한 Identity Mapping입니다. Pre-activation ResNet의 핵심 특징은 첫째, Skip Connection 경로가 어떤 변환도 거치지 않는 완벽한 항등 함수이고, 둘째, 각 블록의 입력이 이미 정규화되어 있어 학습이 안정적이며, 셋째, 1000층 이상의 극도로 깊은 네트워크에서도 잘 작동한다는 점입니다.
이러한 특징들이 원래 ResNet보다 더 나은 성능과 안정성을 제공합니다.
코드 예제
import torch
import torch.nn as nn
class PreActBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
# 주석: Pre-activation - BN과 ReLU를 먼저!
self.bn1 = nn.BatchNorm2d(in_channels)
self.relu1 = nn.ReLU(inplace=True)
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
stride=stride, padding=1, bias=False)
# 주석: 두 번째 Pre-activation
self.bn2 = nn.BatchNorm2d(out_channels)
self.relu2 = nn.ReLU(inplace=True)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
stride=1, padding=1, bias=False)
# 주석: Skip Connection 경로 (필요시)
self.downsample = None
if stride != 1 or in_channels != out_channels:
# 주석: downsample도 Pre-activation 스타일로
self.downsample = nn.Conv2d(in_channels, out_channels,
kernel_size=1, stride=stride, bias=False)
def forward(self, x):
# 주석: Pre-activation - 변환 전에 정규화와 활성화
out = self.bn1(x)
out = self.relu1(out)
# 주석: downsample도 pre-activated 값 사용
identity = self.downsample(out) if self.downsample else x
# 주석: 첫 번째 Convolution
out = self.conv1(out)
# 주석: 두 번째 Pre-activation과 Convolution
out = self.bn2(out)
out = self.relu2(out)
out = self.conv2(out)
# 주석: 순수한 덧셈 - 어떤 비선형성도 없음!
out = out + identity
return out
설명
이것이 하는 일: Pre-activation ResNet은 활성화 함수를 Convolution 앞으로 옮겨서, Identity Mapping을 더욱 순수하게 만들고 학습 안정성을 높입니다. 첫 번째로, forward 메서드의 시작 부분에서 BN과 ReLU를 먼저 적용합니다.
이것은 원래 ResNet과의 가장 큰 차이점입니다. 입력 x가 먼저 정규화되고 활성화된 후에 Convolution을 거치므로, 각 레이어는 항상 "깨끗한" 입력을 받습니다.
이것은 학습 초기의 불안정성을 크게 줄여줍니다. 그 다음으로, downsample 경로도 pre-activated 값을 사용합니다.
원래 ResNet은 원본 입력 x를 그대로 downsample했지만, Pre-activation에서는 BN과 ReLU를 거친 값을 사용합니다. 이렇게 하면 identity 경로와 residual 경로가 비슷한 분포를 가지게 되어 학습이 더 안정적입니다.
세 번째 단계로, 두 번의 "BN → ReLU → Conv" 과정을 거쳐 residual을 계산합니다. 각 Convolution 전에 입력이 정규화되어 있으므로, 내부적으로 활성화 값의 분산이 일정하게 유지됩니다.
이것은 batch size가 작거나 학습 후반부에서 특히 유리합니다. 마지막으로, out = out + identity에서 순수한 덧셈만 일어납니다.
여기에는 ReLU도, BN도 없습니다! 이것이 수학적으로 완벽한 Identity Mapping입니다.
여러분이 이 구조를 사용하면 ResNet-200, ResNet-1001 같은 극도로 깊은 모델도 안정적으로 학습할 수 있습니다. 실무에서의 이점으로는 첫째, 원래 ResNet 대비 0.5-1% 정확도 향상, 둘째, 학습 곡선이 더 부드럽고 안정적, 셋째, hyperparameter에 덜 민감하다는 점이 있습니다.
실전 팁
💡 Pre-activation ResNet은 깊이가 50층 이상일 때 효과가 큽니다. ResNet-18이나 ResNet-34에서는 원래 구조와 비슷한 성능을 보입니다.
💡 첫 번째 블록은 예외적으로 일반 ResNet 스타일을 유지하세요. 입력 이미지에 BN을 적용하는 것은 부자연스러울 수 있습니다.
💡 Transfer Learning할 때는 Pre-activation 모델의 마지막 BN 통계를 주의하세요. 새로운 데이터셋에서 running_mean/var를 재계산해야 합니다.
💡 Mixed Precision Training을 사용한다면 Pre-activation이 더 유리합니다. BN이 Conv 앞에 있어서 수치 안정성이 높습니다.
💡 모델 압축(pruning)을 계획한다면 Pre-activation 구조에서 중요도가 낮은 채널을 찾기가 더 쉽습니다. BN의 gamma 값을 기준으로 사용하세요.
7. ResNeXt - 다중 경로로 표현력 높이기
시작하며
여러분이 ResNet의 성능을 더 높이고 싶을 때 이런 고민을 해본 적 있나요? "레이어를 더 깊게 하는 것 말고, 다른 방법으로 모델을 강화할 수는 없을까?" 이런 고민은 매우 자연스러운 것입니다.
깊이를 늘리는 것도 한계가 있고, 때로는 다른 방향의 개선이 필요합니다. 네트워크의 "폭"을 늘리되, 단순히 채널만 늘리는 것이 아니라 더 똑똑한 방법이 필요했습니다.
바로 이럴 때 필요한 것이 ResNeXt입니다. 이 구조는 "cardinality"라는 개념으로 여러 개의 병렬 경로를 만들어서, 같은 파라미터 수로도 더 높은 성능을 달성합니다.
개요
간단히 말해서, ResNeXt는 하나의 큰 Convolution 대신 여러 개의 작은 Convolution을 병렬로 실행하고 결과를 합치는 "split-transform-merge" 전략을 사용합니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, ResNet-50과 같은 파라미터 수와 계산량을 유지하면서도 약 1-2% 더 높은 정확도를 얻을 수 있습니다.
예를 들어, ImageNet에서 ResNet-50은 77.6% Top-1 정확도를 보이는데, 같은 복잡도의 ResNeXt-50은 78.8%를 달성합니다. 전통적인 ResNet과의 비교를 해보면, ResNet은 256 채널의 단일 경로로 처리하지만, ResNeXt는 32개의 4 채널 경로로 나누어 처리합니다.
총 계산량은 비슷하지만, 다양한 경로가 서로 다른 특징을 학습할 수 있습니다. ResNeXt의 핵심 특징은 첫째, cardinality(경로 개수)라는 새로운 차원을 도입하고, 둘째, 각 경로가 독립적으로 변환을 학습하며, 셋째, Inception 모듈보다 훨씬 간단하고 일관된 구조를 가진다는 점입니다.
이러한 특징들이 효율성과 성능을 동시에 개선합니다.
코드 예제
import torch
import torch.nn as nn
class ResNeXtBlock(nn.Module):
def __init__(self, in_channels, mid_channels, cardinality=32, stride=1):
super().__init__()
# 주석: cardinality = 병렬 경로의 개수
self.cardinality = cardinality
# 주석: 각 경로의 채널 수
group_width = mid_channels * cardinality
# 주석: 1x1 Conv - 차원 축소
self.conv1 = nn.Conv2d(in_channels, group_width, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(group_width)
# 주석: 3x3 Grouped Conv - 핵심! cardinality개의 병렬 경로
self.conv2 = nn.Conv2d(group_width, group_width, kernel_size=3,
stride=stride, padding=1, groups=cardinality, bias=False)
self.bn2 = nn.BatchNorm2d(group_width)
# 주석: 1x1 Conv - 차원 복원 (4배 확장)
self.conv3 = nn.Conv2d(group_width, in_channels * 4, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(in_channels * 4)
self.relu = nn.ReLU(inplace=True)
# 주석: Skip Connection 경로
self.downsample = None
if stride != 1 or in_channels != in_channels * 4:
self.downsample = nn.Sequential(
nn.Conv2d(in_channels, in_channels * 4, kernel_size=1,
stride=stride, bias=False),
nn.BatchNorm2d(in_channels * 4)
)
def forward(self, x):
identity = x
# 주석: 차원 축소
out = self.relu(self.bn1(self.conv1(x)))
# 주석: Grouped Convolution - 32개 경로가 병렬 실행!
out = self.relu(self.bn2(self.conv2(out)))
# 주석: 차원 복원 - 모든 경로의 결과를 합침
out = self.bn3(self.conv3(out))
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
설명
이것이 하는 일: ResNeXt는 하나의 큰 변환 대신 여러 개의 작은 변환을 병렬로 수행하여, 더 다양한 특징을 학습합니다. 첫 번째로, __init__에서 groups=cardinality 파라미터가 핵심입니다.
이것은 PyTorch의 Grouped Convolution 기능을 사용하는데, 입력 채널을 cardinality개 그룹으로 나누어 각각 독립적으로 Convolution을 수행합니다. 예를 들어, 128 채널 입력을 32개 그룹으로 나누면, 각 그룹은 4 채널씩 처리됩니다.
그 다음으로, forward 메서드에서 세 단계 변환이 일어납니다. 첫 번째 1x1 Conv는 채널을 group_width(예: 128)로 조정합니다.
그 다음 Grouped Conv가 실행될 때, 내부적으로 32개의 독립적인 3x3 Convolution이 병렬로 실행됩니다. 각 경로는 4 채널을 입력받아 4 채널을 출력하므로, 전체적으로는 128 → 128 변환이 됩니다.
세 번째 단계로, 각 그룹의 출력이 자동으로 concatenate되어 하나의 텐서가 됩니다. 이것을 마지막 1x1 Conv가 받아서 최종 출력 채널(보통 512)로 확장합니다.
이 과정에서 서로 다른 경로의 정보가 mix되면서 풍부한 표현을 만들어냅니다. 마지막으로, Skip Connection과 더해져서 최종 출력이 됩니다.
여러분이 이 구조를 사용하면 ResNet과 같은 학습 시간과 메모리로도 더 높은 정확도를 얻을 수 있습니다. 실무에서의 이점으로는 첫째, cardinality를 조정해서 성능과 효율성의 균형을 쉽게 조절 가능, 둘째, 모든 경로가 같은 구조라서 구현과 디버깅이 간단, 셋째, 하드웨어 가속기에서 효율적으로 병렬 처리된다는 점이 있습니다.
실전 팁
💡 cardinality는 보통 32가 표준이지만, GPU 메모리가 부족하면 16이나 8로 줄여도 됩니다. 성능 하락은 미미합니다.
💡 각 그룹의 채널 수(group_width / cardinality)는 최소 4 이상으로 유지하세요. 너무 작으면 표현력이 떨어집니다.
💡 Grouped Convolution은 일반 Conv보다 메모리 접근 패턴이 다릅니다. 프로파일링해서 실제 속도 향상을 확인하세요. �� Transfer Learning할 때 cardinality를 바꾸면 가중치를 재사용할 수 없습니다. 같은 cardinality를 유지하세요.
💡 최신 GPU(A100, H100 등)에서는 cardinality를 64까지 늘려도 속도 저하가 거의 없습니다. 실험해보세요.
8. Gradient Highway - 극도로 깊은 네트워크 학습하기
시작하며
여러분이 ResNet을 200층, 500층, 심지어 1000층 이상으로 만들고 싶을 때 어떤 추가 기법이 필요할까요? ResNet도 일정 깊이를 넘어가면 학습이 어려워집니다.
이런 문제는 극도로 깊은 네트워크를 연구하는 학계와 산업계에서 중요한 과제입니다. 단순히 Skip Connection만으로는 부족하고, 기울기가 네트워크 전체를 빠르게 흐를 수 있는 "고속도로"가 필요합니다.
바로 이럴 때 필요한 것이 Gradient Highway 개념입니다. 이것은 여러 Skip Connection을 결합하고, 추가적인 정규화 기법을 사용하여, 1000층 이상의 네트워크도 학습 가능하게 만듭니다.
개요
간단히 말해서, Gradient Highway는 단순한 Skip Connection을 넘어서, 여러 스케일의 Skip Connection을 동시에 사용하거나, 학습 가능한 게이트를 추가하여 기울기 흐름을 더욱 최적화하는 기법들의 총칭입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, ResNet-152도 충분히 깊지만, 특정 복잡한 작업(의료 영상 분석, 고해상도 이미지 처리 등)에서는 더 깊은 모델이 성능 향상을 가져올 수 있습니다.
예를 들어, ResNet-1001 같은 극한 깊이의 모델 연구에서 Gradient Highway 개념이 핵심 역할을 했습니다. 전통적인 Skip Connection과의 비교를 해보면, 일반 ResNet은 한 블록만 건너뛰는 local skip connection을 사용하지만, Gradient Highway는 여러 블록을 건너뛰는 global skip connection이나, 동적으로 skip 비율을 조절하는 gating mechanism을 추가합니다.
Gradient Highway의 핵심 특징은 첫째, 다양한 스케일의 skip connection으로 정보가 여러 경로로 흐르고, 둘째, 학습 가능한 게이트로 각 레이어의 기여도를 동적으로 조절하며, 셋째, 극도로 깊은 네트워크에서도 안정적인 학습이 가능하다는 점입니다. 이러한 특징들이 네트워크의 한계를 더욱 확장시킵니다.
코드 예제
import torch
import torch.nn as nn
class HighwayBlock(nn.Module):
def __init__(self, channels):
super().__init__()
# 주석: 일반적인 변환 경로
self.transform = nn.Sequential(
nn.Conv2d(channels, channels, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(channels),
nn.ReLU(inplace=True)
)
# 주석: 게이트 - 얼마나 변환할지 학습
self.gate = nn.Sequential(
nn.Conv2d(channels, channels, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(channels),
nn.Sigmoid() # 0~1 사이 값으로 비율 결정
)
def forward(self, x):
# 주석: 변환된 값
transform_out = self.transform(x)
# 주석: 게이트 값 계산 (0~1)
gate_value = self.gate(x)
# 주석: 게이트에 따라 변환과 identity를 mix
# gate=1이면 완전히 변환, gate=0이면 완전히 identity
out = gate_value * transform_out + (1 - gate_value) * x
return out
class DenseSkipBlock(nn.Module):
"""여러 이전 레이어의 출력을 모두 받는 DenseNet 스타일"""
def __init__(self, in_channels, growth_rate):
super().__init__()
# 주석: 현재 레이어의 변환
self.conv = nn.Sequential(
nn.BatchNorm2d(in_channels),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels, growth_rate, kernel_size=3, padding=1, bias=False)
)
def forward(self, x_list):
# 주석: 모든 이전 레이어의 출력을 concatenate
x = torch.cat(x_list, dim=1)
# 주석: 새로운 특징 추가
new_features = self.conv(x)
return new_features
설명
이것이 하는 일: Gradient Highway는 기울기가 네트워크를 통과하는 여러 "고속 경로"를 만들어서, 극도로 깊은 네트워크에서도 효과적인 학습을 보장합니다. 첫 번째로, HighwayBlock에서 핵심은 학습 가능한 gate입니다.
일반 ResNet은 항상 transform과 identity를 1:1로 더하지만, Highway는 Sigmoid로 0~1 사이의 가중치를 학습합니다. 네트워크가 스스로 "이 레이어에서는 많이 변환할까, 적게 변환할까?"를 결정하는 것입니다.
예를 들어, 학습 초기에는 gate가 0에 가까워서 거의 identity만 통과시키다가, 점차 학습되면서 필요한 만큼만 변환을 적용합니다. 그 다음으로, out = gate_value * transform_out + (1 - gate_value) * x 공식이 핵심입니다.
이것은 "soft gating"으로, 미분 가능하면서도 동적으로 비율을 조절합니다. 내부적으로 각 픽셀, 각 채널마다 다른 gate 값을 가질 수 있어서, 매우 세밀한 제어가 가능합니다.
세 번째 단계로, DenseSkipBlock은 다른 접근법을 보여줍니다. 이것은 DenseNet의 아이디어로, 이전의 모든 레이어 출력을 concatenate해서 사용합니다.
예를 들어, 5번째 블록은 1, 2, 3, 4번째 블록의 출력을 모두 받습니다. 이것은 매우 강력한 gradient highway를 만들어서, 정보와 기울기가 여러 경로로 흐를 수 있게 합니다.
마지막으로, 이런 기법들을 조합하면 ResNet-1001 같은 극한 깊이의 모델을 만들 수 있습니다. 여러분이 이 기법들을 사용하면 특정 도메인(의료, 위성 이미지 등)에서 깊이에서 오는 이점을 극대화할 수 있습니다.
실무에서의 이점으로는 첫째, 매우 복잡한 패턴도 학습 가능, 둘째, 각 레이어의 활용도를 동적으로 조절하여 효율성 향상, 셋째, 학습 초기의 불안정성 크게 감소라는 점이 있습니다.
실전 팁
💡 Highway gate의 bias를 음수로 초기화하세요(예: -2.0). 학습 초기에 identity를 선호하게 만들어서 안정성을 높입니다.
💡 DenseNet 스타일을 사용할 때는 메모리 사용량을 주의하세요. 모든 중간 출력을 저장하므로 growth_rate를 12-24 정도로 작게 유지하세요.
💡 극도로 깊은 모델(500층+)을 학습할 때는 learning rate를 일반보다 낮게 시작하세요(예: 0.01). Warm-up도 더 길게(20-30 epoch) 설정하세요.
💡 gate 값을 텐서보드에 로깅하면 각 레이어가 얼마나 활용되는지 시각화할 수 있습니다. 디버깅에 매우 유용합니다.
💡 실무에서는 ResNet-152 정도가 최적인 경우가 많습니다. 더 깊게 가기 전에 데이터 증강, 정규화 등 다른 기법을 먼저 시도하세요.
댓글 (0)
함께 보면 좋은 카드 뉴스
데이터 증강과 정규화 완벽 가이드
머신러닝 모델의 성능을 극대화하는 핵심 기법인 데이터 증강과 정규화에 대해 알아봅니다. 실무에서 바로 활용할 수 있는 다양한 기법과 실전 예제를 통해 과적합을 방지하고 모델 성능을 향상시키는 방법을 배웁니다.
CNN 아키텍처 완벽 가이드 LeNet AlexNet VGGNet
컴퓨터 비전의 기초가 되는 세 가지 핵심 CNN 아키텍처를 배웁니다. 손글씨 인식부터 이미지 분류까지, 딥러닝의 발전 과정을 따라가며 각 모델의 구조와 특징을 실습 코드와 함께 이해합니다.
CNN 기초 Convolution과 Pooling 완벽 가이드
CNN의 핵심인 Convolution과 Pooling을 초급자도 쉽게 이해할 수 있도록 설명합니다. 이미지 인식의 원리부터 실제 코드 구현까지, 실무에서 바로 활용 가능한 내용을 담았습니다.
TensorFlow와 Keras 완벽 입문 가이드
머신러닝과 딥러닝의 세계로 들어가는 첫걸음! TensorFlow와 Keras 프레임워크를 처음 접하는 분들을 위한 친절한 가이드입니다. 실무에서 바로 활용할 수 있는 핵심 개념과 예제를 통해 AI 모델 개발의 기초를 탄탄히 다져보세요.
PyTorch Dataset과 DataLoader 완벽 가이드
딥러닝 모델을 학습시킬 때 데이터를 효율적으로 다루는 방법을 배웁니다. PyTorch의 Dataset과 DataLoader를 사용하여 대용량 데이터를 메모리 효율적으로 처리하고, 배치 처리와 셔플링을 자동화하는 방법을 실무 예제와 함께 알아봅니다.