[Zero to ML Master #06] 경사하강법 완전 정복 — 학습률·발산 조건·선형회귀·자동미분 (딥러닝 수학, 2부)

오톨로리즘·2026년 10월 8일
post-thumbnail

학습률을 0.1에서 1.0으로 바꿨더니 손실이 갑자기 폭발한 경험, 있으신가요? 😱 그 이유는 놀랍게도 한 줄짜리 부등식으로 설명됩니다.

2부에서는 1부에서 만든 그래디언트를 나침반 삼아, 안개 낀 산을 실제로 내려가 봅니다. 경사하강법의 한 줄 공식, 학습률에 따른 수렴·진동·발산, 선형회귀 학습, 그리고 프레임워크의 자동미분까지 이어집니다.

📘 이 글은 위키독스 도서 Zero to 머신러닝 딥러닝 Master 의 00-04 미분·경사하강법 — 안개 낀 산에서 내려오기 (2부) 을 블로그용으로 다듬은 글입니다.
💻 실습 코드: GitHub · 🏠 mldict.net · ✍️ 기술 블로그

💻 이 글의 코드: 00-04-derivative-gradient-descent · Open in Colab — 핵심 코드와 실제 실행 결과는 글 아래 💻 GitHub 실습 섹션에 있습니다.

⬅️ 이전 글: #05 미분 쉽게 이해하기 — 수치미분·연쇄법칙·그래디언트 (딥러닝 수학, 1부) \
➡️ 다음 글: #07 베이즈 정리 쉽게 이해하기 — 확률·조건부 확률과 병원 검사의 함정 (1부)

TL;DR — 3줄 요약

  • 그래디언트의 반대 방향이 손실이 가장 빠르게 줄어드는 방향이고, 그 한 줄이 x = x - lr*gx입니다.
  • 학습률은 감쇠율 ∣1−ηf′′∣|1-\eta f''|로 결정됩니다. 너무 크면 발산, 너무 작으면 느립니다.
  • 경사하강법으로 학습한 선형회귀는 정규방정식과 같은 답에 도달하고, 프레임워크는 미분을 자동미분으로 대신합니다.

🔗 1부 복습: 미분은 "지금 이 순간의 기울기", 연쇄법칙은 "기울기를 거꾸로 곱해 가는 규칙", 그래디언트는 "편미분을 모은 벡터로, 가장 가파르게 올라가는 방향"이었습니다. 1부를 아직 못 보셨다면 #05 미분 쉽게 이해하기부터 읽고 오세요.


📍 4단계 — 경사하강법: 안개 낀 산에서 가장 낮은 곳 찾기

🔎 ① 핵심 정의\
경사하강법(Gradient Descent)은 그래디언트의 반대 방향으로 조금씩 이동하며 함수의 최솟값을 찾는 알고리즘입니다.

❓ 왜 필요한가?\
00-03에서는 정규방정식으로 한 번에 답을 구했습니다. 그런데 신경망의 손실함수는 너무 복잡해 공식으로 답을 구할 수 없습니다. 그럴 때 "조금씩 내려가기"만이 유일한 방법입니다.

⏰ 언제 사용하는가?\
모든 딥러닝 학습. GPT든 이미지 생성 모델이든, 학습 과정은 결국 이 한 줄의 수십억 번 반복입니다.

💜 더 쉽게 이해하기 — 🌫️ 안개 낀 산속의 등산객

👣 시야가 1m도 안 되는 안개 속에서 계곡(가장 낮은 곳)을 찾아야 합니다. 지도도 없고 전체 지형도 볼 수 없습니다. 할 수 있는 건 발밑 경사를 느끼는 것뿐입니다.

① 발밑 경사를 느낀다 (= 그래디언트 계산) → ② 가장 가파른 내리막 방향을 잡는다 (= −∇f-\nabla f) → ③ 한 걸음 이동한다 (= 학습률만큼) → ④ 반복 → ⑤ 평평해지면 멈춘다 (= 기울기 ≈ 0)

🎒 여기서 보폭이 중요합니다. 종종걸음이면 해 지도록 도착 못 하고 🐢, 성큼성큼 뛰면 계곡을 건너뛰어 반대편 산비탈로 날아갑니다 💥. 이 보폭이 바로 학습률입니다.

🔬 수학공식 ⑦ — 경사하강법 업데이트 식 ⭐

1변수 버전

xk+1=xk−η f′(xk)x_{k+1} = x_k - \eta\, f'(x_k)

다변수(벡터) 버전 — 신경망은 이쪽입니다

wk+1=wk−η ∇L(wk)\mathbf{w}_{k+1} = \mathbf{w}_k - \eta\, \nabla L(\mathbf{w}_k)

변수 정의\
• xkx_k / wk\mathbf{w}_k : kk번째 스텝에서의 위치(가중치)\
• η\eta (에타) : 학습률(learning rate) — 한 걸음의 보폭, 보통 0.001 ~ 0.5\
• f′(xk)f'(x_k) / ∇L\nabla L : 현재 위치의 기울기(그래디언트)\
• 마이너스 부호 : "기울기의 반대로 간다"는 뜻 — 이 부호가 전부입니다 ⚠️\
\
왜 마이너스인가? — 부호로 이해하기\
• f′(x)>0f'(x)>0 (오른쪽이 오르막) → −ηf′(x)<0-\eta f'(x)<0 → 왼쪽으로 이동 ✅\
• f′(x)<0f'(x)<0 (오른쪽이 내리막) → −ηf′(x)>0-\eta f'(x)>0 → 오른쪽으로 이동 ✅\
• f′(x)=0f'(x)=0 (평평) → 이동량 0 → 정지 🏁\
어느 경우든 낮은 쪽으로 갑니다. 부호 하나가 알고리즘 전체를 굴러가게 만듭니다.

🔢 진행 과정 — 5단계 알고리즘

  1. 시작점 x0x_0와 학습률 η\eta를 정한다 (초기화)
  2. 현재 위치의 기울기 f′(xk)f'(x_k)를 계산한다
  3. xk+1=xk−ηf′(xk)x_{k+1} = x_k - \eta f'(x_k)로 이동한다
  4. 기울기가 충분히 0에 가깝거나, 정해진 횟수에 도달하면 멈춘다
  5. 아니면 2번으로 돌아간다 🔁

📐 시각화 ⑤ — 경사하강법 알고리즘 흐름도

📐 시각화 ⑤ — 경사하강법 알고리즘 흐름도

🧮 단계별 손 계산 — 직접 6스텝 굴려보기\
f(x)=x2−4x+5f(x)=x^2-4x+5, 도함수 f′(x)=2x−4f'(x)=2x-4, 시작점 x0=10x_0=10, 학습률 η=0.1\eta=0.1입니다. 참고로 이 함수의 진짜 최솟값은 f′(x)=0f'(x)=0 → 2x−4=02x-4=0 → x=2x=2, f(2)=1f(2)=1입니다.

스텝 kk현재 xkx_k기울기 f′=2x−4f'=2x-4이동량 −ηf′-\eta f'새 xk+1x_{k+1}f(xk+1)f(x_{k+1})목표까지 x−2x-2
0→110.000016.0000−1.60008.400041.96006.4000
1→28.400012.8000−1.28007.120027.21445.1200
2→37.120010.2400−1.02406.096017.77724.0960
3→46.09608.1920−0.81925.276811.73743.2768
4→55.27686.5536−0.65544.62147.87192.6214
5→64.62145.2429−0.52434.09725.39802.0972

👀 표에서 세 가지를 발견할 수 있습니다.\
① 기울기가 16 → 12.8 → 10.24 → … 로 매번 정확히 0.8배씩 줄어듭니다.\
② 따라서 이동량도 점점 작아져, 계곡에 가까워질수록 저절로 조심스럽게 걷습니다. 🐾\
③ 마지막 열 "x−2x-2"도 6.4 → 5.12 → 4.096 → … 로 정확히 0.8배씩 줄어듭니다.\
이 "0.8"이라는 숫자는 어디서 온 걸까요? 바로 아래에서 유도합니다. 🔍

🔬 수학공식 ⑧ — 발산 조건 직접 유도하기 🔴⭐ (이 장의 하이라이트)

"학습률이 크면 발산한다"는 말을 막연한 조언이 아니라 정확한 부등식으로 만들어봅시다.\
\
① 업데이트 식에 f′(x)=2x−4f'(x)=2x-4 대입

xk+1=xk−η(2xk−4)x_{k+1} = x_k - \eta(2x_k - 4)

② 목표 지점 x=2x=2로부터의 "오차" ek=xk−2e_k = x_k - 2를 정의\
양변에서 2를 빼줍니다.

xk+1−2=xk−2−η(2xk−4)x_{k+1} - 2 = x_k - 2 - \eta(2x_k - 4)

③ 오른쪽의 2xk−42x_k-4를 2(xk−2)2(x_k-2)로 묶기

ek+1=ek−2η eke_{k+1} = e_k - 2\eta\, e_k

④ eke_k로 묶어내기

  ek+1=(1−2η) ek  \boxed{\;e_{k+1} = (1-2\eta)\,e_k\;}

⑤ 해석 — 오차가 매 스텝 (1−2η)(1-2\eta)배씩 곱해집니다. kk번 반복하면:

ek=(1−2η)k e0e_k = (1-2\eta)^k\, e_0

⑥ 수렴 조건 — 오차가 0으로 가려면 곱해지는 수의 크기가 1보다 작아야 합니다.

∣1−2η∣<1⟺  0<η<1  |1-2\eta| < 1 \quad\Longleftrightarrow\quad \boxed{\;0 < \eta < 1\;}

💡 이 부등식이 알려주는 것 — 표로 정리

η\eta∣1−2η∣\vert 1-2\eta\vert예측30스텝 후 실제 xx
0.010.98🐢 아주 느리게 수렴6.3639 (아직 멀다)
0.10.80👍 안정적 수렴2.0099
0.50.00🚀 단 1스텝에 정답!2.0000 (완벽)
0.90.80🔄 0.1과 같은 속도! (부호만 진동)2.0099
1.01.00🔁 영원히 제자리 진동10.0000 (출발점!)
1.11.20💥 발산1901.0105 (폭주 중)

🤯 놀라운 세 가지 발견\
① η=0.5\eta=0.5에서 ∣1−2η∣=0|1-2\eta|=0 → 오차가 한 번에 0이 됩니다. 실제로 x1=10−0.5×16=2x_1 = 10 - 0.5\times16 = 2, 정답에 1스텝만에 도착합니다! 🎯\
② η=0.9\eta=0.9와 η=0.1\eta=0.1이 같은 속도 — ∣1−1.8∣=∣1−0.2∣=0.8|1-1.8|=|1-0.2|=0.8로 같기 때문입니다. 단 0.9는 (1−1.8)=−0.8(1-1.8)=-0.8이 음수라 목표 지점을 좌우로 넘나들며(진동하며) 수렴합니다. 🔄\
③ η=1.0\eta=1.0은 정확히 경계 — ∣1−2∣=1|1-2|=1이라 오차 크기가 전혀 안 줄고, 10→−6→10→−610 \to -6 \to 10 \to -6 무한 반복입니다. 그래서 30스텝 뒤에도 출발점 10 그대로입니다! 😵\
수식이 실험 결과를 완벽하게 예측했습니다. 이것이 수학을 배우는 이유입니다. ✨

🚀 일반화 — 왜 딥러닝은 학습률을 0.001처럼 작게 쓸까?

위 함수는 f′′(x)=2f''(x)=2였고, 조건은 0<η<10<\eta<1 즉 η<2f′′\eta < \frac{2}{f''}였습니다. 일반적으로 2차 미분(곡률)이 클수록 허용 학습률은 작아집니다.

η<2L(L=곡률의 최댓값)\eta < \frac{2}{L} \quad (L = \text{곡률의 최댓값})

신경망의 손실면은 방향마다 곡률이 제각각이고 어떤 방향은 매우 뾰족합니다. 가장 뾰족한 방향에 맞춰야 하므로 학습률이 0.001처럼 작아지는 것입니다. Adam 같은 옵티마이저는 방향마다 학습률을 자동 조절해 이 문제를 완화합니다. 🛠️

📐 시각화 ⑥ — 실제 이동 경로 (손계산 표와 동일한 값)

📐 시각화 ⑥ — 실제 이동 경로 (손계산 표와 동일한 값)

🛠️ 실습 4 🔵

① f(x)=(x−3)2+2f(x)=(x-3)^2+2에 대해 x0=0x_0=0, η=0.2\eta=0.2로 손으로 3스텝 계산하세요. (힌트: f′(x)=2(x−3)f'(x)=2(x-3))\
② 이 함수의 수렴 조건을 유도 ⑧의 방식으로 구하세요. (정답: ek+1=(1−2η)eke_{k+1}=(1-2\eta)e_k, 동일하게 0<η<10<\eta<1)\
③ f(x)=x2f(x)=x^2 (즉 f′′=2f''=2)에서 η=1\eta=1로 두면 어떤 일이 벌어지는지 손으로 확인하세요. (힌트: xk+1=−xkx_{k+1}=-x_k)

📍 5단계 — 코드로 구현하고 실전에 적용하기

🪜 진행 과정: ① 목표함수·도함수 정의 → ② 시작점·학습률 설정 → ③ 반복 루프에서 업데이트 → ④ 이력 기록 → ⑤ 이론 예측과 비교 검증

이번 단계에서는 세 개의 코드를 차례로 만듭니다. 1변수 → 다변수 → 실전 선형회귀 순으로 확장하며, 매번 수식이 어느 코드 줄이 되었는지 확인합니다.

💻 코드 A — 기본 경사하강법 + 이론 검증

🐍 Python — 경사하강법 30회 + 유도한 공식으로 검증

def f(x):
    """목표 함수 f(x) = x² - 4x + 5  (최솟값: x=2 에서 f(2)=1)"""
    return x**2 - 4*x + 5

def df(x):
    """도함수 f'(x) = 2x - 4  ← 2단계에서 유도한 공식 그대로"""
    return 2*x - 4

def gradient_descent(df, x_init, lr, n_steps):
    """
    경사하강법 본체.  수식:  x ← x - η·f'(x)

    비유: 안개 속 등산객이 발밑 경사(df)를 보고
    보폭(lr)만큼 n_steps 번 내려갑니다.
    """
    x = x_init
    history = [x]                       # 이동 경로를 기록 (나중에 그래프용)

    for step in range(n_steps):
        gx = df(x)                      # ① 기울기 계산 (기울기는 관례상 g 접두)
        x = x - lr * gx               # ② ★핵심★ 수식 x ← x - η·f'(x) 그 자체
        history.append(x)               # ③ 기록

        if step < 3 or (step + 1) % 10 == 0:   # 처음 3번 + 10번마다 출력
            print(f"step {step+1:2d}: grad={gx:9.4f}  x={x:9.4f}  f(x)={f(x):9.4f}")

    return x, history

x_final, hist = gradient_descent(df, x_init=10.0, lr=0.1, n_steps=30)

print(f"\n최종 x={x_final:.6f}  f(x)={f(x_final):.6f}   (정답 x=2, f=1)")

# ★ 4단계에서 유도한 공식 e_k = (1-2η)^k · e_0 으로 결과를 예측해봅니다
#   e_0 = 10 - 2 = 8,  (1-2×0.1) = 0.8,  k = 30
predicted = 8 * (0.8 ** 30)
print(f"이론 예측 x-2 = 8*(0.8)^30 = {predicted:.6f}, 실제 = {x_final-2:.6f}")

🔗 GitHub에서 코드 보기 — 00_04_derivative_gradient_descent.py L187–L222

📘 코드 이해 — 수식 한 줄이 코드 한 줄

수학공식파이썬 코드설명
f(x)=x2−4x+5f(x)=x^2-4x+5return x**2 - 4*x + 5목표함수를 1:1로 옮김
f′(x)=2x−4f'(x)=2x-4return 2*x - 42단계 규칙으로 유도한 결과
g=f′(xk)g = f'(x_k)gx = df(x)기울기 변수는 지시서 규칙대로 gx
xk+1=xk−ηgx_{k+1}=x_k-\eta gx = x - lr * gx⭐ 알고리즘 전체가 이 한 줄
ek=(1−2η)ke0e_k=(1-2\eta)^k e_08 * (0.8 ** 30)유도한 이론으로 결과를 미리 예측

🧩 코드 설명 — Python 기초

• 키워드 인자 gradient_descent(df, x_init=10.0, lr=0.1, n_steps=30) — 이름을 붙여 넘기면 순서를 헷갈릴 일이 없고, 읽는 사람도 무엇이 학습률인지 바로 압니다.\
\
• 여러 값 반환 return x, history — 파이썬은 튜플로 여러 값을 한 번에 돌려줍니다. 받을 때는 x_final, hist = ...로 풀어 받습니다.\
\
• % 나머지 연산자 — (step+1) % 10 == 0은 "10의 배수 스텝일 때만"이라는 뜻입니다. 30줄을 다 찍으면 너무 길어서 쓰는 로그 관용구입니다.\
\
• 리스트 append — history.append(x)로 매 스텝 값을 쌓아둡니다. 나중에 matplotlib으로 수렴 곡선을 그릴 때 그대로 씁니다.\
\
• 변수명 규칙 — 지시서에 따라 기울기는 gx, gW, gb 형태로 씁니다. 수식의 η\eta는 코드에서 lr(learning rate)로 통일합니다.

✅🟩 실행 결과

step  1: grad=  16.0000  x=   8.4000  f(x)=  41.9600
step  2: grad=  12.8000  x=   7.1200  f(x)=  27.2144
step  3: grad=  10.2400  x=   6.0960  f(x)=  17.7772
step 10: grad=   2.1475  x=   2.8590  f(x)=   1.7379
step 20: grad=   0.2306  x=   2.0922  f(x)=   1.0085
step 30: grad=   0.0248  x=   2.0099  f(x)=   1.0001

최종 x=2.009904  f(x)=1.000098   (정답 x=2, f=1)
이론 예측 x-2 = 8*(0.8)^30 = 0.009904, 실제 = 0.009904

🎯 가장 중요한 줄은 마지막 줄입니다. 4단계에서 손으로 유도한 ek=(1−2η)ke0e_k=(1-2\eta)^k e_0가 예측한 0.009904와, 코드가 30번 반복해서 얻은 실제값 0.009904가 소수점 6자리까지 완전히 일치합니다. 손계산 표(1~6스텝)의 값들도 그대로 재현되었습니다. 수학 → 코드 → 검증 고리가 완성된 것입니다. ✨

💻 코드 B — 발산 조건 ∣1−2η∣<1|1-2\eta|<1을 실험으로 확인

🐍 Python — 이론과 실험 대조

# 4단계에서 유도한 조건:  |1 - 2η| < 1  ⟺  0 < η < 1
# 이 예측이 정말 맞는지 여섯 가지 학습률로 확인합니다
for lr in [0.01, 0.1, 0.5, 0.9, 1.0, 1.1]:
    x = 10.0
    diverged = False

    for step in range(30):
        x = x - lr * df(x)
        if abs(x) > 1e10:        # 발산 감지 (안 하면 inf/overflow 경고 발생)
            diverged = True
            break

    ratio = abs(1 - 2 * lr)          # 유도한 감쇠율 |1-2η|
    result = "발산 💥" if diverged else f"{x:10.4f}"
    print(f"η={lr:<5} |1-2η|={ratio:.2f}  30스텝 후 x = {result}")

🔗 GitHub에서 코드 보기 — 00_04_derivative_gradient_descent.py L226–L240

✅🟩 실행 결과

η=0.01  |1-2η|=0.98  30스텝 후 x =     6.3639   🐢 너무 느림
η=0.1   |1-2η|=0.80  30스텝 후 x =     2.0099   👍 적당
η=0.5   |1-2η|=0.00  30스텝 후 x =     2.0000   🚀 1스텝 만에 정답
η=0.9   |1-2η|=0.80  30스텝 후 x =     2.0099   🔄 0.1과 동일 (진동하며 수렴)
η=1.0   |1-2η|=1.00  30스텝 후 x =    10.0000   😵 제자리 진동 (출발점 그대로!)
η=1.1   |1-2η|=1.20  30스텝 후 x =  1901.0105   💥 폭주 중

📌 실험이 이론과 정확히 일치합니다. ∣1−2η∣|1-2\eta|가 작을수록 빠르고, 1에 도달하면 멈추지 못하며, 1을 넘으면 발산합니다. 특히 η=1.0\eta=1.0에서 30번을 돌리고도 출발점 10.0으로 되돌아온 것이 인상적입니다 — 10→−6→10→−6⋯10 \to -6 \to 10 \to -6 \cdots 짝수 번 반복이라 제자리인 것이죠. 😵

💻 코드 C — 다변수로 확장 (벡터 한 줄이면 끝)

🐍 Python — 2변수 경사하강법

import numpy as np

def grad_circle(p):
    """f(x,y)=x²+y² 의 그래디언트 ∇f=(2x, 2y) — 3단계에서 유도"""
    return 2 * p              # 벡터 전체에 2를 곱하면 (2x, 2y) 완성!

p = np.array([3.0, 4.0])   # 시작점 (float 필수!)
lr = 0.1

for step in range(30):
    gp = grad_circle(p)              # 그래디언트 (벡터)
    p = p - lr * gp               # ★ 1변수와 완전히 같은 한 줄! (벡터 연산)
    if step < 2 or (step + 1) % 10 == 0:
        print(f"step {step+1:2d}: p={np.round(p,6)}  f={float((p**2).sum()):.8f}")

🔗 GitHub에서 코드 보기 — 00_04_derivative_gradient_descent.py L244–L257

✅🟩 실행 결과

step  1: p=[2.4 3.2]  f=16.00000000
step  2: p=[1.92 2.56]  f=10.24000000
step 10: p=[0.322123 0.429497]  f=0.28823038
step 20: p=[0.034588 0.046117]  f=0.00332307
step 30: p=[0.003714 0.004952]  f=0.00003831

🎉 코드가 1변수 때와 똑같습니다. x = x - lr*gx가 p = p - lr*gp로 바뀌었을 뿐인데, NumPy의 벡터 연산 덕분에 변수가 2개든 200만 개든 코드는 그대로입니다. 이것이 딥러닝이 수십억 개 파라미터를 다룰 수 있는 이유입니다. 🚀\
📌 참고로 (3,4)(3,4)가 (2.4,3.2)(2.4, 3.2)가 된 것은 둘 다 0.8배된 것이고, 이는 1변수에서 유도한 (1−2η)=0.8(1-2\eta)=0.8과 정확히 같습니다. 비율 3:43:4가 유지되므로 원점을 향해 직선으로 내려갑니다. 📐

🚀 실전 응용 — 00-03의 선형회귀를 경사하강법으로 다시 풀기

00-03에서 정규방정식 x^=(A⊤A)−1A⊤b\hat{\mathbf{x}}=(A^{\top}A)^{-1}A^{\top}\mathbf{b}로 한 번에 구했던 답을, 이번엔 한 걸음씩 내려가서 구해봅니다. 두 방법이 같은 답에 도달한다면, 우리가 배운 것이 모두 맞았다는 강력한 증거가 됩니다. 🎯

데이터는 00-03과 동일: (1,2)(1,2), (2,3)(2,3), (3,5)(3,5) — 정규방정식 정답은 w=1.5w=1.5, b=1/3≈0.3333b=1/3\approx0.3333이었습니다.

🔬 수학공식 ⑨ — MSE 손실의 그래디언트 (3단계 예제 ③ 재등장)

손실함수 (MSE)

L(w,b)=1m∑i=1m(wxi+b−yi⏟ei)2L(w,b) = \frac{1}{m}\sum_{i=1}^{m}\bigl(\underbrace{w x_i + b - y_i}_{e_i}\bigr)^2

연쇄법칙으로 편미분 — 바깥 e2→2ee^2 \to 2e, 안쪽 ∂ei∂w=xi\frac{\partial e_i}{\partial w}=x_i, ∂ei∂b=1\frac{\partial e_i}{\partial b}=1

∂L∂w=2m∑i=1meixi∂L∂b=2m∑i=1mei\frac{\partial L}{\partial w} = \frac{2}{m}\sum_{i=1}^{m} e_i x_i \qquad\qquad \frac{\partial L}{\partial b} = \frac{2}{m}\sum_{i=1}^{m} e_i

업데이트 식

w←w−η∂L∂w,b←b−η∂L∂bw \leftarrow w - \eta\frac{\partial L}{\partial w}, \qquad b \leftarrow b - \eta\frac{\partial L}{\partial b}

변수 정의: mm 데이터 개수, xi,yix_i,y_i ii번째 입력·정답, ww 기울기(가중치), bb 절편(편향), eie_i 잔차\
\
📌 00-03과의 연결: 00-03에서 배운 "잔차의 합이 0"이라는 성질은 ∂L∂b=0\frac{\partial L}{\partial b}=0과 같은 말입니다 — 학습이 끝나면 2m∑ei=0\frac{2}{m}\sum e_i = 0이 되니까요! 🔗

🐍 Python — 선형회귀를 경사하강법으로 학습시키기

import numpy as np

# 00-03과 동일한 데이터: (1,2), (2,3), (3,5)
X = np.array([1.0, 2.0, 3.0])
Y = np.array([2.0, 3.0, 5.0])
m = len(X)                    # 데이터 개수

w, b = 0.0, 0.0            # 파라미터 초기화 (아무 값이나 OK)
lr = 0.1

for step in range(1, 1001):
    # ── ① 순전파: 예측하고 오차 구하기 ─────────────────
    pred = w * X + b            # ŷ = wx + b  (벡터 3개 한 번에)
    e = pred - Y                 # 잔차 e = ŷ - y

    # ── ② 역전파: 공식 ⑨를 그대로 코드로 ───────────────
    gw = (2 / m) * np.sum(e * X)   # ∂L/∂w = (2/m)Σ eᵢxᵢ
    gb = (2 / m) * np.sum(e)       # ∂L/∂b = (2/m)Σ eᵢ

    # ── ③ 업데이트: 경사하강 한 걸음 ───────────────────
    w = w - lr * gw
    b = b - lr * gb

    if step in (1, 10, 100, 500, 1000):
        mse = np.mean((w * X + b - Y) ** 2)
        print(f"step {step:4d}: w={w:.6f} b={b:.6f} MSE={mse:.6f}")

# ── ④ 00-03의 정규방정식 답과 비교 검증 ────────────────
A = np.column_stack([X, np.ones(m)])
exact = np.linalg.lstsq(A, Y, rcond=None)[0]
print("정규방정식(00-03) 정답:", np.round(exact, 6))

🔗 GitHub에서 코드 보기 — 00_04_derivative_gradient_descent.py L261–L291

📘 코드 이해 — 공식 ⑨가 코드의 어느 줄인가

수학공식코드단계
y^i=wxi+b\hat{y}_i = wx_i+bpred = w * X + b순전파 — 브로드캐스트로 3개 동시 계산
ei=y^i−yie_i = \hat{y}_i - y_ie = pred - Y잔차 (00-03에서 배운 개념)
2m∑eixi\frac{2}{m}\sum e_i x_igw = (2/m)*np.sum(e*X)∑\sum → np.sum, 곱 → *
2m∑ei\frac{2}{m}\sum e_igb = (2/m)*np.sum(e)xix_i가 없는 이유는 ∂e/∂b=1\partial e/\partial b=1이라서
w←w−η∂L∂ww \leftarrow w-\eta\frac{\partial L}{\partial w}w = w - lr * gw4단계 업데이트 식 그대로

🧩 코드 설명 — NumPy 기초

• 브로드캐스트 w * X + b — X가 원소 3개짜리 벡터이고 w, b는 숫자 하나인데, NumPy가 알아서 3개 데이터에 동시에 적용합니다. 반복문이 필요 없습니다. (00-01장 심화에서 배운 그 브로드캐스트입니다)\
\
• e * X — 벡터끼리의 *는 성분별 곱이므로 [e1x1,e2x2,e3x3][e_1x_1, e_2x_2, e_3x_3]이 됩니다. 여기에 np.sum을 씌우면 ∑eixi\sum e_ix_i가 완성됩니다. 사실 이것은 00-01장에서 배운 내적과 같아서, np.dot(e, X)로 써도 똑같습니다! 🔗\
\
• step in (1, 10, 100, 500, 1000) — 튜플 안에 있는지 검사하는 문법입니다. 특정 스텝에서만 로그를 남길 때 편합니다.\
\
• 왜 w, b = 0.0, 0.0부터 시작하나? 어디서 출발하든 같은 골짜기로 굴러갑니다. MSE 손실은 그릇 모양(볼록함수)이라 지역 최솟값이 없기 때문입니다. 🥣

✅🟩 실행 결과

step    1: w=1.533333 b=0.666667 MSE=0.216296
step   10: w=1.405646 b=0.547822 MSE=0.062155
step  100: w=1.489440 b=0.357339 MSE=0.055638
step  500: w=1.499999 b=0.333335 MSE=0.055556
step 1000: w=1.500000 b=0.333333 MSE=0.055556
정규방정식(00-03) 정답: [1.5      0.333333]

🏆 이 장의 결승점입니다. 경사하강법으로 1000번 굴러 도착한 w=1.500000w=1.500000, b=0.333333b=0.333333이, 00-03에서 공식 한 방으로 구한 답과 소수점 6자리까지 완전히 같습니다. 서로 완전히 다른 두 길(대수적 공식 vs 반복적 하강)이 같은 목적지에 도착한 것입니다. 🎯

📌 MSE 값도 주목하세요. 0.055556으로 수렴했는데, 이는 118\frac{1}{18}이며 00-03에서 구한 오차 제곱합 16\frac{1}{6}을 데이터 개수 3으로 나눈 값입니다 (1/63=118\frac{1/6}{3}=\frac{1}{18}). 같은 수치가 두 장에 걸쳐 다시 나타났습니다. 🔗

🛠️ 실습 5 🔴🚀

① 학습률을 [0.001, 0.05, 0.3, 0.7]로 바꿔가며 1000스텝 후 결과를 표로 정리하고, 어디서부터 불안정해지는지 찾아보세요.\
② 00-03의 키·몸무게 데이터(165~185cm)로 같은 코드를 돌려보세요. ⚠️ 아마 발산할 것입니다! 왜 그런지 생각해보고, XX를 (X - X.mean())/X.std()로 정규화한 뒤 다시 시도해보세요. (힌트: 입력값이 크면 그래디언트도 커집니다 — 실무에서 데이터 정규화가 필수인 이유)\
③ MSE가 매 스텝 어떻게 줄어드는지 리스트에 저장하고, matplotlib으로 학습 곡선을 그려보세요. 📉

📍 6단계 — 프레임워크 연결: 자동미분

🔎 ① 핵심 정의\
자동미분(automatic differentiation, autograd)은 우리가 손으로 유도했던 도함수를, 프레임워크가 계산 과정을 기록해뒀다가 자동으로 만들어주는 기술입니다.

❓ 왜 필요한가?\
층이 100개인 신경망의 도함수를 손으로 유도하는 것은 불가능합니다. 게다가 모델 구조를 바꿀 때마다 다시 유도해야 한다면 실험 자체가 불가능해집니다.

⏰ 언제 사용하는가?\
사실상 모든 딥러닝 코드에서. 우리가 gw = (2/m)*np.sum(e*X)라고 손수 쓴 줄이, loss.backward() 한 줄로 대체됩니다.

💜 더 쉽게 이해하기 — 📹 자동차 블랙박스

자동차 블랙박스는 주행 중 일어난 모든 일을 순서대로 녹화합니다. 사고가 나면 녹화본을 거꾸로 되감아 원인을 추적하죠. 📹

오토그라드도 똑같습니다. 순전파 때 "어떤 연산이 어떤 순서로 일어났는지"를 계산 그래프로 녹화해두고, backward()를 부르면 녹화본을 거꾸로 재생하며 연쇄법칙을 순서대로 곱해 나갑니다. 2단계에서 배운 그 연쇄법칙이 여기서 자동으로 실행되는 것입니다. ⚙️

🔀 프레임워크 관점 — 우리가 손으로 쓴 줄이 어디로 갔나

개념NumPy (우리 코드)DeZeroPyTorchTensorFlowJAX
미분 계산df(x) 손수 작성y.backward()loss.backward()tape.gradient()jax.grad(f)
기울기 저장변수 gx로 직접x.gradx.gradtape가 반환함수가 반환
업데이트x = x - lr*gxoptimizers.SGDoptimizer.step()apply_gradients직접 갱신
기울기 초기화불필요 (매번 새로 계산)cleargrads()zero_grad() ⚠️불필요불필요 (순수함수)
GPU❌일부✅✅✅

🐍 Python — 같은 문제를 세 프레임워크로

# ══════════ ① PyTorch ══════════
import torch

x = torch.tensor(10.0, requires_grad=True)   # 미분 대상으로 등록(=녹화 시작)
optimizer = torch.optim.SGD([x], lr=0.1)         # SGD = 우리가 만든 그 경사하강법

for step in range(30):
    loss = x**2 - 4*x + 5      # 순전파 (계산 과정이 녹화됨)
    optimizer.zero_grad()          # ⚠️ 이전 기울기 초기화 (안 하면 누적!)
    loss.backward()                # 역전파 → x.grad 에 2x-4 가 자동으로 채워짐
    optimizer.step()               # x = x - lr*x.grad 를 자동 수행

print("PyTorch  :", round(x.item(), 6))    # 2.009904 — 우리 코드와 동일!

# ══════════ ② TensorFlow ══════════
import tensorflow as tf

x = tf.Variable(10.0)
opt = tf.optimizers.SGD(learning_rate=0.1)

for step in range(30):
    with tf.GradientTape() as tape:      # '테이프'에 녹화 (블랙박스 비유 그대로!)
        loss = x**2 - 4*x + 5
    grads = tape.gradient(loss, [x])       # 되감아서 기울기 추출
    opt.apply_gradients(zip(grads, [x]))    # 업데이트

print("TensorFlow:", round(float(x.numpy()), 6))

# ══════════ ③ JAX ══════════
import jax, jax.numpy as jnp

def loss_fn(x):
    """손실함수를 '순수 함수'로 정의 — JAX 스타일"""
    return x**2 - 4*x + 5

grad_fn = jax.grad(loss_fn)        # ★ 도함수 '함수'를 통째로 만들어줌!

x = 10.0
for step in range(30):
    x = x - 0.1 * grad_fn(x)   # 우리가 손으로 쓴 코드와 형태가 똑같음!

print("JAX      :", round(float(x), 6))

🔗 GitHub에서 코드 보기 — 00_04_derivative_gradient_descent.py L295–L336

✅🟩 실행 결과 (세 프레임워크 모두 동일)

PyTorch  : 2.009904
TensorFlow: 2.009904
JAX      : 2.009904

# 참고 — 우리가 NumPy로 손수 만든 코드 A의 결과: 2.009904
# 그리고 손으로 유도한 이론 예측: 2 + 8×(0.8)^30 = 2.009904

🏆 네 가지 구현과 하나의 이론이 모두 같은 값에 도달했습니다. 프레임워크가 마법을 부리는 것이 아니라, 우리가 손으로 한 계산을 그대로 자동화했을 뿐이라는 것을 보여줍니다. 원리를 알면 어떤 프레임워크로 갈아타도 두렵지 않습니다. 💪

🧩 코드 설명 — 프레임워크 문법 기초

• requires_grad=True — "이 텐서는 미분 대상이니 계산 과정을 녹화하라"는 표시입니다. 이게 없으면 x.grad가 None입니다.\
\
• with tf.GradientTape() as tape: — with 블록 안쪽에서 일어난 연산만 녹화합니다. 블록 밖에서 계산한 손실은 미분할 수 없습니다.\
\
• jax.grad(loss_fn) — 가장 인상적인 문법입니다. 값이 아니라 "도함수 함수" 자체를 만들어 돌려줍니다. 수학에서 f→f′f \to f'로 가는 연산을 코드로 그대로 표현한 것이죠. 🤯\
\
• x.item() — 원소가 하나인 텐서에서 순수 파이썬 숫자를 꺼냅니다. print(x)하면 tensor(2.0099, requires_grad=True)처럼 지저분하게 나오기 때문입니다.\
\
• zip(grads, [x]) — 기울기와 변수를 짝지어 묶습니다. TensorFlow는 (기울기, 변수) 쌍의 리스트를 요구합니다.

🐞 실수하기 쉬운 부분 — PyTorch의 zero_grad()

❌ 왜 틀리나? optimizer.zero_grad()를 빼면 학습이 이상하게 빨라지거나 발산합니다.\
❌ 원인: PyTorch는 backward()를 부를 때 기울기를 덮어쓰지 않고 더합니다(누적). 초기화를 안 하면 2번째 스텝의 기울기는 1+2번 합, 3번째는 1+2+3번 합이 되어 사실상 학습률이 계속 커지는 효과가 납니다.\
❓ 왜 그런 설계인가? 메모리가 부족해 큰 배치를 쪼개 여러 번 backward()한 뒤 한 번에 업데이트하는 기법(gradient accumulation)을 지원하기 위해서입니다. 즉 기능이지 버그가 아닙니다.\
✅ 수정: 매 스텝 시작에 optimizer.zero_grad()를 반드시 호출하세요. 순서는 zero_grad → backward → step입니다.

🛠️ 실습 6 🔴

① PyTorch 코드에서 zero_grad()를 주석 처리하고 돌려보세요. 결과가 어떻게 달라지나요?\
② 앞의 선형회귀(코드 D)를 PyTorch로 다시 작성하고, NumPy 결과 w=1.5, b=0.3333w=1.5,\,b=0.3333과 비교하세요.\
③ jax.grad(jax.grad(loss_fn))로 2차 미분을 구해보세요. (정답: f′′(x)=2f''(x)=2) 🤓

🔴 심화 — 지역 최솟값과 모멘텀

지금까지 다룬 f(x)=x2−4x+5f(x)=x^2-4x+5나 MSE는 골짜기가 하나뿐인 그릇 모양(볼록함수)이라, 어디서 출발하든 반드시 전역 최솟값에 도착했습니다. 🥣

그런데 실제 신경망의 손실면은 울퉁불퉁한 산맥입니다. 얕은 웅덩이(지역 최솟값)에 빠지면, 주변이 다 오르막이라 기울기가 0이 되어 더 깊은 계곡이 옆에 있어도 멈춰버립니다. 🕳️

💜 더 쉽게 이해하기 — 🎳 볼링공과 관성

경사하강법은 매 순간 멈춰서 경사를 재는 신중한 등산객입니다. 작은 웅덩이에 들어가면 그대로 주저앉습니다.

반면 모멘텀(Momentum)은 등산객 대신 굴러가는 볼링공입니다. 🎳 이전에 굴러온 관성이 있어서 얕은 웅덩이는 탄력으로 넘어가 더 깊은 골짜기까지 갑니다.

🔬 수학공식 ⑩ — 모멘텀 (맛보기)

vk+1=βvk−ηf′(xk),xk+1=xk+vk+1v_{k+1} = \beta v_k - \eta f'(x_k), \qquad x_{k+1} = x_k + v_{k+1}

변수 정의: vv 는 속도(velocity), β\beta 는 관성 계수(보통 0.9)\
기존 경사하강법은 β=0\beta=0인 특수한 경우입니다. β\beta가 클수록 이전 방향을 더 오래 기억합니다.\
코드로는 v = beta*v - lr*gx / x = x + v 두 줄뿐입니다. 자세한 내용은 이후 권에서 다룹니다. 🚀

📌 다행히도 실제 딥러닝에서는 지역 최솟값이 생각만큼 큰 문제가 아니라는 연구가 많습니다. 차원이 아주 높으면 "모든 방향이 오르막인 지점"이 확률적으로 드물기 때문입니다. 오히려 안장점(saddle point)이 더 흔한 장애물이며, 모멘텀·Adam이 이를 잘 빠져나옵니다. 🐎

⚠️ 실수하기 쉬운 부분 — 종합

❌ 실수 1 — 부호를 반대로\
왜 틀리나? x = x + lr*gx로 쓰면 오르막으로 올라가 손실이 폭증합니다. 원인: "경사하강"이 빼기라는 것을 잊음. 수정: 반드시 x = x - lr*gx. 증상: 손실이 첫 스텝부터 커지면 100% 이 문제입니다.

❌ 실수 2 — 학습률 과다\
왜 틀리나? 손실이 nan이나 inf로 튑니다. 원인: ∣1−ηL∣>1|1-\eta L|>1이 되어 발산. 수정: 학습률을 10분의 1로 줄여 재시도. 그래도 안 되면 또 10분의 1.

❌ 실수 3 — 입력 스케일 미조정 ⭐\
왜 틀리나? 키(165~185) 같은 큰 값을 그대로 넣으면 그래디언트가 수천이 되어 발산합니다. 원인: ∂L∂w=2m∑eixi\frac{\partial L}{\partial w}=\frac{2}{m}\sum e_ix_i에서 xix_i가 크면 기울기도 커짐. 수정: X = (X - X.mean())/X.std()로 정규화. 실무에서 데이터 정규화가 필수인 이유가 바로 이것입니다.

❌ 실수 4 — 정수 배열\
np.array([3, 4])는 정수형이라 미세한 갱신이 잘려버립니다. 그래디언트가 계속 0이면 print(p.dtype)부터 확인하세요. 수정: np.array([3.0, 4.0]).

❌ 실수 5 — 연쇄법칙 안쪽 미분 누락\
(3x2+1)4(3x^2+1)^4를 4(3x2+1)34(3x^2+1)^3까지만 쓰고 ×6x\times 6x를 잊는 실수. 수정: check_grad로 항상 검증하세요.

❌ 실수 6 — zero_grad() 누락\
PyTorch에서 기울기가 누적되어 사실상 학습률이 커집니다. 순서: zero_grad → backward → step.

🐞 디버깅 팁 — 증상별 처방전

증상의심할 원인확인 방법
💥 손실이 inf/nan학습률 과다, 입력 스케일lr을 1/10로, X.std() 출력
📈 손실이 계속 증가부호 오류 (+를 씀)업데이트 줄의 - 확인
🐢 손실이 거의 안 줄음학습률 과소lr을 10배로, 기울기 크기 출력
🔄 손실이 위아래 진동1−ηL1-\eta L이 음수 (η가 다소 큼)lr을 절반으로
0️⃣ 그래디언트가 전부 0정수 dtype, 원상복구 누락print(p.dtype)
❓ x.grad가 Nonerequires_grad 미설정print(x.requires_grad)
🔍 수치미분이 이상함hh가 너무 작음(자릿수 손실)h=1e-4로 고정, 중심차분 사용
❌ 유도는 맞는데 검증 실패절대오차로 비교함상대오차로 변경

📝 연습문제

🧠 개념 문제

Q1 🟢 그래디언트가 가리키는 방향은 함수 값이 (증가/감소)하는 방향이다. 알맞은 것을 고르고, 경사하강법이 왜 반대로 가는지 설명하시오.

Q2 🔵 수치미분에서 전진차분보다 중심차분을 쓰는 이유를 오차 관점에서 설명하시오. 또 hh를 무한정 작게 하면 안 되는 이유는?

🧮 계산 문제

Q3 🟢 f(x)=2x2−8x+3f(x)=2x^2-8x+3을 미분하고, x=1x=1에서 η=0.25\eta=0.25로 경사하강 한 스텝 후의 xx를 구하시오.

Q4 🔵 y=(2x2−3)5y=\bigl(2x^2-3\bigr)^5를 연쇄법칙으로 미분하시오.

Q5 🔴 f(x)=3x2f(x)=3x^2에 경사하강법을 적용할 때, 발산하지 않는 η\eta의 범위를 유도 ⑧과 같은 방식으로 구하시오.

💻 구현 문제

Q6 🔵 numerical_diff로 g(x)=sin⁡xg(x)=\sin x의 x=0x=0에서 미분값을 근사하고, 이론값 cos⁡0=1\cos 0=1과 비교하는 코드를 작성하시오.

Q7 🔴 손실 이력을 저장해 "MSE가 이전보다 커지면 자동으로 학습률을 절반으로 줄이는" 경사하강법을 구현하시오.

🚀 사고 확장 문제

Q8 🔴 손실함수에 골짜기가 여러 개라면 경사하강법이 항상 전역 최솟값을 찾는다고 보장할 수 있는가? 이유와 완화 방법을 설명하시오.

Q9 🔴🚀 00-03의 정규방정식은 "한 번에" 답을 준다. 그런데도 딥러닝이 굳이 느린 경사하강법을 쓰는 이유를 두 가지 이상 쓰시오.

✅ 해설

A1. 증가입니다. 그래디언트는 함수가 가장 빠르게 커지는 방향을 가리킵니다. 우리는 손실을 줄이고 싶으므로 정확히 반대 방향 −∇f-\nabla f로 이동합니다. 업데이트 식의 마이너스 부호가 바로 이 뜻입니다.

A2. 전진차분의 오차는 O(h)O(h), 중심차분은 O(h2)O(h^2)입니다. 좌우 대칭으로 재면 곡선의 휘어짐에서 오는 오차가 서로 상쇄되기 때문입니다. 하지만 hh를 너무 작게 하면 f(x+h)와 f(x-h)가 부동소수점 정밀도 안에서 거의 같아져 뺄셈 결과가 뭉개집니다(자릿수 손실). 그래서 h≈10−4h\approx10^{-4}가 실용적 최적점입니다.

A3. f′(x)=4x−8f'(x)=4x-8, f′(1)=−4f'(1)=-4. 따라서 x1=1−0.25×(−4)=1+1=2x_1 = 1 - 0.25\times(-4) = 1+1 = \mathbf{2}. 참고로 이 함수의 최솟값은 x=2x=2이므로 한 스텝에 정답에 도달했습니다 — f′′=4f''=4이고 η=1f′′=0.25\eta=\frac{1}{f''}=0.25였기 때문입니다! 🎯

A4. 안쪽 u=2x2−3u=2x^2-3 → dudx=4x\frac{du}{dx}=4x. 바깥 y=u5y=u^5 → dydu=5u4\frac{dy}{du}=5u^4. 곱하면 dydx=5(2x2−3)4×4x=20x(2x2−3)4\frac{dy}{dx}=5(2x^2-3)^4 \times 4x = \mathbf{20x(2x^2-3)^4}.

A5. f′(x)=6xf'(x)=6x이므로 xk+1=xk−6ηxk=(1−6η)xkx_{k+1}=x_k-6\eta x_k=(1-6\eta)x_k. 최솟값이 x=0x=0이므로 오차가 곧 xx 자신입니다. 수렴 조건은 ∣1−6η∣<1|1-6\eta|<1 → 0<η<13\mathbf{0<\eta<\frac{1}{3}}. 일반식 η<2f′′=26=13\eta<\frac{2}{f''}=\frac{2}{6}=\frac13과 일치합니다. ✅

A6. numerical_diff(math.sin, 0.0) → 1.0000000000 (중심차분은 sin⁡\sin이 홀함수라 오차가 극히 작습니다). 이론값 cos⁡0=1\cos 0=1과 일치합니다.

A7. 매 스텝 MSE를 계산해 prev_loss와 비교하고, if loss > prev_loss: lr = lr * 0.5를 넣으면 됩니다. 이런 기법을 학습률 스케줄링(adaptive learning rate)이라 하며, Adam 같은 옵티마이저가 이를 정교하게 자동화한 것입니다.

A8. 보장하지 못합니다. 경사하강법은 현재 위치의 국소 기울기만 보므로 지역 최솟값에 갇힐 수 있습니다. 완화책: ① 모멘텀으로 관성을 주어 얕은 웅덩이를 넘어감, ② SGD의 미니배치 잡음이 흔들어 빠져나오게 함, ③ 여러 시작점에서 실행, ④ Adam 등 적응형 옵티마이저. 다만 고차원에서는 지역 최솟값보다 안장점이 더 흔한 문제입니다.

A9. ① 공식이 존재하지 않음 — 신경망 손실은 비선형·비볼록이라 ∇L=0\nabla L=0을 대수적으로 풀 수 없습니다. ② 계산 비용 — 정규방정식은 (A⊤A)−1(A^{\top}A)^{-1} 계산이 파라미터 수의 세제곱에 비례해, 파라미터가 수백만이면 불가능합니다. ③ 메모리 — 전체 데이터를 한 번에 올릴 수 없어도 미니배치로 조금씩 학습할 수 있습니다. ④ 온라인 학습 — 데이터가 계속 들어와도 이어서 학습 가능합니다.

✨ 이번 장에서 배운 내용

🎯 개념 요약

🔬 미분 — 극한으로 정의된 순간 변화율\
📐 공식 — (xn)′=nxn−1(x^n)'=nx^{n-1}, 연쇄법칙 f′(g)g′f'(g)g'\
🧭 그래디언트 — 편미분을 모은 벡터, 최대 증가 방향\
⛰️ 경사하강법 — x←x−ηf′(x)x \leftarrow x-\eta f'(x) 한 줄\
⚖️ 발산 조건 — ∣1−2η∣<1|1-2\eta|<1 을 직접 유도\
🚀 응용 — 선형회귀가 정규방정식과 같은 답에 도달\
🤖 자동미분 — 손계산을 프레임워크가 대신

📌 핵심 문장 3가지

① 🔬 미분 = 두 점을 무한히 가깝게 붙였을 때의 기울기이며, 코드에서는 중심차분으로 근사한다.\
\
② 🧭 그래디언트의 반대 방향이 손실이 가장 빠르게 줄어드는 방향이고, 그 한 줄이 x = x - lr*gx이다.\
\
③ ⚖️ 학습률은 감쇠율 ∣1−ηf′′∣|1-\eta f''|로 결정된다 — 너무 크면 발산, 너무 작으면 느림.

☑️ 개념 체크리스트 (성취 기준)

☐ f(x)=x2f(x)=x^2의 x=5x=5에서 기울기를 손으로 계산할 수 있다 (정답 10) 🟢\
☐ 극한 정의에서 hh가 약분되는 과정을 직접 쓸 수 있다 🟢\
☐ f(x)=3x2−2x+1f(x)=3x^2-2x+1을 미분할 수 있다 (정답 6x−26x-2) 🟢\
☐ (3x2+1)4(3x^2+1)^4를 연쇄법칙으로 미분할 수 있다 (정답 24x(3x2+1)324x(3x^2+1)^3) 🔵\
☐ 시그모이드 미분 σ(1−σ)\sigma(1-\sigma)를 유도하고, 왜 출력을 저장하는지 설명할 수 있다 🔵⭐\
☐ 편미분에서 "나머지 변수는 상수"의 의미를 교차항 예제로 설명할 수 있다 🔵\
☐ 그래디언트가 등고선과 수직인 이유를 설명할 수 있다 🔵\
☐ 경사하강법 업데이트 식의 마이너스 부호를 부호별로 설명할 수 있다 🔵⭐\
☐ ek+1=(1−2η)eke_{k+1}=(1-2\eta)e_k를 유도하고 수렴 조건을 말할 수 있다 🔴⭐\
☐ η=0.5\eta=0.5에서 1스텝에 수렴하는 이유를 설명할 수 있다 🔴\
☐ MSE의 편미분 2m∑eixi\frac{2}{m}\sum e_ix_i를 유도하고 코드로 옮길 수 있다 🔴🚀\
☐ PyTorch의 zero_grad()·backward()·step()이 우리 코드의 어느 줄인지 짝지을 수 있다 🔴

🚫 실수 방지 체크리스트

☐ 업데이트 식의 부호가 빼기(−)인가\
☐ 학습률이 2f′′\frac{2}{f''}보다 작은가 (발산 위험 확인)\
☐ 입력 데이터를 정규화했는가 (스케일이 크면 발산)\
☐ 배열 dtype이 float인가 (정수면 갱신이 잘림)\
☐ 연쇄법칙에서 안쪽 미분을 곱했는가\
☐ 수치미분에서 h=10−4h=10^{-4} 부근을 썼는가 (너무 작으면 자릿수 손실)\
☐ 검증할 때 절대오차가 아닌 상대오차를 썼는가\
☐ numerical_gradient에서 p[i] = tmp 원상복구를 했는가\
☐ 배열을 함수에 넘길 때 .copy()로 원본을 보호했는가\
☐ PyTorch에서 매 스텝 zero_grad()를 호출했는가\
☐ 수식 변수명과 코드 변수명이 일치하는가 (η\eta ↔ lr, ∇\nabla ↔ gx/gw)\
☐ HTML 태그가 모두 닫히고, Mermaid 노드 ID가 ASCII인가

📚 추가 학습

✏️ 직접 구현 — check_grad를 확장해 자주 쓰는 함수 10개의 도함수를 유도하고 전부 검증해보기\
📉 학습 곡선 — matplotlib으로 학습률별 손실 곡선을 한 그래프에 겹쳐 그려 발산 조건을 눈으로 확인하기\
🎳 모멘텀 구현 — v = beta*v - lr*gx 두 줄을 추가해 수렴 속도가 얼마나 빨라지는지 비교하기\
🔥 PyTorch 비교 — 5단계 선형회귀를 nn.Linear와 nn.MSELoss로 다시 작성하기\
🧵 TensorFlow 비교 — GradientTape로 같은 문제를 풀고 문법 차이를 표로 정리하기\
⚡ JAX 비교 — jax.grad와 jax.jit을 함께 써서 속도 차이를 측정하기\
🧮 2차 미분 — jax.hessian으로 헤시안을 구해 곡률 f′′f''가 학습률 상한과 어떻게 연결되는지 확인하기

➡️

🔮 다음 장 예고 — 00-05. 확률·베이즈 정리 — 병원 검사의 함정 🏥🎲

이번 장에서 우리는 AI가 "기울기를 따라 점점 더 정확해지는" 과정을 배웠습니다. 그런데 아무리 학습해도 AI의 예측은 100% 확신이 아니라 "고양이일 확률 87%"처럼 확률로 나옵니다.

다음 장에서는 "정확도 99%인 검사에서 양성이 나왔는데, 실제로 병에 걸렸을 확률은 겨우 9%"라는 충격적인 계산을 직접 해봅니다. 🤯 직관을 완전히 뒤엎는 베이즈 정리가, 스팸 필터부터 의료 AI까지 어떻게 쓰이는지 살펴봅니다!


💻 GitHub 실습 — 코드로 직접 확인하기

📂 이 장의 코드 폴더: book/00-math-overview-for-beginners/00-04-derivative-gradient-descent

책에 실린 코드를 장(절)별로 모아 둔 폴더입니다. 아래 파일을 열어 보거나, Colab에서 바로 실행해 보세요.

파일용도링크
00_04_derivative_gradient_descent.py책의 코드 블록 10개를 순서대로 모은 실행 스크립트 (# %% 셀 구분)코드 보기
00_04_derivative_gradient_descent.ipynb같은 코드를 블록당 셀 1개로 담은 주피터 노트북보기 · Colab
README.md코드 블록 목록 · 실행 방법 · 검증 결과보기
🎬 medium-series/06-2-gradient-descent/Medium 시리즈 예제 — 같은 주제를 블로그용으로 다듬은 실습폴더 · Colab

🧪 저장소 검증 상태: 🧩 문맥 필요(조각) — Function 이(가) 이 절 안에서 정의되지 않습니다 — 데이터·변수·클래스를 미리 준비했다고 가정한 설명용 코드 조각입니다.

🔑 핵심 코드 1 — 경사하강법 본체 gradient_descent와 이론 예측

📄 00_04_derivative_gradient_descent.py L187–L222 · Block 5 (📍 5단계 — 코드로 구현하고 실전에 적용하기)

def f(x):
    """목표 함수 f(x) = x² - 4x + 5  (최솟값: x=2 에서 f(2)=1)"""
    return x**2 - 4*x + 5

def df(x):
    """도함수 f'(x) = 2x - 4  ← 2단계에서 유도한 공식 그대로"""
    return 2*x - 4

def gradient_descent(df, x_init, lr, n_steps):
    """
    경사하강법 본체.  수식:  x ← x - η·f'(x)

    비유: 안개 속 등산객이 발밑 경사(df)를 보고
    보폭(lr)만큼 n_steps 번 내려갑니다.
    """
    x = x_init
    history = [x]                       # 이동 경로를 기록 (나중에 그래프용)

    for step in range(n_steps):
        gx = df(x)                      # ① 기울기 계산 (기울기는 관례상 g 접두)
        x = x - lr * gx               # ② ★핵심★ 수식 x ← x - η·f'(x) 그 자체
        history.append(x)               # ③ 기록

        if step < 3 or (step + 1) % 10 == 0:   # 처음 3번 + 10번마다 출력
            print(f"step {step+1:2d}: grad={gx:9.4f}  x={x:9.4f}  f(x)={f(x):9.4f}")

    return x, history

x_final, hist = gradient_descent(df, x_init=10.0, lr=0.1, n_steps=30)

print(f"\n최종 x={x_final:.6f}  f(x)={f(x_final):.6f}   (정답 x=2, f=1)")

# ★ 4단계에서 유도한 공식 e_k = (1-2η)^k · e_0 으로 결과를 예측해봅니다
#   e_0 = 10 - 2 = 8,  (1-2×0.1) = 0.8,  k = 30
predicted = 8 * (0.8 ** 30)
print(f"이론 예측 x-2 = 8*(0.8)^30 = {predicted:.6f}, 실제 = {x_final-2:.6f}")

▶ 실행 결과 — 위 코드를 실행한 출력 (직접 실행: Windows · Python 3.11.2 · numpy 2.4.6, 앞 블록부터 순서대로 실행)

step  1: grad=  16.0000  x=   8.4000  f(x)=  41.9600
step  2: grad=  12.8000  x=   7.1200  f(x)=  27.2144
step  3: grad=  10.2400  x=   6.0960  f(x)=  17.7772
step 10: grad=   2.1475  x=   2.8590  f(x)=   1.7379
step 20: grad=   0.2306  x=   2.0922  f(x)=   1.0085
step 30: grad=   0.0248  x=   2.0099  f(x)=   1.0001

최종 x=2.009904  f(x)=1.000098   (정답 x=2, f=1)
이론 예측 x-2 = 8*(0.8)^30 = 0.009904, 실제 = 0.009904

🔑 핵심 코드 2 — 선형회귀를 경사하강법으로 학습하고 정규방정식과 비교

📄 00_04_derivative_gradient_descent.py L261–L291 · Block 8 (📍 5단계 — 코드로 구현하고 실전에 적용하기)

import numpy as np

# 00-03과 동일한 데이터: (1,2), (2,3), (3,5)
X = np.array([1.0, 2.0, 3.0])
Y = np.array([2.0, 3.0, 5.0])
m = len(X)                    # 데이터 개수

w, b = 0.0, 0.0            # 파라미터 초기화 (아무 값이나 OK)
lr = 0.1

for step in range(1, 1001):
    # ── ① 순전파: 예측하고 오차 구하기 ─────────────────
    pred = w * X + b            # ŷ = wx + b  (벡터 3개 한 번에)
    e = pred - Y                 # 잔차 e = ŷ - y

    # ── ② 역전파: 공식 ⑨를 그대로 코드로 ───────────────
    gw = (2 / m) * np.sum(e * X)   # ∂L/∂w = (2/m)Σ eᵢxᵢ
    gb = (2 / m) * np.sum(e)       # ∂L/∂b = (2/m)Σ eᵢ

    # ── ③ 업데이트: 경사하강 한 걸음 ───────────────────
    w = w - lr * gw
    b = b - lr * gb

    if step in (1, 10, 100, 500, 1000):
        mse = np.mean((w * X + b - Y) ** 2)
        print(f"step {step:4d}: w={w:.6f} b={b:.6f} MSE={mse:.6f}")

# ── ④ 00-03의 정규방정식 답과 비교 검증 ────────────────
A = np.column_stack([X, np.ones(m)])
exact = np.linalg.lstsq(A, Y, rcond=None)[0]
print("정규방정식(00-03) 정답:", np.round(exact, 6))

▶ 실행 결과 — 위 코드를 실행한 출력 (직접 실행: Windows · Python 3.11.2 · numpy 2.4.6, 앞 블록부터 순서대로 실행)

step    1: w=1.533333 b=0.666667 MSE=0.216296
step   10: w=1.405646 b=0.547822 MSE=0.062155
step  100: w=1.489440 b=0.357339 MSE=0.055638
step  500: w=1.499999 b=0.333335 MSE=0.055556
step 1000: w=1.500000 b=0.333333 MSE=0.055556
정규방정식(00-03) 정답: [1.5      0.333333]

📝 실행 메모

  • Block 3 은(는) 단독 실행되지 않습니다: NameError: name 'Function' is not defined — 앞 절에서 만든 변수·클래스를 가정한 책의 설명용 코드 조각입니다
  • Block 9 은(는) PyTorch·TensorFlow·JAX 가 필요한 코드라 실행하지 않았습니다 (대형 프레임워크/외부 요구사항 — 출력을 싣지 않았습니다).

☁️ Colab에서 바로 실행

👉 Open in Colab — 00_04_derivative_gradient_descent.ipynb · 설치 없이 브라우저에서 셀을 위에서부터 순서대로 실행하세요.

🖥️ 내 컴퓨터에서 실행하기

git clone https://github.com/otoloism/Zero_to_ML_Master.git
cd Zero_to_ML_Master
python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r book/requirements.txt   # 책 전체용 (PyTorch 등 포함, 용량 큼)
# 이 장만 실행한다면 이것만으로 충분합니다: pip install numpy
cd book/00-math-overview-for-beginners/00-04-derivative-gradient-descent
python 00_04_derivative_gradient_descent.py

📦 이 장의 일부 블록은 JAX, PyTorch, TensorFlow 이(가) 필요합니다. 설치하지 않으면 해당 블록만 오류가 나고 나머지는 정상 실행됩니다.

⭐ GitHub Star / Fork

⭐ 코드가 도움이 되셨다면 GitHub 저장소에 Star를 눌러 주세요. 새 장의 코드가 올라올 때 가장 먼저 알 수 있습니다.
🍴 직접 고쳐 보며 실험하고 싶다면 Fork 해서 내 저장소에서 마음껏 바꿔 보세요. 오류 제보·개선 제안은 Issues로 환영합니다!


🔮 다음 글 예고

다음 글은 00-05 확률·베이즈 정리 (1부) — 병원 검사의 함정 🏥 입니다. 정확도 99%짜리 검사에서 양성이 나왔는데 실제 환자일 확률은 겨우 1.94% 라는 충격적인 결과를, 확률 → 조건부 확률 → 베이즈 정리 순서로 차근차근 파헤칩니다.

⬅️ 이전 글: #05 미분 쉽게 이해하기 — 수치미분·연쇄법칙·그래디언트 (딥러닝 수학, 1부) \
➡️ 다음 글: #07 베이즈 정리 쉽게 이해하기 — 확률·조건부 확률과 병원 검사의 함정 (1부)


📚 더 깊이 공부하고 싶다면

이 시리즈는 수학을 놓았던 비전공자가 AI를 직접 구현하기까지의 공부 기록을 그대로 옮긴 연재입니다. 원문 책, 코드, 개념 사전을 함께 보면 훨씬 빨리 늡니다. 🚀

바로가기이런 분께
📘 책 (위키독스, 무료)Zero to 머신러닝 딥러닝 Master5권 · 20개 장 · 163개 절 전체를 순서대로 읽고 싶은 분
📄 이 글의 원문00-04 미분·경사하강법 — 안개 낀 산에서 내려오기 (2부)연습문제·해설까지 전부 보고 싶은 분
💻 실습 코드 (GitHub)otoloism/Zero_to_ML_Master.py·.ipynb를 Colab에서 바로 돌려보고 싶은 분
🏠 머신러닝 백과사전mldict.net본문 속 핵심 개념을 카드로 다시 확인하고 싶은 분
✍️ 기술 블로그wikidocs.net/blog/@mldict새 글 알림을 가장 먼저 받고 싶은 분

⭐ 도움이 되셨다면 velog 팔로우와 GitHub Star를 눌러 주세요. 다음 글을 쓰는 가장 큰 힘이 됩니다. 궁금한 점은 댓글로 편하게 남겨 주세요! 🙌

profile
자유주의자

0개의 댓글