Level 4: 실전 프로젝트
🚀

Level 4

MNIST 손글씨 분류

첫 번째 완전한 딥러닝 프로젝트

약 5분
MNIST 손글씨 분류 강의 영상
강의 영상 보기 (새 탭에서 재생)YouTube

📓Google Colab에서 실습하기

이 레슨은 PyTorch/GPU가 필요합니다. 노트북을 다운로드 후 Google Colab에서 열어주세요.

학습 내용

MNIST 손글씨 분류

🖥️ 로컬 실행 필수

이 레슨의 모든 코드는 PyTorch를 사용하며, 브라우저에서 실행되지 않습니다.

실행 방법:

  1. Python 설치: python.org
  2. 터미널에서 PyTorch 설치: pip install torch torchvision
  3. 레슨 하단의 **"전체 통합 코드"**를 복사하여 mnist_train.py로 저장
  4. 터미널에서 실행: python mnist_train.py

💡 각 코드 조각은 학습용으로, 개별 실행이 불가합니다. 전체 통합 코드를 사용하세요.

학습 목표

이 레슨을 완료하면:

  • MNIST 데이터셋의 구조와 역사적 의미를 이해합니다
  • 완전한 딥러닝 파이프라인을 PyTorch로 구현합니다
  • 데이터 전처리부터 모델 평가까지 전 과정을 설명할 수 있습니다
  • 98% 이상의 정확도를 달성하는 모델을 만듭니다

MNIST란 무엇인가?

비유: MNIST는 딥러닝의 "Hello, World!"입니다. 프로그래밍을 배울 때 첫 프로그램으로 "Hello, World!"를 출력하듯, 딥러닝을 배울 때 첫 프로젝트로 MNIST를 다룹니다.

MNIST(Modified National Institute of Standards and Technology)는 손으로 쓴 숫자(0~9) 이미지 데이터셋입니다. 1998년 Yann LeCun이 공개한 이후, 수십 년간 머신러닝 알고리즘의 벤치마크로 사용되어 왔습니다.

항목내용
전체 이미지 수70,000개
훈련 데이터60,000개
테스트 데이터10,000개
이미지 크기28 x 28 픽셀
색상흑백 (1채널)
클래스 수10개 (숫자 0~9)
픽셀 값 범위0(검정) ~ 255(흰색)

왜 MNIST로 시작하는가?

  1. 단순함: 28x28 흑백 이미지라 복잡한 전처리가 필요 없습니다
  2. 빠른 학습: 데이터가 작아 CPU로도 수 분 내에 학습됩니다
  3. 높은 성능: 간단한 모델로도 98% 이상 정확도를 달성할 수 있습니다
  4. 검증된 벤치마크: 수십 년간 사용되어 참고 자료가 풍부합니다

Step 1: 환경 설정 및 데이터 로드

라이브러리 임포트

딥러닝 프로젝트는 항상 필요한 도구를 불러오는 것부터 시작합니다. 각 라이브러리의 역할을 이해하는 것이 중요합니다.

python
⚠️ 로컬 실행 필요
# [코드 조각 1/7] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) import torch # PyTorch 핵심 라이브러리 import torch.nn as nn # 신경망 레이어 모음 import torch.optim as optim # 최적화 알고리즘 (Adam, SGD 등) from torch.utils.data import DataLoader # 배치 단위 데이터 공급 from torchvision import datasets, transforms # 이미지 데이터셋과 변환 # GPU가 있으면 GPU를, 없으면 CPU를 사용합니다 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("Using device:", device)
라이브러리역할비유
torch텐서 연산 핵심공장의 기본 기계
torch.nn레이어, 손실함수조립용 부품
torch.optim최적화 알고리즘품질 개선 담당자
DataLoader배치 데이터 공급컨베이어 벨트
torchvision이미지 관련 유틸이미지 전문 도구함

데이터 전처리: transforms

비유: 요리 전에 재료를 씻고 손질하듯, 모델에 데이터를 넣기 전에 전처리가 필요합니다.

