Feed a short numeric sequence into a single LSTM cell and watch the three gates fire at each step. Each gate outputs a value between 0 and 1 — 0 means fully closed (block this), 1 means fully open (pass this through). The cell state is the long-term memory; the hidden state is what the cell exposes to the outside world.
<p class="hint">A single LSTM cell processes one value at a time. Each step the three gates control what the cell <b>forgets</b>, <b>learns</b>, and <b>shows</b>. Press <b>Step</b> to advance, or <b>Run all</b> to watch the full sequence.</p>
<div class="seq-row">
<label>Input sequence (up to 8 values, space-separated):</label>
<div class="seq-input-row">
<input id="seqInput" type="text" value="0.9 0.1 0.8 0.2 0.7 0.3" />
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="timeline" id="timeline"></div>
<div class="cell-panel">
<div class="gates-grid" id="gatesGrid">
<div class="gate-block" id="gForget"><div class="gate-label">Forget gate <span class="gate-sym">f</span></div><div class="gate-bar-wrap"><div class="gate-bar" id="barF"></div></div><div class="gate-val" id="valF">—</div></div>
<div class="gate-block" id="gInput"><div class="gate-label">Input gate <span class="gate-sym">i</span></div><div class="gate-bar-wrap"><div class="gate-bar" id="barI"></div></div><div class="gate-val" id="valI">—</div></div>
<div class="gate-block" id="gOutput"><div class="gate-label">Output gate <span class="gate-sym">o</span></div><div class="gate-bar-wrap"><div class="gate-bar" id="barO"></div></div><div class="gate-val" id="valO">—</div></div>
<div class="gate-block" id="gCandidate"><div class="gate-label">Candidate <span class="gate-sym">g̃</span></div><div class="gate-bar-wrap two-sided"><div class="gate-bar cand" id="barC"></div></div><div class="gate-val" id="valC">—</div></div>
</div>
<div class="state-row">
<div class="state-block">
<div class="state-label">Cell state <span class="gate-sym">C</span></div>
<div class="state-bar-wrap two-sided"><div class="state-bar" id="barCell"></div></div>
<div class="state-val" id="valCell">0.00</div>
</div>
<div class="state-block">
<div class="state-label">Hidden state <span class="gate-sym">h</span></div>
<div class="state-bar-wrap two-sided"><div class="state-bar" id="barHidden"></div></div>
<div class="state-val" id="valHidden">0.00</div>
</div>
</div>
</div>
<div class="btns">
<button id="stepBtn" type="button">Step ▶</button>
<button id="runBtn" type="button">Run all</button>
</div>
<div class="status" id="status">Press Step to begin.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.seq-row { margin-bottom: .6rem; }
.seq-row label { font-size: .82rem; color: #555; display: block; margin-bottom: .3rem; }
.seq-input-row { display: flex; gap: .4rem; }
#seqInput { flex: 1; font: 14px ui-monospace, monospace; padding: .3rem .5rem; border: 1px solid #bbb; border-radius: 6px; }
.timeline { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: .7rem; min-height: 32px; }
.titem { padding: .22rem .5rem; border-radius: 6px; font: 600 13px ui-monospace, monospace;
background: #e8eef3; border: 1px solid #cdd9e3; color: #444; transition: all .2s; }
.titem.active { background: #1d3557; color: #fff; border-color: #1d3557; }
.titem.done { background: #d0f0d9; border-color: #5cb87a; color: #1a5c2a; }
.cell-panel { background: #f7f9fb; border: 1px solid #dde5ee; border-radius: 10px; padding: .8rem 1rem; margin-bottom: .6rem; }
.gates-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem .9rem; margin-bottom: .7rem; }
.gate-block { }
.gate-label { font-size: .78rem; color: #555; margin-bottom: .25rem; }
.gate-sym { font-style: italic; font-size: .82rem; }
.gate-bar-wrap { background: #e2e8f0; border-radius: 4px; height: 12px; overflow: hidden; position: relative; }
.gate-bar { height: 100%; border-radius: 4px; transition: width .35s, background .35s; width: 0%; background: #4a90d9; }
.gate-bar.cand { position: absolute; left: 50%; transform: scaleX(1); transform-origin: left; background: #e07b3c; }
.two-sided { }
.gate-val { font: 600 13px ui-monospace, monospace; color: #333; margin-top: .18rem; }
.state-row { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem .9rem; }
.state-block { }
.state-label { font-size: .78rem; color: #555; margin-bottom: .25rem; }
.state-bar-wrap { background: #e2e8f0; border-radius: 4px; height: 14px; overflow: hidden; position: relative; }
.state-bar { height: 100%; border-radius: 4px; transition: width .35s, left .35s, background .35s; position: absolute; background: #5a7088; }
.state-val { font: 700 14px ui-monospace, monospace; color: #1d3557; margin-top: .2rem; }
.btns { display: flex; gap: .5rem; margin-bottom: .4rem; }
button { font: 600 14px system-ui; padding: .42rem .9rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; color: #1a5c2a; }
.status.info { color: #444; }
.status.done { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simulated LSTM cell with fixed weights chosen to produce clear, illustrative gate behaviour.
// Weights are not trained — they are hand-tuned so the gates open/close visibly as the input alternates between high (~0.8) and low (~0.2) values.
// LSTM equations:
// f = sigmoid(Wf*[h,x] + bf) forget gate
// i = sigmoid(Wi*[h,x] + bi) input gate
// g = tanh (Wg*[h,x] + bg) candidate
// o = sigmoid(Wo*[h,x] + bo) output gate
// C = f*C + i*g new cell state
// h = o*tanh(C) new hidden state
// Single hidden unit; input size 1. Weight layout: [w_h, w_x, bias].
const WEIGHTS = {
f: { wh: 0.4, wx: -1.2, b: 0.6 }, // forget: high input closes it slightly
i: { wh: 0.3, wx: 1.5, b: -0.4 }, // input: opens strongly for high input
g: { wh: 0.5, wx: 2.0, b: -0.3 }, // candidate: tracks input with some history
o: { wh: 0.6, wx: 0.8, b: -0.2 }, // output: mostly open, modulated by h
};
const sigmoid = x => 1 / (1 + Math.exp(-x));
const tanh = x => Math.tanh(x);
function lstmStep(h, C, x) {
const W = WEIGHTS;
const f = sigmoid(W.f.wh * h + W.f.wx * x + W.f.b);
const i = sigmoid(W.i.wh * h + W.i.wx * x + W.i.b);
const g = tanh (W.g.wh * h + W.g.wx * x + W.g.b);
const o = sigmoid(W.o.wh * h + W.o.wx * x + W.o.b);
const Cnew = f * C + i * g;
const hnew = o * tanh(Cnew);
return { f, i, g, o, C: Cnew, h: hnew };
}
let seq = [], step = 0, h = 0, C = 0;
function parseSeq() {
const raw = document.getElementById('seqInput').value.trim();
const vals = raw.split(/\s+/).map(Number).filter(v => !isNaN(v)).slice(0, 8);
return vals.length ? vals : [0.9, 0.1, 0.8, 0.2, 0.7, 0.3];
}
function buildTimeline() {
const tl = document.getElementById('timeline');
tl.innerHTML = '';
seq.forEach((v, i) => {
const d = document.createElement('div');
d.className = 'titem' + (i < step ? ' done' : i === step ? ' active' : '');
d.textContent = v.toFixed(2);
tl.appendChild(d);
});
}
function setBar(barId, val, twoSided) {
const bar = document.getElementById(barId);
if (twoSided) {
// val in [-1, 1]; center at 50%
const pct = Math.abs(val) * 50;
if (val >= 0) {
bar.style.left = '50%';
bar.style.width = pct + '%';
bar.style.background = '#4a90d9';
} else {
bar.style.left = (50 - pct) + '%';
bar.style.width = pct + '%';
bar.style.background = '#e07b3c';
}
} else {
bar.style.width = (val * 100) + '%';
}
}
function setBarState(barId, val) {
const bar = document.getElementById(barId);
const clampedVal = Math.max(-1, Math.min(1, val));
const pct = Math.abs(clampedVal) * 50;
if (clampedVal >= 0) {
bar.style.left = '50%';
bar.style.width = pct + '%';
bar.style.background = '#1d7a3d';
} else {
bar.style.left = (50 - pct) + '%';
bar.style.width = pct + '%';
bar.style.background = '#c0392b';
}
}
function updateDisplay(res, x) {
document.getElementById('valF').textContent = res.f.toFixed(3);
document.getElementById('valI').textContent = res.i.toFixed(3);
document.getElementById('valO').textContent = res.o.toFixed(3);
document.getElementById('valC').textContent = res.g.toFixed(3);
document.getElementById('valCell').textContent = res.C.toFixed(3);
document.getElementById('valHidden').textContent = res.h.toFixed(3);
setBar('barF', res.f, false);
setBar('barI', res.i, false);
setBar('barO', res.o, false);
setBar('barC', res.g, true);
setBarState('barCell', res.C);
setBarState('barHidden', res.h);
}
function doStep() {
if (step >= seq.length) return;
const x = seq[step];
const res = lstmStep(h, C, x);
h = res.h; C = res.C;
step++;
buildTimeline();
updateDisplay(res, x);
const done = step >= seq.length;
document.getElementById('stepBtn').disabled = done;
document.getElementById('status').textContent = done
? fmt('Sequence complete. Final hidden state: %h% | Final cell state: %C%', { h: h.toFixed(4), C: C.toFixed(4) })
: fmt('Step %step%/%length% — input %x% → h: %h% C: %C%', { step: step, length: seq.length, x: x.toFixed(2), h: res.h.toFixed(3), C: res.C.toFixed(3) });
document.getElementById('status').className = 'status' + (done ? ' done' : '');
}
function reset() {
seq = parseSeq(); step = 0; h = 0; C = 0;
buildTimeline();
['barF','barI','barO','barC','barCell','barHidden'].forEach(id => {
const b = document.getElementById(id);
b.style.width = '0%'; b.style.left = '50%';
});
['valF','valI','valO','valC'].forEach(id => document.getElementById(id).textContent = '—');
document.getElementById('valCell').textContent = '0.000';
document.getElementById('valHidden').textContent = '0.000';
document.getElementById('stepBtn').disabled = false;
document.getElementById('status').textContent = 'Press Step to begin.';
document.getElementById('status').className = 'status info';
}
async function runAll() {
document.getElementById('runBtn').disabled = true;
document.getElementById('stepBtn').disabled = true;
while (step < seq.length) {
doStep();
await new Promise(r => setTimeout(r, 500));
}
document.getElementById('runBtn').disabled = false;
}
document.getElementById('stepBtn').addEventListener('click', doStep);
document.getElementById('runBtn').addEventListener('click', runAll);
document.getElementById('resetBtn').addEventListener('click', reset);
document.getElementById('seqInput').addEventListener('change', reset);
reset();
// ── Node.js verification shim (runs only outside browser) ──────────────────
if (typeof window === 'undefined') {
// Verify gate outputs are in [0,1] for sigmoid gates, [-1,1] for tanh candidate
function verify() {
const testCases = [
{ h: 0, C: 0, x: 0.9 },
{ h: 0.3, C: 0.5, x: 0.1 },
{ h: -0.2, C: -0.4, x: 0.8 },
];
let ok = true;
testCases.forEach(({ h, C, x }, idx) => {
const r = lstmStep(h, C, x);
const gatesInRange = [r.f, r.i, r.o].every(v => v >= 0 && v <= 1);
const candInRange = r.g >= -1 && r.g <= 1;
// Cell state update: C_new = f*C + i*g (no hard bound in general)
const cellCorrect = Math.abs(r.C - (r.f * C + r.i * r.g)) < 1e-9;
const hidCorrect = Math.abs(r.h - r.o * Math.tanh(r.C)) < 1e-9;
if (!gatesInRange || !candInRange || !cellCorrect || !hidCorrect) {
console.error('FAIL case', idx, r);
ok = false;
}
});
// Verify forget gate closes for high input (wx is negative for f)
const r_high = lstmStep(0, 0, 0.9);
const r_low = lstmStep(0, 0, 0.1);
if (r_high.f >= r_low.f) {
console.error('FAIL: forget gate should be lower for high input', r_high.f, r_low.f);
ok = false;
}
// Verify input gate opens more for high input (wx positive for i)
if (r_high.i <= r_low.i) {
console.error('FAIL: input gate should be higher for high input', r_high.i, r_low.i);
ok = false;
}
if (ok) console.log('PASS: all LSTM gate and state assertions passed');
}
verify();
}
<p class="hint">A single LSTM cell processes one value at a time. Each step the three gates control what the cell <b>forgets</b>, <b>learns</b>, and <b>shows</b>. Press <b>Step</b> to advance, or <b>Run all</b> to watch the full sequence.</p>
<div class="seq-row">
<label>Input sequence (up to 8 values, space-separated):</label>
<div class="seq-input-row">
<input id="seqInput" type="text" value="0.9 0.1 0.8 0.2 0.7 0.3" />
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
</div>
<div class="timeline" id="timeline"></div>
<div class="cell-panel">
<div class="gates-grid" id="gatesGrid">
<div class="gate-block" id="gForget"><div class="gate-label">Forget gate <span class="gate-sym">f</span></div><div class="gate-bar-wrap"><div class="gate-bar" id="barF"></div></div><div class="gate-val" id="valF">—</div></div>
<div class="gate-block" id="gInput"><div class="gate-label">Input gate <span class="gate-sym">i</span></div><div class="gate-bar-wrap"><div class="gate-bar" id="barI"></div></div><div class="gate-val" id="valI">—</div></div>
<div class="gate-block" id="gOutput"><div class="gate-label">Output gate <span class="gate-sym">o</span></div><div class="gate-bar-wrap"><div class="gate-bar" id="barO"></div></div><div class="gate-val" id="valO">—</div></div>
<div class="gate-block" id="gCandidate"><div class="gate-label">Candidate <span class="gate-sym">g̃</span></div><div class="gate-bar-wrap two-sided"><div class="gate-bar cand" id="barC"></div></div><div class="gate-val" id="valC">—</div></div>
</div>
<div class="state-row">
<div class="state-block">
<div class="state-label">Cell state <span class="gate-sym">C</span></div>
<div class="state-bar-wrap two-sided"><div class="state-bar" id="barCell"></div></div>
<div class="state-val" id="valCell">0.00</div>
</div>
<div class="state-block">
<div class="state-label">Hidden state <span class="gate-sym">h</span></div>
<div class="state-bar-wrap two-sided"><div class="state-bar" id="barHidden"></div></div>
<div class="state-val" id="valHidden">0.00</div>
</div>
</div>
</div>
<div class="btns">
<button id="stepBtn" type="button">Step ▶</button>
<button id="runBtn" type="button">Run all</button>
</div>
<div class="status" id="status">Press Step to begin.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.seq-row { margin-bottom: .6rem; }
.seq-row label { font-size: .82rem; color: #555; display: block; margin-bottom: .3rem; }
.seq-input-row { display: flex; gap: .4rem; }
#seqInput { flex: 1; font: 14px ui-monospace, monospace; padding: .3rem .5rem; border: 1px solid #bbb; border-radius: 6px; }
.timeline { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: .7rem; min-height: 32px; }
.titem { padding: .22rem .5rem; border-radius: 6px; font: 600 13px ui-monospace, monospace;
background: #e8eef3; border: 1px solid #cdd9e3; color: #444; transition: all .2s; }
.titem.active { background: #1d3557; color: #fff; border-color: #1d3557; }
.titem.done { background: #d0f0d9; border-color: #5cb87a; color: #1a5c2a; }
.cell-panel { background: #f7f9fb; border: 1px solid #dde5ee; border-radius: 10px; padding: .8rem 1rem; margin-bottom: .6rem; }
.gates-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem .9rem; margin-bottom: .7rem; }
.gate-block { }
.gate-label { font-size: .78rem; color: #555; margin-bottom: .25rem; }
.gate-sym { font-style: italic; font-size: .82rem; }
.gate-bar-wrap { background: #e2e8f0; border-radius: 4px; height: 12px; overflow: hidden; position: relative; }
.gate-bar { height: 100%; border-radius: 4px; transition: width .35s, background .35s; width: 0%; background: #4a90d9; }
.gate-bar.cand { position: absolute; left: 50%; transform: scaleX(1); transform-origin: left; background: #e07b3c; }
.two-sided { }
.gate-val { font: 600 13px ui-monospace, monospace; color: #333; margin-top: .18rem; }
.state-row { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem .9rem; }
.state-block { }
.state-label { font-size: .78rem; color: #555; margin-bottom: .25rem; }
.state-bar-wrap { background: #e2e8f0; border-radius: 4px; height: 14px; overflow: hidden; position: relative; }
.state-bar { height: 100%; border-radius: 4px; transition: width .35s, left .35s, background .35s; position: absolute; background: #5a7088; }
.state-val { font: 700 14px ui-monospace, monospace; color: #1d3557; margin-top: .2rem; }
.btns { display: flex; gap: .5rem; margin-bottom: .4rem; }
button { font: 600 14px system-ui; padding: .42rem .9rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; color: #1a5c2a; }
.status.info { color: #444; }
.status.done { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Simulated LSTM cell with fixed weights chosen to produce clear, illustrative gate behaviour.
// Weights are not trained — they are hand-tuned so the gates open/close visibly as the input alternates between high (~0.8) and low (~0.2) values.
// LSTM equations:
// f = sigmoid(Wf*[h,x] + bf) forget gate
// i = sigmoid(Wi*[h,x] + bi) input gate
// g = tanh (Wg*[h,x] + bg) candidate
// o = sigmoid(Wo*[h,x] + bo) output gate
// C = f*C + i*g new cell state
// h = o*tanh(C) new hidden state
// Single hidden unit; input size 1. Weight layout: [w_h, w_x, bias].
const WEIGHTS = {
f: { wh: 0.4, wx: -1.2, b: 0.6 }, // forget: high input closes it slightly
i: { wh: 0.3, wx: 1.5, b: -0.4 }, // input: opens strongly for high input
g: { wh: 0.5, wx: 2.0, b: -0.3 }, // candidate: tracks input with some history
o: { wh: 0.6, wx: 0.8, b: -0.2 }, // output: mostly open, modulated by h
};
const sigmoid = x => 1 / (1 + Math.exp(-x));
const tanh = x => Math.tanh(x);
function lstmStep(h, C, x) {
const W = WEIGHTS;
const f = sigmoid(W.f.wh * h + W.f.wx * x + W.f.b);
const i = sigmoid(W.i.wh * h + W.i.wx * x + W.i.b);
const g = tanh (W.g.wh * h + W.g.wx * x + W.g.b);
const o = sigmoid(W.o.wh * h + W.o.wx * x + W.o.b);
const Cnew = f * C + i * g;
const hnew = o * tanh(Cnew);
return { f, i, g, o, C: Cnew, h: hnew };
}
let seq = [], step = 0, h = 0, C = 0;
function parseSeq() {
const raw = document.getElementById('seqInput').value.trim();
const vals = raw.split(/\s+/).map(Number).filter(v => !isNaN(v)).slice(0, 8);
return vals.length ? vals : [0.9, 0.1, 0.8, 0.2, 0.7, 0.3];
}
function buildTimeline() {
const tl = document.getElementById('timeline');
tl.innerHTML = '';
seq.forEach((v, i) => {
const d = document.createElement('div');
d.className = 'titem' + (i < step ? ' done' : i === step ? ' active' : '');
d.textContent = v.toFixed(2);
tl.appendChild(d);
});
}
function setBar(barId, val, twoSided) {
const bar = document.getElementById(barId);
if (twoSided) {
// val in [-1, 1]; center at 50%
const pct = Math.abs(val) * 50;
if (val >= 0) {
bar.style.left = '50%';
bar.style.width = pct + '%';
bar.style.background = '#4a90d9';
} else {
bar.style.left = (50 - pct) + '%';
bar.style.width = pct + '%';
bar.style.background = '#e07b3c';
}
} else {
bar.style.width = (val * 100) + '%';
}
}
function setBarState(barId, val) {
const bar = document.getElementById(barId);
const clampedVal = Math.max(-1, Math.min(1, val));
const pct = Math.abs(clampedVal) * 50;
if (clampedVal >= 0) {
bar.style.left = '50%';
bar.style.width = pct + '%';
bar.style.background = '#1d7a3d';
} else {
bar.style.left = (50 - pct) + '%';
bar.style.width = pct + '%';
bar.style.background = '#c0392b';
}
}
function updateDisplay(res, x) {
document.getElementById('valF').textContent = res.f.toFixed(3);
document.getElementById('valI').textContent = res.i.toFixed(3);
document.getElementById('valO').textContent = res.o.toFixed(3);
document.getElementById('valC').textContent = res.g.toFixed(3);
document.getElementById('valCell').textContent = res.C.toFixed(3);
document.getElementById('valHidden').textContent = res.h.toFixed(3);
setBar('barF', res.f, false);
setBar('barI', res.i, false);
setBar('barO', res.o, false);
setBar('barC', res.g, true);
setBarState('barCell', res.C);
setBarState('barHidden', res.h);
}
function doStep() {
if (step >= seq.length) return;
const x = seq[step];
const res = lstmStep(h, C, x);
h = res.h; C = res.C;
step++;
buildTimeline();
updateDisplay(res, x);
const done = step >= seq.length;
document.getElementById('stepBtn').disabled = done;
document.getElementById('status').textContent = done
? fmt('Sequence complete. Final hidden state: %h% | Final cell state: %C%', { h: h.toFixed(4), C: C.toFixed(4) })
: fmt('Step %step%/%length% — input %x% → h: %h% C: %C%', { step: step, length: seq.length, x: x.toFixed(2), h: res.h.toFixed(3), C: res.C.toFixed(3) });
document.getElementById('status').className = 'status' + (done ? ' done' : '');
}
function reset() {
seq = parseSeq(); step = 0; h = 0; C = 0;
buildTimeline();
['barF','barI','barO','barC','barCell','barHidden'].forEach(id => {
const b = document.getElementById(id);
b.style.width = '0%'; b.style.left = '50%';
});
['valF','valI','valO','valC'].forEach(id => document.getElementById(id).textContent = '—');
document.getElementById('valCell').textContent = '0.000';
document.getElementById('valHidden').textContent = '0.000';
document.getElementById('stepBtn').disabled = false;
document.getElementById('status').textContent = 'Press Step to begin.';
document.getElementById('status').className = 'status info';
}
async function runAll() {
document.getElementById('runBtn').disabled = true;
document.getElementById('stepBtn').disabled = true;
while (step < seq.length) {
doStep();
await new Promise(r => setTimeout(r, 500));
}
document.getElementById('runBtn').disabled = false;
}
document.getElementById('stepBtn').addEventListener('click', doStep);
document.getElementById('runBtn').addEventListener('click', runAll);
document.getElementById('resetBtn').addEventListener('click', reset);
document.getElementById('seqInput').addEventListener('change', reset);
reset();
// ── Node.js verification shim (runs only outside browser) ──────────────────
if (typeof window === 'undefined') {
// Verify gate outputs are in [0,1] for sigmoid gates, [-1,1] for tanh candidate
function verify() {
const testCases = [
{ h: 0, C: 0, x: 0.9 },
{ h: 0.3, C: 0.5, x: 0.1 },
{ h: -0.2, C: -0.4, x: 0.8 },
];
let ok = true;
testCases.forEach(({ h, C, x }, idx) => {
const r = lstmStep(h, C, x);
const gatesInRange = [r.f, r.i, r.o].every(v => v >= 0 && v <= 1);
const candInRange = r.g >= -1 && r.g <= 1;
// Cell state update: C_new = f*C + i*g (no hard bound in general)
const cellCorrect = Math.abs(r.C - (r.f * C + r.i * r.g)) < 1e-9;
const hidCorrect = Math.abs(r.h - r.o * Math.tanh(r.C)) < 1e-9;
if (!gatesInRange || !candInRange || !cellCorrect || !hidCorrect) {
console.error('FAIL case', idx, r);
ok = false;
}
});
// Verify forget gate closes for high input (wx is negative for f)
const r_high = lstmStep(0, 0, 0.9);
const r_low = lstmStep(0, 0, 0.1);
if (r_high.f >= r_low.f) {
console.error('FAIL: forget gate should be lower for high input', r_high.f, r_low.f);
ok = false;
}
// Verify input gate opens more for high input (wx positive for i)
if (r_high.i <= r_low.i) {
console.error('FAIL: input gate should be higher for high input', r_high.i, r_low.i);
ok = false;
}
if (ok) console.log('PASS: all LSTM gate and state assertions passed');
}
verify();
}
Comments
Loading comments...