{
 "nbformat": 4,
 "nbformat_minor": 0,
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Junior KAIST AI 2차시 - End-to-End 가짜 이미지 탐지 모델 학습\n",
    "**이름:** 한정연  \n",
    "**학교:** 용인외대부고 국제과정 2학년\n",
    "\n",
    "1차시에서는 **CLIP + k-NN** 으로 분류했는데, 이번에는 **ResNet-18** 딥러닝 모델을 직접 학습시켜서 진짜/가짜 이미지를 분류해보겠습니다. 탐정(Convolutional Layer)이 단서를 찾고 판사(Linear Layer)가 최종 판결을 내리는 End-to-End 파이프라인입니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 1. 필수 라이브러리 설치\n",
    "Google Colab에서만 실행하면 됩니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "!pip install torch torchvision matplotlib tqdm scikit-learn gdown"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 2. 라이브러리 불러오기\n",
    "딥러닝과 이미지 처리에 필요한 라이브러리를 한 번에 불러옵니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "import os                                    # 폴더와 파일을 다루기 위한 라이브러리\n",
    "import random                                # 랜덤하게 데이터를 섞기 위한 라이브러리\n",
    "import numpy as np                           # 수학 계산용 라이브러리\n",
    "from PIL import Image                        # 이미지를 다루기 위한 라이브러리\n",
    "import matplotlib.pyplot as plt              # 그래프와 이미지를 시각화\n",
    "from tqdm import tqdm                        # 진행 상태를 보여주는 라이브러리\n",
    "\n",
    "import torch                                 # 딥러닝의 핵심 라이브러리\n",
    "import torch.nn as nn                        # 신경망 구성에 필요한 모듈\n",
    "import torch.optim as optim                  # 최적화를 위한 모듈\n",
    "from torchvision import models, transforms   # 사전 학습된 모델과 데이터 변환\n",
    "from torch.utils.data import DataLoader, Dataset"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 3. 데이터 경로 설정\n",
    "1차시와 같은 진짜/가짜 이미지 데이터셋을 다운받고 압축을 풀어줍니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "!gdown https://drive.google.com/uc?id=1qBmDau3_VCXqKamNqkfqVw3hAzy600jD\n",
    "!unzip -q /content/real_vs_fake_dataset.zip -d /content/\n",
    "\n",
    "# Real과 Fake 이미지 폴더 경로\n",
    "real_image_dir = '/content/real_vs_fake_dataset/0_real'\n",
    "fake_image_dir = '/content/real_vs_fake_dataset/1_fake'\n",
    "\n",
    "print('Real 이미지 수:', len(os.listdir(real_image_dir)))\n",
    "print('Fake 이미지 수:', len(os.listdir(fake_image_dir)))"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 4. 데이터 처리 클래스 정의\n",
    "탐정이 이미지를 쉽게 분석할 수 있도록 데이터를 준비하는 `ImageDataset` 클래스를 만듭니다. 폴더에서 이미지를 하나씩 불러오고 라벨(진짜/가짜)도 정해주는 역할을 합니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "class ImageDataset(Dataset):\n",
    "    def __init__(self, image_dirs, labels, transform=None):\n",
    "        self.image_paths = []\n",
    "        self.labels = []\n",
    "        for image_dir, label in zip(image_dirs, labels):\n",
    "            paths = [os.path.join(image_dir, img)\n",
    "                     for img in os.listdir(image_dir)\n",
    "                     if img.endswith(('.png', '.jpg', '.jpeg'))]\n",
    "            self.image_paths.extend(paths)\n",
    "            self.labels.extend([label] * len(paths))\n",
    "        self.transform = transform\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.image_paths)\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        image = Image.open(self.image_paths[idx]).convert('RGB')\n",
    "        label = self.labels[idx]\n",
    "        if self.transform:\n",
    "            image = self.transform(image)\n",
    "        return image, label"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 5. 데이터 전처리\n",
    "이미지를 ResNet-18 입력 규격(224×224)에 맞게 변환합니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "transform = transforms.Compose([\n",
    "    # ResNet-18은 224x224 크기의 이미지를 입력으로 받습니다.\n",
    "    transforms.Resize((224, 224)),\n",
    "    # 이미지를 텐서(숫자 배열)로 변환합니다.\n",
    "    transforms.ToTensor(),\n",
    "    # ResNet-18 사전학습에 쓰인 ImageNet 표준화 값\n",
    "    transforms.Normalize(mean=[0.485, 0.456, 0.406],\n",
    "                         std=[0.229, 0.224, 0.225])\n",
    "])"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 6. 데이터셋 생성 및 분리 (8:2)\n",
    "전체 데이터를 학습용 80% / 테스트용 20%로 나눠줍니다. 학습용으로 모델을 배우게 하고, 테스트용으로 잘 배웠는지 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# Real과 Fake 이미지에 라벨 부여\n",
    "real_label = 0   # Real → 0\n",
    "fake_label = 1   # Fake → 1\n",
    "\n",
    "dataset = ImageDataset(\n",
    "    [real_image_dir, fake_image_dir],\n",
    "    [real_label, fake_label],\n",
    "    transform=transform,\n",
    ")\n",
    "\n",
    "# 8:2로 학습/테스트 분리\n",
    "train_size = int(0.8 * len(dataset))\n",
    "test_size = len(dataset) - train_size\n",
    "train_dataset, test_dataset = torch.utils.data.random_split(dataset, [train_size, test_size])\n",
    "\n",
    "# DataLoader로 배치 단위 묶기\n",
    "batch_size = 16\n",
    "train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\n",
    "test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)\n",
    "\n",
    "print(f'전체 {len(dataset)}장 → 학습 {len(train_dataset)}장 / 테스트 {len(test_dataset)}장')"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 7. ResNet-18 모델 정의\n",
    "사전학습된 ResNet-18을 불러와 마지막 Linear Layer만 2-class(진짜/가짜)로 교체합니다. 탐정 512차원 단서 → 판사 2개 점수."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# 사전학습된 ResNet-18 모델 불러오기\n",
    "model = models.resnet18(pretrained=True)\n",
    "\n",
    "# 마지막 레이어를 2개의 출력 노드(진짜/가짜)로 변경\n",
    "model.fc = nn.Linear(model.fc.in_features, 2)\n",
    "\n",
    "# GPU가 있으면 GPU로 이동\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "model = model.to(device)\n",
    "\n",
    "print('device:', device)"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 8. 학습 준비 (손실 함수와 최적화)\n",
    "분류 문제니까 손실 함수는 `CrossEntropyLoss`, 최적화는 `Adam` (학습률 0.001)을 씁니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "# 분류 문제에 적합한 손실 함수\n",
    "criterion = nn.CrossEntropyLoss()\n",
    "\n",
    "# 모델 학습을 위한 최적화 알고리즘\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.001)"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 9. 모델 학습 함수\n",
    "탐정은 단서를 찾고 판사는 결과를 판단합니다. 매 epoch마다 손실 값(loss)을 출력해서 얼마나 잘 배우고 있는지 확인합니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "def train_model(model, train_loader, criterion, optimizer, epochs=5):\n",
    "    model.train()   # 모델을 학습 모드로 설정\n",
    "    for epoch in range(epochs):\n",
    "        total_loss = 0\n",
    "        for images, labels in tqdm(train_loader, desc=f'Epoch {epoch + 1}/{epochs}'):\n",
    "            images, labels = images.to(device), labels.to(device)\n",
    "\n",
    "            # 모델이 예측한 결과와 손실 계산\n",
    "            outputs = model(images)\n",
    "            loss = criterion(outputs, labels)\n",
    "\n",
    "            # 역전파를 통해 모델 가중치 업데이트\n",
    "            optimizer.zero_grad()\n",
    "            loss.backward()\n",
    "            optimizer.step()\n",
    "\n",
    "            total_loss += loss.item()\n",
    "        print(f'Epoch {epoch + 1}, Loss: {total_loss / len(train_loader):.4f}')"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 10. 모델 평가 함수\n",
    "테스트 데이터로 모델이 얼마나 잘 분류하는지 정확도(Accuracy)를 계산합니다."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "def evaluate_model(model, test_loader):\n",
    "    model.eval()   # 모델을 평가 모드로 설정\n",
    "    correct = 0\n",
    "    total = 0\n",
    "    with torch.no_grad():   # 평가 시에는 가중치 업데이트 안 함\n",
    "        for images, labels in test_loader:\n",
    "            images, labels = images.to(device), labels.to(device)\n",
    "            outputs = model(images)\n",
    "            _, predicted = torch.max(outputs, 1)   # 가장 높은 점수의 클래스를 예측값으로\n",
    "            total += labels.size(0)\n",
    "            correct += (predicted == labels).sum().item()\n",
    "    print(f'Accuracy: {100 * correct / total:.2f}%')"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Step 11. 학습 및 평가 실행\n",
    "GPU 환경에서 5 epoch 학습에 약 2~5분 정도 걸립니다. (CPU는 더 오래 걸려요)"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "source": [
    "train_model(model, train_loader, criterion, optimizer, epochs=5)\n",
    "evaluate_model(model, test_loader)"
   ],
   "execution_count": null,
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## 실전 해결 — 결과 정리 및 개선 아이디어"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Q1. 1차시 CLIP + k-NN과 비교해서 어떤 결과를 얻었나요?\n",
    "\n",
    "| 지표 | 1차시 (CLIP + k-NN, k=3) | 2차시 (ResNet-18 End-to-End, 5 epoch) |\n",
    "|------|--------------------------|----------------------------------------|\n",
    "| 정확도 | **68%** | **60%** |\n",
    "| 학습 시간 | 즉시 분류 (학습 없음) | 5 epoch 학습에 GPU 기준 약 3~4분 |\n",
    "| 원리 | 사전학습 CLIP 특징 + k-NN 거리 기반 | ResNet-18 마지막 fc 를 2-class 로 바꿔 전체 가중치 업데이트 |\n",
    "\n",
    "처음에는 직접 학습시키는 2차시 모델이 더 좋게 나올 줄 알았는데, 막상 실행해보니 **1차시(68%)가 2차시(60%)보다 8%p 더 높게** 나왔습니다. 시간도 1차시는 학습이 따로 없어서 바로 결과가 나왔는데, 2차시는 5 epoch 돌리는 데 3~4분 정도 걸렸습니다. 들이는 시간이 더 많은데 성능은 오히려 떨어졌으니 살짝 의외였습니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Q2. 왜 이런 결과가 나왔을까요?\n",
    "\n",
    "real_vs_fake_dataset이 이미지 수백 장 정도라서 데이터 양은 많지 않은 편입니다. 그런데 ResNet-18을 `pretrained=True`로 불러왔기 때문에, 이미 ImageNet에서 수많은 이미지를 학습한 가중치를 가져온 상태에서 우리 데이터에 맞춰 마지막 레이어만 다시 학습한 셈입니다. 그래서 적은 데이터로도 어느 정도 성능이 나온 것 같습니다.\n",
    "\n",
    "1차시 CLIP이 잘 됐던 이유도 비슷한데, CLIP은 수억 장의 이미지-텍스트 쌍으로 학습된 굉장히 똑똑한 특징 추출기라서, 단순한 k-NN만으로도 분류가 잘 된 거라고 생각합니다. 결국 두 방식 모두 \"누군가 미리 학습해 놓은 모델의 힘을 빌렸다\"는 점에서 비슷합니다.\n",
    "\n",
    "Epoch 5는 좀 짧은 편이라 Loss가 더 줄어들 여지가 있어 보였습니다. 더 오래 학습하면 정확도가 올라갈 수 있을 것 같습니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Q3. 어떻게 하면 더 좋은 성능을 달성할 수 있을까요?\n",
    "\n",
    "교재에 나온 개선 아이디어 중에 직접 시도해보고 싶은 것을 정리해보았습니다.\n",
    "\n",
    "1. **데이터 증강 (Data Augmentation)** — `transforms.RandomHorizontalFlip()`이나 `RandomRotation()`을 추가하면 모델이 더 다양한 이미지를 본 것처럼 학습할 수 있어서 정확도가 올라갈 것 같습니다.\n",
    "2. **Epoch 수 늘리기** — 5에서 10이나 20으로 늘려서 Loss가 더 떨어지는지 보고 싶습니다. 너무 늘리면 과적합(overfitting)이 생길 수 있다고 들었습니다.\n",
    "3. **학습률 조정** — `lr=0.001`을 0.0001로 줄여서 더 천천히 안정적으로 학습시켜보고 싶습니다.\n",
    "4. **더 큰 모델 사용** — ResNet-18 대신 ResNet-50을 써보면 어떨지 궁금합니다. 학습 시간은 더 길어질 것 같지만요.\n",
    "\n",
    "한 번에 여러 개를 바꾸면 어느 게 효과가 있었는지 알기 힘드니까, 한 가지씩 바꾸면서 실험해 볼 계획입니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 실험 기록 (작성 예정)\n",
    "\n",
    "| 실험 | 변경 항목 | 설정 값 | 학습 시간 | 정확도 | 비고 |\n",
    "|------|-----------|---------|-----------|--------|------|\n",
    "| 1 (기준) | — | epochs=5, lr=0.001 | 약 ___분 | ___% | 기준 모델 |\n",
    "| 2 | epochs ↑ | epochs=10 | | | |\n",
    "| 3 | lr ↓ | lr=0.0001 | | | |\n",
    "| 4 | 증강 추가 | RandomHorizontalFlip | | | |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## 제출 체크리스트\n",
    "\n",
    "- [ ] **Step 1~11** 코드 실행 + 결과 캡처 (각 셀의 출력)\n",
    "- [ ] **Q1, Q2, Q3** 답변 채우기 (실제 정확도/시간 수치 반영)\n",
    "- [ ] 노트북 파일을 1개의 zip으로 압축\n",
    "- [ ] 2026년 5월 3일(일) 23:59 이전 제출"
   ]
  }
 ]
}