/* =========================================================
   figures2.jsx — 讲义插图(M03 机器学习与深度学习 · M04 前沿与应用)
   ---------------------------------------------------------
   往 figures.jsx 里声明的 FIGN 注册表上继续添加,并复用它的
   图元(FigFrame / FT / FB / FA / FAP / FBars / fpath)。
   index.html 先加载 figures.jsx。
   ========================================================= */

/* =========================================================
   M03 · 机器学习与深度学习
   ========================================================= */

/* c301 —— 偏差-方差:欠拟合与过拟合 */
FIGN["c301-biasvar"] = ({ idx }) => {
  const X0 = 62, X1 = 470, Y0 = 46, Y1 = 168, N = 60;
  const xf = (i) => X0 + (i / N) * (X1 - X0);
  const yf = (v) => Y1 - (1 - v) * (Y1 - Y0);
  const train = (i) => 0.62 * Math.exp(-i / 16) + 0.03;
  const valid = (i) => 0.62 * Math.exp(-i / 16) + 0.06 + Math.pow(Math.max(0, i - 26) / 60, 2) * 1.5;
  const best = 26;
  return (
    <FigFrame h={244} idx={idx}
      cap="训练误差一路向下,验证误差先降后升——两条线分开的那一刻,模型开始背答案而不是学规律。机器学习课里几乎所有手段(正则、早停、更多数据、更简单的模型)都是在把右边那条上翘的尾巴按下去。">
      <FT x={16} y={24} c="tt">模型复杂度与两条误差曲线</FT>
      {[0, 0.5, 1].map((f) => <line key={f} x1={X0} x2={X1} y1={yf(f)} y2={yf(f)} className="grid" />)}
      <line x1={X0} x2={X1} y1={Y1} y2={Y1} className="axis" />
      <FT x={X0 - 8} y={Y0 + 6} c="tn" a="end">高</FT>
      <FT x={X0 - 8} y={Y1} c="tn" a="end">低</FT>
      <FT x={X0 - 8} y={(Y0 + Y1) / 2} c="tn" a="end">误差</FT>
      <FT x={X1} y={Y1 + 16} c="tn" a="end">模型复杂度 / 训练轮数 →</FT>
      <polyline className="cv" points={fpath(N + 1, xf, (i) => yf(1 - train(i)))} />
      <polyline className="cv a" points={fpath(N + 1, xf, (i) => yf(1 - Math.min(1, valid(i))))} />
      <FT x={X1 + 8} y={yf(1 - train(N)) + 4} c="tp">训练误差</FT>
      <FT x={X1 + 8} y={yf(1 - Math.min(1, valid(N))) + 4} c="ta">验证误差</FT>
      <line x1={xf(best)} x2={xf(best)} y1={Y0} y2={Y1} className="ln d" />
      <FT x={xf(best)} y={Y0 - 4} c="t" a="middle">最佳点</FT>
      <FT x={xf(8)} y={Y0 + 18} c="tn" a="middle">欠拟合</FT>
      <FT x={xf(50)} y={Y0 + 18} c="tn" a="middle">过拟合</FT>
      <FT x={16} y={202} c="tn" w={628}>诊断顺序永远是:先看两条线之间的间隙。间隙小但都很高 → 欠拟合,该加容量;间隙大 → 过拟合,该加正则或加数据。跳过这一步直接调超参,是最常见的时间浪费。</FT>
    </FigFrame>
  );
};

/* c301 —— 一次完整的机器学习工作流 */
FIGN["c301-pipeline"] = ({ idx }) => (
  <FigFrame h={244} idx={idx}
    cap="课堂上的机器学习从「给定数据集」开始,真实项目里 70% 的时间花在这条线的前两格和最后一格。模型选型反而是最标准化、最不容易出错的一段——这就是为什么「换个更强的模型」很少是正确答案。">
    <FT x={16} y={24} c="tt">真实项目里,时间花在哪</FT>
    {[["问题定义", "什么算成功?用什么指标?", 0.18],
      ["数据收集与清洗", "缺失、噪声、泄漏、样本不均衡", 0.30],
      ["特征工程", "领域知识进入模型的唯一入口", 0.16],
      ["模型选型与调参", "最标准化的一段", 0.10],
      ["评估与错误分析", "看错的那些样本,而不是看分数", 0.16],
      ["上线与监控", "线上分布和训练集从来不一样", 0.10]].map(([t, s, w], i) => {
      const y = 46 + i * 32;
      return (
        <g key={i}>
          <FT x={16} y={y + 16} c="t">{t}</FT>
          <FT x={140} y={y + 16} c="tn" w={280}>{s}</FT>
          <rect x={432} y={y + 5} width={196 * (w / 0.30)} height={14} rx={2}
            className={i === 3 ? "fmid" : "fp"} fillOpacity={0.85} />
          <FT x={640} y={y + 16} c="tn" a="end">{Math.round(w * 100)}%</FT>
        </g>
      );
    })}
    <FT x={432} y={40} c="tn">大致时间占比 →</FT>
    <FT x={16} y={252} c="tn" w={628}>错误分析是被跳过最多、回报最高的一格:把验证集里做错的 50 条拉出来分类,你几乎总能发现一两类系统性错误,而它们往往一条规则就能修掉。</FT>
  </FigFrame>
);

