Files
lr_miad_nn/views/index.erb
T

465 lines
14 KiB
Plaintext
Raw 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.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Сети Хопфилда и Хэмминга</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: monospace;
background: #1a1a2e;
color: #e0e0e0;
padding: 2rem;
}
h1 { font-size: 1.4rem; margin-bottom: 2rem; color: #a0c4ff; }
h2 { font-size: 1rem; margin-bottom: 0.75rem; color: #8ab4f8; }
.section { margin-bottom: 2.5rem; }
.patterns-row {
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.pattern-box { text-align: center; }
.pattern-label { margin-bottom: 0.4rem; font-size: 0.85rem; color: #aaa; }
.grid {
display: grid;
grid-template-columns: repeat(8, 28px);
grid-template-rows: repeat(8, 28px);
gap: 2px;
}
.cell {
width: 28px;
height: 28px;
border-radius: 3px;
background: #2a2a4a;
transition: background 0.1s;
}
.cell.on { background: #a0c4ff; }
.cell.off { background: #2a2a4a; }
.cell.interactive { cursor: pointer; }
.cell.interactive:hover { opacity: 0.75; }
.controls {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
button {
padding: 0.5rem 1.5rem;
background: #a0c4ff;
color: #1a1a2e;
border: none;
border-radius: 4px;
cursor: pointer;
font-family: monospace;
font-size: 0.9rem;
font-weight: bold;
}
button:hover { background: #c0d8ff; }
button.secondary {
background: #2a2a4a;
color: #8ab4f8;
border: 1px solid #8ab4f8;
}
button.secondary:hover { background: #3a3a5a; }
.step-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
color: #aaa;
margin-bottom: 1.5rem;
cursor: pointer;
user-select: none;
}
.step-toggle input { cursor: pointer; accent-color: #8ab4f8; }
.step-speed {
display: none;
align-items: center;
gap: 0.5rem;
font-size: 0.82rem;
color: #aaa;
margin-left: 1rem;
}
.step-speed.visible { display: flex; }
.step-speed input[type=range] { accent-color: #8ab4f8; }
.recall-area {
display: flex;
gap: 3rem;
align-items: flex-start;
flex-wrap: wrap;
}
.result-box { text-align: center; }
.net-results {
display: flex;
gap: 2.5rem;
align-items: flex-start;
flex-wrap: wrap;
}
.net-block {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.net-title {
font-size: 0.8rem;
color: #8ab4f8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.step-counter {
font-size: 0.78rem;
color: #666;
min-height: 1.2em;
}
.hamming-answer {
font-size: 2rem;
font-weight: bold;
color: #a0c4ff;
padding: 0.5rem 0;
}
.maxnet-bars {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 200px;
}
.bar-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
}
.bar-name { width: 1.5rem; color: #aaa; }
.bar-track {
flex: 1;
height: 14px;
background: #2a2a4a;
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
background: #8ab4f8;
border-radius: 3px;
transition: width 0.15s;
}
.bar-val { width: 3rem; text-align: right; color: #666; }
.status-label {
margin-top: 0.5rem;
font-size: 0.82rem;
color: #aaa;
}
.status-label.yes { color: #80ffb0; }
.status-label.no { color: #ffaaaa; }
#results-section { display: none; }
#loading {
display: none;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
font-size: 0.85rem;
color: #8ab4f8;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid #2a2a4a;
border-top-color: #8ab4f8;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<h1>Сети Хопфилда и Хэмминга</h1>
<div class="section">
<h2>Эталонные образы</h2>
<div class="patterns-row">
<% @patterns.each do |name, vector| %>
<div class="pattern-box">
<div class="pattern-label"><%= name %></div>
<div class="grid">
<% vector.each do |v| %>
<div class="cell <%= v == 1 ? 'on' : 'off' %>"></div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
<div class="section">
<h2>Ввод зашумлённого образа</h2>
<div id="loading"><div class="spinner"></div>Вычисляю...</div>
<div class="controls">
<button onclick="recall()">Распознать</button>
<button class="secondary" onclick="clearGrid()">Очистить</button>
<button class="secondary" onclick="randomNoise()">Случайный шум</button>
</div>
<label class="step-toggle">
<input type="checkbox" id="step-mode"> Пошаговый режим
<span class="step-speed" id="speed-control">
&nbsp;· скорость:
<input type="range" id="step-delay" min="50" max="800" value="300" step="50">
<span id="delay-label">300 мс</span>
</span>
</label>
<div class="recall-area">
<div class="pattern-box">
<div class="pattern-label">Вход</div>
<div class="grid" id="input-grid">
<% 64.times do %>
<div class="cell off interactive" data-val="-1"></div>
<% end %>
</div>
</div>
<div id="results-section">
<div class="net-results">
<div class="net-block">
<div class="net-title">Хопфилд — восстановление</div>
<div class="step-counter" id="hopfield-step"></div>
<div class="pattern-box">
<div class="grid" id="output-grid">
<% 64.times do %>
<div class="cell off"></div>
<% end %>
</div>
</div>
<div class="status-label" id="hopfield-label"></div>
</div>
<div class="net-block">
<div class="net-title">Хэмминг — MAXNET</div>
<div class="step-counter" id="hamming-step"></div>
<div class="maxnet-bars" id="maxnet-bars"></div>
<div class="hamming-answer" id="hamming-answer">—</div>
<div class="status-label" id="hamming-label"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const patterns = <%= @patterns.transform_values { |v| v }.to_json %>;
document.querySelectorAll('#input-grid .cell').forEach(cell => {
cell.addEventListener('click', () => {
const isOn = cell.dataset.val === '1';
cell.dataset.val = isOn ? '-1' : '1';
cell.className = 'cell interactive ' + (isOn ? 'off' : 'on');
});
});
const stepModeCheckbox = document.getElementById('step-mode');
const speedControl = document.getElementById('speed-control');
const stepDelaySlider = document.getElementById('step-delay');
const delayLabel = document.getElementById('delay-label');
stepModeCheckbox.addEventListener('change', () => {
speedControl.classList.toggle('visible', stepModeCheckbox.checked);
});
stepDelaySlider.addEventListener('input', () => {
delayLabel.textContent = stepDelaySlider.value + ' мс';
});
function getInputVector() {
return [...document.querySelectorAll('#input-grid .cell')]
.map(c => parseFloat(c.dataset.val));
}
function clearGrid() {
document.querySelectorAll('#input-grid .cell').forEach(c => {
c.dataset.val = '-1';
c.className = 'cell interactive off';
});
document.getElementById('results-section').style.display = 'none';
}
function randomNoise() {
document.querySelectorAll('#input-grid .cell').forEach(c => {
const v = Math.random() > 0.5 ? '1' : '-1';
c.dataset.val = v;
c.className = 'cell interactive ' + (v === '1' ? 'on' : 'off');
});
document.getElementById('results-section').style.display = 'none';
}
function vectorsEqual(a, b) {
return a.every((v, i) => Math.abs(v - b[i]) < 0.01);
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
function renderGrid(cells, vector) {
cells.forEach((c, i) => {
c.className = 'cell ' + (vector[i] === 1 ? 'on' : 'off');
});
}
function renderBars(names, values) {
const max = Math.max(...values, 1);
const container = document.getElementById('maxnet-bars');
if (!container.children.length) {
container.innerHTML = names.map(n => `
<div class="bar-row">
<span class="bar-name">${n}</span>
<div class="bar-track"><div class="bar-fill" style="width:0%"></div></div>
<span class="bar-val">0</span>
</div>`).join('');
}
[...container.querySelectorAll('.bar-row')].forEach((row, i) => {
const pct = (values[i] / max * 100).toFixed(1);
row.querySelector('.bar-fill').style.width = pct + '%';
row.querySelector('.bar-val').textContent = values[i].toFixed(1);
});
}
async function showSteps(data) {
const delay = parseInt(stepDelaySlider.value);
const cells = [...document.querySelectorAll('#output-grid .cell')];
const names = data.pattern_names;
document.getElementById('results-section').style.display = 'block';
document.getElementById('hamming-answer').textContent = '—';
document.getElementById('hamming-label').textContent = '';
document.getElementById('hopfield-label').textContent = '';
// Инициализируем бары
renderBars(names, data.hamming_steps[0]);
const hfSteps = data.hopfield_steps;
const hmSteps = data.hamming_steps;
const total = Math.max(hfSteps.length, hmSteps.length);
for (let i = 0; i < total; i++) {
if (i < hfSteps.length) {
renderGrid(cells, hfSteps[i]);
document.getElementById('hopfield-step').textContent =
i === 0 ? 'Шаг 0 — входной вектор' : `Шаг ${i} из ${hfSteps.length - 1}`;
}
if (i < hmSteps.length) {
renderBars(names, hmSteps[i]);
document.getElementById('hamming-step').textContent =
i === 0 ? 'Шаг 0 — слой Хэмминга' : `Итерация MAXNET ${i} из ${hmSteps.length - 1}`;
}
await sleep(delay);
}
// Итог Хопфилд
const hfFinal = hfSteps[hfSteps.length - 1];
document.getElementById('hopfield-step').textContent =
`Завершено за ${hfSteps.length - 1} шаг(ов)`;
const hfMatch = Object.entries(patterns).find(([_, v]) => vectorsEqual(hfFinal, v))?.[0];
const hfLabel = document.getElementById('hopfield-label');
hfLabel.textContent = hfMatch ? `Совпадает с эталоном: ${hfMatch}` : 'Не совпадает ни с одним эталоном';
hfLabel.className = 'status-label ' + (hfMatch ? 'yes' : 'no');
// Итог Хэмминг
document.getElementById('hamming-step').textContent =
`Завершено за ${hmSteps.length - 1} итерац(ий)`;
document.getElementById('hamming-answer').textContent = data.hamming_winner;
const hmLabel = document.getElementById('hamming-label');
hmLabel.textContent = `Ближайший эталон: ${data.hamming_winner}`;
hmLabel.className = 'status-label yes';
}
function showResult(data) {
const cells = [...document.querySelectorAll('#output-grid .cell')];
document.getElementById('hopfield-step').textContent = '';
document.getElementById('hamming-step').textContent = '';
renderGrid(cells, data.hopfield);
const hfMatch = Object.entries(patterns).find(([_, v]) => vectorsEqual(data.hopfield, v))?.[0];
const hfLabel = document.getElementById('hopfield-label');
hfLabel.textContent = hfMatch ? `Совпадает с эталоном: ${hfMatch}` : 'Не совпадает ни с одним эталоном';
hfLabel.className = 'status-label ' + (hfMatch ? 'yes' : 'no');
document.getElementById('hamming-answer').textContent = data.hamming;
const hmLabel = document.getElementById('hamming-label');
hmLabel.textContent = `Ближайший эталон: ${data.hamming}`;
hmLabel.className = 'status-label yes';
// Скрываем бары в обычном режиме
document.getElementById('maxnet-bars').innerHTML = '';
document.getElementById('results-section').style.display = 'block';
}
async function recall() {
const vector = getInputVector();
const stepMode = stepModeCheckbox.checked;
const endpoint = stepMode ? '/recall_steps' : '/recall';
document.getElementById('loading').style.display = 'flex';
document.getElementById('results-section').style.display = 'none';
try {
const resp = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vector })
});
const data = await resp.json();
document.getElementById('loading').style.display = 'none';
if (stepMode) {
await showSteps(data);
} else {
showResult(data);
}
} catch {
document.getElementById('loading').style.display = 'none';
alert('Ошибка соединения с сервером');
}
}
</script>
</body>
<footer style="margin-top: 3rem; font-size: 0.8rem; color: #555; text-align: center;">
&copy; Визуализация работы сетей Хопфилда и Хэмминга by Антипенко Д.А. - 2026. Ruby/Sinatra
</html>