python
# [코드 조각 2/7] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) # 데이터 변환 파이프라인을 정의합니다 transform = transforms.Compose([ # 1단계: PIL 이미지를 PyTorch 텐서로 변환 (0~255 -> 0.0~1.0) transforms.ToTensor(), # 2단계: 정규화 - 평균 0.1307, 표준편차 0.3081로 표준화 # 이 값은 MNIST 전체 데이터에서 미리 계산된 통계값입니다 transforms.Normalize((0.1307,), (0.3081,)) ])

왜 정규화를 하는가?

정규화 전 픽셀 값 범위는 01이고, 정규화 후에는 대략 -0.422.82 범위가 됩니다. 이렇게 하면:

  • 학습 속도가 빨라집니다 (그래디언트가 안정적)
  • 모든 특성(feature)이 비슷한 스케일을 가집니다

데이터셋 다운로드와 DataLoader 생성

python
# [코드 조각 3/7] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) # 훈련 데이터셋 (60,000개) train_dataset = datasets.MNIST( root="./data", # 다운로드 경로 train=True, # 훈련용 데이터 download=True, # 없으면 자동 다운로드 transform=transform # 위에서 정의한 전처리 적용 ) # 테스트 데이터셋 (10,000개) test_dataset = datasets.MNIST( root="./data", train=False, # 테스트용 데이터 download=True, transform=transform ) # DataLoader: 데이터를 배치 단위로 묶어서 공급합니다 train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)

비유: Dataset은 창고에 쌓인 재료이고, DataLoader는 그 재료를 64개씩 묶어서 공장(모델)으로 보내는 컨베이어 벨트입니다. shuffle=True는 매번 다른 순서로 재료를 보내서 모델이 순서에 의존하지 않게 합니다.


Step 2: 모델 설계 (MLP)

비유: 모델 설계는 건물 설계도를 그리는 것과 같습니다. 입구(입력층)에서 출구(출력층)까지 데이터가 어떤 경로로 흘러갈지 결정합니다.

우리는 다층 퍼셉트론(MLP, Multi-Layer Perceptron)을 사용합니다. MLP는 가장 기본적인 신경망 구조입니다.

레이어입력 크기출력 크기활성화 함수설명
Flatten28x28784-2D 이미지를 1D로 펼침
은닉층 1784512ReLU주요 특징 추출
Dropout512512-과적합 방지 (20% 뉴런 비활성화)
은닉층 2512256ReLU세부 특징 추출
Dropout256256-과적합 방지
출력층25610-각 숫자(0~9)에 대한 점수
python
# [코드 조각 4/7] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) class MNISTClassifier(nn.Module): def __init__(self): super().__init__() # 첫 번째 은닉층: 784개 입력 -> 512개 출력 self.fc1 = nn.Linear(784, 512) # 두 번째 은닉층: 512개 입력 -> 256개 출력 self.fc2 = nn.Linear(512, 256) # 출력층: 256개 입력 -> 10개 출력 (숫자 0~9) self.fc3 = nn.Linear(256, 10) # Dropout: 학습 시 뉴런의 20%를 무작위로 끕니다 self.dropout = nn.Dropout(0.2) def forward(self, x): # 28x28 이미지를 784 길이의 1차원 벡터로 변환 x = x.view(-1, 784) # 첫 번째 레이어 통과 후 ReLU 활성화 x = torch.relu(self.fc1(x)) x = self.dropout(x) # 두 번째 레이어 통과 후 ReLU 활성화 x = torch.relu(self.fc2(x)) x = self.dropout(x) # 출력층 (활성화 함수 없음 - CrossEntropyLoss가 처리) x = self.fc3(x) return x # 모델 생성 후 디바이스(GPU/CPU)로 이동 model = MNISTClassifier().to(device)

왜 출력층에 활성화 함수가 없는가?

