학습의 실전: 학습률, 배치, 러닝커브 진단, 역전파, 정형 데이터

학습 목표
  • 학습률의 역할을 알고, 너무 작을 때와 너무 클 때 무슨 일이 생기는지 설명할 수 있다
  • 배치 크기 \(n\), \(1\), 적당한 크기(미니배치)의 차이를 설명하고, 에폭과 이터레이션을 계산할 수 있다
  • 학습 데이터와 Unknown Data를 구분하고, 일반화가 무엇인지 말할 수 있다
  • 과적합과 과소적합을 그림으로 구분할 수 있다
  • 훈련/테스트 데이터 분할의 세 단계를 안다
  • 러닝커브를 읽고 학습 상태(정상, 언더피팅, 오버피팅, 데이터 부족)를 진단할 수 있다
  • Sigmoid의 도함수와 연쇄법칙으로 2층 네트워크의 기울기를 손으로 구할 수 있다
  • 원-핫 인코딩과 표준화를 손으로 계산할 수 있다

앞 장의 알고리즘에서 정해야 할 것

앞 장은 경사하강법으로 끝났다.

경사하강법 (Gradient Descent)
  1. 초기 가중치를 무작위로 생성한다. \(W \sim \mathcal{N}(0, \sigma^2)\)
  2. 다음 과정을 더 이상 해가 개선되지 않을 때까지 반복한다.
    • 기울기를 계산한다. \(\dfrac{\partial J(W)}{\partial W}\)
    • 기울기의 \(-\)방향으로 이동한다 (가중치 업데이트). \(W \leftarrow W - \eta\, \dfrac{\partial J(W)}{\partial W}\)
  3. 최종 가중치를 리턴한다.

이 알고리즘을 실제로 돌리려면 두 가지를 정해야 한다. 한 번에 얼마나 이동하는가(\(\eta\)), 그리고 기울기를 어떤 데이터로 계산하는가(전체인가, 일부인가). 그리고 다 돌린 뒤에는 잘 학습됐는지 어떻게 아는가가 남는다. 이 장은 이 세 가지를 차례로 다룬다.


학습관련 이슈 1: 학습률 (Learning Rate)

학습률 (Learning Rate) \(\eta\)

매번 가중치를 업데이트할 때, 기울기 방향으로 어느 정도 이동할지 결정하는 값.

\[W \leftarrow W - \eta\, \frac{\partial J(W)}{\partial W}\]

기울기는 방향과 급한 정도를 알려 주지만 “몇 걸음 갈지”는 정해 주지 않는다. 그것을 정하는 것이 \(\eta\)다. 학습 전에 사람이 정한다(하이퍼파라미터).

학습률의 효과

  • 너무 낮은 학습률: 최적해에 너무 천천히 수렴한다
  • 너무 높은 학습률: 최적해를 찾기까지 널뛰기하거나, 아예 벗어난다

앞 장의 손실 \(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)
표 1: 세 가지 학습률에서 w의 변화 — w₀ = 6, J(w) = (w − 3)²
η 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\) (전체)

  1. 초기 가중치를 무작위로 생성한다
  2. 다음 과정을 더 이상 해가 개선되지 않을 때까지 반복한다
    • 모든 데이터로 편미분을 계산한다. \(\dfrac{\partial J(W)}{\partial W}\)
    • 편미분 방향으로 이동한다 (가중치 업데이트)
  3. 최종 가중치를 리턴한다

기울기가 정확하지만, 데이터의 수가 커질수록 편미분 계산의 부하가 커진다.

배치 사이즈 \(= 1\)

알고리즘에서 바뀌는 곳은 두 줄뿐이다. 나머지는 그대로다.

  1. 초기 가중치를 무작위로 생성한다
  2. 다음 과정을 더 이상 해가 개선되지 않을 때까지 반복한다
    • (전체 데이터 대신) 관측치 \(i\)를 하나 선택한다
    • 그 한 건의 손실 \(J_i(W)\)로 편미분을 계산한다. \(\dfrac{\partial J_i(W)}{\partial W}\)
    • 편미분 방향으로 이동한다 (가중치 업데이트)
  3. 최종 가중치를 리턴한다
확률적 경사하강법 (SGD, Stochastic Gradient Descent)

