실습 3주차, 이번 주는 현대 AI의 근간이 된 Transformer(트랜스포머) 아키텍처를 논문을 이해하는 것을 목표로 삼았습니다. 단순히 라이브러리를 사용하는 것을 넘어, 내부 연산의 흐름과 설계 의도를 파고든 기록을 남깁니다.

1. Transformer: 왜 'Attention'만으로 충분한가?

기존 NLP의 표준이었던 RNN(Recurrent Neural Networks)은 문장을 단어 단위로 한 단계씩 순차적으로 처리했습니다. 하지만 이러한 방식은 두 가지 치명적인 병목 현상을 야기했습니다.

❶ 순차적 연산의 한계 (Sequential Bottleneck)

RNN은 t 시점의 연산을 하기 위해 t-1 시점의 결과가 반드시 필요합니다. 이는 최신 GPU의 강점인 병렬 연산(Parallel Computing)을 원천적으로 차단하여, 모델이 커질수록 학습 시간이 기하급수적으로 늘어나는 결과를 초래했습니다.

❷ 정보 소실 (Vanishing Gradient & Long-term Dependency)

문장이 길어질수록 앞부분의 정보가 뒷부분까지 전달되지 못하고 흐릿해지는 문제가 있었습니다. LSTM과 GRU가 이를 보완하려 했으나, 수백 단어 이상의 긴 문맥(Context)을 완벽히 유지하기엔 역부족이었습니다.

❸ 트랜스포머의 해법: "모든 단어를 동시에 보라"

트랜스포머는 '순서'라는 개념을 연산 순서에서 분리해냈습니다.

  • Self-Attention: 문장 내의 모든 단어 쌍(Pair) 간의 관계를 한 번에 계산합니다. "The animal didn't cross the street because it was too tired"라는 문장에서 'it'이 무엇을 가리키는지 찾을 때, RNN처럼 앞의 단어를 다 거쳐오는 것이 아니라 'it'과 'animal'의 유사도를 즉각적으로 계산해버리는 방식입니다.
  • Positional Encoding: 순차 연산을 없애면서 사라진 '단어의 위치 정보'를 보완하기 위해 도입되었습니다. 단어 임베딩에 사인(Sine)과 코사인(Cosine) 함수 기반의 고유 위치 값을 더해주어, 모델이 "이 단어는 문장의 앞에 있다"는 것을 수학적으로 인지하게 만듭니다.

결과적으로, 트랜스포머는 모든 정보를 한 번에(Global Context) 보면서도 병렬 처리가 가능해졌고, 이를 통해 훨씬 거대한 데이터를 빠른 속도로 학습할 수 있게 되었습니다. 이것이 바로 논문 제목이 "Attention이면 충분하다(Attention Is All You Need)"인 이유입니다.


2. 핵심 메커니즘: Scaled Dot-Product Attention

트랜스포머의 성능은 결국 "입력 데이터 중 어디에 집중(Attention)할 것인가"를 수학적으로 얼마나 잘 표현하느냐에 달려 있습니다. 그 수식이 바로 아래의 공식입니다.

이 수식이 왜 이렇게 설계되었는지, 각 구성 요소의 역할을 깊이 있게 분석해 보았습니다.

❶ Query, Key, Value의 역할극

단순히 데이터를 넣는 것이 아니라, 데이터를 세 가지 목적에 맞게 투영(Projection)합니다.

  • Query (질문): "현재 내가 처리 중인 이 단어와 관련된 정보가 어디에 있지?"
  • Key (색인): "나는 이런 정보를 가지고 있어. 네 질문과 얼마나 일치하니?"
  • Value (값): "일치한다면, 내가 가진 이 실제 정보를 줄게."

❷ Dot-product (QK^T): 관계의 수치화

질문(Q)과 색인(K)을 내적(Dot-product)하는 과정입니다. 두 벡터가 유사한 방향을 향할수록 내적값은 커집니다. 즉, 단어 사이의 연관성(Similarity)을 점수로 환산하는 단계입니다. 이 점수가 높을수록 해당 단어의 정보를 더 많이 가져오게 됩니다.

❸ Scaling ($\frac{1}{\sqrt{d_k}}): 왜 굳이 나누는가?

이 부분이 설계의 핵심입니다.

  • 문제점: 모델의 차원(d_k)이 커질수록 내적값(QK^T)의 절댓값도 기하급수적으로 커질 가능성이 높습니다.
  • 결과: 내적값이 너무 커지면 softmax 함수를 통과할 때 출력값이 0 또는 1에 극단적으로 치우치게 됩니다.
  • 치명적 단점: 소프트맥스 그래프의 끝부분에서는 기울기(Gradient)가 거의 0에 수렴하여, 기울기 소실(Vanishing Gradient) 문제가 발생하고 학습이 멈춰버립니다.
  • 해결: 이를 방지하기 위해 차원의 크기인 sqrt{d_k}로 나누어(Scaling) 값의 범위를 조절함으로써 학습의 안정성을 확보한 것입니다.

❹ Multi-Head Attention: 앙상블의 지혜

논문에서는 이 연산을 한 번만 하는 것이 아니라, 여러 개의 'Head'로 나누어 병렬로 수행합니다.

  • 한 Head는 '문법적 관계'에 집중하고,
  • 다른 Head는 '의미적 관계'에 집중하며,
  • 또 다른 Head는 *먼 거리의 맥락'에 집중합니다.
  • 이렇게 나온 서로 다른 결과들을 하나로 합침으로써(Concatenate), 모델은 문장을 다각도에서 입체적으로 이해할 수 있게 됩니다.

3. Encoder vs Decoder: 닮은 듯 다른 두 엔진

트랜스포머 아키텍처는 정보를 압축하는 인코더와 이를 바탕으로 새로운 정보를 생성하는 디코더의 협업 시스템입니다. 두 엔진은 겉보기엔 비슷하지만, 정보를 다루는 '태도'에서 결정적인 차이가 있습니다.

❶ Encoder: 양방향 문맥의 마스터 (Bidirectional Context)

인코더의 역할은 입력받은 문장을 완벽하게 이해하는 것입니다.

  • Self-Attention: 인코더 내에서는 모든 단어가 서로를 자유롭게 바라봅니다. 예를 들어 "사과가 맛있다"라는 문장에서 '사과'를 처리할 때 '맛있다'라는 정보를 즉시 가져올 수 있습니다.
  • Global Understanding: 문장 전체의 관계를 한 번에 파악하여 각 단어의 고차원적인 의미 벡터(Context Vector)를 만들어냅니다.

❷ Decoder: 예측의 마술사와 'Masking'의 규칙

디코더는 인코더가 넘겨준 정보를 바탕으로 한 단어씩 결과를 출력합니다. 여기서 가장 중요한 점은 "미래를 봐서는 안 된다"는 것입니다.

  • Look-ahead Masking: 번역을 할 때 모델이 다음에 올 단어를 미리 알고 있다면 학습이 제대로 되지 않습니다. 그래서 현재 시점보다 뒤에 있는 단어들을 수학적으로 '가리는(Masking)' 기법을 사용합니다. 이를 통해 디코더는 오직 '지금까지 생성한 단어들'만 참고하여 다음 단어를 예측하게 됩니다.
  • Auto-regressive: 자신의 출력을 다시 자신의 입력으로 사용하는 자기 회귀적 특성을 가집니다.

❸ Encoder-Decoder Attention: 두 엔진의 교차점

디코더의 중간에는 인코더와 대화하는 특별한 층이 있습니다.

  • The Bridge: 여기서 디코더는 "내가 지금 단어를 하나 만들어야 하는데, 인코더 네가 분석한 문장에서 어떤 부분에 집중하면 좋을까?"라고 질문(Query)을 던집니다.
  • Interaction: 인코더는 자신이 만든 Key와 Value를 제공하며 응답합니다. 이 과정을 통해 "I love you"라는 영어 문장에서 'love'를 처리할 때 한국어 '사랑'이라는 부분에 강한 Attention을 가할 수 있게 됩니다.

4 후기

이번 논문 스터디를 통해 얻은 가장 큰 수확은 "수학적 엄밀함이 곧 모델의 성능으로 이어진다"는 확신으로, 단순히 오픈 소스 코드를 복사해 붙여넣는 수동적인 방식에서 벗어나 아키텍처의 설계 의도를 논리적으로 파악할 때 비로소 모델을 완벽히 제어할 수 있음을 깨달았습니다. 특히 내적값이 커짐에 따라 발생하는 Gradient Vanishing 문제를 해결하기 위해 도입된 Scaling(sqrt{d_k})의 수학적 필연성을 이해하며 작은 수식 하나가 거대 모델의 학습 가능 여부를 결정짓는 결정적 단서임을 체감했고, 인과관계를 보존하기 위한 Masking 기법이 어떻게 행렬 연산 내부에서 논리적 제약 조건을 구현하는지 학습하며 딥러닝이 단순한 '블랙박스'가 아닌 철저히 계산된 공학의 산물임을 확인했습니다.

