CNN(Convolutional Neural Network) — LeNet에서 ResNet까지

지난 30년의 주요 합성곱 신경망(CNN)은 모두 합성곱(convolution) — 비선형성(nonlinearity) — 다운샘플링(downsample)이라는 동일한 조리법(recipe)에 새로운 아이디어 하나를 덧붙인 구조입니다. 그 아이디어를 순서대로 익힙니다.

유형: Learn + Build 언어: Python 선수 학습: Phase 3 Lesson 11 PyTorch, Phase 4 Lesson 01 Image Fundamentals, Phase 4 Lesson 02 Convolutions from Scratch 소요 시간: 약 75분

학습 목표

  • LeNet-5 -> AlexNet -> VGG -> Inception -> ResNet으로 이어지는 아키텍처 계보(architecture lineage)를 따라가고 각 계열(family)이 기여한 핵심 아이디어를 설명합니다.
  • PyTorch로 LeNet-5, VGG 스타일 블록(VGG-style block), ResNet BasicBlock을 각각 40줄 이하의 코드로 구현합니다.
  • 잔차 연결(residual connection)이 왜 1,000개 층(layer) 규모의 신경망(network)을 학습 불가능(untrainable)한 상태에서 최첨단(state-of-the-art) 성능으로 바꾸는지 설명합니다.
  • ResNet-18, ResNet-50 같은 최신 백본(modern backbone)을 읽고 출력 형상(output shape), 수용 영역(receptive field), 파라미터 수(parameter count)를 예측합니다.

문제

2011년 ImageNet 분류기(classifier)의 최고 성능은 상위 5개 정확도(top-5 accuracy) 기준 약 74%였습니다. 2012년 AlexNet은 85%, 2015년 ResNet은 96%를 기록했습니다. 새로운 데이터나 새로운 GPU 세대(generation)가 아니라 아키텍처 아이디어가 만든 차이였습니다.

실무 비전 엔지니어(vision engineer)는 어떤 아이디어가 어떤 논문(paper)에서 나왔는지 알아야 합니다. 2026년에 배포하는 운영용 백본(production backbone) 역시 같은 조각들을 재조합한 결과이기 때문입니다. 그룹 합성곱(grouped conv)은 CNN에서 트랜스포머(transformer)로 이어졌고, 잔차 연결은 ResNet에서 모든 LLM으로 이어졌으며, 배치 정규화(batch normalization)는 확산 모델(diffusion model)에서도 여전히 살아 있습니다.

이 신경망들을 순서대로 공부하면 “가장 큰 모델부터 쓰는” 실수를 줄일 수 있습니다. MNIST에는 ResNet이 필요 없습니다. 각 계열의 확장 곡선(scaling curve)을 알면 문제와 예산에 맞는 위치를 고를 수 있습니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.AlexNet(2012)이 GPU에서 깊은 합성곱 신경망(deep CNN) 학습을 실용적으로 만든 단일 아키텍처 아이디어는 무엇입니까?

2.VGG가 하나의 큰 커널 대신 3x3 합성곱을 쌓는 방식을 선호한 이유는 무엇입니까?

0/2 답변 완료

개념

비전(Vision)을 바꾼 네 가지 아이디어

timeline
    title Four ideas, four families
    1998 : LeNet-5 : Conv + pool + FC for digits, CPU training, 60k params
    2012 : AlexNet : Deeper + ReLU + dropout + two GPUs, ImageNet 10 points jump
    2014 : VGG / Inception : 3x3 stacks(VGG), parallel filter sizes(Inception)
    2015 : ResNet : Identity skip connections unlock 100+ layer training

고전 비전(classical vision)에서 이 네 번의 도약만큼 중요한 변화는 많지 않습니다.

LeNet-5(1998)

얀 르쿤(Yann LeCun)이 만든 숫자 인식기(digit recognizer)입니다. 약 60,000개의 파라미터, 두 개의 합성곱-풀링 블록(conv-pool block), 두 개의 완전 연결 계층(fully connected layer), 그리고 하이퍼볼릭 탄젠트(tanh) 활성화 함수(activation)로 구성됩니다.