매 관측치마다 손실을 계산하고 가중치를 업데이트하는 알고리즘.

  • 장점: 한 번의 업데이트가 매우 빠르다
  • 단점: 데이터 한 건의 기울기는 전체 기울기와 다르다. 지나치게 민감해서 방향이 계속 흔들린다

배치 사이즈 = 적당

역시 같은 두 줄만 바뀐다. 한 건 대신 \(B\)건을 묶는다.

  1. 초기 가중치를 무작위로 생성한다
  2. 다음 과정을 더 이상 해가 개선되지 않을 때까지 반복한다
    • (전체 데이터 대신) 데이터를 \(B\)개씩 배치(batch)로 묶는다
    • 배치 단위로 편미분을 계산한다. \(\dfrac{\partial J(W)}{\partial W} \approx \dfrac{1}{B} \displaystyle\sum_{k=1}^{B} \dfrac{\partial J_k(W)}{\partial W}\)
    • 편미분 방향으로 이동한다 (가중치 업데이트)
  3. 최종 가중치를 리턴한다
미니배치 SGD (Stochastic Gradient Descent with Mini Batch)

배치 사이즈만큼 손실을 계산한 뒤 가중치를 업데이트하는 알고리즘.

계산이 빠르고, \(B\)건의 평균이므로 최적해 개선 방향을 잘 예측한다. 실무의 표준이다. \(B\)는 보통 32, 64, 128처럼 2의 거듭제곱을 쓴다.

배치 \(= n\) 배치 \(= 1\) 배치 \(= B\) (미니배치)
업데이트 1회의 계산량 매우 큼 매우 작음 작음
기울기의 정확도 정확 매우 흔들림 적당히 정확
이름 Batch GD SGD Mini-batch SGD

에폭과 이터레이션

배치로 나누어 학습하면 “몇 번 반복했는가”를 두 가지로 셀 수 있다.

이터레이션 (Iteration)과 에폭 (Epoch)
  • 이터레이션: 배치 하나로 가중치를 한 번 업데이트하는 것
  • 에폭: 전체 데이터를 한 번 다 사용하는 것

데이터가 \(n\)건, 배치 크기가 \(B\)이면 1 에폭 = \(n / B\) 이터레이션이다.

