학습 목표
🎯
오늘 목표(이 4개만 확실히)
- train/test 전처리를 분리해서 “학습은 다양하게, 평가는 일정하게” 만든다.
- 정규화된 텐서를 사람 눈으로 보기 좋게 되돌려(imshow) 출력한다.
- pretrained 모델을 불러오고 마지막 FC를 클래스 개수에 맞게 교체한다.
- 예측 인덱스 추출(torch.max/argmax)과 epoch 평균 loss(running_loss)를 정확히 계산한다.
※ “왜 shuffle을 다르게 하냐”, “permute가 왜 필요하냐”, “loss에 왜 batch_size를 곱하냐”가 오늘 핵심 포인트입니다.
핵심 개념 (초심자용 설명)
- 전처리(train/test 분리): train에는 증강(랜덤 뒤집기 등)을 넣어 모델이 다양한 상황을 보게 함. test는 평가가 목적이므로 조건을 고정.
- shuffle 옵션: train은 shuffle=True로 섞어 학습 편향을 줄임. test는 보통 shuffle=False로 결과 확인/디버깅이 쉬움.
- permute(1,2,0) 의미: PyTorch 이미지 1장 텐서는 (C,H,W). matplotlib은 (H,W,C)를 기대 → 차원 순서 바꾸기.
- FC 교체 의미: 특징 추출부는 그대로(이미 학습됨). 마지막 분류기만 ‘우리 데이터 클래스 수’로 바꾸면 전이학습 가능.
- 예측 인덱스: outputs는 (B,C). dim=1에서 최댓값 인덱스가 예측 클래스.
- running_loss: 배치 loss가 평균(mean)이면, loss×batch_size로 누적해야 ‘전체 평균’이 정확함(마지막 배치 크기 때문).
- 클래스별 정확도: 배치 순서와 무관. labels[i](정답 클래스 번호)를 기준으로 그 클래스 칸(correct/total)에 누적.
전체 흐름 한 장 요약
✅
이 순서로만 보면 끝
전처리 → 로더 → 모델 → 학습 → 평가 → 시각화
1) Dataset 생성
├─ train_transform (증강 포함)
└─ test_transform (평가용 최소 전처리)
2) DataLoader 생성
├─ train_loader: shuffle=True
└─ test_loader : shuffle=False
3) 모델 준비
├─ pretrained 모델 로드 (resnet18)
└─ 마지막 fc 교체: nn.Linear(in_features, n_output)
4) 학습(Train)
├─ outputs = net(images)
├─ loss = criterion(outputs, labels)
├─ running_loss += loss * batch_size
└─ epoch_loss = running_loss / total_samples
5) 평가(Eval)
├─ predicted = argmax(outputs, dim=1)
├─ c = (predicted == labels) # 맞춤/틀림 마스크
└─ 클래스별로 correct/total 누적핵심 코드 (주석 많이)
⚠️
중요
기존에 plt.subplot을 쓰면 축이 1개만 만들어집니다. 여러 칸은 plt.subplots가 맞아요.
A. 전처리 분리 (Train vs Test)
# ✅ 목표: 학습(train)과 평가(test)에 "다른 전처리"를 적용한다.
# - train: 데이터 증강(augmentation)으로 다양한 형태를 보여줘 일반화 성능↑
# - test : 평가용이므로 "항상 같은 조건"으로만 전처리(재현성↑)
from torchvision import transforms
train_transform = transforms.Compose([
transforms.Resize((224, 224)), # 모델 입력 크기에 맞춤 (예: ResNet 계열)
transforms.RandomHorizontalFlip(p=0.5), # 좌우 반전 (확률 50%) → 증강
transforms.ToTensor(), # PIL 이미지 → Tensor(C,H,W), 값 범위 [0,1]
transforms.Normalize(mean=(0.5,0.5,0.5), std=(0.5,0.5,0.5)) # 값 범위 대략 [-1,1]
])
test_transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=(0.5,0.5,0.5), std=(0.5,0.5,0.5))
])B. DataLoader 설정 (shuffle 차이)
# ✅ 목표: 배치(batch)로 나눠서 학습시키고, train은 섞고(test는 보통 안 섞음)
from torch.utils.data import DataLoader
batch_size = 32
train_loader = DataLoader(
train_dataset,
batch_size=batch_size,
shuffle=True, # ✅ train은 섞기(중요): 학습이 한쪽 순서에 치우치지 않음
num_workers=2,
pin_memory=True
)
test_loader = DataLoader(
test_dataset,
batch_size=batch_size,
shuffle=False, # ✅ test는 보통 섞지 않음: 결과 재현/디버깅 쉬움
num_workers=2,
pin_memory=True
)C. 이미지 출력 (denormalize + clamp + permute)
# ✅ 목표: 정규화된 텐서를 "사람이 보기 좋게" 다시 [0,1]로 되돌려 출력한다.
import torch
import matplotlib.pyplot as plt
images, labels = next(iter(train_loader)) # 배치 1개 꺼내기
# images: (B, C, H, W) 예: (32, 3, 224, 224)
# labels: (B,)
# 1) 정규화 해제(denormalize)
# Normalize(mean=0.5, std=0.5)로 했다면,
# 원복: x = x_norm * 0.5 + 0.5
images = images * 0.5 + 0.5
# 2) 안전하게 값 범위를 [0,1]로 제한 (시각화 깨짐 방지)
images = torch.clamp(images, 0, 1)
# 3) 서브플롯 만들기 (⚠️ subplots가 맞음)
num_images = 8
fig, axis = plt.subplots(2, 4, figsize=(12, 6))
axis = axis.ravel() # (2,4) → (8,) 로 평탄화
for i in range(num_images):
# images[i]는 (C,H,W) 형태 → matplotlib은 (H,W,C)를 선호
img = images[i].permute(1, 2, 0).cpu().numpy()
axis[i].imshow(img)
axis[i].set_title(f"label = {labels[i].item()}", fontsize=12)
axis[i].axis("off")
plt.tight_layout()
plt.show()D. Pretrained 모델 불러오기 + FC 교체
# ✅ 목표: pretrained 모델(예: ResNet-18)을 불러오고,
# 마지막 분류기(fc)를 "우리 클래스 개수(n_output)"에 맞게 교체한다.
import torch
import torch.nn as nn
from torchvision import models
# 1) 난수 고정(재현성)
def torch_seed(seed=42):
import random, numpy as np
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch_seed(42)
# 2) pretrained 모델 로드
net = models.resnet18(pretrained=True)
# 3) 기존 fc 입력 차원 확인 (ResNet-18은 보통 512)
fc_in_features = net.fc.in_features
print("기존 입력 차원:", fc_in_features)
# 4) 마지막 출력 노드 수를 n_output(클래스 개수)로 교체
n_output = 10 # 예: 클래스가 10개면 10
net.fc = nn.Linear(fc_in_features, n_output)
print("교체된 마지막 레이어:", net.fc)E. 예측 클래스 인덱스 뽑기 (torch.max / argmax)
# ✅ outputs는 보통 (B, C) = (배치크기, 클래스개수) 로짓/점수
# 각 샘플마다 "가장 큰 점수"를 가진 클래스가 예측 클래스
outputs = net(images) # (B, C)
# 방법 1) torch.max
# _, predicted: predicted는 "최댓값 위치의 인덱스"
_, predicted = torch.max(outputs, dim=1)
# 방법 2) torch.argmax (더 직관적)
predicted2 = torch.argmax(outputs, dim=1)
# predicted / predicted2는 (B,) 형태, 각 값은 0~C-1 사이F. running_loss 누적 & epoch 평균 loss
# ✅ 왜 loss.item()*batch_size를 더하나?
# - loss가 '배치 평균(mean)'인 경우가 많음
# - 마지막 배치는 batch_size가 작을 수 있어서,
# 단순히 배치 평균끼리 평균내면 왜곡됨
# - 그래서 "샘플 개수로 가중치"를 주기 위해 batch_size를 곱함
running_loss = 0.0
total = 0
net.train()
for images, labels in train_loader:
outputs = net(images)
loss = criterion(outputs, labels) # 보통 reduction='mean' → 배치 평균 손실
# ✅ 배치 평균 loss × 배치 샘플 수 → '배치 손실 합'처럼 누적
running_loss += loss.item() * images.size(0)
# ✅ 전체 샘플 수 누적
total += labels.size(0)
epoch_loss = running_loss / total
print("epoch 평균 loss =", epoch_loss)G. 클래스별 정확도 집계 (label 기준으로 묶기)
# ✅ 포인트:
# '순서대로' 세는 게 아니라,
# labels[i] (정답 클래스 번호) 를 보고 해당 클래스 칸에 누적한다.
num_classes = len(classes)
class_correct = [0] * num_classes
class_total = [0] * num_classes
net.eval()
with torch.no_grad():
for images, labels in test_loader:
outputs = net(images)
_, predicted = torch.max(outputs, dim=1) # (B,)
c = (predicted == labels) # (B,) bool → 맞으면 True
for i in range(labels.size(0)):
label = labels[i].item() # 정답 클래스 번호 (0~C-1)
class_correct[label] += int(c[i].item()) # True→1, False→0
class_total[label] += 1
# 클래스별 정확도 출력
for k in range(num_classes):
acc = class_correct[k] / max(1, class_total[k])
print(f"{classes[k]}: {acc:.3f} ({class_correct[k]}/{class_total[k]})")자주 나는 실수 (에러 메시지 기준)
🧯
에러는 “원인 → 해결”을 딱 2줄로 고정
나중에 보고서 쓸 때도 이 표를 그대로 쓰면 정리 속도가 빨라집니다.
| 증상(에러/현상) | 원인 | 해결 |
|---|---|---|
| IndexError: index 8 is out of bounds for axis 0 with size 8 | 2x4 subplot은 8칸인데 num_images=10으로 더 많이 그리려고 해서 발생 | num_images=min(num_images,len(axis)) 또는 rows/cols를 num_images에 맞춰 동적으로 생성 |
| classes[labels[i]]에서 에러/이상 동작 | labels[i]가 tensor일 때 파이썬 리스트 인덱싱이 불안정할 수 있음 | classes[labels[i].item()] 처럼 .item()으로 정수로 변환 |
| .numpy() 변환 에러 (CUDA) | GPU 텐서는 바로 numpy로 변환 불가 | img = tensor.cpu().numpy() |
보고서 포맷 템플릿 (앞으로 계속 이대로)
📌
고정 포맷
✅ 앞으로도 같은 포맷으로 쓰려면 아래 순서만 고정하면 됩니다. 1) 제목/날짜/환경 2) 학습 목표(2~4줄) 3) 핵심 개념(불릿 6~10개, 초심자 기준으로 설명) 4) 전체 흐름(1~2분 컷으로 보는 단계 요약) 5) 핵심 코드(전처리 → 로더 → 모델 → 학습 → 평가 → 시각화 순서) 6) 자주 나는 실수(에러 메시지, 원인, 해결) 7) 다음 액션(다음 차시에서 확인할 것) 아래 HTML 스켈레톤은 그대로 복붙해서 사용하세요.
<section class="card" id="XX">
<h2>XX차시 핵심 코드 보고서</h2>
<p class="meta">작성일: YYYY-MM-DD · 환경: PyTorch/torchvision · 모델: (예: ResNet-18)</p>
<div class="callout">
<div class="icon cyan">🎯</div>
<div>
<b>학습 목표</b>
<ul>
<li>...</li>
<li>...</li>
</ul>
</div>
</div>
<h3>핵심 개념</h3>
<ul>
<li>...</li>
</ul>
<h3>전체 흐름</h3>
<pre class="code"><code>...</code></pre>
<h3>핵심 코드</h3>
<pre class="code"><code>...</code></pre>
<h3>자주 나는 실수</h3>
<table>
<tr><th>증상</th><th>원인</th><th>해결</th></tr>
<tr><td>...</td><td>...</td><td>...</td></tr>
</table>
<h3>다음 액션</h3>
<ul>
<li>...</li>
</ul>
</section>