# Part 7: 딥러닝 기초와 PyTorch

**⬅️ 이전 시간: [Part 6: 머신러닝 모델링과 평가](../06_machine_learning/part_6_machine_learning.md)**
**➡️ 다음 시간: [Part 7.1: 순환 신경망 (RNN)과 LSTM](./part_7.1_recurrent_neural_networks.md)**

---

## 1. 학습 목표 (Learning Objectives)

이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.

- 딥러닝과 머신러닝의 차이점을 설명하고, PyTorch의 역할을 이해할 수 있습니다.
- PyTorch `Tensor`를 다루고, `nn.Module`을 상속받아 직접 인공 신경망 모델을 설계할 수 있습니다.
- 손실 함수, 옵티마이저의 역할을 이해하고, 딥러닝의 핵심인 학습 루프(Training Loop)를 구현할 수 있습니다.
- 순전파(Forward Propagation)와 역전파(Backward Propagation)의 기본 개념을 설명할 수 있습니다.
- `torchvision`을 사용하여 표준 데이터셋(MNIST)을 불러오고, 간단한 CNN 모델을 만들어 이미지 분류 문제를 해결할 수 있습니다.

## 핵심 요약 (Key takeaways)

딥러닝은 Scikit-learn과 달리 PyTorch와 같은 '조립 키트'를 사용하여 모델을 직접 설계합니다. 데이터의 기본 단위인 텐서(Tensor)를 다루고, `nn.Module`을 상속받아 신경망을 구성합니다. 학습은 모델의 예측(순전파)과 실제 값의 오차를 계산(손실 함수)하고, 이 오차를 기반으로 파라미터를 조정(옵티마이저, 역전파)하는 과정을 반복(학습 루프)하는 것입니다. 이 과정을 통해 MNIST 같은 이미지 데이터를 분류하는 CNN 모델까지 구현할 수 있습니다.

## 3. 도입: 모델을 직접 조립하는 즐거움, PyTorch (Introduction)

드디어 딥러닝의 세계에 입문합니다. 지난 시간까지 우리는 Scikit-learn이라는 강력한 '완성품'을 사용했다면, 이제부터는 **PyTorch**라는 '조립 키트'를 사용하여 우리만의 모델을 직접 만들어 볼 것입니다.

> **💡 비유: PyTorch는 '레고 블록', Scikit-learn은 '완성된 장난감'**
> - **Scikit-learn**: '트럭', '비행기'처럼 이미 완성된 모델을 가져와 사용하는 **'완성품 장난감'**과 같습니다. 사용하기 편리하고 대부분의 정형 데이터 문제에 효과적입니다.
> - **PyTorch**: 다양한 모양의 **'레고 블록'**을 제공하여, 사용자가 원하는 어떤 복잡한 구조(신경망)라도 직접 조립할 수 있게 해줍니다. 이미지, 텍스트 등 비정형 데이터를 다루는 딥러닝에 필수적이며, 연구와 산업 현장에서 가장 널리 쓰이는 도구 중 하나입니다.

> [!TIP]
> 본 파트의 모든 예제 코드는 `../../source_code/part_7_deep_learning.py` 파일에서 직접 실행하고 수정해볼 수 있습니다.

---

## 4. PyTorch 딥러닝의 기본 구성 요소

> **🎯 1-2일차 목표:** PyTorch의 텐서를 이해하고, 인공 신경망의 기본 구조를 코드로 구현합니다.

### 4-1. 딥러닝의 데이터, 텐서(Tensor)
- **텐서**: PyTorch에서 데이터를 다루는 기본 단위입니다. NumPy의 `ndarray`와 매우 유사하지만, 두 가지 결정적인 차이가 있습니다.
    1.  **GPU 가속**: `.to('cuda')` 코드 한 줄로 GPU를 사용한 초고속 연산이 가능합니다.
    2.  **자동 미분(Autograd)**: 딥러닝 학습의 핵심인 '역전파'를 위해 자동으로 미분값을 계산해줍니다.

### 4-2. 인공 신경망(ANN)과 다층 퍼셉트론(MLP) 모델 만들기
PyTorch에서는 `torch.nn.Module`을 상속받아 우리만의 신경망 클래스(레고 조립 설명서)를 정의합니다. 가장 기본적이면서도 강력한 신경망 구조 중 하나는 바로 **다층 퍼셉트론(Multi-Layer Perceptron, MLP)** 입니다. 아래 `SimpleNet` 예제가 바로 MLP의 간단한 구현체입니다.

- **`__init__(self)`**: 모델에 필요한 레고 블록들(레이어)을 정의하는 곳.
- **`forward(self, x)`**: 레고 블록들을 어떤 순서로 조립할지(데이터 흐름) 정의하는 곳.