계산 예시. 데이터 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)
표 2: 데이터 1,000건일 때 배치 크기에 따른 이터레이션 수
배치 크기 \(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가 이 계산이다.

인터랙티브 — 배치 크기에 따른 경사하강법 비교

같은 손실 \(J(w) = (w-3)^2\)에서 배치 크기에 따라 가중치의 경로가 어떻게 달라지는지 확인하세요. 배치가 작을수록 기울기가 흔들려 경로가 거칠지만 업데이트가 잦습니다. (기울기의 흔들림은 이 시뮬레이션에서 인공적으로 재현한 것입니다. 실제로는 배치에 어떤 데이터가 뽑히느냐에 따라 생깁니다.)

시도해 보세요: SGD(B=1)를 선택하고 시드를 바꾸면 매번 다른 경로로 수렴합니다. 배치 = n은 시드와 무관하게 항상 같은 경로입니다.


러닝커브를 이용한 모델 진단

학습률과 배치를 정해 학습을 끝냈다. 그런데 잘 학습된 것인지 어떻게 아는가? 손실이 0에 가까우면 좋은 것일까? 이 절은 그 질문에 답한다.

학습 데이터와 Unknown Data

학습 데이터 (Training Data)

모델을 학습하는 데 사용된 데이터.

Unknown Data

학습 데이터로 사용되지 않았지만, 학습된 모형이 미래에 맞추어야 하는 데이터.

1장의 사원 8명 데이터(경력 → 연봉)를 학습 데이터라 하자. 이 회사에는 사원이 8명만 있는 것이 아니다. 아직 보지 못한 다른 사원들, 앞으로 입사할 사원들이 Unknown Data다.

일반화 (Generalization)

일반화 (Generalization)
  • 모델의 학습 = 주어진 학습 데이터를 사용하여 가중치를 정하는 것
  • 모델의 목표 = 이전에 보지 못한 Unknown Data를 잘 예측하는 것
  • 모델이 Unknown Data를 맞출 수 있는 능력 = 일반화

학습 데이터를 잘 맞추는 것은 수단이고, Unknown Data를 잘 맞추는 것이 목적이다.

딥러닝 모형의 특성

신경망을 구성하는 계층이 깊어질수록, 또한 한 계층에서 사용되는 퍼셉트론의 개수가 많을수록, 학습 데이터에 대한 정확도는 높아진다. 표현할 수 있는 곡선이 복잡해지기 때문이다.

학습 데이터 8명에 점점 복잡한 모형을 맞춰 보자. (모형의 복잡도는 다항식의 차수로 흉내 낸다. 신경망의 깊이·노드 수를 늘리는 것과 같은 효과다.)

왼쪽에서 오른쪽으로 모형이 복잡해질수록 RMSE(학습 데이터에 대한 오차)는 줄고 \(R^2\)는 1에 가까워진다. 맨 오른쪽 모형은 8개 점을 전부 정확히 통과한다. 학습 데이터에 대한 정확도만 보면 가장 좋은 모형이다.

과적합 (Overfitting)

이 네 모형에 Unknown Data를 겹쳐 보자.

과적합 (Overfitting)

학습 데이터에 대한 정확도가 가장 높았던 모델(왼쪽).

  • 모델이 학습에 사용된 데이터를 지나치게 충실하게 표현했다
  • 학습 데이터 사이사이에서 곡선이 요동쳐 Unknown Data에 대한 일반화에 실패한다

과소적합 (Underfitting)

과소적합 (Underfitting)

학습 데이터에 대한 정확도가 가장 낮았던 모델(오른쪽).

  • 모델이 데이터를 지나치게 단순하게 표현했다
  • 학습 데이터도 못 맞추고, 따라서 Unknown Data에 대한 일반화에도 실패한다

이상적인 딥러닝 모형

과적합과 과소적합 사이의 적절한 모형을 찾아야 한다.

과소적합 (Underfitting) 이상적 (Ideal fit) 과적합 (Overfitting)
모형 너무 단순 적당 너무 복잡
학습 데이터 오차 크다 작다 거의 0
Unknown Data 오차 크다 작다 크다
인터랙티브 — 모형 복잡도를 바꾸며 과적합을 확인하세요

차수를 조절해 보세요. 낮으면 훈련 점조차 못 맞추고(과소적합), 높으면 훈련 점은 완벽히 통과하지만 검증 오차가 폭발합니다(과적합).

딥러닝 모형의 일반화 능력 측정 방법

과적합과 과소적합을 피하려면 모델의 일반화 능력을 측정해야 한다. 하지만 모델을 학습하는 시점에 Unknown Data를 수집할 수는 없다. 미래의 데이터이기 때문이다.

대안: 우리가 가지고 있는 데이터를 둘로 나눈다.

훈련 데이터와 테스트 데이터
  • 훈련 데이터 (Training Data): 모델의 학습에 사용
  • 테스트 데이터 (Testing Data): 모델의 일반화 능력 검증에 사용. 학습에는 절대 쓰지 않아 Unknown Data 역할을 한다

참고: 경우에 따라 테스트 데이터를 검증 데이터(validation 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)
표 3: Step 1 — 훈련 데이터(파랑)와 테스트 데이터(빨강)로 나누기
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. 테스트 데이터로 모델 정확도 측정. 학습된 모델에 테스트 데이터의 경력을 넣어 연봉을 예측하고, 실제 연봉과 비교한다. 이 오차가 일반화 능력의 추정치다.

전체 데이터 사원 8명 훈련 데이터 6명 테스트 데이터 2명 Step 1 모델 가중치 학습 Step 2 학습 Step 3 예측 테스트 오차 = 일반화 능력

러닝커브 그리기

러닝커브 (Learning Curve)

에폭의 증가에 따른 훈련 손실(Train Loss)과 테스트 손실(Test Loss)의 경향을 그린 그래프.

가로축이 에폭(또는 이터레이션), 세로축이 손실이다. 두 곡선을 같은 그림에 그린다. 훈련 곡선만 그리면 아래의 진단을 할 수 없다.

학습이 진행되는 동안 매 에폭 끝에 두 손실을 기록하면 곡선이 자라난다. 슬라이더를 움직여 보자.

처음에는 두 손실이 함께 내려간다. 어느 지점부터 훈련 손실은 계속 내려가는데 테스트 손실이 되올라간다. 그 지점부터 모델은 훈련 데이터의 세부(잡음)를 암기하기 시작한 것이다. 과적합의 시작점이다.

러닝커브로 학습 상태 진단하기

러닝커브의 추세를 보면 모델의 학습 상황을 진단할 수 있다. 여섯 가지 전형을 보자.

① 잘 학습됨. 에폭이 진행되면서 훈련·검증 손실이 유사한 수준으로 낮아지고, 안정적인 값을 유지한다. 그대로 종료하면 된다.

② 언더피팅 — 평평. 훈련 손실이 거의 감소하지 않는다. 학습이 거의 이루어지지 않고 있다. 학습률과 모형 크기를 점검한다.

③ 언더피팅 — 아직 감소 중. 에폭의 끝에 도달했는데도 훈련 손실이 계속 내려가고 있다. 더 학습될 여지가 남아 있다. 에폭을 늘린다.

④ 오버피팅. 훈련 손실은 계속 감소하는데 검증 손실은 더 이상 감소하지 않는 지점이 생긴다. 그 지점 이후는 과적합이다. 대응은 아래 절에서 다룬다.

⑤ 훈련 데이터 부족. 둘 다 감소하지만 검증 손실이 심하게 크다. 훈련 데이터셋이 전체를 충분히 대표하지 못한다. 데이터를 더 모은다.

⑥ 검증 데이터 부족. 훈련 손실은 안정적인데 검증 손실이 심하게 요동친다. 검증 데이터셋을 충분히 확보하지 못했다. 검증 데이터를 늘린다.

여섯 전형을 한 표로 정리한다.

# 러닝커브의 모양 진단 처방
1 에폭이 진행되면서 훈련·검증 손실이 유사한 수준으로 낮아지고, 안정적인 값을 유지 잘 학습됨 그대로 종료
2 훈련 손실이 평평하다 (거의 감소하지 않음) 언더피팅 학습이 거의 이루어지지 않고 있다. 학습률·모형 크기를 점검
3 에폭의 끝에 도달했는데도 훈련 손실이 계속 감소 중 언더피팅 더 학습될 여지가 남아 있다. 에폭을 늘린다
4 훈련 손실은 지속 감소, 검증 손실은 더 이상 감소하지 않는 지점 발생 오버피팅 그 지점 이후는 과적합. 조기 종료, 드롭아웃, 데이터 추가
5 둘 다 감소하지만 검증 손실이 심하게 크다 (간격이 크다) 훈련 데이터 부족 훈련 데이터셋이 전체를 충분히 대표하지 못한다. 데이터를 더 모은다
6 훈련 손실은 안정적인데 검증 손실이 심하게 요동친다 검증 데이터 부족 검증 데이터셋을 충분히 확보하지 못했다. 검증 데이터를 늘린다

연습을 더 하려면: 도장 5주차의 유형 B(러닝커브 진단)가 이 표다. 실습 4에서는 직접 학습시킨 모델의 러닝커브를 이 표로 진단한다.

과적합 방지 전략 1: Early Stopping

④ 오버피팅으로 진단되면 두 가지 대응이 있다.

조기 종료 (Early Stopping)

에폭이 진행되어 과적합 구간으로 들어가기 전에 멈추는 것. 테스트 손실이 증가하기 시작하는 시점에서 학습을 멈춘다. 앞의 러닝커브 그리기 보드에서 “Stop training here!”로 표시된 지점이다.

실무에서는 끝까지 돌리되 테스트 손실이 가장 낮았던 시점의 가중치를 저장해 두고, 그것을 최종 모델로 쓴다.

과적합 방지 전략 2: Dropout

드롭아웃 (Dropout)

학습 과정에서 퍼셉트론의 일부만 (무작위로) 활성화시켜 학습시키는 것. 매 이터레이션마다 은닉노드 중 일정 비율(보통 50%)을 무작위로 골라 출력을 0으로 만든다.

특정 노드 하나에 의존해 정답을 암기하는 것을 막고, 어느 노드가 꺼져도 동작하도록 만들어 일반화를 돕는다. 예측할 때는 모든 노드를 켠다.

x₁x₂x₃z₁z₂z₃z₄z₅z₆z₇z₈ŷ₁ŷ₂입력층은닉층 1은닉층 2출력층

회색 노드가 이번 이터레이션에서 꺼진(drop된) 퍼셉트론이다. 다음 이터레이션에는 다른 노드가 꺼진다. PyTorch에서는 nn.Dropout(p=0.5) 층 하나를 끼워 넣으면 된다.


보충 1: 기울기는 어디서 오는가 — 역전파

3장 예제 신경망의 가중치 13개 가운데 출력층으로 들어오는 4개의 편미분은 3장에서 배운 대로 바로 구할 수 있었다. 손실이 그 가중치에 직접 걸려 있기 때문이다. 은닉층의 9개는 다르다. 손실에서 출력층을 거쳐 은닉층까지 층을 되짚어 가야 한다. 그 방법이 역전파다. 층을 되짚는 원리만 보면 되므로, 가장 작은 신경망으로 확인한다. 은닉노드 1개짜리 2층 네트워크다.

w₁w₂xhŷ입력은닉 h = σ(w₁x + b₁)출력 ŷ = w₂h + b₂

\[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}\)로 네 단계를 거쳐 영향을 준다.

