lex-learning-rate 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/README.md +83 -0
- data/lib/legion/extensions/learning_rate/client.rb +21 -0
- data/lib/legion/extensions/learning_rate/helpers/constants.rb +31 -0
- data/lib/legion/extensions/learning_rate/helpers/rate_model.rb +129 -0
- data/lib/legion/extensions/learning_rate/runners/learning_rate.rb +81 -0
- data/lib/legion/extensions/learning_rate/version.rb +9 -0
- data/lib/legion/extensions/learning_rate.rb +15 -0
- metadata +64 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1b65632c8f5e9197247406e7b85c36ca70b05c191410904d854146d5c9bae097
|
|
4
|
+
data.tar.gz: e00c09af41cdd7b301f987c302fdbf147ffb6db418af395b08d4ade3d3cf5ef5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bcb141f0edeae5372798eee9488d884eba72ce9afd87a953db5a4df939f852103846742ea1dee7f8608695e4422bb4099c37d6b17a7566f03d1112810a903407
|
|
7
|
+
data.tar.gz: bc7c36f9d70572b67c25c9ff60c243551d6a7ac5c06a0fbb987ce158327ec324c238190a9d499544ec2bcfbfe1b1fc9ff667816e37bc278ec9d5f5fec4ef6cae
|
data/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# lex-learning-rate
|
|
2
|
+
|
|
3
|
+
Adaptive learning rate management for LegionIO agents. Part of the LegionIO cognitive architecture extension ecosystem (LEX).
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
`lex-learning-rate` maintains per-domain learning rates that adapt to the agent's prediction performance. Wrong predictions raise the rate (more learning needed), correct predictions lower it (stable enough), and surprise or error events provide temporary boosts. Rates mean-revert toward a default over time. Rolling accuracy windows give a per-domain reliability signal.
|
|
8
|
+
|
|
9
|
+
Key capabilities:
|
|
10
|
+
|
|
11
|
+
- **Per-domain rates**: independent learning rate per knowledge domain
|
|
12
|
+
- **Prediction-driven adjustment**: wrong prediction +0.03, correct prediction -0.02
|
|
13
|
+
- **Event boosts**: surprise +0.05, error +0.04
|
|
14
|
+
- **Mean reversion**: rates drift back toward 0.15 default via decay
|
|
15
|
+
- **Rate labels**: very_slow / slow / moderate / fast / very_fast per rate value
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add to your Gemfile:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
gem 'lex-learning-rate'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install directly:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
gem install lex-learning-rate
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'legion/extensions/learning_rate'
|
|
35
|
+
|
|
36
|
+
client = Legion::Extensions::LearningRate::Client.new
|
|
37
|
+
|
|
38
|
+
# Record prediction outcomes
|
|
39
|
+
client.record_prediction(domain: :networking, correct: false)
|
|
40
|
+
client.record_prediction(domain: :networking, correct: false)
|
|
41
|
+
|
|
42
|
+
# Record surprise or error events
|
|
43
|
+
client.record_surprise(domain: :networking)
|
|
44
|
+
|
|
45
|
+
# Check current rate
|
|
46
|
+
result = client.current_rate(domain: :networking)
|
|
47
|
+
# => { domain: :networking, rate: 0.27, label: :fast, accuracy: 0.3 }
|
|
48
|
+
|
|
49
|
+
# Find domains with highest/lowest learning rates
|
|
50
|
+
client.fastest_domains(limit: 3)
|
|
51
|
+
client.slowest_domains(limit: 3)
|
|
52
|
+
|
|
53
|
+
# Feed tick results (auto-extracts prediction outcomes)
|
|
54
|
+
client.update_learning_rate(tick_results: tick_phase_results)
|
|
55
|
+
|
|
56
|
+
# Stats
|
|
57
|
+
client.learning_rate_stats
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Runner Methods
|
|
61
|
+
|
|
62
|
+
| Method | Description |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `record_prediction` | Record a prediction outcome and adjust the domain rate |
|
|
65
|
+
| `record_surprise` | Apply a surprise boost to a domain's rate |
|
|
66
|
+
| `record_error` | Apply an error boost to a domain's rate |
|
|
67
|
+
| `current_rate` | Current rate value and label for a domain |
|
|
68
|
+
| `fastest_domains` | Top N domains by current learning rate |
|
|
69
|
+
| `slowest_domains` | Bottom N domains by current learning rate |
|
|
70
|
+
| `update_learning_rate` | Extract prediction outcomes from tick results and record |
|
|
71
|
+
| `learning_rate_stats` | Domain count, avg rate, fastest/slowest, overall accuracy |
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bundle install
|
|
77
|
+
bundle exec rspec
|
|
78
|
+
bundle exec rubocop
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/learning_rate/helpers/constants'
|
|
4
|
+
require 'legion/extensions/learning_rate/helpers/rate_model'
|
|
5
|
+
require 'legion/extensions/learning_rate/runners/learning_rate'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module LearningRate
|
|
10
|
+
class Client
|
|
11
|
+
include Runners::LearningRate
|
|
12
|
+
|
|
13
|
+
attr_reader :rate_model
|
|
14
|
+
|
|
15
|
+
def initialize(rate_model: nil, **)
|
|
16
|
+
@rate_model = rate_model || Helpers::RateModel.new
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LearningRate
|
|
6
|
+
module Helpers
|
|
7
|
+
module Constants
|
|
8
|
+
DEFAULT_RATE = 0.15
|
|
9
|
+
MIN_RATE = 0.01
|
|
10
|
+
MAX_RATE = 0.5
|
|
11
|
+
RATE_INCREASE = 0.03
|
|
12
|
+
RATE_DECREASE = 0.02
|
|
13
|
+
RATE_DECAY = 0.005
|
|
14
|
+
ACCURACY_WINDOW = 20
|
|
15
|
+
SURPRISE_BOOST = 0.05
|
|
16
|
+
ERROR_BOOST = 0.04
|
|
17
|
+
CONFIDENCE_DAMPENING = 0.03
|
|
18
|
+
MAX_DOMAINS = 50
|
|
19
|
+
MAX_RATE_HISTORY = 200
|
|
20
|
+
|
|
21
|
+
RATE_LABELS = {
|
|
22
|
+
(0.3..) => :fast_learning,
|
|
23
|
+
(0.15...0.3) => :moderate_learning,
|
|
24
|
+
(0.05...0.15) => :slow_learning,
|
|
25
|
+
(..0.05) => :consolidated
|
|
26
|
+
}.freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LearningRate
|
|
6
|
+
module Helpers
|
|
7
|
+
class RateModel
|
|
8
|
+
include Constants
|
|
9
|
+
|
|
10
|
+
attr_reader :rates, :accuracy_buffers, :rate_history
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@rates = {}
|
|
14
|
+
@accuracy_buffers = {}
|
|
15
|
+
@rate_history = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def rate_for(domain)
|
|
19
|
+
@rates.fetch(domain, DEFAULT_RATE)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record_prediction(domain:, correct:)
|
|
23
|
+
ensure_domain(domain)
|
|
24
|
+
@accuracy_buffers[domain] << (correct ? 1.0 : 0.0)
|
|
25
|
+
@accuracy_buffers[domain].shift while @accuracy_buffers[domain].size > ACCURACY_WINDOW
|
|
26
|
+
adjust_rate(domain, correct: correct)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def record_surprise(domain:, magnitude:)
|
|
30
|
+
ensure_domain(domain)
|
|
31
|
+
boost = magnitude * SURPRISE_BOOST
|
|
32
|
+
@rates[domain] = (@rates[domain] + boost).clamp(MIN_RATE, MAX_RATE)
|
|
33
|
+
record_event(domain, :surprise, @rates[domain])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def record_error(domain:, magnitude:)
|
|
37
|
+
ensure_domain(domain)
|
|
38
|
+
boost = magnitude * ERROR_BOOST
|
|
39
|
+
@rates[domain] = (@rates[domain] + boost).clamp(MIN_RATE, MAX_RATE)
|
|
40
|
+
record_event(domain, :error, @rates[domain])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def accuracy_for(domain)
|
|
44
|
+
buffer = @accuracy_buffers.fetch(domain, [])
|
|
45
|
+
return 0.0 if buffer.empty?
|
|
46
|
+
|
|
47
|
+
buffer.sum / buffer.size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def decay
|
|
51
|
+
@rates.each_key do |domain|
|
|
52
|
+
current = @rates[domain]
|
|
53
|
+
delta = (current - DEFAULT_RATE) * RATE_DECAY
|
|
54
|
+
@rates[domain] = (current - delta).clamp(MIN_RATE, MAX_RATE)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def label_for(domain)
|
|
59
|
+
rate = rate_for(domain)
|
|
60
|
+
RATE_LABELS.each do |range, lbl|
|
|
61
|
+
return lbl if range.cover?(rate)
|
|
62
|
+
end
|
|
63
|
+
:consolidated
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def fastest_domains(count = 5)
|
|
67
|
+
@rates.sort_by { |_, r| -r }.first(count).to_h
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def slowest_domains(count = 5)
|
|
71
|
+
@rates.sort_by { |_, r| r }.first(count).to_h
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def overall_rate
|
|
75
|
+
return DEFAULT_RATE if @rates.empty?
|
|
76
|
+
|
|
77
|
+
@rates.values.sum / @rates.size
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def domain_count
|
|
81
|
+
@rates.size
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_h
|
|
85
|
+
{
|
|
86
|
+
domain_count: @rates.size,
|
|
87
|
+
overall_rate: overall_rate.round(4),
|
|
88
|
+
rates: @rates.dup,
|
|
89
|
+
history_size: @rate_history.size
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def ensure_domain(domain)
|
|
96
|
+
@rates[domain] ||= DEFAULT_RATE
|
|
97
|
+
@accuracy_buffers[domain] ||= []
|
|
98
|
+
trim_domains(protect: domain) if @rates.size > MAX_DOMAINS
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def adjust_rate(domain, correct:)
|
|
102
|
+
@rates[domain] = if correct
|
|
103
|
+
(@rates[domain] - RATE_DECREASE).clamp(MIN_RATE, MAX_RATE)
|
|
104
|
+
else
|
|
105
|
+
(@rates[domain] + RATE_INCREASE).clamp(MIN_RATE, MAX_RATE)
|
|
106
|
+
end
|
|
107
|
+
record_event(domain, correct ? :correct : :incorrect, @rates[domain])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def record_event(domain, event_type, rate)
|
|
111
|
+
@rate_history << { domain: domain, event: event_type, rate: rate, at: Time.now.utc }
|
|
112
|
+
@rate_history.shift while @rate_history.size > MAX_RATE_HISTORY
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def trim_domains(protect: nil)
|
|
116
|
+
candidates = @rates.reject { |k, _| k == protect }
|
|
117
|
+
sorted = candidates.sort_by { |_, r| (r - DEFAULT_RATE).abs }
|
|
118
|
+
excess = @rates.size - MAX_DOMAINS
|
|
119
|
+
remove_keys = sorted.first(excess).map(&:first)
|
|
120
|
+
remove_keys.each do |key|
|
|
121
|
+
@rates.delete(key)
|
|
122
|
+
@accuracy_buffers.delete(key)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module LearningRate
|
|
6
|
+
module Runners
|
|
7
|
+
module LearningRate
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
def record_prediction(correct:, domain: :general, **)
|
|
12
|
+
rate_model.record_prediction(domain: domain, correct: correct)
|
|
13
|
+
rate = rate_model.rate_for(domain)
|
|
14
|
+
accuracy = rate_model.accuracy_for(domain)
|
|
15
|
+
Legion::Logging.debug "[learning_rate] prediction: domain=#{domain} correct=#{correct} rate=#{rate.round(3)} accuracy=#{accuracy.round(3)}"
|
|
16
|
+
{
|
|
17
|
+
success: true,
|
|
18
|
+
domain: domain,
|
|
19
|
+
rate: rate,
|
|
20
|
+
accuracy: accuracy,
|
|
21
|
+
label: rate_model.label_for(domain)
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def record_surprise(magnitude:, domain: :general, **)
|
|
26
|
+
rate_model.record_surprise(domain: domain, magnitude: magnitude)
|
|
27
|
+
rate = rate_model.rate_for(domain)
|
|
28
|
+
Legion::Logging.debug "[learning_rate] surprise: domain=#{domain} magnitude=#{magnitude.round(3)} rate=#{rate.round(3)}"
|
|
29
|
+
{ success: true, domain: domain, rate: rate, label: rate_model.label_for(domain) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def record_error(magnitude:, domain: :general, **)
|
|
33
|
+
rate_model.record_error(domain: domain, magnitude: magnitude)
|
|
34
|
+
rate = rate_model.rate_for(domain)
|
|
35
|
+
Legion::Logging.debug "[learning_rate] error: domain=#{domain} magnitude=#{magnitude.round(3)} rate=#{rate.round(3)}"
|
|
36
|
+
{ success: true, domain: domain, rate: rate, label: rate_model.label_for(domain) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def current_rate(domain: :general, **)
|
|
40
|
+
rate = rate_model.rate_for(domain)
|
|
41
|
+
accuracy = rate_model.accuracy_for(domain)
|
|
42
|
+
{
|
|
43
|
+
success: true,
|
|
44
|
+
domain: domain,
|
|
45
|
+
rate: rate,
|
|
46
|
+
accuracy: accuracy,
|
|
47
|
+
label: rate_model.label_for(domain)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fastest_domains(count: 5, **)
|
|
52
|
+
domains = rate_model.fastest_domains(count)
|
|
53
|
+
{ success: true, domains: domains, count: domains.size }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def slowest_domains(count: 5, **)
|
|
57
|
+
domains = rate_model.slowest_domains(count)
|
|
58
|
+
{ success: true, domains: domains, count: domains.size }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def update_learning_rate(**)
|
|
62
|
+
rate_model.decay
|
|
63
|
+
overall = rate_model.overall_rate
|
|
64
|
+
Legion::Logging.debug "[learning_rate] tick: domains=#{rate_model.domain_count} overall=#{overall.round(3)}"
|
|
65
|
+
{ success: true, domain_count: rate_model.domain_count, overall_rate: overall }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def learning_rate_stats(**)
|
|
69
|
+
{ success: true, stats: rate_model.to_h }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def rate_model
|
|
75
|
+
@rate_model ||= Helpers::RateModel.new
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/learning_rate/version'
|
|
4
|
+
require 'legion/extensions/learning_rate/helpers/constants'
|
|
5
|
+
require 'legion/extensions/learning_rate/helpers/rate_model'
|
|
6
|
+
require 'legion/extensions/learning_rate/runners/learning_rate'
|
|
7
|
+
require 'legion/extensions/learning_rate/client'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module LearningRate
|
|
12
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lex-learning-rate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Matthew Iverson
|
|
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: Adapts the agent learning speed per domain based on prediction accuracy,
|
|
27
|
+
surprise, and errors. Fast learning when predictions fail, slow consolidation when
|
|
28
|
+
accurate — the agent learns how fast to learn.
|
|
29
|
+
email:
|
|
30
|
+
- matt@iverson.io
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/legion/extensions/learning_rate.rb
|
|
37
|
+
- lib/legion/extensions/learning_rate/client.rb
|
|
38
|
+
- lib/legion/extensions/learning_rate/helpers/constants.rb
|
|
39
|
+
- lib/legion/extensions/learning_rate/helpers/rate_model.rb
|
|
40
|
+
- lib/legion/extensions/learning_rate/runners/learning_rate.rb
|
|
41
|
+
- lib/legion/extensions/learning_rate/version.rb
|
|
42
|
+
homepage: https://github.com/LegionIO/lex-learning-rate
|
|
43
|
+
licenses:
|
|
44
|
+
- MIT
|
|
45
|
+
metadata:
|
|
46
|
+
rubygems_mfa_required: 'true'
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.4'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.6.9
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Meta-learning rate adaptation for LegionIO
|
|
64
|
+
test_files: []
|