전에는 단순히 0~9까지의 손글씨 데이터가 있는 MNIST를 사용하였습니다.

이번에는 FashionMNIST 데이터셋을 활용하여 일반적인 딥러닝 모델(MLP)과 이미지 처리에 특화된 합성곱 신경망(CNN)의 성능 차이를 심층 분석해 보았습니다.

 

1. FashionMNIST 데이터셋이란?

MNIST가 손글씨 숫자였다면, FashionMNIST는 운동화, 셔츠, 가방 등 10가지 종류의 패션 아이템으로 구성된 데이터셋입니다.

  • 크기: 28 x 28 픽셀 (흑백)
  • 특징: 숫자 데이터보다 형태가 복잡하고 경계선이 다양하여, 일반적인 MLP로는 높은 정확도를 얻기가 더 까다롭습니다.

2.  MLP와 CNN의 구조 차이

단순 딥러닝 (MLP) 구조

가장 기본적인 형태인 다층 퍼셉트론(Multi-Layer Perceptron)입니다.

  • 특징: 이미지를 1차원으로 길게 펼쳐서(Flatten) 입력합니다.
  • 한계: 이미지를 한 줄로 세우다 보니 픽셀 간의 공간적 구조(예: 소매와 몸통의 연결성) 정보가 손실됩니다.

합성곱 신경망 (CNN) 구조

이미지의 특징을 추출하는 데 최적화된 CNN 모델입니다.

CNN이 강력한 이유:

  1. 커널(Kernel/Filter): 이미지의 국소적인 부분(가로선, 세로선, 질감 등)을 훑으며 특징을 추출합니다.
  2. 풀링(Pooling): 중요한 정보만 남기고 데이터 크기를 줄여 모델이 사소한 변화(이미지가 약간 치우침 등)에 강해지도록 만듭니다.
  3. 공간 정보 유지: 이미지를 펼치지 않고 2차원 그대로 학습하므로 형태를 훨씬 잘 이해합니다.

3. 전체 코드

코드가 다소 길어지는 관계로 링크 첨부 하였습니다.

https://github.com/docodocod/ML-DL/blob/main/DL/fashionMNIST.ipynb

 

ML-DL/DL/fashionMNIST.ipynb at main · docodocod/ML-DL

Contribute to docodocod/ML-DL development by creating an account on GitHub.

github.com

 

4.코드 상세 분석

두 모델 모두 데이터 준비 단계는 동일하지만, 딥러닝의 기초가 되는 매우 중요한 부분입니다.

train_dataset = datasets.FashionMNIST(root='FashionMNIST_data/', train=True, transform=transforms.ToTensor(), download=True)
test_dataset = datasets.FashionMNIST(root='FashionMNIST_data/', train=False, transform=transforms.ToTensor(), download=True)
  • transforms.ToTensor(): 가장 중요한 전처리입니다. 0~255 사이의 픽셀 강도 값을 0~1 사이의 실수값으로 정규화(Normalization)하고, 데이터를 파이토치 연산에 최적화된 Tensor 형태로 변환합니다.
  • random_split: 데이터를 훈련용(85%)과 검증용(15%)으로 분리합니다. 모델이 훈련 데이터에만 익숙해지는 '과적합'을 방지하고, 중간중간 실력을 테스트하기 위함입니다.
  • DataLoader: 데이터를 BATCH_SIZE=32 단위로 쪼개서 공급합니다. 이는 메모리 절약뿐만 아니라, 가중치를 더 자주 업데이트하게 하여 학습의 효율을 높입니다.

[MLP 모델: 선형적인 정보 처리]

class MyDeepLearningModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()      # (1, 28, 28) 이미지를 784개의 긴 줄로 만듦
        self.fc1 = nn.Linear(784, 256)   # 784개의 특징 -> 256개의 특징으로 압축
        self.reLU = nn.ReLU()           # 비선형성 추가
        self.dropout = nn.Dropout(0.3)  # 과적합 방지 (뉴런 30% 끄기)
        self.fc2 = nn.Linear(256, 10)    # 최종 10개 클래스로 판별
  • 작동 방식: 이미지를 단순히 '숫자의 집합'으로 봅니다. 픽셀 간의 위치 관계보다는 각 픽셀값이 가지는 통계적인 빈도를 학습합니다.

