🧩 전이학습이란?
전이 학습(Transfer Learning)은 대규모 데이터셋에서 이미 학습된 모델을 새로운 문제에 재사용하는 방법이다.
일반적으로 CNN(합성곱 신경망, Convolutional Neural Network)을 처음부터 학습하려면 많은 데이터와 시간이 필요하다. 하지만 ImageNet처럼 큰 데이터셋으로 미리 학습된 모델은 이미 에지, 색상 변화, 질감, 도형이나 형태 등의 시각적인 특징을 학습했다. CIFAR-10처럼 비교적 작은 데이터셋에 대해 학습할 때 이러한 사전 학습 모델을 활용하면 더 빠르고 안정적으로 학습할 수 있다.
| 방법 | Backbone 가중치 | 분류기(Classifier) | 특징 |
| Linear Probing | 동결 | 학습 | 빠르고 적은 데이터에 유리 |
| Fine-tuning | 학습 | 학습 | 성능 향상 가능, 학습 시간 증가 |
| Scratch Training | 랜덤 초기화 후 전체 학습 | 학습 | 많은 데이터와 긴 학습 필요 |
Linear Probing
사전 학습된 특징 추출 부분을 동결하고 마지막 분류층만 학습한다.

- 학습이 빠름
- 필요한 GPU 메모리가 적음
- 데이터가 적을 때 안정적
- 새로운 데이터와 ImageNet의 차이가 크면 성능에 한계가 있음
Fine-tuning
사전 학습된 모델의 일부 또는 전체를 작은 학습률로 추가 학습한다.

- 새로운 데이터에 더 잘 적응
- 일반적으로 Linear Probing보다 높은 성능
- 학습률이 너무 크면 사전 학습된 지식을 잃을 수 있음
🧩 고양이/강아지 이미지 분류기 실습
실행 환경 세팅
import copy
import random
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Subset
from torchvision import transforms
from torchvision.datasets import OxfordIIITPet
from torchvision.models import resnet18, ResNet18_Weights
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(SEED)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("사용 장치:", device)
여담으로 GPU 사용할 수 있으면 cuda를 출력하라고 하길래 cuda가 뭔가 했더니, CUDA(Compute Unified Device Architecture)는 NVIDIA가 만든 GPU 병렬 컴퓨팅 플랫폼 및 프로그래밍 모델이라고 한다.
데이터 전처리와 데이터 증강
# ImageNet 정규화
IMAGE_SIZE = 224
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
# 학습용 데이터 증강
train_transform = transforms.Compose([
transforms.RandomResizedCrop(
IMAGE_SIZE,
scale=(0.7, 1.0)
),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ColorJitter(
brightness=0.2,
contrast=0.2,
saturation=0.2
),
transforms.ToTensor(),
transforms.Normalize(
mean=IMAGENET_MEAN,
std=IMAGENET_STD
)
])
eval_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(IMAGE_SIZE),
transforms.ToTensor(),
transforms.Normalize(
mean=IMAGENET_MEAN,
std=IMAGENET_STD
)
])
- 학습용 이미지에는 무작위 변환을 적용해 모델이 특정 위치, 방향, 조명 등에 과도하게 의존하는 것을 줄인다.
- 검증/테스트용 데이터에는 무작위 변환을 사용하지 않는다.
데이터셋 준비
Oxford-IIIT Pet 데이터셋의 trainval 데이터를 학습용과 검증용으로 나누고, 별도의 test 데이터를 최종 평가에 사용한다.
DATA_ROOT = "./data"
# 데이터 길이와 분할 인덱스를 만들기 위한 원본 데이터셋
base_trainval = OxfordIIITPet(
root=DATA_ROOT,
split="trainval",
target_types="binary-category",
download=True
)
num_total = len(base_trainval)
num_train = int(num_total * 0.8)
generator = torch.Generator().manual_seed(SEED)
indices = torch.randperm(num_total, generator=generator).tolist()
train_indices = indices[:num_train]
val_indices = indices[num_train:]
print("전체 trainval:", num_total)
print("학습 데이터:", len(train_indices))
print("검증 데이터:", len(val_indices))

