2장. 신경망과 딥러닝의 기본 원리
학습 루프: 에포크와 배치
문제집 한 권으로 시험을 준비한다고 해 봅시다. 하루에 몇 쪽씩 풀고 채점해 틀린 것을 고칩니다. 한 권을 다 풀면 처음부터 다시 풉니다. 하루 분량이 배치, 한 권을 다 도는 것이 에포크입니다.
PyTorch 의 학습은 이 다섯 줄을 배치마다 되풀이하는 것이 전부입니다. 지난 기울기 지우기, 예측 내기, 손실 재기, 기울기 구하기, 가중치 고치기입니다. 큰 그림의 "해 보기, 채점하기, 책임 나누기, 조금 고치기"가 2번부터 5번입니다.
에포크, 배치, 반복
| 말 | 뜻 | 실습 4에서 |
|---|---|---|
| 배치 크기 | 한 번 고칠 때 보는 건수 | 32건 |
| 반복(스텝) | 배치 하나로 한 번 고치는 것 | 학습용 584건 ÷ 32 → 에포크마다 19번 |
| 에포크 | 학습용 전체를 한 번 다 도는 것 | 200번 |
전체를 한꺼번에 보지 않고 배치로 나누는 까닭은 둘입니다. 큰 데이터는 한꺼번에 메모리에 올릴 수 없고, 조금씩 자주 고치는 편이 빨리 좋아지기 때문입니다.
학습 루프의 뼈대
아래는 실습 4에서 쓸 루프의 뼈대입니다. 모양만 보는 코드라서 데이터는 난수로 두었습니다.
import torch
from torch import nn
torch.manual_seed(0) # 결과를 같게 하려고 씨앗을 고정한다
X = torch.randn(64, 7) # 설명용 입력 64건
y = torch.randn(64, 1) # 설명용 정답 64건
model = nn.Linear(7, 1) # 가장 작은 신경망: 뉴런 하나
loss_fn = nn.MSELoss()
opt = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(3): # 에포크: 전체를 세 번 돈다
for i in range(0, 64, 32): # 배치: 32건씩 끊어서
xb, yb = X[i:i + 32], y[i:i + 32]
opt.zero_grad() # 1) 지난 기울기 지우기
pred = model(xb) # 2) 순전파
loss = loss_fn(pred, yb) # 3) 손실
loss.backward() # 4) 역전파
opt.step() # 5) 가중치 고치기
print(epoch, round(loss.item(), 4))에포크가 지날수록 손실이 조금씩 줄어듭니다. 실행하면 에포크 0, 1, 2 의 손실이 1.1541, 1.1331, 1.1132 로 나옵니다. 데이터가 난수라 크게 줄지는 않습니다. 실제 데이터로 하는 것은 실습 4에서 봅니다.
코드에 나오는 함수
문제집 비유로 함수마다 하는 일을 정리합니다. 여기서 모델이 공부하는 학생, 데이터가 문제집, 옵티마이저가 틀린 것을 고쳐 주는 선생님입니다.
| 함수 | 하는 일 | 문제집으로 치면 |
|---|---|---|
torch.manual_seed(0) | 난수의 씨앗을 고정한다. 다시 돌려도 같은 난수가 나온다 | 매번 같은 문제집을 고른다 |
torch.randn(64, 7) | 평균 0, 표준편차 1 근처의 난수로 (64, 7) 텐서를 만든다. 여기서는 연습용 가짜 데이터다 | 연습용 문제 64개를 만든다 |
nn.Linear(7, 1) | 입력 7개를 받아 가중합 1개를 내는 층을 만든다. 가중치 7개와 편향 1개를 안에 갖고 있고, 처음에는 작은 난수로 채워진다 | 아직 아무것도 모르는 학생 |
nn.MSELoss() | 예측과 정답의 제곱 오차 평균을 재는 손실 함수 객체("손실 함수" 절) | 채점 기준 |
model.parameters() | 모델이 가진 학습할 값(가중치와 편향)을 모두 꺼낸다 | 학생이 고칠 수 있는 공부 습관 목록 |
torch.optim.Adam(..., lr=0.01) | 그 값들을 학습률 0.01 로 고칠 옵티마이저를 만든다. 보폭을 알아서 조절하는 Adam 을 쓴다("학습률" 절) | 선생님을 정한다 |
X[i:i + 32] | 32건씩 잘라 배치를 만든다. i 가 0 이면 0~31번, 32 면 32~63번 | 오늘 풀 쪽을 펼친다 |
opt.zero_grad() | 지난 배치에서 구한 기울기를 0 으로 지운다 | 어제 채점 표시를 지운다 |
model(xb) | 배치를 모델에 넣어 예측을 낸다(순전파). 안에서는 forward 가 불린다 | 오늘 쪽을 푼다 |
loss_fn(pred, yb) | 예측과 정답을 견줘 손실을 낸다 | 채점한다 |
loss.backward() | 손실에서 거꾸로 따라가 가중치마다 기울기를 구한다("역전파와 자동 미분" 절) | 어디서 왜 틀렸는지 짚는다 |
opt.step() | 기울기를 보고 가중치를 조금 고친다 | 공부 습관을 조금 고친다 |
loss.item() | 손실 텐서에서 숫자 하나를 꺼낸다 | 오늘 점수를 적는다 |
zero_grad() 가 필요한 까닭은 "역전파와 자동 미분" 절에서 본 대로 backward() 가 기울기를 앞의 값에 더하기 때문입니다. 어제 채점 표시를 지우지 않으면 오늘 틀린 것과 어제 틀린 것이 섞여 버립니다.
모델과 배치를 들여다보기
nn.Linear 와 배치가 실제로 무엇을 담고 있는지 찍어 봅니다.
import torch
from torch import nn
torch.manual_seed(0)
X = torch.randn(64, 7)
model = nn.Linear(7, 1)
print(model) # 층의 생김새
print(model.weight.shape, model.bias.shape) # 가중치와 편향의 모양
print(sum(p.numel() for p in model.parameters())) # 학습할 값의 수
xb = X[0:32] # 첫 배치
print(xb.shape, model(xb).shape) # 배치를 넣으면 예측이 배치 건수만큼실행하면 이렇게 나옵니다.
| 출력 | 뜻 |
|---|---|
Linear(in_features=7, out_features=1, bias=True) | 입력 7개, 출력 1개, 편향이 있는 층 |
torch.Size([1, 7]) torch.Size([1]) | 가중치 7개(출력 1개 × 입력 7개)와 편향 1개 |
8 | 학습할 값은 모두 8개(7 + 1). opt.step() 이 이 8개를 고친다 |
torch.Size([32, 7]) torch.Size([32, 1]) | 32건을 한 번에 넣으면 예측도 32개가 나온다 |
이 코드가 도는 차례
바깥 for 가 에포크, 안쪽 for 가 배치입니다. 64건을 32건씩 나누면 배치가 2개이므로, 에포크마다 다섯 줄을 2번 돌고, 3 에포크 동안 모두 6번 가중치를 고칩니다.
| 에포크 | 배치 1(0~31번) | 배치 2(32~63번) | 찍는 손실 |
|---|---|---|---|
| 0 | 고침 1 | 고침 2 | 1.1541 |
| 1 | 고침 3 | 고침 4 | 1.1331 |
| 2 | 고침 5 | 고침 6 | 1.1132 |
print 는 안쪽 for 가 끝난 뒤에 있으므로, 찍히는 손실은 그 에포크의 마지막 배치(배치 2)의 손실입니다. 고칠 때마다 손실이 조금씩 줄어드는 것이 학습이 되고 있다는 신호입니다.
주의할 점
- 다섯 줄의 순서는 바꾸지 않습니다. 특히
zero_grad()를backward()뒤에 두면 방금 구한 기울기를 지워 버립니다. - 에포크가 너무 많으면 1장에서 본 과적합이 일어납니다. 시험용 손실이 다시 오르기 시작하면 멈출 때입니다.