Class: Bayesnet::Factor

Inherits:
Object
  • Object
show all
Defined in:
lib/bayesnet/factor.rb

Overview

Factor if a function of several variables (A, B, …), where every variable cold take values from some finite set

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.build(&block) ⇒ Object

+++ Factor DSL +++

Factor DSL entry point:



10
11
12
13
14
# File 'lib/bayesnet/factor.rb', line 10

def self.build(&block)
  factor = new
  factor.instance_eval(&block)
  factor
end

Instance Method Details

#*(other) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/bayesnet/factor.rb', line 140

def *(other)
  common_scope = @scope.keys & other.scope.keys
  new_scope = scope.merge(other.scope)
  new_vals = {}
  group1 = group_by_scope_values(common_scope)
  group2 = other.group_by_scope_values(common_scope)
  group1.each do |scope, vals1|
    combo = vals1.product(group2[scope])
    combo.each do |(val1, val2)|
      # values in scope must match variables order in new_scope, i.e.
      # they must match `new_scope.var_names`
      # The code bellow ensures it by merging two hashes in the same
      # wasy as `new_scope`` is constructed above
      val_by_name1 = var_names.zip(val1.first).to_h
      val_by_name2 = other.var_names.zip(val2.first).to_h
      new_vals[val_by_name1.merge(val_by_name2).values] = val1.last*val2.last
    end
  end
  Factor.new(new_scope, new_vals)
end

#[](*context) ⇒ Object

accessor factor value, i.e “‘ factor = Bayesnet::Factor.build do

scope weather: %i[sunny cloudy]
scope mood: %i[bad good]
val :sunny, :bad, 0.1
...

end factor[:sunny, :bad] # 0.1 “‘



66
67
68
69
70
71
72
73
# File 'lib/bayesnet/factor.rb', line 66

def [](*context)
  key = if context.size == 1 && context[0].is_a?(Hash)
          context[0].slice(*var_names).values
        else
          context
        end
  @vals[key]
end

#contextes(*var_names) ⇒ Object

returns all combinations of values of ‘var_names`



76
77
78
79
80
# File 'lib/bayesnet/factor.rb', line 76

def contextes(*var_names)
  return [] if var_names.empty?

  @scope[var_names[0]].product(*var_names[1..].map { |var_name| @scope[var_name] })
end

#eliminate(var_name) ⇒ Object



128
129
130
131
132
# File 'lib/bayesnet/factor.rb', line 128

def eliminate(var_name)
  keep_var_names = var_names
  keep_var_names.delete(var_name)
  marginalize(keep_var_names)
end

#group_by_scope_values(scope_keys) ⇒ Object



161
162
163
164
# File 'lib/bayesnet/factor.rb', line 161

def group_by_scope_values(scope_keys)
  indices = scope_keys.map { |k| index_by_var_name[k] }
  @vals.group_by { |context, _val| indices.map { |i| context[i] } }
end

#marginalize(var_names) ⇒ Object

Returns new context defined over ‘var_names`, all other variables get eliminated. For every combination of `var_names`’s values the value of new factor is defined by summing up values in original factor having compatible value



118
119
120
121
122
123
124
125
126
# File 'lib/bayesnet/factor.rb', line 118

def marginalize(var_names)
  scope = @scope.slice(*var_names)

  indices = scope.keys.map { |k| index_by_var_name[k] }
  vals = @vals.group_by { |context, _val| indices.map { |i| context[i] } }
  vals.transform_values! { |v| v.map(&:last).sum }

  self.class.new(scope, vals)
end

#normalizeObject

returns new normalized factor, i.e. where sum of all values is 1.0



88
89
90
91
92
93
# File 'lib/bayesnet/factor.rb', line 88

def normalize
  vals = @vals.clone
  norm_factor = vals.map(&:last).sum * 1.0
  vals.each { |k, _v| vals[k] /= norm_factor }
  self.class.new(@scope.clone, vals)
end

#reduce_to(context) ⇒ Object

Returns factor built as follows:

  1. Original factor gets filtered out by variables having values compatible with ‘context`

  2. Returned factor does not have any variables from ‘context` (because they have

same values, after step 1) The ‘context` argument supposed to be an evidence, somewhat like `:sunny`



101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/bayesnet/factor.rb', line 101

def reduce_to(context)
  limited_context = context.slice(*scope.keys)
  return self.class.new(@scope, @vals) if limited_context.empty?
  limited_scope = @scope.slice(*(@scope.keys - limited_context.keys))

  context_vals = limited_context.values
  indices = limited_context.keys.map { |k| index_by_var_name[k] }
  vals = @vals.select { |k, _v| indices.map { |i| k[i] } == context_vals }
  vals.transform_keys! { |k| delete_by_indices(k, indices) }

  self.class.new(limited_scope, vals)
end

#scope(var_name_to_values = nil) ⇒ Object

Factor DSL Defining variable with list of its possible values looks like: “‘ Bayesnet::Factor.build do

scope weather: %i[sunny cloudy]
scope mood: %i[bad good]
...

“‘ ^ this code defines to variables `weather` and `mood`, where `weather` could be :sunny or :cloudy, and `mood` could be :bad or :good



27
28
29
30
31
32
33
# File 'lib/bayesnet/factor.rb', line 27

def scope(var_name_to_values = nil)
  if var_name_to_values
    @scope.merge!(var_name_to_values)
  else
    @scope
  end
end

#select(subcontext) ⇒ Object



134
135
136
137
138
# File 'lib/bayesnet/factor.rb', line 134

def select(subcontext)
  @vals.select do |context, _|
    var_names.zip(context).slice(subcontext.keys) == subcontext
  end
end

#val(*context_and_val) ⇒ Object

Factor DSL Specifies factor value for some set of variable values, i.e. “‘ Bayesnet::Factor.build do

scope weather: %i[sunny cloudy]
scope mood: %i[bad good]
val :sunny, :bad, 0.1
...

“‘ ^ this code says the value of factor for [weather == :sunny, mood == :bad] is 0.1



45
46
47
48
# File 'lib/bayesnet/factor.rb', line 45

def val(*context_and_val)
  context_and_val = context_and_val[0] if context_and_val.size == 1 && context_and_val[0].is_a?(Array)
  @vals[context_and_val[0..-2]] = context_and_val[-1]
end

#valuesObject

returns all possible values



83
84
85
# File 'lib/bayesnet/factor.rb', line 83

def values
  @vals.values
end

#var_namesObject

List of variable names



52
53
54
# File 'lib/bayesnet/factor.rb', line 52

def var_names
  @scope.keys
end