{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "66c6a22c",
   "metadata": {},
   "source": [
    "# Part 7: 딥러닝 기초와 PyTorch\n",
    "\n",
    "**⬅️ 이전 시간: [Part 6: 머신러닝 모델링과 평가](../06_machine_learning/part_6_machine_learning.md)**\n",
    "**➡️ 다음 시간: [Part 7.1: 순환 신경망 (RNN)과 LSTM](./part_7.1_recurrent_neural_networks.md)**\n",
    "\n",
    "---\n",
    "\n",
    "<br>\n",
    "\n",
    "> ## 1. 학습 목표 (Learning Objectives)\n",
    ">\n",
    "> 이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.\n",
    "> \n",
    "> - 딥러닝과 머신러닝의 차이점을 설명하고, PyTorch의 역할을 이해할 수 있습니다.\n",
    "> - PyTorch `Tensor`를 다루고, `nn.Module`을 상속받아 직접 인공 신경망 모델을 설계할 수 있습니다.\n",
    "> - 손실 함수, 옵티마이저의 역할을 이해하고, 딥러닝의 핵심인 학습 루프(Training Loop)를 구현할 수 있습니다.\n",
    "> - 순전파(Forward Propagation)와 역전파(Backward Propagation)의 기본 개념을 설명할 수 있습니다.\n",
    "> - `torchvision`을 사용하여 표준 데이터셋(MNIST)을 불러오고, 간단한 CNN 모델을 만들어 이미지 분류 문제를 해결할 수 있습니다.\n",
    "\n",
    "<br>\n",
    "\n",
    "> ## 2. 핵심 요약 (Key Summary)\n",
    "> \n",
    "> 딥러닝은 Scikit-learn과 달리 PyTorch와 같은 '조립 키트'를 사용하여 모델을 직접 설계합니다. 데이터의 기본 단위인 텐서(Tensor)를 다루고, `nn.Module`을 상속받아 신경망을 구성합니다. 학습은 모델의 예측(순전파)과 실제 값의 오차를 계산(손실 함수)하고, 이 오차를 기반으로 파라미터를 조정(옵티마이저, 역전파)하는 과정을 반복(학습 루프)하는 것입니다. 이 과정을 통해 MNIST 같은 이미지 데이터를 분류하는 CNN 모델까지 구현할 수 있습니다.\n",
    "\n",
    "<br>\n",
    "\n",
    "> ## 3. 도입: 모델을 직접 조립하는 즐거움, PyTorch (Introduction)\n",
    ">\n",
    "> 드디어 딥러닝의 세계에 입문합니다. 지난 시간까지 우리는 Scikit-learn이라는 강력한 '완성품'을 사용했다면, 이제부터는 **PyTorch**라는 '조립 키트'를 사용하여 우리만의 모델을 직접 만들어 볼 것입니다.\n",
    "> \n",
    "> > [!NOTE] 비유: PyTorch는 '레고 블록', Scikit-learn은 '완성된 장난감'\n",
    "> > - **Scikit-learn**: '트럭', '비행기'처럼 이미 완성된 모델을 가져와 사용하는 **'완성품 장난감'**과 같습니다. 사용하기 편리하고 대부분의 정형 데이터 문제에 효과적입니다.\n",
    "> > - **PyTorch**: 다양한 모양의 **'레고 블록'**을 제공하여, 사용자가 원하는 어떤 복잡한 구조(신경망)라도 직접 조립할 수 있게 해줍니다. 이미지, 텍스트 등 비정형 데이터를 다루는 딥러닝에 필수적이며, 연구와 산업 현장에서 가장 널리 쓰이는 도구 중 하나입니다.\n",
    "> \n",
    "> ### 3.1. 왜 딥러닝인가? 머신러닝의 한계와 표현 학습\n",
    "> \n",
    "> 그렇다면 왜 Scikit-learn의 편리한 모델들을 두고, 굳이 복잡하게 신경망을 직접 조립해야 할까요? 이는 전통적인 머신러닝이 **비정형 데이터(이미지, 텍스트, 음성 등)**를 다루는 데 명확한 한계를 가지고 있기 때문입니다.\n",
    "> \n",
    "> - **전통적인 머신러닝의 한계: 수동 특징 공학 (Manual Feature Engineering)**\n",
    ">   - 머신러닝 모델이 좋은 성능을 내기 위해서는, 데이터의 특성을 잘 나타내는 '특징(Feature)'을 사람이 직접 설계하고 추출해야 했습니다. 예를 들어, 고양이 사진을 분류하기 위해 '귀가 뾰족한가?', '수염이 있는가?'와 같은 특징을 코드로 구현해야 했습니다. 이 과정은 매우 어렵고, 많은 시간과 도메인 지식이 필요하며, 데이터가 복잡해질수록 사실상 불가능에 가깝습니다.\n",
    "> \n",
    "> - **딥러닝의 혁신: 표현 학습 (Representation Learning)**\n",
    ">   - 딥러닝은 이 **특징 추출 과정을 자동화**합니다. 개발자는 모델의 큰 구조만 설계하고 데이터를 보여주기만 하면, 모델이 데이터로부터 직접 문제 해결에 필요한 특징들을 **스스로 학습**합니다. 이것이 바로 **표현 학습**입니다.\n",
    ">   - 딥러닝 모델의 '깊은(deep)' 여러 계층들은, 데이터의 원초적인 형태(픽셀, 단어)로부터 시작하여 점차 더 복잡하고 추상적인 특징(선 -> 형태 -> 객체)을 학습해 나갑니다.\n",
    "> \n",
    "> ![ML vs DL](https://i.imgur.com/x0G3R9a.png)\n",
    "> \n",
    "> > [!TIP]\n",
    "> > 본 파트의 모든 예제 코드는 `../../source_code/part_7_deep_learning.py` 파일에서 직접 실행하고 수정해볼 수 있습니다.\n",
    "\n",
    "---\n",
    "\n",
    "<br>\n",
    "\n",
    "> ## 4. PyTorch 딥러닝의 기본 구성 요소\n",
    "> \n",
    "> > **🎯 1-2일차 목표:** PyTorch의 텐서를 이해하고, 인공 신경망의 기본 구조를 코드로 구현합니다.\n",
    "> \n",
    "> ### 4-1. 딥러닝의 데이터, 텐서(Tensor)\n",
    "> - **텐서**: PyTorch에서 데이터를 다루는 기본 단위입니다. NumPy의 `ndarray`와 매우 유사하지만, 두 가지 결정적인 차이가 있습니다.\n",
    ">     1.  **GPU 가속**: `.to('cuda')` 코드 한 줄로 GPU를 사용한 초고속 연산이 가능합니다.\n",
    ">     2.  **자동 미분(Autograd)**: 딥러닝 학습의 핵심인 '역전파'를 위해 자동으로 미분값을 계산해줍니다.\n",
    "> \n",
    "> ### 4-2. 인공 신경망(ANN)과 다층 퍼셉트론(MLP) 모델 만들기\n",
    "> PyTorch에서는 `torch.nn.Module`을 상속받아 우리만의 신경망 클래스(레고 조립 설명서)를 정의합니다. 가장 기본적이면서도 강력한 신경망 구조 중 하나는 바로 **다층 퍼셉트론(Multi-Layer Perceptron, MLP)** 입니다. 아래 `SimpleNet` 예제가 바로 MLP의 간단한 구현체입니다.\n",
    "> \n",
    "> - **`__init__(self)`**: 모델에 필요한 레고 블록들(레이어)을 정의하는 곳.\n",
    "> - **`forward(self, x)`**: 레고 블록들을 어떤 순서로 조립할지(데이터 흐름) 정의하는 곳.\n",
    "> \n",
    "> "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1952e29b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "\n",
    "# 모델 정의 (MLP 예시)\n",
    "class SimpleNet(nn.Module):\n",
    "    # 1. 필요한 레고 블록 선언\n",
    "    def __init__(self, input_size, hidden_size, num_classes):\n",
    "        super(SimpleNet, self).__init__()\n",
    "        self.fc1 = nn.Linear(input_size, hidden_size) # 완전 연결 계층 (Input -> Hidden)\n",
    "        self.relu = nn.ReLU()                         # 활성화 함수\n",
    "        self.fc2 = nn.Linear(hidden_size, num_classes)  # 완전 연결 계층 (Hidden -> Output)\n",
    "\n",
    "    # 2. 레고 블록 조립 순서 정의\n",
    "    def forward(self, x):\n",
    "        out = self.fc1(x)\n",
    "        out = self.relu(out) # 💡 비선형성을 추가하여 모델의 표현력을 높임\n",
    "        out = self.fc2(out)\n",
    "        return out\n",
    "\n",
    "model = SimpleNet(input_size=784, hidden_size=500, num_classes=10) # MNIST 예시\n",
    "print(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "280cdaeb",
   "metadata": {},
   "source": [
    "\n",
    "> \n",
    "> ### 심화: 활성화 함수, 왜 필요한가? (Deep Dive: Activation Functions)\n",
    "> \n",
    "> **Q: 왜 굳이 `ReLU` 같은 활성화 함수를 중간에 삽입해야 할까요? 선형 계층(`nn.Linear`)만 여러 개 쌓으면 더 간단하지 않을까요?**\n",
    "> \n",
    "> **A: 모델에 '비선형성(Non-linearity)'을 부여하기 위해서입니다.** 만약 활성화 함수 없이 선형 계층만 계속 쌓는다면, 이는 결국 하나의 거대한 선형 계층을 쌓은 것과 수학적으로 동일합니다.\n",
    "> \n",
    "> - **선형 변환의 한계**: 선형 변환은 데이터를 늘리거나, 줄이거나, 회전시키는 것은 가능하지만, 구불구불한 복잡한 형태의 데이터를 구분하는 경계선을 만들 수는 없습니다.\n",
    "> - **비선형성의 마법**: 활성화 함수는 이러한 선형 변환 결과에 '비틀기'나 '접기'와 같은 비선형적인 변환을 가합니다. 이 과정을 여러 층 반복함으로써, 신경망은 아무리 복잡한 데이터 분포라도 학습하고 구분해낼 수 있는 유연하고 강력한 표현력(Expressiveness)을 갖추게 됩니다.\n",
    "> \n",
    "> **주요 활성화 함수 비교**\n",
    "> \n",
    "> | 활성화 함수 | 수식 / 특징 | 장점 | 단점 | 주요 사용처 |\n",
    "> |---|---|---|---|---|\n",
    "> | **Sigmoid** | `1 / (1 + e^-x)`<br>출력을 (0, 1) 사이로 압축 | 출력을 확률처럼 해석 가능 | **Vanishing Gradient** 문제 발생<br>출력의 중심이 0이 아님 | 이진 분류 문제의 출력층 |\n",
    "> | **Tanh** | `(e^x - e^-x) / (e^x + e^-x)`<br>출력을 (-1, 1) 사이로 압축 | 출력의 중심이 0 (Sigmoid 단점 보완) | **Vanishing Gradient** 문제 여전히 존재 | RNN의 은닉층에서 종종 사용 |\n",
    "> | **ReLU** | `max(0, x)`<br>입력이 0보다 작으면 0, 크면 그대로 출력 | 계산이 매우 빠름<br>Vanishing Gradient 문제 해결 | **Dying ReLU** 문제 (입력이 음수면 뉴런이 죽음) | 대부분의 딥러닝 모델의 은닉층 (가장 대중적) |\n",
    "> | **Leaky ReLU** | `max(α*x, x)` (α는 작은 수, 예: 0.01)<br>ReLU의 변형 | Dying ReLU 문제 해결 | α 값을 직접 설정해야 함 | ReLU 대신 사용해볼 수 있는 대안 |\n",
    "> | **Softmax** | `e^zi / Σ(e^zj)`<br>다중 클래스 분류의 출력을 확률 분포로 변환 | 각 클래스에 대한 확률을 직관적으로 보여줌 | - | 다중 클래스 분류 문제의 출력층 |\n",
    "\n",
    "---\n",
    "\n",
    "<br>\n",
    "\n",
    "> ## 5. 딥러닝 모델 학습 과정\n",
    "> \n",
    "> > **🎯 3-4일차 목표:** 손실 함수, 옵티마이저의 개념을 이해하고 전체 학습 루프를 구현합니다.\n",
    "> \n",
    "> ### 5-1. 모델은 어떻게 학습하는가: 경사 하강법과 옵티마이저\n",
    "> \n",
    "> 딥러닝 학습의 목표는 손실 함수가 만든 '손실 지형(Loss Landscape)'의 가장 낮은 지점을 찾아가는 것입니다. 이 과정은 마치 안개 속에서 산을 내려오는 등산가와 같습니다.\n",
    "> \n",
    "> - **경사 하강법 (Gradient Descent)**: 등산가는 현재 위치에서 **경사가 가장 가파른 방향(Gradient)**을 따라 한 걸음씩 내려갑니다. 이 과정을 반복하면 결국 계곡의 가장 낮은 지점(최소 손실)에 도달할 수 있습니다. 여기서 '경사(Gradient)'가 바로 `loss.backward()`를 통해 계산되는 미분값입니다.\n",
    "> \n",
    "> - **옵티마이저 (Optimizer)**: '한 걸음'을 어떻게 내디딜지 결정하는 전략입니다.\n",
    ">     - 얼마나 큰 보폭으로 내려갈지 (**학습률, Learning Rate**)\n",
    ">     - 이전 걸음을 얼마나 참조할지 (**모멘텀, Momentum**)\n",
    ">     - 각 파라미터마다 보폭을 다르게 할지 등을 결정합니다.\n",
    "> \n",
    "> **대표적인 옵티마이저**\n",
    "> - **SGD (Stochastic Gradient Descent)**: 가장 기본적인 경사 하강법. 한 번에 하나의 데이터(또는 작은 배치)를 보고 파라미터를 업데이트합니다.\n",
    "> - **Adam (Adaptive Moment Estimation)**: SGD에 모멘텀과 적응적 학습률 개념을 더한, 현재 가장 널리 사용되는 안정적이고 성능 좋은 옵티마이저입니다. 대부분의 경우 Adam으로 시작하는 것이 좋습니다.\n",
    "> \n",
    "> ![Gradient Descent](https://i.imgur.com/sSgG4fV.gif)\n",
    "> \n",
    "> ### 5-2. 모델의 예측을 어떻게 평가할까? (손실 함수)\n",
    "> \n",
    "> 훌륭한 모델을 만들기 위해서는, 현재 모델이 얼마나 '잘하고' 또는 '못하고' 있는지를 측정할 객관적인 '평가 지표'가 필요합니다. 이 역할을 하는 것이 바로 **손실 함수(Loss Function)** 입니다.\n",
    "> \n",
    "> - **손실 함수(Loss Function)**: 모델의 예측값(`prediction`)과 실제 정답(`target`) 사이의 차이, 즉 **'오차(Error)'를 계산하는 함수**입니다. 딥러닝 학습의 목표는 이 손실 함수가 계산한 값을 **최소화(Minimize)**하는 것입니다.\n",
    "> - **옵티마이저(Optimizer)**: 손실 함수가 계산한 오차를 기반으로, 모델의 파라미터(가중치)를 어느 방향으로 얼마나 업데이트할지 결정하는 **'조율사'**. (예: `torch.optim.Adam`)\n",
    "> \n",
    "> ### 심화: 대표적인 손실 함수 (MSE vs Cross-Entropy)\n",
    "> \n",
    "> 손실 함수는 해결하려는 문제의 종류에 따라 적절한 것을 선택해야 합니다.\n",
    "> \n",
    "> #### 1. 평균 제곱 오차 (Mean Squared Error, MSE)\n",
    "> - **개념**: 예측값과 실제 정답 사이의 '거리'를 기반으로 오차를 측정합니다. 각 데이터 포인트의 오차를 제곱하고, 이를 모두 더한 후 평균을 냅니다.\n",
    "> - **언제 사용하나?**: **회귀(Regression)** 문제. (예: 집값 예측, 주가 예측 등 연속적인 값을 예측할 때)\n",
    "> - **PyTorch**: `nn.MSELoss()`\n",
    "> - **예시**:\n",
    ">   - 정답: `25.0` / 예측: `24.5` -> 오차: `(25.0 - 24.5)^2 = 0.25`\n",
    ">   - 정답: `25.0` / 예측: `20.0` -> 오차: `(25.0 - 20.0)^2 = 25.0` (오차가 클수록 손실이 기하급수적으로 커짐)\n",
    "> \n",
    "> #### 2. 교차 엔트로피 오차 (Cross-Entropy Loss)\n",
    "> - **개념**: 모델이 예측한 '확률 분포'와 실제 정답의 '확률 분포'가 얼마나 다른지를 측정합니다. 모델이 정답 클래스를 얼마나 높은 확률로 예측했는지에 따라 손실이 결정됩니다.\n",
    "> - **수식**:\n",
    ">   \\[ L_{CE} = -\\sum_{i=1}^{C} y_i \\log(\\hat{y}_i) \\]\n",
    ">   여기서 \\(C\\)는 클래스의 수, \\(y_i\\)는 \\(i\\)번째 클래스에 대한 실제 값(정답은 1, 나머지는 0), \\(\\hat{y}_i\\)는 모델이 예측한 \\(i\\)번째 클래스에 대한 확률입니다.\n",
    "> - **언제 사용하나?**: **분류(Classification)** 문제. (예: 이미지 분류, 스팸 메일 분류 등 카테고리를 예측할 때)\n",
    "> - **PyTorch**: `nn.CrossEntropyLoss()`\n",
    ">   - `nn.CrossEntropyLoss`는 내부적으로 `Softmax` 함수를 적용한 후 `NLLLoss`를 계산하므로, 모델의 마지막 레이어에 별도의 `Softmax`를 추가할 필요가 없습니다.\n",
    "> - **예시 (3개 클래스 분류)**:\n",
    ">   - 정답: `[1, 0, 0]` (첫 번째 클래스가 정답)\n",
    ">   - 예측 A (잘 맞춤): `[0.9, 0.05, 0.05]` -> 낮은 손실(Loss)\n",
    ">   - 예측 B (못 맞춤): `[0.1, 0.8, 0.1]` -> 높은 손실(Loss)\n",
    "> \n",
    "> ### 5-3. 핵심 프로세스: 학습 루프(Training Loop)\n",
    "> 딥러닝 모델 학습은 '에포크(Epoch)'라는 단위로 반복됩니다. 1 에포크는 전체 훈련 데이터를 한 번 모두 사용하는 것을 의미하며, 학습은 다음 4단계로 이루어집니다.\n",
    "> \n",
    "> 1.  **`model(inputs)`**: 모델에 입력을 넣어 예측값(`outputs`)을 계산합니다 (**순전파**).\n",
    "> 2.  **`loss = criterion(outputs, labels)`**: 예측값과 실제 정답을 비교하여 오차(`loss`)를 계산합니다.\n",
    "> 3.  **`optimizer.zero_grad()`**: 이전 스텝의 기울기 값을 초기화합니다. (이걸 해주지 않으면 기울기가 계속 누적됩니다.)\n",
    "> 4.  **`loss.backward()`**: 계산된 오차를 기반으로, 각 파라미터가 오차에 얼마나 기여했는지 미분값을 계산합니다 (**역전파**).\n",
    "> 5.  **`optimizer.step()`**: 계산된 미분값을 바탕으로 모델의 파라미터를 업데이트하여 오차를 줄이는 방향으로 나아갑니다.\n",
    "> \n",
    "> > [!NOTE] 순전파와 역전파 (Forward & Backward Propagation)\n",
    "> > - **순전파 (Forward)**: 내가 만든 레시피로 요리를 해서(모델 예측) 손님에게 내놓는 과정.\n",
    "> > - **역전파 (Backward)**: 손님의 피드백(\"너무 짜요!\")을 듣고, 소금을 얼마나 넣었는지, 간장을 얼마나 넣었는지 원인을 **연쇄 법칙(Chain Rule)**에 따라 거슬러 올라가 각 재료(파라미터)의 '책임'을 계산하는 과정. `optimizer.step()`은 이 분석을 바탕으로 다음 요리에서는 소금을 덜 넣는 행동입니다.\n",
    "> \n",
    "> ### 5-4. 학습, 검증, 그리고 추론 (Training, Validation, and Inference)\n",
    "> \n",
    "> 딥러닝 모델의 생명주기는 크게 '학습', '검증', '추론' 세 단계로 나뉩니다.\n",
    "> \n",
    "> - **학습 (Training)**: `train_loader` 와 같이 훈련용 데이터를 사용하여 모델의 파라미터(가중치)를 최적화하는 과정입니다. 위에서 설명한 **학습 루프**가 바로 이 단계에 해당합니다. 모델은 정답을 보면서 오차를 줄여나갑니다.\n",
    "> - **검증 (Validation)**: 학습 과정 중에 모델이 얼마나 잘 학습되고 있는지, 과적합(Overfitting)은 발생하지 않는지 확인하기 위한 단계입니다. 학습에 사용되지 않은 `validation_loader` 데이터로 모델의 성능을 주기적으로 평가합니다. 이 평가 결과를 바탕으로 학습률(Learning Rate)을 조절하거나 학습을 조기 종료하는 등의 결정을 내립니다.\n",
    "> - **추론 (Inference)**: 학습이 완료된 모델을 사용하여 새로운 데이터에 대한 예측을 수행하는 단계입니다. 이 단계에서는 더 이상 모델의 가중치를 업데이트하지 않으므로, 역전파나 옵티마이저가 필요 없습니다. `torch.no_grad()` 컨텍스트 안에서 실행하여 메모리 사용량을 줄이고 계산 속도를 높이는 것이 일반적입니다. 서비스 배포 환경에서 모델이 사용되는 단계가 바로 추론입니다.\n",
    "> \n",
    "> ### 5-5. 딥러닝 모델의 과적합 방지 기술 (Regularization)\n",
    "> \n",
    "> 딥러닝 모델은 파라미터가 매우 많기 때문에, 머신러닝 모델보다 과적합(Overfitting)의 위험이 훨씬 더 큽니다. 모델이 훈련 데이터에만 과도하게 최적화되는 것을 막고, 일반화 성능을 높이기 위해 다양한 정규화(Regularization) 기법이 사용됩니다.\n",
    "> \n",
    "> - **드롭아웃 (Dropout)**\n",
    ">   - **원리**: 훈련 과정에서 각 뉴런을 정해진 확률(e.g., p=0.5)로 무작위로 '비활성화'시킵니다. 매번 다른 구성의 더 작은 네트워크를 학습시키는 효과를 줍니다.\n",
    ">   - **효과**: 특정 뉴런에 과도하게 의존하는 것을 방지하고, 모델이 더 강건한(robust) 특징을 학습하도록 강제합니다. (비유: 팀 프로젝트에서 매번 몇몇 팀원이 무작위로 결석하여, 남은 사람들만으로 과제를 해결하게 만들어 개개인의 역량을 키우는 것)\n",
    ">   - **구현**: `nn.Dropout(p=0.5)`\n",
    "> \n",
    "> - **배치 정규화 (Batch Normalization)**\n",
    ">   - **원리**: 각 레이어의 입력 분포를 평균 0, 분산 1로 정규화하여, 학습 과정에서 각 층의 입력 분포가 심하게 변동하는 **내부 공변량 변화(Internal Covariate Shift)** 문제를 완화합니다.\n",
    ">   - **효과**:\n",
    ">     - **학습 속도 향상**: 그래디언트 흐름을 원활하게 하여 학습을 안정시키고 속도를 높입니다.\n",
    ">     - **규제 효과**: 미니배치의 통계량에 따라 출력이 조금씩 달라지므로, 약간의 노이즈를 추가하는 효과가 있어 드롭아웃과 유사한 규제 역할을 합니다.\n",
    ">     - **초기값 민감도 감소**: 초기 가중치 설정에 덜 민감해집니다.\n",
    ">   - **구현**: `nn.BatchNorm1d`, `nn.BatchNorm2d`\n",
    "> \n",
    "> > [!IMPORTANT]\n",
    "> > `model.train()`과 `model.eval()` 모드를 반드시 전환해주어야 합니다.\n",
    "> > - `model.train()`: 드롭아웃과 배치 정규화를 **활성화**합니다.\n",
    "> > - `model.eval()`: 드롭아웃과 배치 정규화를 **비활성화**하고, 학습 과정에서 계산해둔 전체 데이터의 통계량을 사용하여 일관된 출력을 보장합니다. 검증 및 추론 시에는 반드시 `eval()` 모드를 사용해야 합니다.\n",
    "\n",
    "---\n",
    "\n",
    "<br>\n",
    "\n",
    "> ## 6. 연습 문제: MNIST 손글씨 분류\n",
    "> \n",
    "> > **🎯 5일차 목표:** PyTorch를 사용하여 딥러닝의 \"Hello, World!\"인 MNIST 손글씨 분류기를 직접 만듭니다.\n",
    "> \n",
    "> ### 문제:\n",
    "> `torchvision`을 사용하여 MNIST 데이터셋을 불러오고, 간단한 CNN(합성곱 신경망) 모델을 구축하여 손글씨 숫자를 분류하는 전체 코드를 완성하세요.\n",
    "> \n",
    "> "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "742fc07e",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torchvision\n",
    "import torchvision.transforms as transforms\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "# 1. 데이터셋 및 로더 준비\n",
    "transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])\n",
    "train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)\n",
    "test_dataset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform)\n",
    "train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)\n",
    "test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)\n",
    "\n",
    "# 2. CNN 모델 정의\n",
    "class ConvNet(nn.Module):\n",
    "    def __init__(self):\n",
    "        super(ConvNet, self).__init__()\n",
    "        self.layer1 = nn.Sequential(\n",
    "            nn.Conv2d(1, 32, kernel_size=5, stride=1, padding=2),\n",
    "            nn.ReLU(),\n",
    "            nn.MaxPool2d(kernel_size=2, stride=2))\n",
    "        self.layer2 = nn.Sequential(\n",
    "            nn.Conv2d(32, 64, kernel_size=5, stride=1, padding=2),\n",
    "            nn.ReLU(),\n",
    "            nn.MaxPool2d(kernel_size=2, stride=2))\n",
    "        self.fc = nn.Linear(7*7*64, 10)\n",
    "\n",
    "    def forward(self, x):\n",
    "        out = self.layer1(x)\n",
    "        out = self.layer2(out)\n",
    "        out = out.reshape(out.size(0), -1) # Flatten\n",
    "        out = self.fc(out)\n",
    "        return out\n",
    "\n",
    "# 3. 모델, 손실함수, 옵티마이저 설정\n",
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "model = ConvNet().to(device)\n",
    "criterion = nn.CrossEntropyLoss()\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=0.001)\n",
    "\n",
    "# 4. 모델 학습\n",
    "num_epochs = 5\n",
    "for epoch in range(num_epochs):\n",
    "    for i, (images, labels) in enumerate(train_loader):\n",
    "        images = images.to(device)\n",
    "        labels = 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",
    "        if (i+1) % 100 == 0:\n",
    "            print(f'Epoch [{epoch+1}/{num_epochs}], Step [{i+1}/{len(train_loader)}], Loss: {loss.item():.4f}')\n",
    "\n",
    "# 5. 모델 평가\n",
    "model.eval()  # 평가 모드\n",
    "with torch.no_grad():\n",
    "    correct = 0\n",
    "    total = 0\n",
    "    for images, labels in test_loader:\n",
    "        images = images.to(device)\n",
    "        labels = labels.to(device)\n",
    "        outputs = model(images)\n",
    "        _, predicted = torch.max(outputs.data, 1)\n",
    "        total += labels.size(0)\n",
    "        correct += (predicted == labels).sum().item()\n",
    "\n",
    "    print(f'Test Accuracy of the model on the 10000 test images: {100 * correct / total} %')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed49ba70",
   "metadata": {},
   "source": [
    "\n",
    "\n",
    "---\n",
    "\n",
    "<br>\n",
    "\n",
    "> ## 7. 마무리 및 다음 파트 예고\n",
    "> \n",
    "> 이번 파트에서는 PyTorch를 사용하여 딥러닝의 기본 개념부터 실제 모델 구현까지의 여정을 함께했습니다. 이제 여러분은 딥러닝 모델을 '조립'하는 데 필요한 핵심 부품들(텐서, `nn.Module`, 손실 함수, 옵티마이저)을 이해하고, 이를 조합하여 학습 루프를 통해 모델을 훈련시킬 수 있습니다.\n",
    "> \n",
    "> 우리가 만든 간단한 CNN 모델이 어떻게 수많은 손글씨 이미지를 보고 99%에 가까운 정확도로 숫자를 맞추는지, 그 과정의 신비로움을 조금이나마 느끼셨기를 바랍니다.\n",
    "> \n",
    "> 다음 파트에서는 딥러닝의 또 다른 중요한 축인 **순차 데이터(Sequential Data)**를 다루는 방법을 배웁니다. 주가, 문장, 음악과 같이 '순서'가 중요한 데이터를 처리하기 위해 탄생한 **순환 신경망(Recurrent Neural Network, RNN)**과 그 발전된 형태인 **LSTM**에 대해 깊이 있게 탐구할 것입니다.\n",
    "> \n",
    "> > [!TIP]\n",
    "> > **다음으로 무엇을 해야 할까요?**\n",
    "> > - `source_code/part_7_deep_learning.py` 파일의 코드를 실행하며, 하이퍼파라미터(학습률, 배치 크기, 에포크 수)를 변경해보세요. 성능이 어떻게 변하는지 관찰하는 것은 매우 중요합니다.\n",
    "> > - `ConvNet` 모델의 구조를 바꿔보세요. `nn.Conv2d` 레이어를 더 추가하거나, `kernel_size`, `hidden_size`를 변경해보는 등 다양한 실험을 해보세요.\n",
    "> > - `torchvision.datasets.CIFAR10`과 같이 다른 데이터셋에 도전해보세요. (이미지 크기와 채널 수가 다르므로 모델 구조 수정이 필요합니다.)\n",
    "> > - 궁금한 점이 있다면 언제든지 질문해주세요!\n",
    "\n",
    "---\n",
    "\n",
    "**⬅️ 이전 시간: [Part 6: 머신러닝 모델링과 평가](../06_machine_learning/part_6_machine_learning.md)**\n",
    "**➡️ 다음 시간: [Part 7.1: 순환 신경망 (RNN)과 LSTM](./part_7.1_recurrent_neural_networks.md)**"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}