학습 데이터와 검증 데이터에 서로 다른 변환을 적용하려면 데이터셋 객체를 각각 생성해야 한다.
train_full = OxfordIIITPet(
root=DATA_ROOT,
split="trainval",
target_types="binary-category",
transform=train_transform,
download=False
)
val_full = OxfordIIITPet(
root=DATA_ROOT,
split="trainval",
target_types="binary-category",
transform=eval_transform,
download=False
)
test_dataset = OxfordIIITPet(
root=DATA_ROOT,
split="test",
target_types="binary-category",
transform=eval_transform,
download=True
)
train_dataset = Subset(train_full, train_indices)
val_dataset = Subset(val_full, val_indices)
print("학습 데이터:", len(train_dataset))
print("검증 데이터:", len(val_dataset))
print("테스트 데이터:", len(test_dataset))

binary-category의 레이블은 다음과 같다.
class_names = ["cat", "dog"] # 고양이 0 / 강아지 1
DataLoader 만들기
BATCH_SIZE = 32
NUM_WORKERS = 2 # 안되면 0으로
train_loader = DataLoader(
train_dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=NUM_WORKERS,
pin_memory=torch.cuda.is_available()
)
val_loader = DataLoader(
val_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
num_workers=NUM_WORKERS,
pin_memory=torch.cuda.is_available()
)
test_loader = DataLoader(
test_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
num_workers=NUM_WORKERS,
pin_memory=torch.cuda.is_available()
)
images, labels = next(iter(train_loader))
print("이미지 배치 크기:", images.shape)
print("레이블 배치 크기:", labels.shape)
print("레이블 예시:", labels[:10])

이미지 시각화
정규화된 이미지를 다시 원래 범위에 가깝게 복원해 출력한다.
def denormalize(image_tensor):
mean = torch.tensor(IMAGENET_MEAN).view(3, 1, 1)
std = torch.tensor(IMAGENET_STD).view(3, 1, 1)
image_tensor = image_tensor.cpu() * std + mean
return image_tensor.clamp(0, 1)
images, labels = next(iter(train_loader))
plt.figure(figsize=(12, 6))
for i in range(8):
image = denormalize(images[i])
image = image.permute(1, 2, 0)
plt.subplot(2, 4, i + 1)
plt.imshow(image)
plt.title(class_names[labels[i].item()])
plt.axis("off")
plt.tight_layout()
plt.show()

