ResNet: 두 판 사이의 차이

IT 위키
편집 요약 없음
편집 요약 없음
 
211번째 줄: 211번째 줄:
*[[Convolutional Neural Network]]
*[[Convolutional Neural Network]]
*[[Deep Learning]]
*[[Deep Learning]]
*[[Wide ResNet]]
*[[ResNeXt]]
*[[ResNeXt]]
*[[DenseNet]]
*[[DenseNet]]

2025년 10월 25일 (토) 05:18 기준 최신판

ResNet(Residual Network)은 딥러닝에서 층이 매우 깊은 신경망을 안정적으로 학습하기 위해 제안된 구조이다.

개요[편집 | 원본 편집]

ResNet은 입력을 그대로 다음 층에 더해주는 스킵 연결(skip or shortcut connection)을 도입하여, 각 층이 원래 함수 H(x)를 직접 학습하는 대신 잔차(residual) 함수 F(x) = H(x) - x를 학습하도록 재정의한 구조이다. 이 방식은 기울기 소실(vanishing gradient) 문제를 완화하고, 매우 깊은 신경망에서도 안정적으로 학습이 가능하게 한다.

등장 배경[편집 | 원본 편집]

깊은 신경망을 설계할수록 성능이 향상될 것으로 기대되지만, 현실에서는 층을 깊게 쌓을수록 학습이 어려워지고 오히려 성능이 나빠지는 성능 저하(degradation) 문제가 발생한다. 이 문제를 해결하기 위해 2015년 마이크로소프트 연구진은 "Deep Residual Learning for Image Recognition" 논문에서 ResNet 구조를 제안하였고, ImageNet 대회에서 우수한 성능을 기록했다.

주요 버전[편집 | 원본 편집]

ResNet은 층의 수(depth)에 따라 다양한 버전으로 구성된다. 각 버전은 기본적으로 동일한 아이디어(잔차 학습)를 유지하지만, 블록의 종류, 반복 횟수, 채널 수, 계산량 등이 다르게 설계되어 있다.

  • ResNet-18
    • 18개의 가중치 층으로 구성된 비교적 얕은 모델
    • 기본 블록(Basic Block)을 사용하며, 각 블록은 3×3 합성곱 2개로 이루어짐
    • 계산량이 적고, 소형 모델이나 임베디드 환경에서 자주 사용됨
    • ImageNet 기준으로 약 11M 파라미터를 가짐
  • ResNet-34
    • ResNet-18보다 깊지만 동일한 기본 블록 구조를 유지
    • 각 스테이지(conv2_x~conv5_x)의 블록 수가 늘어나 더 풍부한 표현 가능
    • 학습 안정성을 유지하면서도 정확도를 개선한 중간 규모 모델
  • ResNet-50
    • 병목 블록(Bottleneck Block) 구조를 처음 도입한 대표 모델
    • 각 블록이 1×1 → 3×3 → 1×1 합성곱으로 구성되어, 연산량 대비 표현력이 높음
    • 채널 수가 64→256, 128→512, 256→1024, 512→2048로 점진적으로 증가
    • ImageNet 분류 기준으로 약 25M 파라미터를 가지며, 실무에서 가장 널리 사용됨
  • ResNet-101
    • ResNet-50보다 conv4_x 스테이지의 병목 블록 수를 크게 늘려 깊이를 확장
    • 더 풍부한 특징 추출이 가능하며, 고해상도 이미지나 세밀한 분류 작업에서 우수
    • 연산량은 늘어나지만, 전이 학습(Transfer Learning) 용도로 자주 활용됨
    • 약 44M 파라미터 규모
  • ResNet-152
    • 가장 깊은 버전 중 하나로, 152개의 층으로 구성
    • conv4_x 스테이지가 매우 깊으며, 학습에는 많은 계산 자원이 필요
    • 매우 정밀한 표현을 요구하는 연구나 대형 데이터셋에서 주로 사용
    • 약 60M 파라미터를 가지며, 학습 효율보다 정확도에 중점을 둔 설계

이처럼 ResNet의 각 버전은 단순히 층 수만 다른 것이 아니라, 블록의 형태, 채널 구성, 계산 효율, 성능 균형을 실험적으로 조정한 휴리스틱 설계 결과이다.

구조 및 구성 요소[편집 | 원본 편집]

