2장. 신경망과 딥러닝의 기본 원리
실습 2 — 자동 미분으로 직선 맞추기
PyTorch 의 자동 미분과 경사하강법을, 층 없이 가중치 두 개(w, b)만으로 직접 돌려 봅니다. 점 다섯 개가 y = 2x + 1 위에 있을 때, w 와 b 를 0 에서 출발시켜 2 와 1 을 찾아가게 합니다.
1단계. 기울기 하나 구해 보기
import torch
# requires_grad=True: 이 값의 기울기가 필요하다고 표시한다
w = torch.tensor(2.0, requires_grad=True)
y = w ** 2 + 3 * w # 계산 과정이 기록된다
y.backward() # y 를 w 로 미분: 2w + 3
print("w = 2 에서의 기울기:", w.grad.item())w = 2 에서의 기울기: 7.0이 단계의 함수입니다.
| 코드 | 쉬운 뜻 |
|---|---|
torch.tensor(2.0, requires_grad=True) | 숫자 2 를 담은 0차원 텐서를 만들고, "이 값의 기울기가 필요하다"고 표시한다 |
y.backward() | y 를 만든 계산을 거꾸로 따라가 기울기를 구해 w.grad 에 넣는다 |
w.grad.item() | w.grad 에 담긴 기울기 텐서에서 숫자 하나를 꺼낸다 |
2단계. 데이터와 가중치 준비
# 점 다섯 개: 모두 y = 2x + 1 위에 있다
x = torch.tensor([1.0, 2.0, 3.0, 4.0, 5.0])
y = 2 * x + 1
# 찾아야 할 가중치 w 와 편향 b. 0 에서 출발한다
w = torch.tensor(0.0, requires_grad=True)
b = torch.tensor(0.0, requires_grad=True)
lr = 0.02 # 학습률(보폭)
print("정답:", y.tolist())정답: [3.0, 5.0, 7.0, 9.0, 11.0]이 단계의 함수입니다.
| 코드 | 쉬운 뜻 |
|---|---|
torch.tensor([1.0, ..., 5.0]) | 점 다섯 개의 x 값을 1차원 텐서로 만든다 |
2 * x + 1 | 칸마다 계산한다. 다섯 점의 정답 y 가 한 번에 나온다 |
torch.tensor(0.0, requires_grad=True) | 찾아야 할 w, b 를 0 에서 출발시키고 기울기를 구하도록 표시한다 |
lr = 0.02 | 학습률(보폭)을 정한다. 함수가 아니라 숫자 하나다 |
y.tolist() | 텐서를 파이썬 리스트로 바꾼다. 찍을 때 tensor(...) 없이 숫자만 보인다 |
3단계. 경사하강법 손으로 돌리기
for step in range(1, 501):
pred = w * x + b # 순전파: 지금의 w, b 로 예측
loss = ((pred - y) ** 2).mean() # 손실: MSE
loss.backward() # 역전파: w.grad, b.grad 가 채워진다
with torch.no_grad(): # 가중치를 고치는 계산은 기록하지 않는다
w -= lr * w.grad # 기울기 반대쪽으로 한 걸음
b -= lr * b.grad
w.grad.zero_() # 다음 걸음을 위해 기울기를 지운다
b.grad.zero_()
if step in (1, 10, 50, 100, 500):
print(f"{step:3d}번째 w={w.item():.3f} b={b.item():.3f} 손실={loss.item():.4f}") 1번째 w=1.000 b=0.280 손실=57.0000
10번째 w=2.104 b=0.612 손실=0.0282
50번째 w=2.082 b=0.705 손실=0.0161
100번째 w=2.058 b=0.790 손실=0.0082
500번째 w=2.004 b=0.986 손실=0.0000이 단계의 함수입니다.
| 코드 | 쉬운 뜻 |
|---|---|
for step in range(1, 501) | 경사하강법 걸음을 500번 되풀이한다 |
w * x + b | 지금의 w, b 로 다섯 점을 한꺼번에 예측한다(순전파) |
((pred - y) ** 2).mean() | 오차를 제곱해 평균 낸다. nn.MSELoss() 와 같은 계산을 손으로 적은 것이다 |
loss.backward() | 손실에서 거꾸로 따라가 w.grad, b.grad 를 채운다 |
with torch.no_grad(): | 이 들여쓰기 안의 계산은 기록하지 않는다. 가중치를 고치는 일은 학습할 계산이 아니기 때문이다 |
w -= lr * w.grad | w 를 "학습률 × 기울기"만큼 줄인다. 기울기가 음수면 오히려 커진다 |
w.grad.zero_() | 기울기를 0 으로 지운다. 이름 끝의 _ 는 PyTorch 에서 "그 자리에서 바꾼다"는 표시다 |
w.item(), loss.item() | 텐서에서 숫자 하나를 꺼내 찍는다 |
f"{w.item():.3f}" | 소수 셋째 자리까지 찍는다. {step:3d} 는 세 칸에 맞춰 찍는다 |
이 실습이 뜻하는 것
이 실습은 샤워기의 물 온도를 맞추는 일과 같습니다. 먼저 손잡이를 살짝 돌리면 온도가 얼마나 바뀌는지 느껴 보고(1단계), 원하는 온도를 정하고 손잡이를 맨 처음 자리에 둔 뒤(2단계), "너무 차다, 조금 더"를 500번 되풀이하며 손잡이를 고칩니다(3단계). 여기서 손잡이 위치가 w 와 b, 원하는 온도가 정답 y, 지금 온도와의 차이가 손실, "얼마나 돌릴까"가 학습률 × 기울기입니다. 비유와 다른 점은, 샤워기는 손잡이가 하나지만 여기서는 w 와 b 두 개를 함께 돌린다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 기울기 | 7.0 | w 를 조금 늘리면 y 가 그 7배쯤 늘어난다는 뜻입니다. 미분 식을 적지 않았는데 backward() 가 구했습니다 |
| 2. 정답 | [3.0, 5.0, 7.0, 9.0, 11.0] | 점 다섯 개의 정답입니다. 모델은 이 숫자만 보고 2 와 1 을 찾아야 합니다 |
| 3. 1번째 | w=1.000, b=0.280, 손실 57.0000 | 손실 57 은 w, b 가 0 일 때 잰 값입니다. 한 걸음 만에 w 가 1 로 크게 움직였습니다 |
| 3. 10번째 | w=2.104, 손실 0.0282 | w 가 정답 2 를 조금 넘었습니다. 손실은 이미 크게 줄었습니다 |
| 3. 100번째 | w=2.058, b=0.790 | w 는 2 쪽으로 돌아오고, b 는 1 을 향해 천천히 올라갑니다 |
| 3. 500번째 | w=2.004, b=0.986, 손실 0.0000 | 2 와 1 에 거의 닿았습니다. 손실 0.0000 은 소수 넷째 자리 아래로 작아졌다는 뜻입니다 |
한 줄로 줄이면, 정답 공식(y = 2x + 1)을 알려 주지 않아도 "기울기 반대쪽으로 조금씩"만 되풀이하면 w 와 b 가 2 와 1 을 찾아간다는 것을 보인 실습입니다. 실습 4부터는 이 손 계산을 nn.Linear 와 옵티마이저가 대신합니다. 점 다섯 개가 정확히 한 직선 위에 있는 쉬운 문제라서 손실이 0 근처까지 내려간 것이고, 실제 데이터에서는 손실이 0 까지 가지 않습니다.
코드 설명
- 1단계의 기울기 7.0 은 2w + 3 에 w = 2 를 넣은 값입니다. 미분 식을 적지 않았는데도
backward()가 구했습니다. torch.no_grad()안에서 가중치를 고치는 까닭은, 고치는 계산까지 기록되면 다음 역전파가 엉키기 때문입니다.w.grad.zero_()를 빼면 기울기가 걸음마다 쌓여서 점점 큰 걸음을 걷게 됩니다. 학습 루프의zero_grad()와 같은 일입니다.- 첫 걸음 뒤 w=1.000, b=0.280 에서 출발해, 10번째에 w 가 2.104 로 정답 2 를 조금 넘었다가 다시 내려옵니다. 500번째에는 w=2.004, b=0.986 으로 2 와 1 에 거의 닿고, 손실은 0.0000 으로 찍힙니다(소수 넷째 자리 아래로 작아졌다는 뜻입니다). b 가 w 보다 늦게 따라오는 것은 b 의 기울기가 더 작기 때문입니다.
- 실습 4부터는 이 손 계산을
nn.Linear와 옵티마이저가 대신 해 줍니다. 하는 일은 똑같습니다.