▶ 展示 · 回索引

01-why-optimization-roadmap.html

<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>kp1 優化是深度學習的核心 · Optimization is the engine of deep learning</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="kp1 優化是深度學習的核心 · Optimization is the engine of deep learning"></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');
/* kp1 — 導論:優化是深度學習的核心 */
const st = { k: 7, opt: 'first', delta: 0.3, t0: performance.now() - 5000 };
const SUP = '⁰¹²³⁴⁵⁶⁷⁸⁹', sup = n => String(n).split('').map(c => SUP[+c]).join('');
function fmtBytes(b){ const U = ['B','KB','MB','GB','TB','PB','EB','ZB']; const i = clamp(Math.floor(Math.log10(b)/3), 0, 7); const v = b / Math.pow(1000, i); return (v < 10 ? v.toFixed(1) : v.toFixed(0)) + ' ' + U[i]; }
function fmtDur(s){
  if (s < 1e-3) return '< 1 ms'; if (s < 1) return (s*1000).toFixed(0) + ' ms'; if (s < 60) return s.toFixed(1) + ' 秒 s';
  if (s < 3600) return (s/60).toFixed(1) + ' 分 min'; if (s < 86400) return (s/3600).toFixed(1) + ' 小時 h';
  if (s < 3.156e7) return (s/86400).toFixed(1) + ' 天 days'; const y = s / 3.156e7; return (y < 1e4 ? y.toFixed(1) : y.toExponential(1)) + ' 年 years';
}
function fmtDurEn(s){
  if (s < 1e-3) return '< 1 ms'; if (s < 1) return (s*1000).toFixed(0) + ' ms'; if (s < 60) return s.toFixed(1) + ' s';
  if (s < 3600) return (s/60).toFixed(1) + ' min'; if (s < 86400) return (s/3600).toFixed(1) + ' hours';
  if (s < 3.156e7) return (s/86400).toFixed(1) + ' days'; const y = s / 3.156e7; return (y < 1e4 ? y.toFixed(1) : y.toExponential(1)) + ' years';
}
const G = (w, c, s) => Math.exp(-Math.pow((w - c) / s, 2));
const Lf = w => 0.9 - 0.8 * G(w, -1.6, 0.13) - 0.8 * G(w, 1.6, 0.9);
const WSHARP = -1.6, WFLAT = 1.6, W0 = 0;

/* geometry */
const LP = { x: 40, y: 262, w: 740, h: 508 }, RP = { x: 820, y: 262, w: 740, h: 508 };

/* controls */
slider({ id:'k', x: LP.x + 24, y: LP.y + 208, w: 692, min: 3, max: 10, step: 1, value: st.k, color: P.red,
  label: '參數量 d  Number of parameters', fmt: v => 'd = 10^{' + v + '}', onChange: v => { st.k = v; } });
seg({ id:'opt', x: RP.x + 24, y: RP.y + 78, w: 400, h: 42, value: st.opt, fs: 15,
  options: [{ id:'first', label:'一階 First-order', color: P.green }, { id:'second', label:'二階 Second-order', color: P.red }],
  onChange: v => { st.opt = v; st.t0 = performance.now(); setAnimating(true); } });
slider({ id:'delta', x: RP.x + 452, y: RP.y + 78, w: 264, min: 0, max: 0.8, step: 0.02, value: st.delta, color: P.amber,
  label: '測試偏移 Shift δ', fmt: v => v.toFixed(2), onChange: v => { st.delta = v; } });

/* formulas (KaTeX) */
addFormula({ id:'newton', tex: String.raw`\Delta\boldsymbol{w}=-\big[\nabla^{2}f(\boldsymbol{w})\big]^{-1}\nabla f(\boldsymbol{w})`, x: LP.x + 200, y: LP.y + 110, width: 520, size: 27, align: 'left' });
addFormula({ id:'gd', tex: String.raw`\Delta\boldsymbol{w}=-\eta\,\nabla f(\boldsymbol{w})`, x: LP.x + 200, y: LP.y + 160, width: 520, size: 27, align: 'left' });
addFormula({ id:'dl', tex: String.raw`\Delta L=L(\boldsymbol{w}^{*}+\boldsymbol{\delta})-L(\boldsymbol{w}^{*})`, x: RP.x + RP.w/2, y: RP.y + 430, width: 640, size: 27, align: 'center' });
addFormula({ id:'minf', tex: String.raw`\min_{\boldsymbol{w}}\, f(\boldsymbol{w})`, x: 40 + 2*(440+100) + 340, y: 150 + 34, width: 150, size: 24, align: 'center', color: '#ffffff' });