```python
import torch
import torch.nn as nn

# 모델 정의 (MLP 예시)
class SimpleNet(nn.Module):
    # 1. 필요한 레고 블록 선언
    def __init__(self, input_size, hidden_size, num_classes):
        super(SimpleNet, self).__init__()
        self.fc1 = nn.Linear(input_size, hidden_size) # 완전 연결 계층 (Input -> Hidden)
        self.relu = nn.ReLU()                         # 활성화 함수
        self.fc2 = nn.Linear(hidden_size, num_classes)  # 완전 연결 계층 (Hidden -> Output)

    # 2. 레고 블록 조립 순서 정의
    def forward(self, x):
        out = self.fc1(x)
        out = self.relu(out) # 💡 비선형성을 추가하여 모델의 표현력을 높임
        out = self.fc2(out)
        return out

model = SimpleNet(input_size=784, hidden_size=500, num_classes=10) # MNIST 예시
print(model)
```
> ### 심화: 활성화 함수, 왜 필요한가? (Deep Dive: Activation Functions)
> 
> **Q: 왜 굳이 `ReLU` 같은 활성화 함수를 중간에 삽입해야 할까요? 선형 계층(`nn.Linear`)만 여러 개 쌓으면 더 간단하지 않을까요?**
> 
> **A: 모델에 '비선형성(Non-linearity)'을 부여하기 위해서입니다.** 만약 활성화 함수 없이 선형 계층만 계속 쌓는다면, 이는 결국 하나의 거대한 선형 계층을 쌓은 것과 수학적으로 동일합니다.
> 
> - **선형 변환의 한계**: 선형 변환은 데이터를 늘리거나, 줄이거나, 회전시키는 것은 가능하지만, 구불구불한 복잡한 형태의 데이터를 구분하는 경계선을 만들 수는 없습니다.
> - **비선형성의 마법**: 활성화 함수는 이러한 선형 변환 결과에 '비틀기'나 '접기'와 같은 비선형적인 변환을 가합니다. 이 과정을 여러 층 반복함으로써, 신경망은 아무리 복잡한 데이터 분포라도 학습하고 구분해낼 수 있는 유연하고 강력한 표현력(Expressiveness)을 갖추게 됩니다.
> 
> **주요 활성화 함수 비교**
> 
> | 활성화 함수 | 수식 / 특징 | 장점 | 단점 | 주요 사용처 |
> |---|---|---|---|---|
> | **Sigmoid** | `1 / (1 + e^-x)`<br>출력을 (0, 1) 사이로 압축 | 출력을 확률처럼 해석 가능 | **Vanishing Gradient** 문제 발생<br>출력의 중심이 0이 아님 | 이진 분류 문제의 출력층 |
> | **Tanh** | `(e^x - e^-x) / (e^x + e^-x)`<br>출력을 (-1, 1) 사이로 압축 | 출력의 중심이 0 (Sigmoid 단점 보완) | **Vanishing Gradient** 문제 여전히 존재 | RNN의 은닉층에서 종종 사용 |
> | **ReLU** | `max(0, x)`<br>입력이 0보다 작으면 0, 크면 그대로 출력 | 계산이 매우 빠름<br>Vanishing Gradient 문제 해결 | **Dying ReLU** 문제 (입력이 음수면 뉴런이 죽음) | 대부분의 딥러닝 모델의 은닉층 (가장 대중적) |
> | **Leaky ReLU** | `max(α*x, x)` (α는 작은 수, 예: 0.01)<br>ReLU의 변형 | Dying ReLU 문제 해결 | α 값을 직접 설정해야 함 | ReLU 대신 사용해볼 수 있는 대안 |
>| **Softmax** | `e^zi / Σ(e^zj)`<br>다중 클래스 분류의 출력을 확률 분포로 변환 | 각 클래스에 대한 확률을 직관적으로 보여줌 | - | 다중 클래스 분류 문제의 출력층 |
> 
> 
> ---
> 
> ## 5. 딥러닝 모델 학습 과정
> 
> > **🎯 3-4일차 목표:** 손실 함수, 옵티마이저의 개념을 이해하고 전체 학습 루프를 구현합니다.
> 
> ### 5-1. 모델의 예측을 어떻게 평가할까? (손실 함수)
> 
> 훌륭한 모델을 만들기 위해서는, 현재 모델이 얼마나 '잘하고' 또는 '못하고' 있는지를 측정할 객관적인 '평가 지표'가 필요합니다. 이 역할을 하는 것이 바로 **손실 함수(Loss Function)** 입니다.
> 
> - **손실 함수(Loss Function)**: 모델의 예측값(`prediction`)과 실제 정답(`target`) 사이의 차이, 즉 **'오차(Error)'를 계산하는 함수**입니다. 딥러닝 학습의 목표는 이 손실 함수가 계산한 값을 **최소화(Minimize)**하는 것입니다.
> - **옵티마이저(Optimizer)**: 손실 함수가 계산한 오차를 기반으로, 모델의 파라미터(가중치)를 어느 방향으로 얼마나 업데이트할지 결정하는 **'조율사'**. (예: `torch.optim.Adam`)
> 
> ### 심화: 대표적인 손실 함수 (MSE vs Cross-Entropy)
> 
> 손실 함수는 해결하려는 문제의 종류에 따라 적절한 것을 선택해야 합니다.
> 
> #### 1. 평균 제곱 오차 (Mean Squared Error, MSE)
> - **개념**: 예측값과 실제 정답 사이의 '거리'를 기반으로 오차를 측정합니다. 각 데이터 포인트의 오차를 제곱하고, 이를 모두 더한 후 평균을 냅니다.
> - **언제 사용하나?**: **회귀(Regression)** 문제. (예: 집값 예측, 주가 예측 등 연속적인 값을 예측할 때)
> - **PyTorch**: `nn.MSELoss()`
> - **예시**:
>   - 정답: `25.0` / 예측: `24.5` -> 오차: `(25.0 - 24.5)^2 = 0.25`
>   - 정답: `25.0` / 예측: `20.0` -> 오차: `(25.0 - 20.0)^2 = 25.0` (오차가 클수록 손실이 기하급수적으로 커짐)
> 
> #### 2. 교차 엔트로피 오차 (Cross-Entropy Loss)
> - **개념**: 모델이 예측한 '확률 분포'와 실제 정답의 '확률 분포'가 얼마나 다른지를 측정합니다. 모델이 정답 클래스를 얼마나 높은 확률로 예측했는지에 따라 손실이 결정됩니다.
> - **수식**:
>   \[ L_{CE} = -\sum_{i=1}^{C} y_i \log(\hat{y}_i) \]
>   여기서 \(C\)는 클래스의 수, \(y_i\)는 \(i\)번째 클래스에 대한 실제 값(정답은 1, 나머지는 0), \(\hat{y}_i\)는 모델이 예측한 \(i\)번째 클래스에 대한 확률입니다.
> - **언제 사용하나?**: **분류(Classification)** 문제. (예: 이미지 분류, 스팸 메일 분류 등 카테고리를 예측할 때)
> - **PyTorch**: `nn.CrossEntropyLoss()`
>   - `nn.CrossEntropyLoss`는 내부적으로 `Softmax` 함수를 적용한 후 `NLLLoss`를 계산하므로, 모델의 마지막 레이어에 별도의 `Softmax`를 추가할 필요가 없습니다.
> - **예시 (3개 클래스 분류)**:
>   - 정답: `[1, 0, 0]` (첫 번째 클래스가 정답)
>   - 예측 A (잘 맞춤): `[0.9, 0.05, 0.05]` -> 낮은 손실(Loss)
>   - 예측 B (못 맞춤): `[0.1, 0.8, 0.1]` -> 높은 손실(Loss)
> 
> ### 5-2. 핵심 프로세스: 학습 루프(Training Loop)
> 딥러닝 모델 학습은 '에포크(Epoch)'라는 단위로 반복됩니다. 1 에포크는 전체 훈련 데이터를 한 번 모두 사용하는 것을 의미하며, 학습은 다음 4단계로 이루어집니다.
> 
> 1.  **`model(inputs)`**: 모델에 입력을 넣어 예측값(`outputs`)을 계산합니다 (**순전파**).
> 2.  **`loss = criterion(outputs, labels)`**: 예측값과 실제 정답을 비교하여 오차(`loss`)를 계산합니다.
> 3.  **`loss.backward()`**: 계산된 오차를 기반으로, 각 파라미터가 오차에 얼마나 기여했는지 미분값을 계산합니다 (**역전파**).
> 4.  **`optimizer.step()`**: 계산된 미분값을 바탕으로 모델의 파라미터를 업데이트하여 오차를 줄이는 방향으로 나아갑니다.
> 
> > **💡 순전파와 역전파**
> > - **순전파 (Forward)**: 내가 만든 레시피로 요리를 해서(모델 예측) 손님에게 내놓는 과정.
> > - **역전파 (Backward)**: 손님의 피드백("너무 짜요!")을 듣고, 소금을 얼마나 넣었는지, 간장을 얼마나 넣었는지 원인을 거슬러 올라가 분석하는 과정. `optimizer.step()`은 이 분석을 바탕으로 다음 요리에서는 소금을 덜 넣는 행동입니다.
> 
> ### 5-3. 학습, 검증, 그리고 추론 (Training, Validation, and Inference)
> 
> 딥러닝 모델의 생명주기는 크게 '학습', '검증', '추론' 세 단계로 나뉩니다.
> 
> - **학습 (Training)**: `train_loader` 와 같이 훈련용 데이터를 사용하여 모델의 파라미터(가중치)를 최적화하는 과정입니다. 위에서 설명한 **학습 루프**가 바로 이 단계에 해당합니다. 모델은 정답을 보면서 오차를 줄여나갑니다.
> - **검증 (Validation)**: 학습 과정 중에 모델이 얼마나 잘 학습되고 있는지, 과적합(Overfitting)은 발생하지 않는지 확인하기 위한 단계입니다. 학습에 사용되지 않은 `validation_loader` 데이터로 모델의 성능을 주기적으로 평가합니다. 이 평가 결과를 바탕으로 학습률(Learning Rate)을 조절하거나 학습을 조기 종료하는 등의 결정을 내립니다.
> - **추론 (Inference)**: 학습이 완료된 모델을 사용하여 새로운 데이터에 대한 예측을 수행하는 단계입니다. 이 단계에서는 더 이상 모델의 가중치를 업데이트하지 않으므로, 역전파나 옵티마이저가 필요 없습니다. `torch.no_grad()` 컨텍스트 안에서 실행하여 메모리 사용량을 줄이고 계산 속도를 높이는 것이 일반적입니다. 서비스 배포 환경에서 모델이 사용되는 단계가 바로 추론입니다.
> 
> ---
> 
> ## 연습 문제: MNIST 손글씨 분류
> > **🎯 5일차 목표:** PyTorch를 사용하여 딥러닝의 "Hello, World!"인 MNIST 손글씨 분류기를 직접 만듭니다.
> 
> ### 문제:
> `torchvision`을 사용하여 MNIST 데이터셋을 불러오고, 간단한 CNN(합성곱 신경망) 모델을 구축하여 손글씨 숫자를 분류하는 전체 코드를 완성하세요.
> 
> ```python
> import torch
> import torch.nn as nn
> import torchvision
> import torchvision.transforms as transforms
> from torch.utils.data import DataLoader
> 
> # 1. 데이터셋 및 로더 준비
> transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
> train_dataset = torchvision.datasets.MNIST(root='./data', train=True, transform=transform, download=True)
> test_dataset = torchvision.datasets.MNIST(root='./data', train=False, transform=transform)
> train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
> test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)
> 
> # 2. CNN 모델 정의
> class ConvNet(nn.Module):
>     def __init__(self):
>         super(ConvNet, self).__init__()
>         self.layer1 = nn.Sequential(
>             nn.Conv2d(1, 32, kernel_size=5, stride=1, padding=2),
>             nn.ReLU(),
>             nn.MaxPool2d(kernel_size=2, stride=2))
>         self.layer2 = nn.Sequential(
>             nn.Conv2d(32, 64, kernel_size=5, stride=1, padding=2),
>             nn.ReLU(),
>             nn.MaxPool2d(kernel_size=2, stride=2))
>         self.fc = nn.Linear(7*7*64, 10)
> 
>     def forward(self, x):
>         out = self.layer1(x)
>         out = self.layer2(out)
>         out = out.reshape(out.size(0), -1) # Flatten
>         out = self.fc(out)
>         return out
> 
> # 3. 모델, 손실함수, 옵티마이저 설정
> device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
> model = ConvNet().to(device)
> criterion = nn.CrossEntropyLoss()
> optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
> 
> # 4. 학습 루프 구현
> # for epoch ...
> #   for i, (images, labels) in enumerate(train_loader):
> #     - images, labels를 device로 이동
> #     - 순전파, 손실 계산, 역전파, 파라미터 업데이트 코드 작성
> 
> ### 6-1. 심화: 주요 CNN 아키텍처의 발전사
> 간단한 CNN 모델을 직접 만들어본 지금, 딥러닝 역사에 한 획을 그은 주요 CNN 아키텍처들이 어떻게 발전해왔는지 살펴보는 것은 매우 의미 있는 일입니다. 각 모델이 어떤 문제를 해결하려 했고, 어떤 아이디어를 제시했는지 이해하면 더 깊이 있는 모델을 설계하는 데 도움이 될 것입니다.
> 
> | 모델명 | 발표 연도 | 주요 특징 및 기여 | 주요 사용처 및 선택 가이드 |
> |---|---|---|---|
> | **LeNet-5** | 1998 | LeCun 교수가 제안. 최초의 성공적인 CNN. `Conv`-`Pool`-`Conv`-`Pool`-`FC` 구조의 기반을 마련. | **학습용/기본 CNN 구조 이해**: CNN의 기본 동작 원리를 학습하고 간단한 분류 문제를 해결할 때 좋습니다. 현대적인 문제에 직접 사용하기에는 성능이 부족합니다. |
> | **AlexNet** | 2012 | 이미지넷(ILSVRC) 챌린지에서 압도적 성능으로 우승하며 **딥러닝의 부흥**을 이끌었다. **ReLU**, **Dropout**, **GPU 활용** 등 현재까지도 널리 쓰이는 기법들을 대거 도입했다. | **성능 개선의 출발점**: LeNet보다 복잡한 문제에 적용 가능. 딥러닝 모델의 성능을 어떻게 개선하는지에 대한 아이디어(활성화 함수, 규제)를 얻고 싶을 때 분석해볼 만합니다. |
> | **VGGNet** | 2014 | **3x3의 작은 커널**을 여러 겹 깊게 쌓는 단순하지만 효과적인 구조를 제시. 깊이(Depth)가 성능에 미치는 영향을 증명했다. | **전이 학습(Transfer Learning)의 베이스라인**: 구조가 단순하고 특징 추출 능력이 우수하여, 사전 훈련된 모델을 다른 이미지 문제에 적용하는 전이 학습의 베이스라인으로 널리 사용됩니다. |
> | **GoogLeNet** | 2014 | **인셉션 모듈(Inception Module)**을 도입하여, 여러 크기의 커널을 병렬로 사용함으로써 연산 효율성과 성능을 동시에 잡았다. **파라미터 수를 줄이는 데** 집중했다. | **연산량/메모리가 제한된 환경**: 모바일 기기나 임베디드 시스템처럼 계산 비용에 민감한 환경에서 높은 성능을 내야 할 때 우선적으로 고려됩니다. |
> | **ResNet** | 2015 | **잔차 학습(Residual Learning)**과 **스킵 연결(Skip Connection)**이라는 혁신적인 아이디어로 100층이 넘는 **매우 깊은 신경망**의 학습을 가능하게 했다. **Vanishing Gradient 문제를 해결**하며 딥러닝의 한계를 돌파했다. | **대부분의 컴퓨터 비전 문제의 표준**: 현재까지도 이미지 분류, 객체 탐지, 세그멘테이션 등 다양한 컴퓨터 비전 문제에서 매우 강력한 '기본/표준' 모델로 사용됩니다. |
> 
> #### 1. LeNet-5: CNN의 서막
> - Yann LeCun 교수가 제안한 모델로, 현대적인 CNN 구조(합성곱 층과 풀링 층의 반복, 마지막에 완전 연결 계층)의 청사진을 제시했습니다. 당시에는 손글씨 숫자 인식과 같은 제한된 문제에서 큰 성공을 거두었습니다.
> 
> #### 2. AlexNet: 딥러닝의 폭발
> - 2012년 이미지넷 분류 챌린지에서 전통적인 컴퓨터 비전 기법들을 압도적인 성능 차이로 이기고 우승하며, AI 커뮤니티에 엄청난 충격을 주었습니다. 딥러닝이 학계를 넘어 산업계의 주류 기술로 발돋움하는 결정적인 계기가 되었습니다.
> - **주요 기여**:
>     - **ReLU 활성화 함수**: Sigmoid/Tanh의 Vanishing Gradient 문제를 해결하고 학습 속도를 크게 향상시켰습니다.
>     - **Dropout**: 학습 시 일부 뉴런을 무작위로 비활성화하여 과적합을 방지하는 효과적인 규제(Regularization) 기법을 제시했습니다.
>     - **GPU 활용**: 2개의 GPU를 병렬로 사용하여 대규모 연산을 효율적으로 처리하며 딥러닝의 대중화를 이끌었습니다.
> 
> #### 3. VGGNet: 깊고 단순함의 미학
> - "더 깊게 쌓으면 성능이 좋아질까?"라는 단순한 질문에서 출발했습니다.
> - **주요 기여**:
>     - **3x3 합성곱 필터**: 큰 크기(예: 5x5, 7x7)의 필터를 사용하는 대신, 3x3의 작은 필터를 여러 개 연속으로 사용하여 동일한 Receptive Field를 가지면서도 파라미터 수를 줄이고 비선형성을 증가시켰습니다.
>     - **균일한 구조**: VGG-16, VGG-19 등 깊이만 다른 균일한 구조를 제시하여 모델 설계의 단순함을 보여주었습니다.
> 
> #### 4. GoogLeNet (Inception): 효율적인 깊이의 추구
> - VGGNet처럼 무작정 깊게 쌓는 것이 아니라, "어떻게 하면 연산 자원을 효율적으로 사용하면서 성능을 높일 수 있을까?"를 고민했습니다.
> - **주요 기여**:
>     - **인셉션 모듈(Inception Module)**: 1x1, 3x3, 5x5 합성곱과 3x3 풀링을 하나의 블록 안에서 병렬로 처리하고 그 결과를 합칩니다. 이를 통해 모델이 다양한 스케일의 특징을 동시에 학습할 수 있게 됩니다.
>     - **1x1 Convolution 활용**: 채널(Channel) 수를 줄여(BottleNeck) 전체 연산량을 획기적으로 감소시켜 성능은 유지하거나 향상시켰습니다.
> 
> #### 5. ResNet: 깊이의 한계를 넘어서
> - 네트워크가 깊어질수록 오히려 학습이 더 어려워지고 성능이 저하되는 **퇴화(Degradation) 문제**가 발생했습니다. 이는 Vanishing/Exploding Gradient와는 다른 현상으로, 깊은 망이 얕은 망보다 학습하기 더 어렵다는 문제였습니다.
> - **주요 기여**:
>     - **잔차 학습(Residual Learning)**: 층이 입력값 `x`를 `H(x)`로 직접 변환하도록 학습하는 대신, **변화량(Residual), 즉 `F(x) = H(x) - x`**를 학습하도록 구조를 바꿨습니다.
>     - **스킵 연결(Skip Connection)**: 입력 `x`를 몇 개의 층을 건너뛰어 출력에 그대로 더해주는 `H(x) = F(x) + x` 구조입니다. 이 간단한 '지름길'을 통해 그래디언트가 깊은 층까지 소실되지 않고 잘 흘러갈 수 있게 되어, 152층이라는 전례 없는 깊이의 네트워크 학습을 성공시켰습니다. ResNet의 등장은 이후 수많은 딥러닝 아키텍처에 지대한 영향을 미쳤습니다.
> 
> ### 6-2. 실제 기업 적용 사례 (Real-world Enterprise Use Cases)
> 
> 앞서 살펴본 CNN 아키텍처들의 발전은 오늘날 다양한 산업 분야에서 AI 혁신을 이끄는 밑거름이 되었습니다. 딥러닝 기술이 실제 비즈니스 문제를 어떻게 해결하고 있는지 최신 사례를 통해 살펴보겠습니다.

---

## 심화 프로젝트: 와인 품질 예측 (PyTorch MLP)

이번 장의 MNIST 실습을 통해 이미지 데이터에 대한 딥러닝(CNN)의 맛을 보았습니다. 이번 미니 프로젝트에서는 우리가 더 익숙한 **표(tabular) 형식의 데이터**에 딥러닝을 적용해 봅니다. 이 과정을 통해 딥러닝의 기본 원리(모델 정의, 학습 루프)가 데이터의 종류와 상관없이 동일하게 적용된다는 중요한 사실을 체득하게 됩니다.

Scikit-learn에 내장된 '와인 품질' 데이터셋을 사용하여, 와인의 화학적 특성으로 품질 등급('좋음'/'나쁨')을 예측하는 다층 퍼셉트론(MLP) 모델을 PyTorch로 직접 구축, 훈련, 평가하는 전체 과정을 경험합니다.

### 프로젝트 목표

1.  Scikit-learn의 데이터를 PyTorch `Tensor`로 변환하고, `DataLoader`를 구성합니다.
2.  `nn.Module`을 상속받아 1개 이상의 은닉층을 가진 MLP 모델을 설계합니다.
3.  이진 분류(Binary Classification) 문제에 맞는 손실 함수(`BCEWithLogitsLoss`)와 옵티마이저를 선택합니다.
4.  모델을 훈련시키고, 테스트 데이터에 대한 정확도를 측정하여 성능을 평가합니다.

**최종 분석 리포트 (예시):**
```
Epoch [10/50], Loss: 0.6123
Epoch [20/50], Loss: 0.5248
Epoch [30/50], Loss: 0.4581
Epoch [40/50], Loss: 0.4132
Epoch [50/50], Loss: 0.3887

