지난 시간에는 CLIP 모델을 활용해 진짜와 가짜 이미지를 구분하는 과정을 배웠습니다. CLIP은 미리 학습된 딥러닝 모델로, 이미지의 특징을 추출하고 이를 기반으로 k-NN 알고리즘을 사용해 가짜 이미지를 분류했죠. 여러분은 이 과정에서 딥러닝 모델이 이미지를 분석하고 진짜와 가짜를 구분하는 데 얼마나 강력한지 직접 확인할 수 있었습니다.
그런데 한 가지 궁금하지 않나요?
"우리가 직접 딥러닝 모델을 학습시킨다면 어떨까요?"
CLIP과 같은 사전 학습된 모델은 이미 수많은 데이터를 기반으로 학습된 결과물이지만, 이번에는 여러분이 직접 딥러닝 모델을 설계하고, 학습시키고, 진짜와 가짜 이미지를 분류하는 과정을 체험해 볼 거예요. "내가 만든 모델이 정말 가짜 이미지를 잘 찾아낼까?"라는 질문과 함께 말이죠!
이번 차시에는 간단한 CNN(Convolutional Neural Network) 모델을 사용해 진짜와 가짜 이미지를 분류하는 방법을 배웁니다. 이번 학습을 통해 CNN이 이미지를 이해하는 과정을 탐구하면서, 특징 추출과 분류가 어떻게 이루어지는지 배울 수 있습니다.
지난 시간에는 이미지를 분석해서 중요한 특징을 먼저 뽑아내고, 그 특징으로 진짜인지 가짜인지 예측했어요. 이번에는 End-to-End 학습 방법을 써서 이미지를 모델에 넣기만 하면, 바로 진짜인지 가짜인지 결과를 알려주는 방법을 배워볼 거예요. 모델이 스스로 배우고 똑똑해지는 과정을 함께 알아봅시다.
End-to-End 학습은 처음부터 끝까지 한 번에 모든 과정을 배우는 방법이에요. 가짜 이미지 탐지에서는 이미지를 모델에 넣으면, 모델이 진짜인지 가짜인지 바로 판단할 수 있도록 학습합니다. 이 방식은 사람이 직접 단계별로 작업을 설계할 필요 없이, 모델이 스스로 필요한 특징을 찾아내고 결과를 보여줘요.
예를 들어, 기존 방식에서는 이미지를 분석하기 위해 여러 단계를 거쳤지만, End-to-End 학습은 이 모든 단계를 하나의 모델로 통합해 처리합니다. 이 덕분에 더 간단하고 효율적인 학습이 가능해요.
ResNet-18이라는 모델은 탐정과 판사 팀이 협력해 이미지를 분석하고 진짜인지 가짜인지 판단하는 과정을 거칩니다. 여기서 탐정은 Convolutional Layer, 판사는 Linear Layer 역할을 합니다. 이제 이 팀이 어떻게 일을 하는지 차근차근 알아볼게요!
탐정은 이미지를 보면서 중요한 단서를 찾아냅니다. 처음에는 간단한 단서를 보고, 나중에는 더 복잡한 단서를 찾아요.
탐정은 먼저 "이 이미지에서 가장 뚜렷한 선은 어디지?" 또는 "어떤 부분이 더 밝고 어두운가?" 같은 단서를 모읍니다. 예를 들어, 고양이 사진이라면 "여기 둥근 귀가 있네!", "여기 수염 같은 선이 있어!" 같은 기본적인 정보를 찾습니다.
탐정은 점점 더 깊이 들어가며, "이건 고양이의 눈 모양 같아!", "고양이의 꼬리가 이런 방향으로 휘었네!" 같은 세부적인 단서를 추가로 모읍니다. 단서를 많이 모으면 모을수록 이미지를 더 잘 이해할 수 있게 됩니다.
코드를 통해 탐정이 하는 일을 좀 더 자세히 살펴봅시다. 탐정은 이미지에서 단서를 찾는 역할을 합니다. ResNet-18의 Conv1은 가장 첫 번째 탐정 역할을 수행하며, 단순한 선과 색상 같은 기본적인 패턴을 찾습니다.
Python# ResNet-18 모델 불러오기 model = models.resnet18(pretrained=True) # 첫 번째 Convolutional Layer 확인 (탐정의 첫 번째 역할) conv1 = model.conv1 print(conv1)
OutputConv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
| 구성 요소 | 의미 |
|---|---|
| 필터 (7×7) | 7×7 크기의 작은 창으로 이미지를 훑으며 패턴을 찾습니다. |
| 입력 채널 (3) | RGB 이미지의 3개 채널 (빨강, 초록, 파랑). |
| 출력 채널 (64) | 64개의 단서를 찾아냅니다. |
| Stride (2) | 탐정이 한 번에 두 칸씩 점프하며 이미지를 분석합니다. |
탐정은 처음에 찾은 단서를 가지고 더 복잡한 단서를 찾아갑니다. Residual Block은 탐정이 정보를 잃지 않고 더 깊은 분석을 가능하게 합니다.
Python# Residual Block 확인 (탐정의 두 번째 역할) layer1 = model.layer1 print(layer1)
OutputSequential( (0): BasicBlock( (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) (1): BasicBlock( ... 동일 구조 ... ) )
| 구성 요소 | 의미 |
|---|---|
| Residual Block | 기존 단서를 더 깊이 분석하면서도 중요한 정보를 잃지 않도록 돕는 역할입니다. |
| Conv2d (64, 64) | 64개의 입력 단서를 받아 더 세밀한 64개의 단서를 만듭니다. |
| Stride (1) | 탐정이 한 번에 한 칸씩 점프하므로 더 촘촘히 분석합니다. |
탐정이 단서를 모두 모아오면, 이제 판사가 "이 단서들을 보니, 이건 진짜야!" 또는 "이건 가짜야!"라고 판단합니다.
탐정이 가져온 여러 단서를 하나로 정리합니다. 예를 들어, 탐정이 "둥근 귀, 날카로운 수염, 길다란 꼬리"라는 단서를 가져오면, 판사는 이것들을 조합해 "이건 진짜 고양이일 가능성이 높아!"라고 생각합니다.
판사는 정리된 단서를 바탕으로 최종 결정을 내립니다.
탐정이 단서를 모두 정리하면, 판사는 이를 종합하여 "진짜"인지 "가짜"인지 최종 결론을 내립니다.
Python# ResNet-18의 Linear Layer 확인 (판사 역할) model.fc = nn.Linear(model.fc.in_features, 2) # 2개의 출력값 (진짜/가짜) print(model.fc)
OutputLinear(in_features=512, out_features=2, bias=True)
| 구성 요소 | 의미 |
|---|---|
| 입력 노드 (512) | 탐정이 마지막으로 정리한 512개의 단서를 받습니다. |
| 출력 노드 (2) | 진짜(0)와 가짜(1)를 두 가지로 구분하는 결과를 출력합니다. |
| Bias | 분류를 더 잘하도록 조정 값을 추가합니다. |
Python# 임의의 입력 이미지 텐서 생성 (3x224x224 크기) # 배치 크기 1, RGB 채널 3, 크기 224x224 dummy_image = torch.randn(1, 3, 224, 224) # 첫 번째 탐정(Conv1)이 단서를 찾음 features = model.conv1(dummy_image) print(f"Conv1 출력 크기: {features.shape}")
OutputConv1 출력 크기: torch.Size([1, 64, 112, 112])
→ 64개의 특징 맵(단서)을 찾고, 이미지 크기는 절반(112×112)으로 줄어듭니다.
Python# 전체 모델을 통해 최종 출력 생성 model.fc = nn.Linear(model.fc.in_features, 2) # 두 가지 분류를 하는 판사 model = model.to("cpu") # CPU로 이동 output = model(dummy_image) # 임의의 이미지로 예측 print(f"최종 출력: {output}")
Output최종 출력: tensor([[0.1456, 0.1755]], grad_fn=<AddmmBackward0>)
두 개의 값(진짜, 가짜)에 대한 점수를 출력합니다.
예시처럼 두 값 중 더 높은 값이 가짜(1)로 분류됩니다.
이번에는 ResNet-18 모델을 사용해 이미지를 "진짜"와 "가짜"로 분류하는 과정을 체험해 볼 거예요. ResNet-18은 탐정(Convolutional Layer)과 판사(Linear Layer)로 구성된 팀처럼 작동합니다.
Colab에서 실행할 코드를 준비하려면, 먼저 필요한 도구들을 설치해야 합니다.
Colab# Google Colab에서만 실행하세요. 이 명령어는 딥러닝과 데이터 처리를 위한 라이브러리를 설치합니다. %pip install torch torchvision matplotlib tqdm scikit-learn gdown
필수 라이브러리를 불러와 코드를 준비합니다.
Pythonimport 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 # 데이터 로딩에 필요한 모듈
1차시와 같이 진짜/가짜 이미지 데이터셋을 다운받고 압축을 풀어줍니다.
Python%gdown https://drive.google.com/uc?id=1q8mDau3_VCXqKamNqkfqVw3hAzy600jD %unzip /content/real_vs_fake_dataset.zip # Real과 Fake 이미지를 저장한 폴더 경로를 입력합니다. real_image_dir = "/content/real_vs_fake_dataset/0_real" # Real 이미지가 들어 있는 폴더 fake_image_dir = "/content/real_vs_fake_dataset/1_fake" # Fake 이미지가 들어 있는 폴더
탐정이 이미지를 쉽게 분석할 수 있도록 데이터를 준비합니다. 이미지가 들어있는 폴더에서 하나씩 이미지를 불러오고 라벨(진짜/가짜)도 정해줍니다.
Python# 이미지를 불러오고, 변환하며, 학습에 사용할 준비를 합니다. 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
| 구성 요소 | 역할 |
|---|---|
| ImageDataset 클래스 | 이미지를 가져와서 각 이미지에 라벨(진짜/가짜)을 추가합니다. |
| __getitem__ | 해당 번호(인덱스)의 이미지를 읽고, 변환한 후 라벨과 함께 반환합니다. |
| __len__ | 데이터셋에 포함된 이미지 개수를 반환합니다. |
이미지를 ResNet-18에 맞게 변환합니다.
Pythontransform = transforms.Compose([ # ResNet-18은 224x224 크기의 이미지를 입력으로 받습니다. transforms.Resize((224, 224)), # 이미지를 텐서(숫자 배열)로 변환합니다. transforms.ToTensor(), # ResNet-18에 맞는 표준화 값 transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])
전체 데이터를 학습용 80%와 테스트용 20%로 나눠요. 학습용 데이터로 모델을 배우게 하고, 테스트용 데이터로 모델이 잘 배웠는지 확인합니다.
Python# 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]) # DataLoader를 사용해 데이터를 배치로 나눕니다. 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)
| 구성 요소 | 역할 |
|---|---|
| 이미지 변환 | 이미지를 ResNet-18 모델이 이해할 수 있는 크기(224×224)로 줄입니다. 이미지를 숫자로 바꾸고, 숫자의 크기를 일정하게 맞춥니다(정규화). |
| 데이터셋 나누기 | 전체 데이터를 학습용(80%)과 테스트용(20%)으로 나눠요. |
| DataLoader | 데이터를 한 번에 여러 개씩 묶어서(배치) 모델에 전달합니다. 이렇게 하면 모델이 더 빠르게 학습할 수 있어요. |
탐정과 판사가 팀을 이뤄 이미지를 분석합니다.
Python# 사전 학습된 ResNet-18 모델을 불러오고 출력 크기를 조정합니다. model = models.resnet18(pretrained=True) # 모델의 마지막 레이어를 2개의 출력 노드(진짜/가짜)로 변경합니다. model.fc = nn.Linear(model.fc.in_features, 2) # 모델을 GPU로 이동합니다 (가능한 경우). device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device)
Python# 분류 문제에 적합한 손실 함수 criterion = nn.CrossEntropyLoss() # 모델 학습을 위한 최적화 알고리즘 optimizer = optim.Adam(model.parameters(), lr=0.001)
| 구성 요소 | 역할 |
|---|---|
| ResNet-18 불러오기 | ResNet-18은 이미 많은 데이터를 학습한 똑똑한 모델이에요. 이 모델을 가져와서 우리가 사용할 거예요. |
| 출력 수정하기 | ResNet-18은 원래 여러 종류의 이미지를 구분하지만, 여기서는 진짜(0)와 가짜(1)만 구분하도록 수정합니다. |
| 손실 함수 (Loss) | 모델이 얼마나 틀렸는지 계산하는 도구입니다. 모델이 잘못된 예측을 하면 손실 값을 통해 고칠 수 있어요. |
| 최적화 도구 (Optimizer) | 손실 값을 줄이기 위해 모델이 더 잘 배울 수 있도록 돕는 도구입니다. |
탐정은 이미지를 분석하며 단서를 찾고, 판사는 결과를 판단합니다.
Python# 모델을 학습시키기 위한 함수입니다. 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) loss = criterion(outputs, labels) # 역전파를 통해 모델 가중치 업데이트 optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() print(f"Epoch {epoch + 1}, Loss: {total_loss / len(train_loader):.4f}")
| 구성 요소 | 역할 |
|---|---|
| train_model 함수 | 모델이 데이터를 보고 "이건 진짜다!" 또는 "이건 가짜다!"를 판단하도록 학습시킵니다. |
| 모델이 틀렸을 때 |
|
탐정과 판사 팀이 협력해 이미지를 분석하고 학습한 모델이 얼마나 잘 판단하는지 최종 평가합니다.
Python# 모델을 평가하기 위한 함수입니다. 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}%")
Pythontrain_model(model, train_loader, criterion, optimizer, epochs=5) evaluate_model(model, test_loader)
| 구성 요소 | 역할 |
|---|---|
| evaluate_model 함수 | 테스트 데이터를 사용해서 모델이 학습한 내용을 점검합니다. |
| model.eval() | 평가 모드로 전환해 학습 과정에서 사용하던 불필요한 도구를 끕니다. |
| 정확도 계산 | 모델이 맞춘 예측의 비율을 계산해 "정확도"를 출력합니다. 몇 퍼센트 맞췄는지 알 수 있어요. |
여기까지 따라왔다면 여러분은 이제 딥러닝 모델을 직접 학습시킨 사람입니다. 1차시의 CLIP+k-NN과 비교해서 정확도가 얼마나 달라졌는지, 학습 시간이 얼마나 걸렸는지 직접 확인해 봅시다. 다음 Part 3 실전 해결에서 결과를 정리해 봐요!
1차시에서 만든 CLIP+k-NN 모델과 비교해서, 이번에 만든 ResNet-18 End-to-End 모델은 어떤 결과를 보여줬나요? 정확도, 학습 시간, 데이터 양과의 관계를 정리해 봅시다.
| 실험 번호 | 변경한 항목 | 설정 값 | 학습 시간 | 정확도 | 비고 |
|---|---|---|---|---|---|
| 실험 1 (기준) | — | epochs=5, lr=0.001 | 약 ___분 | ___% | 기준 모델 |
| 실험 2 | epochs ↑ | epochs=10 | |||
| 실험 3 | lr ↓ | lr=0.0001 | |||
| 실험 4 | 증강 추가 | RandomHorizontalFlip |
Conv2d(in_channels, out_channels, kernel_size, stride, padding) 형태로 정의.lr=0.001이 무난한 시작값.| 증상 | 원인 | 해결 |
|---|---|---|
CUDA out of memory | batch_size가 너무 큼 | batch_size를 16 → 8 → 4로 줄이기 |
| Loss가 계속 같은 값에서 멈춤 | 학습률(lr)이 너무 큼/작음 | lr을 0.0001 ~ 0.01 사이에서 조정 |
| Accuracy가 50%에서 안 오름 | 이진 분류에서 한쪽 클래스만 예측 | 데이터 균형 확인, epochs 늘리기 |
FileNotFoundError | gdown/unzip 단계 실패 | Step 3을 다시 실행해 폴더 존재 확인 |
pytorch.org/tutorials — Transfer Learning, CIFAR-10 분류 예제transforms.RandomHorizontalFlip(), RandomRotation(), ColorJitter() 등2차시까지 마쳤다면 딥러닝 모델 학습의 전체 사이클을 한 번 돌려본 셈입니다. 다음 단계로는 (1) 전이학습(Transfer Learning) 깊이 다루기, (2) Vision Transformer(ViT)로 모델 확장, (3) 설명 가능한 AI(Grad-CAM)로 모델이 어디를 보고 판단했는지 시각화 — 이 세 가지를 추천합니다.