AI/Model

[Pytorch] MNIST 문자 인식 모델

TSpoons 2025. 1. 10. 13:40

개발 환경 : google colab
모델 학습 하드웨어 : Intel(R) Core(TM) i5-1035G7 CPU @ 1.20GHz
 
MNIST(Modified National Institute of Standard an Technology) 데이터셋:

 
숫자 분류 구현 및 최적화하는 대표적인 예제이다.

1. Pytorch를 사용하기 위한 라이브러리 불러오기 

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transfroms

 
 

  • torch: PyTorch 라이브러리. 텐서 연산 및 신경망 구성에 사용
  • torch.nn: 신경망 레이어를 정의할 때 사용
  • torch.optim: 학습 과정에서 사용될 최적화 알고리즘을 위한 모듈
  • torchvision: 이미지 관련 데이터셋과 전처리를 위한 모듈
  • transforms: 데이터 전처리를 위한 모듈

 

Setting

1. GPU(CUDA)가 가능하면 GPU를 사용하고, 아니면 CPU를 사용하도록 설정

device = 'cuda' if torch.cuda.is_available() else 'cpu'
torch.manual_seed(777)
if device == 'cuda':
    torch.cuda.manual_seed_all(777)
print(device + " is available")

 
 
2. 학습 하이퍼파라미터 설정
 
 

learning_rate = 0.001
batch_size = 64
num_classes = 10
epochs = 10
  • learning_rate: 학습률 설정 (경사하강법에서 얼마나 빠르게 가중치를 업데이트할지를 결정).
  • batch_size: 한 번에 학습에 사용할 데이터 샘플의 수.
  • num_classes: MNIST 데이터셋의 클래스 개수는 10 
  • epochs: 전체 데이터셋을 학습할 반복 횟수

 

 

2. 데이터셋 로드 및 전처리

- 데이터의 텐서화

image: (28,28) -> tensor: (1, 28, 28) (1: 채널 수(흑백))

# MNIST 데이터셋 로드
train_set = torchvision.datasets.MNIST(
    root = './data/MNIST',
    train = True,
    download = True,
    transform = transfroms.Compose([
        transfroms.ToTensor()  # 데이터의 텐서화
    ])
)
test_set = torchvision.datasets.MNIST(
    root = './data/MNIST',
    train = False,
    download = True,
    transform = transfroms.Compose([
        transfroms.ToTensor() # 데이터의 텐서화 
    ])
)

 

 

print(f'Train set size: {len(train_set)}')
print(f'Test set size: {len(test_set)}')

print(f'Image size: {image.size()}')
print(f'Label: {label}')

 
 

DataLoader: 데이터를 배치 단위로 로드하여 네트워크에 전달하는 역할이고,  batch_size만큼 데이터를 나눠서 학습에 사용

- 4차원 텐서로 변환
(1,28,28) -> (batch_size,1,28,28)

from torch.utils.data import DataLoader

train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False)

images, labels = next(iter(train_loader))
print(f'Batch size: {images.size()}')  # 배치의 크기 (64, 1, 28, 28)
print(f'Labels: {labels[:10]}')  # 첫 10개의 라벨 확인

 
 
 
 
 
 

3. CNN model 정의

 

4https://yong0810.tistory.com/17

 
- 모델 시각화 도구
https://netron.app/
 
 

class ConvNet(nn.Module):
    def __init__(self):
        super(ConvNet, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)  # 1채널 입력, 10개의 필터, 5x5 커널
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5) # 10채널 입력, 20개의 필터, 5x5 커널
        self.drop2D = nn.Dropout2d(p=0.25)           # 드롭아웃, 25% 확률로 무작위 뉴런 비활성화
        self.mp = nn.MaxPool2d(2)                    # 2x2 크기의 Max Pooling
        self.fc1 = nn.Linear(320, 100)               # 완전 연결층: 320 -> 100
        self.fc2 = nn.Linear(100, 10)                # 완전 연결층: 100 -> 10 (클래스 수)
    
    def forward(self, x):
        x = F.relu(self.mp(self.conv1(x)))  # Conv1 -> ReLU -> Max Pool
        x = F.relu(self.mp(self.conv2(x)))  # Conv2 -> ReLU -> Max Pool
        x = self.drop2D(x)                  # 드롭아웃
        x = x.view(x.size(0), -1)           # 데이터를 평탄화 (Flatten)
        x = self.fc1(x)                     # 완전 연결층 1
        x = self.fc2(x)                     # 완전 연결층 2
        return F.log_softmax(x, dim=1)      # log Softmax로 확률 값 출력

 
ConvNet class 생성자 init 내용
(1) Convolution layer 1 - nn.Conv2d(1, 10, kernel_size=5 )
- (5,5) kernel을 적용시키면 (28,28) -> (24,24) 로 출력
- 입력 채널 1에서 10개의 출력 채널로 변환 
=> (64, 1, 28, 28) -> (64, 10, 24, 24)
 