/* c302 —— 一层的构成 */
FIGN["c302-block"] = ({ idx }) => (
  <FigFrame h={238} idx={idx}
    cap="深度学习的「深」不是堆得多就行——没有残差连接,梯度传不到底层,二十层以上就训不动了。归一化稳住数值尺度,激活提供非线性,残差保证梯度有一条直达通路。三者缺一,深度就是空话。">
    <FT x={16} y={24} c="tt">一层里的四个部件,各管一件事</FT>
    <FB x={16} y={70} w={92} h={42} k="m" t="输入 x" tc="tk" />
    <FA x1={110} y1={91} x2={134} y2={91} />
    <FB x={136} y={70} w={104} h={42} k="p" t="线性变换" s="Wx + b" tc="t" />
    <FA x1={242} y1={91} x2={266} y2={91} />
    <FB x={268} y={70} w={104} h={42} t="归一化" s="稳住尺度" tc="t" />
    <FA x1={374} y1={91} x2={398} y2={91} />
    <FB x={400} y={70} w={104} h={42} k="a" t="激活函数" s="提供非线性" tc="t" />
    <FA x1={506} y1={91} x2={530} y2={91} />
    <circle cx={548} cy={91} r="13" className="bx p" />
    <FT x={548} y={96} c="tk" a="middle">+</FT>
    <FA x1={562} y1={91} x2={586} y2={91} k="p" />
    <FB x={588} y={70} w={56} h={42} k="p" t="输出" tc="tk" />
    <FAP d="M62 68 L 62 40 L 548 40 L 548 76" k="p" />
    <FT x={300} y={34} c="tp" a="middle">残差连接:梯度的直达通路</FT>

    <FT x={16} y={144} c="tt">去掉任何一个会怎样</FT>
    {[["没有非线性", "无论堆多少层,整体仍等价于一个线性变换"],
      ["没有归一化", "激活值尺度逐层漂移,学习率极难调"],
      ["没有残差", "梯度衰减,20 层以上基本训不动"]].map(([t, s], i) => (
      <g key={i}>
        <FB x={16 + i * 214} y={156} w={202} h={52} k="a" t={t} s={s} tc="t" />
      </g>
    ))}
    <FT x={16} y={230} c="tn" w={628}>Transformer 的一层也是这个结构:把「线性变换」换成注意力 + 前馈两个子层,其余三件事一模一样。</FT>
  </FigFrame>
);

/* c302 —— 训练曲线与正则手段 */
FIGN["c302-training"] = ({ idx }) => {
  const X0 = 62, X1 = 470, Y0 = 46, Y1 = 160, N = 60;
  const xf = (i) => X0 + (i / N) * (X1 - X0);
  const yf = (v) => Y1 - v * (Y1 - Y0);
  const good = (i) => Math.exp(-i / 18) * 0.9 + 0.06;
  const big = (i) => i < 8 ? 0.95 - i * 0.02 : 0.8 + 0.15 * Math.sin(i * 0.7) * Math.exp(-i / 40);
  const small = (i) => 0.95 - i * 0.008;
  return (
    <FigFrame h={244} idx={idx}
      cap="学习率是深度学习里唯一一个「调错了其他全白搭」的超参。太大会震荡甚至发散,太小会慢到看不出趋势,合适的那条会在前几个 epoch 就掉得很干脆。看曲线的形状,比看最终数字更能告诉你下一步该改什么。">
      <FT x={16} y={24} c="tt">三条损失曲线,三个诊断</FT>
      {[0, 0.5, 1].map((f) => <line key={f} x1={X0} x2={X1} y1={yf(f)} y2={yf(f)} className="grid" />)}
      <line x1={X0} x2={X1} y1={Y1} y2={Y1} className="axis" />
      <FT x={X0 - 8} y={(Y0 + Y1) / 2} c="tn" a="end">损失</FT>
      <FT x={X1} y={Y1 + 16} c="tn" a="end">训练轮数 →</FT>
      <polyline className="cv a" points={fpath(N + 1, xf, (i) => yf(big(i)))} />
      <polyline className="cv" points={fpath(N + 1, xf, (i) => yf(good(i)))} />
      <polyline className="cv m" points={fpath(N + 1, xf, (i) => yf(small(i)))} />
      <FT x={X1 + 8} y={yf(big(N)) + 4} c="ta">学习率过大</FT>
      <FT x={X1 + 8} y={yf(small(N)) + 4} c="tm">学习率过小</FT>
      <FT x={X1 + 8} y={yf(good(N)) + 4} c="tp">合适</FT>

      <FT x={16} y={190} c="tt">常用正则手段(按性价比)</FT>
      {[["更多数据", "永远第一名"], ["数据增强", "近乎免费的数据"],
        ["早停", "一行代码"], ["权重衰减 / Dropout", "标准配置"]].map(([t, s], i) => (
        <FB key={i} x={16 + i * 159} y={202} w={148} h={44} k={i < 2 ? "p" : "m"} t={t} s={s} tc="ts" />
      ))}
    </FigFrame>
  );
};

/* c303 —— 强化学习的基本循环 */
FIGN["c303-mdp"] = ({ idx }) => (
  <FigFrame h={238} idx={idx}
    cap="强化学习和监督学习最根本的差别:没有人告诉你正确答案,只有一个延迟的、稀疏的奖励信号,而且你这一步的动作会改变将来能看到的数据。这三件事(无标签、延迟奖励、分布随策略变)让它比监督学习难得多。">
    <FT x={16} y={24} c="tt">智能体与环境的循环</FT>
    <FB x={116} y={62} w={160} h={54} k="p" t="智能体 Agent" s="学一个策略 π(a|s)" tc="t" />
    <FB x={396} y={62} w={160} h={54} k="a" t="环境 Environment" s="给出下一状态与奖励" tc="t" />
    <FAP d="M278 78 L 394 78" k="p" />
    <FT x={336} y={72} c="tp" a="middle">动作 aₜ</FT>
    <FAP d="M554 104 L 574 104 L 574 148 L 98 148 L 98 104 L 114 104" k="a" />
    <FT x={336} y={142} c="ta" a="middle">状态 sₜ₊₁ · 奖励 rₜ₊₁</FT>

    <FT x={16} y={186} c="tt">三个让它变难的地方</FT>
    {[["没有正确答案", "只有奖励,没有标签"],
      ["奖励是延迟的", "这一步的好坏,几十步后才知道"],
      ["数据分布随策略变", "策略一改,看到的样本就全变了"]].map(([t, s], i) => (
      <FB key={i} x={16 + i * 214} y={198} w={202} h={52} k="m" t={t} s={s} tc="t" />
    ))}
  </FigFrame>
);

/* c303 —— RL 算法谱系 */
FIGN["c303-family"] = ({ idx }) => (
  <FigFrame h={246} idx={idx}
    cap="所有强化学习算法都在回答同一个问题的不同侧面:我该怎么估计「这样做值不值」。估值函数、直接优化策略、两者都要、以及先学一个环境模型——四条路线各有各的样本效率与稳定性代价。">
    <FT x={16} y={24} c="tt">四条路线,一个问题</FT>
    {[["基于价值 Value-based", "Q-learning · DQN", "学 Q(s,a),再取 argmax。样本效率高,难处理连续动作。", "p"],
      ["基于策略 Policy-based", "REINFORCE · PPO", "直接优化 π。天然支持连续动作,方差大。", "a"],
      ["演员-评论家 Actor-Critic", "A2C · SAC · PPO", "策略负责动作,价值负责减方差。今天的主流。", "p"],
      ["基于模型 Model-based", "Dreamer · MuZero", "先学环境,再在脑子里规划。样本效率最高,模型误差会被放大。", "m"]].map(([t, ex, s, k], i) => {
      const y = 46 + i * 48;
      return (
        <g key={i}>
          <FB x={16} y={y} w={186} h={40} k={k} t={t} tc="t" />
          <FT x={214} y={y + 17} c="tm">{ex}</FT>
          <FT x={214} y={y + 33} c="tn" w={424}>{s}</FT>
        </g>
      );
    })}
    <FT x={16} y={244} c="tn" w={628}>RLHF 用的 PPO 属于第三类。理解它之前,先把「为什么需要一个 critic 来减方差」想清楚——这是从 REINFORCE 到 PPO 的整条主线。</FT>
  </FigFrame>
);

