commit 422cb49b314b911b7835946e2b8ddef54eacae2d Author: Dmitry Date: Sat May 16 22:13:26 2026 +0300 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 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
+
+
+
+
+
+
+
+
+
+ + + + +