새 집에 이사하면 먼저 드라이버·망치부터 챙기죠. Colab에서 첫 실행 셀은 "이번 프로젝트에 필요한 공구를 모으는 단계"입니다.
1
라이브러리 설치
셀 1 — torch · torchvision · matplotlib · tqdm · scikit-learn · gdown왜 필요할까?
Google Colab의 기본 환경에 PyTorch(=torch) 는 이미 깔려 있지만, 버전이 구형이거나 일부 라이브러리가 없을 수 있어서 명시적으로 설치해요.
gdown 은 Google Drive에서 데이터셋을 받기 위한 도구입니다.
셀 1 — Colab 전용
!pip install torch torchvision matplotlib tqdm scikit-learn gdown
🧰
비유 — 공구 상자
2
라이브러리 불러오기
셀 2 — 딥러닝 + 이미지 처리 도구 한 번에 import셀 2
import os # 폴더와 파일을 다루기 위한 라이브러리 import random # 랜덤하게 데이터를 섞기 위한 라이브러리 import numpy as np # 수학 계산용 라이브러리 from PIL import Image # 이미지를 다루기 위한 라이브러리 import matplotlib.pyplot as plt # 그래프와 이미지를 시각화 from tqdm import tqdm # 진행 상태를 보여주는 라이브러리 import torch # 딥러닝의 핵심 라이브러리 import torch.nn as nn # 신경망 구성에 필요한 모듈 import torch.optim as optim # 최적화를 위한 모듈 from torchvision import models, transforms # 사전 학습된 모델과 데이터 변환 from torch.utils.data import DataLoader, Dataset
| 모듈 | 역할 |
|---|---|
| torch.nn | 신경망 레이어 (Conv2d, Linear, ReLU, CrossEntropyLoss 등) 정의용 |
| torch.optim | 학습 최적화 알고리즘 (Adam, SGD 등) |
| torchvision.models | 사전학습된 모델 (ResNet-18, EfficientNet 등) |
| torchvision.transforms | 이미지 크기 조정·정규화·증강 |
| DataLoader, Dataset | 데이터를 배치 단위로 모델에 먹이는 인프라 |
Tip — 한글 폰트는 필요할 때만
이 실습은 matplotlib 그래프를 그리지 않으므로 한글 폰트 설정이 필요 없습니다. 만약 학습 곡선 같은 그래프를 직접 그리려면 코랩에서 !pip install -q koreanize-matplotlib 후 import koreanize_matplotlib 한 줄로 해결됩니다.
3
데이터셋 다운로드 & 경로 설정
셀 3 — 1차시와 같은 real_vs_fake_dataset 재사용셀 3
!gdown https://drive.google.com/uc?id=1qBmDau3_VCXqKamNqkfqVw3hAzy600jD !unzip -oq /content/real_vs_fake_dataset.zip real_image_dir = '/content/real_vs_fake_dataset/0_real' fake_image_dir = '/content/real_vs_fake_dataset/1_fake'
핵심 개념 — 라벨링 규약
폴더명이 0_real, 1_fake 인 것이 포인트. 숫자가 바로 정답 라벨 이 됩니다 (Real=0, Fake=1).
이후 ImageDataset에서 이 숫자를 그대로 모델이 학습할 정답으로 사용합니다.
4
ImageDataset 클래스 정의
셀 4 — PyTorch Dataset 인터페이스 구현왜 클래스로 만들까?
PyTorch의 DataLoader 는 Dataset 인터페이스 (즉 __len__ 과 __getitem__) 를 가진 객체만 먹는다. 이 약속을 지키면 자동으로 batch 묶음·셔플·병렬 로딩이 다 공짜.
셀 4
class ImageDataset(Dataset): def __init__(self, image_dirs, labels, transform=None): self.image_paths = [] self.labels = [] for image_dir, label in zip(image_dirs, labels): paths = [os.path.join(image_dir, img) for img in os.listdir(image_dir) if img.endswith(('.png', '.jpg', '.jpeg'))] self.image_paths.extend(paths) self.labels.extend([label] * len(paths)) self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image = Image.open(self.image_paths[idx]).convert('RGB') label = self.labels[idx] if self.transform: image = self.transform(image) return image, label
| 메서드 | 역할 |
|---|---|
| __init__ | 폴더들을 훑어 파일 경로 리스트 + 라벨 리스트 생성 |
| __len__ | 데이터셋 크기 반환 (DataLoader 가 배치 수 계산할 때 사용) |
| __getitem__ | idx 번째 (이미지 tensor, 라벨) 쌍 반환. transform 자동 적용 |
5
데이터 전처리 — transform
셀 5 — ResNet-18 입력 규격 맞추기셀 5
transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])
| 단계 | 하는 일 |
|---|---|
| Resize((224,224)) | 모든 이미지를 224×224 픽셀로 통일 — ResNet-18 이 학습한 크기 |
| ToTensor() | PIL 이미지(0~255 정수) → PyTorch 텐서(0.0~1.0 실수) |
| Normalize(mean, std) | ImageNet 전체 평균·표준편차로 정규화 — 사전학습 모델 입력 규약 |
주의 — mean/std 숫자의 의미
mean=[0.485, 0.456, 0.406] 은 R·G·B 채널 각각의 ImageNet 학습셋 전체 평균. 이 값을 빼고 std로 나누면 입력이 "ImageNet 학습할 때 본 분포"와 맞춰져서 사전학습 가중치가 제 성능을 낸다. 다른 숫자를 쓰면 정확도가 10~20%p 떨어질 수 있어요.
6
train/test 분리 + DataLoader
셀 6 — 8:2, batch_size=16셀 6
# Real과 Fake 이미지에 라벨 부여 real_label = 0 # Real → 0 fake_label = 1 # Fake → 1 dataset = ImageDataset( [real_image_dir, fake_image_dir], [real_label, fake_label], transform=transform, ) train_size = int(0.8 * len(dataset)) test_size = len(dataset) - train_size train_dataset, test_dataset = torch.utils.data.random_split(dataset, [train_size, test_size]) batch_size = 16 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
📚
비유 — 시험 전 분할
전체 문제집(200문제)을 학습용(160) + 시험용(40) 으로 8:2 나누기. 학습용으로만 공부하고, 시험용은 "모델이 한 번도 본 적 없는 문제"로 남겨둔다.
7
ResNet-18 불러오기 + 판사 교체
셀 7 — pretrained=True, fc 2-class 로셀 7
# 사전학습된 ResNet-18 모델 불러오기 model = models.resnet18(pretrained=True) # 마지막 레이어를 2개의 출력 노드(진짜/가짜)로 변경 model.fc = nn.Linear(model.fc.in_features, 2) # GPU가 있으면 GPU로 이동 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) print('device:', device)
핵심 — 왜 마지막 레이어만 바꿀까?
ResNet-18 원본은 ImageNet 1,000 클래스 분류용 (고양이·개·비행기·...). 우리는 진짜/가짜 2 클래스 만 필요하니 마지막 fc(Fully-Connected Layer = 판사) 만 2-output 으로 교체.
앞쪽 Conv Layer (탐정) 들은 "선·모서리·질감" 같은 보편적 시각 특징을 이미 학습해 놨으니 그대로 재사용하는 것이 Transfer Learning (전이학습) 의 핵심.
| pretrained=True | ImageNet 으로 미리 학습된 가중치 다운로드 |
| model.fc.in_features | 기본 ResNet-18 에서 512 (탐정이 마지막에 정리한 단서 개수) |
| nn.Linear(512, 2) | 512 → 2 개 점수 (Real 점수, Fake 점수) 로 출력하는 새 판사 |
| model.to(device) | GPU 가능 시 GPU 메모리로 이동. 학습 속도 10~50배 ↑ |
8
손실 함수 & 최적화 도구
셀 8 — CrossEntropyLoss + Adam(lr=0.001)셀 8
criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001)
핵심 — 손실 vs 최적화
- 손실 함수 (criterion): "얼마나 틀렸는지" 계산하는 자. 작을수록 좋음.
CrossEntropyLoss는 softmax + log 를 내부에 포함해서 분류에 딱 맞춤. - 최적화 도구 (optimizer): 손실을 줄이는 방향으로 가중치를 조금씩 수정.
Adam은 각 파라미터에 맞는 적응적 학습률을 자동으로 쓰는 "똑똑한" 방식.
⛳
비유 — 골프
손실 = 공과 홀컵의 거리, 옵티마이저 = 공을 홀컵 쪽으로 굴리는 샷. lr(learning rate)은 한 번에 얼마나 세게 칠지. 너무 크면 홀컵을 지나치고, 너무 작으면 영영 못 도달.
9
train_model 함수
셀 9 — forward → loss → backward → step셀 9
def train_model(model, train_loader, criterion, optimizer, epochs=5): model.train() # 모델을 학습 모드로 설정 for epoch in range(epochs): total_loss = 0 for images, labels in tqdm(train_loader, desc=f'Epoch {epoch + 1}/{epochs}'): images, labels = images.to(device), labels.to(device) # 모델이 예측한 결과와 손실 계산 outputs = model(images) # ① forward loss = criterion(outputs, labels) # ② 손실 계산 # 역전파를 통해 모델 가중치 업데이트 optimizer.zero_grad() # ③ 이전 gradient 초기화 loss.backward() # ④ 역전파로 gradient 계산 optimizer.step() # ⑤ 가중치 업데이트 total_loss += loss.item() print(f'Epoch {epoch + 1}, Loss: {total_loss / len(train_loader):.4f}')
핵심 5단계 — 학습의 심장
- forward: 이미지 → 모델 → 예측값 (2개 점수)
- loss 계산: 예측 vs 정답 → 얼마나 틀렸나
- zero_grad: 이전 gradient 지우기 (누적 방지)
- backward: 체인룰로 모든 가중치의 gradient 계산
- step: gradient 반대 방향으로 가중치 이동 (경사하강법)
10
evaluate_model 함수
셀 10 — model.eval() + no_grad + torch.max셀 10
def evaluate_model(model, test_loader): model.eval() # 모델을 평가 모드로 설정 correct = 0 total = 0 with torch.no_grad(): # 평가 시에는 가중치 업데이트 안 함 for images, labels in test_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) # 가장 높은 점수의 클래스를 예측값으로 total += labels.size(0) correct += (predicted == labels).sum().item() print(f'Accuracy: {100 * correct / total:.2f}%')
| model.eval() | 평가 모드로 전환 — Dropout, BatchNorm 의 동작 방식이 바뀜 |
| torch.no_grad() | gradient 계산 비활성화 → 메모리 절약 + 속도 ↑ |
| torch.max(outputs, 1) | 2개 점수 중 더 큰 것의 인덱스 반환 → 예측 클래스 |
11
실행 & 결과 확인
셀 11 — 학습 5 epoch + 평가셀 11
train_model(model, train_loader, criterion, optimizer, epochs=5) evaluate_model(model, test_loader)
예상 출력 (데이터·시드·GPU 환경에 따라 다름)
Epoch 1, Loss: 0.5~0.7Epoch 2, Loss: 0.3~0.5
Epoch 3, Loss: 0.2~0.4
Epoch 4, Loss: 0.1~0.3
Epoch 5, Loss: 0.1~0.3
Accuracy: 75~92% 정도
Tip — 체크포인트
Loss 가 매 에폭 감소하면 정상. 만약 Loss 가 거꾸로 올라가거나 NaN이 뜨면 학습률을 0.0001 로 낮춰 보세요.
🎯 핵심 정리 — 11단계를 한 줄로
- 1~3: 도구 설치 → import → 데이터 받기
- 4~6: Dataset 클래스 → transform → train/test split + DataLoader (배치 묶기)
- 7: 사전학습 ResNet-18 의 판사 레이어만 2-class 로 교체
- 8: 손실함수 CrossEntropyLoss + 최적화 Adam(lr=0.001)
- 9: forward → loss → zero_grad → backward → step 5단계가 학습의 심장
- 10~11: eval 모드 + no_grad 로 정확도 측정