irt_ruby 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +50 -0
- data/LICENSE.txt +21 -0
- data/README.md +168 -0
- data/benchmarks/README.md +135 -0
- data/benchmarks/convergence_benchmark.rb +265 -0
- data/benchmarks/performance_benchmark.rb +153 -0
- data/lib/irt_ruby/rasch_model.rb +123 -33
- data/lib/irt_ruby/three_parameter_model.rb +154 -41
- data/lib/irt_ruby/two_parameter_model.rb +131 -40
- data/lib/irt_ruby/version.rb +1 -1
- data/lib/irt_ruby.rb +1 -0
- metadata +69 -10
@@ -0,0 +1,153 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "irt_ruby"
|
6
|
+
require "benchmark/ips"
|
7
|
+
require "memory_profiler"
|
8
|
+
|
9
|
+
# Generate test data of different sizes
|
10
|
+
def generate_data(num_people, num_items, missing_rate: 0.0)
|
11
|
+
Array.new(num_people) do
|
12
|
+
Array.new(num_items) do
|
13
|
+
if rand < missing_rate
|
14
|
+
nil
|
15
|
+
else
|
16
|
+
rand < 0.6 ? 1 : 0 # 60% probability of correct response
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Dataset configurations
|
23
|
+
DATASET_CONFIGS = [
|
24
|
+
{ people: 10, items: 5, label: "Tiny (10x5)" },
|
25
|
+
{ people: 50, items: 20, label: "Small (50x20)" },
|
26
|
+
{ people: 100, items: 50, label: "Medium (100x50)" },
|
27
|
+
{ people: 200, items: 100, label: "Large (200x100)" },
|
28
|
+
{ people: 500, items: 200, label: "XLarge (500x200)" }
|
29
|
+
].freeze
|
30
|
+
|
31
|
+
puts "=" * 60
|
32
|
+
puts "IRT Ruby Performance Benchmarks"
|
33
|
+
puts "=" * 60
|
34
|
+
puts
|
35
|
+
|
36
|
+
# Benchmark each model type across different dataset sizes
|
37
|
+
DATASET_CONFIGS.each do |config|
|
38
|
+
puts "Dataset: #{config[:label]}"
|
39
|
+
puts "-" * 40
|
40
|
+
|
41
|
+
data = generate_data(config[:people], config[:items])
|
42
|
+
|
43
|
+
Benchmark.ips do |x|
|
44
|
+
x.config(time: 5, warmup: 2)
|
45
|
+
|
46
|
+
x.report("Rasch Model") do
|
47
|
+
model = IrtRuby::RaschModel.new(data, max_iter: 100)
|
48
|
+
model.fit
|
49
|
+
end
|
50
|
+
|
51
|
+
x.report("2PL Model") do
|
52
|
+
model = IrtRuby::TwoParameterModel.new(data, max_iter: 100)
|
53
|
+
model.fit
|
54
|
+
end
|
55
|
+
|
56
|
+
x.report("3PL Model") do
|
57
|
+
model = IrtRuby::ThreeParameterModel.new(data, max_iter: 100)
|
58
|
+
model.fit
|
59
|
+
end
|
60
|
+
|
61
|
+
x.compare!
|
62
|
+
end
|
63
|
+
|
64
|
+
puts
|
65
|
+
end
|
66
|
+
|
67
|
+
# Memory usage analysis for medium dataset
|
68
|
+
puts "=" * 60
|
69
|
+
puts "Memory Usage Analysis (Medium Dataset: 100x50)"
|
70
|
+
puts "=" * 60
|
71
|
+
|
72
|
+
data = generate_data(100, 50)
|
73
|
+
|
74
|
+
%i[RaschModel TwoParameterModel ThreeParameterModel].each do |model_class|
|
75
|
+
puts "\n#{model_class}:"
|
76
|
+
puts "-" * 20
|
77
|
+
|
78
|
+
report = MemoryProfiler.report do
|
79
|
+
model = IrtRuby.const_get(model_class).new(data, max_iter: 100)
|
80
|
+
model.fit
|
81
|
+
end
|
82
|
+
|
83
|
+
puts "Total allocated: #{report.total_allocated_memsize} bytes"
|
84
|
+
puts "Total retained: #{report.total_retained_memsize} bytes"
|
85
|
+
puts "Objects allocated: #{report.total_allocated}"
|
86
|
+
puts "Objects retained: #{report.total_retained}"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Scaling analysis - how performance changes with dataset size
|
90
|
+
puts "\n#{"=" * 60}"
|
91
|
+
puts "Scaling Analysis - Rasch Model Only"
|
92
|
+
puts "=" * 60
|
93
|
+
|
94
|
+
scaling_results = {}
|
95
|
+
|
96
|
+
DATASET_CONFIGS.each do |config|
|
97
|
+
data = generate_data(config[:people], config[:items])
|
98
|
+
|
99
|
+
times = []
|
100
|
+
5.times do
|
101
|
+
start_time = Time.now
|
102
|
+
model = IrtRuby::RaschModel.new(data, max_iter: 100)
|
103
|
+
model.fit
|
104
|
+
end_time = Time.now
|
105
|
+
times << (end_time - start_time)
|
106
|
+
end
|
107
|
+
|
108
|
+
avg_time = times.sum / times.size
|
109
|
+
scaling_results[config[:label]] = {
|
110
|
+
size: config[:people] * config[:items],
|
111
|
+
avg_time: avg_time,
|
112
|
+
people: config[:people],
|
113
|
+
items: config[:items]
|
114
|
+
}
|
115
|
+
|
116
|
+
puts "#{config[:label]}: #{avg_time.round(4)}s (#{config[:people] * config[:items]} data points)"
|
117
|
+
end
|
118
|
+
|
119
|
+
# Calculate scaling coefficient
|
120
|
+
puts "\nScaling Analysis:"
|
121
|
+
puts "-" * 20
|
122
|
+
scaling_results.each_cons(2) do |(label1, data1), (label2, data2)|
|
123
|
+
size_ratio = data2[:size].to_f / data1[:size]
|
124
|
+
time_ratio = data2[:avg_time] / data1[:avg_time]
|
125
|
+
scaling_factor = Math.log(time_ratio) / Math.log(size_ratio)
|
126
|
+
|
127
|
+
puts "#{label1} -> #{label2}: #{size_ratio.round(2)}x size, #{time_ratio.round(2)}x time (O(n^#{scaling_factor.round(2)}))"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Missing data performance impact
|
131
|
+
puts "\n#{"=" * 60}"
|
132
|
+
puts "Missing Data Strategy Performance Impact"
|
133
|
+
puts "=" * 60
|
134
|
+
|
135
|
+
data_with_missing = generate_data(100, 50, missing_rate: 0.2)
|
136
|
+
|
137
|
+
%i[ignore treat_as_incorrect treat_as_correct].each do |strategy|
|
138
|
+
puts "\nMissing Strategy: #{strategy}"
|
139
|
+
puts "-" * 30
|
140
|
+
|
141
|
+
Benchmark.ips do |x|
|
142
|
+
x.config(time: 3, warmup: 1)
|
143
|
+
|
144
|
+
x.report("Rasch") do
|
145
|
+
model = IrtRuby::RaschModel.new(data_with_missing, max_iter: 50, missing_strategy: strategy)
|
146
|
+
model.fit
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
puts "\n#{"=" * 60}"
|
152
|
+
puts "Benchmark Complete!"
|
153
|
+
puts "=" * 60
|
data/lib/irt_ruby/rasch_model.rb
CHANGED
@@ -1,58 +1,148 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "matrix"
|
4
|
-
|
5
3
|
module IrtRuby
|
6
|
-
# A class representing the Rasch model for Item Response Theory.
|
4
|
+
# A class representing the Rasch model for Item Response Theory (ability - difficulty).
|
5
|
+
# Incorporates:
|
6
|
+
# - Adaptive learning rate
|
7
|
+
# - Missing data handling (skip nil)
|
8
|
+
# - Multiple convergence checks (log-likelihood + parameter updates)
|
7
9
|
class RaschModel
|
8
|
-
|
10
|
+
MISSING_STRATEGIES = %i[ignore treat_as_incorrect treat_as_correct].freeze
|
11
|
+
|
12
|
+
def initialize(data,
|
13
|
+
max_iter: 1000,
|
14
|
+
tolerance: 1e-6,
|
15
|
+
param_tolerance: 1e-6,
|
16
|
+
learning_rate: 0.01,
|
17
|
+
decay_factor: 0.5,
|
18
|
+
missing_strategy: :ignore)
|
19
|
+
# data: A Matrix or array-of-arrays of responses (0/1 or nil for missing).
|
20
|
+
# missing_strategy: :ignore (skip), :treat_as_incorrect, :treat_as_correct
|
21
|
+
|
9
22
|
@data = data
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
23
|
+
@data_array = data.to_a
|
24
|
+
num_rows = @data_array.size
|
25
|
+
num_cols = @data_array.first.size
|
26
|
+
|
27
|
+
raise ArgumentError, "missing_strategy must be one of #{MISSING_STRATEGIES}" unless MISSING_STRATEGIES.include?(missing_strategy)
|
28
|
+
|
29
|
+
@missing_strategy = missing_strategy
|
30
|
+
|
31
|
+
# Initialize parameters near zero
|
32
|
+
@abilities = Array.new(num_rows) { rand(-0.25..0.25) }
|
33
|
+
@difficulties = Array.new(num_cols) { rand(-0.25..0.25) }
|
34
|
+
|
35
|
+
@max_iter = max_iter
|
36
|
+
@tolerance = tolerance
|
37
|
+
@param_tolerance = param_tolerance
|
38
|
+
@learning_rate = learning_rate
|
39
|
+
@decay_factor = decay_factor
|
15
40
|
end
|
16
41
|
|
17
|
-
# Sigmoid function to calculate probability
|
18
42
|
def sigmoid(x)
|
19
43
|
1.0 / (1.0 + Math.exp(-x))
|
20
44
|
end
|
21
45
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@
|
26
|
-
|
46
|
+
def resolve_missing(resp)
|
47
|
+
return [resp, false] unless resp.nil?
|
48
|
+
|
49
|
+
case @missing_strategy
|
50
|
+
when :ignore
|
51
|
+
[nil, true]
|
52
|
+
when :treat_as_incorrect
|
53
|
+
[0, false]
|
54
|
+
when :treat_as_correct
|
55
|
+
[1, false]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def log_likelihood
|
60
|
+
total_ll = 0.0
|
61
|
+
@data_array.each_with_index do |row, i|
|
62
|
+
row.each_with_index do |resp, j|
|
63
|
+
value, skip = resolve_missing(resp)
|
64
|
+
next if skip
|
65
|
+
|
27
66
|
prob = sigmoid(@abilities[i] - @difficulties[j])
|
28
|
-
|
67
|
+
total_ll += if value == 1
|
68
|
+
Math.log(prob + 1e-15)
|
69
|
+
else
|
70
|
+
Math.log((1 - prob) + 1e-15)
|
71
|
+
end
|
29
72
|
end
|
30
73
|
end
|
31
|
-
|
74
|
+
total_ll
|
32
75
|
end
|
33
76
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
77
|
+
def compute_gradient
|
78
|
+
grad_abilities = Array.new(@abilities.size, 0.0)
|
79
|
+
grad_difficulties = Array.new(@difficulties.size, 0.0)
|
80
|
+
|
81
|
+
@data_array.each_with_index do |row, i|
|
82
|
+
row.each_with_index do |resp, j|
|
83
|
+
value, skip = resolve_missing(resp)
|
84
|
+
next if skip
|
85
|
+
|
86
|
+
prob = sigmoid(@abilities[i] - @difficulties[j])
|
87
|
+
error = value - prob
|
88
|
+
|
89
|
+
grad_abilities[i] += error
|
90
|
+
grad_difficulties[j] -= error
|
45
91
|
end
|
46
|
-
|
47
|
-
break if (last_likelihood - current_likelihood).abs < @tolerance
|
92
|
+
end
|
48
93
|
|
49
|
-
|
94
|
+
[grad_abilities, grad_difficulties]
|
95
|
+
end
|
96
|
+
|
97
|
+
def apply_gradient_update(grad_abilities, grad_difficulties)
|
98
|
+
old_abilities = @abilities.dup
|
99
|
+
old_difficulties = @difficulties.dup
|
100
|
+
|
101
|
+
@abilities.each_index do |i|
|
102
|
+
@abilities[i] += @learning_rate * grad_abilities[i]
|
103
|
+
end
|
104
|
+
|
105
|
+
@difficulties.each_index do |j|
|
106
|
+
@difficulties[j] += @learning_rate * grad_difficulties[j]
|
107
|
+
end
|
108
|
+
|
109
|
+
[old_abilities, old_difficulties]
|
110
|
+
end
|
111
|
+
|
112
|
+
def average_param_update(old_abilities, old_difficulties)
|
113
|
+
deltas = []
|
114
|
+
@abilities.each_with_index do |a, i|
|
115
|
+
deltas << (a - old_abilities[i]).abs
|
116
|
+
end
|
117
|
+
@difficulties.each_with_index do |d, j|
|
118
|
+
deltas << (d - old_difficulties[j]).abs
|
50
119
|
end
|
120
|
+
deltas.sum / deltas.size
|
51
121
|
end
|
52
122
|
|
53
|
-
# Fit the model to the data
|
54
123
|
def fit
|
55
|
-
|
124
|
+
prev_ll = log_likelihood
|
125
|
+
|
126
|
+
@max_iter.times do
|
127
|
+
grad_abilities, grad_difficulties = compute_gradient
|
128
|
+
|
129
|
+
old_a, old_d = apply_gradient_update(grad_abilities, grad_difficulties)
|
130
|
+
|
131
|
+
current_ll = log_likelihood
|
132
|
+
param_delta = average_param_update(old_a, old_d)
|
133
|
+
|
134
|
+
if current_ll < prev_ll
|
135
|
+
@abilities = old_a
|
136
|
+
@difficulties = old_d
|
137
|
+
@learning_rate *= @decay_factor
|
138
|
+
else
|
139
|
+
ll_diff = (current_ll - prev_ll).abs
|
140
|
+
break if ll_diff < @tolerance && param_delta < @param_tolerance
|
141
|
+
|
142
|
+
prev_ll = current_ll
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
56
146
|
{ abilities: @abilities, difficulties: @difficulties }
|
57
147
|
end
|
58
148
|
end
|
@@ -1,68 +1,181 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "matrix"
|
4
|
-
|
5
3
|
module IrtRuby
|
6
|
-
# A class representing the Three-Parameter model for Item Response Theory.
|
4
|
+
# A class representing the Three-Parameter model (3PL) for Item Response Theory.
|
5
|
+
# Incorporates:
|
6
|
+
# - Adaptive learning rate
|
7
|
+
# - Missing data handling
|
8
|
+
# - Parameter clamping for discrimination, guessing
|
9
|
+
# - Multiple convergence checks
|
10
|
+
# - Separate gradient calculation & updates
|
7
11
|
class ThreeParameterModel
|
8
|
-
|
12
|
+
MISSING_STRATEGIES = %i[ignore treat_as_incorrect treat_as_correct].freeze
|
13
|
+
|
14
|
+
def initialize(data,
|
15
|
+
max_iter: 1000,
|
16
|
+
tolerance: 1e-6,
|
17
|
+
param_tolerance: 1e-6,
|
18
|
+
learning_rate: 0.01,
|
19
|
+
decay_factor: 0.5,
|
20
|
+
missing_strategy: :ignore)
|
9
21
|
@data = data
|
10
|
-
@
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@
|
22
|
+
@data_array = data.to_a
|
23
|
+
num_rows = @data_array.size
|
24
|
+
num_cols = @data_array.first.size
|
25
|
+
|
26
|
+
raise ArgumentError, "missing_strategy must be one of #{MISSING_STRATEGIES}" unless MISSING_STRATEGIES.include?(missing_strategy)
|
27
|
+
|
28
|
+
@missing_strategy = missing_strategy
|
29
|
+
|
30
|
+
# Initialize parameters
|
31
|
+
@abilities = Array.new(num_rows) { rand(-0.25..0.25) }
|
32
|
+
@difficulties = Array.new(num_cols) { rand(-0.25..0.25) }
|
33
|
+
@discriminations = Array.new(num_cols) { rand(0.5..1.5) }
|
34
|
+
@guessings = Array.new(num_cols) { rand(0.0..0.3) }
|
35
|
+
|
36
|
+
@max_iter = max_iter
|
37
|
+
@tolerance = tolerance
|
38
|
+
@param_tolerance = param_tolerance
|
39
|
+
@learning_rate = learning_rate
|
40
|
+
@decay_factor = decay_factor
|
17
41
|
end
|
18
42
|
|
19
|
-
# Sigmoid function to calculate probability
|
20
43
|
def sigmoid(x)
|
21
44
|
1.0 / (1.0 + Math.exp(-x))
|
22
45
|
end
|
23
46
|
|
24
|
-
# Probability
|
47
|
+
# Probability for the 3PL model: c + (1-c)*sigmoid(a*(θ - b))
|
25
48
|
def probability(theta, a, b, c)
|
26
|
-
c + (1 - c) * sigmoid(a * (theta - b))
|
49
|
+
c + ((1.0 - c) * sigmoid(a * (theta - b)))
|
50
|
+
end
|
51
|
+
|
52
|
+
def resolve_missing(resp)
|
53
|
+
return [resp, false] unless resp.nil?
|
54
|
+
|
55
|
+
case @missing_strategy
|
56
|
+
when :ignore
|
57
|
+
[nil, true]
|
58
|
+
when :treat_as_incorrect
|
59
|
+
[0, false]
|
60
|
+
when :treat_as_correct
|
61
|
+
[1, false]
|
62
|
+
end
|
27
63
|
end
|
28
64
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
65
|
+
def log_likelihood
|
66
|
+
ll = 0.0
|
67
|
+
@data_array.each_with_index do |row, i|
|
68
|
+
row.each_with_index do |resp, j|
|
69
|
+
value, skip = resolve_missing(resp)
|
70
|
+
next if skip
|
71
|
+
|
72
|
+
prob = probability(@abilities[i],
|
73
|
+
@discriminations[j],
|
74
|
+
@difficulties[j],
|
75
|
+
@guessings[j])
|
76
|
+
|
77
|
+
ll += if value == 1
|
78
|
+
Math.log(prob + 1e-15)
|
79
|
+
else
|
80
|
+
Math.log((1 - prob) + 1e-15)
|
81
|
+
end
|
36
82
|
end
|
37
83
|
end
|
38
|
-
|
84
|
+
ll
|
39
85
|
end
|
40
86
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
87
|
+
def compute_gradient
|
88
|
+
grad_abilities = Array.new(@abilities.size, 0.0)
|
89
|
+
grad_difficulties = Array.new(@difficulties.size, 0.0)
|
90
|
+
grad_discriminations = Array.new(@discriminations.size, 0.0)
|
91
|
+
grad_guessings = Array.new(@guessings.size, 0.0)
|
92
|
+
|
93
|
+
@data_array.each_with_index do |row, i|
|
94
|
+
row.each_with_index do |resp, j|
|
95
|
+
value, skip = resolve_missing(resp)
|
96
|
+
next if skip
|
97
|
+
|
98
|
+
theta = @abilities[i]
|
99
|
+
a = @discriminations[j]
|
100
|
+
b = @difficulties[j]
|
101
|
+
c = @guessings[j]
|
102
|
+
|
103
|
+
prob = probability(theta, a, b, c)
|
104
|
+
error = value - prob
|
105
|
+
|
106
|
+
grad_abilities[i] += error * a * (1 - c)
|
107
|
+
grad_difficulties[j] -= error * a * (1 - c)
|
108
|
+
grad_discriminations[j] += error * (theta - b) * (1 - c)
|
109
|
+
|
110
|
+
grad_guessings[j] += error * 1.0
|
55
111
|
end
|
56
|
-
|
57
|
-
break if (last_likelihood - current_likelihood).abs < @tolerance
|
112
|
+
end
|
58
113
|
|
59
|
-
|
114
|
+
[grad_abilities, grad_difficulties, grad_discriminations, grad_guessings]
|
115
|
+
end
|
116
|
+
|
117
|
+
def apply_gradient_update(ga, gd, gdisc, gc)
|
118
|
+
old_a = @abilities.dup
|
119
|
+
old_d = @difficulties.dup
|
120
|
+
old_disc = @discriminations.dup
|
121
|
+
old_c = @guessings.dup
|
122
|
+
|
123
|
+
@abilities.each_index do |i|
|
124
|
+
@abilities[i] += @learning_rate * ga[i]
|
60
125
|
end
|
126
|
+
|
127
|
+
@difficulties.each_index do |j|
|
128
|
+
@difficulties[j] += @learning_rate * gd[j]
|
129
|
+
end
|
130
|
+
|
131
|
+
@discriminations.each_index do |j|
|
132
|
+
@discriminations[j] += @learning_rate * gdisc[j]
|
133
|
+
@discriminations[j] = 0.01 if @discriminations[j] < 0.01
|
134
|
+
@discriminations[j] = 5.0 if @discriminations[j] > 5.0
|
135
|
+
end
|
136
|
+
|
137
|
+
@guessings.each_index do |j|
|
138
|
+
@guessings[j] += @learning_rate * gc[j]
|
139
|
+
@guessings[j] = 0.0 if @guessings[j] < 0.0
|
140
|
+
@guessings[j] = 0.35 if @guessings[j] > 0.35
|
141
|
+
end
|
142
|
+
|
143
|
+
[old_a, old_d, old_disc, old_c]
|
144
|
+
end
|
145
|
+
|
146
|
+
def average_param_update(old_a, old_d, old_disc, old_c)
|
147
|
+
deltas = []
|
148
|
+
@abilities.each_with_index { |x, i| deltas << (x - old_a[i]).abs }
|
149
|
+
@difficulties.each_with_index { |x, j| deltas << (x - old_d[j]).abs }
|
150
|
+
@discriminations.each_with_index { |x, j| deltas << (x - old_disc[j]).abs }
|
151
|
+
@guessings.each_with_index { |x, j| deltas << (x - old_c[j]).abs }
|
152
|
+
deltas.sum / deltas.size
|
61
153
|
end
|
62
154
|
|
63
|
-
# Fit the model to the data
|
64
155
|
def fit
|
65
|
-
|
156
|
+
prev_ll = log_likelihood
|
157
|
+
|
158
|
+
@max_iter.times do
|
159
|
+
ga, gd, gdisc, gc = compute_gradient
|
160
|
+
old_a, old_d, old_disc, old_c = apply_gradient_update(ga, gd, gdisc, gc)
|
161
|
+
|
162
|
+
curr_ll = log_likelihood
|
163
|
+
param_delta = average_param_update(old_a, old_d, old_disc, old_c)
|
164
|
+
|
165
|
+
if curr_ll < prev_ll
|
166
|
+
@abilities = old_a
|
167
|
+
@difficulties = old_d
|
168
|
+
@discriminations = old_disc
|
169
|
+
@guessings = old_c
|
170
|
+
@learning_rate *= @decay_factor
|
171
|
+
else
|
172
|
+
ll_diff = (curr_ll - prev_ll).abs
|
173
|
+
break if ll_diff < @tolerance && param_delta < @param_tolerance
|
174
|
+
|
175
|
+
prev_ll = curr_ll
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
66
179
|
{
|
67
180
|
abilities: @abilities,
|
68
181
|
difficulties: @difficulties,
|