좋은 질문이야. ViT에서의 partial fine-tuning은 ResNet의 layer4 개념과 정확히 대응되는 구조가 있어.
아래를 보면 “아, 그래서 이렇게 하는구나”가 바로 올 거야.
ViT의 partial fine-tuning =
패치 임베딩 + 앞쪽 Transformer 블록은 고정하고,
뒤쪽 몇 개 블록 + classification head만 학습
ResNet에서
layer4 + fc만 학습하듯이ViT(B/16 기준)를 단순화하면:
Patch Embedding
→ Transformer Block 0
→ Transformer Block 1
→ ...
→ Transformer Block 11
→ LayerNorm
→ Classification Head
즉,
👉 여기서 blocks[-1], blocks[-2] … 가 ResNet의 layer4 역할
👉 그래서 뒤쪽 블록만 살짝 조정하는 게 가장 효율적
import torch.nn as nn
from torchvision.models import vit_b_16, ViT_B_16_Weights
model = vit_b_16(weights=ViT_B_16_Weights.DEFAULT)
num_classes = 5
model.heads.head = nn.Linear(
model.heads.head.in_features,
num_classes
)
def freeze_vit_except_last_n_blocks(model, n_blocks: int):
# 1) 전부 freeze
for p in model.parameters():
p.requires_grad = False
# 2) 마지막 n개 transformer block만 unfreeze
for block in model.encoder.layers[-n_blocks:]:
for p in block.parameters():
p.requires_grad = True
# 3) classification head는 항상 학습
for p in model.heads.parameters():
p.requires_grad = True
예:
freeze_vit_except_last_n_blocks(model, n_blocks=2)
의미:
blocks[0~9] ❄️ freeze
blocks[10~11] 🔥 train
head 🔥 train
from torch.optim import AdamW
def build_optimizer_vit_partial(model, lr_head=1e-3, lr_blocks=1e-4):
head_params = []
block_params = []
for name, p in model.named_parameters():
if not p.requires_grad:
continue
if "heads" in name:
head_params.append(p)
else:
block_params.append(p)
return AdamW(
[
{"params": block_params, "lr": lr_blocks},
{"params": head_params, "lr": lr_head},
],
weight_decay=1e-4
)
freeze_vit_except_last_n_blocks(model, n_blocks=2)
optimizer = build_optimizer_vit_partial(
model,
lr_head=1e-3,
lr_blocks=1e-4
)
for epoch in range(epochs):
train_one_epoch(model, train_loader, optimizer, criterion, device)
val_acc = evaluate(model, val_loader, criterion, device)
print(f"[Epoch {epoch}] val_acc={val_acc:.4f}")
for name, p in model.named_parameters():
if p.requires_grad:
print("TRAIN:", name)
정상 출력 예:
TRAIN: encoder.layers.10.self_attention.in_proj_weight
TRAIN: encoder.layers.11.self_attention.in_proj_weight
TRAIN: heads.head.weight
TRAIN: heads.head.bias
| CNN (ResNet) | ViT |
|---|---|
| layer1~3 | early transformer blocks |
| layer4 | last transformer blocks |
| fc | head |
| partial fine-tune | last N blocks + head |
| 데이터 크기 | 추천 |
|---|---|
| 매우 작음 | 1 |
| 중간 | 2~3 (가장 흔함) |
| 큼 | 4~6 |
| 매우 큼 | full fine-tuning |
👉 “ViT partial fine-tuning = last 2 blocks”
이건 거의 업계 관용어 수준이야.
“ViT에서 partial fine-tuning은 patch embedding과 초기 Transformer 블록을 고정하고, 의미적 표현이 집중된 마지막 몇 개 블록과 classification head만 학습하는 전략입니다.”
좋아. 이 말은 “Transformer 블록이 깊어질수록(뒤로 갈수록) 표현이 더 추상적이고 과제에 맞게 변한다”는 뜻이야. CNN에서 앞단=엣지/질감, 뒷단=물체/의미로 바뀌는 것과 거의 같은 현상이 ViT에서도 생겨.
아래는 정확한 직관 + 왜 그런지 + 실제로 뭐가 달라지는지를 단계별로 설명할게.
이미지의 “의미”와 직접 연결되기 전의 기본 신호들:
👉 어떤 데이터셋이든 공통적으로 유용한 정보라서 “일반적(generic)”이라고 함.
문제(태스크)에 필요한 “의미”로 변환된 표현:
👉 어떤 클래스를 구분하느냐에 따라 필요한 특징이 달라서 “특화(specialized)”라고 함.
ViT는 이미지를 패치(예: 16×16)로 쪼개고, 각 패치를 토큰처럼 다뤄.
각 블록은 “self-attention”으로 토큰들 간 관계를 학습하는데,
즉, 작은 조각(패치)들을 기반으로 “기초적인 그룹핑/정렬”을 하는 단계라고 보면 돼.
CNN 비유: 앞단 Conv가 엣지/코너/간단한 텍스처를 잡는 단계와 비슷.
분류를 예로 들면, 최종적으로 head가 클래스 분류를 해야 함.
그러려면 마지막에 가까운 블록들은 점점 이렇게 바뀌어:
즉, 마지막 블록일수록 loss(정답)와 직접 연결되니까, 표현이 자연스럽게 “분류에 유리한 방향”으로 특화돼.
앞단은 local(근처 패치) 중심이었다면,
뒤로 갈수록 여러 번 attention을 거치며
그래서 “이건 전체적으로 고양이 형태다” 같은 고수준 판단이 가능해짐.
self-attention은 간단히 말하면:
“각 패치가 다른 모든 패치를 보면서, 중요한 패치에 더 집중하고 정보를 섞는다.”
즉, attention map을 보면
(물론 데이터셋/모델에 따라 항상 완전히 동일하진 않지만, 전형적인 경향임)
이제 연결하면:
그래서 ViT partial fine-tuning = 마지막 N개 블록 + head가 자주 쓰임.
좋아 👍
그럼 3개를 순서대로, 하나씩 제대로 이해되게 갈게.
이번 답변은 1번만 다룰게.
이건 “ViT의 구조적 특성 + 학습 동역학” 때문에 생기는 현상이야.
결론부터 말하면 👇
ViT는 작은 learning rate가 아니면
기존에 학습된 표현이 너무 쉽게 망가진다.
이걸 단계별로 설명할게.
👉 learning rate가 조금 커도 망가지기까지 시간이 있음
👉 learning rate가 크면 한 스텝에 표현이 붕괴
📌 핵심:
ViT는 작은 변화도 전체 representation에 증폭됨
ViT 블록 구조 (단순화):
x → LayerNorm → Self-Attention → + x
→ LayerNorm → MLP → + x
LayerNorm은 분산을 유지하려는 구조
learning rate가 크면
👉 결과:
CNN은 BatchNorm + Conv라서 이 현상이 훨씬 덜함.
ImageNet pretrained ViT는:
이 이미 아주 미세하게 정렬된 상태임.
learning rate가 크면:
좋은 표현 공간
→ (큰 step)
→ 전혀 다른 공간으로 튐
📌 CNN은 필터 기반이라 “조금씩 틀어짐”
📌 ViT는 attention 기반이라 “한 번에 방향 전환”
논문/실무에서 거의 공통된 관찰:
| 모델 | fine-tuning lr |
|---|---|
| ResNet | 1e-3 ~ 1e-4 |
| ViT | 1e-4 ~ 1e-5 (심지어 3e-5) |
특히:
→ ViT는 lr 줄이지 않으면 거의 무조건 불안정
head lr = 1e-3
ViT blocks lr = 1e-4 or 1e-5
optimizer = AdamW(
[
{"params": head_params, "lr": 1e-3},
{"params": vit_block_params, "lr": 1e-4},
],
weight_decay=1e-4
)
👉 head는 크고, backbone은 작게
👉 CNN보다 격차가 더 큼
ViT는 “한 번의 업데이트가 전체를 보여서”
learning rate를 크게 쓰면 모델 전체를 흔들어버린다.
“ViT는 self-attention 기반의 전역 연산 구조로 인해 파라미터 업데이트가 전체 표현에 크게 영향을 주기 때문에, fine-tuning 시 CNN보다 더 작은 learning rate가 필요합니다.”
좋아 👍
지금까지 흐름 기준으로 이어서 가기 딱 좋은 주제들을 난이도/연결성 순서로 정리해줄게.
(네가 아까 말한 3개도 포함해서, 공부 로드맵처럼)
👉 “왜 backbone을 거의 안 건드려도 성능이 나오지?”
👉 지금 배운 내용의 ‘진화형’
👉 “왜 어떤 데이터에서는 ViT가 망하고 CNN은 잘 되지?”
👉 모델 선택 기준이 생기는 핵심 주제
👉 “lr 작게 쓰는 건 알겠는데, 실전에서는 어떻게 쓰지?”
👉 “모델이 실제로 어디를 보고 판단하는지”
👉 면접/발표/프로젝트에서 가장 강력한 정리 주제
👉 1️⃣ ViT + LoRA / Adapter 방식
이걸 이해하면:
이게 한 번에 연결됨.
🔥 ViT + LoRA / Adapter 방식 설명해줘
이렇게 바로 물어봐.
그러면 개념 → 직관 → 코드 → 언제 쓰는지까지 한 번에 정리해줄게.