Visualizer: stroke-feature network with spiking readout panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2f210b16f6
commit
1888412f31
4 changed files with 251 additions and 19 deletions
|
|
@ -327,3 +327,10 @@ KNP, validation: `fc_v_plain` 81,0%, `fc_v_augB` 79,5%. Симулятор (2000
|
||||||
- Колонки KNP на выученных признаках (validation): top-384 — 45,7–54,7% (молчат 96–308), top-512 — 22–37% (молчат 459–695); top-256 c40 — 62,2–**64,8%** (t4000 p0,012); top-256 t1000/t2500 p0,02 — 63,0/63,4%; top-128 — 50,9–55,9%; top-64 — 50% (один класс); top-256 c80 — 64,8/**65,1%**; top-128 c80 — 50,0/55,2%. Предел колонок на этих признаках ≈ 65%: плотнее — молчат, реже — теряют информацию.
|
- Колонки KNP на выученных признаках (validation): top-384 — 45,7–54,7% (молчат 96–308), top-512 — 22–37% (молчат 459–695); top-256 c40 — 62,2–**64,8%** (t4000 p0,012); top-256 t1000/t2500 p0,02 — 63,0/63,4%; top-128 — 50,9–55,9%; top-64 — 50% (один класс); top-256 c80 — 64,8/**65,1%**; top-128 c80 — 50,0/55,2%. Предел колонок на этих признаках ≈ 65%: плотнее — молчат, реже — теряют информацию.
|
||||||
- `objects/dopamine_readout.py`: выход «собака − кошка», обучаемый локальным трёхфакторным правилом: на каждой картинке синапс меняется на η·e·(число спайков входа), e = цель − tanh(сумма) — знаковый дофамин; усреднение весов по ходу; 20 эпох; вход нормирован средним числом спайков (постоянное усиление синапса). На точных счётчиках слоя 1 (K=800, 10 шагов): линейно 69,2% (ridge 71,0%).
|
- `objects/dopamine_readout.py`: выход «собака − кошка», обучаемый локальным трёхфакторным правилом: на каждой картинке синапс меняется на η·e·(число спайков входа), e = цель − tanh(сумма) — знаковый дофамин; усреднение весов по ходу; 20 эпох; вход нормирован средним числом спайков (постоянное усиление синапса). На точных счётчиках слоя 1 (K=800, 10 шагов): линейно 69,2% (ridge 71,0%).
|
||||||
- Нейрон-решатель без подбора порога по validation: смещение поднимается на thr/ACTIVE за шаг, порог k × медиана входа (k = 10…200 — результат не зависит), так что «дошёл до порога» = «сумма > 0». Ridge — **71,8%**; дофаминовые веса после обрезки |w| по 99,5-му перцентилю перед целочислением — **69,3–69,6%** (без обрезки 53,9%: редкие входы получают огромные веса, остальные округляются в 0).
|
- Нейрон-решатель без подбора порога по validation: смещение поднимается на thr/ACTIVE за шаг, порог k × медиана входа (k = 10…200 — результат не зависит), так что «дошёл до порога» = «сумма > 0». Ridge — **71,8%**; дофаминовые веса после обрезки |w| по 99,5-му перцентилю перед целочислением — **69,3–69,6%** (без обрезки 53,9%: редкие входы получают огромные веса, остальные округляются в 0).
|
||||||
|
|
||||||
|
### 02:10 – 02:40: новая сеть на странице
|
||||||
|
- `viz/export_data.py --readout <npz>`: в данные страницы добавляются ядра признаков штрихов, веса спайкового выхода и точность на всём тесте (считается тем же кодом, что в экспериментах). Сеть: признаки coarse (масштаб 0,3842), выход обучен на `fcoarseA_val90000` (90 000 = 30 000 × 3 с аугментацией A), порог 16; веса `~/run/exp/readout_coarseA.npz`, для страницы `readout_coarseA_page.npz`.
|
||||||
|
- `viz/app.html`: панель «4 · Сеть штрихов» — 16 карт 7×7, спайки 10 выходных нейронов, ответ; сеть входит в строку «Итог всех сетей» (работают сдвиги и рисование).
|
||||||
|
- Тест MNIST (10 000, симулятор): колонки 92,2%, аугментация B 89,3%, **сеть штрихов 94,7%**.
|
||||||
|
- Проверка JS против Python (QuickJS, установлен в `~/anvenv` через pip): 105/105 одинаковых ответов на картинках галереи, признаки расходятся максимум на 1 из 255 (округление); весь скрипт страницы разбирается без ошибок.
|
||||||
|
- Опубликовано на тот же адрес: https://claude.ai/artifact/Vc1q1WCZuPY2QAHBSGtrKF (версия 2).
|
||||||
|
|
|
||||||
115
viz/app.html
115
viz/app.html
|
|
@ -102,6 +102,15 @@ table.conf td { color: var(--fg); }
|
||||||
.kpi { display: flex; gap: 20px; flex-wrap: wrap; }
|
.kpi { display: flex; gap: 20px; flex-wrap: wrap; }
|
||||||
.kpi div { display: flex; flex-direction: column; gap: 2px; }
|
.kpi div { display: flex; flex-direction: column; gap: 2px; }
|
||||||
.kpi strong { font: 600 22px var(--f-mono); }
|
.kpi strong { font: 600 22px var(--f-mono); }
|
||||||
|
.rgrid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
|
||||||
|
@media (max-width: 760px) { .rgrid { grid-template-columns: 1fr; } }
|
||||||
|
.fmaps { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 4px; max-width: 420px; }
|
||||||
|
.fmaps canvas { width: 100%; aspect-ratio: 1; image-rendering: pixelated; border-radius: 3px; display: block; }
|
||||||
|
.rvotes { display: grid; grid-template-columns: 22px minmax(0, 1fr) 36px; gap: 4px 8px; align-items: center; }
|
||||||
|
.rvotes .dg { font: 600 13px var(--f-mono); text-align: right; }
|
||||||
|
.rvotes i { display: block; height: 12px; background: var(--spike); border-radius: 2px; min-width: 1px; }
|
||||||
|
.rvotes i.win { background: var(--ok); }
|
||||||
|
.rvotes b { font: 400 12px var(--f-mono); color: var(--muted); }
|
||||||
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
@ -173,6 +182,29 @@ table.conf td { color: var(--fg); }
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel stack" id="rbay" hidden>
|
||||||
|
<div class="row" style="justify-content:space-between">
|
||||||
|
<span class="label">4 · Сеть штрихов: детекторы штрихов → пулинг → 10 выходных нейронов</span>
|
||||||
|
<div class="answer">
|
||||||
|
<span class="label">Ответ</span>
|
||||||
|
<span class="big mono" id="ranswer">–</span>
|
||||||
|
<span class="verdict none" id="rverdict">нет ответа</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="note">Другая сеть, не колонки. 16 детекторов (8 наклонов штриха × 2 толщины) смотрят на всю картинку; каждая карта сжимается до 7×7 клеток, где остаётся самый сильный отклик. Эти 784 значения спайкают так же, как пиксели, а 10 выходных нейронов AltAI считают спайки с целыми весами. Веса выхода обучены вне чипа на сдвинутых, повёрнутых и утолщённых копиях MNIST.</p>
|
||||||
|
<div class="rgrid">
|
||||||
|
<div class="stack" style="gap:6px">
|
||||||
|
<span class="label">Карты штрихов, 7×7</span>
|
||||||
|
<div class="fmaps" id="fmaps"></div>
|
||||||
|
</div>
|
||||||
|
<div class="stack" style="gap:6px">
|
||||||
|
<span class="label">Спайки выходных нейронов за 15 шагов</span>
|
||||||
|
<div id="rvotes" class="rvotes"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="note mono" id="racc"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="two">
|
<section class="two">
|
||||||
<div class="panel stack">
|
<div class="panel stack">
|
||||||
<div class="row" style="justify-content:space-between">
|
<div class="row" style="justify-content:space-between">
|
||||||
|
|
@ -336,17 +368,84 @@ function setImage(img, label, note) {
|
||||||
$("srcnote").textContent = note; recompute(); if (!timer) { step = -1; play(); }
|
$("srcnote").textContent = note; recompute(); if (!timer) { step = -1; play(); }
|
||||||
}
|
}
|
||||||
function renderAllNets() {
|
function renderAllNets() {
|
||||||
if (models.length < 2) { $("allnets").textContent = ""; return; }
|
const mark = a => imageLabel === null || a < 0 ? "" : a === imageLabel ? " ✓" : " ✗";
|
||||||
$("allnets").textContent = "Итог всех сетей: " + models.map(m => {
|
const parts = models.map(m => {
|
||||||
const v = (m === model ? trace : simulate(image, m))[STEPS - 1].votes, a = argmax(v);
|
const a = argmax((m === model ? trace : simulate(image, m))[STEPS - 1].votes);
|
||||||
const mark = imageLabel === null || a < 0 ? "" : a === imageLabel ? " ✓" : " ✗";
|
return `${m.name} — ${a < 0 ? "нет ответа" : a}${mark(a)}`;
|
||||||
return `${m.name} — ${a < 0 ? "нет ответа" : a}${mark}`;
|
});
|
||||||
}).join(" · ");
|
if (R) { const a = readoutRun(image).answer; parts.push(`${R.name} — ${a < 0 ? "нет ответа" : a}${mark(a)}`); }
|
||||||
|
$("allnets").textContent = parts.length > 1 ? "Итог всех сетей: " + parts.join(" · ") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- stroke features + spiking readout (mirrors features/stroke_features.py and readout/spiking_readout.py) ---
|
||||||
|
const R = DATA.readout || null;
|
||||||
|
function strokeFeatures(img) {
|
||||||
|
const K = R.kernels, nk = K.length, ks = K[0].length, h = ks >> 1, out = new Uint8Array(nk * 49);
|
||||||
|
const x = Float32Array.from(img, v => Math.fround(v / 255));
|
||||||
|
for (let k = 0; k < nk; k++) {
|
||||||
|
const resp = new Float32Array(PIX);
|
||||||
|
for (let y = 0; y < 28; y++) for (let xx = 0; xx < 28; xx++) {
|
||||||
|
let s = 0;
|
||||||
|
for (let dy = 0; dy < ks; dy++) { const yy = y + dy - h; if (yy < 0 || yy > 27) continue;
|
||||||
|
for (let dx = 0; dx < ks; dx++) { const xs = xx + dx - h; if (xs >= 0 && xs < 28) s += K[k][dy][dx] * x[yy * 28 + xs]; } }
|
||||||
|
resp[y * 28 + xx] = Math.max(s, 0);
|
||||||
|
}
|
||||||
|
// max pooling 5×5, stride 4, padding 1 → 7×7
|
||||||
|
for (let py = 0; py < 7; py++) for (let px = 0; px < 7; px++) {
|
||||||
|
let m = 0;
|
||||||
|
for (let dy = 0; dy < 5; dy++) for (let dx = 0; dx < 5; dx++) {
|
||||||
|
const yy = 4 * py + dy - 1, xs = 4 * px + dx - 1;
|
||||||
|
if (yy >= 0 && yy < 28 && xs >= 0 && xs < 28) m = Math.max(m, resp[yy * 28 + xs]);
|
||||||
|
}
|
||||||
|
out[k * 49 + py * 7 + px] = Math.floor(Math.min(255, m * 255 / R.scale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
function readoutRun(img) {
|
||||||
|
const f = strokeFeatures(img), n = f.length, W = R.w, C = 10;
|
||||||
|
const st = new Float32Array(n), inc = Float32Array.from(f, v => Math.fround(v * Math.fround(1 / 255)));
|
||||||
|
const pot = new Float64Array(C), count = new Int32Array(C), first = new Int32Array(C).fill(STEPS);
|
||||||
|
for (let t = 0; t < STEPS; t++) {
|
||||||
|
if (t < ACTIVE) {
|
||||||
|
for (let c = 0; c < C; c++) pot[c] += W[n][c];
|
||||||
|
for (let i = 0; i < n; i++) { st[i] = Math.fround(st[i] + inc[i]); if (st[i] >= 1) { st[i] = Math.fround(st[i] - 1); for (let c = 0; c < C; c++) pot[c] += W[i][c]; } }
|
||||||
|
}
|
||||||
|
for (let c = 0; c < C; c++) { if (pot[c] >= R.thr) { count[c]++; if (first[c] === STEPS) first[c] = t; pot[c] = 0; } pot[c] = Math.max(pot[c], 0); }
|
||||||
|
}
|
||||||
|
let best = -1, bs = -Infinity;
|
||||||
|
for (let c = 0; c < C; c++) { const s = count[c] * (STEPS + 1) - first[c]; if (s > bs) { bs = s; best = c; } }
|
||||||
|
if (Math.max(...count) === 0) best = -1;
|
||||||
|
return { features: f, count, answer: best };
|
||||||
|
}
|
||||||
|
function renderReadout() {
|
||||||
|
if (!R || !image) return;
|
||||||
|
const r = readoutRun(image), box = $("fmaps"), on = hexRGB(css("--spike")), off = hexRGB(css("--cell-off"));
|
||||||
|
if (!box.children.length) for (let k = 0; k < 16; k++) { const cv = document.createElement("canvas"); cv.width = cv.height = 7; box.append(cv); }
|
||||||
|
[...box.children].forEach((cv, k) => {
|
||||||
|
const c = cv.getContext("2d"), id = c.createImageData(7, 7);
|
||||||
|
for (let i = 0; i < 49; i++) id.data.set([...mix(off, on, r.features[k * 49 + i] / 255), 255], i * 4);
|
||||||
|
c.putImageData(id, 0, 0);
|
||||||
|
cv.title = `${["0°", "22°", "45°", "67°", "90°", "112°", "135°", "157°"][k % 8]}, ${k < 8 ? "тонкий" : "толстый"} штрих`;
|
||||||
|
});
|
||||||
|
const mx = Math.max(1, ...r.count);
|
||||||
|
$("rvotes").innerHTML = [...r.count].map((v, d) => `<span class="dg">${d}</span><i class="${d === r.answer ? "win" : ""}" style="width:${100 * v / mx}%"></i><b>${v}</b>`).join("");
|
||||||
|
$("ranswer").textContent = r.answer < 0 ? "–" : r.answer;
|
||||||
|
const vd = $("rverdict");
|
||||||
|
if (r.answer < 0) { vd.className = "verdict none"; vd.textContent = "нет ответа"; }
|
||||||
|
else if (imageLabel === null) { vd.className = "verdict none"; vd.textContent = "итог"; }
|
||||||
|
else if (r.answer === imageLabel) { vd.className = "verdict ok"; vd.textContent = "верно"; }
|
||||||
|
else { vd.className = "verdict bad"; vd.textContent = `ошибка, это ${imageLabel}`; }
|
||||||
|
}
|
||||||
|
if (R) {
|
||||||
|
$("rbay").hidden = false;
|
||||||
|
$("racc").textContent = `Весь тест MNIST: ${(100 * R.accuracy).toFixed(1).replace(".", ",")}% верных ответов (симулятор, те же веса и правила нейронов, что в экспериментах).`;
|
||||||
}
|
}
|
||||||
function recompute() {
|
function recompute() {
|
||||||
if (!image) return;
|
if (!image) return;
|
||||||
trace = simulate(image, model);
|
trace = simulate(image, model);
|
||||||
renderAllNets();
|
renderAllNets();
|
||||||
|
renderReadout();
|
||||||
firstWinner = -1;
|
firstWinner = -1;
|
||||||
for (const s of trace) { const j = s.fired.indexOf(1); if (j >= 0) { firstWinner = j; break; } }
|
for (const s of trace) { const j = s.fired.indexOf(1); if (j >= 0) { firstWinner = j; break; } }
|
||||||
digit = firstWinner >= 0 ? Math.floor(firstWinner / model.col) : (imageLabel ?? digit);
|
digit = firstWinner >= 0 ? Math.floor(firstWinner / model.col) : (imageLabel ?? digit);
|
||||||
|
|
@ -454,8 +553,8 @@ function renderConf() {
|
||||||
t.innerHTML = h;
|
t.innerHTML = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { renderStep(); renderMaps(); renderConf(); });
|
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { renderStep(); renderMaps(); renderConf(); renderReadout(); });
|
||||||
new MutationObserver(() => { renderStep(); renderMaps(); renderConf(); }).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
new MutationObserver(() => { renderStep(); renderMaps(); renderConf(); renderReadout(); }).observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] });
|
||||||
|
|
||||||
resetDraw(); renderModels(); renderDigits(); renderConf();
|
resetDraw(); renderModels(); renderDigits(); renderConf();
|
||||||
const startIdx = gal.labels.findIndex((l, i) => l === 3);
|
const startIdx = gal.labels.findIndex((l, i) => l === 3);
|
||||||
|
|
|
||||||
117
viz/aug_viz.html
117
viz/aug_viz.html
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,9 @@
|
||||||
"""Builds altai_viz.html: embeds trained weights, sample test images and full-test statistics into app.html.
|
"""Builds altai_viz.html: embeds trained weights, sample test images and full-test statistics into app.html.
|
||||||
|
|
||||||
usage: python3 export_data.py <out.html> <images.bin> <labels.target> "<name>|<dump_dir>|<inhib>" ...
|
usage: python3 export_data.py <out.html> <images.bin> <labels.target> "<name>|<dump_dir>|<inhib>" ... [--readout r.npz]
|
||||||
|
|
||||||
|
--readout: stroke features + spiking readout network (npz with w, thr, kernels, scale from
|
||||||
|
readout_coarseA_page.npz); its test accuracy is computed here with the same code as the experiments.
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
|
@ -19,7 +22,13 @@ def b64(arr, dtype):
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
out, images, labels_path, *specs = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
|
readout = None
|
||||||
|
if "--readout" in args:
|
||||||
|
i = args.index("--readout")
|
||||||
|
readout = args[i + 1]
|
||||||
|
del args[i:i + 2]
|
||||||
|
out, images, labels_path, *specs = args
|
||||||
total = len(open(labels_path).read().split())
|
total = len(open(labels_path).read().split())
|
||||||
imgs, labels = load_images(images, labels_path, total - 10000, 10000)
|
imgs, labels = load_images(images, labels_path, total - 10000, 10000)
|
||||||
|
|
||||||
|
|
@ -50,7 +59,25 @@ def main():
|
||||||
pick += list(rng.choice(fixed, min(GALLERY_HARD, len(fixed)), replace=False))
|
pick += list(rng.choice(fixed, min(GALLERY_HARD, len(fixed)), replace=False))
|
||||||
pick += list(rng.choice(broken, min(GALLERY_HARD // 2, len(broken)), replace=False))
|
pick += list(rng.choice(broken, min(GALLERY_HARD // 2, len(broken)), replace=False))
|
||||||
pick = sorted(set(int(i) for i in pick))
|
pick = sorted(set(int(i) for i in pick))
|
||||||
|
extra = {}
|
||||||
|
if readout:
|
||||||
|
sys.path.insert(0, "/mnt/c/knp/features")
|
||||||
|
sys.path.insert(0, "/mnt/c/knp/readout")
|
||||||
|
import stroke_features as sf
|
||||||
|
import spiking_readout as sr
|
||||||
|
r = np.load(readout)
|
||||||
|
sf.use_preset("coarse")
|
||||||
|
sf.P["scale"] = float(r["scale"])
|
||||||
|
p = sr.spiking_predict(sf.features(imgs.reshape(-1, 28, 28)), r["w"], float(r["thr"]))
|
||||||
|
confusion = np.zeros((CLASSES, CLASSES + 1), dtype=int)
|
||||||
|
for lab, pr in zip(labels, p):
|
||||||
|
confusion[lab, pr if pr >= 0 else CLASSES] += 1
|
||||||
|
extra["readout"] = {"name": "Штрихи + спайковый выход", "kernels": sf.KERNELS.astype(float).round(6).tolist(),
|
||||||
|
"scale": float(r["scale"]), "thr": float(r["thr"]), "w": r["w"].astype(int).tolist(),
|
||||||
|
"confusion": confusion.tolist(), "accuracy": float((p == labels).mean())}
|
||||||
|
print("readout", f"{(p == labels).mean():.4f}")
|
||||||
data = {
|
data = {
|
||||||
|
**extra,
|
||||||
"models": models,
|
"models": models,
|
||||||
"gallery": {"index": pick, "labels": labels[pick].tolist(), "pixels": b64(imgs[pick], np.uint8)},
|
"gallery": {"index": pick, "labels": labels[pick].tolist(), "pixels": b64(imgs[pick], np.uint8)},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue