예제 
보스턴 집값 예측 예제에서는 출력이 1개(집값)였습니다. 하지만 집값과 함께 지역, 집주인 나이처럼 여러 값을 동시에 예측하고 싶다면, 같은 데이터로 모델을 여러 개 학습할 필요는 없습니다. 출력값을 억지로 하나로 합치기보다, 출력층을 여러 개로 설계해 집값·지역·나이를 각각 출력하게 하면 모델 하나로 다중 예측을 할 수 있습니다.
또한 회귀와 분류는 값을 직접 예측하느냐, 어떤 범주에 들어가느냐의 문제이므로 서로 상관없어 보이지만, 사실 두 문제는 매우 비슷합니다. 신경망의 출력을 그대로 사용하면 회귀가 되고, 출력을 확률 분포로 바꿔 주면 분류 문제가 됩니다.
회귀 와 분류 의 차이 신경망 의 출력 을 그대로 사용 집값 지역 : 나이 신경망 의 출력 을 확률 분포 로 바꿔 ( 소프트 맥스 ) 사용 집값 지역 나이 소프트 맥스 회귀 모델 과 분류 모델 의 차이 를 나타낸 그림
회귀 모델과 분류 모델은 신경망 구조가 완전히 동일하지만, 분류 모델은 최종적으로 출력을 소프트맥스 함수를 통해 확률 분포로 변환합니다. 그대로 출력값을 이용해도 상관없지만, 분류 문제에서 출력을 확률 분포로 바꿔 주는 까닭은 출력값의 범위를 0과 1 사이로 제한해야 해석이 용이하기 때문입니다. 이번 절에서는 여러 출력을 갖는 신경망을 이용해 다중 분류 문제를 알아보겠습니다. 0부터 9까지의 10개 숫자를 손글씨로 표현한 손글씨 데이터 셋을 사용해 실습하겠습니다.
그럼 손글씨 데이터 셋을 사용해서 다중 출력을 연습해보겠습니다. MNIST는 0부터 9까지 10가지의 손글씨 이미지로 구성된 데이터 셋입니다.
실습: MNIST 다중 분류 전체 파이프라인 (PyTorch)
아래 코드는 MNIST(손글씨 숫자 0~9)를 이용해 간단한 다층 퍼셉트론(MLP)을 학습하고, 저장/불러오기, 정확도 평가, 샘플 시각화까지 한 번에 진행하는 예제입니다.
이 코드에서 nn.CrossEntropyLoss()를 사용하므로,
•
모델의 마지막 층은 Softmax를 직접 쓰지 않고 nn.Linear(..., 10)까지만 둡니다.
•
CrossEntropyLoss 내부에서 (LogSoftmax + NLLLoss) 처리가 포함되어 있어 로짓(logits)을 그대로 넣는 것이 정석입니다.
1) 데이터 살펴보기
MNIST는 다음 형태로 들어옵니다.
•
이미지 1장은 (1, 28, 28) 크기의 텐서(흑백 1채널)
•
픽셀 값은 ToTensor()로 인해 0~1 범위의 float
•
라벨은 0~9 정수
간단히 데이터 개수와 샘플 이미지를 확인할 수 있습니다.
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor
training_data = MNIST(root="./", train=True, download=True, transform=ToTensor())
test_data = MNIST(root="./", train=False, download=True, transform=ToTensor())
print(f"학습 데이터: {len(training_data)}개, 평가 데이터: {len(test_data)}개")
# 샘플 하나 확인
img, label = training_data[0]
print(img.shape, img.min().item(), img.max().item(), label) # torch.Size([1,28,28]), 0~1, 라벨
Python
복사
2) 데이터 불러오기 (Dataset → DataLoader)
딥러닝 학습은 보통 데이터를 배치(batch) 단위로 가져오므로 DataLoader를 사용합니다.
•
학습 데이터는 shuffle=True로 섞어 주는 것이 일반적입니다.
•
평가 데이터는 재현성을 위해 shuffle=False로 둡니다.
from torch.utils.data import DataLoader
train_loader = DataLoader(training_data, batch_size=32, shuffle=True)
test_loader = DataLoader(test_data, batch_size=32, shuffle=False)
Python
복사
3) 모델 정의 및 학습하기
데이터를 다루는 작업이 끝났으니, 이제는 이미지를 분류할 모델을 만들 차례입니다. MNIST는 데이터(이미지)가 많고 에포크를 여러 번 돌리기 때문에 가능하면 GPU(CUDA) 를 활용합니다.
학습 루프는 다음 흐름으로 이해하면 됩니다.
•
모델 정의
•
데이터 호출
•
손실 계산
•
오차 역전파 및 최적화
•
원하는 만큼 반복
•
학습 종료
import torch
device = "cuda" if torch.cuda.is_available() else "cpu"
print("device:", device)
Python
복사
•
torch.cuda.is_available()
◦
GPU를 사용할 수 있으면 True
◦
없으면 False
•
이후 모든 텐서(입력, 라벨, 모델 파라미터)는 같은 device에 있어야 연산이 됩니다.
이미지는 보통 2차원(높이×너비) 입니다.
•
예: MNIST 한 장은 (1, 28, 28)
하지만 nn.Linear 기반 MLP(다층 퍼셉트론)는 입력을 (특징 개수)로 쭉 나열한 1차원 벡터로 받습니다.
•
(1, 28, 28) → (784)
배치까지 포함하면 다음처럼 바뀝니다.
•
(B, 1, 28, 28) → (B, 784)
# (B, 1, 28, 28) -> (B, 784)
data = torch.reshape(data, (-1, 784))
Python
복사
•
여기서 -1은 “배치 크기는 자동으로 맞춰라”는 의미입니다.
◦
예: 배치가 64장이면 (64, 1, 28, 28) → (64, 784)
import torch.nn as nn
model = nn.Sequential(
nn.Linear(784, 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, 10) # 0~9, 10개 클래스의 logits
)
model.to(device) # 모델 파라미터를 device(GPU/CPU)로 이동
Python
복사
•
마지막 출력 10은 클래스 개수(0~9)입니다.
•
출력은 Softmax가 적용되기 전의 값인 logits 입니다.
회귀는 MSE를 많이 쓰고, 분류는 Cross Entropy(CE) 를 많이 사용합니다.
•
MSE: 두 값의 거리(차이)를 기반으로 오차를 정의
•
CE: 정답 분포(원-핫)와 예측 분포가 얼마나 다른지(정보량 관점)로 오차를 정의
from torch.optim import Adam
lr = 1e-3
optim = Adam(model.parameters(), lr=lr)
criterion = nn.CrossEntropyLoss()
Python
복사
nn.CrossEntropyLoss()는 내부에 LogSoftmax가 포함되어 있습니다.
그래서 모델 마지막에 Softmax를 따로 붙이지 않고, nn.Linear(..., 10) logits을 바로 넣습니다.
for epoch in range(20):
model.train() # 학습 모드
for data, label in train_loader:
optim.zero_grad() # 이전 step의 gradient 초기화
# 입력 모양 변환 + device 통일
data = torch.reshape(data, (-1, 784)).to(device)
label = label.to(device)
preds = model(data) # (B, 10)
loss = criterion(preds, label) # label은 (B,) 정수 클래스 인덱스
loss.backward() # 오차 역전파
optim.step() # 파라미터 업데이트
print(f"epoch {epoch+1:2d} | loss: {loss.item():.4f}")
Python
복사
새로 등장한(자주 헷갈리는) 함수/메서드 정리
학습이 끝나면 가중치(파라미터)를 파일로 저장해 둡니다.
torch.save(model.state_dict(), "MNIST.pth")
Python
복사
•
.pt와 .pth 모두 흔히 쓰입니다.
•
state_dict()는 모델의 가중치를 딕셔너리 형태로 반환합니다.
4) 모델 성능 평가하기
평가용 데이터셋(test set)을 이용해 분류 정확도(Accuracy)를 계산해 보겠습니다.
저장한 가중치를 불러올 때 map_location으로 어느 장치로 올릴지 정할 수 있습니다.
•
GPU가 있으면 GPU로
•
없으면 CPU로
model.load_state_dict(
torch.load("MNIST.pth", map_location=device, weights_only=True)
)
Python
복사
•
model.eval()
◦
평가 모드로 전환합니다.
◦
(이번 MLP엔 드롭아웃/배치정규화가 없지만) 습관적으로 호출하는 것이 좋습니다.
•
torch.no_grad()
◦
평가에서는 가중치를 업데이트하지 않으므로 기울기가 필요 없습니다.
◦
메모리 사용량과 계산량이 줄어듭니다.
모델 출력 output은 (B, 10) 형태입니다.
•
B: 배치 크기
•
10: 클래스 점수(logits)
output.data.max(1)은 클래스 차원(dim=1) 에서 최댓값을 찾습니다.
•
반환값은 (최댓값, 최댓값의 인덱스) 두 개가 함께 나옵니다.
•
그래서 max(1)[1]을 하면 인덱스(=예측 클래스) 만 가져옵니다.
output = model(data) # (B, 10)
preds = output.data.max(1)[1] # (B,) 각 샘플의 예측 클래스
Python
복사
preds.eq(label)은 원소별 비교를 해서
•
같으면 1(True)
•
다르면 0(False)
에 해당하는 텐서를 만듭니다. 이후
•
sum()으로 맞춘 개수를 더하고
•
item()으로 파이썬 숫자로 꺼냅니다.
corr = preds.eq(label).sum().item()
Python
복사
model.eval()
num_corr = 0
with torch.no_grad():
for data, label in test_loader:
data = torch.reshape(data, (-1, 784)).to(device)
label = label.to(device)
output = model(data)
preds = output.data.max(1)[1]
num_corr += preds.eq(label).sum().item()
acc = num_corr / len(test_data)
print(f"Accuracy: {acc:.4f}")
Python
복사
정확도 해석(경험적 기준)
•
MNIST에서 이 정도 MLP는 약 97% 전후 정확도가 흔합니다.
•
과제/실습 수준에서는 92% 이상이면 무난하게 학습이 된 편으로 봅니다.
(부록) 샘플 이미지 저장
학습 데이터에서 9장을 뽑아 저장합니다.
import matplotlib.pyplot as plt
for i in range(9):
plt.subplot(3, 3, i + 1)
plt.imshow(training_data.data[i], cmap="gray")
plt.axis("off")
plt.suptitle("MNIST Sample Images")
plt.tight_layout()
plt.savefig("mnist_samples.png")
print("샘플 이미지 -> mnist_samples.png 저장 완료")
Python
복사
정리
•
학습 단계에서는 CrossEntropyLoss로 손실을 줄이도록 가중치를 업데이트합니다.
•
평가 단계에서는 argmax로 클래스 예측을 만든 뒤 정답과 비교해 정확도를 계산합니다.
학습 마무리
파이토치를 이용해 간단한 인공 신경망을 만들어 보았습니다. 직접적인 값을 예측하면 회귀 문제, 어느 레이블(클래스)에 속하는지의 확률 분포를 예측하면 분류 문제입니다. 아래의 딥러닝 학습 흐름을 다시 한 번 정리해 봅시다.
딥러닝 학습의 기본 흐름
1.
모델의 정의
2.
최적화 함수 정의
3.
모델의 예측과 실제 값의 차이 계산 (손실 계산)
4.
오차 역전파
5.
최적화 진행
되짚어보기
•
랜덤 계수와 3차 다항식 계수를 학습해서 사인 함수를 예측했습니다.
•
랜덤 계수를 사용하면 사인 함수와 전혀 닮지 않지만, 학습된 3차 다항식 계수는 사인 함수와 비슷하도록 학습되어 좋은 결과가 나옵니다.
•
보스턴 집값 데이터를 간단한 MLP 모델로 학습했습니다.
•
데이터가 순차적으로 존재하므로 신경망이므로 nn.Sequential 클래스로 모델을 구성했습니다.
•
손실 함수로는 실제 집값과 모델의 예측값의 차이를 나타내는 오차를 사용했습니다.
•
실제 집값과 크게 차이가 나지 않는 수준으로 학습이 진행되었습니다.
•
손글씨 데이터를 MLP로 학습했습니다.
•
MLP는 1차원 벡터만 입력으로 받으므로 2차원 이미지를 평탄화(Flatten) 해서 신경망의 입력으로 사용했습니다.
•
파이토치의 Dataset 객체와 DataLoader를 사용해 데이터를 효율적으로 불러왔습니다.
•
손글씨 학습은 정답과 예측 간의 크로스 엔트로피 오차를 줄이도록 학습합니다.
•
정확도는 약 97% 정도로, 대부분의 이미지를 올바르게 분류할 수 있었습니다.