(2) Convolution layer 2 -  nn.Conv2d(10, 20, kernel_size=5)
- (1)과 마찬가지로 layer 설계

 
(3) Dropout 2D layer - nn.Dropout2d(p=0.25)
- 채널 단위로 25% 확률로 드롭아웃 작용
- spatial dimension 변화 없이 정규화
=>(64,20,10,10) -> (64,20,10,10)
 
(4) MaxPool 2d layer - nn.MaxPool2d(2)
- 2*2 윈도우(window)로 spatial dimension을 절반으로 축소
=> (64,20,20,20) -> (64,20,10,10)
 
(5) Fully Conneted layer 1 - nn,LInear(320,100)
- 320개의 입력 뉴런에서 100개의 출력 뉴런으로 변환
=> (64,320) -> (64,100)
 
(6) Fully Conneted layer 2 - nn.LInear(100,10)
- 100개의 입력 뉴런에서 10개의 출력 뉴런으로 변환
=> (64,100) -> (64,10)

 

ConvNet class 멤버함수 forward 내용
(1) 첫 번째 레이어 : x = F.relu(self.mp(self.conv1(x)))
- self.conv1(x) : (64,1,28,28) -> (64,10,24,24)
 

  • 연산: 5x5 커널로 합성곱 수행
  • 작업: 특징 추출 시작, 10개의 서로 다른 특징 맵 생성

 
- self.mp() : (64,10,24,24) -> (64,10,12,12)
 

  • 연산: 2x2 윈도우로 최대값 추출
  • 작업: 공간적 크기 축소, 주요 특징 보존

 
- F.relu() : (64,10,12,12) -> (64,10,12,12)
 

  • 연산: max(0, x) - 음수값을 0으로 변환
  • 작업: 비선형성 추가, 그래디언트 소실 방지

 
(2) 두 번째 레이어: x = F.relu(self.mp(self.conv2(x)))
- self.conv2(x) : (64,10,12,12) -> (64,20,8,8)

  • 연산: 5x5 커널로 합성곱 수행
  • 작업: 특징을 더 쪼개기

- self.mp() : (64,20,8,8) -> (64,20,4,4)
 

  • 연산: 2x2 윈도우로 최대값 추출
  • 작업: 특징 맵 크기 추가 축소

 
- F.relu() : (64,20,4,4) -> (64,20,4,4)
 

  • 연산: max(0, x)
  • 작업: 두 번째 비선형성 추가

 
 
(3) 드롭아웃 : x = self.drop2D(x) : (64,20,4,4) -> (64,20,4,4)
 

  • 연산: 25% 확률로 채널 전체를 0으로 설정
  • 작업: 과적합 방지, 모델의 일반화 능력 향상

(4) 평탄화 : x = x.view(x.size(0), -1) : (64,20,4,4) -> (64, 320)
 

  • 연산: 첫 번째 차원(배치 사이즈)을 제외한 모든 차원을 펼침
  • 작업: 전결합층 입력을 위한 1차원 벡터 변환

(5) 첫 번째 완전연결층: x = self.fc1(x) : (64,320) -> (64, 100)

  • 연산: 선형 변환 W₁x + b₁
  • 작업: 특징 공간 차원 축소, 분류를 위한 특징 조합

(6) 두 번째 완전연결층: x = self.fc2(x) : (64,100) -> (64,10)

  • 연산: 선형 변환 W₂x + b₂
  • 작업: 최종 클래스 점수 계산

(7) Log softmax : return F.log_softmax(x, dim=1) : (64,10) -> (64,10)
 

  • 연산: log(softmax(x)) = log(exp(xi) / Σexp(xj))
  • 작업:
    • 클래스별 확률의 로그값 계산
    • 수치적 안정성 향상
    • 크로스 엔트로피 손실 계산 용이

 
 
 
 

4. 학습 ( 공부 예정)

for epoch in range(epochs):
    avg_cost = 0
    for data, target in train_loader:
        data, target = data.to(device), target.to(device)
        optimizer.zero_grad()  # 기울기 초기화
        hypothesis = model(data)  # 모델을 통해 예측
        cost = criterion(hypothesis, target)  # 손실 계산
        cost.backward()  # 역전파 계산
        optimizer.step()  # 파라미터 업데이트
        avg_cost += cost / len(train_loader)
    print('[Epoch: {:>4}] cost = {:>.9}'.format(epoch + 1, avg_cost))

 