공통 학습 함수
def resnet_forward(model, images):
return model(images)
def vit_forward(model, images):
return model(pixel_values=images).logits
- 1 Epoch 실행 함수
def run_epoch(
model,
data_loader,
criterion,
optimizer=None,
forward_fn=resnet_forward,
freeze_batchnorm=False
):
is_training = optimizer is not None
if is_training:
model.train()
# Linear Probing에서는 BatchNorm 통계도 고정
if freeze_batchnorm:
for module in model.modules():
if isinstance(module, nn.BatchNorm2d):
module.eval()
else:
model.eval()
total_loss = 0.0
total_correct = 0
total_samples = 0
for images, labels in data_loader:
images = images.to(device)
labels = labels.to(device)
if is_training:
optimizer.zero_grad(set_to_none=True)
with torch.set_grad_enabled(is_training):
logits = forward_fn(model, images)
loss = criterion(logits, labels)
if is_training:
loss.backward()
optimizer.step()
predictions = logits.argmax(dim=1)
total_loss += loss.item() * images.size(0)
total_correct += (predictions == labels).sum().item()
total_samples += images.size(0)
average_loss = total_loss / total_samples
accuracy = total_correct / total_samples
return average_loss, accuracy
- 전체 학습 함수: 검증 정확도가 가장 높은 모델 상태를 자동으로 저장한다.
def fit(
model,
train_loader,
val_loader,
criterion,
optimizer,
epochs,
scheduler=None,
forward_fn=resnet_forward,
freeze_batchnorm=False
):
history = {
"train_loss": [],
"train_acc": [],
"val_loss": [],
"val_acc": []
}
best_val_acc = 0.0
best_state = copy.deepcopy(model.state_dict())
for epoch in range(epochs):
train_loss, train_acc = run_epoch(
model=model,
data_loader=train_loader,
criterion=criterion,
optimizer=optimizer,
forward_fn=forward_fn,
freeze_batchnorm=freeze_batchnorm
)
val_loss, val_acc = run_epoch(
model=model,
data_loader=val_loader,
criterion=criterion,
optimizer=None,
forward_fn=forward_fn
)
if scheduler is not None:
scheduler.step()
history["train_loss"].append(train_loss)
history["train_acc"].append(train_acc)
history["val_loss"].append(val_loss)
history["val_acc"].append(val_acc)
if val_acc > best_val_acc:
best_val_acc = val_acc
best_state = copy.deepcopy(model.state_dict())
current_lr = optimizer.param_groups[0]["lr"]
print(
f"Epoch [{epoch + 1:02d}/{epochs:02d}] "
f"LR: {current_lr:.6f} | "
f"Train Loss: {train_loss:.4f}, "
f"Train Acc: {train_acc * 100:.2f}% | "
f"Val Loss: {val_loss:.4f}, "
f"Val Acc: {val_acc * 100:.2f}%"
)
model.load_state_dict(best_state)
print(f"\nBest Validation Accuracy: {best_val_acc * 100:.2f}%")
return history
- 학습 결과 그래프
def plot_history(history, title):
epochs = range(1, len(history["train_loss"]) + 1)
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs, history["train_loss"], label="Train")
plt.plot(epochs, history["val_loss"], label="Validation")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.title(f"{title} - Loss")
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(
epochs,
np.array(history["train_acc"]) * 100,
label="Train"
)
plt.plot(
epochs,
np.array(history["val_acc"]) * 100,
label="Validation"
)
plt.xlabel("Epoch")
plt.ylabel("Accuracy (%)")
plt.title(f"{title} - Accuracy")
plt.legend()
plt.tight_layout()
plt.show()
Linear Probing
- 사전 학습된 ResNet-18 불러오기
weights = ResNet18_Weights.DEFAULT
linear_model = resnet18(weights=weights)
# 기존 ResNet-18은 ImageNet의 1,000개 클래스를 분류하도록 구성됨
print(linear_model.fc)
# 고양이와 강아지 두 클래스만 출력하도록 마지막 분류층을 교체
num_features = linear_model.fc.in_features
linear_model.fc = nn.Linear(num_features, 2)
print(linear_model.fc)

- Backbone 동결
# 전체 파라미터 동결
for parameter in linear_model.parameters():
parameter.requires_grad = False
# 새로 만든 분류층만 학습 가능하게 설정
for parameter in linear_model.fc.parameters():
parameter.requires_grad = True
# 학습 가능한 파라미터 수 확인
def count_parameters(model):
total = sum(p.numel() for p in model.parameters())
trainable = sum(
p.numel() for p in model.parameters()
if p.requires_grad
)
print(f"전체 파라미터: {total:,}")
print(f"학습 파라미터: {trainable:,}")
print(f"학습 비율: {trainable / total * 100:.2f}%")
count_parameters(linear_model)
# 모델을 device(GPU or CPU)로 이동
linear_model = linear_model.to(device)