Test Accuracy: 0.8611
```

### 단계별 구현 가이드

**1. 데이터 준비 (Scikit-learn -> PyTorch)**
- `sklearn.datasets.load_wine`을 사용해 데이터를 불러옵니다.
- 데이터를 Feature와 Target으로 나누고, `train_test_split`으로 훈련/테스트셋을 분리합니다.
- **(중요)** MLP는 스케일링에 민감하므로, `StandardScaler`를 사용해 Feature 데이터를 표준화합니다.
- 모든 데이터를 NumPy 배열에서 PyTorch `Tensor`로 변환합니다.
- `TensorDataset`과 `DataLoader`를 사용하여 PyTorch가 배치(batch) 단위로 데이터를 처리할 수 있도록 준비합니다.

**2. MLP 모델 정의**
- `nn.Module`을 상속받는 `WineNet` 클래스를 만드세요.
- `__init__`: 입력층, 1개 이상의 은닉층(`nn.Linear` + `nn.ReLU`), 출력층(`nn.Linear`)을 정의합니다. 이진 분류이므로 출력층의 뉴런 수는 1개입니다.
- `forward`: 데이터가 각 층을 순서대로 통과하는 흐름을 정의합니다.

**3. 학습 및 평가 루프 구현**
- 모델, 손실 함수(`nn.BCEWithLogitsLoss`), 옵티마이저(`torch.optim.Adam`)를 초기화합니다.
- **학습 루프**: `for epoch in ...`:
    - `for inputs, labels in train_loader:`:
        - `optimizer.zero_grad()`: 이전 배치의 기울기 초기화
        - `outputs = model(inputs)`: 순전파
        - `loss = criterion(outputs, labels.unsqueeze(1))`: 손실 계산
        - `loss.backward()`: 역전파
        - `optimizer.step()`: 가중치 업데이트
- **평가 루프**:
    - `model.eval()` 모드로 전환하고, `with torch.no_grad():` 블록 안에서 실행합니다.
    - 테스트 로더에서 예측을 수행하고, `torch.sigmoid`와 임계값(0.5)을 사용해 로짓(logit)을 0 또는 1의 예측값으로 변환한 뒤, 정확도를 계산합니다.

**전체 코드 구조:**
```python
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 1. 데이터 준비
# 데이터 로드 및 DataFrame 변환
# 와인 데이터셋은 13개의 feature와 3개의 클래스(0, 1, 2)를 가짐
X, y = load_wine(return_X_y=True) 