ResNet은 Residual Block 단위로 구성된다. 각 잔차 블록은 다음과 같은 형태를 갖는다.

  • 기본 블록 (Basic Block)
    • 두 개의 3×3 합성곱 층과 배치 정규화, ReLU 활성화로 구성됨
    • ResNet-18, 34에서 사용
  • 병목 블록 (Bottleneck Block)
    • 1×1 축소 → 3×3 처리 → 1×1 확장의 형태로 세 층을 구성
    • ResNet-50, 101, 152 등에서 사용됨
  • 사전 활성화 블록 (Pre‑activation Block)
    • 잔차 함수 계산 전에 정규화와 활성화를 먼저 적용함
    • 더 깊은 네트워크에서도 안정적인 학습 가능

스킵 연결을 통해 출력은 다음과 같이 계산된다: y = F(x) + x 여기서 F(x)는 잔차 함수이며, x는 블록의 입력이다.

장단점[편집 | 원본 편집]

장점[편집 | 원본 편집]

  • 깊은 구조 설계 가능
    • 수백 개 층 이상에서도 안정적인 학습이 가능하다
  • 기울기 보존
    • 스킵 연결 덕분에 역전파 시 기울기 소실을 방지할 수 있다
  • 수렴 속도 향상
    • 잔차 학습 방식으로 더 빠른 수렴 가능
  • 범용성
    • 다양한 컴퓨터 비전 과제에서 강력한 백본(backbone)으로 활용됨

단점 / 제약[편집 | 원본 편집]

  • 구조 복잡성
    • 블록 설계와 스킵 연결 방식이 복잡해질 수 있음
  • 계산 자원 요구
    • 깊은 네트워크일수록 연산량과 메모리 소모가 커짐
  • 과적합 위험
    • 매우 깊은 모델은 학습 데이터에 과적합될 가능성이 있음

주요 변형 및 후속 연구[편집 | 원본 편집]

  • ResNeXt
    • 그룹 합성곱을 이용해 표현력을 확장한 구조
  • Wide ResNet
    • 층 수를 줄이고 각 층의 너비를 넓힌 변형
  • DenseNet
    • 모든 이전 층의 출력을 연결하여 활용하는 방식
  • SE‑ResNet
    • 채널 중요도를 학습하여 강조하는 SE 모듈을 통합한 구조
  • Improved ResNet
    • 블록 내부 구조 및 스킵 연결 방식을 개선한 다양한 후속 모델들

응용 분야[편집 | 원본 편집]

ResNet은 이미지 분류, 객체 검출, 시맨틱 분할, 비디오 분석 등의 컴퓨터 비전 과제에서 널리 사용된다. 또한 Transformer 구조에서도 잔차 연결 개념이 도입되어 활용되고 있다.

구현 예시 (PyTorch 기준)[편집 | 원본 편집]

다음은 PyTorch로 ResNet을 구현하는 일반적인 방식의 예시이다. torchvision의 기본 구조를 따르며, 블록 구성과 계층 조립을 유연하게 설정할 수 있도록 한다.

import torch
import torch.nn as nn
import torch.nn.functional as F

# 기본 블록 정의 (ResNet-18, 34)
class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3,
                               stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
                               stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample:
            identity = self.downsample(x)

        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += identity
        return F.relu(out)

# 병목 블록 정의 (ResNet-50, 101, 152)
class Bottleneck(nn.Module):
    expansion = 4

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)

        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3,
                               stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)

        self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion,
                               kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)

        self.downsample = downsample

    def forward(self, x):
        identity = x
        if self.downsample:
            identity = self.downsample(x)

        out = F.relu(self.bn1(self.conv1(x)))
        out = F.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out += identity
        return F.relu(out)

# ResNet 본체 정의
class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=1000):
        super(ResNet, self).__init__()
        self.in_channels = 64

        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)

        self.layer1 = self._make_layer(block, 64, layers[0])
        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, blocks, stride=1):
        downsample = None
        if stride != 1 or self.in_channels != out_channels * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels * block.expansion,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * block.expansion),
            )

        layers = []
        layers.append(block(self.in_channels, out_channels, stride, downsample))
        self.in_channels = out_channels * block.expansion

        for _ in range(1, blocks):
            layers.append(block(self.in_channels, out_channels))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.relu(self.bn1(self.conv1(x)))
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)
        return x

# 각 버전에 맞춘 팩토리 함수
def ResNet18():
    return ResNet(BasicBlock, [2, 2, 2, 2])

def ResNet34():
    return ResNet(BasicBlock, [3, 4, 6, 3])

def ResNet50():
    return ResNet(Bottleneck, [3, 4, 6, 3])

def ResNet101():
    return ResNet(Bottleneck, [3, 4, 23, 3])

def ResNet152():
    return ResNet(Bottleneck, [3, 8, 36, 3])

같이 보기[편집 | 원본 편집]

참고 문헌[편집 | 원본 편집]

각주[편집 | 원본 편집]