// mulberry32 — 문제 번호(시드)가 같으면 항상 같은 문제가 생성된다
dojoRng = function (seed) {
let a = (seed * 7919 + 104729) >>> 0;
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}도장 4주차: 경사하강법
도장 사용법
- 대응 이론: Ch03 딥러닝의 학습 (후반: 미분 → 경사하강법 → 2차원 확장)
- 학번을 입력하면 나만의 문제가 나옵니다. 새 문제 버튼으로 무한 반복 연습.
- 이 주차는 계산기가 필요합니다. 지수 e^x 와 자연로그 ln 을 쓰는 문제가 있습니다.
- 유형은 3개 — [응용] 1 → [도전] 2.
- 훈련 기준: 각 유형에서 연속 3문제 무실수.
이 수업의 계산 약속
- log 는 항상 자연로그 ln 입니다 (밑 = e ≈ 2.71828). 상용로그(밑 10)가 아닙니다.
- 별도 지시가 없으면 답은 소수 넷째 자리에서 반올림하여 셋째 자리까지 씁니다.
// 정답 비교 — tol 이내면 정답 처리 (학생의 반올림 오차 허용)
dojoOk = (user, answer, tol = 0.005) =>
user !== null && user !== undefined && Number.isFinite(user) && Math.abs(user - answer) <= toldojoMark = (label, ok, answerShown) => `
<div style="display:flex;align-items:center;gap:0.5em;padding:0.25em 0;">
<span style="font-size:1.1em;">${ok ? "✅" : "❌"}</span>
<span style="font-weight:600;">${label}</span>
<span style="color:${ok ? "#2E7D32" : "#8C1515"};">
${ok ? "정답" : (answerShown != null ? "오답 — 다시 계산해 보세요" : "오답")}
</span>
</div>`// 문제 카드
dojoProblem = (title, bodyHtml) => html`
<div style="font-family:'Pretendard',sans-serif;border:1.5px solid #8C1515;border-radius:8px;
overflow:hidden;margin:0.6em 0;max-width:720px;">
<div style="background:#8C1515;color:white;padding:0.4em 0.9em;font-weight:700;font-size:0.95em;">
${title}
</div>
<div style="padding:0.7em 0.9em;line-height:1.9;font-size:0.95em;">${bodyHtml}</div>
</div>`// 채점 결과 카드
dojoResult = (marks, allOk) => html`
<div style="font-family:'Pretendard',sans-serif;border-radius:8px;max-width:720px;margin:0.4em 0;
padding:0.6em 0.9em;font-size:0.92em;
background:${allOk ? "#E8F5E9" : "#FFF8E1"};
border:1.5px solid ${allOk ? "#2E7D32" : "#D4A017"};">
${marks.join("")}
<div style="margin-top:0.35em;font-weight:700;color:${allOk ? "#2E7D32" : "#B8860B"};">
${allOk ? "🎉 전부 정답! 문제 번호를 바꿔 한 번 더 연습해 보세요." : "빨간 항목을 다시 풀어 보세요. 막히면 풀이 보기를 켜세요."}
</div>
</div>`// 풀이 카드
dojoSolution = (stepsHtml) => html`
<div style="font-family:'Pretendard',sans-serif;border-radius:8px;max-width:720px;margin:0.4em 0;
padding:0.7em 0.9em;background:#E3F2FD;border:1.5px solid #1565C0;
line-height:2.0;font-size:0.92em;">
<div style="font-weight:700;color:#1565C0;margin-bottom:0.2em;">풀이</div>
${stepsHtml}
</div>`// FNV-1a — 학번 문자열을 정수로. 사람마다 다른 문제가 나오게 하는 출발점.
dojoHash = function (str) {
let h = 2166136261 >>> 0;
const s = String(str);
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
return h >>> 0;
}// (학번, 주차, 유형, 라운드) → 문제 번호.
// 학번이 다르면 다른 문제, 같은 학번·같은 라운드면 항상 같은 문제(재현 가능).
dojoSeed = function (sid, week, type, round) {
const key = (sid && String(sid).trim()) ? String(sid).trim() : "guest";
return (dojoHash(key + "|" + week + "|" + type + "|" + round) % 99999) + 1;
}// 학번 입력 — 브라우저에 저장되어 모든 도장 페이지에서 유지된다.
viewof dojoSid = Inputs.text({
label: "학번",
placeholder: "예: 202312345",
value: (() => { try { return localStorage.getItem("dojoSid") || ""; } catch (e) { return ""; } })(),
width: 260
}){
try { localStorage.setItem("dojoSid", dojoSid || ""); } catch (e) {}
const named = dojoSid && dojoSid.trim();
return html`<div style="font-family:'Pretendard',sans-serif;font-size:0.85em;
padding:0.45em 0.8em;border-radius:6px;max-width:720px;margin:0.1em 0 0.9em;
background:${named ? "#E8F5E9" : "#FFF8E1"};
border:1px solid ${named ? "#A5D6A7" : "#E8D9A0"};color:${named ? "#2E7D32" : "#8A6D1B"};">
${named
? `학번 <strong>${dojoSid.trim()}</strong> 기준으로 문제가 생성됩니다 — 옆 사람과 다른 문제가 나옵니다.`
: `학번을 입력하면 <strong>나만의 문제 세트</strong>가 만들어집니다. (비워 두면 공용 연습 문제)`}
</div>`;
}// "새 문제" 버튼 + 문제 코드 표시를 한 줄로 묶는 헤더
dojoBar = (code) => html`
<div style="font-family:'Pretendard',sans-serif;font-size:0.82em;color:#666;
margin:-0.3em 0 0.5em;">
문제 코드 <code style="background:#F1F3F5;padding:0.1em 0.4em;border-radius:4px;
color:#8C1515;font-weight:700;">${code}</code>
<span style="color:#999;"> — 질문할 때 이 코드를 알려주세요</span>
</div>`유형 A — 경사하강 1스텝 [응용]
rE = {
const rng = dojoRng(rE_seed);
const a = dojoInt(rng, 1, 8);
const th = dojoInt(rng, -6, 12);
const eta = dojoPick(rng, [0.05, 0.1, 0.2, 0.25, 0.5]);
const g = 2 * (th - a);
const nt = Math.round((th - eta * g) * 10000) / 10000;
return { a, th, eta, g, nt, l0: Math.pow(th - a, 2), l1: Math.round(Math.pow(nt - a, 2) * 10000) / 10000 };
}dojoProblem(`문제 E — 경사하강 1스텝`, `
손실 함수가 <strong>L(θ) = (θ − ${rE.a})²</strong> 이고,
현재 파라미터는 <strong>θ = ${rE.th}</strong>, 학습률은 <strong>η = ${rE.eta}</strong> 이다.<br>
① 현재 위치의 기울기 <strong>dL/dθ</strong>,
② 1스텝 갱신 후의 <strong>θ<sub>new</sub></strong> 를 구하라.
<div style="margin-top:0.4em;color:#666;font-size:0.92em;">
갱신식: θ<sub>new</sub> = θ − η · (dL/dθ)
</div>`)viewof rE_ans_g = Inputs.number({ label: "dL/dθ =", step: 0.01 })
viewof rE_ans_t = Inputs.number({ label: "θ_new =", step: 0.0001 })
viewof rE_grade = Inputs.toggle({ label: "채점", value: false })
viewof rE_show = Inputs.toggle({ label: "풀이 보기", value: false })rE_grade ? dojoResult(
[dojoMark("기울기", dojoOk(rE_ans_g, rE.g, 0.011)),
dojoMark("θ_new", dojoOk(rE_ans_t, rE.nt, 0.0015))],
dojoOk(rE_ans_g, rE.g, 0.011) && dojoOk(rE_ans_t, rE.nt, 0.0015)
) : html``rE_show ? dojoSolution(`
① L(θ) = (θ − ${rE.a})² 의 도함수는 dL/dθ = 2(θ − ${rE.a}).<br>
θ = ${rE.th} 을 대입: 2 × (${rE.th} − ${rE.a}) = 2 × ${rE.th - rE.a}
= <strong>${rE.g}</strong><br>
② θ<sub>new</sub> = ${rE.th} − ${rE.eta} × (${rE.g}) = ${rE.th} − ${(rE.eta * rE.g).toFixed(4)}
= <strong>${rE.nt}</strong><br>
검산: 손실이 ${rE.l0} → ${rE.l1} 로 ${rE.l1 < rE.l0 ? "줄었다 ✅" : "줄지 않았다 — 학습률이 너무 크다"}<br>
<span style="color:#555;">기울기가 ${rE.g > 0 ? "양수이므로 θ를 <strong>줄이는</strong>" : "음수이므로 θ를 <strong>키우는</strong>"}
방향으로 움직인다. 그래서 갱신식에 <strong>마이너스</strong>가 붙는다.</span>`) : html``유형 B — 퍼셉트론 파라미터의 경사하강 1스텝 [도전]
rF = {
const rng = dojoRng(rF_seed);
const w = dojoInt(rng, -20, 20) / 10;
const b = dojoInt(rng, -20, 20) / 10;
const x = dojoInt(rng, 1, 5);
const y = dojoInt(rng, -10, 30) / 10;
const eta = dojoPick(rng, [0.01, 0.02, 0.05, 0.1]);
const yh = Math.round((w * x + b) * 10000) / 10000;
const r = Math.round((y - yh) * 10000) / 10000;
const gw = Math.round(-2 * r * x * 10000) / 10000;
const gb = Math.round(-2 * r * 10000) / 10000;
return {
w, b, x, y, eta, yh, r, gw, gb,
nw: Math.round((w - eta * gw) * 10000) / 10000,
nb: Math.round((b - eta * gb) * 10000) / 10000
};
}dojoProblem(`문제 F — 파라미터 갱신 1스텝`, `
퍼셉트론 <strong>ŷ = w·x + b</strong> (활성화는 항등함수)의 현재 파라미터가
<strong>w = ${rF.w}, b = ${rF.b}</strong> 이다.
데이터 한 건 <strong>(x = ${rF.x}, y = ${rF.y})</strong> 로 학습한다.
손실은 <strong>L = (y − ŷ)²</strong>, 학습률은 <strong>η = ${rF.eta}</strong>.
<div style="margin:0.5em 0;padding:0.5em 0.8em;background:#F8F9FA;border-radius:6px;">
그래디언트 공식: ∂L/∂w = −2(y − ŷ)·x | ∂L/∂b = −2(y − ŷ)
</div>
① 예측값 ŷ, ② ∂L/∂w, ③ 갱신 후 w<sub>new</sub> 를 구하라. (소수 셋째 자리까지)`)viewof rF_ans_yh = Inputs.number({ label: "ŷ =", step: 0.001 })
viewof rF_ans_gw = Inputs.number({ label: "∂L/∂w =", step: 0.001 })
viewof rF_ans_nw = Inputs.number({ label: "w_new =", step: 0.001 })
viewof rF_grade = Inputs.toggle({ label: "채점", value: false })
viewof rF_show = Inputs.toggle({ label: "풀이 보기", value: false })rF_grade ? dojoResult(
[dojoMark("예측값 ŷ", dojoOk(rF_ans_yh, rF.yh, 0.0015)),
dojoMark("∂L/∂w", dojoOk(rF_ans_gw, rF.gw, 0.0025)),
dojoMark("w_new", dojoOk(rF_ans_nw, rF.nw, 0.0025))],
dojoOk(rF_ans_yh, rF.yh, 0.0015) && dojoOk(rF_ans_gw, rF.gw, 0.0025) && dojoOk(rF_ans_nw, rF.nw, 0.0025)
) : html``rF_show ? dojoSolution(`
① <strong>순전파</strong>: ŷ = ${rF.w} × ${rF.x} + ${rF.b} = <strong>${rF.yh}</strong><br>
② <strong>오차</strong>: y − ŷ = ${rF.y} − ${rF.yh} = ${rF.r}<br>
∂L/∂w = −2 × (${rF.r}) × ${rF.x} = <strong>${rF.gw}</strong><br>
(참고: ∂L/∂b = −2 × (${rF.r}) = ${rF.gb})<br>
③ <strong>갱신</strong>: w<sub>new</sub> = ${rF.w} − ${rF.eta} × (${rF.gw})
= <strong>${rF.nw}</strong> (b<sub>new</sub> = ${rF.nb})<br>
<span style="color:#555;">순전파 → 손실 → 그래디언트 → 갱신. 이 네 단계가 학습 1스텝의 전부다.
∂L/∂w 공식이 <strong>어디서 오는지</strong>는 Ch04의 연쇄법칙에서 직접 유도한다.</span>`) : html``유형 C — 학습률과 수렴 판정 [도전]
rH = {
const rng = dojoRng(rH_seed);
const a = dojoInt(rng, 1, 6);
const th0 = a + dojoPick(rng, [-4, -3, -2, 2, 3, 4]);
const eta = dojoPick(rng, [0.1, 0.25, 0.5, 0.8, 1.0, 1.2]);
const step = (t) => Math.round((t - eta * 2 * (t - a)) * 100000) / 100000;
const t1 = step(th0), t2 = step(t1);
const factor = Math.abs(1 - 2 * eta);
return {
a, th0, eta, t1, t2, factor,
verdict: factor < 1 ? "수렴" : (factor > 1 ? "발산" : "진동(제자리)"),
d0: Math.abs(th0 - a), d2: Math.round(Math.abs(t2 - a) * 100000) / 100000
};
}dojoProblem(`문제 H — 두 스텝과 수렴 판정`, `
손실 함수 <strong>L(θ) = (θ − ${rH.a})²</strong>, 초기값
<strong>θ₀ = ${rH.th0}</strong>, 학습률 <strong>η = ${rH.eta}</strong> 이다.<br>
① 경사하강을 <strong>2번</strong> 수행한 뒤의 θ₂ 를 구하라.<br>
② 이 학습률로 계속 반복하면 <strong>수렴</strong>하는가, <strong>발산</strong>하는가?
<div style="margin-top:0.4em;color:#666;font-size:0.92em;">
힌트: 한 스텝의 갱신은 θ ← θ − η·2(θ − ${rH.a}) 이므로,
최솟값 ${rH.a} 로부터의 거리는 매 스텝 <strong>|1 − 2η|</strong> 배가 된다.
</div>`)viewof rH_ans_t2 = Inputs.number({ label: "θ₂ =", step: 0.0001 })
viewof rH_ans_v = Inputs.radio(["수렴", "발산", "진동(제자리)"], { label: "판정" })
viewof rH_grade = Inputs.toggle({ label: "채점", value: false })
viewof rH_show = Inputs.toggle({ label: "풀이 보기", value: false })rH_grade ? dojoResult(
[dojoMark("θ₂", dojoOk(rH_ans_t2, rH.t2, 0.0015)),
dojoMark("수렴/발산 판정", rH_ans_v === rH.verdict)],
dojoOk(rH_ans_t2, rH.t2, 0.0015) && rH_ans_v === rH.verdict
) : html``rH_show ? dojoSolution(`
<strong>스텝 1</strong>: 기울기 = 2(${rH.th0} − ${rH.a}) = ${2 * (rH.th0 - rH.a)}<br>
θ₁ = ${rH.th0} − ${rH.eta} × ${2 * (rH.th0 - rH.a)} = <strong>${rH.t1}</strong><br>
<strong>스텝 2</strong>: 기울기 = 2(${rH.t1} − ${rH.a}) = ${Math.round(2 * (rH.t1 - rH.a) * 100000) / 100000}<br>
θ₂ = ${rH.t1} − ${rH.eta} × ${Math.round(2 * (rH.t1 - rH.a) * 100000) / 100000}
= <strong>${rH.t2}</strong><br><br>
<strong>판정</strong>: |1 − 2η| = |1 − ${2 * rH.eta}| = <strong>${rH.factor.toFixed(2)}</strong>
${rH.factor < 1 ? "< 1 이므로 거리가 매 스텝 줄어든다 → <strong>수렴</strong>"
: rH.factor > 1 ? "> 1 이므로 거리가 매 스텝 커진다 → <strong>발산</strong>"
: "= 1 이므로 거리가 그대로다 → <strong>진동(제자리)</strong>"}<br>
최솟값까지의 거리: 처음 ${rH.d0} → 2스텝 후 ${rH.d2}<br>
<span style="color:#555;">학습률이 너무 크면 최솟값을 <strong>뛰어넘어</strong> 반대편으로 더 멀리 간다.
Ch03의 경사하강 인터랙티브에서 η를 0.9 이상으로 올렸을 때 본 현상이다.</span>`) : html``다 풀었다면
- 각 유형에서 연속 3문제를 실수 없이 풀었다면 4주차는 완성이다.
- 막히는 유형이 있으면 Ch03 딥러닝의 학습의 “미분: 어느 쪽으로 움직여야 하나”부터 다시 읽어라.
- 앞 주차: 3주차 손실함수 | 다음 주차: 5주차 역전파와 일반화