학습의 실전: 학습률, 배치, 러닝커브 진단, 역전파, 정형 데이터
앞 장의 알고리즘에서 정해야 할 것
앞 장은 경사하강법으로 끝났다.
이 알고리즘을 실제로 돌리려면 두 가지를 정해야 한다. 한 번에 얼마나 이동하는가(\(\eta\)), 그리고 기울기를 어떤 데이터로 계산하는가(전체인가, 일부인가). 그리고 다 돌린 뒤에는 잘 학습됐는지 어떻게 아는가가 남는다. 이 장은 이 세 가지를 차례로 다룬다.
학습관련 이슈 1: 학습률 (Learning Rate)
학습률의 효과
- 너무 낮은 학습률: 최적해에 너무 천천히 수렴한다
- 너무 높은 학습률: 최적해를 찾기까지 널뛰기하거나, 아예 벗어난다
앞 장의 손실 \(J(w) = (w - 3)^2\)에서 \(w_0 = 6\)부터 출발해 세 가지 학습률로 8번 이동해 보자.
코드
import pandas as pd
rows = []
for eta in [0.05, 0.3, 1.05]:
w, traj = 6.0, []
for k in range(9):
traj.append(w)
w = w - eta * 2 * (w - 3)
rows.append({'η': eta, **{f'{k}회': f'{v:.2f}' for k, v in enumerate(traj) if k in (0, 1, 2, 4, 8)}})
pd.DataFrame(rows).to_html(index=False, border=0, escape=False)| η | 0회 | 1회 | 2회 | 4회 | 8회 |
|---|---|---|---|---|---|
| 0.05 | 6.00 | 5.70 | 5.43 | 4.97 | 4.29 |
| 0.30 | 6.00 | 4.20 | 3.48 | 3.08 | 3.00 |
| 1.05 | 6.00 | -0.30 | 6.63 | 7.39 | 9.43 |
\(\eta = 0.05\)는 8번을 움직여도 아직 4.3 근처다. \(\eta = 0.3\)은 8번이면 3.00에 도달한다. \(\eta = 1.05\)는 매번 최솟값을 지나쳐 반대편으로 넘어가고, 그 폭이 점점 커진다. 손실이 줄지 않고 발산한다.
학습률 지정 방법
- 방법 1: 실험적 방법. 여러 학습률로 학습을 돌려 보고 손실이 어떻게 진행되는지 관찰한다. 보통 \(0.1, 0.01, 0.001\)처럼 10배 간격으로 시작한다
- 방법 2: 학습 진행 상황에 따라 학습률을 동적으로 조절. 처음에는 크게, 최솟값 근처에서는 작게 움직이도록 알고리즘이 스스로 조절한다
방법 2를 구현한 알고리즘을 옵티마이저(optimizer)라 부른다. 자주 쓰는 것은 다음과 같다.
| 알고리즘 | PyTorch | 특징 |
|---|---|---|
| SGD | torch.optim.SGD |
기본 경사하강법. 학습률 고정 |
| Adagrad | torch.optim.Adagrad |
자주 갱신된 가중치는 학습률을 줄인다 |
| Adadelta | torch.optim.Adadelta |
Adagrad의 학습률이 0으로 줄어드는 문제를 보완 |
| RMSProp | torch.optim.RMSprop |
최근 기울기 크기에 맞춰 학습률 조절 |
| Adam | torch.optim.Adam |
RMSProp + 관성(momentum). 현재 가장 많이 쓴다 |
이 수업의 실습은 Adam을 기본으로 쓴다. 어느 것을 쓰든 “기울기의 반대 방향으로 이동한다”는 원리는 같다.
연습을 더 하려면: 도장 4주차의 유형 C(학습률과 수렴 판정)가 이 내용이다.
학습관련 이슈 2: 배치(Batch) 사이즈
손실함수 \(J(W)\)는 모든 데이터에 대한 손실의 평균이다. 그러니 기울기 \(\partial J / \partial W\)를 계산하려면 데이터 전부를 신경망에 통과시켜야 한다. 데이터가 100만 건이면 가중치를 한 번 움직이기 위해 100만 번 계산해야 한다.
배치 사이즈 \(= n\) (전체)
- 초기 가중치를 무작위로 생성한다
- 다음 과정을 더 이상 해가 개선되지 않을 때까지 반복한다
- 모든 데이터로 편미분을 계산한다. \(\dfrac{\partial J(W)}{\partial W}\)
- 편미분 방향으로 이동한다 (가중치 업데이트)
- 최종 가중치를 리턴한다
기울기가 정확하지만, 데이터의 수가 커질수록 편미분 계산의 부하가 커진다.
배치 사이즈 \(= 1\)
알고리즘에서 바뀌는 곳은 두 줄뿐이다. 나머지는 그대로다.
- 초기 가중치를 무작위로 생성한다
- 다음 과정을 더 이상 해가 개선되지 않을 때까지 반복한다
- (전체 데이터 대신) 관측치 \(i\)를 하나 선택한다
- 그 한 건의 손실 \(J_i(W)\)로 편미분을 계산한다. \(\dfrac{\partial J_i(W)}{\partial W}\)
- 편미분 방향으로 이동한다 (가중치 업데이트)
- 최종 가중치를 리턴한다
배치 사이즈 = 적당
역시 같은 두 줄만 바뀐다. 한 건 대신 \(B\)건을 묶는다.
- 초기 가중치를 무작위로 생성한다
- 다음 과정을 더 이상 해가 개선되지 않을 때까지 반복한다
- (전체 데이터 대신) 데이터를 \(B\)개씩 배치(batch)로 묶는다
- 배치 단위로 편미분을 계산한다. \(\dfrac{\partial J(W)}{\partial W} \approx \dfrac{1}{B} \displaystyle\sum_{k=1}^{B} \dfrac{\partial J_k(W)}{\partial W}\)
- 편미분 방향으로 이동한다 (가중치 업데이트)
- 최종 가중치를 리턴한다
| 배치 \(= n\) | 배치 \(= 1\) | 배치 \(= B\) (미니배치) | |
|---|---|---|---|
| 업데이트 1회의 계산량 | 매우 큼 | 매우 작음 | 작음 |
| 기울기의 정확도 | 정확 | 매우 흔들림 | 적당히 정확 |
| 이름 | Batch GD | SGD | Mini-batch SGD |
에폭과 이터레이션
배치로 나누어 학습하면 “몇 번 반복했는가”를 두 가지로 셀 수 있다.
계산 예시. 데이터 1,000건, 배치 크기 50이면 1 에폭은 \(1000 / 50 = 20\) 이터레이션이다. 10 에폭을 돌리면 가중치는 \(20 \times 10 = 200\)번 업데이트된다.
코드
import pandas as pd
n = 1000
rows = []
for B in [1000, 100, 50, 10, 1]:
rows.append({'배치 크기 $B$': B, '1 에폭의 이터레이션 수 $n/B$': n // B,
'10 에폭의 업데이트 횟수': (n // B) * 10,
'이름': 'Batch GD' if B == n else ('SGD' if B == 1 else 'Mini-batch SGD')})
pd.DataFrame(rows).to_html(index=False, border=0, escape=False)| 배치 크기 \(B\) | 1 에폭의 이터레이션 수 \(n/B\) | 10 에폭의 업데이트 횟수 | 이름 |
|---|---|---|---|
| 1000 | 1 | 10 | Batch GD |
| 100 | 10 | 100 | Mini-batch SGD |
| 50 | 20 | 200 | Mini-batch SGD |
| 10 | 100 | 1000 | Mini-batch SGD |
| 1 | 1000 | 10000 | SGD |
연습을 더 하려면: 도장 5주차의 유형 E가 이 계산이다.
시도해 보세요: SGD(B=1)를 선택하고 시드를 바꾸면 매번 다른 경로로 수렴합니다. 배치 = n은 시드와 무관하게 항상 같은 경로입니다.
러닝커브를 이용한 모델 진단
학습률과 배치를 정해 학습을 끝냈다. 그런데 잘 학습된 것인지 어떻게 아는가? 손실이 0에 가까우면 좋은 것일까? 이 절은 그 질문에 답한다.
학습 데이터와 Unknown Data
1장의 사원 8명 데이터(경력 → 연봉)를 학습 데이터라 하자. 이 회사에는 사원이 8명만 있는 것이 아니다. 아직 보지 못한 다른 사원들, 앞으로 입사할 사원들이 Unknown Data다.
일반화 (Generalization)
딥러닝 모형의 특성
신경망을 구성하는 계층이 깊어질수록, 또한 한 계층에서 사용되는 퍼셉트론의 개수가 많을수록, 학습 데이터에 대한 정확도는 높아진다. 표현할 수 있는 곡선이 복잡해지기 때문이다.
학습 데이터 8명에 점점 복잡한 모형을 맞춰 보자. (모형의 복잡도는 다항식의 차수로 흉내 낸다. 신경망의 깊이·노드 수를 늘리는 것과 같은 효과다.)
왼쪽에서 오른쪽으로 모형이 복잡해질수록 RMSE(학습 데이터에 대한 오차)는 줄고 \(R^2\)는 1에 가까워진다. 맨 오른쪽 모형은 8개 점을 전부 정확히 통과한다. 학습 데이터에 대한 정확도만 보면 가장 좋은 모형이다.
과적합 (Overfitting)
이 네 모형에 Unknown Data를 겹쳐 보자.
과소적합 (Underfitting)
이상적인 딥러닝 모형
과적합과 과소적합 사이의 적절한 모형을 찾아야 한다.
| 과소적합 (Underfitting) | 이상적 (Ideal fit) | 과적합 (Overfitting) | |
|---|---|---|---|
| 모형 | 너무 단순 | 적당 | 너무 복잡 |
| 학습 데이터 오차 | 크다 | 작다 | 거의 0 |
| Unknown Data 오차 | 크다 | 작다 | 크다 |
딥러닝 모형의 일반화 능력 측정 방법
과적합과 과소적합을 피하려면 모델의 일반화 능력을 측정해야 한다. 하지만 모델을 학습하는 시점에 Unknown Data를 수집할 수는 없다. 미래의 데이터이기 때문이다.
대안: 우리가 가지고 있는 데이터를 둘로 나눈다.
Step 1. 데이터 나누기. 사원 8명을 훈련 6명, 테스트 2명으로 나눈다.
코드
import pandas as pd
sal_x = [1.5, 2.5, 4.2, 5.1, 6.7, 8.3, 9.5, 13.0]
sal_y = [31000, 39000, 43000, 49000, 54000, 67000, 92000, 129000]
split = ['훈련', '훈련', '훈련', '훈련', '훈련', '테스트', '훈련', '테스트']
rows = []
for i, (x, y, s) in enumerate(zip(sal_x, sal_y, split), start=1):
color = '#1565C0' if s == '훈련' else '#C62828'
rows.append({'obs': i, 'X (경력)': x, 'y (연봉)': f'{y:,}',
'용도': f'<b style="color:{color}">{s} 데이터</b>'})
pd.DataFrame(rows).to_html(index=False, border=0, escape=False)| obs | X (경력) | y (연봉) | 용도 |
|---|---|---|---|
| 1 | 1.5 | 31,000 | 훈련 데이터 |
| 2 | 2.5 | 39,000 | 훈련 데이터 |
| 3 | 4.2 | 43,000 | 훈련 데이터 |
| 4 | 5.1 | 49,000 | 훈련 데이터 |
| 5 | 6.7 | 54,000 | 훈련 데이터 |
| 6 | 8.3 | 67,000 | 테스트 데이터 |
| 7 | 9.5 | 92,000 | 훈련 데이터 |
| 8 | 13.0 | 129,000 | 테스트 데이터 |
Step 2. 훈련 데이터로 모델 학습. 훈련 데이터 6명만으로 가중치를 정한다. 테스트 데이터 2명은 모델이 본 적이 없다.
Step 3. 테스트 데이터로 모델 정확도 측정. 학습된 모델에 테스트 데이터의 경력을 넣어 연봉을 예측하고, 실제 연봉과 비교한다. 이 오차가 일반화 능력의 추정치다.
러닝커브 그리기
학습이 진행되는 동안 매 에폭 끝에 두 손실을 기록하면 곡선이 자라난다. 슬라이더를 움직여 보자.
처음에는 두 손실이 함께 내려간다. 어느 지점부터 훈련 손실은 계속 내려가는데 테스트 손실이 되올라간다. 그 지점부터 모델은 훈련 데이터의 세부(잡음)를 암기하기 시작한 것이다. 과적합의 시작점이다.
러닝커브로 학습 상태 진단하기
러닝커브의 추세를 보면 모델의 학습 상황을 진단할 수 있다. 여섯 가지 전형을 보자.
① 잘 학습됨. 에폭이 진행되면서 훈련·검증 손실이 유사한 수준으로 낮아지고, 안정적인 값을 유지한다. 그대로 종료하면 된다.
② 언더피팅 — 평평. 훈련 손실이 거의 감소하지 않는다. 학습이 거의 이루어지지 않고 있다. 학습률과 모형 크기를 점검한다.
③ 언더피팅 — 아직 감소 중. 에폭의 끝에 도달했는데도 훈련 손실이 계속 내려가고 있다. 더 학습될 여지가 남아 있다. 에폭을 늘린다.
④ 오버피팅. 훈련 손실은 계속 감소하는데 검증 손실은 더 이상 감소하지 않는 지점이 생긴다. 그 지점 이후는 과적합이다. 대응은 아래 절에서 다룬다.
⑤ 훈련 데이터 부족. 둘 다 감소하지만 검증 손실이 심하게 크다. 훈련 데이터셋이 전체를 충분히 대표하지 못한다. 데이터를 더 모은다.
⑥ 검증 데이터 부족. 훈련 손실은 안정적인데 검증 손실이 심하게 요동친다. 검증 데이터셋을 충분히 확보하지 못했다. 검증 데이터를 늘린다.
여섯 전형을 한 표로 정리한다.
| # | 러닝커브의 모양 | 진단 | 처방 |
|---|---|---|---|
| 1 | 에폭이 진행되면서 훈련·검증 손실이 유사한 수준으로 낮아지고, 안정적인 값을 유지 | 잘 학습됨 | 그대로 종료 |
| 2 | 훈련 손실이 평평하다 (거의 감소하지 않음) | 언더피팅 | 학습이 거의 이루어지지 않고 있다. 학습률·모형 크기를 점검 |
| 3 | 에폭의 끝에 도달했는데도 훈련 손실이 계속 감소 중 | 언더피팅 | 더 학습될 여지가 남아 있다. 에폭을 늘린다 |
| 4 | 훈련 손실은 지속 감소, 검증 손실은 더 이상 감소하지 않는 지점 발생 | 오버피팅 | 그 지점 이후는 과적합. 조기 종료, 드롭아웃, 데이터 추가 |
| 5 | 둘 다 감소하지만 검증 손실이 심하게 크다 (간격이 크다) | 훈련 데이터 부족 | 훈련 데이터셋이 전체를 충분히 대표하지 못한다. 데이터를 더 모은다 |
| 6 | 훈련 손실은 안정적인데 검증 손실이 심하게 요동친다 | 검증 데이터 부족 | 검증 데이터셋을 충분히 확보하지 못했다. 검증 데이터를 늘린다 |
연습을 더 하려면: 도장 5주차의 유형 B(러닝커브 진단)가 이 표다. 실습 4에서는 직접 학습시킨 모델의 러닝커브를 이 표로 진단한다.
과적합 방지 전략 1: Early Stopping
④ 오버피팅으로 진단되면 두 가지 대응이 있다.
과적합 방지 전략 2: Dropout
회색 노드가 이번 이터레이션에서 꺼진(drop된) 퍼셉트론이다. 다음 이터레이션에는 다른 노드가 꺼진다. PyTorch에서는
nn.Dropout(p=0.5)층 하나를 끼워 넣으면 된다.
보충 1: 기울기는 어디서 오는가 — 역전파
3장 예제 신경망의 가중치 13개 가운데 출력층으로 들어오는 4개의 편미분은 3장에서 배운 대로 바로 구할 수 있었다. 손실이 그 가중치에 직접 걸려 있기 때문이다. 은닉층의 9개는 다르다. 손실에서 출력층을 거쳐 은닉층까지 층을 되짚어 가야 한다. 그 방법이 역전파다. 층을 되짚는 원리만 보면 되므로, 가장 작은 신경망으로 확인한다. 은닉노드 1개짜리 2층 네트워크다.
\[x \xrightarrow{w_1,\, b_1} z_1 = w_1 x + b_1 \xrightarrow{\sigma} h = \sigma(z_1) \xrightarrow{w_2,\, b_2} \hat{y} = w_2 h + b_2 \longrightarrow \mathcal{L} = (y - \hat{y})^2\]
연쇄법칙
손실 \(\mathcal{L}\)은 \(w_1\)의 함수지만, \(w_1\)이 \(\mathcal{L}\)에 직접 들어 있지 않다. \(w_1 \to z_1 \to h \to \hat{y} \to \mathcal{L}\)로 네 단계를 거쳐 영향을 준다.
각 화살표의 미분
| 화살표 | 함수 | 미분 | 이유 |
|---|---|---|---|
| \(\hat{y} \to \mathcal{L}\) | \(\mathcal{L} = (y - \hat{y})^2\) | \(\dfrac{\partial \mathcal{L}}{\partial \hat{y}} = -2(y - \hat{y})\) | 제곱의 미분 \(2(y-\hat{y})\)에 안쪽 \((-1)\) |
| \(h \to \hat{y}\) | \(\hat{y} = w_2 h + b_2\) | \(\dfrac{\partial \hat{y}}{\partial h} = w_2\) | \(h\)의 계수 |
| \(w_2 \to \hat{y}\) | 〃 | \(\dfrac{\partial \hat{y}}{\partial w_2} = h\) | \(w_2\)의 계수 |
| \(z_1 \to h\) | \(h = \sigma(z_1)\) | \(\dfrac{\partial h}{\partial z_1} = \sigma'(z_1) = h(1 - h)\) | 아래 공식 |
| \(w_1 \to z_1\) | \(z_1 = w_1 x + b_1\) | \(\dfrac{\partial z_1}{\partial w_1} = x\) | \(w_1\)의 계수 |
✍️ 손으로 따라 계산하기 — 역전파 한 스텝
설정: \(x = 1.0\), 정답 \(y = 1.0\), 파라미터 \(w_1 = 0.5,\ b_1 = 0,\ w_2 = 1.0,\ b_2 = 0\), 학습률 \(\eta = 0.5\).
1단계 — 순전파 (왼쪽에서 오른쪽으로).
코드
import pandas as pd
import numpy as np
x, y = 1.0, 1.0
w1, b1, w2, b2 = 0.5, 0.0, 1.0, 0.0
z1 = w1 * x + b1
h = 1 / (1 + np.exp(-z1))
y_hat = w2 * h + b2
L = (y - y_hat) ** 2
pd.DataFrame([
{'단계': '가중합', '수식': 'z₁ = w₁x + b₁ = 0.5×1 + 0', '값': f'{z1:.4f}'},
{'단계': '활성화', '수식': 'h = σ(z₁) = σ(0.5)', '값': f'{h:.4f}'},
{'단계': '출력', '수식': 'ŷ = w₂h + b₂ = 1×0.6225 + 0', '값': f'{y_hat:.4f}'},
{'단계': '손실', '수식': 'L = (y − ŷ)² = (1 − 0.6225)²', '값': f'{L:.4f}'},
]).to_html(index=False, border=0, escape=False)| 단계 | 수식 | 값 |
|---|---|---|
| 가중합 | z₁ = w₁x + b₁ = 0.5×1 + 0 | 0.5000 |
| 활성화 | h = σ(z₁) = σ(0.5) | 0.6225 |
| 출력 | ŷ = w₂h + b₂ = 1×0.6225 + 0 | 0.6225 |
| 손실 | L = (y − ŷ)² = (1 − 0.6225)² | 0.1425 |
2단계 — 역전파 (오른쪽에서 왼쪽으로). 손실에서 출발해 화살표의 미분을 차례로 곱한다.
코드
import pandas as pd
dL_dyhat = -2 * (y - y_hat)
dL_dw2 = dL_dyhat * h
dL_db2 = dL_dyhat * 1
dL_dh = dL_dyhat * w2
sigma_prime = h * (1 - h)
dL_dz1 = dL_dh * sigma_prime
dL_dw1 = dL_dz1 * x
dL_db1 = dL_dz1 * 1
pd.DataFrame([
{'대상': '∂L/∂ŷ', '연쇄법칙 전개': '−2(y − ŷ) = −2(1 − 0.6225)', '값': f'{dL_dyhat:.4f}'},
{'대상': '∂L/∂w₂', '연쇄법칙 전개': '∂L/∂ŷ · h = −0.7551 × 0.6225', '값': f'{dL_dw2:.4f}'},
{'대상': '∂L/∂b₂', '연쇄법칙 전개': '∂L/∂ŷ · 1', '값': f'{dL_db2:.4f}'},
{'대상': '∂L/∂h', '연쇄법칙 전개': '∂L/∂ŷ · w₂ = −0.7551 × 1', '값': f'{dL_dh:.4f}'},
{'대상': "σ'(z₁)", '연쇄법칙 전개': 'h(1 − h) = 0.6225 × 0.3775', '값': f'{sigma_prime:.4f}'},
{'대상': '∂L/∂w₁', '연쇄법칙 전개': "∂L/∂h · σ'(z₁) · x = −0.7551 × 0.2350 × 1", '값': f'{dL_dw1:.4f}'},
{'대상': '∂L/∂b₁', '연쇄법칙 전개': "∂L/∂h · σ'(z₁) · 1", '값': f'{dL_db1:.4f}'},
]).to_html(index=False, border=0, escape=False)| 대상 | 연쇄법칙 전개 | 값 |
|---|---|---|
| ∂L/∂ŷ | −2(y − ŷ) = −2(1 − 0.6225) | -0.7551 |
| ∂L/∂w₂ | ∂L/∂ŷ · h = −0.7551 × 0.6225 | -0.4700 |
| ∂L/∂b₂ | ∂L/∂ŷ · 1 | -0.7551 |
| ∂L/∂h | ∂L/∂ŷ · w₂ = −0.7551 × 1 | -0.7551 |
| σ'(z₁) | h(1 − h) = 0.6225 × 0.3775 | 0.2350 |
| ∂L/∂w₁ | ∂L/∂h · σ'(z₁) · x = −0.7551 × 0.2350 × 1 | -0.1774 |
| ∂L/∂b₁ | ∂L/∂h · σ'(z₁) · 1 | -0.1774 |
\(w_1\)의 기울기는 손실 → 출력 → 은닉 → 가중합을 거쳐 온 미분들의 곱이다. 앞쪽 층의 가중치일수록 곱해지는 항이 늘어난다.
3단계 — 갱신과 확인. \(\eta = 0.5\)로 네 파라미터를 갱신하고 손실이 줄었는지 본다.
코드
import pandas as pd
import numpy as np
eta = 0.5
w1_new = w1 - eta * dL_dw1
b1_new = b1 - eta * dL_db1
w2_new = w2 - eta * dL_dw2
b2_new = b2 - eta * dL_db2
z1_new = w1_new * x + b1_new
h_new = 1 / (1 + np.exp(-z1_new))
y_hat_new = w2_new * h_new + b2_new
L_new = (y - y_hat_new) ** 2
pd.DataFrame([
{'항목': 'w₁', '갱신 전': f'{w1:.4f}', '갱신식': f'0.5 − 0.5 × ({dL_dw1:.4f})', '갱신 후': f'{w1_new:.4f}'},
{'항목': 'b₁', '갱신 전': f'{b1:.4f}', '갱신식': f'0 − 0.5 × ({dL_db1:.4f})', '갱신 후': f'{b1_new:.4f}'},
{'항목': 'w₂', '갱신 전': f'{w2:.4f}', '갱신식': f'1 − 0.5 × ({dL_dw2:.4f})', '갱신 후': f'{w2_new:.4f}'},
{'항목': 'b₂', '갱신 전': f'{b2:.4f}', '갱신식': f'0 − 0.5 × ({dL_db2:.4f})', '갱신 후': f'{b2_new:.4f}'},
{'항목': '<b>손실 L</b>', '갱신 전': f'<b>{L:.4f}</b>', '갱신식': '(갱신된 파라미터로 재계산)', '갱신 후': f'<b>{L_new:.4f}</b>'},
]).to_html(index=False, border=0, escape=False)| 항목 | 갱신 전 | 갱신식 | 갱신 후 |
|---|---|---|---|
| w₁ | 0.5000 | 0.5 − 0.5 × (-0.1774) | 0.5887 |
| b₁ | 0.0000 | 0 − 0.5 × (-0.1774) | 0.0887 |
| w₂ | 1.0000 | 1 − 0.5 × (-0.4700) | 1.2350 |
| b₂ | 0.0000 | 0 − 0.5 × (-0.7551) | 0.3775 |
| 손실 L | 0.1425 | (갱신된 파라미터로 재계산) | 0.0386 |
역전파 한 번으로 손실이 줄었다. 이것이 딥러닝 학습의 한 이터레이션 전부다. PyTorch는 이 계산을 loss.backward() 한 줄로 대신해 준다.
기울기 소실 (Vanishing Gradient)
위 계산에서 \(\sigma'(z_1) = 0.2350\)이었고, Sigmoid 도함수의 최댓값은 0.25다. 층이 10개라면 앞쪽 가중치의 기울기에는 \(\sigma'\)이 10번 곱해진다. 최선의 경우에도 \(0.25^{10} \approx 0.000001\)이다. 앞쪽 층의 기울기가 사실상 0이 되어 학습이 멈춘다. 이것이 기울기 소실이다.
2장에서 “ReLU가 현재 가장 많이 쓰인다”고 한 이유가 이것이다. ReLU는 \(z > 0\)에서 도함수가 항상 1이라, 곱해도 기울기가 줄어들지 않는다.
연습을 더 하려면: 도장 5주차의 유형 A(Sigmoid 도함수), F·G(2층 역전파), H(기울기 소실)가 이 계산이다.
보충 2: 정형 데이터를 신경망에 넣기 전에
6주차에 나가는 실습 과제(HW1)는 정형 데이터다. 신경망의 입력은 적절한 크기의 숫자여야 하는데, 정형 데이터는 그렇지 않은 열이 많다.
범주형 변수: 원-핫 인코딩
“브랜드 = 삼성”을 어떻게 숫자로 넣는가? 삼성=1, 애플=2, LG=3으로 넣으면 “LG는 삼성의 3배”라는 거짓 정보가 생긴다.
코드
import pandas as pd
pd.DataFrame({
'원래 값': ['삼성', '애플', 'LG', '애플'],
'브랜드_삼성': [1, 0, 0, 0],
'브랜드_애플': [0, 1, 0, 1],
'브랜드_LG': [0, 0, 1, 0],
}).to_html(index=False, border=0)| 원래 값 | 브랜드_삼성 | 브랜드_애플 | 브랜드_LG |
|---|---|---|---|
| 삼성 | 1 | 0 | 0 |
| 애플 | 0 | 1 | 0 |
| LG | 0 | 0 | 1 |
| 애플 | 0 | 1 | 0 |
스케일 차이: 표준화
나이(20~60)와 연봉(30,000~130,000)이 같은 신경망에 들어가면 연봉 쪽 가중치의 기울기가 압도적으로 커져 학습이 불안정해진다.
코드
import pandas as pd
import numpy as np
ages = [24, 30, 36, 42, 48]
mu = np.mean(ages)
s = np.std(ages)
rows = [{'나이 x': a, 'x − μ': f'{a - mu:.0f}', '(x − μ)²': f'{(a - mu)**2:.0f}', 'z = (x − μ)/s': f'{(a - mu) / s:.3f}'} for a in ages]
rows.append({'나이 x': f'μ = {mu:.0f}', 'x − μ': '', '(x − μ)²': f'합 {sum((a-mu)**2 for a in ages):.0f} → /n = {np.var(ages):.0f} → √ = s = {s:.3f}', 'z = (x − μ)/s': '평균 0, 표준편차 1'})
pd.DataFrame(rows).to_html(index=False, border=0, escape=False)| 나이 x | x − μ | (x − μ)² | z = (x − μ)/s |
|---|---|---|---|
| 24 | -12 | 144 | -1.414 |
| 30 | -6 | 36 | -0.707 |
| 36 | 0 | 0 | 0.000 |
| 42 | 6 | 36 | 0.707 |
| 48 | 12 | 144 | 1.414 |
| μ = 36 | 합 360 → /n = 72 → √ = s = 8.485 | 평균 0, 표준편차 1 |
테스트 데이터를 섞어 계산하면 모델이 테스트 데이터의 정보를 미리 보는 것이 된다(데이터 누수). 훈련 데이터에서 구한 \(\mu, s\)를 테스트 데이터에 그대로 적용한다. 결측치를 평균으로 채울 때도 같다. 앞 절의 “Step 1 데이터 나누기”를 가장 먼저 하는 이유다.
연습을 더 하려면: 도장 5주차의 유형 C(표준화), D(원-핫과 입력 크기)가 이 계산이다.
핵심 요약
| 개념 | 핵심 내용 |
|---|---|
| 학습률 \(\eta\) | 기울기 방향으로 얼마나 이동할지. 너무 낮으면 느리고, 너무 높으면 널뛰기·발산 |
| 학습률 지정 | 실험적으로 고르거나, 옵티마이저(Adam 등)가 동적으로 조절 |
| 배치 크기 | \(n\): 정확하지만 느림 / 1(SGD): 빠르지만 민감 / 미니배치: 실무 표준 |
| 에폭·이터레이션 | 1 에폭 = 전체 데이터 1회 = \(n/B\) 이터레이션 |
| 일반화 | 학습 데이터가 아닌 Unknown Data를 맞추는 능력. 모델의 진짜 목표 |
| 과적합 / 과소적합 | 너무 복잡해 학습 데이터를 암기 / 너무 단순해 학습 데이터도 못 맞춤 |
| 훈련/테스트 분할 | Step 1 나누기 → Step 2 훈련 데이터로 학습 → Step 3 테스트 데이터로 측정 |
| 러닝커브 | 에폭에 따른 훈련·검증 손실. 여섯 전형으로 진단 (표 참조) |
| 과적합 방지 | Early Stopping(검증 손실이 오르기 전에 멈춤), Dropout(무작위로 노드 끄기) |
| 역전파 | 연쇄법칙: 각 화살표의 미분을 뒤에서 앞으로 곱한다. \(\sigma' = h(1-h)\), 최대 0.25 → 기울기 소실 |
| 전처리 | 범주형은 원-핫, 숫자형은 표준화(\(n\)으로 나눔). 통계량은 훈련 데이터에서만 |