input (1, 32, 32)
  conv 5x5 -> (6, 28, 28)
  avg pool 2x2 -> (6, 14, 14)
  conv 5x5 -> (16, 10, 10)
  avg pool 2x2 -> (16, 5, 5)
  flatten -> 400
  dense -> 120
  dense -> 84
  dense -> 10

최신 CNN의 기본 구조, 즉 합성곱과 다운샘플링을 번갈아 쌓은 뒤 작은 분류기 헤드(classifier head)로 보내는 템플릿(template)은 LeNet에서 시작됐습니다.

AlexNet(2012)

ImageNet을 깨뜨린 세 가지 변화는 다음과 같습니다.

  1. tanh 대신 ReLU. 기울기(gradient)가 덜 사라지고 학습 속도가 약 6배 빨라졌습니다.
  2. 완전 연결 헤드의 드롭아웃(dropout). 정규화(regularization)가 단순한 요령이 아니라 하나의 계층(layer)으로 자리 잡았습니다.
  3. 깊이(depth)와 너비(width). 5개의 합성곱 계층, 3개의 밀집 계층(dense layer), 6천만 개의 파라미터를 두 개의 GPU에 모델을 나눠 학습했습니다.

두 GPU 분할은 하드웨어 우회책(workaround)이었지만, ReLU, 드롭아웃, 더 깊고 넓은 신경망이라는 아이디어는 지금도 살아 있습니다.

VGG(2014)

VGG는 “3x3 합성곱만 사용하면서 깊게 쌓으면 어떻게 될까?”라는 질문을 던졌습니다.

stack:   conv 3x3 -> conv 3x3 -> pool 2x2
repeat:  16 또는 19 conv layers

두 개의 3x3 합성곱은 하나의 5x5 합성곱과 동일한 입력 영역(input area)을 보지만, 파라미터는 더 적고 중간에 ReLU를 한 번 더 넣을 수 있습니다. 이 단순함은 이후 아키텍처의 기준점이 되었습니다. 단점은 1억 3,800만 개의 파라미터, 느린 학습, 그리고 비싼 추론(inference) 비용입니다.

Inception(2014)

구글(Google)의 답은 “어떤 커널 크기(kernel size)를 써야 할까?”라는 질문에 대해 “전부 병렬로 쓰자”였습니다.

flowchart LR
    IN["Input feature map"] --> A["1x1 conv"]
    IN --> B["3x3 conv"]
    IN --> C["5x5 conv"]
    IN --> D["3x3 max pool"]
    A --> CAT["Concatenate<br/>along channel axis"]
    B --> CAT
    C --> CAT
    D --> CAT
    CAT --> OUT["Next block"]

    style IN fill:#dbeafe,stroke:#2563eb
    style CAT fill:#fef3c7,stroke:#d97706
    style OUT fill:#dcfce7,stroke:#16a34a

1x1은 채널 혼합(channel mixing), 3x3은 국소 질감(local texture), 5x5는 더 큰 패턴(larger pattern), 풀링(pooling)은 이동 불변(shift-invariant) 특징에 특화됩니다. Inception v1은 파라미터 수를 줄이기 위해 각 분기(branch) 안에 1x1 합성곱 병목(bottleneck)을 사용했습니다.

퇴화 문제(Degradation problem)

2015년에는 VGG-19는 잘 동작했지만 VGG-32는 그렇지 못했습니다. 깊이가 더해지면 좋아져야 할 것 같지만, 20개 계층을 넘어가면 학습 손실(training loss)과 테스트 손실(test loss)이 모두 나빠졌습니다. 이는 과적합(overfitting)이 아니라, 옵티마이저(optimizer)가 많은 비선형 계층을 지나 항등 사상(identity mapping)을 학습하지 못하는 퇴화 문제(degradation problem)입니다.

Plain deep network:
  y = f_L(f_{L-1}(... f_1(x) ...))

Early layer gradient:
  dL/dW_1 = dL/dy * df_L/df_{L-1} * ... * df_1/dW_1

작은 이득(gain)이 여러 번 곱해지면 기울기는 사실상 0이 됩니다. 배치 정규화(batch norm)만으로는 30개 계층 이상의 깊이 문제를 완전히 해결하기 어렵습니다.

ResNet(2015)

허(He), 장(Zhang), 런(Ren), 쑨(Sun)은 단 하나의 변화로 이 문제를 풀었습니다.