[CNN 모델: 공간 정보를 보존하는 특징 추출]

class MyCNNModel(nn.Module):
    def __init__(self):
        super().__init__()
        # 1차 특징 추출: 3x3 필터 32개 사용
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1)
        # 2차 특징 추출: 3x3 필터 64개 사용
        self.conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3, padding=1)
        # 정보 압축: 크기를 절반으로 줄임 (2x2 Max Pooling)
        self.pooling = nn.MaxPool2d(kernel_size=2, stride=2)
        # 분류기 (Fully Connected Layer)
        self.fc1 = nn.Linear(7*7*64, 256)
        self.fc2 = nn.Linear(256, 10)
  • 핵심 연산: Conv2d가 이미지 위를 훑으며 선, 면, 복잡한 문양 등의 특징 지도(Feature Map)를 생성합니다. view(-1, 7*7*64) 부분은 추출된 2차원 특징들을 최종 분류를 위해 1차원으로 변환하는 과정입니다.

학습 및 검증 루프 (Training & Evaluation)

두 코드에서 공통적으로 사용된 핵심 함수들입니다.

① model_train (학습)

  • model.train(): 모델을 학습 모드로 전환합니다. Dropout이 활성화되어 과적합을 방지합니다.
  • optimizer.zero_grad(): 딥러닝은 기울기를 누적하는 성질이 있습니다. 새로운 배치마다 이전의 기울기를 지워줘야 정확한 학습 방향을 잡습니다.
  • loss.backward(): 역전파(Backpropagation)입니다. 출력층에서 발생한 오차를 입력층까지 거꾸로 전달하며 각 가중치가 오차에 기여한 정도를 계산합니다.
  • optimizer.step(): 계산된 기울기를 바탕으로 가중치를 실제로 업데이트합니다.

② model_evaluate / model_test (평가)

  • model.eval(): 평가 모드로 전환합니다. Dropout을 비활성화하여 모든 뉴런의 지식을 총동원합니다.
  • with torch.no_grad(): 가중치를 업데이트하지 않으므로 미분 계산을 끕니다. 메모리 사용량을 대폭 줄이고 연산 속도를 높입니다.

최적화 도구 (Optimizer)의 차이

이 부분이 성능 차이의 숨은 조역입니다.

  • MLP (SGD): torch.optim.SGD. 전통적인 방식으로, 일정한 학습률로 경사를 따라 내려갑니다. 안정적이지만 최적점에 도달하는 속도가 느릴 수 있습니다.
  • CNN (Adam): torch.optim.Adam. 방향뿐만 아니라 속도까지 스스로 조절하는 영리한 옵티마이저입니다. 이 덕분에 CNN이 훨씬 빠르게 99%라는 경이로운 정확도에 도달할 수 있었습니다.

5. 결과 해석 및 결론 분석

MLP vs CNN 손실도

 

MLP vs CNN 정확도

 

  1. 정확도 (Accuracy): MLP(86.75%) vs CNN(99.42%).
    • CNN은 이미지를 있는 그대로 인식하기 때문에 복잡한 의류 패턴을 거의 완벽하게 구분해 냈습니다.
  2. 손실 (Loss): MLP는 손실값이 줄어드는 속도가 더디지만, CNN은 학습 초반에 이미 손실값이 0에 가깝게 급감합니다.
  3. 성능 요인:
    • CNN의 특징 추출(Feature Extraction) 능력.
    • Adam 옵티마이저의 효율적인 가중치 업데이트.
    • GPU(CUDA)를 활용한 대규모 행렬 연산 처리.

6. 후기

코드 전체를 비교해 본 결과 MLP는 이미지를 숫자로만 보지만 CNN은 이미지를 '형태'로 이해하기 때문에 이미지 데이터에는 CNN이 압도적으로 유리하다는 결론을 얻었습니다.

딥러닝을 공부할 때 가장 먼저 접하게 되는 '교과서' 같은 데이터셋이 있습니다. 바로 MNIST입니다. 이 데이터를 활용해 인공지능이 어떻게 숫자를 인식하는지 그 과정을 PyTorch 코드로 상세히 파헤쳐 보았습니다.


