← 주차 목록

Week 10. CNN 심화와 전이학습

AlexNet에서 ResNet까지 거대 네트워크들의 진화, 그리고 남이 학습시킨 모델을 가져와 내 문제에 적용하는 전이학습.

이번 주에 배우는 것

  1. AlexNet — 딥러닝 르네상스의 시작
  2. VGG — 깊이의 힘
  3. GoogLeNet — 넓이의 힘
  4. ResNet — 잔차 연결로 100층을 넘다
  5. 전이학습 — 특성 추출과 미세조정

0. 왜 "거대 모델의 역사"를 굳이 배우는가

오늘날 우리는 models.resnet50(weights='IMAGENET1K_V2') 한 줄로 수천만 장의 이미지로 학습된 모델을 불러옵니다. 너무 편해서 그 속사정을 들여다볼 일이 없지만, 실제로 W9에서 배운 CNN의 기본 블록(합성곱·풀링·ReLU)만으로는 지금의 성능에 결코 도달할 수 없었습니다. 2012년부터 2015년까지, 단 3년 사이에 이 분야는 완전히 다른 모습이 되었고, 그 변화의 핵심 아이디어는 지금도 모든 비전 모델(심지어 트랜스포머조차)의 설계 원칙으로 살아 있습니다.

이번 주의 목표는 두 가지입니다. 첫째, AlexNet → VGG → GoogLeNet → ResNet 으로 이어지는 네 단계의 진화에서 각 모델이 "직전 모델의 어떤 한계"를 깼는지를 이해하는 것. 둘째, 그렇게 학습된 거대 모델을 내 작은 데이터셋에 "빌려다 쓰는" 전이학습의 기법을 배우는 것입니다.

역사적 맥락. 2010~2011년까지 ImageNet 대회 우승팀은 전부 SIFT+Fisher Vector 같은 수공 특성(handcrafted feature)을 쓰는 전통적 컴퓨터 비전 팀들이었습니다. 당시 "신경망으로 이미지 분류를 한다"는 건 학계에서 거의 비웃음을 사는 접근이었습니다. 2012년 AlexNet의 등장은 그래서 단순한 기록 경신이 아니라 패러다임 전환이었습니다.

1. AlexNet (2012) — 딥러닝의 부활

2012년 ImageNet 대회. Krizhevsky·Sutskever·Hinton의 AlexNet이 이전 우승자(에러율 26%)를 16%로 끌어내렸습니다. 이 한 번의 사건이 딥러닝 시대를 열었습니다. 비결은 ① GPU 학습, ② ReLU, ③ 드롭아웃, ④ 데이터 증강.

1.1 구조의 상세

AlexNet은 합성곱 5층 + 완전연결 3층, 총 8층에 약 6천만 파라미터를 가졌습니다. 지금 기준에선 작지만 당시엔 엄청난 규모였고, 그래서 GTX 580 GPU 2장에 모델을 "쪼개서" 병렬로 학습시켰습니다. 첫 층은 $11\times 11$ 큰 필터에 stride 4 — 입력 224×224를 빠르게 55×55로 줄입니다.

초보자가 자주 오해하는 것. "AlexNet이 이긴 이유는 더 깊어서"가 아닙니다. 그 전에도 LeCun의 LeNet(1998)처럼 깊은 CNN은 있었습니다. 진짜 차이는 ① 대규모 데이터(ImageNet 1.4M장), ② 대규모 계산(GPU), ③ 과적합을 막는 현대적 기법들(ReLU+드롭아웃+증강)이 동시에 모였다는 점입니다. 이 "데이터·계산·정규화" 세 박자는 그 이후 모든 딥러닝 성공의 공통 공식입니다.

2. VGG (2014) — 단순함의 승리

3×3 작은 필터만 16~19층 깊이로 단조롭게 쌓아 만든 모델. 단순하고 일관된 구조 덕에 지금도 백본으로 자주 쓰입니다.

2.1 왜 3×3 필터만 고집했는가

옥스포드 VGG 그룹(Simonyan·Zisserman)의 통찰은 이것입니다. 3×3 필터 두 개를 겹치면 5×5 한 개와 같은 수용장(receptive field)을 가진다. 수학적으로 확인해 봅시다.

파라미터 수가 28% 적고, 게다가 비선형(ReLU)이 한 번 더 들어가서 표현력이 더 풍부해집니다. 3×3 세 개는 7×7 한 개와 같은 수용장을 갖고, 파라미터는 $27 vs 49$로 절반 가까이 줄어듭니다. "작은 필터를 많이 쌓자"가 VGG의 철학이고, 이 원칙은 이후 대부분의 CNN이 따르게 됩니다.