# 이진 분류 문제로 변환: class 0을 1(good)로, 나머지(1, 2)를 0(bad)으로
y_binary = (y == 0).astype(int)

# 데이터 분리 및 스케일링
X_train, X_test, y_train, y_test = train_test_split(X, y_binary, test_size=0.2, random_state=42, stratify=y_binary)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# PyTorch 텐서로 변환
X_train_tensor = torch.FloatTensor(X_train_scaled)
y_train_tensor = torch.FloatTensor(y_train)
X_test_tensor = torch.FloatTensor(X_test_scaled)
y_test_tensor = torch.FloatTensor(y_test)

# DataLoader 생성
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)

# 2. MLP 모델 정의
class WineNet(nn.Module):
    def __init__(self, input_features):
        super(WineNet, self).__init__()
        self.layer1 = nn.Linear(input_features, 64)
        self.layer2 = nn.Linear(64, 32)
        self.output_layer = nn.Linear(32, 1)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.layer1(x))
        x = self.relu(self.layer2(x))
        x = self.output_layer(x)
        return x

# 3. 모델, 손실함수, 옵티마이저 설정
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
input_features = X_train.shape[1]
model = WineNet(input_features).to(device)
criterion = nn.BCEWithLogitsLoss() # Sigmoid + BCELoss
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