1. MNIST 데이터셋이란?

MNIST (Modified National Institute of Standards and Technology) 데이터셋은 0부터 9까지의 손글씨 숫자로 이루어진 대규모 데이터베이스입니다.

  • 구성: 60,000개의 학습 데이터와 10,000개의 테스트 데이터로 이루어져 있습니다.
  • 규격: 각 이미지는 $28 \times 28$ 픽셀의 흑백 이미지이며, 각 픽셀은 0(검은색)부터 255(흰색)까지의 밝기 값을 가집니다.
  • 상징성: 머신러닝계의 'Hello World'라고 불릴 만큼, 새로운 알고리즘의 성능을 검증하는 표준으로 오랫동안 사랑받아 왔습니

2. 데이터 설계: "나누고, 섞고, 묶어라"

좋은 모델을 만들기 위해서는 데이터를 단순히 넣는 것이 아니라 전략적으로 관리해야 합니다.

2-1. 훈련, 검증, 테스트의 삼박자

작성하신 코드에서는 전체 6만 개의 데이터를 다음과 같이 나눕니다.

  • Train (51,000): 모델이 실제로 공부하며 가중치를 업데이트하는 데이터입니다.
  • Validation (9,000): 학습 도중 "공부가 잘되고 있나?"를 확인하는 중간 점검용입니다. 이 결과를 보고 하이퍼파라미터를 수정합니다.
  • Test (10,000): 모든 학습이 끝난 후, 실전 성능을 측정하는 최종 시험입니다.

2-2. DataLoader: 효율적인 학습을 위한 '급식소'

BATCH_SIZE=32
train_dataset_loader = DataLoader(dataset=train_dataset, batch_size=BATCH_SIZE, shuffle=True)
  • Batch Size (32): 5만 개를 한 번에 공부하면 모델이 과부하에 걸릴 수 있습니다. 32개씩 끊어서 공부하며 효율을 높입니다.
  • Shuffle (True): 데이터의 순서를 무작위로 섞어 모델이 특정 순서를 외우지 못하게 합니다.

3. 전체 코드

import torch
from torch import nn
from torchvision import datasets, transforms
from torch.utils.data import random_split
from torch.utils.data import Dataset, DataLoader

train_dataset=datasets.MNIST(root="MNIST_data/",train=True, # 학습 데이터
                             #Totenser함수를 사용하면 0~1 사이의 실수들로 바꿔주면서 계산하기 쉽게 만들어준다. 그리고 텐서타입으로 반환해줌
                             transform=transforms.ToTensor(), # 0~255까지의 값을 0~1 사이의 값으로 변환시켜줌
                             download=True)
test_dataset=datasets.MNIST(root="MNIST_data/",train=False, # 테스트 데이
                            transform=transforms.ToTensor(), # 0~255까지의 값을 0~1 사이의 값으로 변환시켜줌
                            download=True)
print(len(train_dataset))
 
60000
print(len(test_dataset))
 
10000
train_dataset_size=int(len(train_dataset)*0.85)
validation_dataset_size=int(len(train_dataset)*0.15)

train_dataset,validation_dataset=random_split(train_dataset,[train_dataset_size,validation_dataset_size])

print(len(train_dataset), len(validation_dataset), len(test_dataset))
 
51000 9000 10000
class MyDeepLearningModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten=nn.Flatten()
        self.fc1=nn.Linear(784,256)
        self.relu=nn.ReLU()
        self.dropout=nn.Dropout(0.3)
        self.fc2=nn.Linear(256,10)

    def forward(self, data):
        data=self.flatten(data)
        data=self.fc1(data)
        data=self.relu(data)
        data=self.dropout(data)
        logits=self.fc2(data)
        return logits
BATCH_SIZE=32

train_dataset_loader=DataLoader(dataset=train_dataset,
                                batch_size=BATCH_SIZE,
                                shuffle=True)

validation_dataset_loader=DataLoader(dataset=validation_dataset,
                                     batch_size=BATCH_SIZE,
                                     shuffle=True)

test_dataset_loader=DataLoader(dataset=test_dataset,
                               batch_size=BATCH_SIZE,
                               shuffle=True)
model=MyDeepLearningModel()

loss_function=nn.CrossEntropyLoss()

