lex-meta-learning 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +11 -0
- data/lex-meta-learning.gemspec +30 -0
- data/lib/legion/extensions/meta_learning/client.rb +23 -0
- data/lib/legion/extensions/meta_learning/helpers/constants.rb +42 -0
- data/lib/legion/extensions/meta_learning/helpers/learning_domain.rb +81 -0
- data/lib/legion/extensions/meta_learning/helpers/meta_learning_engine.rb +198 -0
- data/lib/legion/extensions/meta_learning/helpers/strategy.rb +58 -0
- data/lib/legion/extensions/meta_learning/runners/meta_learning.rb +114 -0
- data/lib/legion/extensions/meta_learning/version.rb +9 -0
- data/lib/legion/extensions/meta_learning.rb +17 -0
- data/spec/legion/extensions/meta_learning/client_spec.rb +27 -0
- data/spec/legion/extensions/meta_learning/helpers/constants_spec.rb +43 -0
- data/spec/legion/extensions/meta_learning/helpers/learning_domain_spec.rb +146 -0
- data/spec/legion/extensions/meta_learning/helpers/meta_learning_engine_spec.rb +309 -0
- data/spec/legion/extensions/meta_learning/helpers/strategy_spec.rb +82 -0
- data/spec/legion/extensions/meta_learning/runners/meta_learning_spec.rb +185 -0
- data/spec/spec_helper.rb +20 -0
- metadata +78 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8a759f0665914754bffed745ff2c13f7f2ec290e1c8093a35b725acdd8959578
|
|
4
|
+
data.tar.gz: 6a05f278f9a31fc145394fc9b5f599c997f0d0dcd03f7edb90bd1b6a07fe585f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9b9bddd16d9dac71aed816ad189205954f4568fb5b381f96dfd134db58a71fc3b2d210562d250a30352fcab2fc71a659eedcd95f7eaf35a05fd0427728173a24
|
|
7
|
+
data.tar.gz: 2784c2e50db877ab19b9c8234a317732394356917ad5e380b60a52fe49eaf2515074026ec3d9fe67882326413c94f2a869eca0f924e71f80e48bfd26fa7c68c3
|
data/Gemfile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/meta_learning/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-meta-learning'
|
|
7
|
+
spec.version = Legion::Extensions::MetaLearning::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Meta-Learning'
|
|
12
|
+
spec.description = 'Meta-learning engine for brain-modeled agentic AI — tracks learning efficiency, ' \
|
|
13
|
+
'adapts learning rates, and selects strategies based on past performance'
|
|
14
|
+
spec.homepage = 'https://github.com/LegionIO/lex-meta-learning'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
spec.required_ruby_version = '>= 3.4'
|
|
17
|
+
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-meta-learning'
|
|
20
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-meta-learning'
|
|
21
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-meta-learning'
|
|
22
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-meta-learning/issues'
|
|
23
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
24
|
+
|
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-meta-learning.gemspec Gemfile]
|
|
27
|
+
end
|
|
28
|
+
spec.require_paths = ['lib']
|
|
29
|
+
spec.add_development_dependency 'legion-gaia'
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/meta_learning/helpers/constants'
|
|
4
|
+
require 'legion/extensions/meta_learning/helpers/learning_domain'
|
|
5
|
+
require 'legion/extensions/meta_learning/helpers/strategy'
|
|
6
|
+
require 'legion/extensions/meta_learning/helpers/meta_learning_engine'
|
|
7
|
+
require 'legion/extensions/meta_learning/runners/meta_learning'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module MetaLearning
|
|
12
|
+
class Client
|
|
13
|
+
include Runners::MetaLearning
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def engine
|
|
18
|
+
@engine ||= Helpers::MetaLearningEngine.new
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MetaLearning
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_DOMAINS = 100
|
|
9
|
+
MAX_STRATEGIES = 50
|
|
10
|
+
MAX_EPISODES = 1000
|
|
11
|
+
|
|
12
|
+
DEFAULT_LEARNING_RATE = 0.1
|
|
13
|
+
RATE_BOOST = 0.02
|
|
14
|
+
RATE_DECAY = 0.01
|
|
15
|
+
TRANSFER_BONUS = 0.05
|
|
16
|
+
|
|
17
|
+
PROFICIENCY_LABELS = {
|
|
18
|
+
(0.8..) => :expert,
|
|
19
|
+
(0.6...0.8) => :proficient,
|
|
20
|
+
(0.4...0.6) => :intermediate,
|
|
21
|
+
(0.2...0.4) => :novice,
|
|
22
|
+
(..0.2) => :beginner
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
EFFICIENCY_LABELS = {
|
|
26
|
+
(0.8..) => :highly_efficient,
|
|
27
|
+
(0.6...0.8) => :efficient,
|
|
28
|
+
(0.4...0.6) => :moderate,
|
|
29
|
+
(0.2...0.4) => :slow,
|
|
30
|
+
(..0.2) => :struggling
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
STRATEGY_TYPES = %i[
|
|
34
|
+
repetition elaboration analogy decomposition
|
|
35
|
+
pattern_matching trial_and_error observation
|
|
36
|
+
interleaving spaced_practice retrieval_practice
|
|
37
|
+
].freeze
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module MetaLearning
|
|
8
|
+
module Helpers
|
|
9
|
+
class LearningDomain
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_accessor :preferred_strategy
|
|
13
|
+
attr_reader :id, :name, :proficiency, :learning_rate, :episodes_count, :successes, :failures, :related_domains, :created_at
|
|
14
|
+
|
|
15
|
+
def initialize(name:, learning_rate: DEFAULT_LEARNING_RATE, related_domains: [])
|
|
16
|
+
@id = SecureRandom.uuid
|
|
17
|
+
@name = name
|
|
18
|
+
@proficiency = 0.0
|
|
19
|
+
@learning_rate = learning_rate.clamp(0.001, 1.0)
|
|
20
|
+
@episodes_count = 0
|
|
21
|
+
@successes = 0
|
|
22
|
+
@failures = 0
|
|
23
|
+
@preferred_strategy = nil
|
|
24
|
+
@related_domains = Array(related_domains).dup
|
|
25
|
+
@created_at = Time.now.utc
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record_success!
|
|
29
|
+
@successes += 1
|
|
30
|
+
@episodes_count += 1
|
|
31
|
+
@proficiency = (@proficiency + @learning_rate).clamp(0.0, 1.0).round(10)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def record_failure!
|
|
35
|
+
@failures += 1
|
|
36
|
+
@episodes_count += 1
|
|
37
|
+
penalty = (@learning_rate * 0.5).round(10)
|
|
38
|
+
@proficiency = (@proficiency - penalty).clamp(0.0, 1.0).round(10)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def efficiency
|
|
42
|
+
total = @successes + @failures
|
|
43
|
+
return 0.0 if total.zero?
|
|
44
|
+
|
|
45
|
+
(@successes.to_f / total).round(10)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def efficiency_label
|
|
49
|
+
EFFICIENCY_LABELS.find { |range, _| range.cover?(efficiency) }&.last || :struggling
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def proficiency_label
|
|
53
|
+
PROFICIENCY_LABELS.find { |range, _| range.cover?(@proficiency) }&.last || :beginner
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def adapt_rate!(delta:)
|
|
57
|
+
@learning_rate = (@learning_rate + delta).clamp(0.001, 1.0).round(10)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h
|
|
61
|
+
{
|
|
62
|
+
id: @id,
|
|
63
|
+
name: @name,
|
|
64
|
+
proficiency: @proficiency,
|
|
65
|
+
proficiency_label: proficiency_label,
|
|
66
|
+
learning_rate: @learning_rate,
|
|
67
|
+
episodes_count: @episodes_count,
|
|
68
|
+
successes: @successes,
|
|
69
|
+
failures: @failures,
|
|
70
|
+
efficiency: efficiency,
|
|
71
|
+
efficiency_label: efficiency_label,
|
|
72
|
+
preferred_strategy: @preferred_strategy,
|
|
73
|
+
related_domains: @related_domains,
|
|
74
|
+
created_at: @created_at
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MetaLearning
|
|
6
|
+
module Helpers
|
|
7
|
+
class MetaLearningEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :domains, :strategies, :episodes
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@domains = {}
|
|
14
|
+
@strategies = {}
|
|
15
|
+
@episodes = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_domain(name:, learning_rate: DEFAULT_LEARNING_RATE, related_domains: [])
|
|
19
|
+
return { error: :limit_reached } if @domains.size >= MAX_DOMAINS
|
|
20
|
+
|
|
21
|
+
domain = LearningDomain.new(name: name, learning_rate: learning_rate, related_domains: related_domains)
|
|
22
|
+
@domains[domain.id] = domain
|
|
23
|
+
domain
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_strategy(name:, strategy_type:)
|
|
27
|
+
return { error: :limit_reached } if @strategies.size >= MAX_STRATEGIES
|
|
28
|
+
return { error: :invalid_strategy_type } unless STRATEGY_TYPES.include?(strategy_type)
|
|
29
|
+
|
|
30
|
+
strategy = Strategy.new(name: name, strategy_type: strategy_type)
|
|
31
|
+
@strategies[strategy.id] = strategy
|
|
32
|
+
strategy
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def record_episode(domain_id:, success:, strategy_id: nil)
|
|
36
|
+
domain = @domains[domain_id]
|
|
37
|
+
return { error: :domain_not_found } unless domain
|
|
38
|
+
|
|
39
|
+
success ? domain.record_success! : domain.record_failure!
|
|
40
|
+
|
|
41
|
+
strategy = @strategies[strategy_id] if strategy_id
|
|
42
|
+
strategy&.use!(success: success, domain_name: domain.name)
|
|
43
|
+
|
|
44
|
+
if strategy && success
|
|
45
|
+
current_preferred_rate = preferred_strategy_rate_for(domain)
|
|
46
|
+
domain.preferred_strategy = strategy.name if strategy.success_rate > current_preferred_rate
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
episode = build_episode(domain, strategy_id, success)
|
|
50
|
+
@episodes << episode
|
|
51
|
+
@episodes.shift while @episodes.size > MAX_EPISODES
|
|
52
|
+
|
|
53
|
+
check_transfer_opportunities(domain)
|
|
54
|
+
|
|
55
|
+
episode
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def recommend_strategy(domain_id:)
|
|
59
|
+
domain = @domains[domain_id]
|
|
60
|
+
return { error: :domain_not_found } unless domain
|
|
61
|
+
|
|
62
|
+
candidate = best_strategy_for_domain(domain)
|
|
63
|
+
return { recommendation: nil, reason: :no_data } if candidate.nil?
|
|
64
|
+
|
|
65
|
+
{ recommendation: candidate.name, strategy_id: candidate.id, success_rate: candidate.success_rate }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def transfer_check(source_domain_id:, target_domain_id:)
|
|
69
|
+
source = @domains[source_domain_id]
|
|
70
|
+
target = @domains[target_domain_id]
|
|
71
|
+
return { error: :domain_not_found } unless source && target
|
|
72
|
+
|
|
73
|
+
eligible = source.proficiency >= 0.6 && target.related_domains.include?(source.name)
|
|
74
|
+
{ eligible: eligible, source_proficiency: source.proficiency, target_domain: target.name }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def apply_transfer(source_domain_id:, target_domain_id:)
|
|
78
|
+
source = @domains[source_domain_id]
|
|
79
|
+
target = @domains[target_domain_id]
|
|
80
|
+
return { error: :domain_not_found } unless source && target
|
|
81
|
+
|
|
82
|
+
check = transfer_check(source_domain_id: source_domain_id, target_domain_id: target_domain_id)
|
|
83
|
+
return { applied: false, reason: :not_eligible } unless check[:eligible]
|
|
84
|
+
|
|
85
|
+
target.adapt_rate!(delta: TRANSFER_BONUS)
|
|
86
|
+
{ applied: true, target_domain: target.name, new_learning_rate: target.learning_rate }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def domain_ranking(limit: 10)
|
|
90
|
+
@domains.values
|
|
91
|
+
.sort_by { |d| -d.proficiency }
|
|
92
|
+
.first(limit)
|
|
93
|
+
.map(&:to_h)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def strategy_ranking(limit: 10)
|
|
97
|
+
@strategies.values
|
|
98
|
+
.sort_by { |s| -s.success_rate }
|
|
99
|
+
.first(limit)
|
|
100
|
+
.map(&:to_h)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def overall_efficiency
|
|
104
|
+
return 0.0 if @domains.empty?
|
|
105
|
+
|
|
106
|
+
total = @domains.values.sum(&:efficiency)
|
|
107
|
+
(total / @domains.size).round(10)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def learning_curve(domain_id:)
|
|
111
|
+
domain = @domains[domain_id]
|
|
112
|
+
return { error: :domain_not_found } unless domain
|
|
113
|
+
|
|
114
|
+
domain_episodes = @episodes.select { |e| e[:domain_id] == domain_id }
|
|
115
|
+
{ domain: domain.name, curve: domain_episodes }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def adapt_rates
|
|
119
|
+
adapted = []
|
|
120
|
+
@domains.each_value do |domain|
|
|
121
|
+
next if domain.episodes_count.zero?
|
|
122
|
+
|
|
123
|
+
if domain.efficiency >= 0.8
|
|
124
|
+
domain.adapt_rate!(delta: RATE_BOOST)
|
|
125
|
+
adapted << { domain: domain.name, direction: :boost, new_rate: domain.learning_rate }
|
|
126
|
+
elsif domain.efficiency < 0.2
|
|
127
|
+
domain.adapt_rate!(delta: -RATE_DECAY)
|
|
128
|
+
adapted << { domain: domain.name, direction: :decay, new_rate: domain.learning_rate }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
{ adapted: adapted, count: adapted.size }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def prune_stale_domains(min_episodes: 1)
|
|
135
|
+
before = @domains.size
|
|
136
|
+
@domains.reject! { |_, d| d.episodes_count < min_episodes }
|
|
137
|
+
pruned = before - @domains.size
|
|
138
|
+
{ pruned: pruned, remaining: @domains.size }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def to_h
|
|
142
|
+
{
|
|
143
|
+
domain_count: @domains.size,
|
|
144
|
+
strategy_count: @strategies.size,
|
|
145
|
+
episode_count: @episodes.size,
|
|
146
|
+
overall_efficiency: overall_efficiency,
|
|
147
|
+
top_domain: @domains.values.max_by(&:proficiency)&.name,
|
|
148
|
+
top_strategy: @strategies.values.max_by(&:success_rate)&.name
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def build_episode(domain, strategy_id, success)
|
|
155
|
+
{
|
|
156
|
+
id: SecureRandom.uuid,
|
|
157
|
+
domain_id: domain.id,
|
|
158
|
+
domain_name: domain.name,
|
|
159
|
+
strategy_id: strategy_id,
|
|
160
|
+
success: success,
|
|
161
|
+
proficiency: domain.proficiency,
|
|
162
|
+
recorded_at: Time.now.utc
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def preferred_strategy_rate_for(domain)
|
|
167
|
+
return 0.0 unless domain.preferred_strategy
|
|
168
|
+
|
|
169
|
+
strategy = @strategies.values.find { |s| s.name == domain.preferred_strategy }
|
|
170
|
+
strategy&.success_rate || 0.0
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def best_strategy_for_domain(domain)
|
|
174
|
+
direct = strategies_used_in_domain(domain.name)
|
|
175
|
+
return direct.max_by(&:success_rate) if direct.any?
|
|
176
|
+
|
|
177
|
+
related_strategies = domain.related_domains.flat_map { |rname| strategies_used_in_domain(rname) }.uniq
|
|
178
|
+
related_strategies.max_by(&:success_rate)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def strategies_used_in_domain(domain_name)
|
|
182
|
+
@strategies.values.select { |s| s.domains_used.include?(domain_name) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def check_transfer_opportunities(domain)
|
|
186
|
+
@domains.each_value do |target|
|
|
187
|
+
next if target.id == domain.id
|
|
188
|
+
next unless target.related_domains.include?(domain.name)
|
|
189
|
+
|
|
190
|
+
check = transfer_check(source_domain_id: domain.id, target_domain_id: target.id)
|
|
191
|
+
apply_transfer(source_domain_id: domain.id, target_domain_id: target.id) if check[:eligible]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module MetaLearning
|
|
8
|
+
module Helpers
|
|
9
|
+
class Strategy
|
|
10
|
+
include Constants
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :name, :strategy_type, :usage_count,
|
|
13
|
+
:success_count, :domains_used, :created_at
|
|
14
|
+
|
|
15
|
+
def initialize(name:, strategy_type:)
|
|
16
|
+
@id = SecureRandom.uuid
|
|
17
|
+
@name = name
|
|
18
|
+
@strategy_type = strategy_type
|
|
19
|
+
@usage_count = 0
|
|
20
|
+
@success_count = 0
|
|
21
|
+
@domains_used = []
|
|
22
|
+
@created_at = Time.now.utc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def use!(success:, domain_name: nil)
|
|
26
|
+
@usage_count += 1
|
|
27
|
+
@success_count += 1 if success
|
|
28
|
+
@domains_used << domain_name if domain_name && !@domains_used.include?(domain_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def success_rate
|
|
32
|
+
return 0.0 if @usage_count.zero?
|
|
33
|
+
|
|
34
|
+
(@success_count.to_f / @usage_count).round(10)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def versatility
|
|
38
|
+
@domains_used.uniq.size
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_h
|
|
42
|
+
{
|
|
43
|
+
id: @id,
|
|
44
|
+
name: @name,
|
|
45
|
+
strategy_type: @strategy_type,
|
|
46
|
+
usage_count: @usage_count,
|
|
47
|
+
success_count: @success_count,
|
|
48
|
+
success_rate: success_rate,
|
|
49
|
+
versatility: versatility,
|
|
50
|
+
domains_used: @domains_used,
|
|
51
|
+
created_at: @created_at
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module MetaLearning
|
|
6
|
+
module Runners
|
|
7
|
+
module MetaLearning
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def create_learning_domain(name:, learning_rate: Helpers::Constants::DEFAULT_LEARNING_RATE,
|
|
12
|
+
related_domains: [], **)
|
|
13
|
+
result = engine.create_domain(name: name, learning_rate: learning_rate, related_domains: related_domains)
|
|
14
|
+
if result.is_a?(Hash) && result[:error]
|
|
15
|
+
Legion::Logging.warn "[meta_learning] create_domain failed: #{result[:error]}"
|
|
16
|
+
return result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Legion::Logging.debug "[meta_learning] domain created: #{result.name} id=#{result.id[0..7]}"
|
|
20
|
+
result.to_h
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def register_learning_strategy(name:, strategy_type:, **)
|
|
24
|
+
result = engine.create_strategy(name: name, strategy_type: strategy_type)
|
|
25
|
+
if result.is_a?(Hash) && result[:error]
|
|
26
|
+
Legion::Logging.warn "[meta_learning] create_strategy failed: #{result[:error]}"
|
|
27
|
+
return result
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Legion::Logging.debug "[meta_learning] strategy registered: #{result.name} type=#{result.strategy_type}"
|
|
31
|
+
result.to_h
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def record_learning_episode(domain_id:, success:, strategy_id: nil, **)
|
|
35
|
+
result = engine.record_episode(domain_id: domain_id, strategy_id: strategy_id, success: success)
|
|
36
|
+
if result.is_a?(Hash) && result[:error]
|
|
37
|
+
Legion::Logging.warn "[meta_learning] record_episode failed: #{result[:error]}"
|
|
38
|
+
return result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Legion::Logging.debug "[meta_learning] episode recorded domain=#{result[:domain_name]} " \
|
|
42
|
+
"success=#{success} proficiency=#{result[:proficiency].round(4)}"
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def recommend_learning_strategy(domain_id:, **)
|
|
47
|
+
result = engine.recommend_strategy(domain_id: domain_id)
|
|
48
|
+
Legion::Logging.debug "[meta_learning] strategy recommendation domain=#{domain_id[0..7]} " \
|
|
49
|
+
"recommendation=#{result[:recommendation]}"
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def check_transfer_learning(source_domain_id:, target_domain_id:, **)
|
|
54
|
+
result = engine.transfer_check(source_domain_id: source_domain_id, target_domain_id: target_domain_id)
|
|
55
|
+
Legion::Logging.debug "[meta_learning] transfer check eligible=#{result[:eligible]}"
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def apply_transfer_bonus(source_domain_id:, target_domain_id:, **)
|
|
60
|
+
result = engine.apply_transfer(source_domain_id: source_domain_id, target_domain_id: target_domain_id)
|
|
61
|
+
Legion::Logging.info "[meta_learning] transfer applied=#{result[:applied]}"
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def learning_domain_ranking(limit: 10, **)
|
|
66
|
+
ranking = engine.domain_ranking(limit: limit)
|
|
67
|
+
Legion::Logging.debug "[meta_learning] domain ranking returned #{ranking.size} domains"
|
|
68
|
+
{ ranking: ranking, count: ranking.size }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def learning_strategy_ranking(limit: 10, **)
|
|
72
|
+
ranking = engine.strategy_ranking(limit: limit)
|
|
73
|
+
Legion::Logging.debug "[meta_learning] strategy ranking returned #{ranking.size} strategies"
|
|
74
|
+
{ ranking: ranking, count: ranking.size }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def learning_curve_report(domain_id:, **)
|
|
78
|
+
result = engine.learning_curve(domain_id: domain_id)
|
|
79
|
+
if result.is_a?(Hash) && result[:error]
|
|
80
|
+
Legion::Logging.warn "[meta_learning] learning_curve failed: #{result[:error]}"
|
|
81
|
+
return result
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
Legion::Logging.debug "[meta_learning] learning curve domain=#{result[:domain]} " \
|
|
85
|
+
"episodes=#{result[:curve].size}"
|
|
86
|
+
result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update_meta_learning(**)
|
|
90
|
+
adapt_result = engine.adapt_rates
|
|
91
|
+
prune_result = engine.prune_stale_domains
|
|
92
|
+
stats = engine.to_h
|
|
93
|
+
Legion::Logging.info "[meta_learning] update: adapted=#{adapt_result[:count]} " \
|
|
94
|
+
"pruned=#{prune_result[:pruned]} domains=#{stats[:domain_count]}"
|
|
95
|
+
{ adapt: adapt_result, prune: prune_result, stats: stats }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def meta_learning_stats(**)
|
|
99
|
+
stats = engine.to_h
|
|
100
|
+
Legion::Logging.debug "[meta_learning] stats domains=#{stats[:domain_count]} " \
|
|
101
|
+
"strategies=#{stats[:strategy_count]} efficiency=#{stats[:overall_efficiency]}"
|
|
102
|
+
stats
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def engine
|
|
108
|
+
@engine ||= Helpers::MetaLearningEngine.new
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/meta_learning/version'
|
|
4
|
+
require 'legion/extensions/meta_learning/helpers/constants'
|
|
5
|
+
require 'legion/extensions/meta_learning/helpers/learning_domain'
|
|
6
|
+
require 'legion/extensions/meta_learning/helpers/strategy'
|
|
7
|
+
require 'legion/extensions/meta_learning/helpers/meta_learning_engine'
|
|
8
|
+
require 'legion/extensions/meta_learning/runners/meta_learning'
|
|
9
|
+
require 'legion/extensions/meta_learning/client'
|
|
10
|
+
|
|
11
|
+
module Legion
|
|
12
|
+
module Extensions
|
|
13
|
+
module MetaLearning
|
|
14
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/meta_learning/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::MetaLearning::Client do
|
|
6
|
+
it 'responds to all runner methods' do
|
|
7
|
+
client = described_class.new
|
|
8
|
+
expect(client).to respond_to(:create_learning_domain)
|
|
9
|
+
expect(client).to respond_to(:register_learning_strategy)
|
|
10
|
+
expect(client).to respond_to(:record_learning_episode)
|
|
11
|
+
expect(client).to respond_to(:recommend_learning_strategy)
|
|
12
|
+
expect(client).to respond_to(:check_transfer_learning)
|
|
13
|
+
expect(client).to respond_to(:apply_transfer_bonus)
|
|
14
|
+
expect(client).to respond_to(:learning_domain_ranking)
|
|
15
|
+
expect(client).to respond_to(:learning_strategy_ranking)
|
|
16
|
+
expect(client).to respond_to(:learning_curve_report)
|
|
17
|
+
expect(client).to respond_to(:update_meta_learning)
|
|
18
|
+
expect(client).to respond_to(:meta_learning_stats)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'maintains isolated state per instance' do
|
|
22
|
+
c1 = described_class.new
|
|
23
|
+
c2 = described_class.new
|
|
24
|
+
c1.create_learning_domain(name: 'ruby')
|
|
25
|
+
expect(c2.meta_learning_stats[:domain_count]).to eq(0)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::MetaLearning::Helpers::Constants do
|
|
4
|
+
it 'defines MAX_DOMAINS as 100' do
|
|
5
|
+
expect(described_module::MAX_DOMAINS).to eq(100)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it 'defines MAX_STRATEGIES as 50' do
|
|
9
|
+
expect(described_module::MAX_STRATEGIES).to eq(50)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'defines MAX_EPISODES as 1000' do
|
|
13
|
+
expect(described_module::MAX_EPISODES).to eq(1000)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'defines DEFAULT_LEARNING_RATE as 0.1' do
|
|
17
|
+
expect(described_module::DEFAULT_LEARNING_RATE).to eq(0.1)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'defines STRATEGY_TYPES as an array of symbols' do
|
|
21
|
+
expect(described_module::STRATEGY_TYPES).to be_an(Array)
|
|
22
|
+
expect(described_module::STRATEGY_TYPES).to all(be_a(Symbol))
|
|
23
|
+
expect(described_module::STRATEGY_TYPES).to include(:repetition, :elaboration, :retrieval_practice)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'defines PROFICIENCY_LABELS covering full 0..1 range' do
|
|
27
|
+
labels = [0.0, 0.1, 0.25, 0.45, 0.65, 0.85].map do |v|
|
|
28
|
+
described_module::PROFICIENCY_LABELS.find { |range, _| range.cover?(v) }&.last
|
|
29
|
+
end
|
|
30
|
+
expect(labels).to all(be_a(Symbol))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'defines EFFICIENCY_LABELS covering full 0..1 range' do
|
|
34
|
+
labels = [0.0, 0.1, 0.25, 0.45, 0.65, 0.85].map do |v|
|
|
35
|
+
described_module::EFFICIENCY_LABELS.find { |range, _| range.cover?(v) }&.last
|
|
36
|
+
end
|
|
37
|
+
expect(labels).to all(be_a(Symbol))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def described_module
|
|
41
|
+
Legion::Extensions::MetaLearning::Helpers::Constants
|
|
42
|
+
end
|
|
43
|
+
end
|