standard block:   y = F(x)
residual block:   y = F(x) + x

+ x 항이 있으면 계층은 F(x)를 0에 가깝게 만들어 아무 일도 하지 않는 항등 변환(identity)을 손쉽게 선택할 수 있습니다. 아주 깊은 ResNet도 블록마다 “나빠지지 않을 비상 탈출구(escape hatch)”를 갖게 됩니다. 옵티마이저는 각 블록을 조금씩만 유용하게 만들면 되고, 그 작은 개선이 100번 쌓이면 최첨단 성능에 도달합니다.

flowchart LR
    X["Input x"] --> F["F(x)<br/>conv + BN + ReLU<br/>conv + BN"]
    X -.->|identity skip| PLUS(["+"])
    F --> PLUS
    PLUS --> RELU["ReLU"]
    RELU --> OUT["y"]

    style X fill:#dbeafe,stroke:#2563eb
    style PLUS fill:#fef3c7,stroke:#d97706
    style OUT fill:#dcfce7,stroke:#16a34a

주요 블록은 두 가지입니다.

  • BasicBlock: ResNet-18/34에 사용됩니다. 두 개의 3x3 합성곱과 한 줄기 건너뛰기 연결(skip connection).
  • Bottleneck: ResNet-50/101/152에 사용됩니다. 1x1 축소(down), 3x3 중간(middle), 1x1 확장(up) 구조이며, 채널 수가 많을 때 효율적입니다.

스트라이드(stride) 2로 다운샘플링하거나 채널 수가 바뀌면 항등 경로(identity path)도 형상(shape)을 맞춰야 하므로 1x1 스트라이드=2 합성곱을 사용합니다.

비전을 넘어선 잔차(residual)

잔차 연결은 단순히 이미지 분류(image classification)에만 국한된 아이디어가 아닙니다. 잔차 연결은 깊은 신경망을 “기울기가 살아남기를 기도하는 구조”에서 안정적이고(reliable) 확장 가능한(scalable) 엔지니어링 도구로 바꿨습니다. 다음 phase에서 다룰 트랜스포머 역시 모든 블록에 동일한 건너뛰기 연결을 갖고 있습니다.

만들어 보기

Step 1: LeNet-5

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

class LeNet5(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, kernel_size=5)
        self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
        self.pool = nn.AvgPool2d(2)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, num_classes)

    def forward(self, x):
        x = self.pool(torch.tanh(self.conv1(x)))
        x = self.pool(torch.tanh(self.conv2(x)))
        x = torch.flatten(x, 1)
        x = torch.tanh(self.fc1(x))
        x = torch.tanh(self.fc2(x))
        return self.fc3(x)

net = LeNet5()
x = torch.randn(1, 1, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

예상 출력은 torch.Size([1, 10])이고, 파라미터 수는 약 61,706개입니다.

Step 2: VGG 블록

두 개의 3x3 합성곱, ReLU, 배치 정규화, 최대 풀링(max pool)으로 구성된 재사용 가능한(reusable) 블록입니다.

class VGGBlock(nn.Module):
    def __init__(self, in_c, out_c):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, padding=1)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, padding=1)
        self.bn2 = nn.BatchNorm2d(out_c)
        self.pool = nn.MaxPool2d(2)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        return self.pool(x)

class MiniVGG(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stack = nn.Sequential(
            VGGBlock(3, 32),
            VGGBlock(32, 64),
            VGGBlock(64, 128),
        )
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(128, num_classes),
        )

    def forward(self, x):
        return self.head(self.stack(x))

net = MiniVGG()
x = torch.randn(1, 3, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

MiniVGG는 CIFAR 크기의 입력에 충분히 강한 약 29만 개 파라미터의 모델입니다.

Step 3: ResNet BasicBlock

class BasicBlock(nn.Module):
    def __init__(self, in_c, out_c, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_c, out_c, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_c)
        self.conv2 = nn.Conv2d(out_c, out_c, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_c)
        if stride != 1 or in_c != out_c:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_c, out_c, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_c),
            )
        else:
            self.shortcut = nn.Identity()

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out = out + self.shortcut(x)
        return F.relu(out)

bias=False는 배치 정규화 관례(convention)입니다. 배치 정규화의 베타(beta) 파라미터가 편향(bias) 역할을 하므로 합성곱의 편향까지 두면 낭비가 됩니다. shortcut 경로는 스트라이드나 채널 수가 바뀔 때만 실제 합성곱이 필요합니다.

Step 4: Tiny ResNet

네 그룹(group)의 BasicBlock을 쌓아 CIFAR 크기 입력용 ResNet을 만듭니다. 2, 3, 4번 그룹의 첫 블록에서 스트라이드 2로 다운샘플링하고 채널 수를 두 배로 늘립니다. 약 280만 개의 파라미터를 가지며, ResNet-152까지 확장되는 표준 조리법(recipe)입니다.

class TinyResNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.stem = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(32),
            nn.ReLU(inplace=True),
        )
        self.layer1 = self._make_group(32, 32, num_blocks=2, stride=1)
        self.layer2 = self._make_group(32, 64, num_blocks=2, stride=2)
        self.layer3 = self._make_group(64, 128, num_blocks=2, stride=2)
        self.layer4 = self._make_group(128, 256, num_blocks=2, stride=2)
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(256, num_classes),
        )

    def _make_group(self, in_c, out_c, num_blocks, stride):
        blocks = [BasicBlock(in_c, out_c, stride=stride)]
        for _ in range(num_blocks - 1):
            blocks.append(BasicBlock(out_c, out_c, stride=1))
        return nn.Sequential(*blocks)

    def forward(self, x):
        x = self.stem(x)
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
        return self.head(x)

net = TinyResNet()
x = torch.randn(1, 3, 32, 32)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

Step 5: 파라미터 대비 특징 효율(Parameter-to-feature efficiency) 비교

LeNet5, MiniVGG, TinyResNet을 동일한 방식으로 요약(summary)합니다. CIFAR-10 기준으로 LeNet은 약 60%, MiniVGG는 약 89%, TinyResNet은 몇 번의 에폭(epoch) 후 약 93%의 정확도(accuracy)를 기대할 수 있습니다. 모델 계열에 따라 파라미터 효율이 달라집니다.

def summary(name, net, x):
    y = net(x)
    params = sum(p.numel() for p in net.parameters())
    print(f"{name:12s}  input {tuple(x.shape)} -> output {tuple(y.shape)}  params {params:>10,}")

x = torch.randn(1, 3, 32, 32)
summary("LeNet5",     LeNet5(),       torch.randn(1, 1, 32, 32))
summary("MiniVGG",    MiniVGG(),      x)
summary("TinyResNet", TinyResNet(),   x)

세 개의 모델, 세 개의 시대, 그리고 세 자릿수 차이의 파라미터 수를 비교합니다. CIFAR-10 정확도 기준으로는 LeNet 약 60%, MiniVGG 약 89%, TinyResNet 약 93%를 기대할 수 있습니다.

사용하기

torchvision.models는 위 계열들의 사전 학습(pretrained) 버전을 제공합니다.

from torchvision.models import resnet18, ResNet18_Weights, vgg16, VGG16_Weights

r18 = resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)
r18.eval()

print(f"ResNet-18 params: {sum(p.numel() for p in r18.parameters()):,}")
print(r18.layer1[0])
print()

v16 = vgg16(weights=VGG16_Weights.IMAGENET1K_V1)
v16.eval()
print(f"VGG-16   params: {sum(p.numel() for p in v16.parameters()):,}")

ResNet-18은 약 1,170만 개, VGG-16은 약 1억 3,800만 개의 파라미터를 가집니다. ImageNet 상위 1개 정확도(top-1 accuracy)는 비슷합니다(69.8% 대 71.6%). 잔차 연결은 약 12배의 파라미터 효율 이득을 가져다줍니다. 그래서 ResNet 계열의 변형(variant)은 ViT가 등장한 2021년까지 지배적이었고, 연산량(compute)이 제약(constraint)인 실제 배포 환경(real-world deployment)에서는 여전히 강세를 보입니다.

전이 학습(transfer learning)은 언제나 같은 조리법을 따릅니다. 사전 학습된 백본을 불러오고(load), 백본을 고정(freeze)한 뒤 분류기 헤드를 교체합니다.

for p in r18.parameters():
    p.requires_grad = False
r18.fc = nn.Linear(r18.fc.in_features, 10)

단 세 줄로, ImageNet이 비용을 치러 학습한 표현(representation)을 물려받는 10-클래스 CIFAR 분류기가 만들어집니다.

산출물 만들기

이 lesson의 최종 산출물은 다음 두 가지입니다.

  • outputs/prompt-backbone-selector.md: 과제(task), 데이터셋 크기, 연산 예산(compute budget)에 맞는 CNN 계열을 고르는 prompt
  • outputs/skill-residual-block-reviewer.md: PyTorch 잔차 블록의 건너뛰기 연결, 배치 정규화 배치(BN placement), 활성화 순서(activation order), 형상 정렬(shape alignment)을 검토하는 skill

연습문제

  1. 쉬움: TinyResNet의 파라미터 수를 계층별로 손으로 계산합니다. sum(p.numel() for p in net.parameters())와 비교합니다. 파라미터 예산 대부분이 합성곱, 배치 정규화, 분류기 헤드 중 어디에 쓰이는지 확인합니다.
  2. 중간: Bottleneck 블록(1x1 -> 3x3 -> 1x1 with skip)을 구현하고 CIFAR용 ResNet-50 스타일 신경망을 만듭니다. TinyResNet과 파라미터 수를 비교합니다.
  3. 어려움: BasicBlock에서 건너뛰기 연결을 제거한 34개 블록의 평범한(plain) 신경망과 34개 블록의 ResNet을 각각 CIFAR-10에서 10 에폭 동안 학습합니다. 학습 손실(training loss)과 에폭의 관계를 그래프로 그려(plot) He 외(He et al.) 논문의 Figure 1처럼 깊은 평범한 신경망이 더 높은 손실로 수렴하는지 확인합니다.

핵심 용어

용어흔한 설명실제 의미
백본(Backbone)"모델 그 자체"과제 헤드(task head)에 전달할 특징 맵(feature map)을 만들어 내는 합성곱 블록의 묶음
잔차 연결(Residual connection)"건너뛰기 연결(skip connection)"y = F(x) + x; F를 0으로 두면 항등 변환을 쉽게 학습할 수 있어 임의의 깊이를 학습 가능(trainable)하게 만듦
BasicBlock"두 개의 3x3 합성곱에 건너뛰기 연결을 더한 것"ResNet-18/34 블록: 합성곱-배치 정규화-ReLU-합성곱-배치 정규화-덧셈-ReLU
Bottleneck"1x1 축소, 3x3, 1x1 확장"ResNet-50/101/152 블록. 채널 수가 많을 때 효율적
퇴화 문제(Degradation problem)"깊을수록 나빠진다"깊은 평범한 합성곱 신경망에서 학습/테스트 오차가 모두 증가하는 현상
스템(Stem)"첫 번째 계층"3채널 입력을 기본 특징 폭(base feature width)으로 바꾸는 초기 합성곱
헤드(Head)"분류기"마지막 백본 블록 뒤에 붙는 적응형 풀링(adaptive pool), 평탄화(flatten), 선형 계층(linear layer)
전이 학습(Transfer learning)"사전 학습된 가중치(pretrained weights)"ImageNet에서 학습된 백본을 불러와 과제 헤드 중심으로 미세 조정(fine-tuning)하는 방식

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 2개

skill-residual-block-reviewer

Review a PyTorch residual block for skip-connection correctness, BN placement, activation order, and shape alignment

Skill
prompt-backbone-selector

Pick the right vision backbone (LeNet, VGG, ResNet, MobileNet, EfficientNet-Lite, ConvNeXt, ViT) for a given task, dataset size, and compute budget

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.ResNet은 잔차 연결 `y = F(x) + x`를 도입했습니다. 이것은 어떤 문제를 해결합니까?

2.in_channels=64, out_channels=128, stride=2인 ResNet BasicBlock에서 단축 분기(shortcut branch)의 역할은 무엇입니까?

3.ResNet-18은 약 1,170만 개의 파라미터로 ImageNet에서 VGG-16(약 1억 3,800만 개 파라미터)과 비슷하거나 더 나은 성능을 냅니다. 이것은 VGG에 대해 무엇을 의미합니까?

0/3 답변 완료