1. 이론적 배경: 다층 퍼셉트론(MLP)과 이미지 데이터
① MNIST 이미지 데이터의 특징
- 컴퓨터에게 이미지는 그저 '숫자들이 적힌 바둑판'일 뿐입니다. MNIST 데이터는 가로 28칸, 세로 28칸($28 \times 28$)으로 이루어진 흑백 이미지입니다.
- 즉, 사진 1장에는 총 $784$개의 픽셀(점)이 있고, 각 픽셀은 0(검은색)부터 255(흰색) 사이의 밝기 값을 가집니다.
② 왜 층(Layer)을 여러 개 쌓을까요? (Deep Learning)
- 이전처럼 입력층과 출력층만 딱 하나씩 있는 모델(단층)은 복잡한 문제를 풀지 못합니다.
- 층을 깊게 쌓으면 모델이 점진적으로 똑똑해집니다.
- 1번째 층: "어? 여기 직선이 있네, 저긴 곡선이 있네." (단순 선/색상 파악)
- 2번째 층: "직선이랑 곡선이 합쳐지니 동그라미 모양이 되네!" (부분적인 형태 파악)
- 마지막 층: "동그라미 두 개가 위아래로 붙어있으니 이건 숫자 '8'이구나!" (최종 판단)
2. 코드 분석 1: 데이터 준비 (DataLoader & ToTensor)
trainset = datasets.MNIST(root=dataset_dir, download=True, transform=transforms.ToTensor())
- transforms.ToTensor()의 2가지 마법:
- 일반적인 이미지 파일 포맷을 파이토치가 계산할 수 있는 Tensor 객체로 바꿉니다.
- 0~255 사이의 픽셀 값을 255로 나누어 0.0 ~ 1.0 사이의 실수로 정규화(Scaling)합니다. 이렇게 숫자의 덩치를 줄여주면 모델이 훨씬 빠르고 정확하게 학습합니다.
train_loader = DataLoader(trainset, batch_size=batch_size, shuffle=True)
- batch_size=256: 6만 장의 사진을 한 번에 학습하면 컴퓨터 메모리가 터집니다. 그래서 "한 번에 256장씩만 묶어서 던져줘!"라고 컨베이어 벨트를 설정하는 것입니다.
3. 코드 분석 2: 모델 조립하기 (Subclass 방식)
가장 핵심이 되는 신경망 모델 설계도(MNISTModel)입니다.
def __init__(self):
self.lr1 = nn.Linear(784, 128)
self.lr2 = nn.Linear(128, 64)
self.lr3 = nn.Linear(64, 32)
self.lr4 = nn.Linear(32, 10) # 출력층: 10개 (숫자 0~9)
- $784$가 들어가는 이유: $28 \times 28$ 사이즈의 이미지를 한 줄로 길게 펴면 784개의 픽셀이 되기 때문입니다.
- 처음 784개의 데이터가 128개 $\rightarrow$ 64개 $\rightarrow$ 32개로 점점 압축되며 핵심 정보만 남기고, 마지막에 0부터 9까지 10개의 확률표로 변환됩니다.
def forward(self, X):
X = torch.flatten(X, start_dim=1) # 2D 이미지를 1D로 쫙 펴기
X = nn.ReLU()(self.lr1(X)) # 비선형 활성화 함수
...
- torch.flatten: 바둑판(2D) 모양의 이미지를 nn.Linear에 넣기 위해 784칸짜리 긴 막대기(1D) 모양으로 납작하게 펴주는(Flatten) 작업입니다.
- nn.ReLU() (매주 중요!): 층과 층 사이에 들어가는 필터입니다. 음수 값은 0으로 무시하고 양수만 통과시킵니다. 이 ReLU가 있어야만 신경망이 단순한 직선을 넘어 구불구불한 복잡한 패턴(비선형)을 학습할 수 있습니다.
4. 코드 분석 3: 손실 함수와 옵티마이저
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
- CrossEntropyLoss: 다중 분류(여러 개의 보기 중 하나를 고르는 객관식 문제)에서 오차를 계산하는 전용 채점기입니다. 이 함수 내부에는 각 숫자가 될 확률을 백분율(%)로 예쁘게 바꿔주는 Softmax 기능이 내장되어 있습니다.
- Adam: 기존에 쓰던 SGD보다 훨씬 똑똑하게 길을 찾는 최신 네비게이션(최적화 알고리즘)입니다. 방향과 보폭을 스스로 조절해가며 정답을 빠르게 찾아냅니다.
이후 학습 루프(Train/Validation) 코드는 이전에 배우셨던 5단계(추론 $\rightarrow$ 오차 계산 $\rightarrow$ 미분 $\rightarrow$ 가중치 업데이트 $\rightarrow$ 초기화)가 그대로 적용된 것입니다. 차이점이 있다면, 실전에서는 모델의 상태를 학습 모드(model.train())와 평가 모드(model.eval())로 스위치 켜듯 바꿔주어야 한다는 점입니다.
1단계: 하드웨어 설정과 데이터 준비 (Dataloader)
1. 하드웨어(Device) 설정
device = "cuda" if torch.cuda.is_available() else "cpu"
딥러닝은 엄청난 양의 행렬 곱셈을 해야 하므로 CPU보다 GPU(그래픽카드)가 수십 배 빠릅니다. 이 코드는 "내 컴퓨터에 NVIDIA GPU(cuda)나 맥북 칩(mps)이 있으면 그걸 쓰고, 없으면 그냥 CPU로 돌려라!"라고 컴퓨터에게 똑똑하게 작업장(메모리)을 배정해 주는 코드입니다.
2. 데이터 가져오기 (datasets & ToTensor)
trainset = datasets.MNIST(root=dataset_dir, download=True, transform=transforms.ToTensor())
- MNIST: 0부터 9까지 흑백 손글씨 이미지 7만 장(학습용 6만, 테스트용 1만)이 모여있는 아주 유명한 데이터셋입니다.
- transforms.ToTensor() (핵심!): 우리가 아는 이미지 파일(JPG, PNG)은 컴퓨터가 바로 계산할 수 없습니다. 이 도구는 1) 이미지를 파이토치용 Tensor로 바꾸고, 2) 0~255 사이인 픽셀 색상값을 0.0 ~ 1.0 사이의 실수로 압축(정규화)해 줍니다. 모델이 훨씬 학습을 잘하게 도와주는 필수 전처리입니다.
3. 컨베이어 벨트 만들기 (DataLoader)
train_loader = DataLoader(trainset, batch_size=batch_size, shuffle=True)
6만 장의 시험지를 모델에게 한 번에 던져주면 컴퓨터가 과로로 터져버립니다.
- batch_size=256: "한 번에 256장씩만 묶어서 던져줘!"라는 뜻입니다.
- shuffle=True: "학습할 때마다 시험지 순서를 마구 섞어줘!" (답을 순서대로 외우는 꼼수를 방지합니다)
2단계: 신경망 모델 조립하기 (Subclass 방식)
이전 선형 회귀에서는 nn.Linear 딱 하나만 썼지만, 이번엔 이미지를 분석해야 하므로 층(Layer)을 여러 개 깊게 쌓습니다. (그래서 '딥' 러닝입니다!)
class MNISTModel(nn.Module):
def __init__(self):
super().__init__()
self.lr1 = nn.Linear(784, 128) # 28x28=784 픽셀을 128개로 압축
self.lr2 = nn.Linear(128, 64)
self.lr3 = nn.Linear(64, 32)
self.lr4 = nn.Linear(32, 10) # 마지막은 반드시 10개! (0~9 숫자)
- __init__ (부품 창고): 모델을 구성할 부품(nn.Linear)들을 미리 사서 창고에 넣어둡니다. 마지막 출력이 10인 이유는 정답(클래스)이 10개이기 때문입니다.
def forward(self, X):
X = torch.flatten(X, start_dim=1)
X = nn.ReLU()(self.lr1(X))
...
output = self.lr4(X)
return output
- forward (조립 라인): 창고에 있는 부품을 꺼내 데이터(X)가 지나갈 길을 만들어줍니다.
- torch.flatten: 이미지는 가로x세로(28x28)인 2차원 네모 모양인데, nn.Linear 기계는 일렬로 선 1차원 데이터만 받을 수 있습니다. 그래서 네모난 이미지를 784개의 픽셀을 가진 긴 막대기 형태로 쫙 펴주는 작업입니다.
- nn.ReLU(): 활성화 함수라고 부릅니다. 데이터가 층을 통과할 때 "음수 값이 나오면 다 0으로 무시하고, 양수만 그대로 통과시켜!"라는 똑똑한 필터입니다. 이 녀석이 들어가야 모델이 복잡한 패턴(곡선 등)을 학습할 수 있습니다.
3단계: 훈련 세팅 (Loss와 Optimizer)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
- CrossEntropyLoss: 예전에는 오차를 구할 때 평균 제곱 오차(MSE)를 썼죠? 하지만 지금처럼 "이 사진이 0부터 9중에 뭐게?"라고 객관식(다중 분류) 문제를 풀 때는 반드시 이 함수를 써야 합니다.
- Adam: 최적화 반장님을 SGD에서 Adam으로 업그레이드했습니다. 훨씬 더 빠르고 똑똑하게 가중치를 찾아가는 일종의 '네비게이션' 같은 알고리즘입니다.
4단계: 대망의 학습 루프 (Train & Validation)
코드가 가장 길지만, 자세히 보면 완전히 똑같은 패턴이 두 번 반복됩니다. 바로 학습(Train)과 모의고사 검증(Validation)입니다.
① 학습 모드 (model.train())
for X_train, y_train in train_loader:
X_train, y_train = X_train.to(device), y_train.to(device) # GPU로 올리기
pred = model(X_train) # 1. 추론
loss = loss_fn(pred, y_train) # 2. 오차 계산
loss.backward() # 3. 미분 계산
optimizer.step() # 4. 가중치 업데이트
optimizer.zero_grad() # 5. 미분값 초기화
이전에 배우셨던 '딥러닝의 심장 5단계'가 그대로 들어있죠? DataLoader가 주는 256장(1 batch)의 이미지를 받아서, 맞히고, 혼나고(loss), 고치는(step) 과정을 끝없이 반복합니다.
② 검증 모드 (model.eval() & torch.no_grad())
with torch.no_grad():
for X_valid, y_valid in test_loader:
...
pred_valid_class = pred_valid.argmax(dim=-1)
한 번 학습이 끝나면, 모델이 진짜 똑똑해졌는지 학습하지 않은 새로운 데이터(test_loader)로 모의고사를 봅니다.
- model.eval(): "이제 시험 볼 거니까 학습할 때 쓰던 컨닝페이퍼 덮어!" 하고 평가 모드로 바꿉니다.
- torch.no_grad(): 미분 엔진 끄기 (학습 안 할 거니까 메모리 절약)
- argmax(dim=-1): 모델은 0~9번 숫자에 대해 각각의 확률을 내뱉습니다. 그중 "가장 확률이 높은(arg MAX) 번호표" 하나를 최종 정답으로 고르는 함수입니다.
5단계: 모델 저장/불러오기 및 새로운 이미지 예측
학습이 다 끝났는데 컴퓨터를 끄면 그동안 똑똑해진 지식(가중치)이 다 날아가겠죠?
- torch.save(model, "mnist_model.pt"): 똑똑해진 뇌(모델)를 파일로 내 PC에 영구 저장합니다.
- torch.load(...): 나중에 다시 파이썬을 켜서 그 뇌를 그대로 불러옵니다.
마지막의 load_data 함수는 정말 실무적인 코드입니다! 인터넷에서 다운받거나 직접 그림판으로 그린 숫자 사진(.jpg, .png)을 파이썬으로 불러와서,
- 흑백으로 바꾸고 (.convert('L'))
- 모델이 배운 사이즈인 28x28로 줄이고 (.resize())
- 텐서로 변환해서 (ToTensor()) 불러온 모델에게 "이거 무슨 숫자게?" 하고 물어보는(Predict) 최종 응용 단계입니다.
'두두 IT > 딥러닝' 카테고리의 다른 글
| 03. 선형회귀 pytorch_linear_regression (0) | 2026.05.26 |
|---|---|
| 02. tensor 다루기 (0) | 2026.05.26 |
| 01. 딥러닝 개요 (Deep Learning Overview) (1) | 2026.05.22 |