연쇄법칙 (Chain Rule)

여러 단계를 거쳐 이어진 함수의 미분은 각 단계의 미분을 곱한 것이다.

\[\frac{\partial \mathcal{L}}{\partial w_1} = \frac{\partial \mathcal{L}}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial h} \cdot \frac{\partial h}{\partial z_1} \cdot \frac{\partial z_1}{\partial w_1}\]

각 단계는 한 줄짜리 미분이다. 손실에서 출발해 뒤에서 앞으로 곱해 나가므로 이 계산을 역전파(backpropagation)라 부른다.

각 화살표의 미분

화살표 함수 미분 이유
\(\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\)의 계수
Sigmoid의 도함수

\[\sigma'(z) = \sigma(z)\,\big(1 - \sigma(z)\big)\]

유도는 이 수업 범위 밖이다. 중요한 것은 쓰기 편하다는 점이다. 순전파에서 이미 계산한 \(h = \sigma(z_1)\)만 있으면 도함수는 \(h(1-h)\)로 바로 나온다. 2장 활성화 함수 그래프의 점선이 이 값이고, 최댓값은 \(z = 0\)에서 0.25다.

✍️ 손으로 따라 계산하기 — 역전파 한 스텝

설정: \(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)
표 4: 순전파 계산
단계 수식 값
가중합 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)
표 5: 역전파 계산 — 뒤에서 앞으로 곱하기
대상 연쇄법칙 전개 값
∂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)
표 6: 파라미터 갱신 후 손실 변화
항목 갱신 전 갱신식 갱신 후
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배”라는 거짓 정보가 생긴다.

