版: 0.6 (2026-04-21 / Python 側を uv 運用に変更)
前提: prompt/00-init.md で指定された本プロジェクトの目的・トーンを継承する。
変更履歴:
uv init / uv add)。Node.js は v22 系で動作確認済み (node --test を使用、npm 不要)。Neural Network (以下 NN) の動作を 「ノード 1 個ずつで何が起きているか」 という粒度で初学者に理解させる。具体的には次の 3 つを、同じ 1 つの画面内で手を動かしながら確認できるようにする。
さらに、上記のノード単位の挙動と、ネットワーク全体としての学習の進行 (損失曲線・決定境界) とが 同時に見える ようにすることで、「ミクロな計算」と「マクロな挙動」を結び付けて理解させる。
| 項目 | 想定 |
|---|---|
| 対象 | 大学情報系学部の初学者 (2〜3 年次, データマイニング / ML 入門相当) |
| 数学 | 偏微分・行列積は「名前を聞いたことはある」程度でも通じる説明を目指す |
| プログラミング | Python/JS を書くことは求めない。ブラウザで開くだけ で動く |
| 利用場面 | (a) 講義中にスクリーン投影しての解説、(b) 学生各自が手元 PC で操作 |
画面は左右に分けず、縦 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] の主役。現在のフェーズに応じて自動で表示内容が切り替わる │
└──────────────────────────────────────────────────────────────┘
純 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 としてエクスポート/インポートできるようにし、授業で「この構成でやってみて」を配りやすくする。
レイヤ 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 は未定義。
L = 0.5 · Σ (ŷ − y)²L = −Σ [ y log ŷ + (1−y) log(1−ŷ) ]出力層 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)}
Δw = −η · dL/dw → w ← w + Δw
Δb = −η · dL/db → b ← b + Δb
v1 は バッチ = 全サンプル平均 の勾配を使い、1 Epoch = 1 Update とする。ノード単位のトレースを見せるときはサンプル数を 1 に落として見せる。
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 の消費順序に依存して挙動が変わると「データのサンプル数を変えたら初期重みも変わった」といった混乱が起きるため、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));
こうすることで、
SUB.SHUFFLE を増やすだけで済む。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 に必ず記録する。
[Init] seedUsed = 1713651234 (auto)
scheme = xavier
組み込みデータセットは以下の 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;
}
重要な設計判断:
net.rngData を渡す。内部で Math.random() を呼ばない (同じ関数を生のランダム源と再現源の両方で共用できるようにするため)。rngData は rngWeights と独立なので、重みの値には影響しない。rngData を サブシードから作り直して 再サンプリングする。これにより、「ノイズを 0.1 → 0.2 にしただけで、全く別のデータ分布になった」という混乱を避けられる (σ 値ごとに同じ seed からは同じ分布が再現される)。ユーザーが選んだ「見たいもの」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] タブに折れ線・ヒートマップ |
stroke-dashoffset をアニメ)。到達時にノードが pulse。| エッジ太さ: | w | に比例、色: 正負で青/赤 |
補助パネルの「式の展開」タブは、数値を見せるのではなく数式から数値へと降りていく過程を見せる ことを役割とする。下記 3 段で常に表示する。
[段1] 一般形 (記号のみ) ← 教科書の式
[段2] この場所の記号に当てはめた形 ← どの変数が何に対応するか
[段3] 現在の数値を入れた形 = 結果 ← 実際の計算
さらに、段1 の各シンボルにカーソルを合わせると、その意味 (例: φ’ = sigmoid’ = a(1−a)) がポップする。
(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
▼/▶ で開閉できるようにする。────────────────── 式の展開 ──────────────────
選択: ノード 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 で足すときも式文字列だけ追加すれば済む。
h1.z = b(=0.10) + w[x1→h1](=0.50)·x1(=1) + w[x2→h1](=-0.30)·x2(=0) = 0.60
h1.a = sigmoid(0.60) = 0.646
dL/dw[x1→h1] = a(x1) · dL/dz(h1) = 1 · 0.021 = 0.021
Δw = −lr · dL/dw = −0.5 · 0.021 = −0.0105
seed: 1713651234 (auto))。rngData をサブシードから作り直し、データのみ再サンプリングする (重みはそのまま)。これにより「σ を微修正したら重みまで変わった」という混乱を避ける。シミュレータ自体に “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 つ)」 を画面下に常駐表示する。
I18N オブジェクトで一元管理し、将来 en を足せる状態にしておく。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 本で行う)。
| 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:// で全機能 |
本プロダクトの 実行 は単一 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 のみ。
uv init / uv add / uv run / uv sync が安定している版。Python 本体のバージョン固定も uv に任せる (.python-version を自動生成)。match 文は使わないが、型注釈の書き方が安定しており演習 TA の環境と揃えやすい。.python-version に 3.10 を書き、uv が足りなければ自動取得する。float64 を明示するため、dtype=np.float64 を徹底する。P1 の動作確認では 2.2.6 を使用 (uv add numpy で自動選択)。uv add pytest で入る。node:test と node:assert だけで単体テストが書ける ため、Jest / Vitest などの追加依存が不要。node --test だけで走る。単一 HTML という最終成果物のポリシーと整合する。ユーザー環境の v22.16.0 はこれを満たすので追加作業不要。本プロジェクトのルート (このフォルダ自身) が 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 は使わない。
スクリプトは 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 は行わない)。
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 側はそれを読み込んで照合するだけ にする。こうすると「参照実装が事実上の教科書」として機能する。
.editorconfig: インデント 2 スペース (JS/HTML/CSS) / 4 スペース (Python)。// @ts-check で十分。| 症状 | 原因 | 対処 |
|---|---|---|
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) |
以上を合意したら、P1 (model.js の純計算とテスト) から着手する。