PyTorch의 CrossEntropyLoss는 내부적으로 Softmax를 포함하고 있습니다. 따라서 출력층에 별도의 Softmax를 넣으면 중복 적용이 되어 성능이 떨어집니다. 이것은 PyTorch를 사용할 때 흔히 하는 실수 중 하나입니다.


Step 3: 학습 설정

python
# [코드 조각 5/7] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) # 손실 함수: 다중 클래스 분류에 사용 criterion = nn.CrossEntropyLoss() # 옵티마이저: Adam (적응적 학습률) optimizer = optim.Adam(model.parameters(), lr=0.001) # 하이퍼파라미터 epochs = 10
설정선택이유
손실 함수CrossEntropyLoss다중 클래스 분류 표준, 내부에 Softmax 포함
옵티마이저Adam학습률 자동 조절, 대부분의 경우 좋은 성능
학습률0.001Adam의 기본 권장값, 안정적 수렴
에폭 수10MNIST는 10 에폭이면 충분히 수렴

Step 4: 학습 루프

비유: 학습 루프는 시험공부와 같습니다. 문제를 풀고(순전파), 답을 확인하고(손실 계산), 틀린 부분을 분석하고(역전파), 그 부분을 보완합니다(가중치 업데이트). 이 과정을 반복할수록 실력이 향상됩니다.

python
# [코드 조각 6/7] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) for epoch in range(epochs): model.train() # 학습 모드 (Dropout 활성화) total_loss = 0 for batch_idx, (data, target) in enumerate(train_loader): # 데이터를 GPU/CPU로 이동 data, target = data.to(device), target.to(device) # [1단계] 그래디언트 초기화 - 이전 배치의 그래디언트를 지웁니다 optimizer.zero_grad() # [2단계] 순전파 - 모델에 데이터를 통과시킵니다 output = model(data) # [3단계] 손실 계산 - 예측과 정답의 차이를 측정합니다 loss = criterion(output, target) # [4단계] 역전파 - 각 가중치가 손실에 미친 영향을 계산합니다 loss.backward() # [5단계] 가중치 업데이트 - 손실을 줄이는 방향으로 조정합니다 optimizer.step() total_loss += loss.item() avg_loss = total_loss / len(train_loader) print("Epoch {}/{}, Loss: {:.4f}".format(epoch+1, epochs, avg_loss))

왜 zero_grad()가 필요한가?

PyTorch는 기본적으로 그래디언트를 누적합니다. 이전 배치의 그래디언트가 남아있으면 현재 배치의 그래디언트와 섞여서 잘못된 방향으로 학습됩니다. 매 배치마다 초기화해야 합니다.


Step 5: 모델 평가

python
# [코드 조각 7/7] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) model.eval() # 평가 모드 (Dropout 비활성화) correct = 0 total = 0 with torch.no_grad(): # 그래디언트 계산 끔 -> 메모리 절약 + 속도 향상 for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) # 가장 높은 점수를 가진 클래스를 예측값으로 선택 _, predicted = torch.max(output.data, 1) total += target.size(0) correct += (predicted == target).sum().item() accuracy = 100 * correct / total print("Test Accuracy: {:.2f}%".format(accuracy)) # 기대 출력: Test Accuracy: 98.XX%

학습 진행에 따른 성능 변화 (예시)

Epoch평균 Loss테스트 정확도설명
10.35~95%기본 패턴 학습
30.12~97%세부 특징 학습
50.08~98%미세 조정 단계
100.04~98.5%거의 수렴 완료

핵심 요약

단계코드설명비유
데이터 로드datasets.MNIST + DataLoader데이터 준비 및 배치 공급재료 준비 + 컨베이어 벨트
모델 설계nn.Module 상속신경망 구조 정의건물 설계도
학습 설정CrossEntropyLoss + Adam목표와 전략 설정학습 계획 수립
학습 루프5단계 반복실제 학습 진행반복 학습
평가eval() + no_grad()최종 성능 측정모의시험

