▶ 展示 · 回索引
07-backprop-single-neuron-delta.html
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>kp6 反向傳播 ① 單一神經元與敏感度 δ · Backpropagation (1/3)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.19.0/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.19.0/dist/katex.min.js"></script>
<style>
html,body{margin:0;height:100%;background:#0b1630;overflow:hidden}
body{display:flex;align-items:center;justify-content:center;font-family:"Noto Sans TC","Noto Sans CJK TC","PingFang TC","Microsoft JhengHei",sans-serif}
.stage{position:relative;width:min(100vw,177.7778vh);height:min(56.25vw,100vh);aspect-ratio:16/9;background:#f3f6fb;overflow:hidden}
canvas{position:absolute;inset:0;width:100%;height:100%;display:block;outline:none}
canvas:focus-visible{outline:3px solid #2459d6;outline-offset:-3px}
#math-layer{position:absolute;left:0;top:0;width:1600px;height:900px;transform-origin:top left;pointer-events:none}
.formula{position:absolute;white-space:nowrap;line-height:1.25}
.formula .katex-display{margin:0}
.formula .katex{font-size:1em}
.formula.left .katex-display,.formula.left .katex-display>.katex{text-align:left}
.formula.right .katex-display,.formula.right .katex-display>.katex{text-align:right}
.formula.center .katex-display,.formula.center .katex-display>.katex{text-align:center}
</style>
</head>
<body>
<div class="stage" id="stage">
<canvas id="canvas" width="1600" height="900" tabindex="0" role="img" aria-label="kp6 反向傳播 ① 單一神經元與敏感度 δ · Backpropagation (1/3)"></canvas>
<div id="math-layer"></div>
</div>
<script>
'use strict';
/* ===== InfographicsCreation shared runtime (Canvas 1600x900 + KaTeX math layer) ===== */
const W = 1600, H = 900;
const P = { paper:'#f3f6fb', grid:'#dfe7f2', ink:'#0f2347', muted:'#475b7d', faint:'#8ea3c3', line:'#c7d4e8',
blue:'#2459d6', blueS:'#dde8fd', red:'#d3342a', redS:'#fde3df', green:'#12935a', greenS:'#d7f2e4',
amber:'#d9760a', amberS:'#fdebcd', violet:'#6a43dc', violetS:'#e7dffb', white:'#ffffff', navy:'#0f2347' };
const FZ = '"Noto Sans TC","Noto Sans CJK TC","PingFang TC","Microsoft JhengHei",sans-serif';
const canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d');
const stage = document.getElementById('stage'), mathLayer = document.getElementById('math-layer');
const AUD = { on: !!window.__AUDIT_ON, text: [], bad: [] };
/* ---------- font loading for canvas text ---------- */
const seenChars = new Set(); let pendingChars = '', fontTimer = null;
function noteChars(s){
for (const ch of s){ if (!seenChars.has(ch)){ seenChars.add(ch); pendingChars += ch; } }
if (pendingChars && !fontTimer && document.fonts && document.fonts.load){
fontTimer = setTimeout(() => {
const txt = pendingChars; pendingChars = ''; fontTimer = null;
Promise.all([400,500,600,700,800,900].map(w => document.fonts.load(w + ' 16px "Noto Sans TC"', txt).catch(()=>0)))
.then(() => requestDraw());
}, 40);
}
}
/* ---------- text ---------- */
function T(s, x, y, o){
o = o || {}; s = String(s); noteChars(s);
const size = o.size || 18, weight = o.weight || 500, align = o.align || 'left';
if (/[\^_]\{/.test(s)) return Tsup(s, x, y, o, size, weight, align);
ctx.font = (o.italic ? 'italic ' : '') + weight + ' ' + size + 'px ' + FZ;
ctx.fillStyle = o.color || P.ink; ctx.textAlign = align; ctx.textBaseline = 'middle';
ctx.fillText(s, x, y);
const w = ctx.measureText(s).width;
if (AUD.on){
const x0 = align === 'center' ? x - w/2 : align === 'right' ? x - w : x;
AUD.text.push({ x0, y0: y - size*0.6, x1: x0 + w, y1: y + size*0.6, s, ok: !!o.ok, maxW: o.maxW });
if (/\\n/.test(s)) AUD.bad.push('literal \\n in text: ' + s);
}
return w;
}
function Tsup(s, x, y, o, size, weight, align){
const parts = [], re = /([\^_])\{([^}]*)\}/g; let last = 0, m, total = 0;
while ((m = re.exec(s))){ if (m.index > last) parts.push([s.slice(last, m.index), 0]); parts.push([m[2], m[1] === '^' ? 1 : 2]); last = re.lastIndex; }
if (last < s.length) parts.push([s.slice(last), 0]);
parts.forEach(p => { p.sz = p[1] ? size * 0.68 : size; ctx.font = weight + ' ' + p.sz + 'px ' + FZ; p.w = ctx.measureText(p[0]).width; total += p.w; });
let cx = align === 'center' ? x - total/2 : align === 'right' ? x - total : x;
ctx.fillStyle = o.color || P.ink; ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
parts.forEach(p => { ctx.font = weight + ' ' + p.sz + 'px ' + FZ; ctx.fillText(p[0], cx, y + (p[1] === 1 ? -size * 0.34 : p[1] === 2 ? size * 0.2 : 0)); cx += p.w; });
if (AUD.on){ const x0 = align === 'center' ? x - total/2 : align === 'right' ? x - total : x; AUD.text.push({ x0, y0: y - size*0.75, x1: x0 + total, y1: y + size*0.6, s, ok: !!o.ok, maxW: o.maxW }); }
return total;
}
const CJK = /[\u2E80-\u9FFF\uF900-\uFAFF\uFF00-\uFFEF\u3000-\u303F\u2014\u2026]/;
const CLOSERS = ',。、;:!?)」』》…—,.;:!?)';
function tokenize(s){
const out = []; let buf = '';
for (const ch of s){
if (CJK.test(ch)){
if (buf){ out.push(buf); buf = ''; }
if (CLOSERS.indexOf(ch) >= 0 && out.length) out[out.length-1] += ch; else out.push(ch);
} else if (ch === ' '){ buf += ch; out.push(buf); buf = ''; } else buf += ch;
}
if (buf) out.push(buf); return out;
}
function wrapLines(s, maxW, size, weight){
ctx.font = weight + ' ' + size + 'px ' + FZ;
const lines = []; let cur = '';
for (const t of tokenize(s)){
const test = cur + t;
if (cur && ctx.measureText(test).width > maxW){ lines.push(cur.trimEnd()); cur = t.trimStart(); } else cur = test;
}
if (cur) lines.push(cur.trimEnd()); return lines;
}
/* wrap(text, x, y, maxWidth, lineHeight, fontSize, color, weight, align) -> number of lines.
textAlign=left: x is the left edge; center: x is the visual centre; right: x is the right edge. */
function wrap(t, x, y, maxW, lh, size, color, weight, align){
t = String(t).replace(/\\n/g, '\n'); size = size || 18; weight = weight || 500; align = align || 'left';
let n = 0;
for (const para of t.split('\n')){
for (const l of wrapLines(para, maxW, size, weight)){ T(l, x, y + n*lh, { size, weight, color, align, maxW }); n++; }
}
return n;
}
function wrapCenter(t, cx, y, maxW, lh, size, color, weight){ return wrap(t, cx, y, maxW, lh, size, color, weight, 'center'); }
function textWidth(s, size, weight){ ctx.font = (weight||500) + ' ' + size + 'px ' + FZ; return ctx.measureText(String(s)).width; }
/* ---------- shapes ---------- */
function rr(x, y, w, h, r){
ctx.beginPath(); ctx.moveTo(x+r, y); ctx.arcTo(x+w, y, x+w, y+h, r); ctx.arcTo(x+w, y+h, x, y+h, r);
ctx.arcTo(x, y+h, x, y, r); ctx.arcTo(x, y, x+w, y, r); ctx.closePath();
}
function panel(x, y, w, h, o){
o = o || {}; rr(x, y, w, h, o.r == null ? 16 : o.r);
ctx.fillStyle = o.fill || '#fff'; ctx.fill();
if (o.lw !== 0){ ctx.strokeStyle = o.stroke || P.line; ctx.lineWidth = o.lw || 1.6; ctx.stroke(); }
}
function arrow(x1, y1, x2, y2, color, lw, hs){
lw = lw || 3; hs = hs || 11; const a = Math.atan2(y2-y1, x2-x1);
ctx.strokeStyle = color; ctx.fillStyle = color; ctx.lineWidth = lw; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2 - Math.cos(a)*hs*0.6, y2 - Math.sin(a)*hs*0.6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x2, y2);
ctx.lineTo(x2 - hs*Math.cos(a-0.45), y2 - hs*Math.sin(a-0.45));
ctx.lineTo(x2 - hs*Math.cos(a+0.45), y2 - hs*Math.sin(a+0.45)); ctx.closePath(); ctx.fill();
}
function dot(x, y, r, fill, stroke, lw){
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2);
if (fill){ ctx.fillStyle = fill; ctx.fill(); }
if (stroke){ ctx.strokeStyle = stroke; ctx.lineWidth = lw || 1.5; ctx.stroke(); }
}
function line(x1, y1, x2, y2, color, lw, dash){
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.strokeStyle = color; ctx.lineWidth = lw || 1.5;
ctx.setLineDash(dash || []); ctx.stroke(); ctx.setLineDash([]);
}
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const lerp = (a, b, t) => a + (b - a) * t;
const ease = t => t < 0.5 ? 2*t*t : 1 - Math.pow(-2*t + 2, 2) / 2;
/* ---------- page chrome ---------- */
function chrome(m){
ctx.fillStyle = P.paper; ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = P.grid; ctx.lineWidth = 1; ctx.beginPath();
for (let x = 0; x <= W; x += 40){ ctx.moveTo(x + .5, 0); ctx.lineTo(x + .5, H); }
for (let y = 0; y <= H; y += 40){ ctx.moveTo(0, y + .5); ctx.lineTo(W, y + .5); }
ctx.stroke();
const cw = textWidth(m.tag, 15.5, 700) + 30;
rr(40, 24, cw, 28, 14); ctx.strokeStyle = P.blue; ctx.lineWidth = 1.6; ctx.stroke();
T(m.tag, 40 + cw/2, 38.5, { size: 15.5, weight: 700, color: P.blue, align: 'center' });
T(m.zh, 40, 82, { size: 42, weight: 900, color: P.ink });
T(m.en, 42, 119, { size: 22, weight: 500, color: P.blue });
T('IDL 115 · Chapter 3 — Backpropagation & Optimization in Deep Neural Networks', 40, 886, { size: 13.5, weight: 400, color: P.muted });
T(m.foot, W - 40, 886, { size: 13.5, weight: 600, color: P.muted, align: 'right' });
}
/* ---------- KaTeX math layer (shares the 1600x900 logical coordinate system) ---------- */
const MATH = {};
function placeFormula(m){
const o = m.o, el = m.el;
el.style.fontSize = (o.size || 28) + 'px'; el.style.color = o.color || P.ink; el.style.width = o.width + 'px';
el.className = 'formula ' + (o.align || 'center');
const left = o.align === 'left' ? o.x : o.align === 'right' ? o.x - o.width : o.x - o.width/2;
el.style.left = left + 'px'; el.style.top = o.y + 'px';
el.style.transform = 'translate(' + (o.offsetX || 0) + 'px,' + ((o.offsetY || 0) - 0) + 'px) translateY(-50%)';
}
function addFormula(o){
const el = document.createElement('div'); mathLayer.appendChild(el);
const m = MATH[o.id] = { el, o: Object.assign({ align:'center', size:28, width:600 }, o), tex: null };
placeFormula(m); setFormula(o.id, o.tex); return m;
}
function setFormula(id, tex, patch){
const m = MATH[id]; if (!m) return;
if (patch){ Object.assign(m.o, patch); placeFormula(m); }
if (m.tex !== tex){
m.tex = tex;
if (window.katex) window.katex.render(tex, m.el, { displayMode: true, throwOnError: false });
else { m.el.textContent = ''; AUD.bad.push('KaTeX not loaded'); }
}
}
function showFormula(id, on){ const m = MATH[id]; if (m) m.el.style.visibility = on ? 'visible' : 'hidden'; }
function syncMathLayer(){
const r = stage.getBoundingClientRect();
mathLayer.style.transform = 'scale(' + (r.width / W) + ',' + (r.height / H) + ')';
}
/* ---------- canvas widgets (slider / segmented / button) ---------- */
const UI = { items: [], focus: -1, drag: null, hover: -1 };
function sliderVal(it, t){
let v = it.log ? Math.pow(10, lerp(Math.log10(it.min), Math.log10(it.max), t)) : lerp(it.min, it.max, t);
if (it.step) v = Math.round(v / it.step) * it.step;
return clamp(+v.toFixed(6), it.min, it.max);
}
function sliderT(it){ return it.log ? (Math.log10(it.value) - Math.log10(it.min)) / (Math.log10(it.max) - Math.log10(it.min)) : (it.value - it.min) / (it.max - it.min); }
function slider(o){ const it = Object.assign({ type:'slider', color:P.blue, fmt: v => String(v), value: o.min, visible: true }, o); UI.items.push(it); return it; }
function seg(o){ const it = Object.assign({ type:'seg', color:P.blue, h:42, visible: true }, o); UI.items.push(it); return it; }
function button(o){ const it = Object.assign({ type:'button', color:P.navy, h:42, visible: true }, o); UI.items.push(it); return it; }
function isVis(it){ return typeof it.visible === 'function' ? it.visible() : it.visible; }
function uiHit(it, px, py){
if (it.type === 'slider') return px >= it.x - 14 && px <= it.x + it.w + 14 && py >= it.y - 8 && py <= it.y + 46;
return px >= it.x && px <= it.x + it.w && py >= it.y && py <= it.y + it.h;
}
function uiDraw(){
UI.items.forEach((it, idx) => {
if (!isVis(it)) return;
const focus = UI.focus === idx;
if (it.type === 'slider'){
T(it.label, it.x, it.y + 6, { size: 16, weight: 700, color: P.ink });
T(it.fmt(it.value), it.x + it.w, it.y + 6, { size: 16, weight: 800, color: it.color, align: 'right' });
const ty = it.y + 34, t = sliderT(it);
rr(it.x, ty - 3, it.w, 6, 3); ctx.fillStyle = '#d5dfef'; ctx.fill();
rr(it.x, ty - 3, it.w * t, 6, 3); ctx.fillStyle = it.color; ctx.fill();
dot(it.x + it.w * t, ty, 11, '#fff', it.color, 3.2);
if (focus){ dot(it.x + it.w * t, ty, 17, null, P.ink, 2); }
} else if (it.type === 'seg'){
const n = it.options.length, bw = it.w / n;
panel(it.x, it.y, it.w, it.h, { r: 10, fill: '#eef3fb', stroke: P.line, lw: 1.4 });
it.options.forEach((op, i) => {
const on = op.id === it.value, bx = it.x + i * bw;
if (on){ rr(bx + 3, it.y + 3, bw - 6, it.h - 6, 8); ctx.fillStyle = op.color || it.color; ctx.fill(); }
T(op.label, bx + bw/2, it.y + it.h/2, { size: it.fs || 16, weight: 800, color: on ? '#fff' : P.muted, align: 'center', maxW: bw - 8 });
});
if (focus){ rr(it.x - 3, it.y - 3, it.w + 6, it.h + 6, 12); ctx.strokeStyle = P.ink; ctx.lineWidth = 2; ctx.stroke(); }
} else if (it.type === 'button'){
const dis = typeof it.disabled === 'function' ? it.disabled() : it.disabled;
rr(it.x, it.y, it.w, it.h, 10);
ctx.fillStyle = dis ? '#dfe6f2' : (it.ghost ? '#fff' : it.color); ctx.fill();
ctx.strokeStyle = dis ? P.line : it.color; ctx.lineWidth = 1.8; ctx.stroke();
T(typeof it.text === 'function' ? it.text() : it.text, it.x + it.w/2, it.y + it.h/2, { size: it.fs || 16, weight: 800, align: 'center', color: dis ? P.faint : (it.ghost ? it.color : '#fff'), maxW: it.w - 8 });
if (focus){ rr(it.x - 3, it.y - 3, it.w + 6, it.h + 6, 12); ctx.strokeStyle = P.ink; ctx.lineWidth = 2; ctx.stroke(); }
}
});
}
function uiSet(id, v){ const it = UI.items.find(i => i.id === id); if (!it) return; it.value = v; if (it.onChange) it.onChange(v); requestDraw(); }
function uiPoint(e){ const r = canvas.getBoundingClientRect(); return [(e.clientX - r.left) * W / r.width, (e.clientY - r.top) * H / r.height]; }
canvas.addEventListener('pointerdown', e => {
const [px, py] = uiPoint(e);
for (let i = UI.items.length - 1; i >= 0; i--){
const it = UI.items[i]; if (!isVis(it) || !uiHit(it, px, py)) continue;
UI.focus = i; canvas.focus();
if (it.type === 'slider'){ UI.drag = it; canvas.setPointerCapture(e.pointerId); const v = sliderVal(it, clamp((px - it.x) / it.w, 0, 1)); if (v !== it.value){ it.value = v; it.onChange && it.onChange(v); } }
else if (it.type === 'seg'){ const k = clamp(Math.floor((px - it.x) / (it.w / it.options.length)), 0, it.options.length - 1); const v = it.options[k].id; if (v !== it.value){ it.value = v; it.onChange && it.onChange(v); } }
else if (it.type === 'button'){ const dis = typeof it.disabled === 'function' ? it.disabled() : it.disabled; if (!dis && it.onClick) it.onClick(); }
requestDraw(); return;
}
});
canvas.addEventListener('pointermove', e => {
const [px, py] = uiPoint(e);
if (UI.drag){ const it = UI.drag, v = sliderVal(it, clamp((px - it.x) / it.w, 0, 1)); if (v !== it.value){ it.value = v; it.onChange && it.onChange(v); requestDraw(); } return; }
let over = false; UI.items.forEach(it => { if (isVis(it) && uiHit(it, px, py)) over = true; });
canvas.style.cursor = over ? 'pointer' : 'default';
});
canvas.addEventListener('pointerup', () => { UI.drag = null; });
canvas.addEventListener('keydown', e => {
const vis = UI.items.map((it, i) => i).filter(i => isVis(UI.items[i]));
if (!vis.length) return;
if (e.key === 'Tab'){
const cur = vis.indexOf(UI.focus); let nxt = cur + (e.shiftKey ? -1 : 1);
if (cur === -1) nxt = e.shiftKey ? vis.length - 1 : 0;
if (nxt < 0 || nxt >= vis.length){ UI.focus = -1; requestDraw(); return; }
UI.focus = vis[nxt]; e.preventDefault(); requestDraw(); return;
}
const it = UI.items[UI.focus]; if (!it) return;
if (it.type === 'slider' && (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'ArrowUp')){
const dir = (e.key === 'ArrowRight' || e.key === 'ArrowUp') ? 1 : -1, t = sliderT(it);
const step = it.step ? (it.log ? 0 : it.step) : (it.max - it.min) / 50;
let v = it.log ? sliderVal(it, clamp(t + dir * 0.05, 0, 1)) : clamp(+(it.value + dir * step).toFixed(6), it.min, it.max);
it.value = v; it.onChange && it.onChange(v); e.preventDefault(); requestDraw();
} else if (it.type === 'seg' && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')){
const k = it.options.findIndex(o => o.id === it.value), n = it.options.length;
it.value = it.options[(k + (e.key === 'ArrowRight' ? 1 : n - 1)) % n].id; it.onChange && it.onChange(it.value); e.preventDefault(); requestDraw();
} else if (it.type === 'button' && (e.key === 'Enter' || e.key === ' ')){ it.onClick && it.onClick(); e.preventDefault(); requestDraw(); }
});
canvas.addEventListener('blur', () => { UI.focus = -1; requestDraw(); });
/* ---------- render loop ---------- */
let drawFn = null, pending = false, animating = false;
function setAnimating(b){ animating = b; if (b) requestDraw(); }
function requestDraw(){ if (pending) return; pending = true; requestAnimationFrame(render); }
function fitCanvas(){
const r = stage.getBoundingClientRect(), dpr = Math.min(window.devicePixelRatio || 1, 2);
const s = clamp(r.width * dpr / W, 1, 2.4), w = Math.round(W * s), h = Math.round(H * s);
if (canvas.width !== w || canvas.height !== h){ canvas.width = w; canvas.height = h; }
return s;
}
function render(now){
pending = false; const s = fitCanvas(); ctx.setTransform(s, 0, 0, s, 0, 0);
AUD.text.length = 0; AUD.bad.length = 0;
ctx.clearRect(0, 0, W, H); drawFn(now || performance.now()); uiDraw();
if (animating) requestDraw();
}
function start(fn, ariaLabel){
drawFn = fn; if (ariaLabel) canvas.setAttribute('aria-label', ariaLabel);
syncMathLayer(); new ResizeObserver(() => { syncMathLayer(); requestDraw(); }).observe(stage);
window.addEventListener('resize', () => { syncMathLayer(); requestDraw(); });
requestDraw(); window.__ready = true;
}
/* ---------- audit hooks (used by the automated browser render audit) ---------- */
window.__ui = { set: uiSet, items: UI.items };
window.__audit = function(){
const issues = [], M = 4, boxes = AUD.text;
AUD.bad.forEach(b => issues.push('BAD: ' + b));
boxes.forEach(b => {
if (b.x0 < M || b.x1 > W - M || b.y0 < M || b.y1 > H - M) issues.push('OUT-OF-CANVAS: "' + b.s + '"');
if (b.maxW && (b.x1 - b.x0) > b.maxW + 2) issues.push('WIDER-THAN-MAXW: "' + b.s + '" (' + Math.round(b.x1 - b.x0) + '>' + Math.round(b.maxW) + ')');
});
for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++){
const a = boxes[i], b = boxes[j]; if (a.ok || b.ok) continue;
const ox = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0), oy = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
if (ox > 3 && oy > 5) issues.push('TEXT-OVERLAP: "' + a.s + '" x "' + b.s + '"');
}
const sr = stage.getBoundingClientRect(), sc = sr.width / W, fr = [];
Object.keys(MATH).forEach(id => {
const m = MATH[id]; if (m.el.style.visibility === 'hidden') return;
const h = m.el.querySelector('.katex-html'); if (!h){ issues.push('FORMULA-NOT-RENDERED: ' + id); return; }
const r = h.getBoundingClientRect(), box = { id, x0: (r.left - sr.left) / sc, x1: (r.right - sr.left) / sc, y0: (r.top - sr.top) / sc, y1: (r.bottom - sr.top) / sc };
fr.push(box);
if (box.x0 < M || box.x1 > W - M || box.y0 < M || box.y1 > H - M) issues.push('FORMULA-OUT-OF-CANVAS: ' + id);
const allow = m.o.width, cx = m.o.align === 'left' ? m.o.x : m.o.align === 'right' ? m.o.x - allow : m.o.x - allow/2;
if (box.x0 < cx - 2 || box.x1 > cx + allow + 2) issues.push('FORMULA-OVERFLOW-WIDTH: ' + id + ' (' + Math.round(box.x1 - box.x0) + ' > ' + allow + ')');
if (/\\\\[A-Za-z]/.test(m.tex) && m.tex.indexOf('\\\\') !== -1) issues.push('SUSPECT-DOUBLE-BACKSLASH: ' + id);
const txt = h.textContent || '';
['hatp', 'frac', 'cdot', 'approx', 'theta', 'sigma', 'boldsymbol', 'nabla', 'partial'].forEach(w => { if (txt.indexOf(w) >= 0) issues.push('RAW-TEX-VISIBLE(' + w + '): ' + id); });
boxes.forEach(b => {
const ox = Math.min(box.x1, b.x1) - Math.max(box.x0, b.x0), oy = Math.min(box.y1, b.y1) - Math.max(box.y0, b.y0);
if (!b.ok && ox > 3 && oy > 5) issues.push('FORMULA-TEXT-OVERLAP: ' + id + ' x "' + b.s + '"');
});
});
for (let i = 0; i < fr.length; i++) for (let j = i + 1; j < fr.length; j++){
const a = fr[i], b = fr[j], ox = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0), oy = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
if (ox > 3 && oy > 3) issues.push('FORMULA-OVERLAP: ' + a.id + ' x ' + b.id);
}
return { issues, formulas: fr.map(f => ({ id: f.id, nx: +(f.x0 / W).toFixed(4), ny: +(f.y0 / H).toFixed(4), nw: +((f.x1 - f.x0) / W).toFixed(4) })), nText: boxes.length };
};
</script>
<script>
(function boot(){
if (!window.katex && !window.__katexWait){ window.__katexWait = 0; }
if (!window.katex && window.__katexWait++ < 60){ return setTimeout(boot, 100); }
if (!window.katex) console.warn('KaTeX failed to load');
/* shared helpers for the backpropagation pages */
const SIG = {
sq: { label: 'a²', f: a => a * a, d: a => 2 * a },
tanh: { label: 'tanh', f: a => Math.tanh(a), d: a => 1 - Math.tanh(a) * Math.tanh(a) },
relu: { label: 'ReLU', f: a => Math.max(0, a), d: a => a > 0 ? 1 : 0 }
};
function f2(v, n){ n = n == null ? 2 : n; const s = v.toFixed(n); return /^-0\.?0*$/.test(s) ? s.slice(1) : s; }
const signCol = v => v >= 0 ? P.blue : P.red;
function lockedSlot(x, y, w, h, msg){
rr(x, y, w, h, 12); ctx.setLineDash([7, 5]); ctx.strokeStyle = P.faint; ctx.lineWidth = 1.6; ctx.stroke(); ctx.setLineDash([]);
T(msg, x + w/2, y + h/2, { size: 16, weight: 600, color: P.faint, align: 'center', maxW: w - 20 });
}
/* ===== shared helpers for all course pages (Deriv panel, plots, sims) ===== */
function mulberry(a){ 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; }; }
function gauss(r){ let u = 0; while (!u) u = r(); const v = r(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); }
function f2(v, n){ n = n == null ? 2 : n; const s = v.toFixed(n); return /^-0\.?0*$/.test(s) ? s.slice(1) : s; }
const LPG = { x: 40, y: 150, w: 600, h: 710 }, RPG = { x: 660, y: 150, w: 900, h: 710 };
/* plot box helper */
function mkP(x, y, w, h, xr, yr, o){ o = o || {};
const p = { x, y, w, h, xr, yr, o };
p.X = v => x + (v - xr[0]) / (xr[1] - xr[0]) * w;
p.Y = o.logY ? (v => y + h - (Math.log10(Math.max(v, 1e-300)) - Math.log10(yr[0])) / (Math.log10(yr[1]) - Math.log10(yr[0])) * h) : (v => y + h - (v - yr[0]) / (yr[1] - yr[0]) * h);
return p; }
function pframe(p, o){ o = o || {};
if (o.fill !== false){ rr(p.x - 6, p.y - 6, p.w + 12, p.h + 12, 8); ctx.fillStyle = '#f6f9ff'; ctx.fill(); }
(o.xticks || []).forEach(t => { const v = Array.isArray(t) ? t[0] : t, lab = Array.isArray(t) ? t[1] : String(t);
if (o.grid) line(p.X(v), p.y, p.X(v), p.y + p.h, '#e1e8f3', 1); T(lab, p.X(v), p.y + p.h + 15, { size: 12.5, weight: 600, color: P.muted, align: 'center' }); });
(o.yticks || []).forEach(t => { const v = Array.isArray(t) ? t[0] : t, lab = Array.isArray(t) ? t[1] : String(t);
if (o.grid !== false) line(p.x, p.Y(v), p.x + p.w, p.Y(v), '#e1e8f3', 1); T(lab, p.x - 8, p.Y(v), { size: 12.5, weight: 600, color: P.muted, align: 'right' }); });
line(p.x, p.y + p.h, p.x + p.w, p.y + p.h, P.faint, 1.5); line(p.x, p.y, p.x, p.y + p.h, P.faint, 1.5);
if (o.xl) T(o.xl, p.x + p.w, p.y + p.h + 32, { size: 13, weight: 700, color: P.muted, align: 'right' });
if (o.yl) T(o.yl, p.x, p.y - 14, { size: 13, weight: 700, color: P.muted });
}
function pcurve(p, pts, color, lw, dash, upto){ if (pts.length < 2) return; ctx.save(); ctx.beginPath(); ctx.rect(p.x - 1, p.y - 1, p.w + 2, p.h + 2); ctx.clip();
ctx.beginPath(); const n = upto == null ? pts.length - 1 : Math.min(pts.length - 1, upto);
for (let i = 0; i <= n; i++){ const q = pts[i]; i ? ctx.lineTo(p.X(q[0]), p.Y(q[1])) : ctx.moveTo(p.X(q[0]), p.Y(q[1])); }
ctx.strokeStyle = color; ctx.lineWidth = lw || 2.6; ctx.lineJoin = 'round'; ctx.setLineDash(dash || []); ctx.stroke(); ctx.setLineDash([]); ctx.restore(); }
function pfn(p, fn, x0, x1, color, lw, dash, N){ const pts = []; N = N || 240; for (let i = 0; i <= N; i++){ const x = x0 + (x1 - x0) * i / N; pts.push([x, fn(x)]); } pcurve(p, pts, color, lw, dash); }
function pdot(p, x, y, r, fill, stroke){ dot(p.X(x), p.Y(y), r, fill, stroke || '#fff', 2); }
/* elliptical contours of 0.5*(a x^2 + b y^2) */
function ellipses(p, a, b, levels, cx, cy){ ctx.save(); ctx.beginPath(); ctx.rect(p.x, p.y, p.w, p.h); ctx.clip();
levels.forEach((c, i) => { const rx = Math.sqrt(2 * c / a), ry = Math.sqrt(2 * c / b); ctx.beginPath(); ctx.ellipse(p.X(cx || 0), p.Y(cy || 0), rx * p.w / (p.xr[1] - p.xr[0]), ry * p.h / (p.yr[1] - p.yr[0]), 0, 0, Math.PI * 2);
ctx.strokeStyle = i < 2 ? '#7fa0ea' : '#b4c8f2'; ctx.lineWidth = 1.3; ctx.stroke(); }); ctx.restore(); }
function badge(x, y, text, bg, fg, size){ size = size || 14; const w = textWidth(text, size, 800) + 22; rr(x, y - 13, w, 26, 13); ctx.fillStyle = bg; ctx.fill(); T(text, x + w / 2, y, { size, weight: 800, color: fg, align: 'center' }); return w; }
function card(x, y, w, h, o){ o = o || {}; rr(x, y, w, h, o.r || 12); ctx.fillStyle = o.fill || '#fafcff'; ctx.fill(); ctx.strokeStyle = o.stroke || P.line; ctx.lineWidth = o.lw || 1.5; ctx.stroke(); }
/* stepwise derivation panel: scenes = [{tab, formulas:[{tex,h,size}], note:[zh,en], draw?}] */
function Deriv(o){
const D = { step: 1, o }, gap = o.gap == null ? 12 : o.gap, n = o.scenes.length;
const top = o.y + 124, noteH = o.noteH || 92, noteTop = o.y + o.h - 24 - noteH, areaBottom = noteTop - 14;
seg({ id: o.id, x: o.x + 24, y: o.y + 62, w: o.w - 48, h: 44, value: 1, fs: n > 3 ? 14 : 15, color: P.blue,
options: o.scenes.map((s, i) => ({ id: i + 1, label: s.tab })), onChange: v => { D.step = v; sync(); } });
o.scenes.forEach((s, i) => {
const total = s.formulas.reduce((q, f) => q + f.h, 0) + Math.max(0, s.formulas.length - 1) * gap;
let cy = top + ((areaBottom - top) - total) / 2;
s.formulas.forEach((f, j) => { f.id = 'd' + o.id + '_' + i + '_' + j; addFormula({ id: f.id, tex: f.tex, x: o.x + o.w / 2, y: cy + f.h / 2, width: o.w - 72, size: f.size || 26 }); cy += f.h + gap; });
});
function sync(){ o.scenes.forEach((s, i) => s.formulas.forEach(f => showFormula(f.id, i + 1 === D.step))); }
sync();
D.draw = function(){
panel(o.x, o.y, o.w, o.h);
T(o.title[0], o.x + 24, o.y + 28, { size: 23, weight: 800, maxW: o.w - 48 });
const s = o.scenes[D.step - 1];
card(o.x + 24, top - 6, o.w - 48, areaBottom - top + 12, { fill: '#f3f7ff', stroke: P.blue, lw: 2 });
if (s.draw) s.draw({ x: o.x + 24, y: top - 6, w: o.w - 48, h: areaBottom - top + 12 });
const k = wrap(s.note[0], o.x + 28, noteTop + 10, o.w - 56, 21, 15.5, P.ink, 700);
wrap(s.note[1], o.x + 28, noteTop + 10 + k * 21 + 6, o.w - 56, 18, 13.5, P.muted, 500);
};
D.set = v => uiSet(o.id, v);
return D;
}
/* generic contour lines by marching squares */
function contourLines(p, f, levels, color, lw, nx, ny){
nx = nx || 110; ny = ny || 70; const gx = [], gy = [], V = [];
for (let i = 0; i <= nx; i++) gx.push(p.xr[0] + (p.xr[1] - p.xr[0]) * i / nx);
for (let j = 0; j <= ny; j++) gy.push(p.yr[0] + (p.yr[1] - p.yr[0]) * j / ny);
for (let i = 0; i <= nx; i++){ V.push([]); for (let j = 0; j <= ny; j++) V[i].push(f(gx[i], gy[j])); }
ctx.save(); ctx.beginPath(); ctx.rect(p.x, p.y, p.w, p.h); ctx.clip(); ctx.lineWidth = lw || 1.3; ctx.lineCap = 'round';
levels.forEach((lv, li) => {
ctx.strokeStyle = typeof color === 'function' ? color(lv, li) : (color || '#9db8ee'); ctx.beginPath();
for (let i = 0; i < nx; i++) for (let j = 0; j < ny; j++){
const v = [V[i][j], V[i + 1][j], V[i + 1][j + 1], V[i][j + 1]], xs = [gx[i], gx[i + 1], gx[i + 1], gx[i]], ys = [gy[j], gy[j], gy[j + 1], gy[j + 1]], pts = [];
for (let e = 0; e < 4; e++){ const a = e, b = (e + 1) % 4; if ((v[a] - lv) * (v[b] - lv) < 0){ const t = (lv - v[a]) / (v[b] - v[a]); pts.push([xs[a] + (xs[b] - xs[a]) * t, ys[a] + (ys[b] - ys[a]) * t]); } }
if (pts.length === 2){ ctx.moveTo(p.X(pts[0][0]), p.Y(pts[0][1])); ctx.lineTo(p.X(pts[1][0]), p.Y(pts[1][1])); }
else if (pts.length === 4){ ctx.moveTo(p.X(pts[0][0]), p.Y(pts[0][1])); ctx.lineTo(p.X(pts[1][0]), p.Y(pts[1][1])); ctx.moveTo(p.X(pts[2][0]), p.Y(pts[2][1])); ctx.lineTo(p.X(pts[3][0]), p.Y(pts[3][1])); }
}
ctx.stroke();
});
ctx.restore();
}
function fmtSci(v){ if (v === 0) return '0'; const e = Math.floor(Math.log10(Math.abs(v))); return (v / Math.pow(10, e)).toFixed(0) + 'e' + e; }
function pillText(text, x, y, o){ o = o || {}; const size = o.size || 13, w = textWidth(text, size, o.weight || 800) + 14, al = o.align || 'left', x0 = al === 'right' ? x - w + 7 : al === 'center' ? x - w / 2 : x - 7;
rr(x0, y - size * 0.85, w, size * 1.7, 6); ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.fill(); return T(text, x, y, o); }
/* kp6 (1/3) — 單一神經元與誤差敏感度 δ (Eq. 9–12) */
const Z = [1.0, 2.0, -1.0];
const st = { step: 1, act: 'tanh', w: [0.5, -0.3, 0.8], t: 0.5, eta: 0.1, hist: [] };
const LP = { x: 40, y: 150, w: 700, h: 710 }, RP = { x: 760, y: 150, w: 800, h: 710 };
function calc(w, act, t){
const s = SIG[act]; const a = w.reduce((q, wi, i) => q + wi * Z[i], 0), z = s.f(a), e = 0.5 * (z - t) * (z - t);
const delta = (z - t) * s.d(a), g = Z.map(zl => delta * zl);
return { a, z, e, delta, g };
}
function numCheck(){
let worst = 0; const h = 1e-5, c = calc(st.w, st.act, st.t);
for (let i = 0; i < 3; i++){ const wp = st.w.slice(), wm = st.w.slice(); wp[i] += h; wm[i] -= h;
const gn = (calc(wp, st.act, st.t).e - calc(wm, st.act, st.t).e) / (2 * h); worst = Math.max(worst, Math.abs(gn - c.g[i])); }
return worst;
}
function resetHist(){ st.hist = [calc(st.w, st.act, st.t).e]; }
resetHist();
/* controls */
seg({ id:'step', x: LP.x + 24, y: LP.y + 64, w: 652, h: 46, value: st.step, fs: 15, color: P.blue,
options: [{ id:1, label:'① 前向 Forward' }, { id:2, label:'② 鏈式法則 Chain rule' }, { id:3, label:'③ 定義 δ Define δ' }],
onChange: v => { st.step = v; syncFormulas(); } });
seg({ id:'act', x: RP.x + 24, y: RP.y + 540, w: 330, h: 40, value: st.act, fs: 15, color: P.violet,
options: [{ id:'sq', label:'σ = a²' }, { id:'tanh', label:'σ = tanh' }, { id:'relu', label:'σ = ReLU' }],
onChange: v => { st.act = v; resetHist(); } });
slider({ id:'t', x: RP.x + 380, y: RP.y + 536, w: 396, min: -2, max: 2, step: 0.1, value: st.t, color: P.green,
label: '目標值 Target t', fmt: v => f2(v, 1), onChange: v => { st.t = v; resetHist(); } });
[0, 1, 2].forEach(i => slider({ id: 'w' + i, x: RP.x + 24 + i * 258, y: RP.y + 600, w: 236, min: -2, max: 2, step: 0.1, value: st.w[i], color: P.blue,
label: '權重 w_{' + (i + 1) + '}', fmt: v => f2(v, 1), onChange: v => { st.w[i] = v; resetHist(); } }));
slider({ id:'eta', x: RP.x + 24, y: RP.y + 658, w: 236, min: 0.01, max: 0.3, step: 0.01, value: st.eta, color: P.amber,
label: '學習率 η', fmt: v => f2(v, 2), onChange: v => { st.eta = v; } });
button({ id:'go', x: RP.x + 282, y: RP.y + 662, w: 236, h: 42, text: '沿 −∇ 走一步 Step', color: P.navy,
onClick: () => { const c = calc(st.w, st.act, st.t); for (let i = 0; i < 3; i++){ st.w[i] = clamp(+(st.w[i] - st.eta * c.g[i]).toFixed(6), -2, 2); const it = UI.items.find(q => q.id === 'w' + i); it.value = st.w[i]; }
st.hist.push(calc(st.w, st.act, st.t).e); if (st.hist.length > 40) st.hist.shift(); } });
button({ id:'reset', x: RP.x + 540, y: RP.y + 662, w: 236, h: 42, text: '重設 Reset', ghost: true, color: P.muted,
onClick: () => { st.w = [0.5, -0.3, 0.8]; [0, 1, 2].forEach(i => { UI.items.find(q => q.id === 'w' + i).value = st.w[i]; }); resetHist(); } });
/* formulas */
const FX = LP.x + LP.w/2;
addFormula({ id:'e9', tex: String.raw`a_i=\sum_{\ell=1}^{m} w_{i\ell}\,z_\ell \tag{9}`, x: FX, y: LP.y + 172, width: 652, size: 25 });
addFormula({ id:'e10', tex: String.raw`z_i:=\sigma_i(a_i) \tag{10}`, x: FX, y: LP.y + 236, width: 652, size: 25 });
addFormula({ id:'e11', tex: String.raw`\frac{\partial e}{\partial w_{i\ell}}=\frac{\partial e}{\partial a_i}\times\frac{\partial a_i}{\partial w_{i\ell}}=\delta_i\times z_\ell \tag{11}`, x: FX, y: LP.y + 376, width: 652, size: 25 });
addFormula({ id:'e12', tex: String.raw`\delta_i:=\frac{\partial e}{\partial a_i} \tag{12}`, x: FX, y: LP.y + 522, width: 652, size: 25 });
addFormula({ id:'res', tex: String.raw`\boxed{\;\frac{\partial e}{\partial w_{i\ell}}=\delta_i\,z_\ell\;}`, x: FX, y: LP.y + 598, width: 652, size: 29 });
function syncFormulas(){ showFormula('e9', st.step >= 1); showFormula('e10', st.step >= 1); showFormula('e11', st.step >= 2); showFormula('e12', st.step >= 3); showFormula('res', st.step >= 3); }
syncFormulas();
const NOTES = [
['前向:a_{i} 是激活前的加權和,z_{i} 是激活後的輸出,損失 e 由 z_{i} 決定。', 'Forward: a_{i} is the weighted sum before activation; z_{i} is the output after it.'],
['鏈式法則:w 先影響 a_{i},a_{i} 再影響 e,梯度是兩個偏導數的乘積。', 'Chain rule: w changes a_{i}, a_{i} changes e — a product of two partials.'],
['將 ∂e/∂a_{i} 命名為 δ_{i};又 ∂a_{i}/∂w = z_{ℓ},故梯度 = δ_{i} × z_{ℓ}。', 'Name ∂e/∂a_{i} as δ_{i}; as ∂a_{i}/∂w = z_{ℓ}, the gradient is δ_{i} × z_{ℓ}.']
];
function drawLeft(){
const { x, y, w, h } = LP; panel(x, y, w, h);
T('逐步推導 Step by step', x + 24, y + 30, { size: 24, weight: 800 });
const slots = [[124, 190, 46], [324, 146, 46], [480, 216, 34]];
slots.forEach(([dy, dh, noff], i) => {
const on = st.step >= i + 1, cur = st.step === i + 1;
if (!on){ lockedSlot(x + 24, y + dy, w - 48, dh, '按 ' + ['①','②','③'][i] + ' 繼續 · press ' + ['①','②','③'][i] + ' to continue'); return; }
rr(x + 24, y + dy, w - 48, dh, 12); ctx.fillStyle = cur ? '#f3f7ff' : '#fafcff'; ctx.fill(); ctx.strokeStyle = cur ? P.blue : P.line; ctx.lineWidth = cur ? 2.4 : 1.4; ctx.stroke();
const ny = y + dy + dh - noff;
const n = wrap(NOTES[i][0], x + 42, ny, w - 84, 19, 14.5, P.ink, 600);
wrap(NOTES[i][1], x + 42, ny + n * 19 + 1, w - 84, 18, 13.5, P.muted, 500);
});
}
const NY = (cy, i) => cy + (i - 1) * 72;
function drawRight(){
const { x, y, w, h } = RP; panel(x, y, w, h); const c = calc(st.w, st.act, st.t);
T('動手算:一個神經元的梯度', x + 24, y + 30, { size: 24, weight: 800 });
T('Hands-on: the gradient of one neuron (inputs z = 1.0, 2.0, −1.0)', x + 24, y + 56, { size: 15, weight: 500, color: P.muted });
const cy = y + 168, sx = x + 360, ox = x + 470, zx = x + 590, ex = x + 712;
[0, 1, 2].forEach(i => {
const ny = NY(cy, i); line(x + 74, ny, sx - 28, cy, signCol(st.w[i]), 2 + Math.min(4, Math.abs(st.w[i]) * 1.6));
const lx = lerp(x + 74, sx - 28, 0.42), ly = lerp(ny, cy, 0.42) - 11;
T('w_{' + (i + 1) + '} = ' + f2(st.w[i], 1), lx, ly, { size: 14, weight: 700, color: signCol(st.w[i]), align: 'center' });
});
[0, 1, 2].forEach(i => { const ny = NY(cy, i); dot(x + 52, ny, 22, P.blueS, P.blue, 2.4); T('z_{' + (i + 1) + '}', x + 52, ny, { size: 17, weight: 700, align: 'center' });
T('= ' + f2(Z[i], 1), x + 52, ny + 31, { size: 13.5, weight: 600, color: P.muted, align: 'center' }); });
dot(sx, cy, 30, '#fff', P.ink, 3); T('Σ', sx, cy, { size: 26, weight: 800, align: 'center' });
arrow(sx + 34, cy, ox - 36, cy, P.blue, 3, 10);
rr(ox - 30, cy - 22, 60, 44, 10); ctx.fillStyle = P.violetS; ctx.fill(); ctx.strokeStyle = P.violet; ctx.lineWidth = 2.4; ctx.stroke(); T('σ', ox, cy, { size: 24, weight: 800, color: P.violet, align: 'center' });
arrow(ox + 34, cy, zx - 30, cy, P.blue, 3, 10);
dot(zx, cy, 26, '#fff', P.ink, 3); T('z_{i}', zx, cy, { size: 18, weight: 700, align: 'center' });
arrow(zx + 30, cy, ex - 44, cy, P.blue, 3, 10);
rr(ex - 40, cy - 22, 80, 44, 10); ctx.fillStyle = P.redS; ctx.fill(); ctx.strokeStyle = P.red; ctx.lineWidth = 2.4; ctx.stroke(); T('e', ex, cy, { size: 22, weight: 800, color: P.red, align: 'center' });
T('a_{i} = ' + f2(c.a), (sx + ox) / 2, cy + 52, { size: 15, weight: 700, color: P.blue, align: 'center' });
T('z_{i} = ' + f2(c.z), zx, cy + 52, { size: 15, weight: 700, color: P.blue, align: 'center' });
T('e = ' + f2(c.e, 3), ex, cy + 52, { size: 15, weight: 700, color: P.red, align: 'center' });
arrow(ex - 20, cy + 82, sx + 6, cy + 82, P.red, 3, 10);
T('δ_{i} = ∂e/∂a_{i} = ' + f2(c.delta), (sx + ex) / 2, cy + 102, { size: 16, weight: 800, color: P.red, align: 'center' });
/* gradients */
const gy = y + 312;
line(x + 24, gy - 14, x + w - 24, gy - 14, P.line, 1.4);
T('每個權重的梯度 · gradient per weight', x + 24, gy + 2, { size: 16, weight: 800, maxW: 440 });
T('∂e/∂w_{ℓ} = δ_{i} · z_{ℓ}', x + 24, gy + 24, { size: 15, weight: 700, color: P.red, maxW: 440 });
const bc = x + 270, half = 100, sc = Math.max(1, ...c.g.map(Math.abs));
line(bc, gy + 38, bc, gy + 142, P.faint, 1.6);
c.g.forEach((g, i) => {
const ry = gy + 58 + i * 34; T('∂e/∂w_{' + (i + 1) + '}', x + 24, ry, { size: 16, weight: 700 });
const bw = g / sc * half; rr(Math.min(bc, bc + bw), ry - 12, Math.max(3, Math.abs(bw)), 24, 5); ctx.fillStyle = signCol(-g); ctx.fill();
T(f2(g, 3), x + 440, ry, { size: 17, weight: 800, color: signCol(-g), align: 'right' });
});
wrap('梯度為正(紅)→ 該權重要調小;為負(藍)→ 調大。\nPositive (red): decrease w; negative (blue): increase w.', x + 24, gy + 158, 440, 19, 13.5, P.muted, 500);
const chk = numCheck(); T('與數值微分相差 |Δ| = ' + chk.toExponential(1) + ' · matches numeric diff.', x + 24, gy + 206, { size: 13.5, weight: 700, color: P.green, maxW: 440 });
/* sparkline */
const sx0 = x + 508, sx1 = x + w - 32, sy0 = y + 352, sy1 = y + 480;
panel(sx0 - 14, sy0 - 40, sx1 - sx0 + 28, sy1 - sy0 + 70, { r: 10, fill: '#f6f9ff', stroke: '#dbe5f3', lw: 1.2 });
T('損失 e vs 步數 loss vs steps', sx0, sy0 - 22, { size: 14.5, weight: 800 });
const hmax = Math.max(1e-6, ...st.hist), n = Math.max(2, st.hist.length);
line(sx0, sy1, sx1, sy1, P.faint, 1.4);
ctx.beginPath(); st.hist.forEach((v, i) => { const px = sx0 + (sx1 - sx0) * i / (n - 1 > 0 ? Math.max(n - 1, 5) : 1), py = sy1 - v / hmax * (sy1 - sy0); i ? ctx.lineTo(px, py) : ctx.moveTo(px, py); });
ctx.strokeStyle = P.red; ctx.lineWidth = 2.8; ctx.lineJoin = 'round'; ctx.stroke();
const lp = st.hist.length - 1; dot(sx0 + (sx1 - sx0) * lp / Math.max(n - 1, 5), sy1 - st.hist[lp] / hmax * (sy1 - sy0), 5, P.red, '#fff', 2);
T('e = ' + f2(st.hist[lp], 4) + ' (步 step ' + lp + ')', sx1, sy1 + 18, { size: 14, weight: 800, color: P.red, align: 'right' });
}
function draw(){
chrome({ tag: 'kp6 · 反向傳播 Backpropagation (1/3)', zh: '反向傳播 ①:單一神經元與敏感度 δ', en: 'Backpropagation ①: one neuron and the sensitivity δ', foot: 'kp6 · 本知識點共 3 張 · page 1 of 3' });
drawLeft(); drawRight();
}
window.__states = [
{ name: 'step1', set: { step: 1 } }, { name: 'step2', set: { step: 2 } }, { name: 'step3', set: { step: 3 } },
{ name: 'relu', set: { step: 3, act: 'relu' } }, { name: 'sq-w-max', set: { act: 'sq', w0: 2, w1: 2, w2: 2 } },
{ name: 'tanh-w-min', set: { act: 'tanh', w0: -2, w1: -2, w2: -2, t: -2 } }, { name: 'eta-max-steps', set: { eta: 0.3 }, click: ['go','go','go','go'] },
{ name: 'reset', set: {}, click: ['reset'] }
];
window.__reset = () => { window.__ui.set('step', 3); window.__ui.set('act', 'tanh'); window.__ui.set('t', 0.5); window.__ui.set('eta', 0.1); window.__ui.items.find(i => i.id === 'reset').onClick(); };
window.__settle = () => {};
start(draw, 'kp6 反向傳播 (1/3):單一神經元的前向計算、鏈式法則與誤差敏感度 δ,右側可調權重並驗證梯度。');
})();
</script>
</body>
</html>