optimizer=torch.optim.SGD(model.parameters(), lr=1e-2)
def model_train(dataloader, model, loss_function, optimizer):

    model.train() # 신경망을 학습모드(모델 파라미터를 업데이트하는 모드로)로 전환

    train_loss_sum=train_correct=train_total=0
    total_train_batch=len(dataloader)

    for images, labels in dataloader:
        x_train=images.view(-1,28*28) # 처음 크기는 (batch_size, 1,28,28)인데 이걸 (batch_size, 784)로 변환
        y_train=labels

        outputs=model(x_train) # 입력 데이터에 대해 예측 값 계산
        loss=loss_function(outputs, y_train) # 모델 예측 값과 정답과의 오차인 손실함수 계산

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        train_loss_sum+=loss.item()

        train_total+=y_train.size(0)
        train_correct+=((torch.argmax(outputs,1)==y_train)).sum().item()

    train_avg_loss=train_loss_sum/total_train_batch #학습 데이터 평균 오차 계산
    train_avg_accuracy=100*train_correct/train_total # 학습데이터 평균 정확도 게산

    return (train_avg_loss, train_avg_accuracy)
def model_evaluate(dataloader, model, loss_function, optimizer):

    model.eval() # 신경망을 추론모드로 전환

    with torch.no_grad(): # 미분 하지 않겠다는 코드 즉 모델 파라미터를 업데이트 시키지 않겠다는 의미

        val_loss_sum=val_correct=val_total=0
        total_val_batch=len(dataloader)

        for images, labels in dataloader: # images에는 MNIST 이미지, labels에는 0~9 정답 숫자

            x_val=images.view(-1,28*28) # 처음 크기는 (batch_size, 1,28,28)인데 이걸 (batch_size, 784)로 변환
            y_val=labels

            outputs=model(x_val)
            loss=loss_function(outputs, y_val)

            val_loss_sum+=loss.item()

            val_total+=y_val.size(0)

            val_correct+=((torch.argmax(outputs, 1)==y_val)).sum().item()

        val_avg_loss=val_loss_sum/total_val_batch # 검증데이터 평균 오차 계산
        val_avg_accuracy=100*val_correct/val_total # 검증 데이터 평균 정확도 계산

    return (val_avg_loss, val_avg_accuracy)
train_loss_list=[]
train_accuracy_list=[]

val_loss_list=[]
val_accuracy_list=[]

EPOCH=20

for epoch in range(EPOCH):

    #===========model train=============
    train_avg_loss, train_avg_accuracy=model_train(train_dataset_loader,
                                                   model,loss_function,
                                                   optimizer)
    train_loss_list.append(train_avg_loss)
    train_accuracy_list.append(train_avg_accuracy)
    #-----------------------------------


    #===========model evaluation========
    val_avg_loss, val_avg_accuracy=model_evaluate(validation_dataset_loader,
                                                  model, loss_function,
                                                  optimizer)

    val_loss_list.append(val_avg_loss)
    val_accuracy_list.append(val_avg_accuracy)

    print(f'epoch:{epoch}, train loss={train_avg_loss}, train accuracy={train_avg_accuracy}, val loss={val_avg_loss}, val accuracy={val_avg_accuracy}')