(1) epochs : 전체 데이터셋을 반복 학습하는 횟수
(2) 배치 데이터 로드 및 출력 결과 대입
- data : 입력 이미지 배치 (64,1,28,28)
- target : 정답 레이블 배치 (64,)
(3) 그래디언트 초기화 : optimer.zero_grad()
- 이전 배치에서 계산된 그래디언트 제거
- 그래디언트 누적 방지
(4) 순전파 : hypothesis = model(data)
- 위에서 만든 model에 입력 데이터 통과 시키기
(5) 손실 계산 : cost = criterion(hypothesis, target)
- 예측값과 실제값의 차이 계산
- CrossEntropyLoss 계산 과정

  • 예측값의 Log Softmax 계산 (모델 설계에서 포함함)
  • 정답 레이블에 해당하는 로그 확률 선택
  • 음의 로그 우도 계산

(6) 역전파 : cost.backward()
- 손실에 대한 각 파라미터의 그래디언트 계산
- 연쇄 법칙(Chain Rule) 사용
- 과정

  • 출력층부터 시작
  • 각 층(layer)을 거슬러 올라가며 그래디언트(gradient 계산
  • requires_grad=True인 모든 텐서에 대해 .grad 속성 업데이트

 
(7) 파라미터 업데이트 
- 계산된 그래디언트를 사용하여 모델 파라미터 업데이트
- 학습률(learning rate)에 따라 업데이트 크기 조절
 
Adam optimizer의 경우:

  1. 1차 모멘텀(momentum) 계산
  2. 2차 모멘텀(velocity) 계산
  3. 편향 보정
  4. 파라미터 업데이트

(8) 평균 손실 계산 : avg_cost += cost / len(train_loader)
 

  • 현재 epoch의 평균 손실 누적
  • len(train_loader): 총 배치 수
  • 전체 데이터셋에 대한 평균 성능 측정

 
(9) 학습 진행 상황 출력

 
 
 

5. 평가

model.eval()  # 모델을 평가 모드로 전환 (dropout, batch_norm 해제)
with torch.no_grad():  # 기울기 계산 비활성화
    correct = 0
    total = 0
    for data, target in test_loader:
        data, target = data.to(device), target.to(device)
        out = model(data)
        preds = torch.max(out.data, 1)[1]  # 예측된 클래스 값
        total += len(target)
        correct += (preds == target).sum().item()  # 정확도 계산
    print('Test Accuracy: ', 100.*correct/total, '%')

 
(1) 모델 평가 모드 설정 - model.eval()
- 학습 시 사용된 확률적 요소들 비활성화
- 일관된 추론 결과 보장, 전체 네트워크 활성화 유지
 
(2) 그래디언트 계산 비활성화 - with torch.no_grad();
- 추론 시 역전파가 필요없으므로 모든 연산에서 그래디언트 추적 비활성화
 
(3) 테스트 데이터 처리
(4) 모델 추론 - out = model(data)
 
(5) 예측 클래스 계산 - preds = torch.max(out.data, 1)[1]
 

  • out.data: 출력 텐서의 값만 선택
  • torch.max(tensor, dim=1):
    • dim=1 (클래스 차원)을 따라 최대값 검색
    • 반환값: (최대값, 최대값의 인덱스)
  • [1]: 인덱스 값만 선택 (예측 클래스)

(6) 정확도 계산
- 예측값과 실제값을 비교하여 맞은 개수에 전체 시도 개수를 나눔
 
 
 
 
 

6. Real test

import matplotlib.pyplot as plt
import numpy as np

num_images = 10


model.eval()

# 예측할 데이터와 실제 레이블을 가져오기
data_iter = iter(test_loader)
images, labels = next(data_iter)


images, labels = images.to(device), labels.to(device)

# 모델 예측
with torch.no_grad():
    outputs = model(images)
    _, predicted = torch.max(outputs.data, 1)


fig = plt.figure(figsize=(12, 6))
for idx in range(num_images):
    ax = fig.add_subplot(2, 5, idx + 1)
    ax.imshow(images[idx].cpu().squeeze(), cmap='gray')  # 이미지를 시각화 (채널을 제거)
    ax.set_title(f'True: {labels[idx].item()}, Pred: {predicted[idx].item()}')  # 실제 레이블과 예측값 표시
    ax.axis('off')  # 축 제거

plt.tight_layout()
plt.show()

 
(1) 예측 - _, predicted = torch.max(outputs.data, 1)
- torch.max로 예측 클래스 추출

  • outputs: (batch_size, 10) 형태의 로그 확률
  • predicted: (batch_size,) 형태의 예측 클래스

(2) 이미지 표시- ax.imshow(images[idx].cpu().squeeze(), cmap='gray')
 

  • images[idx]: 단일 이미지 선택
  • .cpu(): GPU 텐서를 CPU로 이동 (Matplotlib는 CPU 텐서만 처리 가능)
  • .squeeze(): 차원 축소 (1, 28, 28) → (28, 28)
  • cmap='gray': 흑백 이미지로 표시