- Linear Probing 학습
criterion = nn.CrossEntropyLoss()
linear_optimizer = torch.optim.AdamW(
filter(
lambda parameter: parameter.requires_grad,
linear_model.parameters()
),
lr=1e-3, # 분류층은 무작위로 초기화되었으므로 비교적 큰 학습률을 사용
weight_decay=1e-4
)
LINEAR_EPOCHS = 5
linear_history = fit(
model=linear_model,
train_loader=train_loader,
val_loader=val_loader,
criterion=criterion,
optimizer=linear_optimizer,
epochs=LINEAR_EPOCHS,
forward_fn=resnet_forward,
freeze_batchnorm=True
)
plot_history(linear_history, "ResNet-18 Linear Probing")
# 테스트 데이터에서 평가
linear_test_loss, linear_test_acc = run_epoch(
model=linear_model,
data_loader=test_loader,
criterion=criterion,
optimizer=None,
forward_fn=resnet_forward
)
print(f"Linear Probing Test Loss: {linear_test_loss:.4f}")
print(f"Linear Probing Test Accuracy: {linear_test_acc * 100:.2f}%")



Fine-tuning
Linear Probing이 끝난 모델의 전체 파라미터를 다시 학습 가능하게 설정한다.
for parameter in linear_model.parameters():
parameter.requires_grad = True
count_parameters(linear_model)
backbone_parameters = []
classifier_parameters = []
for name, parameter in linear_model.named_parameters():
if name.startswith("fc."):
classifier_parameters.append(parameter)
else:
backbone_parameters.append(parameter)
# Fine-tuning에서는 사전 학습된 가중치가 크게 변하지 않도록 작은 학습률을 사용함
finetune_optimizer = torch.optim.AdamW(
[
{
"params": backbone_parameters,
"lr": 1e-5
},
{
"params": classifier_parameters,
"lr": 1e-4
}
],
weight_decay=1e-4
)

- 학습률 스케줄러: CosineAnnealingLR은 학습이 진행될수록 학습률을 코사인 곡선 형태로 감소시킨다.
FINETUNE_EPOCHS = 5
finetune_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
finetune_optimizer,
T_max=FINETUNE_EPOCHS
)
finetune_history = fit(
model=linear_model,
train_loader=train_loader,
val_loader=val_loader,
criterion=criterion,
optimizer=finetune_optimizer,
epochs=FINETUNE_EPOCHS,
scheduler=finetune_scheduler,
forward_fn=resnet_forward,
freeze_batchnorm=False
)
plot_history(finetune_history, "ResNet-18 Fine-tuning")
# 테스트 데이터로 평가
finetune_test_loss, finetune_test_acc = run_epoch(
model=linear_model,
data_loader=test_loader,
criterion=criterion,
optimizer=None,
forward_fn=resnet_forward
)
print(f"Fine-tuning Test Loss: {finetune_test_loss:.4f}")
print(f"Fine-tuning Test Accuracy: {finetune_test_acc * 100:.2f}%")



Scratch 모델과 비교하기
전이 학습의 효과를 확인하려면 사전 학습을 사용하지 않은 ResNet-18과 비교하면 다. 원래 Scratch 학습은 더 많은 에포크가 필요하지만, 실습 시간을 줄이기 위해 여기서는 10에포크만 사용한다.
scratch_model = resnet18(weights=None)
num_features = scratch_model.fc.in_features
scratch_model.fc = nn.Linear(num_features, 2)
scratch_model = scratch_model.to(device)
count_parameters(scratch_model)
SCRATCH_EPOCHS = 10
scratch_optimizer = torch.optim.AdamW(
scratch_model.parameters(),
lr=3e-4,
weight_decay=1e-4
)
scratch_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
scratch_optimizer,
T_max=SCRATCH_EPOCHS
)
scratch_history = fit(
model=scratch_model,
train_loader=train_loader,
val_loader=val_loader,
criterion=criterion,
optimizer=scratch_optimizer,
epochs=SCRATCH_EPOCHS,
scheduler=scratch_scheduler,
forward_fn=resnet_forward
)
plot_history(scratch_history, "ResNet-18 Scratch")
scratch_test_loss, scratch_test_acc = run_epoch(
model=scratch_model,
data_loader=test_loader,
criterion=criterion,
optimizer=None,
forward_fn=resnet_forward
)
print(f"Scratch Test Accuracy: {scratch_test_acc * 100:.2f}%")



