파이토치 학습 루프(PyTorch Training Loop) 완벽 가이드
파이토치(PyTorch) 학습 루프를 구성하는 것은 간단해 보이지만, 코드의 순서가 조금만 어긋나도 메모리 초과(OOM)나 수렴 실패 등을 겪게 됩니다. 이 글은 올바른 학습 루프의 구조를 소개하고, 흔히 저지르는 치명적인 순서 실수들을 디버깅 방법과 함께 상세히 설명합니다.
← 에세이 ← 에세이
파이토치(PyTorch) 학습 루프를 구성하는 것은 꽤 간단하지만, 모든 것을 올바른 위치와 순서에 배치하는 것은 생각보다 까다롭고 오류에 취약하게 느껴질 수 있습니다. 수많은 움직이는 부품들이 있으며, 가장 기본적인 오류들을 수정한 후에는 다른 실수들을 발견하기가 꽤 어려울 수 있습니다. 코드 한 줄이 잘못된 위치에 들어가면 학습이 수렴하지 않거나, 잘못된 결과가 나오거나, 과도한 메모리를 소비하게 됩니다. 아래 섹션에서는 각 작업을 순서대로 살펴보며, 각 부분을 정확히 어떻게 작성해야 하는지, 그리고 주의해야 할 모든 일반적인 실수들을 설명하겠습니다. 분산 학습, FSDP, 멀티 GPU 설정 등은 여기서 다루지 않으며, 향후 에세이에서 다루겠습니다. (위의 애니메이션은 합성 데이터(synthetic data)로 루프를 실행하고 각 에포크(epoch)마다 결정 경계(decision boundary)를 캡처하여 만들었습니다.)
전체 학습 루프
먼저 전체 학습 루프를 살펴보겠습니다. 아직 이해하거나 외울 필요는 없으며, 그저 전체적인 구조를 느껴보시면 됩니다.
1 import torch
2 import torch . nn as nn
3 from torch . utils . data import DataLoader , TensorDataset
4
5 # --- 데이터 ---
6 dataset = TensorDataset ( X_train , y_train )
7 loader = DataLoader ( dataset , batch_size = 64 , shuffle = True )
8
9 # --- 모델, 손실 함수, 옵티마이저 ---
10 model = MLP ( in_features = 2 , hidden = 128 , out_features = 3 )
11 criterion = nn . CrossEntropyLoss ( )
12 optimiser = torch . optim . Adam ( model . parameters ( ) , lr = 1e-3 )
13 scheduler = torch . optim . lr_scheduler . CosineAnnealingLR ( optimiser , T_max = 100 )
14
15 # --- 루프 ---
16 for epoch in range ( 100 ) :
17 model . train ( )
18 for X_batch , y_batch in loader :
19 optimiser . zero_grad ( )
20 logits = model ( X_batch )
21 loss = criterion ( logits , y_batch )
22 loss . backward ( )
23 torch . nn . utils . clip_grad_norm_ ( model . parameters ( ) , max_norm = 1.0 )
24 optimiser . step ( )
25 scheduler . step ( )
26
27 model . eval ( )
28 with torch . no_grad ( ) :
29 val_logits = model ( X_val )
30 val_loss = criterion ( val_logits , y_val )
이제 각 라인을 살펴보며 어떤 역할을 하는지, 그리고 코드가 어떻게 망가질 수 있는지 이해해 보겠습니다. 몇 가지 흔한 실수부터 시작하겠습니다.
요약 (TL;DR): 순서가 절대적으로 중요한 곳
다음은 배치 순서를 약간 실수하여 학습 루프를 망칠 수 있는 가장 흔한 실패 사례입니다. 이를 암기해야 하는 이유는 이들 중 어느 것도 예외(exception)를 발생시키지 않기 때문입니다. 시간이 지나면서 학습 실행 중 어떤 종류의 오류를 찾아야 하는지 감을 잡게 되겠지만, 처음 몇 번은 이 요약본이 큰 도움이 될 것입니다.
| 코드 라인 | 잘못된 위치 | 문제점 (무엇이 망가지는가) |
|---|---|---|
model.to(device) |
optimiser = ... 이후 |
데이터 타입 변환(예: model.half())이 결합될 때 nn.Module.to()는 새로운 nn.Parameter 객체를 할당합니다. 옵티마이저는 버려진 원본을 참조하여 그것들에만 업데이트를 적용하게 됩니다. |
optimiser.zero_grad() |
loss.backward() 이후 |
여러 배치의 기울기(gradient)가 누적됩니다. 업데이트 시 현재 배치만 단독으로 쓰이는 게 아니라 누적된 합산 값이 사용됩니다. |
clip_grad_norm_() |
loss.backward() 이전 |
.grad가 비어 있습니다. 이 호출은 아무 일도 일어나지 않는(no-op) 상태가 됩니다. |
clip_grad_norm_() |
optimiser.step() 이후 |
이미 적용된 기울기를 클리핑합니다. 아무런 효과가 없습니다. |
scheduler.step() |
배치 루프 내부 | 에포크당 한 번이 아니라 len(loader) 횟수만큼 학습률(LR)이 감소합니다. |
model.eval() 이후 model.train() 생략 |
— | 드롭아웃(Dropout)이 비활성화되고 배치 정규화(BatchNorm)가 고정됩니다. 오류 없이 평가(eval) 모드에서 학습이 진행됩니다. |
검증 중 torch.no_grad() 생략 |
— | 모든 검증 배치에서 Autograd(자동 미분) 그래프가 빌드됩니다. OOM(메모리 부족)이 발생할 때까지 메모리가 계속 증가합니다. |
loss.item() 대신 loss 로깅 |
— | 로깅 호출 기간 동안 계산 그래프가 메모리에 고정되어 해제되지 않습니다. |
이제 이 각각에 대해 자세히 살펴보겠습니다.
데이터 파이프라인 관행
커스텀 파이토치 데이터셋 (쉬움 Q 344)
__len__ 및 __getitem__을 구현하여 커스텀 파이토치 데이터셋을 구현해 보세요.
파이토치의 데이터 파이프라인에는 데이터셋(Dataset)과 데이터로더(DataLoader)라는 두 부분이 있습니다. 데이터셋은 단순히 __len__(데이터셋에 몇 개의 요소가 있는지 반환)과 __getitem__(놀랍지 않게도 요소를 가져옵니다)을 구현하는 파이썬 객체일 뿐입니다.