학습 체크리스트

  • MNIST 데이터셋의 구조(28x28, 흑백, 10클래스)를 설명할 수 있다
  • transforms.Normalize의 역할을 이해한다
  • MLP 모델의 각 레이어 역할을 설명할 수 있다
  • 학습 루프 5단계를 순서대로 나열할 수 있다
  • model.train()과 model.eval()의 차이를 안다
  • torch.no_grad()를 왜 사용하는지 설명할 수 있다

전체 통합 코드

아래는 위에서 설명한 모든 코드 조각을 하나로 합친 완전한 실행 가능 코드입니다. 이 코드를 복사하여 실행하면 MNIST 분류기를 학습시킬 수 있습니다.

python
⚠️ 로컬 실행 필요
# ============================================================ # MNIST 손글씨 분류 - 전체 통합 코드 # 이 파일 하나로 전체 학습 파이프라인을 실행할 수 있습니다. # ============================================================ import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms # ============================================================ # 1. 디바이스 설정 # ============================================================ device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Using device: {device}") # ============================================================ # 2. 데이터 전처리 및 로드 # ============================================================ transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 데이터셋 다운로드 및 로드 train_dataset = datasets.MNIST(root="./data", train=True, download=True, transform=transform) test_dataset = datasets.MNIST(root="./data", train=False, download=True, transform=transform) # DataLoader 생성 train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False) print(f"훈련 데이터: {len(train_dataset)}개") print(f"테스트 데이터: {len(test_dataset)}개") # ============================================================ # 3. 모델 정의 (MLP) # ============================================================ class MNISTClassifier(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(784, 512) self.fc2 = nn.Linear(512, 256) self.fc3 = nn.Linear(256, 10) self.dropout = nn.Dropout(0.2) def forward(self, x): x = x.view(-1, 784) x = torch.relu(self.fc1(x)) x = self.dropout(x) x = torch.relu(self.fc2(x)) x = self.dropout(x) x = self.fc3(x) return x model = MNISTClassifier().to(device) print(f"모델 파라미터 수: {sum(p.numel() for p in model.parameters()):,}개") # ============================================================ # 4. 학습 설정 # ============================================================ criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) epochs = 10 # ============================================================ # 5. 학습 함수 # ============================================================ def train_one_epoch(model, train_loader, criterion, optimizer, device): model.train() total_loss = 0 for data, target in train_loader: data, target = data.to(device), target.to(device) optimizer.zero_grad() output = model(data) loss = criterion(output, target) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(train_loader) # ============================================================ # 6. 평가 함수 # ============================================================ def evaluate(model, test_loader, device): model.eval() correct = 0 total = 0 with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) output = model(data) _, predicted = torch.max(output.data, 1) total += target.size(0) correct += (predicted == target).sum().item() return 100 * correct / total # ============================================================ # 7. 학습 실행 # ============================================================ print("\n학습 시작...") print("-" * 40) for epoch in range(epochs): avg_loss = train_one_epoch(model, train_loader, criterion, optimizer, device) accuracy = evaluate(model, test_loader, device) print(f"Epoch {epoch+1:2d}/{epochs} | Loss: {avg_loss:.4f} | Test Accuracy: {accuracy:.2f}%") print("-" * 40) print("학습 완료!") # ============================================================ # 8. 최종 평가 # ============================================================ final_accuracy = evaluate(model, test_loader, device) print(f"\n최종 테스트 정확도: {final_accuracy:.2f}%") # ============================================================ # 9. 모델 저장 (선택사항) # ============================================================ # torch.save(model.state_dict(), "mnist_classifier.pth") # print("모델 저장 완료: mnist_classifier.pth")

실행 방법:

  1. 위 코드를 mnist_train.py 파일로 저장
  2. 터미널에서 python mnist_train.py 실행
  3. 약 1-2분 후 98% 이상의 정확도 달성

예상 출력:

Using device: cpu 훈련 데이터: 60000개 테스트 데이터: 10000개 모델 파라미터 수: 535,818개 학습 시작... ---------------------------------------- Epoch 1/10 | Loss: 0.3521 | Test Accuracy: 95.42% Epoch 2/10 | Loss: 0.1423 | Test Accuracy: 96.89% ... Epoch 10/10 | Loss: 0.0412 | Test Accuracy: 98.21% ---------------------------------------- 학습 완료! 최종 테스트 정확도: 98.21%

모델 저장과 불러오기

.pth 파일이란?

학습이 완료되면 모델의 **가중치(weights)**를 파일로 저장할 수 있습니다. PyTorch에서는 .pth 확장자를 사용합니다.

항목설명
저장 내용신경망의 모든 가중치와 편향(bias) 값
파일 크기약 2MB (535,818개 파라미터 기준)
형식PyTorch 직렬화 형식

왜 모델을 저장하나요?

  1. 학습 시간 절약: 10 에폭 학습에 수 분 소요 → 저장하면 다시 학습 불필요
  2. 배포: 학습된 모델을 웹 서버나 앱에서 사용
  3. 재사용: 학습 없이 즉시 예측(추론) 가능

비유: 요리 레시피를 완성한 후 레시피북에 적어두는 것과 같습니다. 다음에 요리할 때 처음부터 개발할 필요 없이 레시피만 보면 됩니다.

모델 저장하기

python
# [코드 조각] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) # 모델의 가중치만 저장 (권장 방식) torch.save(model.state_dict(), "mnist_classifier.pth") print("모델 저장 완료!")

모델 불러오기

python
# [코드 조각] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) # 1. 모델 구조 정의 (학습할 때와 동일해야 함) model = MNISTClassifier() # 2. 저장된 가중치 불러오기 model.load_state_dict(torch.load("mnist_classifier.pth", weights_only=True)) # 3. 평가 모드로 전환 (Dropout 비활성화) model.eval() # 4. 예측 수행 with torch.no_grad(): output = model(image) predicted = torch.argmax(output, dim=1)

추론(예측) 전체 코드

저장된 모델을 불러와서 예측하는 전체 코드입니다.

python
⚠️ 로컬 실행 필요
# [추론용 전체 코드] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) # ============================================================ # MNIST 추론(예측) 예제 # mnist_classifier.pth 파일이 있어야 실행됩니다. # ============================================================ import torch import torch.nn as nn from torchvision import datasets, transforms import random # 모델 구조 정의 class MNISTClassifier(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(784, 512) self.fc2 = nn.Linear(512, 256) self.fc3 = nn.Linear(256, 10) self.dropout = nn.Dropout(0.2) def forward(self, x): x = x.view(-1, 784) x = torch.relu(self.fc1(x)) x = self.dropout(x) x = torch.relu(self.fc2(x)) x = self.dropout(x) x = self.fc3(x) return x # 모델 로드 model = MNISTClassifier() model.load_state_dict(torch.load("mnist_classifier.pth", weights_only=True)) model.eval() print("모델 로드 완료!") # 테스트 데이터 준비 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) test_dataset = datasets.MNIST(root="./data", train=False, download=True, transform=transform) # 랜덤 샘플 5개 예측 print("\n랜덤 샘플 5개 예측 결과:") for i in range(5): idx = random.randint(0, len(test_dataset) - 1) image, label = test_dataset[idx] with torch.no_grad(): output = model(image.unsqueeze(0)) probabilities = torch.softmax(output, dim=1) predicted = torch.argmax(output, dim=1).item() confidence = probabilities[0][predicted].item() * 100 status = "O" if predicted == label else "X" print(f" 정답={label}, 예측={predicted}, 확신도={confidence:.1f}% [{status}]")

예상 출력:

모델 로드 완료!

랜덤 샘플 5개 예측 결과:
  정답=2, 예측=2, 확신도=100.0% [O]
  정답=8, 예측=8, 확신도=99.8% [O]
  정답=7, 예측=7, 확신도=100.0% [O]
  정답=3, 예측=3, 확신도=99.5% [O]
  정답=5, 예측=5, 확신도=98.7% [O]