모델 성능 정량 비교
results = {
"Scratch": scratch_test_acc * 100,
"Linear Probing": linear_test_acc * 100,
"Fine-tuning": finetune_test_acc * 100
}
for model_name, accuracy in results.items():
print(f"{model_name:15s}: {accuracy:.2f}%")
# 그래프 시각화
plt.figure(figsize=(8, 5))
bars = plt.bar(
results.keys(),
results.values(),
color=["gray", "royalblue", "orange"]
)
plt.ylabel("Test Accuracy (%)")
plt.title("Transfer Learning Comparison")
plt.ylim(0, 110)
for bar, accuracy in zip(bars, results.values()):
plt.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + 1,
f"{accuracy:.2f}%",
ha="center"
)
plt.tight_layout()
plt.show()


- Scratch: 87.74%
- 초반 학습 속도가 느림
- 데이터가 적으면 과적합되기 쉬움
- Linear Probing: 98.88%
- 적은 에포크로 빠르게 높은 정확도에 도달
- ImageNet 특징이 고양이/강아지 분류에 유용하다는 의미
- Fine-tuning: 99.26%
- Backbone이 고양이/강아지 데이터에 적응
- Linear Probing보다 성능이 추가로 향상될 가능성이 높음
전이 학습의 개선 정도는 다음과 같이 계산할 수 있다.
transfer_gain = linear_test_acc - scratch_test_acc
finetune_gain = finetune_test_acc - linear_test_acc
print(
"Scratch 대비 Linear Probing 향상:",
f"{transfer_gain * 100:.2f}%p"
)
print(
"Linear Probing 대비 Fine-tuning 향상:",
f"{finetune_gain * 100:.2f}%p"
)

혼동 행렬 확인
고양이와 강아지 중 어떤 클래스를 더 자주 틀리는 지 확인해보자.
def get_predictions(model, data_loader, forward_fn=resnet_forward):
model.eval()
all_labels = []
all_predictions = []
with torch.no_grad():
for images, labels in data_loader:
images = images.to(device)
logits = forward_fn(model, images)
predictions = logits.argmax(dim=1).cpu()
all_labels.extend(labels.numpy())
all_predictions.extend(predictions.numpy())
return np.array(all_labels), np.array(all_predictions)
true_labels, predicted_labels = get_predictions(
linear_model,
test_loader,
forward_fn=resnet_forward
)
cm = confusion_matrix(true_labels, predicted_labels)
display = ConfusionMatrixDisplay(
confusion_matrix=cm,
display_labels=class_names
)
display.plot(cmap="Blues")
plt.title("Fine-tuned ResNet-18 Confusion Matrix")
plt.show()

from sklearn.metrics import f1_score, classification_report
macro_f1 = f1_score(true_labels, predicted_labels, average='macro')
print(f"Macro F1-Score: {macro_f1:.4f}")
print("\n[ Classification Report ]")
print(classification_report(true_labels, predicted_labels, target_names=class_names))

예측 결과 시각화
def show_predictions(
model,
dataset,
num_images=8,
forward_fn=resnet_forward
):
model.eval()
plt.figure(figsize=(12, 6))
for i in range(num_images):
image, label = dataset[i]
input_tensor = image.unsqueeze(0).to(device)
with torch.no_grad():
logits = forward_fn(model, input_tensor)
probability = torch.softmax(logits, dim=1)
prediction = probability.argmax(dim=1).item()
confidence = probability[0, prediction].item()
display_image = denormalize(image).permute(1, 2, 0)
color = "green" if prediction == label else "red"
plt.subplot(2, 4, i + 1)
plt.imshow(display_image)
plt.title(
f"True: {class_names[label]}\n"
f"Pred: {class_names[prediction]} "
f"({confidence * 100:.1f}%)",
color=color
)
plt.axis("off")
plt.tight_layout()
plt.show()
show_predictions(
model=linear_model,
dataset=test_dataset,
num_images=8
)