/* c304 —— 有向图与无向图 */
FIGN["c304-pgm"] = ({ idx }) => {
  const node = (x, y, t, k) => (
    <g><circle cx={x} cy={y} r="19" className={`bx ${k || ""}`} />
      <text x={x} y={y + 5} textAnchor="middle" className="tk">{t}</text></g>
  );
  return (
    <FigFrame h={244} idx={idx}
      cap="概率图模型的核心价值是把「谁和谁无关」画出来:一张图省下的不是笔墨,是指数级的参数。四个二值变量的联合分布要 15 个参数,而如果图告诉你它们构成一条链,只需要 7 个。条件独立就是这门课要教的全部。">
      <FT x={16} y={24} c="tt">用图把条件独立写下来</FT>

      <FT x={16} y={52} c="tp">有向图(贝叶斯网络):描述因果与生成过程</FT>
      {node(70, 100, "A", "p")}{node(160, 100, "B", "p")}{node(115, 158, "C", "p")}
      <FA x1={88} y1={112} x2={102} y2={142} k="p" />
      <FA x1={142} y1={112} x2={128} y2={142} k="p" />
      <FT x={200} y={104} c="tn" w={110}>A ⊥ B,但一旦观测到 C,两者就相关了(解释消除)</FT>

      <FT x={352} y={52} c="ta">无向图(马尔可夫随机场):描述相互作用</FT>
      {node(406, 100, "X", "a")}{node(496, 100, "Y", "a")}{node(451, 158, "Z", "a")}
      <line x1={425} y1={100} x2={477} y2={100} className="ln a" />
      <line x1={419} y1={114} x2={441} y2={142} className="ln a" />
      <line x1={483} y1={114} x2={461} y2={142} className="ln a" />
      <FT x={536} y={104} c="tn" w={108}>没有方向,只有「相邻的变量互相影响」</FT>

      <FT x={16} y={206} c="t">参数量的差别:</FT>
      <FT x={130} y={206} c="tn" w={514}>4 个二值变量的完整联合分布需要 2⁴−1 = 15 个参数;若图结构说明它们是一条链,只需要 1+2+2+2 = 7 个。变量一多,这个差距就是指数级的。</FT>
    </FigFrame>
  );
};

/* c304 —— 精确推断 vs 近似推断 */
FIGN["c304-inference"] = ({ idx }) => (
  <FigFrame h={236} idx={idx}
    cap="推断就是「已知一部分变量,问另一部分的分布」。图稀疏时可以精确算;图一稠密,精确推断立刻变成指数复杂度,只能退到采样或变分。变分推断正是 VAE 的数学底座——这门课不是屠龙术,它直接通到生成模型。">
    <FT x={16} y={24} c="tt">图一稠密,精确解就没了</FT>
    <FB x={16} y={48} w={300} h={30} k="p" t="精确推断 · 图稀疏时可用" tc="t" />
    {[["变量消元", "按顺序把变量积掉,顺序决定代价"],
      ["信念传播", "在树上传消息,一次遍历得到全部边缘分布"],
      ["联结树", "把图变成树再传,复杂度看树宽"]].map(([t, s], i) => (
      <FB key={i} x={16} y={86 + i * 44} w={300} h={38} k="m" t={t} s={s} tc="ts" />
    ))}

    <FB x={344} y={48} w={300} h={30} k="a" t="近似推断 · 图稠密时唯一的路" tc="t" />
    {[["MCMC 采样", "渐近精确,但收敛慢、难判断收敛"],
      ["变分推断", "把推断变成优化:找一个最接近的简单分布"],
      ["期望传播", "在矩匹配的意义下逐点近似"]].map(([t, s], i) => (
      <FB key={i} x={344} y={86 + i * 44} w={300} h={38} k="m" t={t} s={s} tc="ts" />
    ))}
    <FT x={16} y={236} c="tn" w={628}>变分推断把「求积分」换成「求最优」——这一步转换是 VAE 的 ELBO、以及扩散模型训练目标的共同来源。学这门课最实际的回报,就是后面看生成模型的推导时不再卡壳。</FT>
  </FigFrame>
);

