Compare commits
7 Commits
feature/formulas
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bac542902c | |||
| 26f4909ead | |||
| 4c8e7b9762 | |||
| 8adfa7bd93 | |||
| 75e8f3a400 | |||
| 7c7e28c3d6 | |||
| 7cce8921ab |
@@ -0,0 +1,21 @@
|
|||||||
|
name: Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Login to registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.GIT_PASS }}" | \
|
||||||
|
docker login git.ada-dev.ru -u ${{ secrets.GIT_USER }} --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
run: |
|
||||||
|
docker build -t git.ada-dev.ru/${{ gitea.repository }}:latest .
|
||||||
|
docker push git.ada-dev.ru/${{ gitea.repository }}:latest
|
||||||
+2
-4
@@ -6,7 +6,6 @@ GEM
|
|||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
multi_json (1.21.1)
|
multi_json (1.21.1)
|
||||||
mustermann (3.1.1)
|
mustermann (3.1.1)
|
||||||
nio4r (2.7.5)
|
|
||||||
nokogiri (1.19.3-aarch64-linux-gnu)
|
nokogiri (1.19.3-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.3-aarch64-linux-musl)
|
nokogiri (1.19.3-aarch64-linux-musl)
|
||||||
@@ -23,8 +22,6 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.3-x86_64-linux-musl)
|
nokogiri (1.19.3-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
puma (8.0.1)
|
|
||||||
nio4r (~> 2.0)
|
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.6)
|
rack (3.2.6)
|
||||||
rack-protection (4.2.1)
|
rack-protection (4.2.1)
|
||||||
@@ -60,6 +57,7 @@ GEM
|
|||||||
sinatra (= 4.2.1)
|
sinatra (= 4.2.1)
|
||||||
tilt (~> 2.0)
|
tilt (~> 2.0)
|
||||||
tilt (2.7.0)
|
tilt (2.7.0)
|
||||||
|
webrick (1.9.2)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux-gnu
|
aarch64-linux-gnu
|
||||||
@@ -72,12 +70,12 @@ PLATFORMS
|
|||||||
x86_64-linux-musl
|
x86_64-linux-musl
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
puma
|
|
||||||
rackup
|
rackup
|
||||||
roo
|
roo
|
||||||
rubyXL
|
rubyXL
|
||||||
sinatra
|
sinatra
|
||||||
sinatra-contrib
|
sinatra-contrib
|
||||||
|
webrick
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
2.5.3
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Как работает приложение
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle install
|
||||||
|
ruby app.rb # веб-интерфейс на http://localhost:4567
|
||||||
|
# или
|
||||||
|
ruby main.rb # только CLI: загружает паттерны, обучает, выводит инфо
|
||||||
|
```
|
||||||
|
|
||||||
|
В Docker: собирается образ, `generate_excel.rb` генерирует `patterns.xlsx` при старте, Sinatra слушает на 4567.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Данные
|
||||||
|
|
||||||
|
Паттерны хранятся в `patterns.xlsx`. Каждый лист = один символ (T, H, E, X...).
|
||||||
|
Ячейка `x` → `1`, остальное → `-1`. Размер: 8×8 = вектор из 64 значений.
|
||||||
|
|
||||||
|
`loader.rb` читает Excel через гем `roo` и возвращает `{name => matrix_8x8}`.
|
||||||
|
В вычислениях матрицы разворачиваются в векторы через `flatten`.
|
||||||
|
|
||||||
|
Если хочешь добавить/изменить паттерны — правь `generate_excel.rb` и перезапусти.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Веб-интерфейс
|
||||||
|
|
||||||
|
Одна страница (`views/index.erb`), тёмная тема, 8×8 кликабельная сетка ввода.
|
||||||
|
|
||||||
|
**Что можно сделать:**
|
||||||
|
- Нарисовать паттерн кликами (или добавить шум кнопкой)
|
||||||
|
- Нажать «Распознать» — сервер прогоняет через обе сети
|
||||||
|
- Включить «Пошаговый режим» — видно, как Хопфилд сходится шаг за шагом
|
||||||
|
- Включить «Показывать формулы» — рядом с анимацией появляются LaTeX-формулы вычислений
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Маршруты (`app.rb`)
|
||||||
|
|
||||||
|
| Маршрут | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `GET /` | Загружает паттерны, отдаёт HTML |
|
||||||
|
| `POST /recall` | Быстро: вектор → итоговый результат обеих сетей |
|
||||||
|
| `POST /recall_steps` | То же + все промежуточные состояния для анимации |
|
||||||
|
| `POST /recall_trace` | То же + net-значения, сколько нейронов изменилось на каждом шаге, трассировка MAXNET |
|
||||||
|
|
||||||
|
Все POST принимают JSON `{vector: [...64 числа...]}`, возвращают JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Алгоритмы
|
||||||
|
|
||||||
|
### Хопфилд (`hopfield.rb`)
|
||||||
|
|
||||||
|
**Обучение** — один раз при запуске:
|
||||||
|
```
|
||||||
|
W[i][j] += v[i] * v[j] для каждого паттерна v, i ≠ j
|
||||||
|
```
|
||||||
|
Матрица весов 64×64, кэшируется в `weights.cache`.
|
||||||
|
|
||||||
|
**Recall** — итерации до сходимости:
|
||||||
|
```
|
||||||
|
new[i] = sign( Σ_j W[i][j] * x[j] ) # sign: ≥0 → 1, <0 → -1
|
||||||
|
```
|
||||||
|
Все нейроны обновляются синхронно. Стоп: состояние не изменилось (EPS=0.01) или 200 итераций.
|
||||||
|
|
||||||
|
### Хэмминг (`hamming.rb`)
|
||||||
|
|
||||||
|
**Шаг 1 — слой Хэмминга** (один раз):
|
||||||
|
```
|
||||||
|
output[k] = (dot(pattern_k, input) + 64) / 2
|
||||||
|
```
|
||||||
|
Результат: число от 0 до 64 — сколько бит совпало с k-м эталоном.
|
||||||
|
|
||||||
|
**Шаг 2 — MAXNET** (итерации до одного победителя):
|
||||||
|
```
|
||||||
|
new[i] = max(output[i] - eps * Σ_{j≠i} output[j], 0) # eps = 0.9 / кол-во паттернов
|
||||||
|
```
|
||||||
|
Нейроны конкурируют: слабые гаснут, остаётся один. Его индекс = ближайший паттерн.
|
||||||
@@ -4,6 +4,7 @@ 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
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ post '/recall_steps' do
|
|||||||
MAX_MAXNET_ITER.times do
|
MAX_MAXNET_ITER.times do
|
||||||
nxt = maxnet_step(outputs, eps)
|
nxt = maxnet_step(outputs, eps)
|
||||||
hamming_steps << nxt.map { |v| v.round(2) }
|
hamming_steps << nxt.map { |v| v.round(2) }
|
||||||
break if nxt.count { |v| v > 0 } <= 1
|
break if nxt.count { |v| v > 1e-9 } <= 1
|
||||||
outputs = nxt
|
outputs = nxt
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -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 > 1e-9 } <= 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)
|
||||||
|
|||||||
+5
-7
@@ -13,17 +13,15 @@ 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
|
||||||
|
|
||||||
outputs = hamming_layer(pattern_vectors, input_vec)
|
outputs = hamming_layer(pattern_vectors, input_vec)
|
||||||
MAX_MAXNET_ITER.times do
|
iter = 0
|
||||||
next_out = maxnet_step(outputs, eps)
|
while outputs.count { |v| v > 1e-9 } > 1 && iter < MAX_MAXNET_ITER
|
||||||
return next_out.each_with_index.max_by { |v, _| v }[1] if next_out.count { |v| v > 0 } <= 1
|
outputs = maxnet_step(outputs, eps)
|
||||||
outputs = next_out
|
iter += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
outputs.each_with_index.max_by { |v, _| v }[1]
|
outputs.each_with_index.max_by { |v, _| v }[1]
|
||||||
end
|
end
|
||||||
|
|||||||
+6
-4
@@ -40,10 +40,12 @@ MAX_HOPFIELD_ITER = 200
|
|||||||
|
|
||||||
def recall_hopfield(weights, input_vec)
|
def recall_hopfield(weights, input_vec)
|
||||||
current = input_vec.dup
|
current = input_vec.dup
|
||||||
MAX_HOPFIELD_ITER.times do
|
next_vec = step_hopfield(weights, current)
|
||||||
next_vec = step_hopfield(weights, current)
|
iter = 1
|
||||||
return current if vectors_equal?(current, next_vec)
|
while !vectors_equal?(current, next_vec) && iter < MAX_HOPFIELD_ITER
|
||||||
current = next_vec
|
current = next_vec
|
||||||
|
next_vec = step_hopfield(weights, current)
|
||||||
|
iter += 1
|
||||||
end
|
end
|
||||||
current
|
next_vec
|
||||||
end
|
end
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
require 'roo'
|
require 'roo'
|
||||||
|
require 'zip'
|
||||||
|
Zip.warn_invalid_date = false
|
||||||
|
|
||||||
def load_patterns(file)
|
def load_patterns(file)
|
||||||
xlsx = Roo::Spreadsheet.open(file)
|
xlsx = Roo::Spreadsheet.open(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
|
||||||
+192
-22
@@ -3,6 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Сети Хопфилда и Хэмминга</title>
|
<title>Сети Хопфилда и Хэмминга</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%231a1a2e'/%3E%3Cline x1='6' y1='10' x2='16' y2='6' stroke='%238ab4f8' stroke-width='1'/%3E%3Cline x1='6' y1='10' x2='16' y2='16' stroke='%238ab4f8' stroke-width='1'/%3E%3Cline x1='6' y1='10' x2='16' y2='26' stroke='%238ab4f8' stroke-width='1'/%3E%3Cline x1='6' y1='22' x2='16' y2='6' stroke='%238ab4f8' stroke-width='1'/%3E%3Cline x1='6' y1='22' x2='16' y2='16' stroke='%238ab4f8' stroke-width='1'/%3E%3Cline x1='6' y1='22' x2='16' y2='26' stroke='%238ab4f8' stroke-width='1'/%3E%3Cline x1='16' y1='6' x2='26' y2='16' stroke='%238ab4f8' stroke-width='1'/%3E%3Cline x1='16' y1='16' x2='26' y2='16' stroke='%238ab4f8' stroke-width='1'/%3E%3Cline x1='16' y1='26' x2='26' y2='16' stroke='%238ab4f8' stroke-width='1'/%3E%3Ccircle cx='6' cy='10' r='3' fill='%23a0c4ff'/%3E%3Ccircle cx='6' cy='22' r='3' fill='%23a0c4ff'/%3E%3Ccircle cx='16' cy='6' r='3' fill='%23a0c4ff'/%3E%3Ccircle cx='16' cy='16' r='3' fill='%23a0c4ff'/%3E%3Ccircle cx='16' cy='26' r='3' fill='%23a0c4ff'/%3E%3Ccircle cx='26' cy='16' r='3' fill='%23a0c4ff'/%3E%3C/svg%3E">
|
||||||
|
<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 +187,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;
|
||||||
@@ -207,7 +264,10 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>Сети Хопфилда и Хэмминга</h1>
|
<div style="display:flex; align-items:center; gap:1rem; margin-bottom:2rem;">
|
||||||
|
<h1 style="margin-bottom:0;">Сети Хопфилда и Хэмминга</h1>
|
||||||
|
<a href="https://ada-dev.ru" class="secondary" style="padding:0.5rem 1.5rem; background:#2a2a4a; color:#8ab4f8; border:1px solid #8ab4f8; border-radius:4px; cursor:pointer; font-family:monospace; font-size:0.9rem; font-weight:bold; text-decoration:none;">Домой</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Эталонные образы</h2>
|
<h2>Эталонные образы</h2>
|
||||||
@@ -241,6 +301,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 +315,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,15 +364,68 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const stepModeCheckbox = document.getElementById('step-mode');
|
let currentRunId = 0;
|
||||||
const speedControl = document.getElementById('speed-control');
|
|
||||||
const stepDelaySlider = document.getElementById('step-delay');
|
const stepModeCheckbox = document.getElementById('step-mode');
|
||||||
const delayLabel = document.getElementById('delay-label');
|
const formulaModeCheckbox = document.getElementById('formula-mode');
|
||||||
|
const speedControl = document.getElementById('speed-control');
|
||||||
|
const stepDelaySlider = document.getElementById('step-delay');
|
||||||
|
const delayLabel = document.getElementById('delay-label');
|
||||||
|
|
||||||
stepModeCheckbox.addEventListener('change', () => {
|
stepModeCheckbox.addEventListener('change', () => {
|
||||||
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`s_j(p{+}1) = \sum_{i=0}^{n-1} w_{ij}\,y_i(p)`);
|
||||||
|
hfFormula(String.raw`y_j(p{+}1) = f\!\left[s_j(p{+}1)\right], \quad f(s)=\begin{cases}1&s\geq0\\-1&s<0\end{cases}`);
|
||||||
|
data.hopfield_nets.forEach((nd, i) => {
|
||||||
|
const mark = nd.changed === 0 ? ' ← сошлось' : '';
|
||||||
|
hfLine(`p=${i + 1}: изменено ${nd.changed}/64${mark}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
hmFormula(String.raw`w_{ik} = \tfrac{x_i^k}{2}, \quad T_k = \tfrac{n}{2}`);
|
||||||
|
hmFormula(String.raw`y_j^{(1)} = \sum_{i=0}^{n-1} w_{ij}\,x_i + T_j`);
|
||||||
|
const layer = names.map((n, i) => `${n}\\!=\\!${t.initial_outputs[i]}`).join(',\\;');
|
||||||
|
hmFormula(String.raw`y_j^{(2)}(0) = y_j^{(1)}:\;` + layer);
|
||||||
|
hmFormula(String.raw`0 < \varepsilon < \tfrac{1}{m},\quad \varepsilon = \tfrac{0.9}{${t.k}} = ${t.eps}`);
|
||||||
|
hmFormula(String.raw`s_j^{(2)}(p{+}1) = y_j^{(2)}(p) - \varepsilon\!\sum_{k \neq j} y_k^{(2)}(p)`);
|
||||||
|
hmFormula(String.raw`y_j^{(2)}(p{+}1) = f\!\left[s_j^{(2)}(p{+}1)\right]`);
|
||||||
|
data.hamming_steps.slice(1).forEach((step, i) => {
|
||||||
|
const vals = names.map((n, j) => `${n}\\!=\\!${step[j]}`).join(',\\;');
|
||||||
|
hmFormula(`p=${i + 1}:\\; y^{(2)} = [\\;` + 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 +435,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 +454,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 +489,24 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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`s_j(p{+}1) = \sum_{i=0}^{n-1} w_{ij}\,y_i(p)`);
|
||||||
|
hfFormula(String.raw`y_j(p{+}1) = f\!\left[s_j(p{+}1)\right], \quad f(s)=\begin{cases}1&s\geq0\\-1&s<0\end{cases}`);
|
||||||
|
}
|
||||||
|
|
||||||
renderBars(names, data.hamming_steps[0]);
|
renderBars(names, data.hamming_steps[0]);
|
||||||
|
|
||||||
const hfSteps = data.hopfield_steps;
|
const hfSteps = data.hopfield_steps;
|
||||||
@@ -379,16 +518,36 @@
|
|||||||
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(`p=${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`w_{ik} = \tfrac{x_i^k}{2}, \quad T_k = \tfrac{n}{2}`);
|
||||||
|
hmFormula(String.raw`y_j^{(1)} = \sum_{i=0}^{n-1} w_{ij}\,x_i + T_j`);
|
||||||
|
const layer = names.map((n, j) => `${n}\\!=\\!${t.initial_outputs[j]}`).join(',\\;');
|
||||||
|
hmFormula(String.raw`y_j^{(2)}(0) = y_j^{(1)}:\;` + layer);
|
||||||
|
hmFormula(String.raw`0 < \varepsilon < \tfrac{1}{m},\quad \varepsilon = \tfrac{0.9}{${t.k}} = ${t.eps}`);
|
||||||
|
hmFormula(String.raw`s_j^{(2)}(p{+}1) = y_j^{(2)}(p) - \varepsilon\!\sum_{k \neq j} y_k^{(2)}(p)`);
|
||||||
|
hmFormula(String.raw`y_j^{(2)}(p{+}1) = f\!\left[s_j^{(2)}(p{+}1)\right]`);
|
||||||
|
} else {
|
||||||
|
const vals = names.map((n, j) => `${n}\\!=\\!${hmSteps[i][j]}`).join(',\\;');
|
||||||
|
hmFormula(`p=${i}:\\; y^{(2)} = [\\;` + 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 +556,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{победитель (}\,y_j^{(2)}\text{ max):}\;\mathbf{` + data.hamming_winner + `}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showResult(data) {
|
function showResult(data) {
|
||||||
@@ -418,24 +577,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 vector = getInputVector();
|
const runId = ++currentRunId;
|
||||||
const stepMode = stepModeCheckbox.checked;
|
const vector = getInputVector();
|
||||||
const endpoint = stepMode ? '/recall_steps' : '/recall';
|
const stepMode = stepModeCheckbox.checked;
|
||||||
|
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 +610,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,4 +627,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
<footer style="margin-top:3rem; font-size:0.75rem; color:#666; text-align:center;">
|
||||||
|
© Created by Antipenko D.A. for MIAD - 2026
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user