모델 저장 및 불러오기
# 모델 저장하기
torch.save(
linear_model.state_dict(),
"cat_dog_resnet18_finetuned.pth"
)
# 모델 불러오기
loaded_model = resnet18(weights=None)
num_features = loaded_model.fc.in_features
loaded_model.fc = nn.Linear(num_features, 2)
loaded_model.load_state_dict(
torch.load(
"cat_dog_resnet18_finetuned.pth",
map_location=device
)
)
loaded_model = loaded_model.to(device)
loaded_model.eval()
print("모델 불러오기 완료")
실제 이미지 테스트
이렇게 만든 모델을 검증/테스트셋이 아닌 실제 사진으로 테스트해보자.
- 모델 불러오기
import torch
import torch.nn as nn
from PIL import Image, ImageOps
from torchvision import transforms
from torchvision.models import resnet18
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
class_names = ["cat", "dog"]
# 학습할 때와 동일한 모델 구조 생성
model = resnet18(weights=None)
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, 2)
# 저장한 Fine-tuning 가중치 불러오기
model.load_state_dict(
torch.load(
"cat_dog_resnet18_finetuned.pth",
map_location=device
)
)
model = model.to(device)
# 추론 모드 설정
model.eval()
print("모델 불러오기 완료")
- 이미지 전처리 정의: 추론할 때는 학습 과정의 무작위 데이터 증강이 아니라 검증/테스트에서 사용한 전처리를 적용해야 한다.
IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]
predict_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=IMAGENET_MEAN,
std=IMAGENET_STD
)
])
- 이미지 분류하기
def predict_image(image_path, model):
# 이미지 불러오기
image = Image.open(image_path)
# 스마트폰 사진 등의 회전 정보를 적용
image = ImageOps.exif_transpose(image)
# RGB 이미지로 변환
image = image.convert("RGB")
# [3, 224, 224]
input_tensor = predict_transform(image)
# 배치 차원 추가: [1, 3, 224, 224]
input_batch = input_tensor.unsqueeze(0).to(device)
model.eval()
with torch.no_grad():
logits = model(input_batch)
# 출력값을 클래스별 확률로 변환
probabilities = torch.softmax(logits, dim=1)
predicted_index = probabilities.argmax(dim=1).item()
confidence = probabilities[0, predicted_index].item()
result = {
"prediction": class_names[predicted_index],
"confidence": confidence,
"cat_probability": probabilities[0, 0].item(),
"dog_probability": probabilities[0, 1].item()
}
return image, result
image, result = predict_image(
image_path="munsik.jpg",
model=model
)
print("예측 결과:", result["prediction"])
print(f"신뢰도: {result['confidence'] * 100:.2f}%")
print(f"고양이 확률: {result['cat_probability'] * 100:.2f}%")
print(f"강아지 확률: {result['dog_probability'] * 100:.2f}%")
import matplotlib.pyplot as plt
korean_name = {
"cat": "고양이",
"dog": "강아지"
}
plt.figure(figsize=(6, 6))
plt.imshow(image)
plt.title(
f"Prediction: {result['prediction']}\n"
f"Confidence: {result['confidence'] * 100:.2f}%"
)
plt.axis("off")
plt.show()
print(
f"이 이미지는 {korean_name[result['prediction']]}로 예측되었습니다. "
f"신뢰도는 {result['confidence'] * 100:.2f}%입니다."
)


테스트 결과 모델이 문식이 사진을 강아지로 예측하는 것을 성공한 것을 확인할 수 있다.
'CS > Python' 카테고리의 다른 글
| 파이썬으로 ViT 찍먹하기 (0) | 2026.08.18 |
|---|---|
| 파이썬으로 Attention 찍먹하기 (0) | 2026.08.12 |
| 파이썬으로 토큰화+임베딩 찍먹하기 (0) | 2026.08.12 |
| 파이썬으로 MLP 찍먹하기 (0) | 2026.08.11 |
| 파이썬의 PyTorch 라이브러리에 대해 알아보기 (0) | 2026.08.10 |