# 4. 학습 루프
num_epochs = 50
for epoch in range(num_epochs):
    model.train()
    for inputs, labels in train_loader:
        inputs, labels = inputs.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels.unsqueeze(1))
        loss.backward()
        optimizer.step()
    
    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}')

# 5. 평가 루프
model.eval()
with torch.no_grad():
    X_test_tensor, y_test_tensor = X_test_tensor.to(device), y_test_tensor.to(device)
    
    y_pred_logits = model(X_test_tensor)
    y_pred_prob = torch.sigmoid(y_pred_logits)
    y_pred = (y_pred_prob > 0.5).float()
    
    accuracy = (y_pred.squeeze() == y_test_tensor).float().mean()
    print(f'\nTest Accuracy: {accuracy.item():.4f}')

```
이 프로젝트를 통해 여러분은 어떤 종류의 데이터(이미지, 텍스트, 정형 데이터)를 다루든, PyTorch의 일관된 설계 철학 안에서 문제를 해결하는 능력을 갖추게 될 것입니다.

---

## ⚠️ What Could Go Wrong? (토론 주제)

이론을 배우고 코드를 실행하는 것을 넘어, 실제 딥러닝 프로젝트에서 마주할 수 있는 문제들에 대해 미리 고민해보는 것은 매우 중요합니다. 다음 주제들에 대해 함께 토론해보세요.

1.  **"Dying ReLU" 문제의 심각성**
    -   **상황**: 학습 과정 모니터링 중, 특정 은닉층의 뉴런 중 상당수가 항상 0만을 출력하는 'Dying ReLU' 현상을 발견했습니다.
    -   **토론**:
        -   이 현상이 모델 학습에 구체적으로 어떤 악영향을 미칠까요? (그래디언트 흐름, 파라미터 업데이트 등)
        -   이론 섹션에서 배운 Leaky ReLU 외에, 이 문제를 진단하고 해결하기 위해 시도해볼 수 있는 방법에는 어떤 것들이 있을까요? (가중치 초기화 기법, 학습률 조정 등)

2.  **교과서적인 과적합(Overfitting) 발생**
    -   **상황**: 우리가 만든 MNIST 분류기의 훈련(train) 정확도는 99%에 달하지만, 검증(validation) 정확도는 90%에서 정체되고 있습니다. 훈련 손실은 계속 감소하지만, 검증 손실은 오히려 증가하기 시작합니다.
    -   **토론**:
        -   이는 전형적인 과적합 신호입니다. 현재 `ConvNet` 코드 구조를 기준으로, 과적합을 완화하기 위해 시도해볼 수 있는 구체적인 방법 3가지를 논의하고, 각 방법의 장단점을 설명해보세요. (예: `nn.Dropout` 추가, `torchvision.transforms`를 이용한 데이터 증강, 모델 구조 변경 등)

3.  **옵티마이저 선택의 중요성**
    -   **상황**: 실습 코드에서는 `Adam` 옵티마이저를 기본으로 사용했습니다. 만약 `SGD` with momentum으로 옵티마이저를 변경하여 처음부터 다시 학습시킨다면, 학습 곡선(learning curve)은 어떻게 달라질 것으로 예상되나요?
    -   **토론**:
        -   `Adam`과 `SGD`의 핵심적인 차이점은 무엇인가요? (파라미터별 학습률 조정, 모멘텀 등)
        -   어떤 종류의 문제나 데이터셋에서 `Adam`이, 또 어떤 상황에서 `SGD`가 더 나은 선택이 될 수 있을지 각자의 경험과 근거를 들어 토론해보세요.

4.  **배치 사이즈(Batch Size)는 클수록 좋을까?**
    -   **상황**: 더 빠른 학습을 위해 배치 사이즈를 64에서 1024로 크게 늘렸더니, GPU 메모리 부족(Out of Memory) 오류 없이 학습은 완료되었지만 최종 검증 정확도가 오히려 약간 떨어졌습니다.
    -   **토론**:
        -   배치 사이즈를 크게 하는 것의 장점(학습 속도, 안정적인 그래디언트)과 단점(메모리 사용량, 일반화 성능 저하 가능성)은 무엇일까요?
        -   '큰 배치 사이즈가 일반화 성능을 해칠 수 있다'는 주장의 배경이 되는 'Sharp Minimum'과 'Flat Minimum'의 개념에 대해 조사하고 설명해보세요.

---

## 7. 트러블슈팅 (Troubleshooting)

- **`RuntimeError: mat1 and mat2 shapes cannot be multiplied (...)` 오류가 발생했나요?**
  - 신경망의 연속된 두 `nn.Linear` 계층 간에 입력과 출력의 크기가 맞지 않는다는 의미입니다. 예를 들어, `nn.Linear(in_features=10, out_features=20)` 다음에는 `nn.Linear(in_features=20, ...)`가 와야 합니다. 특히 CNN에서 `nn.Linear` 계층으로 넘어갈 때 Flatten 이후의 크기를 정확히 계산했는지 확인해야 합니다.
- **손실(Loss)이 전혀 줄어들지 않나요?**
  - **학습률(Learning Rate)**이 너무 높거나 낮을 수 있습니다. 너무 높으면 최적점을 지나쳐 발산하고, 너무 낮으면 학습이 매우 느리거나 지역 최적점(Local Minima)에 갇힐 수 있습니다. 옵티마이저의 `lr` 값을 조절해보세요.
  - 데이터에 문제가 있을 수도 있습니다. 입력 데이터와 레이블이 제대로 매칭되었는지, 데이터 정규화가 올바르게 적용되었는지 확인해보세요.
- **GPU를 사용하고 있는데도 `CUDA out of memory` 오류가 발생하나요?**
  - GPU 메모리보다 큰 모델이나 데이터를 올리려고 할 때 발생합니다. **배치 사이즈(Batch Size)**를 줄이는 것이 가장 일반적인 해결책입니다. 그래도 문제가 해결되지 않는다면, 모델의 파라미터 수를 줄이거나(더 얕은 모델 사용), 이미지의 해상도를 낮추는 방법을 고려할 수 있습니다.
- **`model.eval()`과 `torch.no_grad()`는 왜 사용해야 하나요?**
  - **`model.eval()`**: 모델을 평가(evaluation/inference) 모드로 전환합니다. 이는 드롭아웃(Dropout)이나 배치 정규화(Batch Normalization)와 같이 훈련 때와 평가 때 다르게 동작해야 하는 레이어들을 올바르게 설정해주는 역할을 합니다.
  - **`torch.no_grad()`**: 이 컨텍스트 블록 안에서는 그래디언트 계산을 수행하지 않도록 하여, 불필요한 메모리 사용을 막고 계산 속도를 높여줍니다. 모델 평가나 추론 시에는 역전파가 필요 없으므로 반드시 사용해주는 것이 좋습니다.

더 자세한 문제 해결 가이드나 다른 동료들이 겪은 문제에 대한 해결책이 궁금하다면, 아래 문서를 참고해주세요.

- **➡️ [Geumdo-Docs: TROUBLESHOOTING.md](../../../TROUBLESHOOTING.md)**

---

| 기업 | 적용 분야 | 사용 기술 및 모델 (추정) | 비즈니스 가치 |
| :--- | :--- | :--- | :--- |
| **Tesla** | **자율 주행 (Autopilot / FSD)** | 다중 카메라 영상 기반의 **컴퓨터 비전**, Transformer 기반의 End-to-End 딥러닝 모델 (e.g., HydraNets) | 주변 환경(차선, 차량, 신호등, 보행자)을 3D 공간으로 인식하고, 차량의 행동을 결정. **운전자 보조 및 완전 자율 주행**을 목표로 함. |
| **Google (DeepMind)** | **단백질 구조 예측 (AlphaFold)** | 어텐션 기반의 딥러닝 네트워크 (Transformer와 유사) | 수십 년간 난제였던 단백질 3차원 구조 예측 문제를 해결. **신약 개발, 질병 연구** 등 생명 과학 분야에 혁신적인 발전을 가져옴. |
| **OpenAI** | **대규모 언어 모델 / 생성형 AI (GPT-4, DALL-E, Sora)** | **Transformer** 아키텍처 기반의 GPT(Generative Pre-trained Transformer) | 인간과 유사한 수준의 **텍스트 생성, 코드 작성, 이미지/영상 제작** 등 복잡한 창작 및 지능적 작업 수행. 생산성의 패러다임을 바꾸고 있음. |
| **Meta (FAIR)** | **콘텐츠 이해 및 추천, AR/VR** | CNN, RNN, Transformer 등 다양한 모델 활용 | 뉴스피드/릴스 추천, 유해 콘텐츠 자동 감지, 번역 등 핵심 서비스를 고도화. 차세대 플랫폼인 **AR 글래스, VR 헤드셋**의 객체 인식 및 상호작용에 활용. |
| **NVIDIA** | **AI 기반 신약/소재 개발 (BioNeMo)** | 생성형 AI, 분자 역학 시뮬레이션 | 신약 개발 후보 물질을 탐색하고 설계하는 데 드는 막대한 시간과 비용을 획기적으로 절감. **디지털 트윈** 기술과 결합하여 신소재 개발 가속화. |

---

## 8. 되짚어보기 (Summary)

이번 파트에서는 머신러닝의 한계를 넘어, 이미지와 텍스트 같은 비정형 데이터까지 처리할 수 있는 딥러닝의 세계로 들어섰습니다.

- **PyTorch는 레고 블록**: 우리는 `nn.Module`이라는 '설명서'와 `nn.Linear`, `nn.Conv2d` 같은 '블록'으로 직접 신경망을 조립하는 법을 배웠습니다.
- **학습은 레시피 수정 과정**: 모델이 예측(순전파)을 하면, 손실 함수로 오차를 계산하고, 옵티마이저를 통해 오차를 줄이는 방향으로 파라미터를 수정(역전파)하는 학습 루프의 원리를 이해했습니다.
- **CNN은 이미지 전문가**: 합성곱(Convolution)과 풀링(Pooling) 연산을 통해 이미지의 공간적 특징을 효과적으로 추출하는 CNN의 원리를 배우고, MNIST 분류 실습을 통해 직접 구현해보았습니다.
- **거인의 어깨 위에서**: LeNet부터 ResNet에 이르기까지, 딥러닝 아키텍처의 발전사를 통해 문제 해결을 위한 다양한 아이디어를 엿보았고, Tesla, OpenAI 등 실제 기업들이 어떻게 딥러닝으로 세상을 바꾸고 있는지 확인했습니다.

이제 여러분은 복잡한 딥러닝 모델의 구조를 이해하고, PyTorch를 사용하여 직접 구현할 수 있는 탄탄한 기초를 갖추게 되었습니다.

## 9. 더 깊이 알아보기 (Further Reading)

### 실전 코드 & 튜토리얼
- [PyTorch 공식 튜토리얼 (한국어)](https://tutorials.pytorch.kr/): "60분 만에 끝내는 딥러닝"부터 분야별 심화 예제까지, PyTorch를 마스터하기 위한 최고의 자료입니다.
- [PyTorch Lightning](https://www.pytorchlightning.ai/): `trainer.fit(model)`과 같이 PyTorch 코드를 더 구조화하고, 보일러플레이트를 획기적으로 줄여주는 상위 프레임워크입니다.
- [Hugging Face](https://huggingface.co/): 최신 Transformer 모델들을 쉽게 다운로드하고 사용할 수 있는 플랫폼. 자연어 처리를 한다면 반드시 알아야 할 필수 라이브러리입니다.

### 이론 및 개념 심화
- **[Dive into Deep Learning](https://d2l.ai/)**: 딥러닝의 거의 모든 주제를 다루는 포괄적인 온라인 교재. 수학적 설명과 PyTorch 코드가 함께 제공됩니다.
- **[CS231n: Convolutional Neural Networks for Visual Recognition](https://cs231n.github.io/)**: 스탠포드 대학의 전설적인 컴퓨터 비전 강의. CNN의 원리와 응용에 대해 가장 깊이 있게 배우고 싶다면 강력히 추천합니다.
- **[Attention Is All You Need (논문)](https://arxiv.org/abs/1706.03762)**: 현대 딥러닝의 흐름을 바꾼 Transformer 모델을 제안한 논문. `part_7.2`를 학습하기 전에 한번쯤 읽어보는 것을 권장합니다.
- **[3Blue1Brown: 신경망 영상 시리즈](https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi)**: 역전파(Backpropagation)와 같은 딥러닝의 핵심 수학 개념을 세상에서 가장 직관적인 시각 자료로 설명해줍니다.
- **[A Comprehensive Guide to Convolutional Neural Networks](https://towardsdatascience.com/a-comprehensive-guide-to-convolutional-neural-networks-the-eli5-way-3bd2b1164a53)**: CNN의 기본 개념부터 주요 아키텍처까지 시각 자료와 함께 친절하게 설명하는 아티클입니다.

---

**➡️ 다음 시간: [Part 7.1: 순환 신경망 (RNN)과 LSTM](./part_7.1_recurrent_neural_networks.md)**