단점도 분명합니다. VGG-16은 약 138M 파라미터로 AlexNet의 2배가 넘고, 대부분이 마지막 완전연결층(특히 첫 FC층 $7\times 7 \times 512 \to 4096$이 약 1억 개)에 몰려 있습니다. 이 구조적 낭비가 GoogLeNet과 ResNet이 FC를 버리고 전역 평균 풀링(global average pooling)으로 가는 동기가 됩니다.

3. GoogLeNet / Inception (2014) — 넓이의 힘

"필터 크기를 하나로 정하지 말고 1×1, 3×3, 5×5를 동시에 적용해 합치자". Inception 모듈이라는 이 발상으로 깊이와 너비를 동시에 늘렸습니다.

3.1 1×1 합성곱의 마법

Inception의 진짜 주역은 1×1 합성곱입니다. "1×1이 무슨 의미가 있지? 픽셀 하나만 보는데?" 라고 생각하기 쉬운데, 채널 차원에서 보면 이야기가 다릅니다. 입력이 $H\times W \times 256$일 때 1×1 합성곱 64개를 적용하면 출력은 $H\times W \times 64$ — 즉 공간 정보는 그대로 두고 채널을 압축합니다. 이것은 각 픽셀 위치에서 256차원 벡터를 64차원으로 선형 투영하는 것과 정확히 같고, 따라서 채널 간의 의미 있는 조합을 학습합니다.

이 "채널 병목(bottleneck)"을 3×3, 5×5 앞에 끼우면 계산량이 극적으로 줄어듭니다. 예: $28\times 28 \times 192$ 입력에 $5\times 5 \times 32$ 합성곱 → $28\times 28\times 32$ 출력을 직접 하면 곱셈이 약 1.2억 회. 중간에 1×1로 16채널까지 줄이면 약 1200만 회로 10분의 1이 됩니다. 이 트릭 덕분에 GoogLeNet은 VGG보다 12배 적은 파라미터(약 7M)로 더 좋은 성능을 냈습니다.

또 하나, GoogLeNet은 완전연결층을 전역 평균 풀링으로 대체했습니다. 마지막 feature map이 $7\times 7 \times 1024$라면, 각 채널의 49개 값을 평균 내서 1024차원 벡터 하나를 얻고 이걸로 바로 분류합니다. 파라미터가 0개인 데다 공간적으로 robust 합니다.

4. ResNet (2015) — 층을 무한히 쌓는 법

층을 너무 깊이 쌓으면 오히려 학습이 안 됩니다(기울기 소실의 친구인 "성능 열화"). He Kaiming이 제안한 잔차 연결(residual connection)로 이 문제를 깼습니다.

$$ y = F(x) + x $$

입력을 출력에 그대로 더해주면 기울기가 우회로를 통해 흐를 수 있습니다. 이 작은 변화로 152층, 그리고 1000층까지도 학습이 가능해졌습니다.

4.1 "성능 열화"라는 이상한 현상

ResNet 논문의 첫 장에는 충격적인 그래프가 나옵니다. 56층 평면 네트워크(plain net)가 20층보다 훈련 오차부터 더 높습니다. 과적합이라면 훈련 오차는 낮아야 하니, 이건 단순한 최적화 실패입니다. 이론상 56층 네트워크는 20층 네트워크를 "부분적으로" 포함할 수 있습니다(뒤 36층을 항등 함수로 만들면 됨). 그런데 SGD는 그 항등 함수조차 찾지 못했던 겁니다.

He 카이밍의 발상은 기발할 정도로 단순합니다. "항등 함수를 찾기가 그렇게 어렵다면, 기본값을 항등으로 만들어 두고 블록은 차이(residual)만 학습하게 하자." 식으로 쓰면 $y = F(x) + x$이고, $F$를 0으로만 만들면 자동으로 항등이 됩니다. 아무것도 안 하는 게 쉬워진 거죠.

4.2 기울기가 흐르는 고속도로

역전파 관점에서 잔차 연결의 효과를 봅시다. 손실 $L$에 대한 입력 $x$의 기울기는

$$ \frac{\partial L}{\partial x} = \frac{\partial L}{\partial y}\cdot\left(1 + \frac{\partial F}{\partial x}\right) $$

핵심은 괄호 안의 "1"입니다. $F$의 기울기가 0에 가까워져도 1이라는 우회로가 남아 있기 때문에 기울기가 절대 0이 되지 않습니다. 이것이 W8의 기울기 소실을 수백 층 규모에서 우회하는 결정적 장치입니다. 오른쪽 데모에서 층이 깊어질수록 잔차 있음(파랑)은 완만하게만 감소하는 반면, 잔차 없음(주황)은 $0.85^L$로 기하급수적으로 0에 수렴하는 걸 볼 수 있습니다.

4.3 Bottleneck 블록과 깊은 ResNet

ResNet-50 이상에서는 계산량을 줄이기 위해 "bottleneck" 블록을 씁니다. 1×1 → 3×3 → 1×1 의 세 층으로 구성되며, 앞뒤 1×1이 채널을 줄였다가 다시 늘립니다. 이 구조는 Inception의 1×1 트릭과 ResNet의 항등 우회로를 결합한 것입니다.

🎮 인터랙티브: 잔차 연결의 효과

같은 50층 네트워크에서 잔차 연결이 있을 때(파랑)와 없을 때(주황) 입력층에 도달하는 기울기 크기를 비교합니다.

4.5 진화의 한 장 요약

모델연도파라미터Top-5 오차핵심 아이디어
AlexNet2012860M16.4%ReLU, 드롭아웃, GPU
VGG-16201416138M7.3%3×3만, 단조로운 깊이
GoogLeNet2014227M6.7%Inception, 1×1, GAP
ResNet-152201515260M3.6%잔차 연결

ResNet-152가 VGG-16과 파라미터 수는 비슷한데 오차는 절반 이하라는 점에 주목하세요. 모델의 힘은 "얼마나 큰가"가 아니라 "얼마나 잘 설계됐는가"에 더 크게 달려 있습니다.

5. 전이학습 — 거인의 어깨에 올라타기

ImageNet에서 1400만 장으로 학습된 ResNet은 이미 "이미지의 일반적인 특성"을 알고 있습니다. 우리가 가진 데이터가 1000장뿐이어도, 이 사전학습 모델을 가져와서 마지막 층만 갈아끼우면 좋은 결과를 얻을 수 있습니다.

5.1 왜 특성이 "옮겨 가는가"

Yosinski 등(2014)의 유명한 실험은 CNN의 각 층이 학습한 특성을 시각화해 보여 줍니다. 놀라운 사실은 거의 모든 이미지 데이터셋에서 앞쪽 층은 비슷한 것을 학습한다는 것입니다. 1층은 에지와 색깔 얼룩, 2층은 텍스처, 3층은 간단한 모양… 이런 저수준 특성은 고양이 사진이든 의료 영상이든 위성 사진이든 공통으로 필요합니다. 도메인 특유의 차이는 주로 뒤쪽 층에서 나타납니다. 그래서 "앞은 얼리고 뒤만 학습" 하는 전이학습이 작동하는 겁니다.

5.2 두 가지 모드

🎮 인터랙티브: 동결할 층 수 선택

네트워크의 어디서부터 풀어줄지(unfreeze) 정해보세요. 너무 많이 풀면 작은 데이터로 학습이 망가지고, 너무 적게 풀면 새 도메인에 적응하지 못합니다.

경험칙. 새 데이터가 ① 적고 ② 비슷한 도메인이면 특성 추출만, ① 적고 ② 다른 도메인이면 마지막 1~2층 미세조정, ① 많고 ② 비슷하면 전체 미세조정(작은 lr), ① 많고 ② 다르면 처음부터 학습.

5.3 실전에서 자주 빠지는 함정

6. 학습률 스케줄

전이학습에서는 보통 매우 작은 학습률($10^{-5}$~$10^{-4}$)로 시작합니다. 큰 학습률은 사전학습된 가중치를 망가뜨리기 때문입니다. 워밍업(warmup)과 코사인 감쇠를 함께 쓰는 게 표준입니다.

워밍업이란 처음 수백 스텝 동안 lr을 0에서 목표값까지 선형으로 올리는 것입니다. 초기에 큰 lr을 바로 주면 랜덤 초기화된 새 분류기 층이 이상한 방향으로 튀면서 그 뒤쪽의 안정된 층까지 흔들어 버리는데, 워밍업이 이걸 막습니다. 그 후에는 코사인 곡선을 따라 lr을 부드럽게 0으로 줄여 나가며 최종 수렴을 다듬습니다.

자기지도 사전학습의 시대. 2020년 이후에는 ImageNet 라벨 없이도 사전학습을 하는 SimCLR, MoCo, MAE 같은 방법들이 주류가 됐습니다. "라벨이 있는 1000만 장"이 아니라 "라벨 없는 1억 장"으로 더 좋은 표현을 얻을 수 있다는 발견이 비전 전이학습의 두 번째 혁명이었습니다.

7. 코드 예제 (PyTorch)

import torch
import torchvision.models as models
import torch.nn as nn

# 사전학습된 ResNet50 불러오기
model = models.resnet50(weights='IMAGENET1K_V2')

# 모든 파라미터 동결
for p in model.parameters():
    p.requires_grad = False

# 마지막 FC 층만 새 클래스 수에 맞게 교체 (자동으로 학습 가능)
model.fc = nn.Linear(model.fc.in_features, 10)

optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3)

📖 더 깊이 공부하기