Files
2026-10-27 00:14:30 +00:00

590 lines
21 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.
/* Neural Lab — gerçek bir MLP'nin forward pass + backpropagation ile eğitimi.
Saf JS, bağımlılık yok. Öğrenme tamamen istemci tarafında çalışır. */
(function () {
"use strict";
// ---------------- Yardımcılar ----------------
function randn() { // Box-Muller
var u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
function clamp(v, a, b) { return v < a ? a : (v > b ? b : v); }
// ---------------- Aktif fonksiyonlar ----------------
// Her biri: f(x), türev f'(x) (çıktıya değil, girdi x'e göre)
var ACT = {
sigmoid: {
f: function (x) { return 1 / (1 + Math.exp(-clamp(x, -40, 40))); },
df: function (x) { var s = ACT.sigmoid.f(x); return s * (1 - s); }
},
tanh: {
f: function (x) { return Math.tanh(x); },
df: function (x) { var t = Math.tanh(x); return 1 - t * t; }
},
relu: {
f: function (x) { return x > 0 ? x : 0; },
df: function (x) { return x > 0 ? 1 : 0; }
}
};
// ---------------- Veri Setleri ----------------
// Her örnek: {x:[x1,x2], y:0|1}. Koordinatlar [-1,1] aralığında.
var DATA = {
xor: function (n) {
var pts = [];
for (var i = 0; i < n; i++) {
var x1 = Math.random() * 2 - 1;
var x2 = Math.random() * 2 - 1;
// küçük gürültü: sınırda biraz karışıklık
var g = (Math.random() - 0.5) * 0.12;
var y = ((x1 * x2) + g) >= 0 ? 1 : 0;
pts.push({ x: [x1, x2], y: y });
}
return pts;
},
circle: function (n) {
var pts = [];
for (var i = 0; i < n; i++) {
var x1 = Math.random() * 2 - 1;
var x2 = Math.random() * 2 - 1;
var r = Math.sqrt(x1 * x1 + x2 * x2);
var g = (Math.random() - 0.5) * 0.1;
var y = (r + g) < 0.62 ? 1 : 0;
pts.push({ x: [x1, x2], y: y });
}
return pts;
},
moons: function (n) {
var pts = [], half = Math.floor(n / 2);
for (var i = 0; i < half; i++) { // üst hilal
var t = Math.PI * Math.random();
var x1 = Math.cos(t) * 0.75;
var x2 = Math.sin(t) * 0.75 - 0.15;
pts.push({ x: [clamp(x1 + (Math.random() - 0.5) * 0.14, -1, 1),
clamp(x2 + (Math.random() - 0.5) * 0.14, -1, 1)], y: 0 });
}
for (var j = 0; j < n - half; j++) { // alt hilal (kaydırılmış)
var t2 = Math.PI * Math.random();
var y1 = Math.cos(t2) * 0.75 + 0.25;
var y2 = Math.sin(t2) * 0.75 + 0.15;
pts.push({ x: [clamp(y1 + (Math.random() - 0.5) * 0.14, -1, 1),
clamp(-y2 + (Math.random() - 0.5) * 0.14, -1, 1)], y: 1 });
}
return pts;
}
};
// ---------------- Yapay Sinir Ağı (MLP) ----------------
// Katman yapısı örn: [2, 8, 1] veya [2, 8, 8, 1]
function MLP(sizes, actName, optName, lr) {
this.sizes = sizes.slice();
this.actName = actName;
this.act = ACT[actName];
this.opt = optName;
this.lr = lr;
this.W = []; // her katman için ağırlık matrisi [out][in]
this.b = []; // bias vektörü [out]
this.vW = []; this.vb = []; // momentum hızları
this.init();
}
MLP.prototype.init = function () {
this.W = []; this.b = []; this.vW = []; this.vb = [];
for (var l = 0; l < this.sizes.length - 1; l++) {
var nin = this.sizes[l], nout = this.sizes[l + 1];
var Wl = [], bl = [], vwl = [], vbl = [];
// He init (ReLU) / Xavier (diğerleri)
var scale = this.actName === "relu" ? Math.sqrt(2 / nin) : Math.sqrt(1 / nin);
for (var o = 0; o < nout; o++) {
var row = [], vrow = [];
for (var i = 0; i < nin; i++) { row.push(randn() * scale); vrow.push(0); }
Wl.push(row); vwl.push(vrow);
bl.push(0); vbl.push(0);
}
this.W.push(Wl); this.b.push(bl);
this.vW.push(vwl); this.vb.push(vbl);
}
};
// Forward pass: her katmanın z (ön-aktivasyon) ve a (aktivasyon) değerlerini saklar
MLP.prototype.forward = function (x) {
var acts = [x.slice()]; // a[0] = girdi
var zs = []; // z[l] = W[l]·a[l]+b
var a = x;
for (var l = 0; l < this.W.length; l++) {
var W = this.W[l], b = this.b[l], nout = W.length;
var z = new Array(nout), out = new Array(nout);
var isLast = (l === this.W.length - 1);
for (var o = 0; o < nout; o++) {
var s = b[o], Wo = W[o];
for (var i = 0; i < a.length; i++) s += Wo[i] * a[i];
z[o] = s;
out[o] = isLast ? ACT.sigmoid.f(s) : this.act.f(s);
}
zs.push(z); acts.push(out);
a = out;
}
return { acts: acts, zs: zs, yhat: a[0] };
};
// Tek örnekte backpropagation: gradyanları döndürür
MLP.prototype.backprop = function (x, y) {
var fwd = this.forward(x);
var acts = fwd.acts, zs = fwd.zs;
var L = this.W.length;
var deltas = new Array(L); // her katman için delta vektörü
// Çıkış katmanı: BCE + sigmoid → delta = (yhat - y)
var yhat = acts[L][0];
deltas[L - 1] = [yhat - y];
// Geriye yayılım
for (var l = L - 2; l >= 0; l--) {
var Wnext = this.W[l + 1], dnext = deltas[l + 1];
var nin = this.sizes[l + 1];
var d = new Array(nin);
for (var i = 0; i < nin; i++) {
var sum = 0;
for (var o = 0; o < dnext.length; o++) sum += Wnext[o][i] * dnext[o];
d[i] = sum * this.act.df(zs[l][i]);
}
deltas[l] = d;
}
// Gradyanları hesapla
var gW = [], gb = [];
for (var l2 = 0; l2 < L; l2++) {
var aPrev = acts[l2], d2 = deltas[l2];
var gw = [], gbias = [];
for (var o2 = 0; o2 < this.W[l2].length; o2++) {
var grow = [];
for (var i2 = 0; i2 < aPrev.length; i2++) grow.push(d2[o2] * aPrev[i2]);
gw.push(grow);
gbias.push(d2[o2]);
}
gW.push(gw); gb.push(gbias);
}
return { gW: gW, gb: gb, loss: -Math.log(clamp(y === 1 ? yhat : 1 - yhat, 1e-9, 1)) };
};
// Bir epoch: tüm veri üzerinde gradyanları topla, ortala, uygula
MLP.prototype.trainEpoch = function (data) {
var L = this.W.length;
var sumGW = [], sumGB = [];
for (var l = 0; l < L; l++) {
var gw = [], gb = [];
for (var o = 0; o < this.W[l].length; o++) {
var row = [];
for (var i = 0; i < this.W[l][o].length; i++) row.push(0);
gw.push(row); gb.push(0);
}
sumGW.push(gw); sumGB.push(gb);
}
var totalLoss = 0, correct = 0;
for (var n = 0; n < data.length; n++) {
var r = this.backprop(data[n].x, data[n].y);
totalLoss += r.loss;
var yhat = this.forward(data[n].x).yhat;
if ((yhat >= 0.5 ? 1 : 0) === data[n].y) correct++;
for (var l2 = 0; l2 < L; l2++) {
for (var o2 = 0; o2 < sumGW[l2].length; o2++) {
for (var i2 = 0; i2 < sumGW[l2][o2].length; i2++) sumGW[l2][o2][i2] += r.gW[l2][o2][i2];
sumGB[l2][o2] += r.gb[l2][o2];
}
}
}
var m = data.length;
// Gradyan inişi + (opsiyonel) momentum
var mu = this.opt === "momentum" ? 0.9 : 0;
for (var l3 = 0; l3 < L; l3++) {
for (var o3 = 0; o3 < this.W[l3].length; o3++) {
var gbo = sumGB[l3][o3] / m;
if (mu > 0) {
this.vb[l3][o3] = mu * this.vb[l3][o3] - this.lr * gbo;
this.b[l3][o3] += this.vb[l3][o3];
} else {
this.b[l3][o3] -= this.lr * gbo;
}
for (var i3 = 0; i3 < this.W[l3][o3].length; i3++) {
var g = sumGW[l3][o3][i3] / m;
if (mu > 0) {
this.vW[l3][o3][i3] = mu * this.vW[l3][o3][i3] - this.lr * g;
this.W[l3][o3][i3] += this.vW[l3][o3][i3];
} else {
this.W[l3][o3][i3] -= this.lr * g;
}
}
}
}
return { loss: totalLoss / m, acc: correct / m };
};
// Tek örnek tahmini (karar sınırı için)
MLP.prototype.predict = function (x) { return this.forward(x).yhat; };
// Sayısal gradyan kontrolü (backprop doğruluğu kanıtı)
MLP.prototype.gradCheck = function (x, y) {
var eps = 1e-5, maxRel = 0;
var r = this.backprop(x, y);
// rastgele birkaç ağırlığı kontrol et
var checks = 0;
for (var l = 0; l < this.W.length && checks < 6; l++) {
for (var o = 0; o < this.W[l].length && checks < 6; o++) {
var i = Math.floor(Math.random() * this.W[l][o].length);
var orig = this.W[l][o][i];
this.W[l][o][i] = orig + eps;
var lp = -Math.log(clamp(y === 1 ? this.forward(x).yhat : 1 - this.forward(x).yhat, 1e-9, 1));
this.W[l][o][i] = orig - eps;
var lm = -Math.log(clamp(y === 1 ? this.forward(x).yhat : 1 - this.forward(x).yhat, 1e-9, 1));
this.W[l][o][i] = orig;
var num = (lp - lm) / (2 * eps);
var ana = r.gW[l][o][i];
var denom = Math.max(1e-7, Math.abs(num) + Math.abs(ana));
maxRel = Math.max(maxRel, Math.abs(num - ana) / denom);
checks++;
}
}
return maxRel; // ~1e-6 civarı ise backprop doğru
};
// ---------------- Uygulama Durumu ----------------
var ST = {
dataType: "xor",
actName: "sigmoid",
hidden: 8,
lr: 0.30,
layers: 1,
opt: "sgd",
net: null,
data: [],
epoch: 0,
losshist: [],
acchist: [],
running: false,
speed: 1,
lastLoss: 0,
lastAcc: 0,
grads: null // son backprop gradyanları (görselleştirme için)
};
var N_POINTS = 120;
function buildNet() {
var sizes = [2];
for (var i = 0; i < ST.layers; i++) sizes.push(ST.hidden);
sizes.push(1);
ST.net = new MLP(sizes, ST.actName, ST.opt, ST.lr);
ST.epoch = 0;
ST.losshist = [];
ST.acchist = [];
}
function regenData() { ST.data = DATA[ST.dataType](N_POINTS); }
// ---------------- DOM Refs ----------------
var $ = function (id) { return document.getElementById(id); };
var elEpoch = $("statEpoch"), elLoss = $("statLoss"), elAcc = $("statAcc"), elState = $("statState");
var elValHidden = $("valHidden"), elValLr = $("valLr");
var btnTrain = $("btnTrain"), btnStep = $("btnStep"), btnReset = $("btnReset");
var btnNewData = $("btnNewData"), btnSpeed = $("btnSpeed");
var svg = $("net"), nodeDetail = $("nodeDetail"), gradCheckEl = $("gradCheck");
var dbCanvas = $("db"), dbCtx = dbCanvas.getContext("2d");
var lossCtx = $("lossChart").getContext("2d"), accCtx = $("accChart").getContext("2d");
// ---------------- Ağ Görselleştirme (SVG) ----------------
var SVG_W = 640, SVG_H = 460;
var nodeCoords = []; // [katman][nöron] = {x,y}
function layoutNodes() {
nodeCoords = [];
var sizes = ST.net.sizes;
var padX = 90, padY = 40;
var usableW = SVG_W - padX * 2, usableH = SVG_H - padY * 2;
var nLayers = sizes.length;
for (var l = 0; l < nLayers; l++) {
var n = sizes[l];
var col = [];
var x = padX + (nLayers === 1 ? 0 : (usableW * l / (nLayers - 1)));
for (var i = 0; i < n; i++) {
var y = n === 1 ? SVG_H / 2 : (padY + usableH * i / (n - 1));
col.push({ x: x, y: y });
}
nodeCoords.push(col);
}
}
function weightColor(w, maxAbs) {
var t = clamp(Math.abs(w) / (maxAbs || 1), 0, 1);
// negatif = sıcak/pembe kırmızı, pozitif = teal/yeşil
if (w >= 0) return "rgba(94,234,212," + (0.15 + t * 0.85) + ")";
return "rgba(244,114,182," + (0.15 + t * 0.85) + ")";
}
function renderNet() {
layoutNodes();
var nLayers = ST.net.sizes.length;
// en büyük |ağırlık| — renk normalizasyonu
var maxAbs = 1e-6;
for (var l = 0; l < ST.net.W.length; l++)
for (var o = 0; o < ST.net.W[l].length; o++)
for (var i = 0; i < ST.net.W[l][o].length; i++)
maxAbs = Math.max(maxAbs, Math.abs(ST.net.W[l][o][i]));
var parts = [];
// Bağlantı çizgileri (önce, nöronların altında kalsın)
for (var l2 = 0; l2 < ST.net.W.length; l2++) {
var Wl = ST.net.W[l2];
for (var o2 = 0; o2 < Wl.length; o2++) {
for (var i2 = 0; i2 < Wl[o2].length; i2++) {
var w = Wl[o2][i2];
var a = nodeCoords[l2][i2], b = nodeCoords[l2 + 1][o2];
var t2 = clamp(Math.abs(w) / maxAbs, 0, 1);
var sw = 0.6 + t2 * 3.4;
parts.push('<line class="net-link" x1="' + a.x.toFixed(1) + '" y1="' + a.y.toFixed(1) +
'" x2="' + b.x.toFixed(1) + '" y2="' + b.y.toFixed(1) +
'" stroke="' + weightColor(w, maxAbs) + '" stroke-width="' + sw.toFixed(2) + '" />');
}
}
}
// Nöronlar
var layerNames = [];
for (var ln = 0; ln < nLayers; ln++) {
if (ln === 0) layerNames.push("Girdi");
else if (ln === nLayers - 1) layerNames.push("Çıkış");
else layerNames.push("Gizli " + ln);
}
for (var l3 = 0; l3 < nLayers; l3++) {
for (var n3 = 0; n3 < nodeCoords[l3].length; n3++) {
var p = nodeCoords[l3][n3];
var isOut = l3 === nLayers - 1;
var fill = ln === 0 ? "#334155" : (isOut ? "#f472b6" : "#5eead4");
var r = l3 === 0 ? 13 : (isOut ? 15 : 12);
parts.push('<circle class="net-node" data-l="' + l3 + '" data-n="' + n3 + '" cx="' +
p.x.toFixed(1) + '" cy="' + p.y.toFixed(1) + '" r="' + r + '" fill="' + fill +
'" stroke="#0a0e17" stroke-width="2.5" />');
}
// katman etiketi
var cy = SVG_H - 8;
parts.push('<text class="net-lbl" x="' + nodeCoords[l3][0].x.toFixed(1) +
'" y="' + cy + '" text-anchor="middle">' + layerNames[l3] + '</text>');
}
svg.innerHTML = parts.join("");
// nöron tıklama
var nodes = svg.querySelectorAll(".net-node");
for (var k = 0; k < nodes.length; k++) {
nodes[k].addEventListener("click", onNodeClick);
}
}
function onNodeClick(e) {
var l = +e.target.getAttribute("data-l");
var n = +e.target.getAttribute("data-n");
// o anki aktivasyonu yeniden hesapla (bir ortalama örnek üzerinden)
var sample = ST.data[0];
if (!sample) return;
var fwd = ST.net.forward(sample.x);
var val = fwd.acts[l] ? fwd.acts[l][n] : null;
var zval = l > 0 && fwd.zs[l - 1] ? fwd.zs[l - 1][n] : null;
nodeDetail.innerHTML =
"Katman <b>" + l + "</b> · Nöron <b>" + n + "</b><br>" +
"Aktivasyon a = <b>" + (val == null ? "—" : val.toFixed(4)) + "</b><br>" +
(zval != null ? "Ön-aktivasyon z = <b>" + zval.toFixed(4) + "</b><br>" : "") +
"Örnek girdi [" + sample.x[0].toFixed(2) + ", " + sample.x[1].toFixed(2) + "]";
}
// ---------------- Karar Sınırı ----------------
function drawDecisionBoundary() {
var W = dbCanvas.width, H = dbCanvas.height;
var res = 56; // hesap çözünürlüğü (grid)
var img = dbCtx.createImageData(W, H);
var d = img.data;
for (var py = 0; py < H; py++) {
for (var px = 0; px < W; px++) {
// canvas → [-1,1] koordinat (x2 ekseni ters)
var x1 = (px / W) * 2 - 1;
var x2 = (1 - py / H) * 2 - 1;
var p = ST.net.predict([x1, x2]);
// renk: p=0 (sınıf A teal) → p=1 (sınıf B pembe)
var r = Math.round(94 * (1 - p) + 244 * p);
var g = Math.round(234 * (1 - p) + 114 * p);
var b = Math.round(212 * (1 - p) + 182 * p);
var idx = (py * W + px) * 4;
d[idx] = r * 0.35; d[idx + 1] = g * 0.35; d[idx + 2] = b * 0.35; d[idx + 3] = 255;
}
}
dbCtx.putImageData(img, 0, 0);
// veri noktaları
for (var i = 0; i < ST.data.length; i++) {
var s = ST.data[i];
var cx = (s.x[0] + 1) / 2 * W;
var cy = (1 - (s.x[1] + 1) / 2) * H;
dbCtx.beginPath();
dbCtx.arc(cx, cy, 4.2, 0, Math.PI * 2);
dbCtx.fillStyle = s.y === 1 ? "rgba(244,114,182,.95)" : "rgba(94,234,212,.95)";
dbCtx.fill();
dbCtx.lineWidth = 1.4;
dbCtx.strokeStyle = "rgba(10,14,23,.9)";
dbCtx.stroke();
}
}
// ---------------- Grafikler ----------------
function drawChart(ctx, hist, color, fixedMax) {
var W = ctx.canvas.width, H = ctx.canvas.height;
var fillCol = ctx.canvas.id === "lossChart" ? "rgba(249,115,115,.12)" : "rgba(94,234,212,.12)";
ctx.clearRect(0, 0, W, H);
// ızgara
ctx.strokeStyle = "rgba(255,255,255,.06)";
ctx.lineWidth = 1;
for (var gy = 0; gy <= 4; gy++) {
var yy = (H * gy / 4);
ctx.beginPath(); ctx.moveTo(0, yy); ctx.lineTo(W, yy); ctx.stroke();
}
if (hist.length < 2) { return; }
var maxV = fixedMax != null ? fixedMax : Math.max.apply(null, hist) * 1.15;
if (maxV <= 0) maxV = 1;
ctx.beginPath();
var n = hist.length;
for (var i = 0; i < n; i++) {
var x = (i / (n - 1)) * W;
var y = H - clamp(hist[i] / maxV, 0, 1) * (H - 8) - 4;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.lineJoin = "round";
ctx.stroke();
// dolgu
ctx.lineTo(W, H); ctx.lineTo(0, H); ctx.closePath();
ctx.fillStyle = fillCol;
ctx.fill();
}
// ---------------- UI Güncelleme ----------------
function updateStats() {
elEpoch.textContent = ST.epoch;
elLoss.textContent = ST.lastLoss ? ST.lastLoss.toFixed(4) : "—";
elAcc.textContent = ST.lastAcc ? (ST.lastAcc * 100).toFixed(1) + "%" : "—";
elState.textContent = ST.running ? "eğitiliyor…" : (ST.epoch ? "duraklatıldı" : "hazır");
}
function refreshAll() {
renderNet();
drawDecisionBoundary();
drawChart(lossCtx, ST.losshist, "rgb(249,115,115)", null);
drawChart(accCtx, ST.acchist, "rgb(94,234,212)", 1);
updateStats();
}
// ---------------- Eğitim Döngüsü ----------------
function doEpoch() {
var r = ST.net.trainEpoch(ST.data);
ST.epoch++;
ST.lastLoss = r.loss;
ST.lastAcc = r.acc;
ST.losshist.push(r.loss);
ST.acchist.push(r.acc);
if (ST.losshist.length > 500) { ST.losshist.shift(); ST.acchist.shift(); }
}
var rafId = null;
function loop() {
if (!ST.running) return;
for (var s = 0; s < ST.speed; s++) doEpoch();
refreshAll();
rafId = requestAnimationFrame(loop);
}
function startTraining() {
if (ST.running) return;
ST.running = true;
btnTrain.textContent = "⏸ Duraklat";
btnTrain.classList.add("running");
updateStats();
rafId = requestAnimationFrame(loop);
}
function stopTraining() {
ST.running = false;
if (rafId) cancelAnimationFrame(rafId);
btnTrain.textContent = "▶ Eğit";
btnTrain.classList.remove("running");
updateStats();
}
// ---------------- Olay Bağlama ----------------
function bindSeg(id, key, parse, after) {
var seg = $(id);
seg.addEventListener("click", function (e) {
if (e.target.tagName !== "BUTTON") return;
var btns = seg.querySelectorAll("button");
for (var i = 0; i < btns.length; i++) btns[i].classList.remove("on");
e.target.classList.add("on");
ST[key] = parse(e.target.getAttribute("data-v"));
if (after) after();
});
}
bindSeg("segData", "dataType", String, function () { regenData(); refreshAll(); });
bindSeg("segAct", "actName", String, function () { buildNet(); refreshAll(); });
bindSeg("segLayers", "layers", Number, function () { buildNet(); refreshAll(); });
bindSeg("segOpt", "opt", String, function () { if (ST.net) ST.net.opt = ST.opt; });
$("rHidden").addEventListener("input", function (e) {
ST.hidden = +e.target.value; elValHidden.textContent = ST.hidden;
});
$("rHidden").addEventListener("change", function () { buildNet(); refreshAll(); });
$("rLr").addEventListener("input", function (e) {
ST.lr = +e.target.value; elValLr.textContent = ST.lr.toFixed(2);
if (ST.net) ST.net.lr = ST.lr;
});
btnTrain.addEventListener("click", function () {
if (ST.running) stopTraining(); else startTraining();
});
btnStep.addEventListener("click", function () {
if (ST.running) stopTraining();
doEpoch(); refreshAll();
});
btnReset.addEventListener("click", function () {
stopTraining(); buildNet(); ST.lastLoss = 0; ST.lastAcc = 0; refreshAll();
});
btnNewData.addEventListener("click", function () {
regenData(); refreshAll();
});
var speeds = [1, 4, 16, 64];
var si = 0;
btnSpeed.addEventListener("click", function () {
si = (si + 1) % speeds.length;
ST.speed = speeds[si];
btnSpeed.textContent = "⚡ Hız: x" + ST.speed;
});
// ---------------- Başlat ----------------
buildNet();
regenData();
refreshAll();
// gradyan doğrulama (backprop'un gerçekliğini kanıtla)
try {
var rel = ST.net.gradCheck(ST.data[0].x, ST.data[0].y);
if (rel < 1e-3) {
gradCheckEl.textContent = "Gradyan kontrolü: ✓ geçti (maks rel.hata " + rel.toExponential(1) + ")";
gradCheckEl.classList.remove("warn");
} else {
gradCheckEl.textContent = "Gradyan kontrolü: sapma " + rel.toExponential(1);
gradCheckEl.classList.add("warn");
}
} catch (err) {
gradCheckEl.textContent = "Gradyan kontrolü: atlandı";
}
// dışa aç (test için)
window.__NEURAL_LAB__ = { ST: ST, doEpoch: doEpoch, refresh: refreshAll };
})();