Implement trace functionality for Hopfield and Hamming networks; update Gemfile.lock and app.rb for new dependencies and features

This commit is contained in:
Dmitry
2026-05-17 16:24:34 +03:00
parent 7cce8921ab
commit 7c7e28c3d6
4 changed files with 240 additions and 25 deletions
+40 -1
View File
@@ -4,11 +4,12 @@ require_relative 'loader'
require_relative 'functions' require_relative 'functions'
require_relative 'hopfield' require_relative 'hopfield'
require_relative 'hamming' require_relative 'hamming'
require_relative 'trace'
EPS = 0.01 EPS = 0.01
set :bind, '0.0.0.0' set :bind, '0.0.0.0'
set :port, 4567 set :port, 4568
set :server, 'webrick' set :server, 'webrick'
disable :protection disable :protection
set :host_authorization, { allow_if: ->(_env) { true } } set :host_authorization, { allow_if: ->(_env) { true } }
@@ -64,6 +65,44 @@ post '/recall_steps' do
}.to_json }.to_json
end end
post '/recall_trace' do
content_type :json
noisy = JSON.parse(request.body.read)['vector'].map(&:to_f)
hopfield_steps = [noisy.map(&:to_i)]
hopfield_nets = []
current = noisy.dup
MAX_HOPFIELD_ITER.times do
net = trace_hopfield_step(WEIGHTS, current)
nxt = net.map { |v| v >= 0 ? 1 : -1 }
changed = current.each_with_index.count { |v, i| v.to_i != nxt[i] }
hopfield_nets << { net: net.map { |v| v.round(3) }, changed: changed }
hopfield_steps << nxt
break if vectors_equal?(current, nxt)
current = nxt
end
eps_val = 0.9 / PATTERN_VECTORS.length
outputs = hamming_layer(PATTERN_VECTORS, noisy)
hamming_steps = [outputs.map { |v| v.round(2) }]
MAX_MAXNET_ITER.times do
nxt = maxnet_step(outputs, eps_val)
hamming_steps << nxt.map { |v| v.round(2) }
break if nxt.count { |v| v > 0 } <= 1
outputs = nxt
end
winner_idx = outputs.each_with_index.max_by { |v, _| v }[1]
{
hopfield_steps: hopfield_steps,
hopfield_nets: hopfield_nets,
hamming_steps: hamming_steps,
pattern_names: PATTERN_NAMES,
hamming_winner: PATTERN_NAMES[winner_idx],
hamming_trace: hamming_trace(PATTERN_VECTORS, noisy)
}.to_json
end
post '/recall' do post '/recall' do
content_type :json content_type :json
noisy = JSON.parse(request.body.read)['vector'].map(&:to_f) noisy = JSON.parse(request.body.read)['vector'].map(&:to_f)
+1 -1
View File
@@ -13,7 +13,7 @@ def maxnet_step(outputs, eps)
end end
end end
MAX_MAXNET_ITER = 500 MAX_MAXNET_ITER = 100
def recall_hamming(pattern_vectors, input_vec) def recall_hamming(pattern_vectors, input_vec)
eps = 0.9 / pattern_vectors.length eps = 0.9 / pattern_vectors.length
+18
View File
@@ -0,0 +1,18 @@
def trace_hopfield_step(weights, vector)
n = vector.length
Array.new(n) { |i| n.times.sum { |j| weights[i][j] * vector[j] } }
end
def hamming_trace(pattern_vectors, input_vec)
n = input_vec.length
k = pattern_vectors.length
dots = pattern_vectors.map { |pv| pv.zip(input_vec).sum { |a, b| a * b } }
initial = dots.map { |dot| (dot.to_f + n) / 2.0 }
{
eps: (0.9 / k).round(6),
k: k,
n: n,
dot_products: dots,
initial_outputs: initial.map { |v| v.round(2) }
}
end
+172 -14
View File
@@ -3,6 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Сети Хопфилда и Хэмминга</title> <title>Сети Хопфилда и Хэмминга</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<style> <style>
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -184,6 +186,60 @@
#results-section { display: none; } #results-section { display: none; }
#trace-panels {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.trace-panel {
flex: 1;
min-width: 220px;
background: #1e1e3a;
border: 1px solid #3a3a5a;
border-radius: 6px;
padding: 0.75rem 1rem;
font-size: 0.88rem;
color: #b0c4e8;
line-height: 1.8;
max-height: 420px;
overflow-y: auto;
}
.trace-panel-title {
font-family: monospace;
font-size: 0.75rem;
color: #8ab4f8;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.5rem;
border-bottom: 1px solid #3a3a5a;
padding-bottom: 0.3rem;
}
.trace-line {
font-family: monospace;
font-size: 0.82rem;
color: #888;
padding-left: 0.5rem;
}
.trace-header {
font-family: monospace;
font-size: 0.8rem;
color: #8ab4f8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.5rem;
}
.trace-formula {
padding: 0.2rem 0;
}
.katex { color: #e8d8ff; }
#loading { #loading {
display: none; display: none;
align-items: center; align-items: center;
@@ -241,6 +297,9 @@
<span id="delay-label">300 мс</span> <span id="delay-label">300 мс</span>
</span> </span>
</label> </label>
<label class="step-toggle">
<input type="checkbox" id="formula-mode"> Показывать формулы вычислений
</label>
<div class="recall-area"> <div class="recall-area">
<div class="pattern-box"> <div class="pattern-box">
@@ -252,6 +311,17 @@
</div> </div>
</div> </div>
<div id="trace-panels" style="display:none">
<div class="trace-panel">
<div class="trace-panel-title">Хопфилд</div>
<div id="trace-hopfield"></div>
</div>
<div class="trace-panel">
<div class="trace-panel-title">Хэмминг</div>
<div id="trace-hamming"></div>
</div>
</div>
<div id="results-section"> <div id="results-section">
<div class="net-results"> <div class="net-results">
<div class="net-block"> <div class="net-block">
@@ -290,7 +360,10 @@
}); });
}); });
let currentRunId = 0;
const stepModeCheckbox = document.getElementById('step-mode'); const stepModeCheckbox = document.getElementById('step-mode');
const formulaModeCheckbox = document.getElementById('formula-mode');
const speedControl = document.getElementById('speed-control'); const speedControl = document.getElementById('speed-control');
const stepDelaySlider = document.getElementById('step-delay'); const stepDelaySlider = document.getElementById('step-delay');
const delayLabel = document.getElementById('delay-label'); const delayLabel = document.getElementById('delay-label');
@@ -299,6 +372,53 @@
speedControl.classList.toggle('visible', stepModeCheckbox.checked); speedControl.classList.toggle('visible', stepModeCheckbox.checked);
}); });
function traceEl(panelId, className, content, isLatex = false) {
const panel = document.getElementById(panelId);
const el = document.createElement('div');
el.className = className;
if (isLatex) {
el.innerHTML = katex.renderToString(content, { throwOnError: false, displayMode: false });
} else {
el.textContent = content;
}
panel.appendChild(el);
panel.scrollTop = panel.scrollHeight;
}
const hfLine = (text) => traceEl('trace-hopfield', 'trace-line', text);
const hfFormula = (tex) => traceEl('trace-hopfield', 'trace-formula', tex, true);
const hmLine = (text) => traceEl('trace-hamming', 'trace-line', text);
const hmFormula = (tex) => traceEl('trace-hamming', 'trace-formula', tex, true);
function clearTrace() {
document.getElementById('trace-hopfield').innerHTML = '';
document.getElementById('trace-hamming').innerHTML = '';
document.getElementById('trace-panels').style.display = 'none';
}
function buildTrace(data) {
document.getElementById('trace-panels').style.display = 'flex';
const names = data.pattern_names;
const t = data.hamming_trace;
hfFormula(String.raw`\operatorname{net}_i = \sum_{j} W_{ij} \cdot x_j \qquad x_i = \operatorname{sgn}(\operatorname{net}_i)`);
data.hopfield_nets.forEach((nd, i) => {
const mark = nd.changed === 0 ? ' ← сошлось' : '';
hfLine(`шаг ${i + 1}: изменено ${nd.changed}/64${mark}`);
});
hmFormula(String.raw`y_k = \frac{\mathbf{x} \cdot \mathbf{p}_k + n}{2}`);
const layer = names.map((n, i) => `${n}\\!=\\!${t.initial_outputs[i]}`).join(',\\;');
hmFormula(String.raw`\text{слой: }\;` + layer);
hmFormula(String.raw`\varepsilon = \tfrac{0.9}{K} = \tfrac{0.9}{${t.k}} = ${t.eps}`);
hmFormula(String.raw`y_i(t{+}1) = \max\!\left(0,\; y_i(t) - \varepsilon\!\sum_{j \neq i} y_j(t)\right)`);
data.hamming_steps.slice(1).forEach((step, i) => {
const vals = names.map((n, j) => `${n}\\!=\\!${step[j]}`).join(',\\;');
hmFormula(`\\text{iter ${i + 1}: }\\;` + vals);
});
hmFormula(String.raw`\Rightarrow \text{победитель: } \mathbf{` + data.hamming_winner + `}`);
}
stepDelaySlider.addEventListener('input', () => { stepDelaySlider.addEventListener('input', () => {
delayLabel.textContent = stepDelaySlider.value + ' мс'; delayLabel.textContent = stepDelaySlider.value + ' мс';
}); });
@@ -308,12 +428,17 @@
.map(c => parseFloat(c.dataset.val)); .map(c => parseFloat(c.dataset.val));
} }
function hideResults() {
document.getElementById('results-section').style.display = 'none';
clearTrace();
}
function clearGrid() { function clearGrid() {
document.querySelectorAll('#input-grid .cell').forEach(c => { document.querySelectorAll('#input-grid .cell').forEach(c => {
c.dataset.val = '-1'; c.dataset.val = '-1';
c.className = 'cell interactive off'; c.className = 'cell interactive off';
}); });
document.getElementById('results-section').style.display = 'none'; hideResults();
} }
function randomNoise() { function randomNoise() {
@@ -322,7 +447,7 @@
c.dataset.val = v; c.dataset.val = v;
c.className = 'cell interactive ' + (v === '1' ? 'on' : 'off'); c.className = 'cell interactive ' + (v === '1' ? 'on' : 'off');
}); });
document.getElementById('results-section').style.display = 'none'; hideResults();
} }
function vectorsEqual(a, b) { function vectorsEqual(a, b) {
@@ -357,17 +482,23 @@
}); });
} }
async function showSteps(data) { async function showSteps(data, runId) {
const delay = parseInt(stepDelaySlider.value); const delay = parseInt(stepDelaySlider.value);
const cells = [...document.querySelectorAll('#output-grid .cell')]; const cells = [...document.querySelectorAll('#output-grid .cell')];
const names = data.pattern_names; const names = data.pattern_names;
const showTrace = formulaModeCheckbox.checked && data.hopfield_nets;
document.getElementById('results-section').style.display = 'block'; document.getElementById('results-section').style.display = 'block';
document.getElementById('hamming-answer').textContent = '—'; document.getElementById('hamming-answer').textContent = '—';
document.getElementById('hamming-label').textContent = ''; document.getElementById('hamming-label').textContent = '';
document.getElementById('hopfield-label').textContent = ''; document.getElementById('hopfield-label').textContent = '';
// Инициализируем бары if (showTrace) {
clearTrace();
document.getElementById('trace-panels').style.display = 'flex';
hfFormula(String.raw`\operatorname{net}_i = \sum_{j} W_{ij} \cdot x_j \qquad x_i = \operatorname{sgn}(\operatorname{net}_i)`);
}
renderBars(names, data.hamming_steps[0]); renderBars(names, data.hamming_steps[0]);
const hfSteps = data.hopfield_steps; const hfSteps = data.hopfield_steps;
@@ -379,16 +510,34 @@
renderGrid(cells, hfSteps[i]); renderGrid(cells, hfSteps[i]);
document.getElementById('hopfield-step').textContent = document.getElementById('hopfield-step').textContent =
i === 0 ? 'Шаг 0 — входной вектор' : `Шаг ${i} из ${hfSteps.length - 1}`; i === 0 ? 'Шаг 0 — входной вектор' : `Шаг ${i} из ${hfSteps.length - 1}`;
if (showTrace && i > 0 && data.hopfield_nets[i - 1]) {
const nd = data.hopfield_nets[i - 1];
const mark = nd.changed === 0 ? ' ← сошлось' : '';
hfLine(`шаг ${i}: изменено ${nd.changed}/64${mark}`);
}
} }
if (i < hmSteps.length) { if (i < hmSteps.length) {
renderBars(names, hmSteps[i]); renderBars(names, hmSteps[i]);
document.getElementById('hamming-step').textContent = document.getElementById('hamming-step').textContent =
i === 0 ? 'Шаг 0 — слой Хэмминга' : `Итерация MAXNET ${i} из ${hmSteps.length - 1}`; i === 0 ? 'Шаг 0 — слой Хэмминга' : `Итерация MAXNET ${i} из ${hmSteps.length - 1}`;
if (showTrace) {
const t = data.hamming_trace;
if (i === 0) {
hmFormula(String.raw`y_k = \frac{\mathbf{x} \cdot \mathbf{p}_k + n}{2}`);
const layer = names.map((n, j) => `${n}\\!=\\!${t.initial_outputs[j]}`).join(',\\;');
hmFormula(String.raw`\text{слой: }\;` + layer);
hmFormula(String.raw`\varepsilon = \tfrac{0.9}{${t.k}} = ${t.eps}`);
hmFormula(String.raw`y_i(t{+}1) = \max\!\left(0,\; y_i(t) - \varepsilon\!\sum_{j \neq i} y_j(t)\right)`);
} else {
const vals = names.map((n, j) => `${n}\\!=\\!${hmSteps[i][j]}`).join(',\\;');
hmFormula(`\\text{iter ${i}: }\\;` + vals);
}
}
} }
await sleep(delay); await sleep(delay);
if (runId !== currentRunId) return;
} }
// Итог Хопфилд
const hfFinal = hfSteps[hfSteps.length - 1]; const hfFinal = hfSteps[hfSteps.length - 1];
document.getElementById('hopfield-step').textContent = document.getElementById('hopfield-step').textContent =
`Завершено за ${hfSteps.length - 1} шаг(ов)`; `Завершено за ${hfSteps.length - 1} шаг(ов)`;
@@ -397,13 +546,13 @@
hfLabel.textContent = hfMatch ? `Совпадает с эталоном: ${hfMatch}` : 'Не совпадает ни с одним эталоном'; hfLabel.textContent = hfMatch ? `Совпадает с эталоном: ${hfMatch}` : 'Не совпадает ни с одним эталоном';
hfLabel.className = 'status-label ' + (hfMatch ? 'yes' : 'no'); hfLabel.className = 'status-label ' + (hfMatch ? 'yes' : 'no');
// Итог Хэмминг
document.getElementById('hamming-step').textContent = document.getElementById('hamming-step').textContent =
`Завершено за ${hmSteps.length - 1} итерац(ий)`; `Завершено за ${hmSteps.length - 1} итерац(ий)`;
document.getElementById('hamming-answer').textContent = data.hamming_winner; document.getElementById('hamming-answer').textContent = data.hamming_winner;
const hmLabel = document.getElementById('hamming-label'); const hmLabel = document.getElementById('hamming-label');
hmLabel.textContent = `Ближайший эталон: ${data.hamming_winner}`; hmLabel.textContent = `Ближайший эталон: ${data.hamming_winner}`;
hmLabel.className = 'status-label yes'; hmLabel.className = 'status-label yes';
if (showTrace) hmFormula(String.raw`\Rightarrow \text{победитель: } \mathbf{` + data.hamming_winner + `}`);
} }
function showResult(data) { function showResult(data) {
@@ -418,24 +567,32 @@
hfLabel.textContent = hfMatch ? `Совпадает с эталоном: ${hfMatch}` : 'Не совпадает ни с одним эталоном'; hfLabel.textContent = hfMatch ? `Совпадает с эталоном: ${hfMatch}` : 'Не совпадает ни с одним эталоном';
hfLabel.className = 'status-label ' + (hfMatch ? 'yes' : 'no'); hfLabel.className = 'status-label ' + (hfMatch ? 'yes' : 'no');
document.getElementById('hamming-answer').textContent = data.hamming; document.getElementById('hamming-answer').textContent = data.hamming_winner || data.hamming;
const hmLabel = document.getElementById('hamming-label'); const hmLabel = document.getElementById('hamming-label');
hmLabel.textContent = `Ближайший эталон: ${data.hamming}`; hmLabel.textContent = `Ближайший эталон: ${data.hamming_winner || data.hamming}`;
hmLabel.className = 'status-label yes'; hmLabel.className = 'status-label yes';
// Скрываем бары в обычном режиме
document.getElementById('maxnet-bars').innerHTML = ''; document.getElementById('maxnet-bars').innerHTML = '';
document.getElementById('results-section').style.display = 'block'; document.getElementById('results-section').style.display = 'block';
if (data.hopfield_nets && data.hopfield_nets.length) {
clearTrace();
buildTrace(data);
}
} }
async function recall() { async function recall() {
const runId = ++currentRunId;
const vector = getInputVector(); const vector = getInputVector();
const stepMode = stepModeCheckbox.checked; const stepMode = stepModeCheckbox.checked;
const endpoint = stepMode ? '/recall_steps' : '/recall'; const formulaMode = formulaModeCheckbox.checked;
const endpoint = formulaMode ? '/recall_trace'
: stepMode ? '/recall_steps'
: '/recall';
document.getElementById('loading').style.display = 'flex'; document.getElementById('loading').style.display = 'flex';
document.getElementById('results-section').style.display = 'none'; document.getElementById('results-section').style.display = 'none';
clearTrace();
try { try {
const resp = await fetch(endpoint, { const resp = await fetch(endpoint, {
@@ -443,11 +600,12 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vector }) body: JSON.stringify({ vector })
}); });
if (runId !== currentRunId) return;
const data = await resp.json(); const data = await resp.json();
document.getElementById('loading').style.display = 'none'; document.getElementById('loading').style.display = 'none';
if (stepMode) { if (stepMode) {
await showSteps(data); await showSteps(data, runId);
} else { } else {
showResult(data); showResult(data);
} }
@@ -459,6 +617,6 @@
</script> </script>
</body> </body>
<footer style="margin-top: 3rem; font-size: 0.8rem; color: #555; text-align: center;"> <footer style="margin-top:3rem; font-size:0.75rem; color:#666; text-align:center;">
&copy; Визуализация работы сетей Хопфилда и Хэмминга by Антипенко Д.А. - 2026. Ruby/Sinatra &copy; Develop by Antipenko D.A. for MIAD - 2026
</html> </html>