// 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;
};
}18 도장 4주차: 역전파·일반화·정형 데이터
도장 사용법
- 대응 이론: Ch04 학습의 실전 — 학습률, 배치, 역전파, 일반화, 정형 데이터
- 학번을 입력하면 나만의 문제가 나옵니다. 새 문제 버튼으로 무한 반복 연습.
- 계산기가 필요합니다. 유형 F·G는 이 수업 손계산의 정점입니다 — 시간을 들여 천천히.
- 유형은 8개 — [기본] 3 → [응용] 2 → [도전] 3.
- 훈련 기준: 각 유형에서 연속 3문제 무실수.
이 수업의 계산 약속
- 표준편차는 편차 제곱합을 n 으로 나눈 뒤 제곱근을 취한다 (모표준편차). n−1 이 아니다.
- 별도 지시가 없으면 소수 넷째 자리에서 반올림하여 셋째 자리까지 쓴다.
// 정답 비교 — 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>`18.1 유형 A — Sigmoid 도함수 [기본]
sA = {
const rng = dojoRng(sA_seed);
let z = dojoInt(rng, -30, 30) / 10; if (z === 0) z = 0.8;
const h = 1 / (1 + Math.exp(-z));
return { z, h: Math.round(h * 1000) / 1000, d: Math.round(h * (1 - h) * 1000) / 1000 };
}dojoProblem(`문제 A — σ(z)와 σ'(z)`, `
어떤 은닉 노드의 가중합이 <strong>z = ${sA.z}</strong> 이다.<br>
① Sigmoid 출력 <strong>h = σ(z) = 1/(1+e<sup>−z</sup>)</strong>,
② 그 도함수 <strong>σ'(z) = h(1−h)</strong> 를 구하라. (소수 셋째 자리까지)`)viewof sA_ans_h = Inputs.number({ label: "h = σ(z) =", step: 0.001 })
viewof sA_ans_d = Inputs.number({ label: "σ'(z) =", step: 0.001 })
viewof sA_grade = Inputs.toggle({ label: "채점", value: false })
viewof sA_show = Inputs.toggle({ label: "풀이 보기", value: false })sA_grade ? dojoResult(
[dojoMark("h = σ(z)", dojoOk(sA_ans_h, sA.h, 0.0015)),
dojoMark("σ'(z)", dojoOk(sA_ans_d, sA.d, 0.0015))],
dojoOk(sA_ans_h, sA.h, 0.0015) && dojoOk(sA_ans_d, sA.d, 0.0015)
) : html``sA_show ? dojoSolution(`
① h = 1 / (1 + e^(−${sA.z})) = 1 / (1 + ${Math.exp(-sA.z).toFixed(4)})
= <strong>${sA.h.toFixed(3)}</strong><br>
② σ'(z) = h(1 − h) = ${sA.h.toFixed(3)} × ${(1 - sA.h).toFixed(3)}
= <strong>${sA.d.toFixed(3)}</strong><br>
<span style="color:#555;">도함수를 다시 미분할 필요가 없다 —
<strong>순전파에서 이미 구한 h 만으로</strong> 바로 나온다. 이것이 역전파가 빠른 이유 중 하나다.<br>
σ'(z)의 최댓값은 z=0에서 <strong>0.25</strong>. 이 값이 층마다 곱해지는 것이 기울기 소실이다.</span>`) : html``18.2 유형 B — 러닝커브 진단 [기본]
sB = {
const rng = dojoRng(sB_seed);
const kind = dojoInt(rng, 0, 2);
if (kind === 0) {
const tr = dojoInt(rng, 90, 180) / 100;
return { tr, va: Math.round((tr + dojoInt(rng, 2, 15) / 100) * 100) / 100,
dx: "과소적합", rx: "모형을 키우거나 더 오래 학습한다" };
} else if (kind === 1) {
const tr = dojoInt(rng, 2, 12) / 100;
return { tr, va: Math.round((tr + dojoInt(rng, 80, 200) / 100) * 100) / 100,
dx: "과적합", rx: "조기 종료 · 데이터 추가 · 모형 축소 · 드롭아웃" };
} else {
const tr = dojoInt(rng, 8, 25) / 100;
return { tr, va: Math.round((tr + dojoInt(rng, 1, 6) / 100) * 100) / 100,
dx: "잘 학습됨", rx: "그대로 종료해도 좋다" };
}
}dojoProblem(`문제 B — 러닝커브 읽기`, `
어떤 모형을 충분히 학습시킨 뒤 손실을 측정하였다.
<div style="margin:0.5em 0;padding:0.6em 0.9em;background:#F8F9FA;border-radius:6px;line-height:1.9;">
훈련 손실 = <strong>${sB.tr}</strong><br>
검증 손실 = <strong>${sB.va}</strong>
</div>
이 모형의 상태를 진단하라.`)viewof sB_ans = Inputs.radio(["과소적합", "과적합", "잘 학습됨"], { label: "진단" })
viewof sB_grade = Inputs.toggle({ label: "채점", value: false })
viewof sB_show = Inputs.toggle({ label: "풀이 보기", value: false })sB_show ? dojoSolution(`
훈련 ${sB.tr} / 검증 ${sB.va} → 간격 = ${(Math.round((sB.va - sB.tr) * 100) / 100).toFixed(2)}<br>
진단: <strong>${sB.dx}</strong><br>
처방: ${sB.rx}<br><br>
<span style="color:#555;">판단 규칙 —<br>
· 훈련 <strong>높음</strong> + 검증 높음 → <strong>과소적합</strong> (아직 배우지도 못함)<br>
· 훈련 <strong>낮음</strong> + 검증 <strong>높음</strong>(간격 큼) → <strong>과적합</strong> (암기함)<br>
· 둘 다 낮음 → <strong>잘 학습됨</strong><br>
주의: 검증 손실이 훈련보다 <em>약간</em> 큰 것은 정상이다. 문제는 <strong>간격이 벌어지는 것</strong>이다.</span>`) : html``18.3 유형 C — 표준화 [기본]
sC = {
const rng = dojoRng(sC_seed);
const v = [];
for (let i = 0; i < 4; i++) v.push(dojoInt(rng, 20, 60));
const need = (5 * dojoInt(rng, 25, 50)) - v.reduce((a, b) => a + b, 0);
v.push(need >= 10 && need <= 80 ? need : 40);
const mu = v.reduce((a, b) => a + b, 0) / 5;
const varr = v.reduce((a, b) => a + Math.pow(b - mu, 2), 0) / 5;
const s = Math.sqrt(varr);
const j = dojoInt(rng, 0, 4);
return { v, mu, varr, s, j, x: v[j], z: Math.round((v[j] - mu) / s * 1000) / 1000 };
}dojoProblem(`문제 C — 표준화`, `
훈련 데이터의 '나이' 변수 값이 다음과 같다.
<div style="text-align:center;margin:0.5em 0;font-size:1.05em;">
<strong>${sC.v.join(", ")}</strong>
</div>
① 평균 <strong>μ</strong>, ② 표준편차 <strong>s</strong>,
③ <strong>x = ${sC.x}</strong> 의 표준화 값 <strong>z = (x − μ)/s</strong> 를 구하라.
<div style="margin-top:0.4em;color:#666;font-size:0.92em;">
표준편차는 편차 제곱합을 <strong>n(=5)</strong> 으로 나눈 뒤 제곱근을 취한다.
</div>`)viewof sC_ans_mu = Inputs.number({ label: "μ =", step: 0.001 })
viewof sC_ans_s = Inputs.number({ label: "s =", step: 0.001 })
viewof sC_ans_z = Inputs.number({ label: "z =", step: 0.001 })
viewof sC_grade = Inputs.toggle({ label: "채점", value: false })
viewof sC_show = Inputs.toggle({ label: "풀이 보기", value: false })sC_grade ? dojoResult(
[dojoMark("평균 μ", dojoOk(sC_ans_mu, sC.mu, 0.0015)),
dojoMark("표준편차 s", dojoOk(sC_ans_s, Math.round(sC.s * 1000) / 1000, 0.0025)),
dojoMark("표준화 z", dojoOk(sC_ans_z, sC.z, 0.0035))],
dojoOk(sC_ans_mu, sC.mu, 0.0015) &&
dojoOk(sC_ans_s, Math.round(sC.s * 1000) / 1000, 0.0025) &&
dojoOk(sC_ans_z, sC.z, 0.0035)
) : html``sC_show ? dojoSolution(`
① μ = (${sC.v.join(" + ")}) / 5 = ${sC.v.reduce((a, b) => a + b, 0)} / 5
= <strong>${sC.mu}</strong><br>
② 편차와 그 제곱:<br>
${sC.v.map(x => ` ${x} − ${sC.mu} = ${(x - sC.mu).toFixed(1)}
→ ${Math.pow(x - sC.mu, 2).toFixed(2)}`).join("<br>")}<br>
분산 = 합 ${sC.v.reduce((a, b) => a + Math.pow(b - sC.mu, 2), 0).toFixed(2)} / 5
= ${sC.varr.toFixed(3)} → s = √${sC.varr.toFixed(3)} = <strong>${sC.s.toFixed(3)}</strong><br>
③ z = (${sC.x} − ${sC.mu}) / ${sC.s.toFixed(3)} = <strong>${sC.z.toFixed(3)}</strong><br>
<span style="color:#555;">이 μ와 s는 <strong>훈련 데이터에서만</strong> 계산하고,
검증·테스트에는 <strong>같은 값을 그대로</strong> 적용한다.
검증 데이터를 섞어 계산하면 데이터 누수(leakage)다.</span>`) : html``18.4 유형 D — 원-핫 인코딩과 입력 크기 [응용]
sD = {
const rng = dojoRng(sD_seed);
const nNum = dojoInt(rng, 2, 6);
const cats = [];
const k = dojoInt(rng, 2, 3);
const names = ["부서", "지역", "제품 등급", "배송 수단", "요일"];
for (let i = 0; i < k; i++) cats.push({ n: names[i], c: dojoInt(rng, 3, 8) });
const catCols = cats.reduce((a, b) => a + b.c, 0);
const inCols = nNum + catCols;
const nHid = dojoInt(rng, 4, 16);
return { nNum, cats, catCols, inCols, nHid, p1: inCols * nHid + nHid };
}dojoProblem(`문제 D — 원-핫 후 입력 크기`, `
정형 데이터에 <strong>숫자형 변수 ${sD.nNum}개</strong>와 다음 범주형 변수가 있다.
<div style="margin:0.5em 0;padding:0.5em 0.8em;background:#F8F9FA;border-radius:6px;">
${sD.cats.map(c => `<strong>${c.n}</strong> — 범주 ${c.c}개`).join("<br>")}
</div>
범주형 변수를 모두 <strong>원-핫 인코딩</strong>한 뒤 이 데이터를
은닉층 <strong>${sD.nHid}개</strong> 노드의 신경망에 넣는다.<br>
① <strong>입력층 노드 수</strong>, ② <strong>입력층 → 은닉층 파라미터 수</strong>(가중치+편향)를 구하라.`)viewof sD_ans_in = Inputs.number({ label: "입력층 노드 수 =", step: 1 })
viewof sD_ans_p = Inputs.number({ label: "파라미터 수 =", step: 1 })
viewof sD_grade = Inputs.toggle({ label: "채점", value: false })
viewof sD_show = Inputs.toggle({ label: "풀이 보기", value: false })sD_grade ? dojoResult(
[dojoMark("입력층 노드 수", dojoOk(sD_ans_in, sD.inCols, 0.001)),
dojoMark("파라미터 수", dojoOk(sD_ans_p, sD.p1, 0.001))],
dojoOk(sD_ans_in, sD.inCols, 0.001) && dojoOk(sD_ans_p, sD.p1, 0.001)
) : html``sD_show ? dojoSolution(`
① 원-핫은 <strong>범주 수만큼의 0/1 열</strong>을 만든다:<br>
${sD.cats.map(c => ` ${c.n} → ${c.c}개 열`).join("<br>")}<br>
범주형이 만든 열 = ${sD.cats.map(c => c.c).join(" + ")} = ${sD.catCols}<br>
입력층 노드 = 숫자형 ${sD.nNum} + ${sD.catCols} = <strong>${sD.inCols}</strong><br>
② 파라미터 = 입력 × 은닉 + 편향 = ${sD.inCols} × ${sD.nHid} + ${sD.nHid}
= ${sD.inCols * sD.nHid} + ${sD.nHid} = <strong>${sD.p1}</strong><br>
<span style="color:#555;">범주가 많은 변수 하나가 입력층을 크게 부풀리고,
그만큼 파라미터가 늘어난다. 범주가 수백 개인 변수(예: 우편번호)를 그대로 원-핫하면
모형이 감당할 수 없이 커진다.</span>`) : html``18.5 유형 E — 에폭과 이터레이션 [응용]
sE = {
const rng = dojoRng(sE_seed);
const n = dojoInt(rng, 300, 5000);
const B = dojoPick(rng, [16, 32, 64, 128, 256]);
const E = dojoInt(rng, 5, 40);
const it = Math.ceil(n / B);
return { n, B, E, it, total: it * E, last: n % B === 0 ? B : n % B };
}dojoProblem(`문제 E — 갱신 횟수 세기`, `
훈련 데이터가 <strong>${sE.n.toLocaleString()}건</strong>이고,
배치 크기 <strong>B = ${sE.B}</strong> 의 미니배치 경사하강법으로
<strong>${sE.E} 에폭</strong> 학습한다. (마지막 배치는 남는 만큼만 사용)<br>
① <strong>1 에폭당 이터레이션(파라미터 갱신) 횟수</strong>,
② <strong>전체 학습 동안의 총 갱신 횟수</strong>를 구하라.`)viewof sE_ans_it = Inputs.number({ label: "1 에폭당 이터레이션 =", step: 1 })
viewof sE_ans_tot = Inputs.number({ label: "총 갱신 횟수 =", step: 1 })
viewof sE_grade = Inputs.toggle({ label: "채점", value: false })
viewof sE_show = Inputs.toggle({ label: "풀이 보기", value: false })sE_grade ? dojoResult(
[dojoMark("1 에폭당 이터레이션", dojoOk(sE_ans_it, sE.it, 0.001)),
dojoMark("총 갱신 횟수", dojoOk(sE_ans_tot, sE.total, 0.001))],
dojoOk(sE_ans_it, sE.it, 0.001) && dojoOk(sE_ans_tot, sE.total, 0.001)
) : html``sE_show ? dojoSolution(`
① 이터레이션 = ⌈n / B⌉ = ⌈${sE.n} / ${sE.B}⌉ = ⌈${(sE.n / sE.B).toFixed(3)}⌉
= <strong>${sE.it}</strong><br>
(꽉 찬 배치 ${Math.floor(sE.n / sE.B)}개 + 마지막에 ${sE.last}건짜리 배치
${sE.n % sE.B === 0 ? "없음" : "1개"})<br>
② 총 갱신 = ${sE.it} × ${sE.E} 에폭 = <strong>${sE.total.toLocaleString()}</strong><br>
<span style="color:#555;">배치를 키우면 1에폭당 갱신 횟수가 줄어든다 —
같은 에폭 수라도 <strong>학습이 덜 진행</strong>된다. 배치를 키울 때 학습률도 함께
키우는 관행은 여기서 나온다.</span>`) : html``18.6 유형 F — 2층 역전파 ① 출력층 [도전]
sF = {
const rng = dojoRng(sF_seed);
const x = dojoInt(rng, 5, 20) / 10;
const w1 = dojoInt(rng, -15, 15) / 10 || 0.5;
const b1 = dojoInt(rng, -10, 10) / 10;
const w2 = dojoInt(rng, 5, 20) / 10;
const b2 = dojoInt(rng, -10, 10) / 10;
const y = dojoInt(rng, 0, 20) / 10;
const z1 = w1 * x + b1;
const h = 1 / (1 + Math.exp(-z1));
const yh = w2 * h + b2;
const dLdy = -2 * (y - yh);
return {
x, w1, b1, w2, b2, y,
z1: Math.round(z1 * 10000) / 10000,
h: Math.round(h * 10000) / 10000,
yh: Math.round(yh * 10000) / 10000,
dLdy: Math.round(dLdy * 10000) / 10000,
gw2: Math.round(dLdy * h * 1000) / 1000,
gb2: Math.round(dLdy * 1000) / 1000
};
}dojoProblem(`문제 F — 순전파와 출력층 그래디언트`, `
은닉 노드 1개짜리 2층 신경망이다.
<div style="text-align:center;margin:0.5em 0;padding:0.5em;background:#F8F9FA;border-radius:6px;">
x → <strong>z₁ = w₁x + b₁</strong> → <strong>h = σ(z₁)</strong>
→ <strong>ŷ = w₂h + b₂</strong> → <strong>L = (y − ŷ)²</strong>
</div>
<strong>x = ${sF.x}, y = ${sF.y}</strong>,
파라미터는 <strong>w₁ = ${sF.w1}, b₁ = ${sF.b1}, w₂ = ${sF.w2}, b₂ = ${sF.b2}</strong>.<br>
① <strong>h</strong>, ② <strong>ŷ</strong>, ③ <strong>∂L/∂w₂</strong> 를 구하라.
<div style="margin-top:0.4em;color:#666;font-size:0.92em;">
∂L/∂ŷ = −2(y − ŷ) · ∂ŷ/∂w₂ = h → ∂L/∂w₂ = ∂L/∂ŷ × h
</div>`)viewof sF_ans_h = Inputs.number({ label: "h =", step: 0.001 })
viewof sF_ans_yh = Inputs.number({ label: "ŷ =", step: 0.001 })
viewof sF_ans_g = Inputs.number({ label: "∂L/∂w₂ =", step: 0.001 })
viewof sF_grade = Inputs.toggle({ label: "채점", value: false })
viewof sF_show = Inputs.toggle({ label: "풀이 보기", value: false })sF_grade ? dojoResult(
[dojoMark("h", dojoOk(sF_ans_h, sF.h, 0.0015)),
dojoMark("ŷ", dojoOk(sF_ans_yh, sF.yh, 0.0025)),
dojoMark("∂L/∂w₂", dojoOk(sF_ans_g, sF.gw2, 0.0035))],
dojoOk(sF_ans_h, sF.h, 0.0015) && dojoOk(sF_ans_yh, sF.yh, 0.0025) && dojoOk(sF_ans_g, sF.gw2, 0.0035)
) : html``sF_show ? dojoSolution(`
<strong>순전파</strong> (왼쪽 → 오른쪽)<br>
z₁ = ${sF.w1} × ${sF.x} + ${sF.b1} = <strong>${sF.z1}</strong><br>
h = σ(${sF.z1}) = 1/(1 + e^(−${sF.z1})) = <strong>${sF.h.toFixed(4)}</strong><br>
ŷ = ${sF.w2} × ${sF.h.toFixed(4)} + ${sF.b2} = <strong>${sF.yh.toFixed(4)}</strong><br><br>
<strong>역전파</strong> (오른쪽 → 왼쪽)<br>
∂L/∂ŷ = −2(y − ŷ) = −2 × (${sF.y} − ${sF.yh.toFixed(4)})
= <strong>${sF.dLdy.toFixed(4)}</strong><br>
∂L/∂w₂ = ∂L/∂ŷ × h = ${sF.dLdy.toFixed(4)} × ${sF.h.toFixed(4)}
= <strong>${sF.gw2.toFixed(3)}</strong><br>
(참고: ∂L/∂b₂ = ∂L/∂ŷ × 1 = ${sF.gb2.toFixed(3)})<br>
<span style="color:#555;">순전파에서 계산한 h 를 역전파에서 <strong>다시 쓴다</strong>.
그래서 프레임워크는 순전파 중간값을 메모리에 보관한다.</span>`) : html``18.7 유형 G — 2층 역전파 ② 연쇄법칙 끝까지 [도전]
sG = {
const rng = dojoRng(sG_seed);
const x = dojoInt(rng, 5, 20) / 10;
const w2 = dojoInt(rng, 5, 25) / 10;
const h = dojoInt(rng, 20, 80) / 100;
const yh = dojoInt(rng, 0, 25) / 10;
const y = dojoInt(rng, 0, 25) / 10;
const dLdy = -2 * (y - yh);
const dLdh = dLdy * w2;
const sig = h * (1 - h);
const dLdz = dLdh * sig;
return {
x, w2, h, yh, y,
dLdy: Math.round(dLdy * 10000) / 10000,
dLdh: Math.round(dLdh * 10000) / 10000,
sig: Math.round(sig * 10000) / 10000,
dLdz: Math.round(dLdz * 10000) / 10000,
gw1: Math.round(dLdz * x * 1000) / 1000,
gb1: Math.round(dLdz * 1000) / 1000
};
}dojoProblem(`문제 G — ∂L/∂w₁ 구하기`, `
같은 구조의 2층 신경망에서 <strong>순전파 결과가 이미 다음과 같이 계산되어 있다</strong>.
<div style="margin:0.5em 0;padding:0.6em 0.9em;background:#F8F9FA;border-radius:6px;line-height:1.9;">
입력 <strong>x = ${sG.x}</strong>, 정답 <strong>y = ${sG.y}</strong><br>
은닉 출력 <strong>h = ${sG.h}</strong>, 예측 <strong>ŷ = ${sG.yh}</strong>,
출력층 가중치 <strong>w₂ = ${sG.w2}</strong>
</div>
연쇄법칙으로 <strong>∂L/∂w₁</strong> 을 구하라. (소수 셋째 자리까지)
<div style="margin-top:0.5em;padding:0.5em 0.8em;background:#EAF0FA;border-radius:6px;font-size:0.93em;">
경로: ∂L/∂w₁ = <strong>∂L/∂ŷ</strong> × <strong>∂ŷ/∂h</strong>
× <strong>∂h/∂z₁</strong> × <strong>∂z₁/∂w₁</strong><br>
= [−2(y − ŷ)] × [w₂] × [h(1−h)] × [x]
</div>`)viewof sG_ans_dz = Inputs.number({ label: "∂L/∂z₁ =", step: 0.001 })
viewof sG_ans_gw = Inputs.number({ label: "∂L/∂w₁ =", step: 0.001 })
viewof sG_grade = Inputs.toggle({ label: "채점", value: false })
viewof sG_show = Inputs.toggle({ label: "풀이 보기", value: false })sG_grade ? dojoResult(
[dojoMark("∂L/∂z₁", dojoOk(sG_ans_dz, sG.dLdz, 0.0035)),
dojoMark("∂L/∂w₁", dojoOk(sG_ans_gw, sG.gw1, 0.0035))],
dojoOk(sG_ans_dz, sG.dLdz, 0.0035) && dojoOk(sG_ans_gw, sG.gw1, 0.0035)
) : html``sG_show ? dojoSolution(`
화살표를 하나씩 거슬러 올라가며 <strong>곱하기만</strong> 한다.<br>
① ∂L/∂ŷ = −2(${sG.y} − ${sG.yh}) = <strong>${sG.dLdy.toFixed(4)}</strong><br>
② ∂L/∂h = ∂L/∂ŷ × w₂ = ${sG.dLdy.toFixed(4)} × ${sG.w2}
= <strong>${sG.dLdh.toFixed(4)}</strong><br>
③ σ'(z₁) = h(1−h) = ${sG.h} × ${(1 - sG.h).toFixed(2)} = ${sG.sig.toFixed(4)}<br>
∂L/∂z₁ = ∂L/∂h × σ'(z₁) = ${sG.dLdh.toFixed(4)} × ${sG.sig.toFixed(4)}
= <strong>${sG.dLdz.toFixed(4)}</strong><br>
④ ∂L/∂w₁ = ∂L/∂z₁ × x = ${sG.dLdz.toFixed(4)} × ${sG.x}
= <strong>${sG.gw1.toFixed(3)}</strong><br>
(참고: ∂L/∂b₁ = ∂L/∂z₁ × 1 = ${sG.gb1.toFixed(3)})<br><br>
<span style="color:#555;">주목: ③에서 곱한 <strong>${sG.sig.toFixed(4)}</strong> 는 1보다 작다.
층이 깊어질수록 이런 값이 계속 곱해져 앞쪽 층의 그래디언트가 작아진다 —
<strong>기울기 소실</strong>. 유형 H에서 확인한다.</span>`) : html``18.8 유형 H — 기울기 소실 [도전]
sH = {
const rng = dojoRng(sH_seed);
const k = dojoInt(rng, 3, 4);
const v = [];
for (let i = 0; i < k; i++) v.push(dojoInt(rng, 4, 25) / 100);
const prod = v.reduce((a, b) => a * b, 1);
const nMax = Math.ceil(Math.log(0.001) / Math.log(0.25));
return { k, v, prod: Math.round(prod * 1000000) / 1000000, nMax };
}dojoProblem(`문제 H — 그래디언트는 얼마나 줄어드는가`, `
Sigmoid 은닉층이 <strong>${sH.k}개</strong> 쌓인 신경망에서, 역전파가 각 층을 지날 때
곱해지는 σ'(z) 값이 다음과 같이 계산되었다.
<div style="text-align:center;margin:0.5em 0;font-size:1.05em;">
<strong>${sH.v.join(", ")}</strong>
</div>
① 이 ${sH.k}개 층을 모두 지났을 때 그래디언트에 곱해지는 <strong>감쇠 계수</strong>
(네 값의 곱)를 구하라. <span style="color:#666;">(소수 여섯째 자리까지)</span><br>
② 같은 네트워크의 은닉층이 모두 <strong>ReLU (z > 0 구간)</strong> 였다면
이 감쇠 계수는 얼마인가?`)viewof sH_ans_p = Inputs.number({ label: "Sigmoid 감쇠 계수 =", step: 0.000001 })
viewof sH_ans_r = Inputs.number({ label: "ReLU였다면 =", step: 0.01 })
viewof sH_grade = Inputs.toggle({ label: "채점", value: false })
viewof sH_show = Inputs.toggle({ label: "풀이 보기", value: false })sH_grade ? dojoResult(
[dojoMark("Sigmoid 감쇠 계수", dojoOk(sH_ans_p, sH.prod, 0.0000015)),
dojoMark("ReLU 감쇠 계수", dojoOk(sH_ans_r, 1, 0.001))],
dojoOk(sH_ans_p, sH.prod, 0.0000015) && dojoOk(sH_ans_r, 1, 0.001)
) : html``sH_show ? dojoSolution(`
① 역전파는 각 층의 국소 미분을 <strong>곱</strong>해 나간다:<br>
${sH.v.join(" × ")} = <strong>${sH.prod}</strong><br>
즉 앞쪽 층에 도달한 그래디언트는 원래의 약
<strong>${(sH.prod * 100).toFixed(4)}%</strong> 로 줄어든다.<br>
② ReLU는 z > 0 에서 도함수가 <strong>항상 1</strong> 이므로,
몇 층을 지나도 1 × 1 × … = <strong>1</strong>. 그래디언트가 줄지 않는다.<br><br>
<span style="color:#555;">σ'(z)의 최댓값은 0.25다. 최선의 경우에도 층마다 ¼로 줄어든다 —
${sH.nMax}개 층만 지나도 1/1000 이하가 된다.
이것이 깊은 신경망에서 <strong>Sigmoid를 은닉층에 쓰지 않는</strong> 이유이고,
ReLU가 표준이 된 이유다.</span>`) : html``18.9 다 풀었다면
- 각 유형에서 연속 3문제를 실수 없이 풀었다면 4주차는 완성이다.
- 유형 F·G를 막힘없이 풀 수 있다면 중간고사 역전파 문항은 준비가 끝난 것이다.
- 막히는 유형이 있으면 Ch04 학습의 실전으로 돌아가라.
- 앞 주차: 3주차 손실함수와 경사하강법