nn-anatomy

NN forward + backward 理解のための GUI シミュレータ — 設計書

版: 0.6 (2026-04-21 / Python 側を uv 運用に変更) 前提: prompt/00-init.md で指定された本プロジェクトの目的・トーンを継承する。

変更履歴:


1. 目的

Neural Network (以下 NN) の動作を 「ノード 1 個ずつで何が起きているか」 という粒度で初学者に理解させる。具体的には次の 3 つを、同じ 1 つの画面内で手を動かしながら確認できるようにする。

  1. Forward: 入力 x がどのように重み w・バイアス b と結びつき、活性化関数 φ を通って次の層に伝わるのか
  2. Backward: 損失 L から逆向きに勾配 dL/dz, dL/dw, dL/db がどう伝播するのか
  3. Update: 勾配降下法によって w, b がどれだけ、どの方向に更新されるのか (Δw, Δb)

さらに、上記のノード単位の挙動と、ネットワーク全体としての学習の進行 (損失曲線・決定境界) とが 同時に見える ようにすることで、「ミクロな計算」と「マクロな挙動」を結び付けて理解させる。

2. 想定ユーザーと前提知識

項目 想定
対象 大学情報系学部の初学者 (2〜3 年次, データマイニング / ML 入門相当)
数学 偏微分・行列積は「名前を聞いたことはある」程度でも通じる説明を目指す
プログラミング Python/JS を書くことは求めない。ブラウザで開くだけ で動く
利用場面 (a) 講義中にスクリーン投影しての解説、(b) 学生各自が手元 PC で操作

3. スコープ

In scope (v1)

Out of scope (v1)

4. 画面レイアウト (ワイヤー)

画面は左右に分けず、縦 3 ペイン で構成する。全ペインが同時に見えることが最重要。

┌──────────────────────────────────────────────────────────────┐
│ [A] コントロールバー                                          │
│  Network: [2]-[3]-[1] ▼  Act: [Sigmoid▼]  LR: [0.5] Data:[XOR▼]│
│  Seed: [     ] (空=毎回ランダム)   Init: [Xavier▼]             │
│  [Forward] [Backward] [Update] [1 Step = F+B+U] [Run 100] [Reset]│
├──────────────────────────────────────────────────────────────┤
│ [B] ネットワーク・ビュー (メイン)                              │
│                                                              │
│   x1 ●────w11───● h1 (z=.., a=..)                            │
│       ╲   w12  ╱│                                            │
│        ╲     ╱  │──w_o1──● ŷ  →  L = ..                      │
│       ╱ ╲   ╱   │                                            │
│   x2 ●───w22────● h2 (z=.., a=..)                            │
│                                                              │
│   ※ ノードをクリック → 右の詳細パネルに展開                  │
│   ※ 「現在のフェーズ: FORWARD / BACKWARD / UPDATE」表示       │
├──────────────────────────────────────────────────────────────┤
│ [C] 補助パネル (タブ切替)                                     │
│  ┌Loss┐ ┌決定境界┐ ┌式の展開★┐ ┌イベントログ┐                 │
│   折れ線  2D 等高線  Fw/Bw/Up の式  「x1→h1 に 0.3 を送った」等 │
│  ★ [C] の主役。現在のフェーズに応じて自動で表示内容が切り替わる │
└──────────────────────────────────────────────────────────────┘

ペインの役割

5. データモデル

純 JS (TypeScript 風に記述)。単一 HTML 内に <script> で閉じ込める。

type Activation = 'sigmoid' | 'tanh' | 'relu' | 'linear';
type Loss       = 'mse' | 'bce';

interface Node {
  id: string;          // 'L1_N0' など
  z: number;           // pre-activation
  a: number;           // post-activation
  b: number;           // bias
  dL_dz: number;       // 誤差信号 (δ)
  dL_db: number;       // バイアスに対する勾配
  db_prev: number;     // 直前 step の更新量 (可視化用)
}

interface Edge {
  from: string;        // node id
  to:   string;
  w: number;
  dL_dw: number;
  dw_prev: number;     // Δw (直前 step の更新量)
}

interface Layer {
  index: number;
  nodes: Node[];
  activation: Activation;
}

interface Net {
  layers: Layer[];     // layers[0] は入力層 (活性化なし)
  edges:  Edge[];      // 全エッジ (層をまたぐ)
  loss:   Loss;
  lr:     number;
  phase:  'idle' | 'forward' | 'backward' | 'update';
  history: { epoch: number; loss: number }[];

