Files
lr_miad_nn/views/index.erb
T
Dmitry 422cb49b31 Add initial implementation of Hopfield and Hamming networks with web interface
- Create Dockerfile and entrypoint script for application setup
- Implement pattern loading from Excel and weight caching
- Develop core functions for Hopfield and Hamming networks
- Add Sinatra web server for user interaction
- Create HTML interface for displaying patterns and results
- Include necessary gems in Gemfile and lockfile
- Add .dockerignore and .gitignore files
2026-05-16 22:13:26 +03:00

463 lines
13 KiB
Plaintext

<!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>
</html>