// 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;
};
}17 도장 3주차: 손실함수와 경사하강법
도장 사용법
- 대응 이론: Ch03 딥러닝의 학습
- 학번을 입력하면 나만의 문제가 나옵니다. 새 문제 버튼으로 무한 반복 연습.
- 이 주차는 계산기가 필요합니다. 지수 e^x 와 자연로그 ln 을 쓰는 문제가 있습니다.
- 유형은 8개 — [기본] 3 → [응용] 2 → [도전] 3.
- 훈련 기준: 각 유형에서 연속 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>`17.1 유형 A — MSE 계산 [기본]
rA = {
const rng = dojoRng(rA_seed);
const n = dojoInt(rng, 4, 5);
const ys = [], hs = [];
for (let i = 0; i < n; i++) {
const y = dojoInt(rng, 150, 190);
const e = dojoInt(rng, -6, 6);
ys.push(y); hs.push(y - e);
}
const errs = ys.map((y, i) => y - hs[i]);
const sq = errs.map(e => e * e);
const sum = sq.reduce((a, b) => a + b, 0);
return { n, ys, hs, errs, sq, sum, mse: Math.round(sum / n * 1000) / 1000 };
}dojoProblem(`문제 A — 평균제곱오차`, `
키(cm)를 예측하는 모형의 결과가 다음과 같다.
<div style="margin:0.5em 0;">
<table style="border-collapse:collapse;font-size:0.95em;">
<tr style="background:#8C1515;color:#fff;">
<th style="padding:0.25em 0.8em;">i</th>
${rA.ys.map((_, i) => `<th style="padding:0.25em 0.8em;">${i + 1}</th>`).join("")}
</tr>
<tr><td style="padding:0.25em 0.8em;border:1px solid #ddd;"><strong>실제 y</strong></td>
${rA.ys.map(v => `<td style="padding:0.25em 0.8em;border:1px solid #ddd;text-align:center;">${v}</td>`).join("")}</tr>
<tr><td style="padding:0.25em 0.8em;border:1px solid #ddd;"><strong>예측 ŷ</strong></td>
${rA.hs.map(v => `<td style="padding:0.25em 0.8em;border:1px solid #ddd;text-align:center;">${v}</td>`).join("")}</tr>
</table>
</div>
<strong>MSE = (1/n)·Σ(yᵢ − ŷᵢ)²</strong> 를 구하라. (소수 셋째 자리까지)`)viewof rA_ans = Inputs.number({ label: "MSE =", step: 0.001 })
viewof rA_grade = Inputs.toggle({ label: "채점", value: false })
viewof rA_show = Inputs.toggle({ label: "풀이 보기", value: false })rA_grade ? dojoResult(
[dojoMark("MSE", dojoOk(rA_ans, rA.mse, 0.0015))],
dojoOk(rA_ans, rA.mse, 0.0015)
) : html``rA_show ? dojoSolution(`
① 각 오차와 그 제곱:<br>
${rA.ys.map((y, i) => ` (${y} − ${rA.hs[i]}) = ${rA.errs[i]} → ${rA.errs[i]}² = ${rA.sq[i]}`).join("<br>")}<br>
② 제곱합 = ${rA.sq.join(" + ")} = <strong>${rA.sum}</strong><br>
③ MSE = ${rA.sum} / ${rA.n} = <strong>${rA.mse}</strong><br>
<span style="color:#555;">제곱하므로 오차의 부호는 사라지고, 큰 오차에 더 큰 벌칙이 간다.</span>`) : html``17.2 유형 B — Softmax 확률 [기본]
rB = {
const rng = dojoRng(rB_seed);
const z = [dojoInt(rng, -20, 30) / 10, dojoInt(rng, -20, 30) / 10, dojoInt(rng, -20, 30) / 10];
const ex = z.map(v => Math.exp(v));
const s = ex.reduce((a, b) => a + b, 0);
const p = ex.map(v => Math.round(v / s * 1000) / 1000);
return { z, ex, s, p, argmax: p.indexOf(Math.max(...p)) + 1 };
}dojoProblem(`문제 B — Softmax`, `
3개 클래스 분류에서 출력층의 값이 다음과 같다.
<div style="text-align:center;margin:0.5em 0;font-size:1.05em;">
<strong>z₁ = ${rB.z[0]}, z₂ = ${rB.z[1]}, z₃ = ${rB.z[2]}</strong>
</div>
Softmax(zₖ) = e^zₖ / (e^z₁ + e^z₂ + e^z₃) 으로
각 클래스의 확률을 <strong>소수 셋째 자리까지</strong> 구하라.`)viewof rB_p1 = Inputs.number({ label: "p₁ =", step: 0.001 })
viewof rB_p2 = Inputs.number({ label: "p₂ =", step: 0.001 })
viewof rB_p3 = Inputs.number({ label: "p₃ =", step: 0.001 })
viewof rB_grade = Inputs.toggle({ label: "채점", value: false })
viewof rB_show = Inputs.toggle({ label: "풀이 보기", value: false })rB_grade ? dojoResult(
[dojoMark("p₁", dojoOk(rB_p1, rB.p[0], 0.0015)),
dojoMark("p₂", dojoOk(rB_p2, rB.p[1], 0.0015)),
dojoMark("p₃", dojoOk(rB_p3, rB.p[2], 0.0015))],
dojoOk(rB_p1, rB.p[0], 0.0015) && dojoOk(rB_p2, rB.p[1], 0.0015) && dojoOk(rB_p3, rB.p[2], 0.0015)
) : html``rB_show ? dojoSolution(`
① 각 z에 지수를 취한다:<br>
e^${rB.z[0]} = ${rB.ex[0].toFixed(4)},
e^${rB.z[1]} = ${rB.ex[1].toFixed(4)},
e^${rB.z[2]} = ${rB.ex[2].toFixed(4)}<br>
② 분모(합) = ${rB.ex.map(v => v.toFixed(4)).join(" + ")} = <strong>${rB.s.toFixed(4)}</strong><br>
③ 각각을 합으로 나눈다:<br>
p₁ = ${rB.ex[0].toFixed(4)}/${rB.s.toFixed(4)} = <strong>${rB.p[0].toFixed(3)}</strong><br>
p₂ = ${rB.ex[1].toFixed(4)}/${rB.s.toFixed(4)} = <strong>${rB.p[1].toFixed(3)}</strong><br>
p₃ = ${rB.ex[2].toFixed(4)}/${rB.s.toFixed(4)} = <strong>${rB.p[2].toFixed(3)}</strong><br>
<span style="color:#555;">검산: 세 확률의 합은 항상 1이다 (반올림 오차 제외).
가장 큰 z(=z${["₁","₂","₃"][rB.argmax - 1]})가 가장 높은 확률을 받는다.</span>`) : html``17.3 유형 C — 이진 교차 엔트로피 (1건) [기본]
rC = {
const rng = dojoRng(rC_seed);
const y = dojoInt(rng, 0, 1);
const p = dojoInt(rng, 5, 95) / 100;
const used = y === 1 ? p : 1 - p;
return { y, p, used, loss: Math.round(-Math.log(used) * 1000) / 1000 };
}dojoProblem(`문제 C — 이진 교차 엔트로피`, `
이진 분류에서 어떤 데이터 한 건의 실제 정답이 <strong>y = ${rC.y}</strong> 이고,
모형이 출력한 확률(클래스 1일 확률)이 <strong>ŷ = ${rC.p}</strong> 이다.<br>
이 한 건의 손실을 구하라.
<div style="text-align:center;margin:0.5em 0;">
<strong>L = −[ y·ln(ŷ) + (1−y)·ln(1−ŷ) ]</strong>
</div>
(ln 은 자연로그, 소수 셋째 자리까지)`)viewof rC_ans = Inputs.number({ label: "L =", step: 0.001 })
viewof rC_grade = Inputs.toggle({ label: "채점", value: false })
viewof rC_show = Inputs.toggle({ label: "풀이 보기", value: false })rC_grade ? dojoResult(
[dojoMark("손실 L", dojoOk(rC_ans, rC.loss, 0.0015))],
dojoOk(rC_ans, rC.loss, 0.0015)
) : html``rC_show ? dojoSolution(`
① y = ${rC.y} 이므로 ${rC.y === 1
? "둘째 항 (1−y)·ln(1−ŷ) 이 0이 되어 사라진다. 남는 것은 −ln(ŷ)."
: "첫째 항 y·ln(ŷ) 이 0이 되어 사라진다. 남는 것은 −ln(1−ŷ)."}<br>
② L = −ln(${rC.used}) = −(${Math.log(rC.used).toFixed(4)}) = <strong>${rC.loss.toFixed(3)}</strong><br>
<span style="color:#555;">정답 클래스에 부여한 확률이 ${rC.used} 였다.
이 값이 1에 가까울수록 손실은 0에, 0에 가까울수록 손실은 무한대로 커진다 —
<strong>확신에 찬 오답</strong>에 큰 벌칙을 주는 것이 교차 엔트로피의 성질이다.</span>`) : html``17.4 유형 D — Softmax + 다중 클래스 교차 엔트로피 [응용]
rD = {
const rng = dojoRng(rD_seed);
const z = [dojoInt(rng, -15, 25) / 10, dojoInt(rng, -15, 25) / 10, dojoInt(rng, -15, 25) / 10];
const ex = z.map(v => Math.exp(v));
const s = ex.reduce((a, b) => a + b, 0);
const p = ex.map(v => v / s);
const k = dojoInt(rng, 0, 2);
return {
z, ex, s, k,
pk: Math.round(p[k] * 1000) / 1000,
loss: Math.round(-Math.log(p[k]) * 1000) / 1000
};
}dojoProblem(`문제 D — 다중 클래스 손실`, `
3개 클래스 분류에서 출력층 값이
<strong>z₁ = ${rD.z[0]}, z₂ = ${rD.z[1]}, z₃ = ${rD.z[2]}</strong> 이고,
이 데이터의 <strong>정답은 클래스 ${rD.k + 1}</strong> 이다.<br>
① 정답 클래스의 Softmax 확률, ② 교차 엔트로피 손실
<strong>L = −ln(ŷ<sub>정답</sub>)</strong> 을 구하라. (소수 셋째 자리까지)`)viewof rD_ans_p = Inputs.number({ label: "정답 클래스 확률 =", step: 0.001 })
viewof rD_ans_l = Inputs.number({ label: "L =", step: 0.001 })
viewof rD_grade = Inputs.toggle({ label: "채점", value: false })
viewof rD_show = Inputs.toggle({ label: "풀이 보기", value: false })rD_grade ? dojoResult(
[dojoMark("정답 클래스 확률", dojoOk(rD_ans_p, rD.pk, 0.0015)),
dojoMark("손실 L", dojoOk(rD_ans_l, rD.loss, 0.0025))],
dojoOk(rD_ans_p, rD.pk, 0.0015) && dojoOk(rD_ans_l, rD.loss, 0.0025)
) : html``rD_show ? dojoSolution(`
① 지수: e^${rD.z[0]} = ${rD.ex[0].toFixed(4)}, e^${rD.z[1]} = ${rD.ex[1].toFixed(4)},
e^${rD.z[2]} = ${rD.ex[2].toFixed(4)} → 합 = <strong>${rD.s.toFixed(4)}</strong><br>
정답은 클래스 ${rD.k + 1} 이므로
ŷ = ${rD.ex[rD.k].toFixed(4)} / ${rD.s.toFixed(4)} = <strong>${rD.pk.toFixed(3)}</strong><br>
② L = −ln(${rD.pk.toFixed(3)}) = <strong>${rD.loss.toFixed(3)}</strong><br>
<span style="color:#555;">원-핫 정답 벡터에서 정답 클래스만 y=1이므로,
다중 클래스 교차 엔트로피 −Σ yₖ·ln(ŷₖ) 는 결국 <strong>정답 클래스 항 하나</strong>만 남는다.</span>`) : html``17.5 유형 E — 경사하강 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``17.6 유형 F — 퍼셉트론 파라미터의 경사하강 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``17.7 유형 G — 여러 건의 교차 엔트로피 평균 [도전]
rG = {
const rng = dojoRng(rG_seed);
const n = 4, ys = [], ps = [], ls = [];
for (let i = 0; i < n; i++) {
const y = dojoInt(rng, 0, 1);
const p = dojoInt(rng, 10, 90) / 100;
ys.push(y); ps.push(p);
ls.push(-Math.log(y === 1 ? p : 1 - p));
}
const sum = ls.reduce((a, b) => a + b, 0);
const worst = ls.indexOf(Math.max(...ls)) + 1;
return { n, ys, ps, ls, sum, bce: Math.round(sum / n * 1000) / 1000, worst };
}dojoProblem(`문제 G — 평균 이진 교차 엔트로피`, `
이진 분류 데이터 4건에 대해 모형이 다음 확률을 출력하였다.
<div style="margin:0.5em 0;">
<table style="border-collapse:collapse;font-size:0.95em;">
<tr style="background:#8C1515;color:#fff;">
<th style="padding:0.25em 0.9em;">i</th>
${rG.ys.map((_, i) => `<th style="padding:0.25em 0.9em;">${i + 1}</th>`).join("")}
</tr>
<tr><td style="padding:0.25em 0.9em;border:1px solid #ddd;"><strong>정답 y</strong></td>
${rG.ys.map(v => `<td style="padding:0.25em 0.9em;border:1px solid #ddd;text-align:center;">${v}</td>`).join("")}</tr>
<tr><td style="padding:0.25em 0.9em;border:1px solid #ddd;"><strong>예측 ŷ</strong></td>
${rG.ps.map(v => `<td style="padding:0.25em 0.9em;border:1px solid #ddd;text-align:center;">${v}</td>`).join("")}</tr>
</table>
</div>
① 전체 <strong>평균 BCE</strong>, ② <strong>손실이 가장 큰 데이터의 번호</strong>를 구하라.`)viewof rG_ans_bce = Inputs.number({ label: "평균 BCE =", step: 0.001 })
viewof rG_ans_worst = Inputs.number({ label: "손실 최대 데이터 번호 =", step: 1 })
viewof rG_grade = Inputs.toggle({ label: "채점", value: false })
viewof rG_show = Inputs.toggle({ label: "풀이 보기", value: false })rG_grade ? dojoResult(
[dojoMark("평균 BCE", dojoOk(rG_ans_bce, rG.bce, 0.0025)),
dojoMark("손실 최대 번호", dojoOk(rG_ans_worst, rG.worst, 0.001))],
dojoOk(rG_ans_bce, rG.bce, 0.0025) && dojoOk(rG_ans_worst, rG.worst, 0.001)
) : html``rG_show ? dojoSolution(`
각 건의 손실 = −ln(정답 클래스에 준 확률):<br>
${rG.ys.map((y, i) => {
const used = y === 1 ? rG.ps[i] : 1 - rG.ps[i];
return ` i=${i + 1}: y=${y}, ŷ=${rG.ps[i]} → 정답 확률 ${used.toFixed(2)}
→ −ln(${used.toFixed(2)}) = <strong>${rG.ls[i].toFixed(3)}</strong>`;
}).join("<br>")}<br>
① 평균 = (${rG.ls.map(v => v.toFixed(3)).join(" + ")}) / 4
= ${rG.sum.toFixed(3)} / 4 = <strong>${rG.bce.toFixed(3)}</strong><br>
② 가장 큰 손실은 <strong>i = ${rG.worst}</strong><br>
<span style="color:#555;">y=0인데 ŷ가 크거나, y=1인데 ŷ가 작은 건이 손실을 지배한다.</span>`) : html``17.8 유형 H — 학습률과 수렴 판정 [도전]
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``17.9 다 풀었다면
- 각 유형에서 연속 3문제를 실수 없이 풀었다면 3주차는 완성이다.
- 막히는 유형이 있으면 Ch03 딥러닝의 학습으로 돌아가라.
- 앞 주차: 2주차 퍼셉트론 | 다음 주차: 4주차 역전파와 일반화