Class: IrtRuby::TwoParameterModel
- Inherits:
-
Object
- Object
- IrtRuby::TwoParameterModel
- Defined in:
- lib/irt_ruby/two_parameter_model.rb
Overview
A class representing the Two-Parameter model (2PL) for IRT. Incorporates:
-
Adaptive learning rate
-
Missing data handling
-
Parameter clamping for discrimination
-
Multiple convergence checks
-
Separate gradient calculation & parameter update
Constant Summary collapse
- MISSING_STRATEGIES =
%i[ignore treat_as_incorrect treat_as_correct].freeze
Instance Method Summary collapse
- #apply_gradient_update(ga, gd, gdisc) ⇒ Object
- #average_param_update(old_a, old_d, old_disc) ⇒ Object
- #compute_gradient ⇒ Object
- #fit ⇒ Object
-
#initialize(data, max_iter: 1000, tolerance: 1e-6, param_tolerance: 1e-6, learning_rate: 0.01, decay_factor: 0.5, missing_strategy: :ignore) ⇒ TwoParameterModel
constructor
A new instance of TwoParameterModel.
- #log_likelihood ⇒ Object
- #resolve_missing(resp) ⇒ Object
- #sigmoid(x) ⇒ Object
Constructor Details
#initialize(data, max_iter: 1000, tolerance: 1e-6, param_tolerance: 1e-6, learning_rate: 0.01, decay_factor: 0.5, missing_strategy: :ignore) ⇒ TwoParameterModel
Returns a new instance of TwoParameterModel.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/irt_ruby/two_parameter_model.rb', line 14 def initialize(data, max_iter: 1000, tolerance: 1e-6, param_tolerance: 1e-6, learning_rate: 0.01, decay_factor: 0.5, missing_strategy: :ignore) @data = data @data_array = data.to_a num_rows = @data_array.size num_cols = @data_array.first.size raise ArgumentError, "missing_strategy must be one of #{MISSING_STRATEGIES}" unless MISSING_STRATEGIES.include?(missing_strategy) @missing_strategy = missing_strategy # Initialize parameters # Typically: ability ~ 0, difficulty ~ 0, discrimination ~ 1 @abilities = Array.new(num_rows) { rand(-0.25..0.25) } @difficulties = Array.new(num_cols) { rand(-0.25..0.25) } @discriminations = Array.new(num_cols) { rand(0.5..1.5) } @max_iter = max_iter @tolerance = tolerance @param_tolerance = param_tolerance @learning_rate = learning_rate @decay_factor = decay_factor end |
Instance Method Details
#apply_gradient_update(ga, gd, gdisc) ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/irt_ruby/two_parameter_model.rb', line 96 def apply_gradient_update(ga, gd, gdisc) old_a = @abilities.dup old_d = @difficulties.dup old_disc = @discriminations.dup @abilities.each_index do |i| @abilities[i] += @learning_rate * ga[i] end @difficulties.each_index do |j| @difficulties[j] += @learning_rate * gd[j] end @discriminations.each_index do |j| @discriminations[j] += @learning_rate * gdisc[j] @discriminations[j] = 0.01 if @discriminations[j] < 0.01 @discriminations[j] = 5.0 if @discriminations[j] > 5.0 end [old_a, old_d, old_disc] end |
#average_param_update(old_a, old_d, old_disc) ⇒ Object
118 119 120 121 122 123 124 |
# File 'lib/irt_ruby/two_parameter_model.rb', line 118 def average_param_update(old_a, old_d, old_disc) deltas = [] @abilities.each_with_index { |x, i| deltas << (x - old_a[i]).abs } @difficulties.each_with_index { |x, j| deltas << (x - old_d[j]).abs } @discriminations.each_with_index { |x, j| deltas << (x - old_disc[j]).abs } deltas.sum / deltas.size end |
#compute_gradient ⇒ Object
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/irt_ruby/two_parameter_model.rb', line 74 def compute_gradient grad_abilities = Array.new(@abilities.size, 0.0) grad_difficulties = Array.new(@difficulties.size, 0.0) grad_discriminations = Array.new(@discriminations.size, 0.0) @data_array.each_with_index do |row, i| row.each_with_index do |resp, j| value, skip = resolve_missing(resp) next if skip prob = sigmoid(@discriminations[j] * (@abilities[i] - @difficulties[j])) error = value - prob grad_abilities[i] += error * @discriminations[j] grad_difficulties[j] -= error * @discriminations[j] grad_discriminations[j] += error * (@abilities[i] - @difficulties[j]) end end [grad_abilities, grad_difficulties, grad_discriminations] end |
#fit ⇒ Object
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/irt_ruby/two_parameter_model.rb', line 126 def fit prev_ll = log_likelihood @max_iter.times do ga, gd, gdisc = compute_gradient old_a, old_d, old_disc = apply_gradient_update(ga, gd, gdisc) curr_ll = log_likelihood param_delta = average_param_update(old_a, old_d, old_disc) if curr_ll < prev_ll @abilities = old_a @difficulties = old_d @discriminations = old_disc @learning_rate *= @decay_factor else ll_diff = (curr_ll - prev_ll).abs break if ll_diff < @tolerance && param_delta < @param_tolerance prev_ll = curr_ll end end { abilities: @abilities, difficulties: @difficulties, discriminations: @discriminations } end |
#log_likelihood ⇒ Object
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/irt_ruby/two_parameter_model.rb', line 56 def log_likelihood ll = 0.0 @data_array.each_with_index do |row, i| row.each_with_index do |resp, j| value, skip = resolve_missing(resp) next if skip prob = sigmoid(@discriminations[j] * (@abilities[i] - @difficulties[j])) ll += if value == 1 Math.log(prob + 1e-15) else Math.log((1 - prob) + 1e-15) end end end ll end |
#resolve_missing(resp) ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/irt_ruby/two_parameter_model.rb', line 43 def resolve_missing(resp) return [resp, false] unless resp.nil? case @missing_strategy when :ignore [nil, true] when :treat_as_incorrect [0, false] when :treat_as_correct [1, false] end end |
#sigmoid(x) ⇒ Object
39 40 41 |
# File 'lib/irt_ruby/two_parameter_model.rb', line 39 def sigmoid(x) 1.0 / (1.0 + Math.exp(-x)) end |