{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 19 인공 신경망 실습\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "현재 사용 중인 장치: cpu\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|███████████████████████████████████████████████████████████████████████████| 9.91M/9.91M [00:04<00:00, 2.45MB/s]\n",
      "100%|████████████████████████████████████████████████████████████████████████████| 28.9k/28.9k [00:00<00:00, 139kB/s]\n",
      "100%|███████████████████████████████████████████████████████████████████████████| 1.65M/1.65M [00:01<00:00, 1.09MB/s]\n",
      "100%|████████████████████████████████████████████████████████████████████████████| 4.54k/4.54k [00:00<00:00, 908kB/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "--- 학습 시작 ---\n",
      "Epoch [1/5], Step [200/938], Loss: 0.4581\n",
      "Epoch [1/5], Step [400/938], Loss: 0.2188\n",
      "Epoch [1/5], Step [600/938], Loss: 0.1699\n",
      "Epoch [1/5], Step [800/938], Loss: 0.1541\n",
      "Epoch [2/5], Step [200/938], Loss: 0.1107\n",
      "Epoch [2/5], Step [400/938], Loss: 0.1160\n",
      "Epoch [2/5], Step [600/938], Loss: 0.1056\n",
      "Epoch [2/5], Step [800/938], Loss: 0.1091\n",
      "Epoch [3/5], Step [200/938], Loss: 0.0814\n",
      "Epoch [3/5], Step [400/938], Loss: 0.0927\n",
      "Epoch [3/5], Step [600/938], Loss: 0.0876\n",
      "Epoch [3/5], Step [800/938], Loss: 0.0918\n",
      "Epoch [4/5], Step [200/938], Loss: 0.0669\n",
      "Epoch [4/5], Step [400/938], Loss: 0.0676\n",
      "Epoch [4/5], Step [600/938], Loss: 0.0730\n",
      "Epoch [4/5], Step [800/938], Loss: 0.0754\n",
      "Epoch [5/5], Step [200/938], Loss: 0.0559\n",
      "Epoch [5/5], Step [400/938], Loss: 0.0649\n",
      "Epoch [5/5], Step [600/938], Loss: 0.0602\n",
      "Epoch [5/5], Step [800/938], Loss: 0.0657\n",
      "\n",
      "--- 테스트 시작 ---\n",
      "테스트 데이터 정확도: 98.16%\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "# 1. 환경 설정 및 하이퍼파라미터\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "batch_size = 64\n",
    "learning_rate = 0.001\n",
    "epochs = 5\n",
    "\n",
    "print(f\"현재 사용 중인 장치: {device}\")\n",
    "\n",
    "# 2. 데이터셋 준비 (MNIST)\n",
    "# transforms.ToTensor()는 0~255의 이미지를 0~1 사이로 정규화해줍니다.\n",
    "transform = transforms.Compose([\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize((0.1307,), (0.3081,)) # MNIST 평균과 표준편차로 정규화\n",
    "])\n",
    "\n",
    "# 데이터셋 다운로드\n",
    "train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)\n",
    "test_dataset = datasets.MNIST(root='./data', train=False, transform=transform)\n",
    "\n",
    "# 미니배치 구성을 위한 DataLoader\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",
    "# 3. 모델 정의 (MLP + Dropout)\n",
    "class MultiLayerPerceptron(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(MultiLayerPerceptron, self).__init__()\n",
    "        self.flatten = nn.Flatten()\n",
    "        self.model = nn.Sequential(\n",
    "            nn.Linear(28*28, 512),\n",
    "            nn.ReLU(),\n",
    "            nn.Dropout(0.2),       # 과적합 방지 (20% 노드 끔)\n",
    "            \n",
    "            nn.Linear(512, 256),\n",
    "            nn.ReLU(),\n",
    "            nn.Dropout(0.2),       # 과적합 방지 (20% 노드 끔)\n",
    "            \n",
    "            nn.Linear(256, 10)     # 0~9까지의 클래스 출력\n",
    "        )\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = self.flatten(x)\n",
    "        logits = self.model(x)\n",
    "        return logits\n",
    "\n",
    "model = MultiLayerPerceptron().to(device)\n",
    "\n",
    "# 4. 손실 함수와 최적화 도구\n",
    "criterion = nn.CrossEntropyLoss() # 다중 분류를 위한 크로스 엔트로피\n",
    "optimizer = optim.Adam(model.parameters(), lr=learning_rate)\n",
    "\n",
    "# 5. 학습 루프 (Training Loop)\n",
    "print(\"\\n--- 학습 시작 ---\")\n",
    "for epoch in range(epochs):\n",
    "    model.train() # 중요: 학습 모드 (Dropout 활성화)\n",
    "    running_loss = 0.0\n",
    "    \n",
    "    for batch_idx, (data, target) in enumerate(train_loader):\n",
    "        data, target = data.to(device), target.to(device)\n",
    "\n",
    "        # 기울기 초기화\n",
    "        optimizer.zero_grad()\n",
    "        \n",
    "        # 순전파 (Forward)\n",
    "        output = model(data)\n",
    "        loss = criterion(output, target)\n",
    "        \n",
    "        # 역전파 (Backward) 및 가중치 업데이트\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        \n",
    "        running_loss += loss.item()\n",
    "        \n",
    "        if batch_idx % 200 == 199: # 200개 배치마다 출력\n",
    "            print(f'Epoch [{epoch+1}/{epochs}], Step [{batch_idx+1}/{len(train_loader)}], Loss: {running_loss/200:.4f}')\n",
    "            running_loss = 0.0\n",
    "\n",
    "# 6. 모델 평가 (Evaluation)\n",
    "print(\"\\n--- 테스트 시작 ---\")\n",
    "model.eval() # 중요: 평가 모드 (Dropout 비활성화)\n",
    "correct = 0\n",
    "total = 0\n",
    "\n",
    "with torch.no_grad(): # 평가 시에는 기울기 계산이 필요 없음 (메모리 절약)\n",
    "    for data, target in test_loader:\n",
    "        data, target = data.to(device), target.to(device)\n",
    "        outputs = model(data)\n",
    "        _, predicted = torch.max(outputs.data, 1) # 가장 높은 확률을 가진 인덱스 추출\n",
    "        total += target.size(0)\n",
    "        correct += (predicted == target).sum().item()\n",
    "\n",
    "print(f'테스트 데이터 정확도: {100 * correct / total:.2f}%')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "MNIST_CNN(\n",
      "  (conv1): Conv2d(1, 10, kernel_size=(5, 5), stride=(1, 1))\n",
      "  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)\n",
      "  (conv2): Conv2d(10, 20, kernel_size=(5, 5), stride=(1, 1))\n",
      "  (fc1): Linear(in_features=320, out_features=100, bias=True)\n",
      "  (dropout): Dropout(p=0.5, inplace=False)\n",
      "  (fc2): Linear(in_features=100, out_features=10, bias=True)\n",
      ")\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "class MNIST_CNN(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(MNIST_CNN, self).__init__()\n",
    "        \n",
    "        # [특징 추출부 - Feature Extraction]\n",
    "        # 1. 첫 번째 합성곱 층: 입력 채널 1 (흑백), 출력 채널 10, 커널 크기 5x5\n",
    "        self.conv1 = nn.Conv2d(in_channels=1, out_channels=10, kernel_size=5)\n",
    "        \n",
    "        # 2. 첫 번째 풀링 층: 커널 크기 2x2, 스트라이드 2\n",
    "        # (nn.MaxPool2d를 하나만 선언해서 conv1과 conv2 뒤에 공통으로 사용해도 됩니다)\n",
    "        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)\n",
    "        \n",
    "        # 3. 두 번째 합성곱 층: 입력 채널 10, 출력 채널 20, 커널 크기 5x5\n",
    "        self.conv2 = nn.Conv2d(in_channels=10, out_channels=20, kernel_size=5)\n",
    "        \n",
    "        # [분류부 - Classification]\n",
    "        # 4. 첫 번째 FC Layer (은닉층): 4x4x20 = 320 입력 -> 100 출력\n",
    "        self.fc1 = nn.Linear(in_features=320, out_features=100)\n",
    "\n",
    "        # 5. 드롭아웃 객체 정의 (p=0.5는 노드를 50% 확률로 끈다는 의미)\n",
    "        self.dropout = nn.Dropout(p=0.5)\n",
    "        \n",
    "        # 6. 두 번째 FC Layer (출력층): 100 입력 -> 10 출력 (클래스 개수 0~9)\n",
    "        self.fc2 = nn.Linear(in_features=100, out_features=10)\n",
    "\n",
    "    def forward(self, x):\n",
    "        # 입력 x의 초기 크기: (Batch_Size, 1, 28, 28)\n",
    "        \n",
    "        # --- 특징 추출부 ---\n",
    "        # Conv1 -> ReLU -> MaxPool1\n",
    "        # 크기 변화: (28x28) -> Conv -> (24x24) -> Pool -> (12x12)\n",
    "        x = self.pool(F.relu(self.conv1(x)))\n",
    "        \n",
    "        # Conv2 -> ReLU -> MaxPool2\n",
    "        # 크기 변화: (12x12) -> Conv -> (8x8) -> Pool -> (4x4)\n",
    "        x = self.pool(F.relu(self.conv2(x)))\n",
    "        \n",
    "        # --- 평탄화 (Flatten) ---\n",
    "        # 3차원 텐서(20, 4, 4)를 1차원 텐서(320)로 펼침\n",
    "        # x.size(0)은 Batch Size를 의미함\n",
    "        x = x.view(x.size(0), -1) \n",
    "        \n",
    "        # --- 분류부 ---\n",
    "        # 첫 번째 FC Layer -> ReLU\n",
    "        x = F.relu(self.fc1(x))\n",
    "\n",
    "        # 첫 번째 FC Layer를 통과한 후 드롭아웃 적용\n",
    "        x = self.dropout(x)\n",
    "        \n",
    "        # 두 번째 FC Layer\n",
    "        x = self.fc2(x)\n",
    "        \n",
    "        # Log Softmax 적용 (dim=1은 각 클래스에 대해 적용한다는 의미)\n",
    "        output = F.log_softmax(x, dim=1)\n",
    "        \n",
    "        return output\n",
    "\n",
    "# 모델 인스턴스 생성 및 구조 확인\n",
    "model = MNIST_CNN()\n",
    "print(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 모델 학습(python)\n",
    "\n",
    "이제 딥러닝 학습을 작성합니다. 앞서 정의한 `MNIST_CNN` 모델을 이용합니다. 딥러닝 학습 **'4단계 사이클(순전파 $\\rightarrow$ 손실 계산 $\\rightarrow$ 역전파 $\\rightarrow$ 가중치 업데이트)'** 의 이해는 주석을 참조바랍니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch: 1 | Batch: 100 | Loss: 1.0502\n",
      "Epoch: 1 | Batch: 200 | Loss: 0.3523\n",
      "Epoch: 1 | Batch: 300 | Loss: 0.2472\n",
      "Epoch: 1 | Batch: 400 | Loss: 0.2141\n",
      "Epoch: 1 | Batch: 500 | Loss: 0.1950\n",
      "Epoch: 1 | Batch: 600 | Loss: 0.1671\n",
      "Epoch: 1 | Batch: 700 | Loss: 0.1607\n",
      "Epoch: 1 | Batch: 800 | Loss: 0.1409\n",
      "Epoch: 1 | Batch: 900 | Loss: 0.1380\n",
      "Epoch: 2 | Batch: 100 | Loss: 0.1118\n",
      "Epoch: 2 | Batch: 200 | Loss: 0.1111\n",
      "Epoch: 2 | Batch: 300 | Loss: 0.1133\n",
      "Epoch: 2 | Batch: 400 | Loss: 0.1093\n",
      "Epoch: 2 | Batch: 500 | Loss: 0.0983\n",
      "Epoch: 2 | Batch: 600 | Loss: 0.0849\n",
      "Epoch: 2 | Batch: 700 | Loss: 0.0900\n",
      "Epoch: 2 | Batch: 800 | Loss: 0.0965\n",
      "Epoch: 2 | Batch: 900 | Loss: 0.0938\n",
      "Epoch: 3 | Batch: 100 | Loss: 0.0752\n",
      "Epoch: 3 | Batch: 200 | Loss: 0.0823\n",
      "Epoch: 3 | Batch: 300 | Loss: 0.0702\n",
      "Epoch: 3 | Batch: 400 | Loss: 0.0830\n",
      "Epoch: 3 | Batch: 500 | Loss: 0.0742\n",
      "Epoch: 3 | Batch: 600 | Loss: 0.0762\n",
      "Epoch: 3 | Batch: 700 | Loss: 0.0745\n",
      "Epoch: 3 | Batch: 800 | Loss: 0.0681\n",
      "Epoch: 3 | Batch: 900 | Loss: 0.0801\n",
      "Epoch: 4 | Batch: 100 | Loss: 0.0608\n",
      "Epoch: 4 | Batch: 200 | Loss: 0.0578\n",
      "Epoch: 4 | Batch: 300 | Loss: 0.0681\n",
      "Epoch: 4 | Batch: 400 | Loss: 0.0583\n",
      "Epoch: 4 | Batch: 500 | Loss: 0.0546\n",
      "Epoch: 4 | Batch: 600 | Loss: 0.0686\n",
      "Epoch: 4 | Batch: 700 | Loss: 0.0593\n",
      "Epoch: 4 | Batch: 800 | Loss: 0.0616\n",
      "Epoch: 4 | Batch: 900 | Loss: 0.0593\n",
      "Epoch: 5 | Batch: 100 | Loss: 0.0555\n",
      "Epoch: 5 | Batch: 200 | Loss: 0.0517\n",
      "Epoch: 5 | Batch: 300 | Loss: 0.0533\n",
      "Epoch: 5 | Batch: 400 | Loss: 0.0518\n",
      "Epoch: 5 | Batch: 500 | Loss: 0.0461\n",
      "Epoch: 5 | Batch: 600 | Loss: 0.0518\n",
      "Epoch: 5 | Batch: 700 | Loss: 0.0613\n",
      "Epoch: 5 | Batch: 800 | Loss: 0.0576\n",
      "Epoch: 5 | Batch: 900 | Loss: 0.0520\n",
      "훈련이 완료되었습니다!\n"
     ]
    }
   ],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "import torch.nn.functional as F\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "# 1. 데이터 준비 (MNIST 데이터셋 다운로드 및 로더 설정)\n",
    "# 이미지를 텐서로 변환하고 정규화(Normalize)합니다.\n",
    "transform = transforms.Compose([\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize((0.1307,), (0.3081,)) # MNIST 데이터의 평균과 표준편차\n",
    "])\n",
    "\n",
    "# 학습용 데이터셋 로드\n",
    "train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)\n",
    "# 배치 단위로 데이터를 묶어주는 DataLoader (한 번에 64장씩 처리)\n",
    "train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)\n",
    "\n",
    "# 2. 모델, 손실 함수, 최적화 기법 설정\n",
    "# 앞서 정의한 MNIST_CNN 모델 객체 생성\n",
    "model = MNIST_CNN()\n",
    "\n",
    "# 손실 함수 (Loss Function): Negative Log Likelihood Loss\n",
    "# (주의: 모델의 마지막 출력이 log_softmax이므로 NLLLoss를 사용합니다)\n",
    "criterion = nn.NLLLoss() \n",
    "\n",
    "# 최적화 기법 (Optimizer): Adam 옵티마이저 사용 (학습률 0.001)\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.001)\n",
    "\n",
    "# 3. 모델 훈련 (Training Loop)\n",
    "epochs = 5 # 전체 데이터셋을 5번 반복 학습\n",
    "\n",
    "model.train() # 모델을 훈련 모드로 설정 (Dropout, BatchNorm 등이 있을 경우 필수)\n",
    "\n",
    "for epoch in range(epochs):\n",
    "    running_loss = 0.0\n",
    "    \n",
    "    # 배치(Batch) 단위로 데이터를 가져와 학습 진행\n",
    "    for batch_idx, (data, target) in enumerate(train_loader):\n",
    "        \n",
    "        # [핵심] 딥러닝 학습 4단계 사이클\n",
    "        \n",
    "        # 단계 1: 기울기 초기화\n",
    "        # 이전 배치의 기울기(Gradient)가 누적되지 않도록 0으로 만듭니다.\n",
    "        optimizer.zero_grad()\n",
    "        \n",
    "        # 단계 2: 순전파 (Forward Pass)\n",
    "        # 입력 데이터를 모델에 통과시켜 예측값(log 확률)을 얻습니다.\n",
    "        output = model(data)\n",
    "        \n",
    "        # 단계 3: 손실 계산 (Loss Calculation)\n",
    "        # 모델의 예측값과 실제 정답(target)의 오차를 계산합니다.\n",
    "        loss = criterion(output, target)\n",
    "        \n",
    "        # 단계 4: 역전파 및 가중치 업데이트 (Backward & Step)\n",
    "        loss.backward()  # 오차를 뒤로 전파하여 각 가중치의 기울기를 계산합니다.\n",
    "        optimizer.step() # 계산된 기울기를 바탕으로 가중치를 업데이트(학습)합니다.\n",
    "        \n",
    "        # 진행 상황 출력 (100번째 배치마다 로그 출력)\n",
    "        running_loss += loss.item()\n",
    "        if batch_idx % 100 == 99:\n",
    "            print(f'Epoch: {epoch + 1} | Batch: {batch_idx + 1} | Loss: {running_loss / 100:.4f}')\n",
    "            running_loss = 0.0 # 로그 출력 후 누적 오차 초기화\n",
    "\n",
    "print(\"훈련이 완료되었습니다!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 모델 인식(python)\n",
    "\n",
    "이제 훈련이 끝났으니 이 훈련된 모델의 성능(정확도)을 평가하기 위한 테스트(Evaluation) 코드를 작성해야 합니다.\n",
    "다음은 학습된 모델을 테스트 데이터셋에 통과시켜 최종 정확도(Accuracy)와 평균 손실(Loss)을 계산합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "[테스트 결과] 평균 Loss: 0.0331, 정확도: 9881/10000 (98.81%)\n"
     ]
    }
   ],
   "source": [
    "# 1. 테스트 데이터셋 및 로더 준비\n",
    "test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)\n",
    "test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)\n",
    "\n",
    "# 2. 모델 평가 모드 전환\n",
    "model.eval() # 모델을 평가 모드로 설정 (Dropout, BatchNorm 등의 동작을 평가용으로 고정)\n",
    "\n",
    "test_loss = 0\n",
    "correct = 0\n",
    "\n",
    "# 3. 기울기 계산 비활성화 (메모리 절약 및 속도 향상)\n",
    "with torch.no_grad():\n",
    "    for data, target in test_loader:\n",
    "        # 순전파 (Forward Pass)\n",
    "        output = model(data)\n",
    "        \n",
    "        # 배치별 손실을 누적 (reduction='sum'으로 설정하여 전체 합산)\n",
    "        test_loss += F.nll_loss(output, target, reduction='sum').item()\n",
    "        \n",
    "        # 예측값 도출: log_softmax 결과 중 가장 큰 값의 인덱스(클래스)를 찾음\n",
    "        pred = output.argmax(dim=1, keepdim=True)\n",
    "        \n",
    "        # 예측값과 실제 정답이 일치하는 개수 누적\n",
    "        correct += pred.eq(target.view_as(pred)).sum().item()\n",
    "\n",
    "# 4. 최종 결과 계산 및 출력\n",
    "# 전체 손실을 테스트 데이터 전체 개수(10000개)로 나누어 평균 손실 계산\n",
    "test_loss /= len(test_loader.dataset)\n",
    "\n",
    "# 정확도 계산 (백분율)\n",
    "accuracy = 100. * correct / len(test_loader.dataset)\n",
    "\n",
    "print(f'\\n[테스트 결과] 평균 Loss: {test_loss:.4f}, 정확도: {correct}/{len(test_loader.dataset)} ({accuracy:.2f}%)')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> `model.eval()`과 `with torch.no_grad():`는 평가할 때는 가중치를 업데이트할 필요가 없으므로 기울기(Gradient)를 계산하는 엔진을 꺼두는 것입니다. 메모리 사용량을 줄이고 연산 속도를 높여줍니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 결과 시각화\n",
    "다음은 숫자 이미지들을 Matplotlib을 이용해 화면에 띄우고, 모델이 예측한 숫자와 실제 정답을 함께 비교해 보는 코드입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1000x600 with 6 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# 1. 테스트 데이터 로더에서 배치 하나(이미지 1000장 묶음)를 가져옴\n",
    "dataiter = iter(test_loader)\n",
    "images, labels = next(dataiter)\n",
    "\n",
    "# 2. 모델에 이미지를 넣어 예측 수행\n",
    "with torch.no_grad():\n",
    "    model.eval()\n",
    "    outputs = model(images)\n",
    "    # 가장 높은 확률을 가진 인덱스를 예측값으로 선택\n",
    "    _, preds = torch.max(outputs, 1)\n",
    "\n",
    "# 3. 시각화를 위해 첫 6개의 이미지만 선택하여 그리기\n",
    "fig = plt.figure(figsize=(10, 6))\n",
    "\n",
    "for i in range(6):\n",
    "    # 2행 3열의 서브플롯 생성\n",
    "    ax = fig.add_subplot(2, 3, i + 1, xticks=[], yticks=[])\n",
    "    \n",
    "    # 파이토치 텐서(채널, 높이, 너비)를 Matplotlib에 맞게(높이, 너비) 변환하고 넘파이 배열로 변경\n",
    "    img = images[i].numpy().squeeze() \n",
    "    \n",
    "    # 흑백 이미지 출력\n",
    "    ax.imshow(img, cmap='gray')\n",
    "    \n",
    "    # 예측값과 정답을 제목으로 표시 (예측이 맞으면 초록색, 틀리면 빨간색)\n",
    "    true_label = labels[i].item()\n",
    "    pred_label = preds[i].item()\n",
    "    color = 'green' if true_label == pred_label else 'red'\n",
    "    \n",
    "    ax.set_title(f'Pred: {pred_label} / True: {true_label}', color=color, fontweight='bold')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 전체 통합 코드\n",
    "\n",
    "위 코드들을 하나로 통합한 종합 코드를 아래에 보입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# ==========================================\n",
    "# [Cell 1] 라이브러리 임포트 및 데이터 준비\n",
    "# ==========================================\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.optim as optim\n",
    "import torch.nn.functional as F\n",
    "from torchvision import datasets, transforms\n",
    "from torch.utils.data import DataLoader\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# 데이터 정규화 및 텐서 변환 설정\n",
    "transform = transforms.Compose([\n",
    "    transforms.ToTensor(),\n",
    "    transforms.Normalize((0.1307,), (0.3081,))\n",
    "])\n",
    "\n",
    "# 훈련용/테스트용 데이터셋 다운로드 및 로더 설정\n",
    "train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)\n",
    "test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)\n",
    "\n",
    "train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)\n",
    "test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)\n",
    "\n",
    "print(f\"훈련 데이터 개수: {len(train_dataset)} / 테스트 데이터 개수: {len(test_dataset)}\")\n",
    "\n",
    "\n",
    "# ==========================================\n",
    "# [Cell 2] CNN 아키텍처 정의 (Dropout 포함)\n",
    "# ==========================================\n",
    "class MNIST_CNN(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(MNIST_CNN, self).__init__()\n",
    "        \n",
    "        # 특징 추출부\n",
    "        self.conv1 = nn.Conv2d(in_channels=1, out_channels=10, kernel_size=5)\n",
    "        self.pool = nn.MaxPool2d(kernel_size=2, stride=2)\n",
    "        self.conv2 = nn.Conv2d(in_channels=10, out_channels=20, kernel_size=5)\n",
    "        \n",
    "        # 분류부 (Dropout은 첫 번째 은닉층 직후에 적용)\n",
    "        self.fc1 = nn.Linear(in_features=320, out_features=100)\n",
    "        self.dropout = nn.Dropout(p=0.5) \n",
    "        self.fc2 = nn.Linear(in_features=100, out_features=10)\n",
    "\n",
    "    def forward(self, x):\n",
    "        # 특징 추출\n",
    "        x = self.pool(F.relu(self.conv1(x)))\n",
    "        x = self.pool(F.relu(self.conv2(x)))\n",
    "        \n",
    "        # 평탄화 (Flatten)\n",
    "        x = x.view(x.size(0), -1) \n",
    "        \n",
    "        # 분류 및 Dropout 적용\n",
    "        x = F.relu(self.fc1(x))\n",
    "        x = self.dropout(x)\n",
    "        x = self.fc2(x)\n",
    "        \n",
    "        return F.log_softmax(x, dim=1)\n",
    "\n",
    "# 모델, 손실 함수, 최적화 기법 초기화\n",
    "model = MNIST_CNN()\n",
    "criterion = nn.NLLLoss()\n",
    "optimizer = optim.Adam(model.parameters(), lr=0.001)\n",
    "\n",
    "\n",
    "# ==========================================\n",
    "# [Cell 3] 모델 훈련 (Training)\n",
    "# ==========================================\n",
    "epochs = 5\n",
    "\n",
    "print(\"모델 훈련을 시작합니다...\")\n",
    "model.train() # 훈련 모드 설정 (Dropout 활성화)\n",
    "\n",
    "for epoch in range(epochs):\n",
    "    running_loss = 0.0\n",
    "    for batch_idx, (data, target) in enumerate(train_loader):\n",
    "        optimizer.zero_grad()    # 1. 기울기 초기화\n",
    "        output = model(data)     # 2. 순전파\n",
    "        loss = criterion(output, target) # 3. 손실 계산\n",
    "        loss.backward()          # 4. 역전파\n",
    "        optimizer.step()         # 5. 가중치 업데이트\n",
    "        \n",
    "        running_loss += loss.item()\n",
    "        if batch_idx % 200 == 199: # 200 배치마다 로그 출력\n",
    "            print(f'Epoch: {epoch + 1}/{epochs} | Batch: {batch_idx + 1} | Loss: {running_loss / 200:.4f}')\n",
    "            running_loss = 0.0\n",
    "\n",
    "\n",
    "# ==========================================\n",
    "# [Cell 4] 모델 성능 평가 (Evaluation)\n",
    "# ==========================================\n",
    "model.eval() # 평가 모드 설정 (Dropout 비활성화)\n",
    "test_loss = 0\n",
    "correct = 0\n",
    "\n",
    "with torch.no_grad(): # 기울기 계산 비활성화\n",
    "    for data, target in test_loader:\n",
    "        output = model(data)\n",
    "        test_loss += F.nll_loss(output, target, reduction='sum').item()\n",
    "        pred = output.argmax(dim=1, keepdim=True)\n",
    "        correct += pred.eq(target.view_as(pred)).sum().item()\n",
    "\n",
    "test_loss /= len(test_loader.dataset)\n",
    "accuracy = 100. * correct / len(test_loader.dataset)\n",
    "\n",
    "print(f'\\n[최종 테스트 결과] 평균 Loss: {test_loss:.4f} | 정확도: {accuracy:.2f}%')\n",
    "\n",
    "\n",
    "# ==========================================\n",
    "# [Cell 5] 예측 결과 시각화\n",
    "# ==========================================\n",
    "# 테스트 데이터 배치 1개 가져오기\n",
    "dataiter = iter(test_loader)\n",
    "images, labels = next(dataiter)\n",
    "\n",
    "# 예측 수행\n",
    "model.eval()\n",
    "with torch.no_grad():\n",
    "    outputs = model(images)\n",
    "    _, preds = torch.max(outputs, 1)\n",
    "\n",
    "# 첫 6개 이미지 시각화\n",
    "fig = plt.figure(figsize=(10, 6))\n",
    "for i in range(6):\n",
    "    ax = fig.add_subplot(2, 3, i + 1, xticks=[], yticks=[])\n",
    "    img = images[i].numpy().squeeze() \n",
    "    ax.imshow(img, cmap='gray')\n",
    "    \n",
    "    true_label = labels[i].item()\n",
    "    pred_label = preds[i].item()\n",
    "    color = 'green' if true_label == pred_label else 'red'\n",
    "    ax.set_title(f'Pred: {pred_label} / True: {true_label}', color=color, fontweight='bold')\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.15"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