원-핫 인코딩 (One-Hot Encoding)

범주형 변수를 범주 수만큼의 0/1 열로 바꾼다. 각 데이터는 자기 범주 열만 1이다. 변수 하나가 입력노드 여러 개가 되므로 입력층의 크기가 바뀐다.

코드
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)
표 7: 원-핫 인코딩 — 브랜드
원래 값 브랜드_삼성 브랜드_애플 브랜드_LG
삼성 1 0 0
애플 0 1 0
LG 0 0 1
애플 0 1 0

스케일 차이: 표준화

나이(20~60)와 연봉(30,000~130,000)이 같은 신경망에 들어가면 연봉 쪽 가중치의 기울기가 압도적으로 커져 학습이 불안정해진다.

표준화 (Standardization)

각 변수를 평균 0, 표준편차 1로 바꾼다.

\[z = \frac{x - \mu}{s}, \qquad s = \sqrt{\frac{1}{n}\sum_{i=1}^{n}(x_i - \mu)^2}\]

표준편차는 \(n\)으로 나눈다 (\(n-1\)이 아니다). numpy.std와 scikit-learn의 StandardScaler가 모두 \(n\)을 쓴다. 교재·도장·시험 전부 \(n\)으로 통일한다.

코드
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)
표 8: 표준화 손계산 — 나이 [24, 30, 36, 42, 48]
나이 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
Min-Max 정규화 (Normalization)

값을 \([0, 1]\)로 눌러 담는 방법. \(x' = \dfrac{x - x_{\min}}{x_{\max} - x_{\min}}\). 이미지 픽셀 \(0 \sim 255\)를 255로 나누어 \(0 \sim 1\)로 만드는 것이 이것이다(5장).

평균·표준편차·최솟값·최댓값은 훈련 데이터에서만 계산한다

테스트 데이터를 섞어 계산하면 모델이 테스트 데이터의 정보를 미리 보는 것이 된다(데이터 누수). 훈련 데이터에서 구한 \(\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\)으로 나눔). 통계량은 훈련 데이터에서만