From 422cb49b314b911b7835946e2b8ddef54eacae2d Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 16 May 2026 22:13:26 +0300 Subject: [PATCH] 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 --- .dockerignore | 5 + .gitignore | 1 + Dockerfile | 18 ++ Gemfile | 8 + Gemfile.lock | 83 +++++++++ app.rb | 78 ++++++++ entrypoint.sh | 4 + functions.rb | 20 ++ generate_excel.rb | 58 ++++++ hamming.rb | 29 +++ hopfield.rb | 49 +++++ loader.rb | 11 ++ main.rb | 15 ++ views/index.erb | 462 ++++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 841 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 app.rb create mode 100644 entrypoint.sh create mode 100644 functions.rb create mode 100644 generate_excel.rb create mode 100644 hamming.rb create mode 100644 hopfield.rb create mode 100644 loader.rb create mode 100644 main.rb create mode 100644 views/index.erb diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..560a807 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +vendor/ +.bundle/ +.claude/ +weights.cache +*.xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67f24e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +patterns.xlsx diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b62db42 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..84e2a49 --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +gem 'roo' +gem 'rubyXL' +gem 'sinatra' +gem 'sinatra-contrib' +gem 'rackup' +gem 'webrick' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..07c22c4 --- /dev/null +++ b/Gemfile.lock @@ -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 diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..c7bb231 --- /dev/null +++ b/app.rb @@ -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 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..c560637 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e +[ -f patterns.xlsx ] || ruby generate_excel.rb +exec bundle exec ruby app.rb diff --git a/functions.rb b/functions.rb new file mode 100644 index 0000000..d2dac56 --- /dev/null +++ b/functions.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 + diff --git a/generate_excel.rb b/generate_excel.rb new file mode 100644 index 0000000..86985c1 --- /dev/null +++ b/generate_excel.rb @@ -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(', ')})" diff --git a/hamming.rb b/hamming.rb new file mode 100644 index 0000000..18544ff --- /dev/null +++ b/hamming.rb @@ -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 diff --git a/hopfield.rb b/hopfield.rb new file mode 100644 index 0000000..1b8db9f --- /dev/null +++ b/hopfield.rb @@ -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 \ No newline at end of file diff --git a/loader.rb b/loader.rb new file mode 100644 index 0000000..045e88e --- /dev/null +++ b/loader.rb @@ -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 + diff --git a/main.rb b/main.rb new file mode 100644 index 0000000..68a7f25 --- /dev/null +++ b/main.rb @@ -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" diff --git a/views/index.erb b/views/index.erb new file mode 100644 index 0000000..238a580 --- /dev/null +++ b/views/index.erb @@ -0,0 +1,462 @@ + + + + + Сети Хопфилда и Хэмминга + + + + +

Сети Хопфилда и Хэмминга

+ +
+

Эталонные образы

+
+ <% @patterns.each do |name, vector| %> +
+
<%= name %>
+
+ <% vector.each do |v| %> +
+ <% end %> +
+
+ <% end %> +
+
+ +
+

Ввод зашумлённого образа

+
Вычисляю...
+
+ + + +
+ + +
+
+
Вход
+
+ <% 64.times do %> +
+ <% end %> +
+
+ +
+
+
+
Хопфилд — восстановление
+
+
+
+ <% 64.times do %> +
+ <% end %> +
+
+
+
+ +
+
Хэмминг — MAXNET
+
+
+
+
+
+
+
+
+
+ + + + +