const STEPS = [
  { zh:'假設類別', en:'Hypothesis class', d:'選模型家族 · pick a model family' },
  { zh:'損失函數', en:'Loss function', d:'幫模型排名 · rank the models' },
  { zh:'搜尋最佳假設', en:'Search / Optimization', d:'調整參數、壓低損失', hot:true }
];
const ROAD = [
  ['§2','梯度下降','Gradient descent'], ['§3','反向傳播','Backpropagation', true], ['§4','SGD / mini-batch','Stochastic GD'],
  ['§5','自適應學習率','Adam family'], ['§6','平坦極小 SAM','Flat minima'], ['§7','過參數化收斂','Convergence theory']
];

function drawSteps(){
  STEPS.forEach((s, i) => {
    const x = 40 + i * 540, y = 150, w = 440, h = 94;
    rr(x, y, w, h, 14); ctx.fillStyle = s.hot ? P.blue : '#fff'; ctx.fill();
    if (!s.hot){ ctx.strokeStyle = P.line; ctx.lineWidth = 1.6; ctx.stroke(); }
    dot(x + 32, y + 47, 17, s.hot ? '#fff' : P.blueS);
    T(String(i + 1), x + 32, y + 48, { size: 18, weight: 800, color: P.blue, align: 'center' });
    T(s.zh, x + 62, y + 27, { size: 24, weight: 800, color: s.hot ? '#fff' : P.ink });
    T(s.en, x + 62, y + 53, { size: 15, weight: 500, color: s.hot ? '#dbe6ff' : P.muted });
    T(s.d, x + 62, y + 76, { size: 15, weight: 500, color: s.hot ? '#ffffff' : P.muted, maxW: 330 });
    if (s.hot){ rr(x + 330, y + 64, 96, 22, 11); ctx.fillStyle = P.amber; ctx.fill(); T('本章 this ch.', x + 378, y + 75.5, { size: 12.5, weight: 800, color: '#fff', align: 'center' }); }
    if (i < 2) arrow(x + w + 14, y + h/2, x + 540 - 14, y + h/2, P.faint, 3, 11);
  });
}
function drawLeft(){
  const { x, y, w, h } = LP; panel(x, y, w, h);
  T('二階 Newton 為什麼算不動?', x + 24, y + 34, { size: 24, weight: 800 });
  T('Why second-order Newton is unaffordable at scale', x + 24, y + 62, { size: 16, weight: 500, color: P.muted });
  rr(x + 24, y + 96, 158, 28, 14); ctx.fillStyle = P.redS; ctx.fill(); T('二階 Second-order', x + 103, y + 110.5, { size: 15, weight: 800, color: P.red, align: 'center' });
  rr(x + 24, y + 146, 158, 28, 14); ctx.fillStyle = P.greenS; ctx.fill(); T('一階 First-order', x + 103, y + 160.5, { size: 15, weight: 800, color: P.green, align: 'center' });
  line(x + 24, y + 190, x + w - 24, y + 190, P.line, 1.4);
  const d = Math.pow(10, st.k), gb = 4 * d, hb = 4 * d * d, ops = d * d * d;
  /* bars (log axis) */
  const bx0 = x + 270, bx1 = x + 716, pos = lg => bx0 + (lg - 3) / 18 * (bx1 - bx0);
  [3, 6, 9, 12, 15, 18, 21].forEach(lg => line(pos(lg), y + 284, pos(lg), y + 372, '#e1e8f3', 1.2));
  [[3,'1 KB'],[6,'1 MB'],[9,'1 GB'],[12,'1 TB'],[15,'1 PB'],[18,'1 EB']].forEach(([lg, t]) => T(t, pos(lg), y + 386, { size: 13, weight: 600, color: P.muted, align: 'center' }));
  const gpu = pos(Math.log10(8e10)); line(gpu, y + 280, gpu, y + 372, P.amber, 2, [6, 4]);
  T('80 GB GPU', gpu, y + 270, { size: 14, weight: 800, color: P.amber, align: 'center' });
  const rows = [
    { cy: y + 306, a: '梯度 ∇f', b: 'd 個數字 · d numbers', bytes: gb, col: P.green },
    { cy: y + 350, a: 'Hessian ∇²f', b: 'd × d = d² 個數字 · d² numbers', bytes: hb, col: P.red }
  ];
  rows.forEach(r => {
    T(r.a, x + 24, r.cy - 10, { size: 17, weight: 800, maxW: 240 }); T(r.b, x + 24, r.cy + 11, { size: 13.5, weight: 500, color: P.muted, maxW: 240 });
    const ex = pos(clamp(Math.log10(r.bytes), 3, 21)); rr(bx0, r.cy - 14, Math.max(4, ex - bx0), 28, 6); ctx.fillStyle = r.col; ctx.fill();
    const lab = fmtBytes(r.bytes), lw = textWidth(lab, 16, 800);
    if (ex + lw + 14 < x + w - 8){ rr(ex + 4, r.cy - 12, lw + 12, 24, 6); ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.fill(); T(lab, ex + 10, r.cy, { size: 16, weight: 800, color: r.col }); }
    else T(lab, ex - 8, r.cy, { size: 16, weight: 800, color: '#fff', align: 'right' });
  });
  const over = hb > 8e10;
  T('Hessian ' + fmtBytes(hb) + (over ? ' > 80 GB:單張 GPU 放不下' : ' ≤ 80 GB:還放得下'), x + 24, y + 418, { size: 18, weight: 800, color: over ? P.red : P.green, maxW: 692 });
  T('反轉約 d³ 次運算;以 1 PFLOP/s 估算 ≈ ' + fmtDur(ops / 1e15), x + 24, y + 448, { size: 16, weight: 600, maxW: 692 });
  T('Inverting costs about d³ operations; at 1 PFLOP/s that is ≈ ' + fmtDurEn(ops / 1e15), x + 24, y + 472, { size: 13.5, weight: 500, color: P.muted, maxW: 692 });
  T('每個數字以 float32(4 bytes)計 · each number stored as 4-byte float32', x + 24, y + 494, { size: 13.5, weight: 500, color: P.muted, maxW: 692 });
}
function drawRight(now){
  const { x, y, w, h } = RP; panel(x, y, w, h);
  T('尖針 vs 淺碗:訓練損失相同,命運不同', x + 24, y + 34, { size: 24, weight: 800 });
  T('Sharp vs flat minima: same training loss, different fate', x + 24, y + 62, { size: 16, weight: 500, color: P.muted });
  /* plot */
  const x0 = x + 24, x1 = x + w - 24, y0 = y + 136, y1 = y + 330;
  const px = wv => x0 + (wv + 3) / 6 * (x1 - x0), py = L => y1 - L * (y1 - y0 - 14);
  const wEnd = st.opt === 'first' ? WFLAT : WSHARP, other = st.opt === 'first' ? WSHARP : WFLAT;
  const q = clamp((now - st.t0) / 1500, 0, 1); if (q >= 1) setAnimating(false);
  const wb = lerp(W0, wEnd, ease(q));
  /* shift window around chosen minimum */
  ctx.fillStyle = 'rgba(217,118,10,.14)'; ctx.fillRect(px(wEnd), y0, px(wEnd + st.delta) - px(wEnd), y1 - y0);
  ctx.beginPath(); for (let i = 0; i <= 300; i++){ const wv = -3 + 6 * i / 300; i ? ctx.lineTo(px(wv), py(Lf(wv))) : ctx.moveTo(px(wv), py(Lf(wv))); }
  ctx.strokeStyle = P.blue; ctx.lineWidth = 3.4; ctx.lineJoin = 'round'; ctx.stroke();
  line(x0, y1, x1, y1, P.muted, 1.5);
  T('尖針 Sharp', px(WSHARP), y1 + 17, { size: 15, weight: 800, color: P.red, align: 'center' });
  T('淺碗 Flat', px(WFLAT), y1 + 17, { size: 15, weight: 800, color: P.green, align: 'center' });
  dot(px(other), py(Lf(other)), 6, '#fff', P.faint, 2.5);
  dot(px(W0), py(Lf(W0)), 5, '#fff', P.muted, 2); T('w₀', px(W0), py(Lf(W0)) - 16, { size: 14, weight: 700, color: P.muted, align: 'center' });
  const Ls = Lf(wEnd), Lsh = Lf(wEnd + st.delta);
  if (q >= 1 && st.delta > 0){
    line(px(wEnd + st.delta), py(Ls), px(wEnd + st.delta), py(Lsh), P.red, 2.4, [5, 4]);
    dot(px(wEnd + st.delta), py(Lsh), 6.5, P.red, '#fff', 2);
  }
  dot(px(wb), py(Lf(wb)), 8, st.opt === 'first' ? P.green : P.red, '#fff', 2.6);
  /* readouts */
  const dS = Lf(WSHARP + st.delta) - Lf(WSHARP), dF = Lf(WFLAT + st.delta) - Lf(WFLAT);
  [[x + 24, '尖針 sharp', dS, P.red, st.opt === 'second'], [x + 24 + 356, '淺碗 flat', dF, P.green, st.opt === 'first']].forEach(([bx, n, v, c, on]) => {
    rr(bx, y + 362, 336, 40, 10); ctx.fillStyle = on ? (c === P.red ? P.redS : P.greenS) : '#f4f7fc'; ctx.fill(); ctx.strokeStyle = on ? c : P.line; ctx.lineWidth = on ? 2.4 : 1.4; ctx.stroke();
    T(n, bx + 16, y + 382.5, { size: 16, weight: 700, color: on ? c : P.muted });
    T('ΔL = ' + v.toFixed(2), bx + 320, y + 382.5, { size: 18, weight: 800, color: on ? c : P.muted, align: 'right' });
  });
  wrap('示意圖:呈現講義所述的經驗趨勢(一階常停在較平坦處、二階可能落入較尖處),成因仍在研究中。\nIllustration of an empirical tendency reported in the chapter; the underlying cause is still under study.', x + 24, y + 466, 692, 19, 13.5, P.muted, 500);
}
function draw(now){
  chrome({ tag: 'kp1 · 導論 Introduction', zh: '優化:訓練神經網路的引擎', en: 'Optimization — the engine that trains neural networks', foot: 'kp1 · 本知識點共 1 張 · 1 page' });
  drawSteps(); drawLeft(); drawRight(now);
  T('本章路線圖:由地基往上 · Roadmap, from the foundation upward', 40, 786, { size: 15, weight: 700, color: P.muted });
  ROAD.forEach((r, i) => {
    const x = 40 + i * 258.4, y = 798, w = 230, h = 66;
    rr(x, y, w, h, 12); ctx.fillStyle = r[3] ? P.redS : '#fff'; ctx.fill(); ctx.strokeStyle = r[3] ? P.red : P.line; ctx.lineWidth = r[3] ? 2.6 : 1.6; ctx.stroke();
    rr(x + 12, y + 12, 38, 24, 12); ctx.fillStyle = r[3] ? P.red : P.blue; ctx.fill(); T(r[0], x + 31, y + 24.5, { size: 13.5, weight: 800, color: '#fff', align: 'center' });
    T(r[1], x + 60, y + 24, { size: 18, weight: 800, maxW: 160 }); T(r[2], x + 60, y + 48, { size: 14, weight: 500, color: P.muted, maxW: 160 });
    if (r[3]) T('必讀', x + w - 12, y + 12, { size: 12.5, weight: 800, color: P.red, align: 'right' });
    if (i < 5) arrow(x + w + 4, y + h/2, x + 258.4 - 4, y + h/2, P.faint, 2.4, 9);
  });
}
window.__states = [
  { name: 'default', set: {} }, { name: 'd-min', set: { k: 3 } }, { name: 'd-max', set: { k: 10 } }, { name: 'd-mid', set: { k: 5 } },
  { name: 'second-order', set: { opt: 'second' } }, { name: 'delta-min', set: { delta: 0 } }, { name: 'delta-max', set: { delta: 0.8 } },
  { name: 'second+delta-max', set: { opt: 'second', delta: 0.8 } }
];
window.__reset = () => { window.__ui.set('k', 7); window.__ui.set('opt', 'first'); window.__ui.set('delta', 0.3); };
window.__settle = () => { st.t0 = performance.now() - 5000; };
start(draw, 'kp1 導論:優化是深度學習的核心。左:二階 Newton 的 Hessian 記憶體與運算量隨參數量暴增;右:尖銳與平坦極小值的損失地形互動。');

})();
</script>
</body>
</html>