epoch:0, train loss=0.9801464274693912, train accuracy=77.03333333333333, val loss=0.4576594428496158, val accuracy=88.36666666666666
epoch:1, train loss=0.43575271577989144, train accuracy=87.72941176470589, val loss=0.3498166104950381, val accuracy=90.47777777777777
epoch:2, train loss=0.3644874161728652, train accuracy=89.60980392156863, val loss=0.30739057418091076, val accuracy=91.6
epoch:3, train loss=0.32238478469691584, train accuracy=90.82156862745099, val loss=0.27868536130544985, val accuracy=92.4
epoch:4, train loss=0.294447719761015, train accuracy=91.65882352941176, val loss=0.25568844720139994, val accuracy=92.91111111111111
epoch:5, train loss=0.27096652550157746, train accuracy=92.38823529411765, val loss=0.23941183722980902, val accuracy=93.16666666666667
epoch:6, train loss=0.2514003829415911, train accuracy=92.88235294117646, val loss=0.22217259458299224, val accuracy=93.84444444444445
epoch:7, train loss=0.2360796409692975, train accuracy=93.27450980392157, val loss=0.20950249758225384, val accuracy=94.18888888888888
epoch:8, train loss=0.2215461977856839, train accuracy=93.77254901960784, val loss=0.1989852928932994, val accuracy=94.5111111111111
epoch:9, train loss=0.2100676079181349, train accuracy=94.17450980392157, val loss=0.18825880632280034, val accuracy=94.62222222222222
epoch:10, train loss=0.19935951428546322, train accuracy=94.3529411764706, val loss=0.17928070510929148, val accuracy=95.0111111111111
epoch:11, train loss=0.1907167539825497, train accuracy=94.64313725490196, val loss=0.17234331148128348, val accuracy=95.2
epoch:12, train loss=0.1811797786422233, train accuracy=94.85098039215686, val loss=0.16591329714085193, val accuracy=95.5
epoch:13, train loss=0.17282068151910526, train accuracy=95.12745098039215, val loss=0.1595339657910538, val accuracy=95.4888888888889
epoch:14, train loss=0.16772893399124034, train accuracy=95.23333333333333, val loss=0.1530728926089533, val accuracy=95.73333333333333
epoch:15, train loss=0.16029620828595448, train accuracy=95.50980392156863, val loss=0.1482701521673973, val accuracy=96.0111111111111
epoch:16, train loss=0.1540753280620092, train accuracy=95.6470588235294, val loss=0.14427426107333485, val accuracy=96.14444444444445
epoch:17, train loss=0.1494620611746252, train accuracy=95.67843137254901, val loss=0.14024397007566183, val accuracy=96.12222222222222
epoch:18, train loss=0.14349487275551243, train accuracy=95.90588235294118, val loss=0.13517580503029814, val accuracy=96.27777777777777
epoch:19, train loss=0.1399755400034086, train accuracy=96.07058823529412, val loss=0.13144204594485515, val accuracy=96.27777777777777
def model_test(dataloader, model):

    model.eval()

    with torch.no_grad(): # 미분 하지 않겠다는 코드 즉 모델 파라미터를 업데이트 시키지 않겠다는 의미

        test_loss_sum=test_correct=test_total=0
        total_test_batch=len(dataloader)

        for images, labels in dataloader: # images에는 MNIST 이미지, labels에는 0~9 정답 숫자

            x_test=images.view(-1,28*28) # 처음 크기는 (batch_size, 1,28,28)인데 이걸 (batch_size, 784)로 변환
            y_test=labels

            outputs=model(x_test)
            loss=loss_function(outputs, y_test)

            test_loss_sum+=loss.item()

            test_total+=y_test.size(0)

            test_correct+=((torch.argmax(outputs, 1)==y_test)).sum().item()

        test_avg_loss=test_loss_sum/total_test_batch # 검증데이터 평균 오차 계산
        test_avg_accuracy=100*test_correct/test_total # 검증 데이터 평균 정확도 계산

    return (test_avg_loss, test_avg_accuracy)
test_loss,test_accuracy=model_test(test_dataset_loader, model)
print(f'accuracy:{test_accuracy}')
print(f'loss:{test_loss}')
 
accuracy:96.5
loss:0.12182765494817838
import matplotlib.pyplot as plt

plt.title("Loss Trend")
plt.xlabel("epochs")
plt.ylabel("loss")
plt.grid()

plt.plot(train_loss_list, label="train loss")
plt.plot(val_loss_list, label="val loss")
plt.legend(loc='best')

plt.show()
plt.title("Accuracy Trend")
plt.xlabel("epochs")
plt.ylabel("loss")
plt.grid()

plt.plot(train_accuracy_list, label="train accuracy")
plt.plot(val_accuracy_list, label="val accuracy")
plt.legend(loc='best')

plt.show()

4. 코드 상세 분석

이 코드는 데이터 전처리 -> 모델 정의 -> 학습(Train) -> 검증(Validation) -> 테스트(Test)로 이어지는 전형적인 딥러닝 워크플로우를 따르고 있습니다.

① 데이터셋 분할과 로더 (Data Strategy)

train_dataset_size = int(len(train_dataset) * 0.85)
validation_dataset_size = int(len(train_dataset) * 0.15)
train_dataset, validation_dataset = random_split(train_dataset, [train_dataset_size, validation_dataset_size])
  • random_split: 60,000개의 데이터를 85:15 비율로 나눕니다.
    • Train(51,000): 모델이 가중치를 업데이트하는 '공부용'입니다.
    • Validation(9,000): 학습 도중 과적합 여부를 판단하는 '모의고사'입니다.
  • DataLoader: 데이터를 BATCH_SIZE=32만큼 묶어 모델에 공급합니다. 한 번에 모든 데이터를 넣지 않고 조금씩 나누어 넣음으로써 메모리 효율을 높이고, 경사하강법의 성능을 개선합니다.

