lex-transfer-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-transfer-learning.gemspec +29 -0
- data/lib/legion/extensions/transfer_learning/client.rb +24 -0
- data/lib/legion/extensions/transfer_learning/helpers/constants.rb +30 -0
- data/lib/legion/extensions/transfer_learning/helpers/domain_knowledge.rb +70 -0
- data/lib/legion/extensions/transfer_learning/helpers/transfer_engine.rb +184 -0
- data/lib/legion/extensions/transfer_learning/runners/transfer_learning.rb +73 -0
- data/lib/legion/extensions/transfer_learning/version.rb +9 -0
- data/lib/legion/extensions/transfer_learning.rb +15 -0
- data/spec/legion/extensions/transfer_learning/client_spec.rb +18 -0
- data/spec/legion/extensions/transfer_learning/helpers/constants_spec.rb +41 -0
- data/spec/legion/extensions/transfer_learning/helpers/domain_knowledge_spec.rb +134 -0
- data/spec/legion/extensions/transfer_learning/helpers/transfer_engine_spec.rb +299 -0
- data/spec/legion/extensions/transfer_learning/runners/transfer_learning_spec.rb +144 -0
- data/spec/spec_helper.rb +20 -0
- metadata +75 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f75d8ed4ba9863fb0509ade3cc4f3c1ab7e4cdfdc61c63a39377c01929ca2bc1
|
|
4
|
+
data.tar.gz: 6a6fb03022582a040beebbffa0d28411788a9498091628366330ac7b538c90e0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 117eb74b067523792cef8196ebec35d595b0762301c156ceb025ccf773cda5a99a3eec98df59e607dbbee7a98e6ec182f6876e4c6d39a4374fee01ddefb1d5a1
|
|
7
|
+
data.tar.gz: fb49e00c94a694769812012d05877f786377fee2c9db7c9951365e6b9cfd81a886cf26391bd17e483afc4766e54b462d8d4504700577213cfbf579765906f69b
|
data/Gemfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/legion/extensions/transfer_learning/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'lex-transfer-learning'
|
|
7
|
+
spec.version = Legion::Extensions::TransferLearning::VERSION
|
|
8
|
+
spec.authors = ['Esity']
|
|
9
|
+
spec.email = ['matthewdiverson@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'LEX Transfer Learning'
|
|
12
|
+
spec.description = 'Domain knowledge transfer modeling for brain-modeled agentic AI'
|
|
13
|
+
spec.homepage = 'https://github.com/LegionIO/lex-transfer-learning'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-transfer-learning'
|
|
19
|
+
spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-transfer-learning'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-transfer-learning'
|
|
21
|
+
spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-transfer-learning/issues'
|
|
22
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
23
|
+
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
Dir.glob('{lib,spec}/**/*') + %w[lex-transfer-learning.gemspec Gemfile]
|
|
26
|
+
end
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
spec.add_development_dependency 'legion-gaia'
|
|
29
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/transfer_learning/helpers/constants'
|
|
4
|
+
require 'legion/extensions/transfer_learning/helpers/domain_knowledge'
|
|
5
|
+
require 'legion/extensions/transfer_learning/helpers/transfer_engine'
|
|
6
|
+
require 'legion/extensions/transfer_learning/runners/transfer_learning'
|
|
7
|
+
|
|
8
|
+
module Legion
|
|
9
|
+
module Extensions
|
|
10
|
+
module TransferLearning
|
|
11
|
+
class Client
|
|
12
|
+
include Runners::TransferLearning
|
|
13
|
+
|
|
14
|
+
def initialize(**)
|
|
15
|
+
@transfer_engine = Helpers::TransferEngine.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :transfer_engine
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module TransferLearning
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
MAX_DOMAINS = 200
|
|
9
|
+
POSITIVE_TRANSFER_THRESHOLD = 0.6
|
|
10
|
+
NEGATIVE_TRANSFER_THRESHOLD = 0.3
|
|
11
|
+
TRANSFER_BOOST = 0.15
|
|
12
|
+
INTERFERENCE_PENALTY = 0.1
|
|
13
|
+
|
|
14
|
+
TRANSFER_LABELS = {
|
|
15
|
+
positive: 'positive',
|
|
16
|
+
neutral: 'neutral',
|
|
17
|
+
negative: 'negative',
|
|
18
|
+
interference: 'interference'
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
DISTANCE_LABELS = {
|
|
22
|
+
near: 'near',
|
|
23
|
+
moderate: 'moderate',
|
|
24
|
+
far: 'far'
|
|
25
|
+
}.freeze
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Extensions
|
|
7
|
+
module TransferLearning
|
|
8
|
+
module Helpers
|
|
9
|
+
class DomainKnowledge
|
|
10
|
+
attr_reader :id, :domain, :proficiency, :learn_count, :transfer_count
|
|
11
|
+
|
|
12
|
+
PROFICIENCY_LABELS = [
|
|
13
|
+
[0.0, 0.2, 'novice'],
|
|
14
|
+
[0.2, 0.4, 'beginner'],
|
|
15
|
+
[0.4, 0.6, 'intermediate'],
|
|
16
|
+
[0.6, 0.8, 'advanced'],
|
|
17
|
+
[0.8, 1.01, 'expert']
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(domain:)
|
|
21
|
+
@id = SecureRandom.uuid
|
|
22
|
+
@domain = domain
|
|
23
|
+
@proficiency = 0.0
|
|
24
|
+
@learn_count = 0
|
|
25
|
+
@transfer_count = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def learn!(amount:)
|
|
29
|
+
@proficiency = (@proficiency + amount).clamp(0.0, 1.0).round(10)
|
|
30
|
+
@learn_count += 1
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def record_transfer!
|
|
35
|
+
@transfer_count += 1
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def apply_boost!(amount)
|
|
40
|
+
@proficiency = (@proficiency + amount).clamp(0.0, 1.0).round(10)
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_penalty!(amount)
|
|
45
|
+
@proficiency = (@proficiency - amount).clamp(0.0, 1.0).round(10)
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def proficiency_label
|
|
50
|
+
PROFICIENCY_LABELS.each do |low, high, label|
|
|
51
|
+
return label if @proficiency >= low && @proficiency < high
|
|
52
|
+
end
|
|
53
|
+
'novice'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
id: @id,
|
|
59
|
+
domain: @domain,
|
|
60
|
+
proficiency: @proficiency.round(10),
|
|
61
|
+
proficiency_label: proficiency_label,
|
|
62
|
+
learn_count: @learn_count,
|
|
63
|
+
transfer_count: @transfer_count
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module TransferLearning
|
|
6
|
+
module Helpers
|
|
7
|
+
class TransferEngine
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :domains, :similarities, :transfer_history
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@domains = {}
|
|
14
|
+
@similarities = {}
|
|
15
|
+
@transfer_history = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def set_similarity(domain_a:, domain_b:, similarity:)
|
|
19
|
+
sim = similarity.clamp(0.0, 1.0).round(10)
|
|
20
|
+
@similarities[similarity_key(domain_a, domain_b)] = sim
|
|
21
|
+
sim
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def learn_domain(domain:, amount:)
|
|
25
|
+
check_domain_limit!
|
|
26
|
+
entry = get_or_create_domain(domain)
|
|
27
|
+
entry.learn!(amount: amount.clamp(0.0, 1.0))
|
|
28
|
+
entry.to_h
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def attempt_transfer(from_domain:, to_domain:)
|
|
32
|
+
source = @domains[from_domain]
|
|
33
|
+
return { status: :source_not_found, from_domain: from_domain } unless source
|
|
34
|
+
|
|
35
|
+
check_domain_limit!
|
|
36
|
+
target = get_or_create_domain(to_domain)
|
|
37
|
+
sim = similarity_between(from_domain, to_domain)
|
|
38
|
+
type = transfer_type(sim)
|
|
39
|
+
distance = transfer_distance(sim)
|
|
40
|
+
effect = apply_transfer_effect!(target, source, type)
|
|
41
|
+
source.record_transfer!
|
|
42
|
+
record_history!(from_domain, to_domain, sim, type, distance, effect)
|
|
43
|
+
build_transfer_result(from_domain, to_domain, sim, type, distance, effect, target)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def transfer_effectiveness(from_domain:, to_domain:)
|
|
47
|
+
sim = similarity_between(from_domain, to_domain)
|
|
48
|
+
build_effectiveness(from_domain, to_domain, sim, transfer_type(sim),
|
|
49
|
+
@domains[from_domain], @domains[to_domain])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def most_transferable(target_domain:, limit: 5)
|
|
53
|
+
candidates = candidate_rows(target_domain)
|
|
54
|
+
candidates.select { |r| r[:type] == :positive }
|
|
55
|
+
.sort_by { |r| -r[:similarity] }
|
|
56
|
+
.first(limit)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def interference_risks(target_domain:)
|
|
60
|
+
candidate_rows(target_domain)
|
|
61
|
+
.select { |r| r[:type] == :interference }
|
|
62
|
+
.sort_by { |r| -r[:similarity] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def transfer_report
|
|
66
|
+
{
|
|
67
|
+
total_transfers: @transfer_history.size,
|
|
68
|
+
positive_transfers: @transfer_history.count { |h| h[:type] == :positive },
|
|
69
|
+
negative_transfers: @transfer_history.count { |h| h[:type] == :negative },
|
|
70
|
+
neutral_transfers: @transfer_history.count { |h| h[:type] == :neutral },
|
|
71
|
+
interference_events: @transfer_history.count { |h| h[:type] == :interference },
|
|
72
|
+
domain_count: @domains.size,
|
|
73
|
+
similarity_pairs: @similarities.size
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def to_h
|
|
78
|
+
{
|
|
79
|
+
domains: @domains.transform_values(&:to_h),
|
|
80
|
+
similarities: @similarities.dup,
|
|
81
|
+
transfer_history: @transfer_history.dup,
|
|
82
|
+
report: transfer_report
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def get_or_create_domain(name)
|
|
89
|
+
@domains[name] ||= DomainKnowledge.new(domain: name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def similarity_key(domain_a, domain_b)
|
|
93
|
+
[domain_a, domain_b].sort.join(':')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def similarity_between(domain_a, domain_b)
|
|
97
|
+
@similarities[similarity_key(domain_a, domain_b)] || 0.0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def transfer_type(similarity)
|
|
101
|
+
if similarity >= POSITIVE_TRANSFER_THRESHOLD then :positive
|
|
102
|
+
elsif similarity >= NEGATIVE_TRANSFER_THRESHOLD then :interference
|
|
103
|
+
elsif similarity > 0.0 then :negative
|
|
104
|
+
else :neutral
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def transfer_distance(similarity)
|
|
109
|
+
if similarity >= POSITIVE_TRANSFER_THRESHOLD then :near
|
|
110
|
+
elsif similarity >= NEGATIVE_TRANSFER_THRESHOLD then :moderate
|
|
111
|
+
else :far
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def apply_transfer_effect!(target, source, type)
|
|
116
|
+
case type
|
|
117
|
+
when :positive
|
|
118
|
+
scaled = (source.proficiency * TRANSFER_BOOST).round(10)
|
|
119
|
+
target.apply_boost!(scaled)
|
|
120
|
+
scaled
|
|
121
|
+
when :interference
|
|
122
|
+
penalty = (source.proficiency * INTERFERENCE_PENALTY).round(10)
|
|
123
|
+
target.apply_penalty!(penalty)
|
|
124
|
+
-penalty
|
|
125
|
+
else
|
|
126
|
+
0.0
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def record_history!(from_domain, to_domain, sim, type, distance, effect)
|
|
131
|
+
@transfer_history << {
|
|
132
|
+
from_domain: from_domain,
|
|
133
|
+
to_domain: to_domain,
|
|
134
|
+
similarity: sim.round(10),
|
|
135
|
+
type: type,
|
|
136
|
+
distance: distance,
|
|
137
|
+
effect: effect,
|
|
138
|
+
at: Time.now.utc
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def check_domain_limit!
|
|
143
|
+
raise "domain limit of #{MAX_DOMAINS} reached" if @domains.size >= MAX_DOMAINS
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def candidate_rows(target_domain)
|
|
147
|
+
@domains.reject { |name, _| name == target_domain }.map do |name, entry|
|
|
148
|
+
sim = similarity_between(name, target_domain)
|
|
149
|
+
type = transfer_type(sim)
|
|
150
|
+
{ domain: name, proficiency: entry.proficiency.round(10), similarity: sim.round(10), type: type }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_transfer_result(from_domain, to_domain, sim, type, distance, effect, target)
|
|
155
|
+
{
|
|
156
|
+
status: :ok,
|
|
157
|
+
from_domain: from_domain,
|
|
158
|
+
to_domain: to_domain,
|
|
159
|
+
similarity: sim.round(10),
|
|
160
|
+
type: type,
|
|
161
|
+
distance: distance,
|
|
162
|
+
effect: effect,
|
|
163
|
+
proficiency: target.proficiency.round(10)
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def build_effectiveness(from_domain, to_domain, sim, type, source, target)
|
|
168
|
+
{
|
|
169
|
+
from_domain: from_domain,
|
|
170
|
+
to_domain: to_domain,
|
|
171
|
+
similarity: sim.round(10),
|
|
172
|
+
type: type,
|
|
173
|
+
type_label: TRANSFER_LABELS[type],
|
|
174
|
+
distance: transfer_distance(sim),
|
|
175
|
+
distance_label: DISTANCE_LABELS[transfer_distance(sim)],
|
|
176
|
+
source_proficiency: source&.proficiency&.round(10) || 0.0,
|
|
177
|
+
target_proficiency: target&.proficiency&.round(10) || 0.0
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module TransferLearning
|
|
6
|
+
module Runners
|
|
7
|
+
module TransferLearning
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def learn_domain(domain:, amount:, **)
|
|
12
|
+
result = transfer_engine.learn_domain(domain: domain, amount: amount)
|
|
13
|
+
Legion::Logging.info "[transfer_learning] learn: domain=#{domain} amount=#{amount} proficiency=#{result[:proficiency]}"
|
|
14
|
+
result
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def attempt_transfer(from_domain:, to_domain:, **)
|
|
18
|
+
result = transfer_engine.attempt_transfer(from_domain: from_domain, to_domain: to_domain)
|
|
19
|
+
Legion::Logging.info "[transfer_learning] transfer: from=#{from_domain} to=#{to_domain} type=#{result[:type]} effect=#{result[:effect]}"
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_similarity(domain_a:, domain_b:, similarity:, **)
|
|
24
|
+
sim = transfer_engine.set_similarity(domain_a: domain_a, domain_b: domain_b, similarity: similarity)
|
|
25
|
+
Legion::Logging.debug "[transfer_learning] similarity set: #{domain_a}<->#{domain_b} similarity=#{sim}"
|
|
26
|
+
{ domain_a: domain_a, domain_b: domain_b, similarity: sim }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def transfer_effectiveness(from_domain:, to_domain:, **)
|
|
30
|
+
result = transfer_engine.transfer_effectiveness(from_domain: from_domain, to_domain: to_domain)
|
|
31
|
+
Legion::Logging.debug "[transfer_learning] effectiveness: from=#{from_domain} to=#{to_domain} type=#{result[:type]}"
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def most_transferable(target_domain:, limit: 5, **)
|
|
36
|
+
candidates = transfer_engine.most_transferable(target_domain: target_domain, limit: limit)
|
|
37
|
+
Legion::Logging.debug "[transfer_learning] most_transferable: target=#{target_domain} found=#{candidates.size}"
|
|
38
|
+
{ target_domain: target_domain, candidates: candidates, count: candidates.size }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def interference_risks(target_domain:, **)
|
|
42
|
+
risks = transfer_engine.interference_risks(target_domain: target_domain)
|
|
43
|
+
Legion::Logging.debug "[transfer_learning] interference_risks: target=#{target_domain} risks=#{risks.size}"
|
|
44
|
+
{ target_domain: target_domain, risks: risks, count: risks.size }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def transfer_report(**)
|
|
48
|
+
report = transfer_engine.transfer_report
|
|
49
|
+
Legion::Logging.debug "[transfer_learning] report: domains=#{report[:domain_count]} transfers=#{report[:total_transfers]}"
|
|
50
|
+
report
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_domain(domain:, **)
|
|
54
|
+
entry = transfer_engine.domains[domain]
|
|
55
|
+
if entry
|
|
56
|
+
Legion::Logging.debug "[transfer_learning] get_domain: domain=#{domain} proficiency=#{entry.proficiency}"
|
|
57
|
+
{ found: true, domain: entry.to_h }
|
|
58
|
+
else
|
|
59
|
+
Legion::Logging.debug "[transfer_learning] get_domain: domain=#{domain} not found"
|
|
60
|
+
{ found: false, domain: domain }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def transfer_engine
|
|
67
|
+
@transfer_engine ||= Helpers::TransferEngine.new
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/transfer_learning/version'
|
|
4
|
+
require 'legion/extensions/transfer_learning/helpers/constants'
|
|
5
|
+
require 'legion/extensions/transfer_learning/helpers/domain_knowledge'
|
|
6
|
+
require 'legion/extensions/transfer_learning/helpers/transfer_engine'
|
|
7
|
+
require 'legion/extensions/transfer_learning/runners/transfer_learning'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module TransferLearning
|
|
12
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/transfer_learning/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::TransferLearning::Client do
|
|
6
|
+
let(:client) { described_class.new }
|
|
7
|
+
|
|
8
|
+
it 'responds to all runner methods' do
|
|
9
|
+
expect(client).to respond_to(:learn_domain)
|
|
10
|
+
expect(client).to respond_to(:attempt_transfer)
|
|
11
|
+
expect(client).to respond_to(:set_similarity)
|
|
12
|
+
expect(client).to respond_to(:transfer_effectiveness)
|
|
13
|
+
expect(client).to respond_to(:most_transferable)
|
|
14
|
+
expect(client).to respond_to(:interference_risks)
|
|
15
|
+
expect(client).to respond_to(:transfer_report)
|
|
16
|
+
expect(client).to respond_to(:get_domain)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::TransferLearning::Helpers::Constants do
|
|
4
|
+
subject(:mod) { described_class }
|
|
5
|
+
|
|
6
|
+
it 'defines MAX_DOMAINS as 200' do
|
|
7
|
+
expect(mod::MAX_DOMAINS).to eq(200)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it 'defines POSITIVE_TRANSFER_THRESHOLD as 0.6' do
|
|
11
|
+
expect(mod::POSITIVE_TRANSFER_THRESHOLD).to eq(0.6)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'defines NEGATIVE_TRANSFER_THRESHOLD as 0.3' do
|
|
15
|
+
expect(mod::NEGATIVE_TRANSFER_THRESHOLD).to eq(0.3)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'defines TRANSFER_BOOST as 0.15' do
|
|
19
|
+
expect(mod::TRANSFER_BOOST).to eq(0.15)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'defines INTERFERENCE_PENALTY as 0.1' do
|
|
23
|
+
expect(mod::INTERFERENCE_PENALTY).to eq(0.1)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'defines all four TRANSFER_LABELS' do
|
|
27
|
+
expect(mod::TRANSFER_LABELS.keys).to contain_exactly(:positive, :neutral, :negative, :interference)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'defines all three DISTANCE_LABELS' do
|
|
31
|
+
expect(mod::DISTANCE_LABELS.keys).to contain_exactly(:near, :moderate, :far)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'freezes TRANSFER_LABELS' do
|
|
35
|
+
expect(mod::TRANSFER_LABELS).to be_frozen
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'freezes DISTANCE_LABELS' do
|
|
39
|
+
expect(mod::DISTANCE_LABELS).to be_frozen
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::TransferLearning::Helpers::DomainKnowledge do
|
|
4
|
+
subject(:dk) { described_class.new(domain: :ruby) }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'assigns a uuid id' do
|
|
8
|
+
expect(dk.id).to match(/\A[0-9a-f-]{36}\z/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'sets domain' do
|
|
12
|
+
expect(dk.domain).to eq(:ruby)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'starts proficiency at 0.0' do
|
|
16
|
+
expect(dk.proficiency).to eq(0.0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'starts learn_count at 0' do
|
|
20
|
+
expect(dk.learn_count).to eq(0)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'starts transfer_count at 0' do
|
|
24
|
+
expect(dk.transfer_count).to eq(0)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#learn!' do
|
|
29
|
+
it 'increases proficiency by amount' do
|
|
30
|
+
dk.learn!(amount: 0.3)
|
|
31
|
+
expect(dk.proficiency).to eq(0.3)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'increments learn_count' do
|
|
35
|
+
dk.learn!(amount: 0.1)
|
|
36
|
+
expect(dk.learn_count).to eq(1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'clamps proficiency at 1.0' do
|
|
40
|
+
dk.learn!(amount: 1.5)
|
|
41
|
+
expect(dk.proficiency).to eq(1.0)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'clamps proficiency at 0.0 for negative amount' do
|
|
45
|
+
dk.learn!(amount: -0.5)
|
|
46
|
+
expect(dk.proficiency).to eq(0.0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns self for chaining' do
|
|
50
|
+
expect(dk.learn!(amount: 0.1)).to be(dk)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#record_transfer!' do
|
|
55
|
+
it 'increments transfer_count' do
|
|
56
|
+
dk.record_transfer!
|
|
57
|
+
expect(dk.transfer_count).to eq(1)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns self' do
|
|
61
|
+
expect(dk.record_transfer!).to be(dk)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe '#apply_boost!' do
|
|
66
|
+
it 'increases proficiency' do
|
|
67
|
+
dk.learn!(amount: 0.5)
|
|
68
|
+
dk.apply_boost!(0.2)
|
|
69
|
+
expect(dk.proficiency).to be_within(0.0001).of(0.7)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'clamps at 1.0' do
|
|
73
|
+
dk.learn!(amount: 0.9)
|
|
74
|
+
dk.apply_boost!(0.5)
|
|
75
|
+
expect(dk.proficiency).to eq(1.0)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe '#apply_penalty!' do
|
|
80
|
+
it 'decreases proficiency' do
|
|
81
|
+
dk.learn!(amount: 0.5)
|
|
82
|
+
dk.apply_penalty!(0.2)
|
|
83
|
+
expect(dk.proficiency).to be_within(0.0001).of(0.3)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'clamps at 0.0' do
|
|
87
|
+
dk.apply_penalty!(0.5)
|
|
88
|
+
expect(dk.proficiency).to eq(0.0)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#proficiency_label' do
|
|
93
|
+
it 'returns novice for 0.0' do
|
|
94
|
+
expect(dk.proficiency_label).to eq('novice')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns beginner for 0.3' do
|
|
98
|
+
dk.learn!(amount: 0.3)
|
|
99
|
+
expect(dk.proficiency_label).to eq('beginner')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'returns intermediate for 0.5' do
|
|
103
|
+
dk.learn!(amount: 0.5)
|
|
104
|
+
expect(dk.proficiency_label).to eq('intermediate')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'returns advanced for 0.7' do
|
|
108
|
+
dk.learn!(amount: 0.7)
|
|
109
|
+
expect(dk.proficiency_label).to eq('advanced')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'returns expert for 0.9' do
|
|
113
|
+
dk.learn!(amount: 0.9)
|
|
114
|
+
expect(dk.proficiency_label).to eq('expert')
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '#to_h' do
|
|
119
|
+
it 'returns a hash with expected keys' do
|
|
120
|
+
hash = dk.to_h
|
|
121
|
+
expect(hash.keys).to contain_exactly(:id, :domain, :proficiency, :proficiency_label, :learn_count, :transfer_count)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'reflects current proficiency' do
|
|
125
|
+
dk.learn!(amount: 0.4)
|
|
126
|
+
expect(dk.to_h[:proficiency]).to eq(0.4)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'includes proficiency_label' do
|
|
130
|
+
dk.learn!(amount: 0.4)
|
|
131
|
+
expect(dk.to_h[:proficiency_label]).to eq('intermediate')
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Legion::Extensions::TransferLearning::Helpers::TransferEngine do
|
|
4
|
+
subject(:engine) { described_class.new }
|
|
5
|
+
|
|
6
|
+
describe '#initialize' do
|
|
7
|
+
it 'starts with empty domains' do
|
|
8
|
+
expect(engine.domains).to be_empty
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'starts with empty similarities' do
|
|
12
|
+
expect(engine.similarities).to be_empty
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'starts with empty transfer_history' do
|
|
16
|
+
expect(engine.transfer_history).to be_empty
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe '#set_similarity' do
|
|
21
|
+
it 'stores similarity between two domains' do
|
|
22
|
+
engine.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.7)
|
|
23
|
+
expect(engine.similarities['python:ruby']).to eq(0.7)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'is commutative (same key regardless of order)' do
|
|
27
|
+
engine.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.7)
|
|
28
|
+
engine.set_similarity(domain_a: :python, domain_b: :ruby, similarity: 0.8)
|
|
29
|
+
key = 'python:ruby'
|
|
30
|
+
expect(engine.similarities[key]).to eq(0.8)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'clamps similarity to 0.0..1.0' do
|
|
34
|
+
engine.set_similarity(domain_a: :a, domain_b: :b, similarity: 1.5)
|
|
35
|
+
expect(engine.similarities['a:b']).to eq(1.0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'clamps negative similarity to 0.0' do
|
|
39
|
+
engine.set_similarity(domain_a: :a, domain_b: :b, similarity: -0.3)
|
|
40
|
+
expect(engine.similarities['a:b']).to eq(0.0)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'returns the clamped similarity' do
|
|
44
|
+
result = engine.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.5)
|
|
45
|
+
expect(result).to eq(0.5)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe '#learn_domain' do
|
|
50
|
+
it 'creates and returns domain knowledge' do
|
|
51
|
+
result = engine.learn_domain(domain: :ruby, amount: 0.4)
|
|
52
|
+
expect(result[:domain]).to eq(:ruby)
|
|
53
|
+
expect(result[:proficiency]).to eq(0.4)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'accumulates proficiency on repeated calls' do
|
|
57
|
+
engine.learn_domain(domain: :ruby, amount: 0.3)
|
|
58
|
+
result = engine.learn_domain(domain: :ruby, amount: 0.3)
|
|
59
|
+
expect(result[:proficiency]).to be_within(0.0001).of(0.6)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'increments learn_count' do
|
|
63
|
+
engine.learn_domain(domain: :ruby, amount: 0.1)
|
|
64
|
+
result = engine.learn_domain(domain: :ruby, amount: 0.1)
|
|
65
|
+
expect(result[:learn_count]).to eq(2)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'clamps amount before applying' do
|
|
69
|
+
result = engine.learn_domain(domain: :ruby, amount: 5.0)
|
|
70
|
+
expect(result[:proficiency]).to eq(1.0)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe '#attempt_transfer' do
|
|
75
|
+
before do
|
|
76
|
+
engine.learn_domain(domain: :ruby, amount: 0.8)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'returns source_not_found when source domain does not exist' do
|
|
80
|
+
result = engine.attempt_transfer(from_domain: :unknown, to_domain: :python)
|
|
81
|
+
expect(result[:status]).to eq(:source_not_found)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context 'with positive similarity' do
|
|
85
|
+
before { engine.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.8) }
|
|
86
|
+
|
|
87
|
+
it 'returns status :ok' do
|
|
88
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
89
|
+
expect(result[:status]).to eq(:ok)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'classifies as positive transfer' do
|
|
93
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
94
|
+
expect(result[:type]).to eq(:positive)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'classifies distance as near' do
|
|
98
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
99
|
+
expect(result[:distance]).to eq(:near)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'boosts target proficiency' do
|
|
103
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
104
|
+
expect(result[:effect]).to be > 0
|
|
105
|
+
expect(result[:proficiency]).to be > 0
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'increments source transfer_count' do
|
|
109
|
+
engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
110
|
+
expect(engine.domains[:ruby].transfer_count).to eq(1)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'records transfer in history' do
|
|
114
|
+
engine.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
115
|
+
expect(engine.transfer_history.size).to eq(1)
|
|
116
|
+
expect(engine.transfer_history.first[:type]).to eq(:positive)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
context 'with interference similarity (0.3..0.6)' do
|
|
121
|
+
before { engine.set_similarity(domain_a: :ruby, domain_b: :cobol, similarity: 0.4) }
|
|
122
|
+
|
|
123
|
+
it 'classifies as interference' do
|
|
124
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :cobol)
|
|
125
|
+
expect(result[:type]).to eq(:interference)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'applies penalty to target' do
|
|
129
|
+
engine.learn_domain(domain: :cobol, amount: 0.5)
|
|
130
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :cobol)
|
|
131
|
+
expect(result[:effect]).to be < 0
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'classifies distance as moderate' do
|
|
135
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :cobol)
|
|
136
|
+
expect(result[:distance]).to eq(:moderate)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
context 'with low similarity (0.0..0.3)' do
|
|
141
|
+
before { engine.set_similarity(domain_a: :ruby, domain_b: :sql, similarity: 0.1) }
|
|
142
|
+
|
|
143
|
+
it 'classifies as negative transfer' do
|
|
144
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :sql)
|
|
145
|
+
expect(result[:type]).to eq(:negative)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'applies zero effect (negative = no net change)' do
|
|
149
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :sql)
|
|
150
|
+
expect(result[:effect]).to eq(0.0)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'classifies distance as far' do
|
|
154
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :sql)
|
|
155
|
+
expect(result[:distance]).to eq(:far)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'with zero similarity (unknown domains)' do
|
|
160
|
+
it 'classifies as neutral transfer' do
|
|
161
|
+
result = engine.attempt_transfer(from_domain: :ruby, to_domain: :haskell)
|
|
162
|
+
expect(result[:type]).to eq(:neutral)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
describe '#transfer_effectiveness' do
|
|
168
|
+
before do
|
|
169
|
+
engine.learn_domain(domain: :ruby, amount: 0.7)
|
|
170
|
+
engine.learn_domain(domain: :python, amount: 0.3)
|
|
171
|
+
engine.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.75)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'returns effectiveness hash' do
|
|
175
|
+
result = engine.transfer_effectiveness(from_domain: :ruby, to_domain: :python)
|
|
176
|
+
expect(result[:type]).to eq(:positive)
|
|
177
|
+
expect(result[:type_label]).to eq('positive')
|
|
178
|
+
expect(result[:distance]).to eq(:near)
|
|
179
|
+
expect(result[:similarity]).to eq(0.75)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'returns source and target proficiency' do
|
|
183
|
+
result = engine.transfer_effectiveness(from_domain: :ruby, to_domain: :python)
|
|
184
|
+
expect(result[:source_proficiency]).to eq(0.7)
|
|
185
|
+
expect(result[:target_proficiency]).to eq(0.3)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'handles unknown domains gracefully' do
|
|
189
|
+
result = engine.transfer_effectiveness(from_domain: :unknown_a, to_domain: :unknown_b)
|
|
190
|
+
expect(result[:source_proficiency]).to eq(0.0)
|
|
191
|
+
expect(result[:target_proficiency]).to eq(0.0)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
describe '#most_transferable' do
|
|
196
|
+
before do
|
|
197
|
+
engine.learn_domain(domain: :target, amount: 0.2)
|
|
198
|
+
engine.learn_domain(domain: :high_sim, amount: 0.8)
|
|
199
|
+
engine.learn_domain(domain: :low_sim, amount: 0.8)
|
|
200
|
+
engine.learn_domain(domain: :no_sim, amount: 0.8)
|
|
201
|
+
engine.set_similarity(domain_a: :high_sim, domain_b: :target, similarity: 0.9)
|
|
202
|
+
engine.set_similarity(domain_a: :low_sim, domain_b: :target, similarity: 0.1)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'returns only positively transferable domains' do
|
|
206
|
+
result = engine.most_transferable(target_domain: :target)
|
|
207
|
+
expect(result.map { |r| r[:domain] }).to include(:high_sim)
|
|
208
|
+
expect(result.map { |r| r[:domain] }).not_to include(:low_sim)
|
|
209
|
+
expect(result.map { |r| r[:domain] }).not_to include(:no_sim)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'sorts by similarity descending' do
|
|
213
|
+
engine.learn_domain(domain: :mid_sim, amount: 0.8)
|
|
214
|
+
engine.set_similarity(domain_a: :mid_sim, domain_b: :target, similarity: 0.7)
|
|
215
|
+
result = engine.most_transferable(target_domain: :target)
|
|
216
|
+
sims = result.map { |r| r[:similarity] }
|
|
217
|
+
expect(sims).to eq(sims.sort.reverse)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'respects the limit parameter' do
|
|
221
|
+
5.times do |i|
|
|
222
|
+
name = :"domain_#{i}"
|
|
223
|
+
engine.learn_domain(domain: name, amount: 0.5)
|
|
224
|
+
engine.set_similarity(domain_a: name, domain_b: :target, similarity: 0.7 + (i * 0.01))
|
|
225
|
+
end
|
|
226
|
+
result = engine.most_transferable(target_domain: :target, limit: 3)
|
|
227
|
+
expect(result.size).to be <= 3
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
describe '#interference_risks' do
|
|
232
|
+
before do
|
|
233
|
+
engine.learn_domain(domain: :target, amount: 0.3)
|
|
234
|
+
engine.learn_domain(domain: :risky, amount: 0.8)
|
|
235
|
+
engine.learn_domain(domain: :safe, amount: 0.8)
|
|
236
|
+
engine.set_similarity(domain_a: :risky, domain_b: :target, similarity: 0.45)
|
|
237
|
+
engine.set_similarity(domain_a: :safe, domain_b: :target, similarity: 0.8)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'returns only interference-type domains' do
|
|
241
|
+
result = engine.interference_risks(target_domain: :target)
|
|
242
|
+
domains = result.map { |r| r[:domain] }
|
|
243
|
+
expect(domains).to include(:risky)
|
|
244
|
+
expect(domains).not_to include(:safe)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'sorts by similarity descending' do
|
|
248
|
+
engine.learn_domain(domain: :also_risky, amount: 0.5)
|
|
249
|
+
engine.set_similarity(domain_a: :also_risky, domain_b: :target, similarity: 0.35)
|
|
250
|
+
result = engine.interference_risks(target_domain: :target)
|
|
251
|
+
sims = result.map { |r| r[:similarity] }
|
|
252
|
+
expect(sims).to eq(sims.sort.reverse)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it 'returns empty array when no interference risks' do
|
|
256
|
+
engine2 = described_class.new
|
|
257
|
+
engine2.learn_domain(domain: :t, amount: 0.3)
|
|
258
|
+
expect(engine2.interference_risks(target_domain: :t)).to be_empty
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
describe '#transfer_report' do
|
|
263
|
+
it 'returns zero counts for empty engine' do
|
|
264
|
+
report = engine.transfer_report
|
|
265
|
+
expect(report[:total_transfers]).to eq(0)
|
|
266
|
+
expect(report[:domain_count]).to eq(0)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'counts transfers correctly' do
|
|
270
|
+
engine.learn_domain(domain: :a, amount: 0.8)
|
|
271
|
+
engine.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.7)
|
|
272
|
+
engine.attempt_transfer(from_domain: :a, to_domain: :b)
|
|
273
|
+
engine.attempt_transfer(from_domain: :a, to_domain: :b)
|
|
274
|
+
report = engine.transfer_report
|
|
275
|
+
expect(report[:total_transfers]).to eq(2)
|
|
276
|
+
expect(report[:positive_transfers]).to eq(2)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it 'counts interference events' do
|
|
280
|
+
engine.learn_domain(domain: :a, amount: 0.8)
|
|
281
|
+
engine.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.4)
|
|
282
|
+
engine.attempt_transfer(from_domain: :a, to_domain: :b)
|
|
283
|
+
report = engine.transfer_report
|
|
284
|
+
expect(report[:interference_events]).to eq(1)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
describe '#to_h' do
|
|
289
|
+
it 'returns a hash with expected keys' do
|
|
290
|
+
keys = engine.to_h.keys
|
|
291
|
+
expect(keys).to contain_exactly(:domains, :similarities, :transfer_history, :report)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'reflects current state' do
|
|
295
|
+
engine.learn_domain(domain: :ruby, amount: 0.5)
|
|
296
|
+
expect(engine.to_h[:domains].keys).to include(:ruby)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/transfer_learning/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::TransferLearning::Runners::TransferLearning do
|
|
6
|
+
let(:client) { Legion::Extensions::TransferLearning::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#learn_domain' do
|
|
9
|
+
it 'returns domain knowledge hash' do
|
|
10
|
+
result = client.learn_domain(domain: :ruby, amount: 0.5)
|
|
11
|
+
expect(result[:domain]).to eq(:ruby)
|
|
12
|
+
expect(result[:proficiency]).to eq(0.5)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'accumulates proficiency across calls' do
|
|
16
|
+
client.learn_domain(domain: :ruby, amount: 0.3)
|
|
17
|
+
result = client.learn_domain(domain: :ruby, amount: 0.3)
|
|
18
|
+
expect(result[:proficiency]).to be_within(0.0001).of(0.6)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#set_similarity' do
|
|
23
|
+
it 'returns similarity hash' do
|
|
24
|
+
result = client.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.7)
|
|
25
|
+
expect(result[:domain_a]).to eq(:ruby)
|
|
26
|
+
expect(result[:domain_b]).to eq(:python)
|
|
27
|
+
expect(result[:similarity]).to eq(0.7)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#attempt_transfer' do
|
|
32
|
+
before do
|
|
33
|
+
client.learn_domain(domain: :ruby, amount: 0.8)
|
|
34
|
+
client.set_similarity(domain_a: :ruby, domain_b: :python, similarity: 0.8)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'returns ok status for known source' do
|
|
38
|
+
result = client.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
39
|
+
expect(result[:status]).to eq(:ok)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'returns positive type for high similarity' do
|
|
43
|
+
result = client.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
44
|
+
expect(result[:type]).to eq(:positive)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns source_not_found for unknown source' do
|
|
48
|
+
result = client.attempt_transfer(from_domain: :unknown, to_domain: :python)
|
|
49
|
+
expect(result[:status]).to eq(:source_not_found)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'includes proficiency in response' do
|
|
53
|
+
result = client.attempt_transfer(from_domain: :ruby, to_domain: :python)
|
|
54
|
+
expect(result).to have_key(:proficiency)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#transfer_effectiveness' do
|
|
59
|
+
before do
|
|
60
|
+
client.learn_domain(domain: :a, amount: 0.6)
|
|
61
|
+
client.learn_domain(domain: :b, amount: 0.4)
|
|
62
|
+
client.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.7)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'returns effectiveness details' do
|
|
66
|
+
result = client.transfer_effectiveness(from_domain: :a, to_domain: :b)
|
|
67
|
+
expect(result[:type]).to eq(:positive)
|
|
68
|
+
expect(result[:type_label]).to eq('positive')
|
|
69
|
+
expect(result[:distance_label]).to eq('near')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '#most_transferable' do
|
|
74
|
+
before do
|
|
75
|
+
client.learn_domain(domain: :target, amount: 0.2)
|
|
76
|
+
client.learn_domain(domain: :good_source, amount: 0.8)
|
|
77
|
+
client.set_similarity(domain_a: :good_source, domain_b: :target, similarity: 0.9)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'returns candidates hash' do
|
|
81
|
+
result = client.most_transferable(target_domain: :target)
|
|
82
|
+
expect(result[:target_domain]).to eq(:target)
|
|
83
|
+
expect(result[:candidates]).to be_an(Array)
|
|
84
|
+
expect(result[:count]).to eq(result[:candidates].size)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'includes positive transfer candidates' do
|
|
88
|
+
result = client.most_transferable(target_domain: :target)
|
|
89
|
+
expect(result[:candidates].map { |c| c[:domain] }).to include(:good_source)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe '#interference_risks' do
|
|
94
|
+
before do
|
|
95
|
+
client.learn_domain(domain: :target, amount: 0.3)
|
|
96
|
+
client.learn_domain(domain: :risky_source, amount: 0.8)
|
|
97
|
+
client.set_similarity(domain_a: :risky_source, domain_b: :target, similarity: 0.4)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns risks hash' do
|
|
101
|
+
result = client.interference_risks(target_domain: :target)
|
|
102
|
+
expect(result[:target_domain]).to eq(:target)
|
|
103
|
+
expect(result[:risks]).to be_an(Array)
|
|
104
|
+
expect(result[:count]).to eq(result[:risks].size)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'identifies interference risks' do
|
|
108
|
+
result = client.interference_risks(target_domain: :target)
|
|
109
|
+
expect(result[:risks].map { |r| r[:domain] }).to include(:risky_source)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe '#transfer_report' do
|
|
114
|
+
it 'returns a report hash' do
|
|
115
|
+
result = client.transfer_report
|
|
116
|
+
expect(result).to have_key(:total_transfers)
|
|
117
|
+
expect(result).to have_key(:domain_count)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'counts transfers after attempts' do
|
|
121
|
+
client.learn_domain(domain: :a, amount: 0.8)
|
|
122
|
+
client.set_similarity(domain_a: :a, domain_b: :b, similarity: 0.7)
|
|
123
|
+
client.attempt_transfer(from_domain: :a, to_domain: :b)
|
|
124
|
+
result = client.transfer_report
|
|
125
|
+
expect(result[:total_transfers]).to eq(1)
|
|
126
|
+
expect(result[:positive_transfers]).to eq(1)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe '#get_domain' do
|
|
131
|
+
it 'returns found: false for unknown domain' do
|
|
132
|
+
result = client.get_domain(domain: :unknown)
|
|
133
|
+
expect(result[:found]).to be false
|
|
134
|
+
expect(result[:domain]).to eq(:unknown)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'returns found: true for known domain' do
|
|
138
|
+
client.learn_domain(domain: :ruby, amount: 0.5)
|
|
139
|
+
result = client.get_domain(domain: :ruby)
|
|
140
|
+
expect(result[:found]).to be true
|
|
141
|
+
expect(result[:domain][:domain]).to eq(:ruby)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Logging
|
|
7
|
+
def self.debug(_msg); end
|
|
8
|
+
def self.info(_msg); end
|
|
9
|
+
def self.warn(_msg); end
|
|
10
|
+
def self.error(_msg); end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require 'legion/extensions/transfer_learning'
|
|
15
|
+
|
|
16
|
+
RSpec.configure do |config|
|
|
17
|
+
config.example_status_persistence_file_path = '.rspec_status'
|
|
18
|
+
config.disable_monkey_patching!
|
|
19
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-transfer-learning
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Esity
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: legion-gaia
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Domain knowledge transfer modeling for brain-modeled agentic AI
|
|
27
|
+
email:
|
|
28
|
+
- matthewdiverson@gmail.com
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- Gemfile
|
|
34
|
+
- lex-transfer-learning.gemspec
|
|
35
|
+
- lib/legion/extensions/transfer_learning.rb
|
|
36
|
+
- lib/legion/extensions/transfer_learning/client.rb
|
|
37
|
+
- lib/legion/extensions/transfer_learning/helpers/constants.rb
|
|
38
|
+
- lib/legion/extensions/transfer_learning/helpers/domain_knowledge.rb
|
|
39
|
+
- lib/legion/extensions/transfer_learning/helpers/transfer_engine.rb
|
|
40
|
+
- lib/legion/extensions/transfer_learning/runners/transfer_learning.rb
|
|
41
|
+
- lib/legion/extensions/transfer_learning/version.rb
|
|
42
|
+
- spec/legion/extensions/transfer_learning/client_spec.rb
|
|
43
|
+
- spec/legion/extensions/transfer_learning/helpers/constants_spec.rb
|
|
44
|
+
- spec/legion/extensions/transfer_learning/helpers/domain_knowledge_spec.rb
|
|
45
|
+
- spec/legion/extensions/transfer_learning/helpers/transfer_engine_spec.rb
|
|
46
|
+
- spec/legion/extensions/transfer_learning/runners/transfer_learning_spec.rb
|
|
47
|
+
- spec/spec_helper.rb
|
|
48
|
+
homepage: https://github.com/LegionIO/lex-transfer-learning
|
|
49
|
+
licenses:
|
|
50
|
+
- MIT
|
|
51
|
+
metadata:
|
|
52
|
+
homepage_uri: https://github.com/LegionIO/lex-transfer-learning
|
|
53
|
+
source_code_uri: https://github.com/LegionIO/lex-transfer-learning
|
|
54
|
+
documentation_uri: https://github.com/LegionIO/lex-transfer-learning
|
|
55
|
+
changelog_uri: https://github.com/LegionIO/lex-transfer-learning
|
|
56
|
+
bug_tracker_uri: https://github.com/LegionIO/lex-transfer-learning/issues
|
|
57
|
+
rubygems_mfa_required: 'true'
|
|
58
|
+
rdoc_options: []
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '3.4'
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0'
|
|
71
|
+
requirements: []
|
|
72
|
+
rubygems_version: 3.6.9
|
|
73
|
+
specification_version: 4
|
|
74
|
+
summary: LEX Transfer Learning
|
|
75
|
+
test_files: []
|