13차시 핵심 코드 보고서 초심자용

작성일: 2026-01-29 · 목표: “코드를 따라 치면 그대로 동작” + “왜 그런지 설명까지”

전처리 분리 시각화(permute) FC 교체 running_loss

학습 목표

🎯
오늘 목표(이 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 82x4 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>