/* c305 —— 扩散:前向加噪 / 反向去噪 */
FIGN["c305-diffusion"] = ({ idx }) => {
  const rng2 = (seed) => { let s = seed; return () => (s = (s * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff; };
  const tile = (x, y, noise, key) => {
    const r = rng2(7 + Math.round(noise * 100));
    const dots = [];
    for (let i = 0; i < 26; i++) dots.push(<circle key={i} cx={x + 6 + r() * 60} cy={y + 6 + r() * 46} r="2.2" className="fmid" fillOpacity={noise} />);
    return (
      <g key={key}>
        <rect x={x} y={y} width={72} height={58} rx={3} className="bx m" />
        <rect x={x + 10} y={y + 12} width={52} height={34} rx={2} className="fp" fillOpacity={1 - noise} />
        {dots}
      </g>
    );
  };
  return (
    <FigFrame h={240} idx={idx}
      cap="前向过程只是不断加噪声,没有任何需要学的东西;真正要学的是反向那一步——给定一张带噪图,预测「加进去的噪声是什么」。把这个预测器训好,就能从纯噪声一步步倒推回一张图。整个扩散模型就这一件事。">
      <FT x={16} y={24} c="tt">前向加噪(不用学)· 反向去噪(要学)</FT>
      {[0, 0.25, 0.5, 0.75, 1].map((n, i) => (
        <g key={i}>
          {tile(20 + i * 128, 48, n, `t${i}`)}
          {i < 4 && <FA x1={94 + i * 128} y1={77} x2={144 + i * 128} y2={77} />}
          <FT x={56 + i * 128} y={124} c="tn" a="middle">t = {i}</FT>
        </g>
      ))}
      <FT x={20} y={44} c="tp">原图 x₀</FT>
      <FT x={644} y={44} c="ta" a="end">纯噪声 x_T</FT>
      <FAP d="M600 138 L 40 138" k="a" />
      <FT x={330} y={154} c="ta" a="middle">反向:每步预测噪声 εθ(xₜ, t),减掉它</FT>

      <FT x={16} y={186} c="t">训练目标简单到出奇:</FT>
      <FT x={172} y={186} c="tk">‖ ε − εθ(xₜ, t) ‖²</FT>
      <FT x={16} y={212} c="tn" w={628}>随机取一张图、随机取一个时间步、随机采一个噪声,让网络把这个噪声猜出来。没有对抗训练、没有判别器,这就是扩散模型比 GAN 稳定得多的根本原因。</FT>
    </FigFrame>
  );
};

/* c305 —— 生成模型四大家族 */
FIGN["c305-families"] = ({ idx }) => {
  const fam = [
    { t: "VAE", q: 0.55, d: 0.8, s: "训练稳,样本偏糊;隐空间规整好用" },
    { t: "GAN", q: 0.82, d: 0.35, s: "样本锐利,训练难、易模式崩塌" },
    { t: "Flow", q: 0.6, d: 0.7, s: "可算精确似然,架构受可逆性限制" },
    { t: "Diffusion", q: 0.95, d: 0.72, s: "质量最好,采样慢(要几十步)" },
  ];
  return (
    <FigFrame h={244} idx={idx}
      cap="四个家族在「样本质量」和「训练稳定性」上各站一角。扩散模型之所以在这两年赢下大部分战场,是因为它同时拿到了高质量和稳定训练,代价只是采样慢——而采样慢是可以靠蒸馏和更好的采样器补的。">
      <FT x={16} y={24} c="tt">质量 × 稳定性:四个家族的位置</FT>
      {fam.map((f, i) => {
        const y = 46 + i * 48;
        return (
          <g key={i}>
            <FB x={16} y={y} w={104} h={40} k={f.t === "Diffusion" ? "a" : "p"} t={f.t} tc="t" />
            <FT x={134} y={y + 15} c="tn">质量</FT>
            <rect x={172} y={y + 6} width={150 * f.q} height={11} rx={2} className="fa" fillOpacity={0.85} />
            <FT x={134} y={y + 33} c="tn">稳定</FT>
            <rect x={172} y={y + 24} width={150 * f.d} height={11} rx={2} className="fp" fillOpacity={0.85} />
            <FT x={340} y={y + 24} c="tn" w={300}>{f.s}</FT>
          </g>
        );
      })}
      <FT x={16} y={244} c="tn" w={628}>多模态模型(文生图、文生视频)基本都是「扩散 + 一个把文本编码成条件的编码器」。所以这门课的顺序是:先把扩散讲透,再谈条件注入与跨模态对齐。</FT>
    </FigFrame>
  );
};

/* =========================================================
   M04 · 前沿与应用
   ========================================================= */

/* c401 —— NLP 表示的演进 */
FIGN["c401-evolution"] = ({ idx }) => (
  <FigFrame h={244} idx={idx}
    cap="一部 NLP 简史,其实是「怎么表示一个词」的历史:从只数出现次数,到用一个向量表达语义,到让这个向量随上下文改变,最后到让每个位置直接去看全序列。每一步解决的,都是上一步留下的那个具体缺陷。">
    <FT x={16} y={24} c="tt">表示方法的四次跃迁</FT>
    {[["词袋 / TF-IDF", "只数词频", "丢掉了顺序,也不知道近义词", "m"],
      ["词向量 word2vec", "一个词一个向量", "有语义了,但一词一义:bank 只有一个向量", "p"],
      ["上下文表示 RNN/ELMo", "向量随上下文变", "解决了一词多义,但长依赖仍然衰减", "p"],
      ["Transformer / 预训练", "每个位置直接看全序列", "长依赖与并行同时解决,代价是 O(n²)", "a"]].map(([t, k1, k2, k], i) => {
      const y = 46 + i * 46;
      return (
        <g key={i}>
          <FB x={16} y={y} w={188} h={38} k={k} t={t} tc="t" />
          <FT x={216} y={y + 16} c="tm">{k1}</FT>
          <FT x={216} y={y + 32} c="tn" w={424}>{k2}</FT>
          {i < 3 && <FA x1={110} y1={y + 40} x2={110} y2={y + 44} />}
        </g>
      );
    })}
    <FT x={16} y={242} c="tn" w={628}>读这条线索时,重点不是记住模型名字,而是每一次跃迁「上一代到底卡在哪」。这个习惯会让你读 2018–2025 的论文快很多。</FT>
  </FigFrame>
);

/* c401 —— 注意力权重 */
FIGN["c401-attention"] = ({ idx }) => {
  const toks = ["那只", "猫", "追", "老鼠", ",", "它", "累了"];
  const w = [0.05, 0.46, 0.06, 0.22, 0.03, 0.12, 0.06];
  return (
    <FigFrame h={236} idx={idx}
      cap="「它」指的是猫还是老鼠?模型没有任何语法规则,它只是把每个位置的相关性算成一组权重,再做一次加权平均。指代消解在模型内部就长这样——一次 softmax,没有别的。">
      <FT x={16} y={24} c="tt">查询「它」时,注意力落在哪</FT>
      {toks.map((t, i) => (
        <g key={i}>
          <FB x={16 + i * 90} y={48} w={78} h={32} k={i === 5 ? "a" : "m"} t={t} tc="t" />
          <rect x={16 + i * 90} y={96} width={78} height={Math.max(2, w[i] * 96)} rx={2}
            className={w[i] === Math.max(...w) ? "fa" : "fp"} fillOpacity={0.85} />
          <FT x={55 + i * 90} y={210} c="tn" a="middle">{Math.round(w[i] * 100)}%</FT>
        </g>
      ))}
      <FT x={16} y={92} c="tn">注意力权重(和为 1)</FT>
      <FT x={16} y={172} c="ta">查询 token = 「它」</FT>
      <FT x={16} y={232} c="tn" w={628}>公式只有一行:softmax(QKᵀ/√d)·V。除以 √d 不是装饰——维度越高点积越大,不缩放的话 softmax 会塌成 one-hot,梯度全部消失。</FT>
    </FigFrame>
  );
};

/* c402 —— 卷积与感受野 */
FIGN["c402-conv"] = ({ idx }) => (
  <FigFrame h={240} idx={idx}
    cap="卷积的两个假设——局部性(相邻像素才相关)和平移不变性(同一个特征在哪都算特征)——让参数量比全连接小几个数量级。层数越深,一个神经元能「看到」的原图范围越大,特征也就从边缘长成了物体。">
    <FT x={16} y={24} c="tt">层数越深,看得越宽</FT>
    {[["第 1 层", 3, "边缘 · 颜色块", 0.2], ["第 5 层", 19, "纹理 · 局部形状", 0.5], ["第 12 层", 91, "物体部件 · 整体", 0.9]].map(([t, rf, s, g], i) => {
      const x = 16 + i * 214;
      return (
        <g key={i}>
          <FB x={x} y={46} w={202} h={28} k={i === 2 ? "a" : "p"} t={t} tc="t" />
          <rect x={x + 51} y={84} width={100} height={100} rx={3} className="bx m" />
          <rect x={x + 101 - 50 * g} y={134 - 50 * g} width={100 * g} height={100 * g} rx={2}
            className={i === 2 ? "fa" : "fp"} fillOpacity={0.3} />
          <FT x={x + 101} y={200} c="tn" a="middle">感受野 ≈ {rf}×{rf} 像素</FT>
          <FT x={x + 101} y={216} c="tn" a="middle">{s}</FT>
        </g>
      );
    })}
    <FT x={16} y={240} c="tn" w={628}>ViT 把这套换成了「切块 + 注意力」:第一层就是全局感受野。两条路线的取舍是——卷积自带归纳偏置、小数据也能训;ViT 没有偏置,但数据一多就反超。</FT>
  </FigFrame>
);

/* c402 —— CV 任务谱系 */
FIGN["c402-tasks"] = ({ idx }) => (
  <FigFrame h={232} idx={idx}
    cap="四类任务的差别只在「输出的粒度」:一张图一个标签、一个框、一个像素级掩码、还是一段文字。粒度越细,标注越贵——这就是为什么分割数据集永远比分类数据集小,也是自监督和大模型在这里格外有价值的原因。">
    <FT x={16} y={24} c="tt">输出粒度决定了标注成本</FT>
    {[["图像分类", "整图 → 一个标签", "最便宜", 0.15, "p"],
      ["目标检测", "→ 若干个框 + 类别", "中等", 0.42, "p"],
      ["语义 / 实例分割", "→ 每个像素一个类别", "很贵", 0.78, "a"],
      ["图文理解 / 描述", "→ 一段自由文本;对错本身也难判定", "最贵", 1.0, "a"]].map(([t, o, c, w, k], i) => {
      const y = 46 + i * 42;
      return (
        <g key={i}>
          <FB x={16} y={y} w={160} h={34} k={k} t={t} tc="t" />
          <FT x={188} y={y + 21} c="tn" w={200}>{o}</FT>
          <rect x={400} y={y + 10} width={160 * w} height={14} rx={2} className={w > 0.6 ? "fa" : "fp"} fillOpacity={0.85} />
          <FT x={572} y={y + 21} c="tn" w={72}>{c}</FT>
        </g>
      );
    })}
    <FT x={400} y={40} c="tn">标注成本 →</FT>
    <FT x={16} y={230} c="tn" w={628}>实践建议:先用现成的大模型做零样本基线,再决定要不要标数据。很多任务上,零样本已经够用,标注预算该省下来投到评估集上。</FT>
  </FigFrame>
);

/* c403 —— LLM 的三个阶段 */
FIGN["c403-stages"] = ({ idx }) => (
  <FigFrame h={244} idx={idx}
    cap="能力几乎全部来自第一阶段,而你能接触和改变的几乎全在第二、三阶段。这解释了一个反复出现的结论:微调改的是「怎么答」,不是「知道什么」——想让模型知道新事实,该用检索,不是训练。">
    <FT x={16} y={24} c="tt">预训练 → 监督微调 → 对齐</FT>
    {[["预训练 Pretraining", "万亿 token · 数月 · 数千万美元", "学会语言、知识与推理雏形", 1.0, "p"],
      ["监督微调 SFT", "千到十万条 · 几小时", "学会「以什么形式回答」", 0.12, "p"],
      ["对齐 RLHF / DPO", "偏好对 · 几天", "在都对的答案里挑更好的那个", 0.06, "a"]].map(([t, cost, gain, w, k], i) => {
      const y = 46 + i * 56;
      return (
        <g key={i}>
          <FB x={16} y={y} w={190} h={44} k={k} t={t} tc="t" />
          <FT x={218} y={y + 18} c="tm">{cost}</FT>
          <FT x={218} y={y + 34} c="tn" w={200}>{gain}</FT>
          <rect x={440} y={y + 14} width={190 * w} height={16} rx={2} className="fp" fillOpacity={0.85} />
          {i < 2 && <FA x1={111} y1={y + 46} x2={111} y2={y + 54} />}
        </g>
      );
    })}
    <FT x={440} y={40} c="tn">算力占比 →</FT>
    <FT x={16} y={226} c="tn" w={628}>注意第三列的算力条:预训练占了 99% 以上。你作为使用者能动的那两格,加起来还不到 1%——所以「选对基座模型」这个决定,比后面所有调参加起来都重要。</FT>
  </FigFrame>
);

/* c403 —— 缩放定律 */
FIGN["c403-scaling"] = ({ idx }) => {
  const X0 = 62, X1 = 470, Y0 = 46, Y1 = 168;
  const xf = (e) => X0 + ((e - 18) / 8) * (X1 - X0);
  const yf = (l) => Y1 - ((3.4 - l) / 1.5) * (Y1 - Y0);
  const pts = [];
  for (let e = 18; e <= 26; e += 0.25) pts.push(`${xf(e)},${yf(1.9 + 12 * Math.pow(10, -e * 0.13))}`);
  return (
    <FigFrame h={240} idx={idx}
      cap="损失随算力、参数、数据以幂律下降——在双对数坐标下是一条直线,而且直得可以外推。这条线的实用价值在于:它让「再大十倍会怎样」从赌博变成了可以事先算出来的估计。">
      <FT x={16} y={24} c="tt">双对数坐标下,损失是一条直线</FT>
      {[2.0, 2.4, 2.8, 3.2].map((l) => (
        <g key={l}>
          <line x1={X0} x2={X1} y1={yf(l)} y2={yf(l)} className="grid" />
          <FT x={X0 - 8} y={yf(l) + 4} c="tn" a="end">{l.toFixed(1)}</FT>
        </g>
      ))}
      <line x1={X0} x2={X1} y1={Y1} y2={Y1} className="axis" />
      {[18, 20, 22, 24, 26].map((e) => <FT key={e} x={xf(e)} y={Y1 + 15} c="tn" a="middle">10^{e}</FT>)}
      <FT x={X1} y={Y1 + 30} c="tn" a="end">训练算力 FLOPs(对数)→</FT>
      <FT x={X0 - 8} y={Y0 - 6} c="tn" a="end">损失</FT>
      <polyline className="cv" points={pts.join(" ")} />
      <FT x={488} y={70} c="t">三个可交换的资源</FT>
      <FT x={488} y={92} c="tn" w={156}>参数量 N · 数据量 D · 算力 C ≈ 6ND。给定预算,三者的最优配比大约是每个参数配 20 个 token。</FT>
      <FT x={488} y={158} c="ta">但注意</FT>
      <FT x={488} y={176} c="tn" w={156}>推理成本比训练成本重要得多,所以今天流行「过度训练小模型」。</FT>
      <FT x={16} y={204} c="tn" w={440}>缩放定律描述的是平均损失,不是具体能力。某项能力在什么规模「涌现」,这条曲线不告诉你——这也是它最被误读的地方。</FT>
    </FigFrame>
  );
};

/* c404 —— 对齐手段分层 */
FIGN["c404-layers"] = ({ idx }) => (
  <FigFrame h={244} idx={idx}
    cap="安全从来不是一层能解决的。训练期决定模型的默认倾向,推理期挡住当下这一次,系统层决定「就算被绕过了它能做什么」。前两层是概率的,只有第三层是确定的——所以真正的赌注要押在权限设计上。">
    <FT x={16} y={24} c="tt">三层防线,只有一层是确定的</FT>
    {[["训练期对齐", "RLHF · DPO · 宪法式 AI · 数据过滤", "改变模型的默认倾向。覆盖面最广,但无法保证。", "p"],
      ["推理期防护", "输入/输出分类器 · 拒答策略 · 系统提示", "挡住当下这一次。是分类器,就有误伤率。", "a"],
      ["系统层约束", "工具权限 · 沙箱 · 额度 · 人工确认", "确定性的。模型再被绕过,也做不了没授权的事。", "p"]].map(([t, ex, s, k], i) => {
      const y = 46 + i * 58;
      return (
        <g key={i}>
          <FB x={16} y={y} w={166} h={46} k={k} t={t} tc="t" />
          <FT x={194} y={y + 18} c="tm">{ex}</FT>
          <FT x={194} y={y + 34} c="tn" w={444}>{s}</FT>
        </g>
      );
    })}
    <FT x={16} y={230} c="tn" w={628}>提示注入是这门课最该讲透的一点:可信指令和不可信数据混在同一个 token 流里,模型没有可靠办法区分。所以它不能靠「在提示词里再叮嘱一句」解决,只能靠最小权限和不可逆动作确认。</FT>
  </FigFrame>
);

/* c404 —— 有用性与无害性的取舍 */
FIGN["c404-tradeoff"] = ({ idx }) => {
  const X0 = 80, X1 = 420, Y0 = 52, Y1 = 176;
  const pts = [];
  for (let i = 0; i <= 40; i++) { const t = i / 40; pts.push(`${X0 + t * (X1 - X0)},${Y1 - (1 - Math.pow(t, 2.2)) * (Y1 - Y0)}`); }
  return (
    <FigFrame h={244} idx={idx}
      cap="把安全阈值一路调严,有害输出确实变少,但正常请求被拒绝的比例上升得更快。一个把 15% 正常问题判成违规的助手,用户三天就不用了——而且他不会投诉,他会直接走。取舍点必须显式选,不能默认。">
      <FT x={16} y={24} c="tt">越安全 ≠ 越好</FT>
      <line x1={X0} x2={X1} y1={Y1} y2={Y1} className="axis" />
      <line x1={X0} x2={X0} y1={Y0} y2={Y1} className="axis" />
      <polyline className="cv" points={pts.join(" ")} />
      <FT x={X0 - 8} y={Y0 + 6} c="tn" a="end">高</FT>
      <FT x={X0 - 8} y={(Y0 + Y1) / 2} c="tn" a="end">有用性</FT>
      <FT x={X1} y={Y1 + 16} c="tn" a="end">安全严格度 →</FT>
      <circle cx={X0 + 0.32 * (X1 - X0)} cy={Y1 - (1 - Math.pow(0.32, 2.2)) * (Y1 - Y0)} r="5" className="fp" />
      <FT x={X0 + 0.32 * (X1 - X0) + 10} y={Y1 - (1 - Math.pow(0.32, 2.2)) * (Y1 - Y0) - 8} c="tp">通常的合理区间</FT>
      <circle cx={X1 - 8} cy={Y1 - 6} r="5" className="fa" />
      <FT x={X1 - 14} y={Y1 - 16} c="ta" a="end">过度拒绝</FT>
      <FB x={442} y={56} w={202} h={56} k="a" t="必须写下来的三个数" s="误拦率上限 · 漏放一次的代价 · 灰区怎么处理" tc="t" />
      <FB x={442} y={120} w={202} h={56} k="p" t="灰区的正确做法" s="降级而不是拒绝:不调工具、不给具体数字、提示人工确认" tc="t" />
      <FT x={16} y={212} c="tn" w={628}>内部研发工具和面向公众的金融客服,这三个数的合理取值差好几个量级。没写下来的团队,实际上是让某个工程师在某个下午凭感觉定了它们。</FT>
    </FigFrame>
  );
};

/* c405 —— 提示 / RAG / 微调 阶梯 */
FIGN["c405-ladder"] = ({ idx }) => (
  <FigFrame h={236} idx={idx}
    cap="效果不够好时,先问「缺的是什么」:缺知识就检索,缺格式和风格才微调,缺推理就换模型或加循环。归因错了,后面全白做——最常见也最贵的错误,是把「缺知识」当成「缺能力」去微调。">
    <FT x={16} y={24} c="tt">从便宜到贵,一级一级往下走</FT>
    {[["改提示词", "半天 · 立刻回滚", 0.06, "p"],
      ["加 few-shot 示例", "1 天 · 10 条样本", 0.14, "p"],
      ["接检索 RAG", "1–2 周 · 改数据即生效", 0.4, "p"],
      ["监督微调", "2–3 周 · 500–2000 条", 0.75, "a"],
      ["偏好优化 / RL", "1–2 月 · 4000 条以上", 1.0, "a"]].map(([t, s, w, k], i) => {
      const y = 46 + i * 34;
      return (
        <g key={i}>
          <rect x={16 + i * 12} y={y} width={180} height={26} rx={3} className={`bx ${k}`} />
          <FT x={26 + i * 12} y={y + 18} c="t">{t}</FT>
          <FT x={224} y={y + 18} c="tn">{s}</FT>
          <rect x={430} y={y + 6} width={200 * w} height={14} rx={2} className={w > 0.6 ? "fa" : "fp"} fillOpacity={0.85} />
        </g>
      );
    })}
    <FT x={430} y={40} c="tn">成本 →</FT>
    <FT x={16} y={228} c="tn" w={628}>每往下一级,成本涨一个量级、可逆性掉一截。默认策略是从上往下走,每一级都跑一次评估集,只有这一级明确解决不了才继续往下。</FT>
  </FigFrame>
);

/* c405 —— RAG 流水线 */
FIGN["c405-rag"] = ({ idx }) => (
  <FigFrame h={244} idx={idx}
    cap="朴素 RAG 是「切块 → 向量搜索 → 塞进提示词」,在真实语料上召回率常常只有五成。每一级改进都在补同一个洞:一次相似度搜索不够。其中混合检索性价比最高——两路可以并行,几乎不加延迟。">
    <FT x={16} y={24} c="tt">一次搜索不够,所以有了这几级</FT>
    {[["切块", "300–800 token · 按结构切而不是按长度切", "m"],
      ["混合检索", "向量 + 关键词,盲区互补,+8~15 点召回", "p"],
      ["重排", "宽召回 50 条,交叉编码器精排出 5 条", "p"],
      ["生成 + 引用", "让答案能指回原文,幻觉才可查", "a"]].map(([t, s, k], i) => {
      const y = 46 + i * 44;
      return (
        <g key={i}>
          <FB x={16} y={y} w={140} h={36} k={k} t={t} tc="t" />
          <FT x={172} y={y + 22} c="tn" w={468}>{s}</FT>
          {i < 3 && <FA x1={86} y1={y + 38} x2={86} y2={y + 44} />}
        </g>
      );
    })}
    <FT x={16} y={230} c="tn" w={628}>评估必须分两层:先量检索(正确文档在不在 top-k),再量生成(给定正确文档能不能答对)。只看最终答案准确率,你永远不知道该修哪一头——这是 RAG 调优里最省时间的一条纪律。</FT>
  </FigFrame>
);

/* c406 —— 研究闭环 */
FIGN["c406-loop"] = ({ idx }) => (
  <FigFrame h={240} idx={idx}
    cap="研究不是「想一个好点子然后实现」,而是一个不断收窄的循环。新手最常卡在第一格和第四格:问题太大导致做不完,或者实验做完了但没有能判定成败的基线。先定基线,再做实验。">
    <FT x={16} y={24} c="tt">读 → 问 → 假设 → 实验 → 判定</FT>
    {[["读文献", "先读 abstract 与 figure,再看正文", 16],
      ["提出问题", "小到能在两周内证伪", 144],
      ["写下假设", "「如果 X,那么指标 Y 会变 Z」", 272],
      ["最小实验", "先做能推翻假设的那一个", 400],
      ["对照基线", "没有基线的结果不是结果", 528]].map(([t, s, x], i) => (
      <g key={i}>
        <FB x={x} y={54} w={116} h={54} k={i === 4 ? "a" : "p"} t={t} s={s} tc="t" />
        {i < 4 && <FA x1={x + 118} y1={81} x2={x + 142} y2={81} k="p" />}
      </g>
    ))}
    <FAP d="M586 110 L 586 140 L 74 140 L 74 112" k="a" />
    <FT x={330} y={156} c="ta" a="middle">结果回头修正问题——这一圈才是研究本身</FT>
    <FT x={16} y={192} c="t">三条给新手的硬建议</FT>
    <FT x={16} y={214} c="tn" w={628}>① 复现一篇论文再谈创新——你会在复现里学到论文没写的九成细节。② 每个实验先写下「什么结果会让我放弃这个假设」。③ 实验记录当天写完,一周后你会忘记当时为什么那样设参数。</FT>
  </FigFrame>
);

/* c406 —— Capstone 时间线 */
FIGN["c406-capstone"] = ({ idx }) => {
  const ph = [
    { t: "选题与调研", w: 2, s: "找到一个有公开基线的问题", k: "fp" },
    { t: "复现基线", w: 3, s: "跑通并对齐论文数字", k: "fp" },
    { t: "改进与实验", w: 5, s: "一次只改一个变量", k: "fa" },
    { t: "消融与分析", w: 2, s: "证明是你的改动带来的提升", k: "fp" },
    { t: "成文与开源", w: 2, s: "README 能让别人一键复现", k: "fp" },
  ];
  const total = ph.reduce((a, b) => a + b.w, 0);
  let acc = 0;
  return (
    <FigFrame h={232} idx={idx}
      cap="一个 14 周的 Capstone,真正花时间的不是「想新点子」,而是复现基线和做消融。能把消融做干净的项目,才有资格说「我的改进有效」——这也是简历上最能说服人的一段。">
      <FT x={16} y={24} c="tt">14 周的分配</FT>
      {ph.map((p, i) => {
        const x = 16 + (acc / total) * 628;
        const w = (p.w / total) * 628;
        acc += p.w;
        return (
          <g key={i}>
            <rect x={x} y={46} width={w - 3} height={30} rx={3} className={p.k} fillOpacity={0.8} />
            <text x={x + w / 2 - 1} y={66} textAnchor="middle" className="tn">{p.w} 周</text>
          </g>
        );
      })}
      {ph.map((p, i) => {
        const y = 92 + i * 26;
        return (
          <g key={i}>
            <FT x={16} y={y + 12} c="t">{i + 1}. {p.t}</FT>
            <FT x={160} y={y + 12} c="tn" w={484}>{p.s}</FT>
          </g>
        );
      })}
      <FT x={16} y={232} c="tn" w={628}>验收标准写在开工那天,而不是结题那天:一个能一键复现的仓库、一张对照基线的表、一段说清「为什么有效」的分析。三样齐了,这个项目才算完成。</FT>
    </FigFrame>
  );
};

/* =========================================================
   c407 本地大模型实战 —— 六步链路,每步下面挂着它的典型故障
   ========================================================= */
FIGN["c407-pipeline"] = ({ idx }) => {
  const steps = [
    { t: "拿代码", s: "git clone", f: "仓库停更 →\n决定依赖版本", k: "" },
    { t: "拿权重", s: "11.6 GB", f: "git-lfs 没装 →\n目录齐全但没权重", k: "a" },
    { t: "装环境", s: "venv + pip", f: "不锁版本 →\nKV Cache 接口报错", k: "a" },
    { t: "加载", s: "from_pretrained", f: "device_map=auto →\n静默卸载到磁盘", k: "a" },
    { t: "对话", s: "cli / web / API", f: "回答正确\n但每字 48 秒", k: "" },
    { t: "排查", s: "读日志", f: "加载太快\n= 根本没读", k: "p" },
  ];
  const X = 14, BW = 98, GAP = 10;
  return (
    <FigFrame h={258} idx={idx}
      cap="从 git clone 到「它回答我了」一共六步,而其中四步各有一个不会报错的坑。注意最后一步:这条链路上最贵的故障都是静默的——程序照常运行,只是慢一千倍。">
      <FT x={14} y={20} c="tt">本地部署的六步链路</FT>
      <FT x={646} y={20} c="tn" a="end">下方为该步的典型故障</FT>

      {steps.map((st, i) => {
        const x = X + i * (BW + GAP);
        return (
          <g key={i}>
            <FB x={x} y={34} w={BW} h={46} k={st.k} t={st.t} s={st.s} />
            {i < steps.length - 1 && <FA x1={x + BW + 1} y1={57} x2={x + BW + GAP - 2} y2={57} />}
            {/* drop line to the failure note */}
            <line x1={x + BW / 2} y1={80} x2={x + BW / 2} y2={98} className="ln d" />
            <rect x={x} y={98} width={BW} height={52} rx={3}
              className={st.k === "a" ? "bx a" : "bx g"} fillOpacity={0.5} />
            {st.f.split("\n").map((ln, j) => (
              <FT key={j} x={x + BW / 2} y={116 + j * 14} c={st.k === "a" ? "ta" : "tn"} a="middle">{ln}</FT>
            ))}
          </g>
        );
      })}

      <line x1={14} y1={170} x2={646} y2={170} className="axis" />
      <FT x={14} y={190} c="tk">四个坑里有三个不会报错。</FT>
      <FT x={14} y={210} c="tn" w={632}>
        没装 git-lfs 会给你一个看起来完整的目录;device_map="auto" 内存不够时不会失败,只会把权重卸到磁盘;而版本不匹配虽然会报错,报的却是一句和真正原因毫无关系的 KeyError。养成的习惯应该是:每一步做完都验证一次,而不是等到最后一步才发现不对。
      </FT>
    </FigFrame>
  );
};

/* =========================================================
   c407 内存账本 —— 精度 × 规模 决定这台机器装不装得下
   ========================================================= */
FIGN["c407-ledger"] = ({ idx }) => {
  // bytes per parameter by precision
  const precs = [
    { n: "fp32", b: 4, note: "默认值,几乎没必要" },
    { n: "bf16 / fp16", b: 2, note: "最常用" },
    { n: "int8", b: 1, note: "需要 CUDA" },
    { n: "int4", b: 0.5, note: "需要 CUDA 或 GGUF" },
  ];
  const models = [{ n: "1.5B", p: 1.5 }, { n: "6B", p: 6.24 }, { n: "13B", p: 13 }];
  const X = 132, CW = 84, SC = 5.0;
  // 一台 16GB 笔记本开着浏览器时的真实可用空间
  const budget = 3.35;
  return (
    <FigFrame h={262} idx={idx}
      cap="权重体积 = 参数量 × 每参数字节数。橙色竖线是那台 15.7GB 笔记本开着浏览器时真正能给权重的空间(可用 4.85GB 再减 1.5GB 运行时)——它比「我有 16GB」给人的印象窄得多,而只有 int4 的 1.5B 与 6B 能落在它左边。">
      <FT x={14} y={20} c="tt">权重体积 vs 一台 16GB 笔记本的真实预算</FT>

      {/* column headers */}
      {models.map((m, i) => (
        <FT key={i} x={X + 46 + i * CW} y={20} c="tn" a="middle">{m.n}</FT>
      ))}
      <FT x={X + 3 * CW + 30} y={20} c="tn">备注</FT>

      {precs.map((pr, r) => {
        const y = 34 + r * 42;
        return (
          <g key={r}>
            <FT x={14} y={y + 24} c="t">{pr.n}</FT>
            <FT x={118} y={y + 24} c="tn" a="end">{`${pr.b} B/参数`}</FT>
            {models.map((m, i) => {
              const gb = m.p * pr.b;
              const fits = gb <= budget;
              return (
                <g key={i}>
                  <rect x={X + i * CW} y={y + 10} width={CW - 8} height={20} rx={3}
                    className={fits ? "bx p" : "bx m"} fillOpacity={fits ? 1 : 0.45} />
                  <FT x={X + (CW - 8) / 2 + i * CW} y={y + 24} c={fits ? "tp" : "tn"} a="middle">
                    {`${gb < 10 ? gb.toFixed(1) : Math.round(gb)} GB`}
                  </FT>
                </g>
              );
            })}
            <FT x={X + 3 * CW + 30} y={y + 24} c="tn">{pr.note}</FT>
          </g>
        );
      })}

      {/* the real budget line */}
      <line x1={X + budget * SC} y1={28} x2={X + budget * SC} y2={210} className="ln a d" />
      <FT x={X + budget * SC + 6} y={222} c="ta">真实可用 ≈ 3.35 GB</FT>
      <FT x={14} y={222} c="tk">蓝色 = 装得下</FT>

      <FT x={14} y={244} c="tn" w={632}>
        关键在于用「可用内存」而不是「总内存」:那台机器总共 15.7GB,但开着浏览器时可用只剩 4.85GB,再扣掉 Python 与 PyTorch 运行时约 1.5GB,留给权重的就只有 3.35GB。而 6B 在 bf16 下要 11.6GB——差的不是一点,是三倍多。
      </FT>
    </FigFrame>
  );
};