  // --- 乱数関連 (v0.3 で追加 / v0.4 でサブストリーム化) ---
  seedInput:  number | null;   // UI で入力された値。空欄なら null
  seedUsed:   number;          // 実際に採用された「マスター seed」 (自動生成含む)
  initScheme: 'xavier' | 'he' | 'uniform' | 'zero';  // 重み初期化方式

  // seedUsed から決定的に導出された 2 本の独立ストリーム。
  // 片方を多く消費してももう片方の出力は変わらないため、
  // 「データだけ作り直す」「重みだけ作り直す」が独立に扱える。
  rngWeights: () => number;    // 重み/バイアス初期化用 [0,1)
  rngData:    () => number;    // データセット生成用     [0,1)
}

ネットワークは JSON としてエクスポート/インポートできるようにし、授業で「この構成でやってみて」を配りやすくする。

6. 計算仕様

6.1 Forward

レイヤ l (≥1) の各ノード j について:

z_j^{(l)} = b_j^{(l)} + Σ_i  w_{ij}^{(l)}  ·  a_i^{(l-1)}
a_j^{(l)} = φ( z_j^{(l)} )

※ 入力層は a = x, z は未定義。

6.2 Loss

6.3 Backward

出力層 L:

dL/dz_j^{(L)} = (∂L/∂a_j^{(L)}) · φ'( z_j^{(L)} )

中間層 l < L:

dL/dz_j^{(l)} = ( Σ_k w_{jk}^{(l+1)} · dL/dz_k^{(l+1)} ) · φ'( z_j^{(l)} )

エッジ / バイアス:

dL/dw_{ij}^{(l)} = a_i^{(l-1)} · dL/dz_j^{(l)}
dL/db_j^{(l)}    =                dL/dz_j^{(l)}

6.4 Update (SGD)

Δw = −η · dL/dw     →   w ← w + Δw
Δb = −η · dL/db     →   b ← b + Δb

v1 は バッチ = 全サンプル平均 の勾配を使い、1 Epoch = 1 Update とする。ノード単位のトレースを見せるときはサンプル数を 1 に落として見せる。

6.5 乱数と初期化 (再現性)

方針

疑似乱数の実装 (Mulberry32)

Math.random() は seed 設定不可のため、Mulberry32 を採用する。32-bit 状態・数行で書ける・分布が十分良い・単一 HTML への埋め込みに向く。

function mulberry32(seed) {
  let t = seed >>> 0;                 // unsigned 32-bit
  return function() {
    t = (t + 0x6D2B79F5) >>> 0;
    let r = Math.imul(t ^ (t >>> 15), 1 | t);
    r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;
    return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
  };
}

正規分布が必要な箇所 (Xavier / He 初期化) は Box-Muller で同じ rng から合成する:

function randn(rng) {
  // Box-Muller。rng は [0,1) を返す関数。
  const u1 = Math.max(rng(), 1e-12);  // log(0) 回避
  const u2 = rng();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

マスター seed からサブシードを導出する

マスター seed の消費順序に依存して挙動が変わると「データのサンプル数を変えたら初期重みも変わった」といった混乱が起きるため、1 本の rng を直接使わず、マスター seed から 2 つのサブシードを固定文字列と XOR で導出する。

// 固定定数 (2^32 未満の任意の値で可)。将来のサブストリーム追加用に名前付きで管理する。
const SUB = {
  WEIGHTS: 0xA5A5A5A5,   // ≒ 'weights'
  DATA:    0x5A5A5A5A,   // ≒ 'data'
};

function deriveSubSeed(masterSeed, tag) {
  // 単純 XOR で十分 (Mulberry32 側が seed 依存のカオスを持つため、
  // サブシード間の相関は実用上無視できる)。
  return (masterSeed ^ tag) >>> 0;
}

net.rngWeights = mulberry32(deriveSubSeed(seedUsed, SUB.WEIGHTS));
net.rngData    = mulberry32(deriveSubSeed(seedUsed, SUB.DATA));

こうすることで、

Net の構築フロー

1. ユーザー入力 seedInput を読む (空欄なら null)
2. seedUsed を決定:
     seedInput !== null    → seedUsed = seedInput
     seedInput === null    → seedUsed = (Date.now() ^ Math.floor(Math.random()*2**31)) >>> 0
3. サブストリームを構築:
     net.rngWeights = mulberry32(deriveSubSeed(seedUsed, SUB.WEIGHTS))
     net.rngData    = mulberry32(deriveSubSeed(seedUsed, SUB.DATA))
4. 重みを init scheme に従って初期化 (rngWeights を使用):
     xavier: w ~ N(0, 1/fan_in)       ← 既定
     he    : w ~ N(0, 2/fan_in)
     uniform: w ~ U(-0.5, 0.5)
     zero   : w = 0   (XOR が解けない実例として教材に使う)
   バイアスは一律 0。
5. seedUsed をイベントログと保存 JSON に必ず記録する。

UI ルール

テスト

6.6 データセット生成 (乱数影響範囲)

組み込みデータセットは以下の 5 種類。シードの影響 (= 乱数を使うか) を明記する。

名前 乱数使用 生成内容
AND なし 4 点の決定的な真理値表
OR なし 4 点の決定的な真理値表
XOR なし 4 点の決定的な真理値表
円形 2 クラス あり 内円 (y=0) と外環 (y=1) から N 点をサンプル + ガウスノイズ
手描き 2D なし ユーザー入力そのもの

円形 2 クラスの生成 (疑似コード):

function makeCircle(n, noiseStd, rng /* = net.rngData */) {
  const pts = [];
  for (let i = 0; i < n; i++) {
    const r     = i < n/2 ? 0.3 : 1.0;         // 内円 / 外環
    const label = i < n/2 ? 0   : 1;
    const theta = 2 * Math.PI * rng();
    const x1 = r * Math.cos(theta) + randn(rng) * noiseStd;
    const x2 = r * Math.sin(theta) + randn(rng) * noiseStd;
    pts.push({ x:[x1,x2], y: label });
  }
  return pts;
}

重要な設計判断:

7. 可視化仕様 (最重要)

ユーザーが選んだ「見たいもの」4 つ、すべてを ON/OFF できるレイヤとして実装する。

レイヤ 内容 表示方法
(i) Values 各ノードの z, a ノード円の内側 2 段に z=0.42 / a=0.60
(ii) Grads dL/dz, dL/dw Backward 中は各ノードに赤字で dL/dz、各エッジ上に dL/dw を表示
(iii) Deltas Δw, Δb Update 直後に、エッジを太さ/色 (正=青 / 負=赤) で表現し、ラベルに Δw=+0.012
(iv) Macro 損失曲線 / 決定境界 [C] タブに折れ線・ヒートマップ

アニメーション規約

ノード / エッジの視覚的エンコード

7.3 補助パネル: 式展開仕様 (過程理解のためのチューター)

補助パネルの「式の展開」タブは、数値を見せるのではなく数式から数値へと降りていく過程を見せる ことを役割とする。下記 3 段で常に表示する。

[段1] 一般形 (記号のみ)           ← 教科書の式
[段2] この場所の記号に当てはめた形  ← どの変数が何に対応するか
[段3] 現在の数値を入れた形 = 結果   ← 実際の計算

さらに、段1 の各シンボルにカーソルを合わせると、その意味 (例: φ’ = sigmoid’ = a(1−a)) がポップする。

7.3.1 フェーズ別の表示内容

(A) Forward フェーズ・ノード選択時 (例: 隠れ層ノード h1)

[段1]  z_j^{(l)} = b_j^{(l)} + Σ_i  w_{ij}^{(l)} · a_i^{(l-1)}
       a_j^{(l)} = φ( z_j^{(l)} )              φ = sigmoid

[段2]  z(h1) = b(h1) + w[x1→h1]·a(x1) + w[x2→h1]·a(x2)
       a(h1) = sigmoid( z(h1) )

[段3]  z(h1) = 0.10 + 0.50·1 + (−0.30)·0 = 0.60
       a(h1) = sigmoid(0.60) = 0.646

(B) Backward フェーズ・出力層ノード選択時 (例: ŷ)

[段1]  dL/dz_j^{(L)} = (∂L/∂a_j^{(L)}) · φ'( z_j^{(L)} )

[段2]  ∂L/∂a(ŷ)  = ŷ − y             (∵ MSE かつ 0.5 係数)
       φ'(z(ŷ)) = a(ŷ)·(1−a(ŷ))       (∵ sigmoid)
       dL/dz(ŷ) = (ŷ − y) · a(ŷ)·(1−a(ŷ))

[段3]  = (0.71 − 1) · 0.71·0.29 = −0.0597

(C) Backward フェーズ・中間層ノード選択時 (例: h1) — 連鎖律の核

[段1]  dL/dz_j^{(l)} = ( Σ_k  w_{jk}^{(l+1)} · dL/dz_k^{(l+1)} ) · φ'( z_j^{(l)} )

[段2]  dL/dz(h1) =
         ( w[h1→ŷ] · dL/dz(ŷ) )        ← 後段からの誤差
         · a(h1)·(1−a(h1))              ← 自分の活性の傾き

[段3]  = ( 0.80 · (−0.0597) ) · 0.646 · 0.354
       = −0.0109

※ 画面上で w[h1→ŷ] と dL/dz(ŷ) の部分に
  「上流のエッジ」「上流ノードの誤差」と吹き出しで注釈を出す。

(D) Backward フェーズ・エッジ選択時 (例: x1 → h1)

[段1]  dL/dw_{ij}^{(l)} = a_i^{(l-1)} · dL/dz_j^{(l)}

[段2]  dL/dw[x1→h1] = a(x1)   ·  dL/dz(h1)
                       └入力側┘   └出力側の誤差┘

[段3]  = 1 · (−0.0109) = −0.0109

(E) Backward フェーズ・バイアス選択時 (ノードの b バッジをクリック)

[段1]  dL/db_j^{(l)} = dL/dz_j^{(l)}

[段2]  dL/db(h1) = dL/dz(h1)      (w は a と掛かるが、b は掛かる相手がない ⇒ そのまま)

[段3]  = −0.0109

(F) Update フェーズ・エッジ選択時 (同じ x1→h1 で継続)

[段1]  Δw = −η · dL/dw          w ← w + Δw

[段2]  Δw[x1→h1] = − lr · dL/dw[x1→h1]

[段3]  = −0.5 · (−0.0109) = +0.00545
       w[x1→h1] : 0.500 → 0.50545     (損失を下げる向きに増えた)

(G) Update フェーズ・ノードのバイアス選択時

[段1]  Δb = −η · dL/db           b ← b + Δb
[段2]  Δb(h1) = − lr · dL/db(h1)
[段3]  = −0.5 · (−0.0109) = +0.00545
       b(h1) : 0.100 → 0.10545

7.3.2 表示ルール

7.3.3 画面モックアップ (式の展開タブ)

────────────────── 式の展開 ──────────────────
選択: ノード h1 / フェーズ: BACKWARD
ひとこと: 出口の誤差を、通ってきた道を逆にたどって配る

[一般形]
  dL/dz_j^{(l)} =  ( Σ_k w_{jk}^{(l+1)} · dL/dz_k^{(l+1)} ) · φ'( z_j^{(l)} )

[当てはめ]
  dL/dz(h1) = ( w[h1→ŷ] · dL/dz(ŷ) ) · a(h1)·(1 − a(h1))
              ─────── 上流 ─────────   ──── 自分の傾き ────

[数値]
  = ( 0.80 · (−0.0597) ) · 0.646 · 0.354
  = −0.0109

[関連]  上流ノード ŷ へジャンプ →   エッジ h1→ŷ を見る →
───────────────────────────────────────────

実装は「段1/段2/段3 を、選択対象 × 現在フェーズ × 活性化関数 × 損失関数 の組で引ける文字列テンプレート」として定義し、テンプレート生成ロジックは src/js/explain.js に分離する。これにより、L2 正則化などを v2 で足すときも式文字列だけ追加すれば済む。

8. インタラクション

9. 教材としての段階シナリオ (重要)

シミュレータ自体に “Lesson” モード を持たせる。左上にレッスン選択があり、選ぶと構成・データ・ヒント文が自動で設定される。

# タイトル 構成 狙い
L1 1 ノードだけの線形変換 1→1, linear z = wx + b を手で動かして感覚を掴む
L2 活性化関数を通す 1→1, sigmoid 同じ z でも a が変わる / 飽和領域で勾配が死ぬのを見る
L3 小さな MLP の Forward 2→2→1, sigmoid ノード単位で Σ が積み上がる様子
L4 損失と勾配 2→2→1, MSE L を数値で確認。1 エッジだけ動かして L がどう変わるか
L5 Backward の連鎖律 同上 δ が右から左に伝わるのをアニメで追う
L6 1 step Update 同上 Δw の向き = 損失を下げる方向を確認
L7 XOR を解く 2→3→1, sigmoid 100 epoch 回して決定境界が曲がっていく様子
L8 活性化による違い 同じ構成で Sigmoid / Tanh / ReLU を切替 勾配消失・ReLU の死に様を比べる

各レッスンには 「このレッスンでチェックすること (3 つ)」 を画面下に常駐表示する。

10. 非機能要件

11. ファイル構成

NN forward + backward 理解のためのGUIシミュレータ/
├── prompt/
│   └── 00-init.md              # プロジェクト総則 (既存前提)
├── docs/
│   ├── design.md               # 本書
│   ├── math-notes.md           # 数式の完全版 (v0.2 以降)
│   └── lesson-plans.md         # 授業運用メモ (v0.2 以降)
├── src/                        # 実装フェーズで使用
│   ├── index.html              # 配布物はこれ 1 つに bundle
│   ├── style.css
│   └── js/
│       ├── model.js            # Net / Forward / Backward / Update
│       ├── view.js             # SVG 描画
│       ├── controller.js       # イベント配線
│       ├── explain.js          # §7.3 の式テンプレート (Fw/Bw/Up × 選択対象)
│       ├── rng.js              # §6.5 の Mulberry32 + randn(Box-Muller)
│       ├── lessons.js          # L1〜L8 定義 (推奨 seed を含む)
│       └── datasets.js         # AND/OR/XOR/円
├── tests/
│   ├── fixtures/               # Python が生成 → JS が読む共通フィクスチャ
│   ├── py/                     # pytest 用: 参照実装の単体テスト
│   └── js/                     # node --test 用: model.js 等の検証
├── tools/
│   └── bundle.py               # src/ を build/nn_sim.html に inline 展開
├── build/
│   └── nn_sim.html             # src をインラインに展開した配布用
├── pyproject.toml              # uv が管理する Python 依存 (numpy, pytest)
├── uv.lock                     # uv が生成するロックファイル (コミット対象)
├── .python-version             # uv が参照する Python バージョン固定
├── Makefile                    # §15.4 の sync / test / bundle / serve
└── .gitignore                  # .venv/, build/, __pycache__/ など

開発時は分割、配布時は 1 ファイルに結合 (軽量ビルドスクリプト 1 本で行う)。

12. 実装ロードマップ

Phase 期間目安 成果物 確認
P0 1 日 本設計書合意
P1 2〜3 日 model.js (純計算) + 数値テスト 手計算 / Python の NumPy 実装と突き合わせる
P2 2〜3 日 view.js 静的描画 L1/L2 がノード値まで見える
P3 3 日 Forward/Backward アニメ 粒が流れるのを目視で
P4 2 日 Update + 学習ループ + Loss 曲線 XOR が収束する
P5 2 日 決定境界 / レッスン定義 L7 が動く
P6 1 日 単一 HTML へ bundle / オフライン検証 file:// で全機能

13. 検証方針 (「本当に教材として使えるか」)

  1. 計算の正しさ: 小さな固定入力で、Python/NumPy の参照実装と JS 実装を一致させる (差 < 1e-9)。
  2. 教育効果の予見チェック: 同僚 (または TA) に L1〜L7 を触らせ、「何が起きているか」を口頭で説明させてログを取る。説明が通らない箇所は UI/文言を直す。
  3. 故意の誤った使い方: LR を 100 にする / 同じ重みで初期化する、など壊れる操作を試し、エラー文言が教育的であること (e.g. 「勾配が発散しています。学習率を下げてみましょう」) を確認。

14. 拡張余地 (v2 以降メモ)

15. 開発環境・セットアップ

15.1 基本方針

本プロダクトの 実行 は単一 HTML + モダンブラウザだけで完結する (= 学生側は何もインストール不要)。ただし 開発・検証 フェーズでは、数値の正しさを担保するために参照実装と自動テストを走らせる必要があり、そのためのランタイムを最小限だけ用意する。

用途 必要なもの 必須?
シミュレータを動かす モダンブラウザ 1 つ 必須
コードを書く エディタ (VS Code 推奨) 必須
バージョン管理 Git 必須
Python パッケージ / 仮想環境の管理 uv (v0.11 以上) 必須 (P1 以降)
参照実装 (NumPy) と突き合わせる uv 経由で入れる NumPy / pytest 必須 (P1 以降)
JS 単体テストを CLI で回す Node.js v20+ (v22 で動作確認済) 必須 (P1 以降)
単一 HTML への bundle uv run python tools/bundle.py 必須 (P6)
ローカルで file:// 問題を回避したい時 uv run python -m http.server 任意

追加インストールが必要なコマンドラインツールは uv と Node.js の 2 つだけ。Python 本体は uv が必要に応じて引っ張ってくるので、別途 pyenv 等を用意する必要はない。JS 側は依存ゼロ (npm も使わない)、Python 側の依存も NumPy と pytest のみ。

15.2 バージョン指定と理由

15.3 初期セットアップ手順

本プロジェクトのルート (このフォルダ自身) が uv プロジェクトとして初期化済み。pyproject.toml / uv.lock / .python-version はコミット対象。.venv/ は .gitignore に従って無視。

# 1) リポジトリ取得
git clone <repo>
cd "NN forward + backward 理解のためのGUIシミュレータ"

# 2) Python 側: uv が pyproject.toml / uv.lock を読んで仮想環境と依存を一発で揃える
uv sync
#    → .venv/ が自動作成され、numpy / pytest が入る
#    → Python 3.10 が無ければ uv が自動取得 (ユーザーは pyenv 等不要)

# 3) Node 側: 追加ライブラリ不要。バージョン確認だけ
node --version                       # v20 以上であることを確認 (v22.16.0 で OK)

# 4) 動作確認
uv run python -c "import numpy; print(numpy.__version__)"   # 参照実装側
node --version && node -e "require('node:test')"            # JS テスト側

# 5) シミュレータを開く (P2 以降)
#    そのまま src/index.html をブラウザで開くだけ。サーバ不要。
open src/index.html                  # macOS
#    file:// が嫌なら:
#    uv run python -m http.server --directory src 8000

新しい Python パッケージを追加したくなった場合は uv add <package>、削除は uv remove <package>pip install は使わない。

15.4 開発中に走らせるコマンド (想定)

スクリプトは package.json / Makefile どちらでも良いが、ここでは Makefile 例を示す (教員・TA にとって読みやすい)。

.PHONY: sync test test-js test-py bundle serve clean

sync:
	uv sync                          # pyproject.toml / uv.lock に従い Python 依存を揃える

test: test-js test-py

test-js:
	node --test tests/js             # node:test で model.js / rng.js / explain.js を検証

test-py:
	uv run pytest tests/py           # NumPy 参照実装単体のテスト

bundle:
	uv run python tools/bundle.py    # src/*.html,css,js → build/nn_sim.html (単一 HTML)

serve:
	uv run python -m http.server --directory src 8000

clean:
	rm -rf build/

tools/bundle.py は数十行程度で書ける (外部依存なし):「src/index.html 内の <link rel="stylesheet" href=...><script src=...> を、参照先ファイルの中身に差し替えて 1 本の HTML として出力するだけ」。uv 経由で走らせるため、Python 標準ライブラリのみで書く (追加の uv add は行わない)。

15.5 数値一致検証 (JS ↔ NumPy)

P1 で最も重要な検証。下記のように、JS 側と Python 側で同じ seed・同じ構成・同じ入力から計算した w, b, z, a, dL/dz, dL/dw を書き出して突き合わせる。

tests/
├── fixtures/
│   └── case_xor_seed42.json          # 構成・seed・入力・期待出力のセット
├── py/
│   └── test_reference.py             # NumPy 実装 → fixtures を再現できることを確認
└── js/
    └── test_model.mjs                # model.js → fixtures と bitwise (差<1e-9) で一致

fixture は Python 側で生成し、JS 側はそれを読み込んで照合するだけ にする。こうすると「参照実装が事実上の教科書」として機能する。

15.6 推奨エディタ設定 (任意)

15.7 想定される環境トラブルと対処

症状 原因 対処
file:// でフォントや SVG がブロックされる 一部ブラウザの CORS 制約 python -m http.server で配信、または Safari の環境設定で「ローカルファイルからのクロスオリジンを許可」
Node 16 で node --test が動かない API が Node 18+ Node 20 LTS を入れる (nvm 推奨)
NumPy の結果と JS が 1e-15 オーダで合わない IEEE754 の評価順差 許容誤差 1e-9 を採用。それ以上ズレたらアルゴリズムを疑う
Math.random() が混入している 乱数方針違反 ESLint で Math.random() を禁止ルールに入れる (no-restricted-globals)

16. リスク・制約


以上を合意したら、P1 (model.js の純計算とテスト) から着手する。