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
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
vendor/
|
||||||
|
.bundle/
|
||||||
|
.claude/
|
||||||
|
weights.cache
|
||||||
|
*.xlsx
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
patterns.xlsx
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
FROM ruby:3.2-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY Gemfile Gemfile.lock ./
|
||||||
|
RUN bundle install --jobs 4 --retry 3
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN ruby generate_excel.rb && chmod +x entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 4567
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
gem 'roo'
|
||||||
|
gem 'rubyXL'
|
||||||
|
gem 'sinatra'
|
||||||
|
gem 'sinatra-contrib'
|
||||||
|
gem 'rackup'
|
||||||
|
gem 'webrick'
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
base64 (0.3.0)
|
||||||
|
csv (3.3.5)
|
||||||
|
logger (1.7.0)
|
||||||
|
multi_json (1.21.1)
|
||||||
|
mustermann (3.1.1)
|
||||||
|
nio4r (2.7.5)
|
||||||
|
nokogiri (1.19.3-aarch64-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.3-aarch64-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.3-arm-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.3-arm-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.3-arm64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.3-x86_64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.19.3-x86_64-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
puma (8.0.1)
|
||||||
|
nio4r (~> 2.0)
|
||||||
|
racc (1.8.1)
|
||||||
|
rack (3.2.6)
|
||||||
|
rack-protection (4.2.1)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
|
logger (>= 1.6.0)
|
||||||
|
rack (>= 3.0.0, < 4)
|
||||||
|
rack-session (2.1.2)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
|
rack (>= 3.0.0)
|
||||||
|
rackup (2.3.1)
|
||||||
|
rack (>= 3)
|
||||||
|
roo (3.0.0)
|
||||||
|
base64 (~> 0.2)
|
||||||
|
csv (~> 3)
|
||||||
|
logger (~> 1)
|
||||||
|
nokogiri (~> 1)
|
||||||
|
rubyzip (>= 3.0.0, < 4.0.0)
|
||||||
|
rubyXL (3.4.35)
|
||||||
|
nokogiri (>= 1.10.8)
|
||||||
|
rubyzip (>= 3.2.2)
|
||||||
|
rubyzip (3.3.0)
|
||||||
|
sinatra (4.2.1)
|
||||||
|
logger (>= 1.6.0)
|
||||||
|
mustermann (~> 3.0)
|
||||||
|
rack (>= 3.0.0, < 4)
|
||||||
|
rack-protection (= 4.2.1)
|
||||||
|
rack-session (>= 2.0.0, < 3)
|
||||||
|
tilt (~> 2.0)
|
||||||
|
sinatra-contrib (4.2.1)
|
||||||
|
multi_json (>= 0.0.2)
|
||||||
|
mustermann (~> 3.0)
|
||||||
|
rack-protection (= 4.2.1)
|
||||||
|
sinatra (= 4.2.1)
|
||||||
|
tilt (~> 2.0)
|
||||||
|
tilt (2.7.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
aarch64-linux-gnu
|
||||||
|
aarch64-linux-musl
|
||||||
|
arm-linux-gnu
|
||||||
|
arm-linux-musl
|
||||||
|
arm64-darwin
|
||||||
|
x86_64-darwin
|
||||||
|
x86_64-linux-gnu
|
||||||
|
x86_64-linux-musl
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
puma
|
||||||
|
rackup
|
||||||
|
roo
|
||||||
|
rubyXL
|
||||||
|
sinatra
|
||||||
|
sinatra-contrib
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.5.3
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
require 'sinatra'
|
||||||
|
require 'json'
|
||||||
|
require_relative 'loader'
|
||||||
|
require_relative 'functions'
|
||||||
|
require_relative 'hopfield'
|
||||||
|
require_relative 'hamming'
|
||||||
|
|
||||||
|
EPS = 0.01
|
||||||
|
|
||||||
|
set :bind, '0.0.0.0'
|
||||||
|
set :port, 4567
|
||||||
|
set :server, 'webrick'
|
||||||
|
disable :protection
|
||||||
|
set :host_authorization, { allow_if: ->(_env) { true } }
|
||||||
|
|
||||||
|
PATTERNS = load_patterns('patterns.xlsx')
|
||||||
|
PATTERN_VECTORS = PATTERNS.map { |_, m| to_vector(m) }
|
||||||
|
PATTERN_NAMES = PATTERNS.keys.map(&:to_s)
|
||||||
|
WEIGHTS = load_weights_cache('patterns.xlsx') || begin
|
||||||
|
puts 'Обучение модели...'
|
||||||
|
w = train_hopfield(PATTERN_VECTORS)
|
||||||
|
save_weights_cache(w, 'patterns.xlsx')
|
||||||
|
puts 'Веса сохранены в кэш.'
|
||||||
|
w
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/' do
|
||||||
|
@patterns = PATTERNS.transform_values { |m| to_vector(m) }
|
||||||
|
erb :index
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/recall_steps' do
|
||||||
|
content_type :json
|
||||||
|
noisy = JSON.parse(request.body.read)['vector'].map(&:to_f)
|
||||||
|
|
||||||
|
# Хопфилд: собираем каждый шаг
|
||||||
|
hopfield_steps = [noisy.map { |v| v.to_i }]
|
||||||
|
current = noisy.dup
|
||||||
|
MAX_HOPFIELD_ITER.times do
|
||||||
|
nxt = step_hopfield(WEIGHTS, current)
|
||||||
|
hopfield_steps << nxt
|
||||||
|
break if vectors_equal?(current, nxt)
|
||||||
|
current = nxt
|
||||||
|
end
|
||||||
|
|
||||||
|
# Хэмминг: собираем состояния MAXNET
|
||||||
|
eps = 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)
|
||||||
|
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,
|
||||||
|
hamming_steps: hamming_steps,
|
||||||
|
pattern_names: PATTERN_NAMES,
|
||||||
|
hamming_winner: PATTERN_NAMES[winner_idx]
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
post '/recall' do
|
||||||
|
content_type :json
|
||||||
|
noisy = JSON.parse(request.body.read)['vector'].map(&:to_f)
|
||||||
|
|
||||||
|
hopfield_result = recall_hopfield(WEIGHTS, noisy)
|
||||||
|
hamming_idx = recall_hamming(PATTERN_VECTORS, noisy)
|
||||||
|
|
||||||
|
{
|
||||||
|
hopfield: hopfield_result,
|
||||||
|
hamming: PATTERN_NAMES[hamming_idx]
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
[ -f patterns.xlsx ] || ruby generate_excel.rb
|
||||||
|
exec bundle exec ruby app.rb
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
def to_vector(matrix)
|
||||||
|
matrix.flatten
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_matrix(vector)
|
||||||
|
vector.each_slice(8).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
# критерий остановки
|
||||||
|
def vectors_equal?(v1, v2, eps = EPS)
|
||||||
|
v1.zip(v2).all? { |a, b| (a - b).abs < eps }
|
||||||
|
end
|
||||||
|
|
||||||
|
# вывод паттерна в терминал
|
||||||
|
def print_pattern(vector)
|
||||||
|
to_matrix(vector).each do |row|
|
||||||
|
puts row.map { |v| v == 1 ? '█' : ' ' }.join
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
require 'rubyXL'
|
||||||
|
require 'rubyXL/convenience_methods'
|
||||||
|
|
||||||
|
PATTERNS = {
|
||||||
|
'T' => [
|
||||||
|
[ 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1]
|
||||||
|
],
|
||||||
|
'H' => [
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1, 1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1, 1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1, 1],
|
||||||
|
[ 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[ 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1, 1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1, 1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1, 1]
|
||||||
|
],
|
||||||
|
'E' => [
|
||||||
|
[ 1, 1, 1, 1, 1, 1, 1, 1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[ 1, 1, 1, 1, 1, 1,-1,-1],
|
||||||
|
[ 1, 1, 1, 1, 1, 1,-1,-1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1,-1],
|
||||||
|
[ 1, 1, 1, 1, 1, 1, 1, 1]
|
||||||
|
],
|
||||||
|
'X' => [
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1, 1],
|
||||||
|
[-1, 1,-1,-1,-1,-1, 1,-1],
|
||||||
|
[-1,-1, 1,-1,-1, 1,-1,-1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1],
|
||||||
|
[-1,-1,-1, 1, 1,-1,-1,-1],
|
||||||
|
[-1,-1, 1,-1,-1, 1,-1,-1],
|
||||||
|
[-1, 1,-1,-1,-1,-1, 1,-1],
|
||||||
|
[ 1,-1,-1,-1,-1,-1,-1, 1]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
wb = RubyXL::Workbook.new
|
||||||
|
wb.worksheets.delete_at(0)
|
||||||
|
PATTERNS.each do |name, matrix|
|
||||||
|
sheet = wb.add_worksheet(name)
|
||||||
|
matrix.each_with_index do |row, r|
|
||||||
|
row.each_with_index do |val, c|
|
||||||
|
sheet.add_cell(r, c, val == 1 ? 'x' : nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
wb.write('patterns.xlsx')
|
||||||
|
puts "Создан patterns.xlsx (листы: #{PATTERNS.keys.join(', ')})"
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
def hamming_layer(pattern_vectors, input_vec)
|
||||||
|
n = input_vec.length
|
||||||
|
pattern_vectors.map do |pv|
|
||||||
|
dot = pv.zip(input_vec).sum { |a, b| a * b }
|
||||||
|
(dot.to_f + n) / 2.0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def maxnet_step(outputs, eps)
|
||||||
|
outputs.map.with_index do |val, i|
|
||||||
|
inhibition = eps * outputs.each_with_index.sum { |v, j| j == i ? 0.0 : v }
|
||||||
|
[val - inhibition, 0.0].max
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
MAX_MAXNET_ITER = 500
|
||||||
|
|
||||||
|
def recall_hamming(pattern_vectors, input_vec)
|
||||||
|
eps = 0.9 / pattern_vectors.length
|
||||||
|
|
||||||
|
outputs = hamming_layer(pattern_vectors, input_vec)
|
||||||
|
MAX_MAXNET_ITER.times do
|
||||||
|
next_out = maxnet_step(outputs, eps)
|
||||||
|
return next_out.each_with_index.max_by { |v, _| v }[1] if next_out.count { |v| v > 0 } <= 1
|
||||||
|
outputs = next_out
|
||||||
|
end
|
||||||
|
|
||||||
|
outputs.each_with_index.max_by { |v, _| v }[1]
|
||||||
|
end
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
WEIGHTS_CACHE_FILE = 'weights.cache'
|
||||||
|
|
||||||
|
def load_weights_cache(patterns_file)
|
||||||
|
return nil unless File.exist?(WEIGHTS_CACHE_FILE)
|
||||||
|
cached = Marshal.load(File.binread(WEIGHTS_CACHE_FILE))
|
||||||
|
cached[:mtime] == File.mtime(patterns_file) ? cached[:weights] : nil
|
||||||
|
rescue
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_weights_cache(weights, patterns_file)
|
||||||
|
File.binwrite(WEIGHTS_CACHE_FILE, Marshal.dump({ mtime: File.mtime(patterns_file), weights: weights }))
|
||||||
|
end
|
||||||
|
|
||||||
|
def train_hopfield(vectors)
|
||||||
|
n = vectors[0].length
|
||||||
|
weights = Array.new(n) { Array.new(n, 0.0) }
|
||||||
|
|
||||||
|
vectors.each do |v|
|
||||||
|
n.times do |i|
|
||||||
|
n.times do |j|
|
||||||
|
weights[i][j] += v[i] * v[j] unless i == j
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
weights
|
||||||
|
end
|
||||||
|
|
||||||
|
def step_hopfield(weights, vector)
|
||||||
|
n = vector.length
|
||||||
|
Array.new(n) do |i|
|
||||||
|
sum = 0.0
|
||||||
|
n.times { |j| sum += weights[i][j] * vector[j] }
|
||||||
|
sum >= 0 ? 1 : -1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
MAX_HOPFIELD_ITER = 200
|
||||||
|
|
||||||
|
def recall_hopfield(weights, input_vec)
|
||||||
|
current = input_vec.dup
|
||||||
|
MAX_HOPFIELD_ITER.times do
|
||||||
|
next_vec = step_hopfield(weights, current)
|
||||||
|
return current if vectors_equal?(current, next_vec)
|
||||||
|
current = next_vec
|
||||||
|
end
|
||||||
|
current
|
||||||
|
end
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
require 'roo'
|
||||||
|
|
||||||
|
def load_patterns(file)
|
||||||
|
xlsx = Roo::Spreadsheet.open(file)
|
||||||
|
xlsx.sheets.each_with_object({}) do |sheet_name, patterns|
|
||||||
|
xlsx.default_sheet = sheet_name
|
||||||
|
matrix = (1..8).map { |row| (1..8).map { |col| xlsx.cell(row, col).to_s.strip.downcase == 'x' ? 1 : -1 } }
|
||||||
|
patterns[sheet_name.to_sym] = matrix
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
require_relative 'loader'
|
||||||
|
require_relative 'functions'
|
||||||
|
require_relative 'hopfield'
|
||||||
|
require_relative 'hamming'
|
||||||
|
|
||||||
|
EPS = 0.01
|
||||||
|
|
||||||
|
PATTERNS = load_patterns('patterns.xlsx')
|
||||||
|
|
||||||
|
pattern_vectors = PATTERNS.map { |_name, m| to_vector(m) }
|
||||||
|
pattern_names = PATTERNS.keys
|
||||||
|
weights = train_hopfield(pattern_vectors)
|
||||||
|
|
||||||
|
puts "Эталоны загружены: #{pattern_names.join(', ')}"
|
||||||
|
puts "Запусти веб-интерфейс: bundle exec ruby app.rb"
|
||||||
+462
@@ -0,0 +1,462 @@
|
|||||||
|
<!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">
|
||||||
|
· скорость:
|
||||||
|
<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>
|
||||||
Reference in New Issue
Block a user