② 신경망 모델 설계 (Model Architecture)

class MyDeepLearningModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()    # 1. 2D -> 1D 변환
        self.fc1 = nn.Linear(784, 256) # 2. 첫 번째 은닉층
        self.relu = nn.ReLU()          # 3. 활성화 함수
        self.dropout = nn.Dropout(0.3) # 4. 과적합 방지
        self.fc2 = nn.Linear(256, 10)  # 5. 출력층
  • nn.Flatten(): 28 x 28 크기의 이미지를 784개의 연속된 숫자로 펼칩니다.
  • nn.Linear(784, 256): 784개의 입력을 받아 256개의 특징을 추출합니다. 이 과정에서 784 x 256개의 가중치(W)가 생성됩니다.
  • nn.ReLU(): 은닉층 뒤에 배치되어 모델에 비선형성을 부여합니다. 복잡한 형태의 숫자를 인식할 수 있게 해주는 핵심 엔진입니다.
  • nn.Dropout(0.3): 학습 시 뉴런의 30%를 무작위로 끕니다. 모델이 특정 뉴런에만 의존하지 않고 전체적으로 골고루 학습하게 하여 일반화 성능을 높입니다.
  • nn.Linear(256, 10): 마지막으로 10개의 숫자(0~9)에 대한 점수를 산출합니다.

③ 손실 함수와 최적화 도구 (Loss & Optimizer)

loss_function = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=1e-2)
  • CrossEntropyLoss: 모델의 예측값(Softmax 확률값)과 실제 정답(Label) 사이의 차이를 계산합니다. 다중 분류 문제에서 가장 표준적으로 사용됩니다.
  • SGD: 계산된 오차(Loss)를 바탕으로 모델의 파라미터를 어느 방향으로 수정할지 결정합니다. lr=1e-2(0.01)은 한 번에 수정할 보폭의 크기입니다.

④ 학습 루프: model_train 함수의 핵심

model.train() # 학습 모드 활성화
for images, labels in dataloader:
    x_train = images.view(-1, 28*28) # 데이터 형태 맞추기
    outputs = model(x_train)         # 순전파 (Forward)
    loss = loss_function(outputs, y_train) # 오차 계산

    optimizer.zero_grad() # 이전 기울기 초기화
    loss.backward()       # 역전파 (Backpropagation)
    optimizer.step()      # 가중치 업데이트 (Step)
  • optimizer.zero_grad(): 파이토치는 가중치를 업데이트할 때 기울기를 누적하기 때문에, 매 배치마다 초기화해 주어야 정확한 학습이 가능합니다.
  • loss.backward(): 이 미분 과정을 통해 각 층의 가중치가 오차에 얼마나 기여했는지 계산합니다.

⑤ 검증 및 성능 평가 (Evaluation)

model.eval() # 추론 모드 활성화
with torch.no_grad(): # 미분 중단
    # ... 오차 및 정확도 계산 ...
  • model.eval(): 학습 시 사용했던 Dropout을 끕니다. 평가할 때는 모든 뉴런을 다 사용해야 하기 때문입니다.
  • torch.no_grad(): 평가 시에는 가중치를 업데이트할 필요가 없으므로 연산 속도를 높이고 메모리를 절약하기 위해 미분 계산을 중단합니다.
  • torch.argmax(outputs, 1): 10개의 출력값 중 가장 점수가 높은 인덱스를 찾아 모델의 최종 예측값으로 결정합니다.

5.  학습 결과 및 그래프 해석

 

최종적으로 Test Accuracy 96.5%라는 결과를 얻었습니다.

  • Loss Trend: Train Loss와 Val Loss가 모두 안정적으로 우하향합니다. 만약 Val Loss만 튀어 오른다면 과적합(Overfitting)이 발생한 것인데, 이 모델은 Dropout 덕분에 매우 안정적입니다.
  • Accuracy Trend: 학습 초기에 정확도가 급격히 상승하다가 점차 수렴하는 모습을 보입니다. 20 Epoch만으로도 충분히 높은 성능에 도달했음을 확인할 수 있습니다.

+ Recent posts