실제 활용 사례

분야활용 예시
웹 서비스사용자가 손글씨 그리면 숫자 인식
모바일 앱카메라로 숫자 인식
문서 스캔손글씨 숫자 자동 입력
우편 분류우편번호 자동 인식

핵심 포인트: 학습은 시간이 걸리지만, .pth 파일만 있으면 즉시 예측이 가능합니다!


다음 단계: 커스텀 데이터로 프로젝트 진행하기

MNIST는 이미 준비된 데이터셋입니다. 실제 프로젝트에서는 직접 데이터를 준비해야 합니다.

MNIST vs 실제 프로젝트 비교

항목MNIST실제 프로젝트
데이터 수집자동 다운로드직접 수집/구매
라벨링이미 완료됨직접 라벨링 필요
전처리표준화됨데이터에 맞게 설계
Datasetdatasets.MNIST커스텀 클래스 작성
모델참고 자료 풍부직접 실험 필요

커스텀 프로젝트 워크플로우

┌─────────────────────────────────────────────────────────────┐ │ 딥러닝 프로젝트 파이프라인 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 데이터 수집 → 이미지, 텍스트, 센서 데이터 등 │ │ 2. 데이터 정리 → 라벨링, 폴더 구조화, 품질 검수 │ │ 3. Dataset 클래스 → PyTorch가 이해하는 형식으로 변환 │ │ 4. DataLoader → 배치 단위로 데이터 공급 │ │ 5. 모델 정의 → 신경망 구조 설계 │ │ 6. 학습/평가 → 학습 루프 실행, 검증 │ │ 7. 모델 저장 → .pth 파일로 가중치 저장 │ │ 8. 배포/추론 → 실제 서비스에 적용 │ └─────────────────────────────────────────────────────────────┘

커스텀 Dataset 클래스 기본 구조

실제 프로젝트에서는 다음과 같이 커스텀 Dataset을 만듭니다:

python
⚠️ 로컬 실행 필요
# [코드 조각] 로컬 Python 환경에서 실행하세요 (브라우저 실행 불가) from torch.utils.data import Dataset from PIL import Image import pandas as pd import os class CustomImageDataset(Dataset): """ 커스텀 Dataset 클래스 필수 메서드 3가지: - __init__: 데이터 로드/초기화 - __len__: 데이터 개수 반환 - __getitem__: 인덱스로 샘플 반환 """ def __init__(self, csv_file, img_dir, transform=None): self.labels_df = pd.read_csv(csv_file) self.img_dir = img_dir self.transform = transform def __len__(self): return len(self.labels_df) def __getitem__(self, idx): # 이미지 로드 img_name = self.labels_df.iloc[idx]['filename'] img_path = os.path.join(self.img_dir, img_name) image = Image.open(img_path).convert('RGB') # 라벨 label = self.labels_df.iloc[idx]['label'] # 변환 적용 if self.transform: image = self.transform(image) return image, label

폴더 구조 예시

data/ ├── train/ # 학습 데이터 (70~80%) │ ├── cat/ # 클래스별 폴더 │ │ ├── cat_001.jpg │ │ └── ... │ ├── dog/ │ └── bird/ ├── val/ # 검증 데이터 (10~15%) │ ├── cat/ │ ├── dog/ │ └── bird/ └── test/ # 테스트 데이터 (10~15%)

참고: 상세한 커스텀 데이터셋 가이드는 학습 자료 폴더의 딥러닝_프로젝트_절차_가이드.md를 참조하세요.

레슨 정보

레벨
Level 4: 실전 프로젝트
예상 소요 시간
약 5분
참고 영상
YouTube 링크

💡실습 환경 안내

이 레벨은 PyTorch/GPU가 필요하여 Google Colab 사용을 권장합니다.

Colab은 무료 GPU를 제공하여 PyTorch, CNN, Transformer 등을 실행할 수 있습니다.