neuralflow/app.js
2026-09-09 22:40:54 +00:00

1002 lines
37 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================================
NEURALFLOW — Gerçek Zamanlı Sinir Ağı Simülatörü
Sıfır kütüphane · saf Canvas 2D · tüm matematik elle
============================================================================ */
"use strict";
/* ---------- Sabitler / durum ---------- */
const MAX_LOSS_POINTS = 360; // saklanan loss noktası üst sınırı
const PULSE_SPEED = 0.55; // puls ilerleme hızı (birim/sn … 1 birim).
const HEAT_REFRESH_EVERY = 6; // kaç framede bir heatmap yeniden çizilir
const GRADIENT_DECAY = 0.94; // node "gradyan ısısı" sönümü
/* Durum: kullanıcı arayüzünden gelen çizim/eğitim ayarları */
let state = {
trainRunning: false, // RUN modu
activation: 'sigmoid', // sigmoid | relu
learningRate: 0.1,
inputCount: 2,
hiddenNeurons: 4, // her gizli katman için nöron
hiddenLayers: 3, // gizli katman sayısı (1..3)
outputCount: 1,
};
state.layerConfig = [];
state.layerSizes = [];
state.layerLabels = [];
/* Ağ modeli */
let net = {
layers: [], // [{name, size}]
activations: [], // [] her katman aktivasyon dizisi
layerSum: [], // [] aktivasyondan önceki W*a+b (backprop kolaylığı)
weights: [], // [katmanIdx][hedefN][kaynakN] (katmanIdx>=1: giriş katmanı weights içinde yok)
biases: [], // [katmanIdx][hedefN] (katmanIdx>=1)
weightPulses: [], // animasyonlu ağırlık "nabzı": [{li,ri,from,to,…}]
nodeBackHeat: [], // [] her katmanın nöronuna "geri ısısı/alevi" oranı
nodeFwdHeat: [], // [] her katmanın nöronuna "ileri ısısı" (sinyal parlaması)
runningInput: [], // mevcut eğitim örneğinin girdi vektörü
runningTarget: [], // hedef çıktı (girdi katmanında etiket için kullanılabilir)
};
let lossHistory = []; // MSE geçmişi
let samplePointer = 0; // sürekli RUN modunda sıradaki eğitim örneğinin index'i
let animationId = null;
let lastTs = 0;
let frameCount = 0;
let epochCounter = 0;
/* CANVAS ctx'leri */
let neuralCv, neuralCtx;
let heatCv, heatCtx;
let lossCv, lossCtx;
/* Geometri: nöron konumları (her çerçevede modelden türetilir) */
let NODE_X = []; // [katman] x merkezi
let NODE_Y = []; // [katman][] nöron y konumu
/* ---------- DOM yardımcıları ---------- */
function $(id) { return document.getElementById(id); }
/* ============================================================================
AKTİVASYON & TÜREVLERİ (hep elle)
============================================================================ */
function sigmoid(x) {
if (x < -45) return 0;
if (x > 45) return 1;
return 1 / (1 + Math.exp(-x));
}
function sigmoidDerivative(activated) {
// activated katmanın ÇIKIŞ değeri (zaten aktivasyon uygulanmış)
return activated * (1 - activated);
}
function relu(x) { return x > 0 ? x : 0; }
function reluDerivative(x) {
// x = activasyona GİREN net girdi (sum). ReLU türevi.
return x > 0 ? 1 : 0;
}
/* Per-node activation + türev, katman çıktı değeri & düz net girdi üzerinden */
function activateNode(kind, x) {
if (kind === 'relu') return relu(x);
return sigmoid(x);
}
function derivativeOf(kind, output, netInput) {
if (kind === 'relu') return reluDerivative(netInput);
return sigmoidDerivative(output);
}
/* Küçük Gaussian benzeri rastgele ağırlık */
function randomWeight() {
return (Math.random() + Math.random() + Math.random()) / 1.5 - 1; // [-1..1]
}
function randomBias() {
return (Math.random() - 0.5) * 0.8;
}
/* ============================================================================
MİMARİ YÖNETİMİ
============================================================================ */
function layersConfig() {
// state -> katman listesi [{name,size}]
const cfg = [{ name: 'input', size: state.inputCount }];
for (let h = 1; h <= state.hiddenLayers; h++) {
cfg.push({ name: 'hidden' + h, size: state.hiddenNeurons });
}
cfg.push({ name: 'output', size: state.outputCount });
return cfg;
}
function buildNetwork() {
const cfg = layersConfig();
state.layerConfig = cfg;
state.layerSizes = cfg.map(function (l) { return l.size; });
state.layerLabels = cfg.map(function (l) { return l.name; });
net.layers = cfg.slice();
const L = cfg.length;
const acts = []; // katman sayısı kadar dizi
const sums = [];
const nbh = [];
const nfh = [];
for (let i = 0; i < L; i++) {
acts.push(new Array(cfg[i].size).fill(0));
sums.push(new Array(cfg[i].size).fill(0));
nbh.push(new Array(cfg[i].size).fill(0));
nfh.push(new Array(cfg[i].size).fill(0));
}
net.activations = acts;
net.layerSum = sums;
net.nodeBackHeat = nbh;
net.nodeFwdHeat = nfh;
// weights/biases: katman indeks 1..L-1 (çıktı ya da gizli katmanı üreten)
const W = [];
const B = [];
for (let k = 1; k < L; k++) {
const prev = cfg[k - 1].size;
const cur = cfg[k].size;
const m = [];
for (let j = 0; j < cur; j++) {
const row = [];
for (let i = 0; i < prev; i++) row.push(randomWeight());
m.push(row);
}
W.push(m);
const b = [];
for (let j = 0; j < cur; j++) b.push(randomBias());
B.push(b);
}
net.weights = W; // length = L-1
net.biases = B;
net.runningInput = new Array(cfg[0].size).fill(0);
net.runningTarget = new Array(cfg[L - 1].size).fill(0);
// puls & ısı listesini sıfırla
net.weightPulses = [];
}
function resetNetwork() {
buildNetwork();
lossHistory = [];
samplePointer = 0;
epochCounter = 0;
updateArchBars();
updateHUD();
}
/* ============================================================================
GERİ YAYILIM (backpropagation) — matematik elle
katman k için delta: önce çıktı katmanı
δ_out = (a - target) · act'(z)
sonra gizli katmanlar geriye doğru zincir kuralı:
δ_k = (W_{k+1}^T · δ_{k+1}) ⊙ act'(z_k)
Gradyanlar: ∇W_k = δ_k · a_{k-1}^T, ∇b_k = δ_k
============================================================================ */
function backprop(target) {
const L = state.layerSizes.length;
const acts = net.activations;
const sums = net.layerSum;
const delta = [];
for (let k = 0; k < L; k++) delta.push(new Float64Array(state.layerSizes[k]));
// ---- çıktı katmanı hatası ----
const out = acts[L - 1];
const dOut = delta[L - 1];
for (let j = 0; j < out.length; j++) {
const err = out[j] - target[j];
dOut[j] = err * derivativeOf(state.activation, out[j], sums[L - 1][j]);
}
// ---- gizli katmanlar geriye ----
for (let k = L - 2; k >= 1; k--) {
const Wnext = net.weights[k]; // (k)->(k+1)
const dCurr = delta[k];
const dNext = delta[k + 1];
const szNext = state.layerSizes[k + 1];
for (let i = 0; i < state.layerSizes[k]; i++) {
let sumErr = 0;
for (let j = 0; j < szNext; j++) sumErr += Wnext[j][i] * dNext[j];
dCurr[i] = sumErr * derivativeOf(state.activation, acts[k][i], sums[k][i]);
}
}
// nodeBackHeat'i gradyan ile "alevle"
for (let k = 1; k < L; k++) {
const dk = delta[k];
const heat = net.nodeBackHeat[k];
for (let j = 0; j < dk.length; j++) {
const mag = Math.abs(dk[j]);
if (mag > heat[j]) heat[j] = Math.min(1, heat[j] + mag * 3);
}
}
return delta;
}
/* ============================================================================
SGD GÜNCELLEMESİ — w -= lr·∇W , b -= lr·∇b
============================================================================ */
function applySGD(learningRate, delta) {
const L = state.layerSizes.length;
const acts = net.activations;
for (let k = 1; k < L; k++) {
const Wk = net.weights[k - 1];
const Bk = net.biases[k - 1];
const dk = delta[k];
const prev = acts[k - 1];
for (let j = 0; j < Wk.length; j++) {
const row = Wk[j];
for (let i = 0; i < row.length; i++) {
row[i] -= learningRate * dk[j] * prev[i];
}
Bk[j] -= learningRate * dk[j];
}
}
}
/* ============================================================================
EĞİTİM ADIMI
============================================================================ */
function emitForwardSignals() {
// ileri yön sinyal puls'ları: katman k-1 -> k, mor/cyan, aktivasyon & |w| ile orantılı
const L = state.layerSizes.length;
const acts = net.activations;
const pulses = net.weightPulses;
for (let k = 1; k < L; k++) {
const prevY = NODE_Y[k - 1]; if (!prevY) continue;
const curY = NODE_Y[k]; if (!curY) continue;
const Wk = net.weights[k - 1];
const xA = NODE_X[k - 1], xB = NODE_X[k];
for (let j = 0; j < curY.length; j++) {
for (let i = 0; i < prevY.length; i++) {
const a = acts[k - 1][i];
const wgt = Wk[j][i];
const mag = a * Math.abs(wgt);
if (mag < 0.03 || pulses.length > 420) continue;
pulses.push({
li: k - 1, ri: k, i, j,
fromX: xA, fromY: prevY[i],
toX: xB, toY: curY[j],
from: i, to: o2n(k, j),
progress: 0, dir: 'fwd',
speed: 0.5 + Math.random() * 0.4 + mag * 2,
mag: Math.min(1, 0.2 + mag * 1.8),
hueRef: wgt >= 0 ? 285 : 330, // pozitif mor, negatif pembe
});
}
}
}
}
/* ============================================================================
İLERİ YAYILIM (forward pass) — matematik elle
katman katman: z_k = W_k·a_{k-1} + b_k , a_k = act(z_k)
============================================================================ */
function forwardPass(inputArr) {
const L = state.layerSizes.length;
const acts = net.activations;
const sums = net.layerSum;
const kind = state.activation;
// girdi katmanı aktivasyonunu koy
const inSize = state.layerSizes[0];
for (let i = 0; i < inSize; i++) {
acts[0][i] = (i < inputArr.length) ? (inputArr[i] || 0) : 0;
}
for (let k = 1; k < L; k++) {
const curSize = state.layerSizes[k];
const prevSize = state.layerSizes[k - 1];
const Wk = net.weights[k - 1];
const Bk = net.biases[k - 1];
const prevA = acts[k - 1];
const curA = acts[k];
const curS = sums[k];
for (let j = 0; j < curSize; j++) {
let z = Bk[j];
const row = Wk[j];
for (let i = 0; i < prevSize; i++) z += row[i] * prevA[i];
curS[j] = z;
curA[j] = activateNode(kind, z);
}
}
return acts[L - 1];
}
/* Ortalama kare hata (MSE) — elle */
function meanSquaredError(target, output) {
const n = Math.min(target.length, output.length);
let s = 0;
for (let j = 0; j < n; j++) {
const d = output[j] - target[j];
s += d * d;
}
return n ? s / n : 0;
}
/* ============================================================================
EĞİTİM ÖRNEĞİ ÜRETİCİ — deterministik, tekrarlı döngü (öğrenilebilir)
Girdi sabit "desen havuzu"ndan, hedef türetilmiş yumuşak fonksiyondur.
Desen halkası tekrar ettiği için küçük ağlar bile zamanla uymayı öğrenir
→ loss çizgisi gerçekten düşer.
============================================================================ */
function nextSample() {
const nIn = state.inputCount;
const nOut = state.outputCount;
if (!nIn || nIn < 1) { state.inputCount = 2; return nextSample(); }
const i = samplePointer % 32; // kısa halka, tekrarlanır
const t = i / 32;
// 1) giriş deseni: bileşik sinusler → pürüzsüz ama ayırt edici girdi
const f1 = Math.sin(t * Math.PI * 2 * 1.0);
const f2 = Math.sin(t * Math.PI * 2 * 2.0 + 0.9);
const f3 = Math.sin(t * Math.PI * 4.0 + 0.4);
const ph = i * 2.399963; // altın oran tarama
const input = [];
for (let x = 0; x < nIn; x++) {
// her giriş biraz farklı fazlanmış dalga (0..1) + ince parazit
const raw = 0.5 * f1 + 0.28 * f2 * Math.cos(0.9 + x * ph) + 0.22 * f3;
input.push((raw * 0.5 + 0.5)); // [0,1] bandı
}
// 2) çıktı hedefi: girişten deterministik türetilmiş sinyal (0.05..0.95)
const target = [];
for (let y = 0; y < nOut; y++) {
const yk = y + 1;
// bileşik sinüs → sigmoid aralığına sıkıştır, öğrenilebilir ilişki
const sig =
Math.sin(input[0] * Math.PI * (1 + yk * 0.17) + yk * 0.6) * 0.5 +
Math.cos((nIn > 1 ? input[nIn - 1] : input[0]) * Math.PI * (0.5 + yk * 0.23)) * 0.5;
target.push(0.5 + 0.42 * sig); // (0.08, 0.92) → sigmoid çıktısı erişebilsin
}
return { input: input, target: target };
}
function trainStep() {
const sample = nextSample();
samplePointer++;
net.runningInput = sample.input.slice();
net.runningTarget = sample.target.slice();
forwardPass(sample.input);
emitForwardSignals();
const out = net.activations[state.layerSizes.length - 1].slice();
const loss = meanSquaredError(sample.target, out);
lossHistory.push(loss);
if (lossHistory.length > MAX_LOSS_POINTS) lossHistory.shift();
epochCounter++;
const delta = backprop(sample.target);
applySGD(state.learningRate, delta);
generateBackflowVisual(delta, sample.target, out);
updateHUD();
return loss;
}
/* "TRAIN STEP" butonu — tek adım + gradyan patlaması */
function onEpochStep() { trainStep(); }
/* ============================================================================
GERİ AKIŞ GÖRSELİ — gradyan büyüklüğüne göre sağdan sola turuncu alev
============================================================================ */
function generateBackflowVisual(delta, target, output) {
const L = state.layerSizes.length;
const pulses = net.weightPulses;
computeLayerXCache();
for (let k = L - 1; k >= 1; k--) {
const dk = delta[k];
const curY = NODE_Y[k];
const prevY = NODE_Y[k - 1];
const xA = columnXOf(k - 1);
const xB = columnXOf(k);
for (let j = 0; j < dk.length; j++) {
const heat = Math.abs(dk[j]);
if (heat < 0.02) continue;
for (let i = 0; i < prevY.length; i++) {
const w = net.weights[k - 1][j][i] || 0;
const mag = heat * (Math.abs(w) + 0.25);
if (mag < 0.006) continue;
if (pulses.length > 320) break;
pulses.push({
li: k - 1, ri: k,
i: i, j: j,
fromX: xA, fromY: prevY[i],
toX: xB, toY: curY[j],
from: i, to: o2n(k, j),
progress: 0, dir: 'bwd',
speed: 0.45 + Math.random() * 0.5 + mag,
mag: Math.min(1, mag),
hueRef: 20, // alev turuncusu
});
}
}
}
}
/* "to" hedefini node global index gibi kullanacak helper (sadece etiket) */
function o2n(k, j) { return k * 100 + j; }
/* ------------------ katman x konumu yardımcıları ------------------ */
let _layerXCache = [];
function computeLayerXCache() {
const n = state.layerSizes.length;
_layerXCache = [];
for (let k = 0; k < n; k++) _layerXCache.push((k + 0.5) / n);
}
function columnXOf(k) {
if (!_layerXCache.length) computeLayerXCache();
return _layerXCache[k];
}
/* ============================================================================
SİNYAL / PULS ANİMASYONU
============================================================================ */
function setLinkPulse(li, i, j, dir, intensity) {
// :harici denetçiler için hedeflenmiş bir puls başlatma API'si
const k = li + 1;
if (k >= state.layerSizes.length) return;
const prevY = NODE_Y[k - 1];
const curY = NODE_Y[k];
if (!prevY || !curY) return;
net.weightPulses.push({
li, ri: k, i, j,
fromX: columnXOf(k - 1), fromY: prevY[i],
toX: columnXOf(k), toY: curY[j],
from: i, to: o2n(k, j),
progress: 0, dir: dir || 'fwd',
speed: 0.5, mag: Math.min(1, intensity || 0.3),
hueRef: dir === 'bwd' ? 22 : 285,
});
}
function tickSignals(dt) {
// dt saniye
const pulses = net.weightPulses;
const alive = [];
for (let p = 0; p < pulses.length; p++) {
const pl = pulses[p];
pl.progress += dt * pl.speed;
if (pl.progress >= 1) { continue; } // ölü -> atla
alive.push(pl);
}
net.weightPulses = alive;
// kurallı sıfırlayıcı büyüme kısıtı yok
}
function growBackHeat() {
// frame bazlı nodeBackHeat sönümü
for (let k = 0; k < net.nodeBackHeat.length; k++) {
const arr = net.nodeBackHeat[k];
for (let j = 0; j < arr.length; j++) {
if (arr[j] > 0.004) arr[j] *= GRADIENT_DECAY; else arr[j] = 0;
}
}
}
function growFwdHeat() {
for (let k = 0; k < net.nodeFwdHeat.length; k++) {
const arr = net.nodeFwdHeat[k];
for (let j = 0; j < arr.length; j++) {
if (arr[j] > 0.002) arr[j] *= 0.92; else arr[j] = 0;
}
}
}
/* ============================================================================
GEOMETRİ: nöron yerleşimi (canvas gerçek pikselinden bağımsız normalized 0..1)
============================================================================ */
function computeGeometry(width, height) {
const L = state.layerSizes.length;
NODE_X = [];
NODE_Y = [];
// padding
const padX = Math.min(46, width * 0.06);
const padY = Math.min(26, height * 0.07);
const drawW = Math.max(10, width - padX * 2);
const drawH = Math.max(10, height - padY * 2);
for (let k = 0; k < L; k++) {
const x = padX + drawW * (L === 1 ? 0.5 : k / (L - 1));
NODE_X.push(x);
const size = state.layerSizes[k];
const col = [];
if (size === 1) col.push(height / 2);
else for (let j = 0; j < size; j++) col.push(padY + drawH * (j / (size - 1)));
NODE_Y.push(col);
}
computeLayerXCache();
}
/* ============================================================================
ÇİZİM: ORTA (NÖRON AKIŞI) KANVASI
============================================================================ */
function drawNeuralCanvas() {
const cv = neuralCv;
if (!cv) return;
const w = cv.width, h = cv.height;
const g = neuralCtx;
g.clearRect(0, 0, w, h);
// zaman birikimi (yumuşak nabız/akış efektleri için)
const tSec = frameCount * 0.016;
// hafif ızgara arka plan deseni — çok hafif "nefes alan" canlılık
const breathe = 0.5 + 0.5 * Math.sin(tSec * 0.7);
g.strokeStyle = 'rgba(90,120,220,' + (0.04 + breathe * 0.03) + ')';
g.lineWidth = 1;
const step = 36;
for (let x = 0; x < w; x += step) { g.beginPath(); g.moveTo(x, 0); g.lineTo(x, h); g.stroke(); }
for (let y = 0; y < h; y += step) { g.beginPath(); g.moveTo(0, y); g.lineTo(w, y); g.stroke(); }
// süzülen "veri tozları" — yavaşça sağa süzülen neon parçacık sürüsü
for (let d = 0; d < 14; d++) {
const drift = ((d * 0.6180339887) + tSec * 0.045) % 1; // her biri farklı fazda akar
const wave = Math.sin(tSec * 0.3 + d * 1.7) * 10;
const dy = (((d * 53.7 + wave) % h) + h) % h;
const dx = drift * w;
const pal = d % 3;
const cc = pal === 0 ? '55,224,255' : pal === 1 ? '143,123,255' : '255,93,158';
const tw = 0.5 + 0.5 * Math.sin(tSec * 2.2 + d * 2.9); // hafif yanıp sönme
g.fillStyle = 'rgba(' + cc + ',' + (0.10 + tw * 0.12) + ')';
g.beginPath();
g.arc(dx, dy, 1.0 + (pal === 2 ? 0.9 : 0.3), 0, Math.PI * 2);
g.fill();
}
const L = state.layerSizes.length;
const acts = net.activations;
// ---- bağlantılar ----
for (let k = 1; k < L; k++) {
const prevY = NODE_Y[k - 1];
const curY = NODE_Y[k];
const Wk = net.weights[k - 1];
const px = NODE_X[k - 1];
const cx = NODE_X[k];
for (let j = 0; j < curY.length; j++) {
for (let i = 0; i < prevY.length; i++) {
const wgt = Wk[j][i];
const a = Math.abs(wgt);
const pos = wgt >= 0;
const alpha = 0.16 + a * 0.7;
g.strokeStyle = pos ? 'rgba(90,200,255,' + alpha + ')'
: 'rgba(255,90,110,' + alpha + ')';
g.lineWidth = 0.6 + a * 3.2;
const y1 = prevY[i], y2 = curY[j];
// hafif kavis
const mx = (px + cx) / 2;
const ctl = mx + (y2 - y1) * 0.4;
const ctlY = (y1 + y2) / 2;
g.beginPath();
g.moveTo(px, y1);
g.quadraticCurveTo(ctl, ctlY, cx, y2);
g.stroke();
// güçlü bağlantıların üstüne "akan sinyal dalgası" (canlılık)
if (a > 0.35) {
const cycle = (tSec * 0.9 + k * 0.6 + ((i + j) % 5) * 0.24) % 1;
if (cycle < 0.75) { // dalga kuyruğu kesilmesin diye sadece bir aralıkta göster
const sc = cycle / 0.75;
const qx = (1 - sc) * (1 - sc) * px + 2 * (1 - sc) * sc * ctl + sc * sc * cx;
const qy = (1 - sc) * (1 - sc) * y1 + 2 * (1 - sc) * sc * ctlY + sc * sc * y2;
const fade = (1 - sc) * alpha;
const wcol = pos ? '90,235,255' : '255,150,120';
const wg = g.createRadialGradient(qx, qy, 0, qx, qy, 6);
wg.addColorStop(0, 'rgba(' + wcol + ',' + (0.5 * fade + 0.2) + ')');
wg.addColorStop(1, 'rgba(' + wcol + ',0)');
g.fillStyle = wg;
g.beginPath(); g.arc(qx, qy, 6, 0, Math.PI * 2); g.fill();
}
}
}
}
}
// ---- puls topları (kaplamalı, renkli parlama) ----
const pulses = net.weightPulses;
for (let p = 0; p < pulses.length; p++) {
const pl = pulses[p];
// normalize konumları gerçek piksele çevir
let x1, y1, x2, y2;
const xCache = _layerXCache;
const li = pl.li, ri = pl.ri;
if (li < 0 || ri >= L) continue;
const px = NODE_X[li], cx = NODE_X[ri];
// puls kaynağı/hedefi nöron Y'lerinden (gerçek piksel)
const srcY = NODE_Y[li][pl.from];
const dstY = NODE_Y[ri][pl.to % 100];
// basit interpolasyon (progress) — kavis ihmal
const t = pl.progress;
const X = px + (cx - px) * t;
const Y = srcY + (dstY - srcY) * t;
let col;
if (pl.dir === 'bwd') {
col = 'hsla(' + pl.hueRef + ',100%,60%,' + (0.5 + pl.mag * 0.5) + ')';
} else {
col = 'hsla(' + pl.hueRef + ',100%,66%,' + (0.35 + pl.mag * 0.5) + ')';
}
const rad = 2 + pl.mag * 6;
const glow = g.createRadialGradient(X, Y, 0, X, Y, rad * 5);
glow.addColorStop(0, col);
glow.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = glow;
g.beginPath();
g.arc(X, Y, rad * 5, 0, Math.PI * 2);
g.fill();
g.fillStyle = col;
g.beginPath();
g.arc(X, Y, rad, 0, Math.PI * 2);
g.fill();
}
// ---- nöronlar (aktivasyona göre parlaklık) ----
for (let k = 0; k < L; k++) {
const x = NODE_X[k];
const arr = NODE_Y[k];
const fwd = net.nodeFwdHeat[k] || [];
const bck = net.nodeBackHeat[k] || [];
// katman etiketi
g.fillStyle = 'rgba(140,170,230,0.5)';
g.font = '11px Consolas, monospace';
g.textAlign = 'center';
const label = state.layerLabels[k] || '';
const isIn = k === 0, isOut = k === L - 1;
g.fillText((isIn ? 'GİRİŞ' : isOut ? 'ÇIKIŞ' : label.toUpperCase()), x, 16);
for (let j = 0; j < arr.length; j++) {
const r = isIn || isOut ? 11 : 8;
const a = acts[k][j]; // aktivasyon 0..1
const glow = fwd[j] || 0;
const back = bck[j] || 0;
// dış parlama
const coreLum = 0.5 + a * 0.9;
let rgb;
if (back > 0.05) {
const t = Math.min(1, back);
rgb = 'rgb(' + Math.round(255 * (0.5 + t * 0.5)) + ',' + Math.round(120 * (1 - t)) + ',' + Math.round(80 * (1 - t)) + ')';
} else {
const bl = Math.round(90 + coreLum * 210);
const gr = Math.round(60 + a * 200);
rgb = 'rgb(' + Math.round(50 + a * 80) + ',' + gr + ',' + bl + ')';
}
// zaman bazlı "canlı nabız": aktif nöron hafifçe büyür / parlar
const pulseK = 1 + (a * 0.22) * (0.5 + 0.5 * Math.sin(tSec * 3.4 + k * 1.3 + j * 0.7));
const glowR = r * 4 * (0.92 + pulseK * 0.08);
const galpha = back > 0.05 ? (0.45 + back * 0.45) : (0.32 + a * 0.5);
const radG = g.createRadialGradient(x, arr[j], 0, x, arr[j], glowR);
radG.addColorStop(0, back > 0.05 ? 'rgba(255,120,40,' + galpha + ')' : 'rgba(90,220,255,' + galpha + ')');
radG.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = radG;
g.beginPath(); g.arc(x, arr[j], glowR, 0, Math.PI * 2); g.fill();
// çekirdek (canlı parlama katmanı + parlayan ana gövde)
const coreR = r * (0.96 * pulseK);
const coreGlow = g.createRadialGradient(x, arr[j], 0, x, arr[j], coreR * 2.5);
coreGlow.addColorStop(0, 'rgba(255,255,255,' + (0.5 + a * 0.4) + ')');
coreGlow.addColorStop(0.5, back > 0.05 ? 'rgba(255,180,90,' + (0.5 + a * 0.3) + ')' : 'rgba(150,240,255,' + (0.45 + a * 0.4) + ')');
coreGlow.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = coreGlow;
g.beginPath(); g.arc(x, arr[j], coreR * 2.5, 0, Math.PI * 2); g.fill();
const grad = g.createRadialGradient(x - 2, arr[j] - 2, 1, x, arr[j], coreR);
grad.addColorStop(0, '#ffffff');
grad.addColorStop(0.4, back > 0.05 ? '#ffc880' : 'rgb(150,240,255)');
grad.addColorStop(1, 'rgba(25,55,115,0.95)');
g.fillStyle = grad;
g.beginPath(); g.arc(x, arr[j], coreR, 0, Math.PI * 2); g.fill();
// çekirdek dış ince halka (neon kontur)
g.strokeStyle = back > 0.05 ? 'rgba(255,180,90,' + (0.5 + back * 0.5) + ')' : 'rgba(140,240,255,' + (0.4 + a * 0.5) + ')';
g.lineWidth = 1 + a * 1.2;
g.beginPath(); g.arc(x, arr[j], coreR - 0.6, 0, Math.PI * 2); g.stroke();
// back ısı parlaması en dış halka
if (back > 0.05) {
g.strokeStyle = 'rgba(255,150,40,' + (0.3 + back * 0.6) + ')';
g.lineWidth = 1.5 + back * 2;
g.beginPath(); g.arc(x, arr[j], r + 3 + back * 8, 0, Math.PI * 2); g.stroke();
}
}
}
}
/* ============================================================================
ÇİZİM: SAĞ PANEL — AĞIRLIK ISILOĞ / GÖRSEL MATRİS
her bağlantı katmanı (k=1..L-1) için mini grid hücre
hücre boyutu pencere bağımlı; tüm katmanlar tek sütunda, üstte en girişçi
============================================================================ */
function drawHeatmap() {
const cv = heatCv; if (!cv) return;
const w = cv.width, h = cv.height;
const g = heatCtx;
g.clearRect(0, 0, w, h);
g.fillStyle = 'rgba(8,12,24,0.5)'; g.fillRect(0, 0, w, h);
const L = state.layerSizes.length;
if (L < 2) return;
// her layer-k matrisi: rows=cur(data) x cols=prev
// bir "matris şeridi"ne yerleştir, üstten alta
let yCursor = 12;
const cellPad = 3;
const labelH = 14;
for (let k = 1; k < L; k++) {
const cur = state.layerSizes[k];
const prevC = state.layerSizes[k - 1];
const mat = net.weights[k - 1];
// toplam matris alanı; max genişlik w-kaydol => cell
const padX = 4;
const availH = Math.max(20, Math.min(cur * (h - labelH) / (L - 1), h / 4.4) - cellPad * cur);
const availW = w - padX * 2;
const cw = Math.min(availW / Math.max(1, prevC), 22);
const ch = Math.min(availH / Math.max(1, cur), 22);
const cell = Math.min(cw, ch, 26);
const gridW = cell * prevC - cellPad * (prevC - 1);
const gridH = cell * cur - cellPad * (cur - 1);
let x0 = (w - gridW) / 2;
let y0 = yCursor + labelH;
// katman etiketi: "k: cur ← prev"
g.fillStyle = 'rgba(150,180,240,0.5)';
g.font = '9px Consolas, monospace';
g.textAlign = 'left';
const lab = state.layerLabels[k] + ' ' + cur + '←' + prevC;
g.fillText(lab.toUpperCase(), x0, yCursor + 10);
for (let j = 0; j < cur; j++) { // satır = hedef nöron
for (let i = 0; i < prevC; i++) { // kolon = kaynak nöron
const val = mat[j][i]; // -1..1
const a = Math.abs(val);
let col;
if (val >= 0) col = 'rgba(45,175,255,' + (0.15 + a * 0.85) + ')';
else col = 'rgba(255,80,100,' + (0.15 + a * 0.85) + ')';
g.fillStyle = col;
const px = x0 + i * (cell + cellPad);
const py = y0 + j * (cell + cellPad);
const rr = cell * 0.16;
roundRect(g, px, py, cell, cell, rr);
g.fill();
}
}
yCursor = y0 + gridH + 14;
}
}
function roundRect(g, x, y, w2, h2, r) {
g.beginPath();
g.moveTo(x + r, y);
g.arcTo(x + w2, y, x + w2, y + h2, r);
g.arcTo(x + w2, y + h2, x, y + h2, r);
g.arcTo(x, y + h2, x, y, r);
g.arcTo(x, y, x + w2, y, r);
g.closePath();
}
/* ============================================================================
ÇİZİM: ALT — LOSS GRAFİĞİ (neon eğri, kaydırmalı)
============================================================================ */
function plotLoss() {
const cv = lossCv; if (!cv) return;
const w = cv.width, h = cv.height;
const g = lossCtx;
g.clearRect(0, 0, w, h);
g.fillStyle = 'rgba(8,12,24,0.5)'; g.fillRect(0, 0, w, h);
g.strokeStyle = 'rgba(120,150,220,0.12)';
for (let gy = 0; gy < h; gy += h / 4) { g.beginPath(); g.moveTo(0, gy); g.lineTo(w, gy); g.stroke(); }
if (lossHistory.length < 2) return;
const maxP = Math.min(MAX_LOSS_POINTS, lossHistory.length);
// Y ölçek: dönemsel max (ilk 20 dahil)
let lo = Infinity, hi = -Infinity;
const from = Math.max(0, lossHistory.length - 260);
for (let i = from; i < lossHistory.length; i++) { const v = lossHistory[i]; if (v < lo) lo = v; if (v > hi) hi = v; }
if (!isFinite(hi) || hi - lo < 1e-6) { hi = lo + 1; }
lo = Math.max(0, lo - (hi - lo) * 0.1);
hi = hi + (hi - lo) * 0.1;
const data = lossHistory.slice(-maxP);
// neon çizgi + alan
const stepX = w / data.length;
const py = function (v) { return h - 8 - ((v - lo) / (hi - lo || 1)) * (h - 16); };
// alan dolgusu
const grad = g.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, 'rgba(255,77,158,0.28)');
grad.addColorStop(1, 'rgba(255,77,158,0)');
g.beginPath();
g.moveTo(0, py(data[0]));
for (let i = 0; i < data.length; i++) g.lineTo(i * stepX, py(data[i]));
g.lineTo(w, h); g.lineTo(0, h); g.closePath();
g.fillStyle = grad; g.fill();
// üst parlayan çizgi
g.beginPath();
g.moveTo(0, py(data[0]));
for (let i = 0; i < data.length; i++) g.lineTo(i * stepX, py(data[i]));
g.strokeStyle = 'rgba(255,110,190,0.15)';
g.lineWidth = 6; g.stroke();
g.strokeStyle = '#ff6eae';
g.lineWidth = 1.6;
g.shadowColor = '#ff6eae'; g.shadowBlur = 8;
g.stroke();
g.shadowBlur = 0;
// mevcut değer noktası
const lv = data[data.length - 1];
g.fillStyle = '#fff';
g.beginPath();
g.arc(w - stepX, py(lv), 2.4, 0, Math.PI * 2);
g.fill();
}
/* ============================================================================
CANVAS BOYUTLANDIRMA + HUD + ARK MİMARİ ÇUBUKLAR
============================================================================ */
function resizeCanvases() {
if (!neuralCv || !heatCv || !lossCv) return;
const dpr = window.devicePixelRatio || 1;
for (const cv of [neuralCv, heatCv, lossCv]) {
const r = cv.getBoundingClientRect();
cv.width = Math.max(20, Math.round(r.width * dpr));
cv.height = Math.max(20, Math.round(r.height * dpr));
cv.getContext('2d').setTransform(dpr, 0, 0, dpr, 0, 0);
// not: yüksek-dpi'da piksel koordinat pikselleştirme: logikal piksel kullanıyoruz
// dpr ile CSS'e oranlı çizeceğiz: transform dpr yaptığımız için coordinates logikal kalıyor.
}
computeGeometry(neuralCv.clientWidth || neuralCv.parentElement.clientWidth, neuralCv.clientHeight || neuralCv.parentElement.clientHeight);
}
function updateHUD() {
const e = $('epochReadout'); if (e) e.textContent = epochCounter;
const lo = $('lossReadout');
if (lo) lo.textContent = lossHistory.length ? lossHistory[lossHistory.length - 1].toFixed(4) : '—';
const ae = $('actReadout'); if (ae) ae.textContent = state.activation;
}
function updateArchBars() {
const holder = $('archBars'); if (!holder) return;
holder.innerHTML = '';
const sizes = state.layerSizes;
const maxS = Math.max.apply(null, sizes);
for (let k = 0; k < sizes.length; k++) {
const b = document.createElement('div');
b.className = 'arch-bar';
const scale = sizes[k] / maxS;
b.style.height = (16 + scale * 26) + 'px';
b.textContent = sizes[k];
if (k === 0) b.style.background = 'linear-gradient(180deg,#6cf,#38e)';
else if (k === sizes.length - 1) b.style.background = 'linear-gradient(180deg,#ff7,#ff4d6d)';
holder.appendChild(b);
}
}
/* ============================================================================
ANA ANİMASYON DÖNGÜSÜ (requestAnimationFrame)
============================================================================ */
function requestAnimationFrameSim(ts) {
animationId = requestAnimationFrame(requestAnimationFrameSim);
const dt = lastTs ? Math.min((ts - lastTs) / 1000, 0.05) : 0.016;
lastTs = ts;
tickSignals(dt);
growBackHeat();
growFwdHeat();
tickAutoTrain();
// çizim
drawNeuralCanvas();
frameCount++;
if (frameCount % HEAT_REFRESH_EVERY === 0) drawHeatmap();
plotLoss();
updateHUD();
}
function startSim() {
if (animationId) return;
lastTs = 0;
animationId = requestAnimationFrame(requestAnimationFrameSim);
}
function stopSim() { if (animationId) { cancelAnimationFrame(animationId); animationId = null; } }
/* ============================================================================
KONTROL / UI BAĞLAMA
============================================================================ */
function applyActivation(choice) {
state.activation = (choice === 'relu') ? 'relu' : 'sigmoid';
updateActivationSeg();
}
function updateActivationSeg() {
const seg = $('activationSeg'); if (!seg) return;
const btns = seg.querySelectorAll('.seg-btn');
btns.forEach(function (b) {
b.classList.toggle('active', b.dataset.act === state.activation);
});
}
function onSliderChange() {
state.inputCount = intVal($('inputSlider'), 2);
state.hiddenNeurons = intVal($('hiddenSlider'), 4);
state.hiddenLayers = intVal($('hiddenCountSlider'), 3);
state.outputCount = intVal($('outputSlider'), 1);
state.learningRate = (intVal($('lrSlider'), 10)) / 100;
const io = $('inputOut'); if (io) io.textContent = state.inputCount;
const ho = $('hiddenOut'); if (ho) ho.textContent = state.hiddenNeurons;
const hco = $('hiddenCountOut'); if (hco) hco.textContent = state.hiddenLayers;
const oo = $('outputOut'); if (oo) oo.textContent = state.outputCount;
const lo2 = $('lrOut'); if (lo2) lo2.textContent = state.learningRate.toFixed(2);
buildNetwork();
updateArchBars();
updateHUD();
}
function intVal(el, d) {
if (!el) return d;
const v = parseInt(el.value, 10);
return isNaN(v) ? d : v;
}
/* RUN/PAUSE mantığı + otomatik forward-pass denemesi her frame */
function toggleRun() {
state.trainRunning = !state.trainRunning;
const b = $('runBtn'); if (b) b.classList.toggle('running', state.trainRunning);
}
function tickAutoTrain() {
// startSim üzerindeki ana döngüde çağrılır — sürekli RUN modunda her frame trainStep
if (!state.trainRunning) return;
if (frameCount % 4 === 0) trainStep();
}
/* ============================================================================
BAŞLATMA / DOM
============================================================================ */
function bindUI() {
const sliders = ['inputSlider', 'hiddenSlider', 'hiddenCountSlider', 'outputSlider', 'lrSlider'];
sliders.forEach(function (s) {
const el = $(s); if (el) el.addEventListener('input', onSliderChange);
});
const runBtn = $('runBtn');
if (runBtn) runBtn.addEventListener('click', toggleRun);
const stepBtn = $('stepBtn');
if (stepBtn) stepBtn.addEventListener('click', onEpochStep);
const resetBtn = $('resetBtn');
if (resetBtn) resetBtn.addEventListener('click', resetNetwork);
const seg = $('activationSeg');
if (seg) seg.addEventListener('click', function (ev) {
const t = ev.target.closest('.seg-btn');
if (t && t.dataset.act) applyActivation(t.dataset.act);
});
window.addEventListener('resize', function () {
resizeCanvases();
drawHeatmap();
plotLoss();
});
}
function init() {
neuralCv = $('neuralCanvas');
heatCv = $('heatCanvas');
lossCv = $('lossCanvas');
if (neuralCv) neuralCtx = neuralCv.getContext('2d');
if (heatCv) heatCtx = heatCv.getContext('2d');
if (lossCv) lossCtx = lossCv.getContext('2d');
onSliderChange(); // layerSizes + ilk buildNetwork
buildNetwork();
bindUI();
resizeCanvases();
updateActivationSeg();
updateHUD();
updateArchBars();
drawHeatmap();
plotLoss();
startSim();
}
document.addEventListener('DOMContentLoaded', init);