이번 차시에서 진짜 중요한 것
코드 아래 표를 보고 “한 줄 의미”를 먼저 이해한 뒤, ‘왜 핵심인지’ 문단을 읽으면 됩니다.
이 리포트는 “코드만 나열”이 아니라, **코드 한 줄마다 (1) 무엇을 하는지 (2) 옵션이 뭔지 (3) 왜 핵심인지**를 붙여서 초심자도 따라가게 만든 버전이다. 14차시의 핵심은 4가지다. 1) Freeze / Partial / Full: 어디까지 학습할지 결정 2) 전이학습 vs 파인튜닝: FC만 학습인지, backbone도 학습인지 3) 단계적 학습률: FC를 먼저 큰 LR로, backbone은 작은 LR로 4) 증강 강도 비교: 약/중/강 증강에서 성능이 어떻게 달라지는지
전이학습 vs 파인튜닝 (코드로 구분)
pretrained 로드 → FC 교체 → (대개 Freeze로 시작)
from torchvision import models
import torch.nn as nn
net = models.resnet18(pretrained=True)
fc_in = net.fc.in_features
n_output = 10
net.fc = nn.Linear(fc_in, n_output)
| 코드 한 줄 | 의미(초심자용) |
|---|---|
net = models.resnet18(pretrained=True) | ImageNet 등으로 미리 학습된 ResNet-18 가중치를 불러온다(=전이학습 시작). |
fc_in = net.fc.in_features | 기존 FC가 받는 입력 차원을 확인한다(ResNet-18은 보통 512). |
net.fc = nn.Linear(fc_in, n_output) | 우리 데이터의 클래스 수(n_output)에 맞게 ‘마지막 분류기(FC)’를 교체한다(필수). |
왜 핵심인가? - backbone(특징추출부)은 이미 ‘일반적인 시각 특징(엣지/패턴/형태)’을 잘 배움 - 데이터가 적을 때 처음부터 학습보다 훨씬 빠르고 안정적 - 대신 “FC 교체 + 학습 범위(Freeze/Partial/Full) 선택”이 성능을 좌우함
Freeze / Partial / Full (어디까지 학습?)
가장 중요한 실험 축: 업데이트할 파라미터의 범위
# (1) Freeze: backbone 고정, FC만 학습
for p in net.parameters():
p.requires_grad = False
for p in net.fc.parameters():
p.requires_grad = True
# (2) Partial: 마지막 블록 + FC 학습 (예: ResNet layer4)
for p in net.layer4.parameters():
p.requires_grad = True
# (3) Full: 전체 학습
for p in net.parameters():
p.requires_grad = True
| 코드 한 줄 | 의미(초심자용) |
|---|---|
for p in net.parameters(): p.requires_grad = False | 모든 파라미터 업데이트를 막는다(=backbone 포함 전부 고정). |
for p in net.fc.parameters(): p.requires_grad = True | FC만 학습 가능하게 풀어준다(전이학습 기본 형태). |
for p in net.layer4.parameters(): p.requires_grad = True | 마지막 블록(layer4)만 추가로 학습(=partial fine-tuning). |
for p in net.parameters(): p.requires_grad = True | 전체를 학습 가능하게 만든다(=full fine-tuning). |
왜 핵심인가? - Freeze: 가장 안정적(데이터 적을 때 과적합↓), 빠름 - Partial: 성능/안정성 균형이 좋아 가장 많이 씀(고수준 특징만 우리 데이터에 맞게 조정) - Full: 표현력은 최대지만 LR 튜닝이 어렵고 과적합/불안정 가능
단계적 학습률 (왜 2단계로 나누나?)
FC는 빠르게, backbone은 조심스럽게
import torch.optim as optim
# Phase 1) Freeze 상태에서 FC만 빠르게 학습 (LR 크게)
for p in net.parameters(): p.requires_grad = False
for p in net.fc.parameters(): p.requires_grad = True
optimizer = optim.SGD(
filter(lambda p: p.requires_grad, net.parameters()),
lr=1e-2, momentum=0.9, weight_decay=1e-4
)
# Phase 2) Partial 또는 Full로 확장 (LR 작게)
for p in net.layer4.parameters(): p.requires_grad = True # partial 예시
optimizer = optim.SGD(
filter(lambda p: p.requires_grad, net.parameters()),
lr=1e-3, momentum=0.9, weight_decay=1e-4
)
| 코드 한 줄 | 의미(초심자용) |
|---|---|
filter(lambda p: p.requires_grad, net.parameters()) | 학습 가능한 파라미터만 optimizer에 넣는다(안 넣으면 freeze가 의미 없어짐). |
lr=1e-2 (Phase1) | FC만 학습하므로 비교적 큰 LR도 안정적으로 가능(빠르게 적응). |
lr=1e-3 (Phase2) | backbone을 건드리면 작은 LR 필수(기존 지식 파괴 방지). |
왜 핵심인가? - pretrained 가중치는 이미 좋은 위치에 있음 → 큰 LR로 건드리면 ‘기존 지식이 망가짐(망각)’ - 그래서 1단계(FC만)에서 빠르게 적응시키고, 2단계(backbone 일부/전체)에서 작은 LR로 조심스럽게 미세조정하는 게 안전하고 성능도 잘 나옴
증강 강도(약/중/강)와 성능 비교
강할수록 좋다 X → “균형점”이 핵심
from torchvision import transforms
aug_weak = transforms.Compose([
transforms.Resize((224,224)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)),
])
aug_medium = transforms.Compose([
transforms.Resize((256,256)),
transforms.RandomResizedCrop((224,224), scale=(0.8,1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(10),
transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)),
])
aug_strong = transforms.Compose([
transforms.Resize((256,256)),
transforms.RandomResizedCrop((224,224), scale=(0.6,1.0)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomRotation(15),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05),
transforms.ToTensor(),
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)),
transforms.RandomErasing(p=0.25, scale=(0.02, 0.15)),
])
| 코드 한 줄 | 의미(초심자용) |
|---|---|
RandomHorizontalFlip(p=0.5) | 50% 확률로 좌우 반전(가장 기본 증강). |
RandomResizedCrop(..., scale=(0.8,1.0)) | 원본의 80~100% 영역을 랜덤 크롭 → medium 증강(너무 과하지 않게). |
ColorJitter(...) | 밝기/대비/채도를 흔들어 색 변화에 강해지게 함(강증강 요소). |
RandomErasing(p=0.25, ...) | 이미지 일부를 지워 가림(occlusion)에 강하게 하지만 과하면 학습이 어려워짐). |
왜 핵심인가? - 약한 증강: 학습은 쉽지만 과적합 위험 - 강한 증강: 일반화에 도움될 수 있지만, 과하면 ‘데이터가 너무 달라져’ 성능이 떨어질 수도 있음 - 그래서 약/중/강을 나눠서 “성능이 가장 좋은 균형점”을 찾는 것이 실험의 핵심
학습/평가 루프에서 ‘무엇이 계산되는가’
여기 흐름을 정확히 알면 실수가 확 줄어듭니다.
running_loss = 0.0
total = 0
net.train()
for images, labels in train_loader:
outputs = net(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_loss += loss.item() * images.size(0)
total += labels.size(0)
epoch_loss = running_loss / total
net.eval()
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
outputs = net(images)
predicted = torch.argmax(outputs, dim=1)
correct += (predicted == labels).sum().item()
total += labels.size(0)
acc = 100 * correct / total
| 코드 한 줄 | 의미(초심자용) |
|---|---|
net.train() | 드롭아웃/배치정규화가 ‘학습 모드’로 동작하게 함. |
optimizer.zero_grad() | 이전 배치의 gradient를 초기화(안 하면 누적돼서 학습이 망가짐). |
loss.backward() | 오차를 기준으로 gradient 계산(역전파). |
optimizer.step() | 계산된 gradient로 파라미터 업데이트. |
running_loss += loss.item() * images.size(0) | 배치 평균 loss × 배치 샘플 수로 누적(마지막 배치 크기가 달라도 공정한 평균). |
with torch.no_grad() | 평가에서는 gradient 계산을 꺼서 더 빠르고 메모리 절약. |
predicted = torch.argmax(outputs, dim=1) | 각 샘플에서 가장 큰 점수의 클래스 인덱스를 예측값으로 선택. |
왜 핵심인가? - loss/acc가 ‘어디서 계산되는지’ 정확히 이해하면 디버깅이 쉬워짐 - running_loss를 배치 크기로 가중 평균내는 방식은 매우 자주 쓰이는 정석
실험 결과 정리 템플릿
“무엇을 바꿨는지”와 “결과(Acc)”를 표로 묶어라.
| 실험명 | Train Mode | Aug | Phase1 LR | Phase2 LR | Best Acc | 해석(한 문장) |
|---|---|---|---|---|---|---|
| freeze + weak | freeze | weak | 1e-2 | - | __ | baseline(안정/빠름) |
| partial + medium | partial | medium | 1e-2 | 1e-3 | __ | 권장 조합 후보 |
| full + medium | full | medium | 1e-2 | 1e-3 | __ | 튜닝 성공 시 최고점 |
| partial + strong | partial | strong | 1e-2 | 1e-3 | __ | 과증강이면 성능↓ 가능 |
각 실험에서 Best Acc를 채우고, 해석(한 문장)만 붙이면 보고서가 완성됩니다.