machine_learning_workbench 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +5 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +70 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +37 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/machine_learning_workbench.rb +19 -0
  14. data/lib/machine_learning_workbench/compressor.rb +1 -0
  15. data/lib/machine_learning_workbench/compressor/vector_quantization.rb +74 -0
  16. data/lib/machine_learning_workbench/monkey.rb +197 -0
  17. data/lib/machine_learning_workbench/neural_network.rb +3 -0
  18. data/lib/machine_learning_workbench/neural_network/base.rb +211 -0
  19. data/lib/machine_learning_workbench/neural_network/feed_forward.rb +20 -0
  20. data/lib/machine_learning_workbench/neural_network/recurrent.rb +35 -0
  21. data/lib/machine_learning_workbench/optimizer.rb +7 -0
  22. data/lib/machine_learning_workbench/optimizer/natural_evolution_strategies/base.rb +112 -0
  23. data/lib/machine_learning_workbench/optimizer/natural_evolution_strategies/bdnes.rb +104 -0
  24. data/lib/machine_learning_workbench/optimizer/natural_evolution_strategies/snes.rb +40 -0
  25. data/lib/machine_learning_workbench/optimizer/natural_evolution_strategies/xnes.rb +46 -0
  26. data/lib/machine_learning_workbench/tools.rb +4 -0
  27. data/lib/machine_learning_workbench/tools/execution.rb +18 -0
  28. data/lib/machine_learning_workbench/tools/imaging.rb +48 -0
  29. data/lib/machine_learning_workbench/tools/normalization.rb +22 -0
  30. data/lib/machine_learning_workbench/tools/verification.rb +11 -0
  31. data/machine_learning_workbench.gemspec +36 -0
  32. metadata +216 -0
@@ -0,0 +1,3 @@
1
+ require_relative 'neural_network/base'
2
+ require_relative 'neural_network/feed_forward'
3
+ require_relative 'neural_network/recurrent'
@@ -0,0 +1,211 @@
1
+
2
+ module MachineLearningWorkbench::NeuralNetwork
3
+ # Neural Network base class
4
+ class Base
5
+
6
+ # @!attribute [r] layers
7
+ # List of matrices, each being the weights
8
+ # connecting a layer's inputs (rows) to a layer's neurons (columns),
9
+ # hence its shape is `[ninputs, nneurs]`
10
+ # @return [Array<NMatrix>] list of weight matrices, each uniquely describing a layer
11
+ # @!attribute [r] state
12
+ # It's a list of one-dimensional matrices, each an input to a layer, plus the output layer's output. The first element is the input to the first layer of the network, which is composed of the network's input, possibly the first layer's activation on the last input (recursion), and a bias (fixed `1`). The second to but-last entries follow the same structure, but with the previous layer's output in place of the network's input. The last entry is the activation of the output layer, without additions since it's not used as an input by anyone.
13
+ # @return [Array<NMatrix>] current state of the network.
14
+ # @!attribute [r] act_fn
15
+ # activation function, common to all neurons (for now)
16
+ # @return [#call] activation function
17
+ # @!attribute [r] struct
18
+ # list of number of (inputs or) neurons in each layer
19
+ # @return [Array<Integer>] structure of the network
20
+ attr_reader :layers, :state, :act_fn, :struct
21
+
22
+
23
+ ## Initialization
24
+
25
+ # @param struct [Array<Integer>] list of layer sizes
26
+ # @param act_fn [Symbol] choice of activation function for the neurons
27
+ def initialize struct, act_fn: nil
28
+ @struct = struct
29
+ @act_fn = self.class.act_fn(act_fn || :sigmoid)
30
+ # @state holds both inputs, possibly recurrency, and bias
31
+ # it is a complete input for the next layer, hence size from layer sizes
32
+ @state = layer_row_sizes.collect do |size|
33
+ NMatrix.zeros([1, size], dtype: :float64)
34
+ end
35
+ # to this, append a matrix to hold the final network output
36
+ @state.push NMatrix.zeros([1, nneurs(-1)], dtype: :float64)
37
+ reset_state
38
+ end
39
+
40
+ # Reset the network to the initial state
41
+ def reset_state
42
+ @state.each do |m| # state has only single-row matrices
43
+ # reset all to zero
44
+ m[0,0..-1] = 0
45
+ # add bias to all but output
46
+ m[0,-1] = 1 unless m.object_id == @state.last.object_id
47
+ end
48
+ end
49
+
50
+ # Initialize the network with random weights
51
+ def init_random
52
+ # Will only be used for testing, no sense optimizing it (NMatrix#rand)
53
+ # Reusing #load_weights instead helps catching bugs
54
+ load_weights nweights.times.collect { rand(-1.0..1.0) }
55
+ end
56
+
57
+ ## Weight utilities
58
+
59
+ # Resets memoization: needed to play with structure modification
60
+ def deep_reset
61
+ # reset memoization
62
+ [:@layer_row_sizes, :@layer_col_sizes, :@nlayers, :@layer_shapes,
63
+ :@nweights_per_layer, :@nweights].each do |sym|
64
+ instance_variable_set sym, nil
65
+ end
66
+ reset_state
67
+ end
68
+
69
+ # Total weights in the network
70
+ # @return [Integer] total number of weights
71
+ def nweights
72
+ @nweights ||= nweights_per_layer.reduce(:+)
73
+ end
74
+
75
+ # List of per-layer number of weights
76
+ # @return [Array<Integer>] list of weights per each layer
77
+ def nweights_per_layer
78
+ @nweights_per_layer ||= layer_shapes.collect { |shape| shape.reduce(:*) }
79
+ end
80
+
81
+ # Count the layers. This is a computation helper, and for this implementation
82
+ # the inputs are considered as if a layer like the others.
83
+ # @return [Integer] number of layers
84
+ def nlayers
85
+ @nlayers ||= layer_shapes.size
86
+ end
87
+
88
+ # Returns the weight matrix
89
+ # @return [Array] three-dimensional Array of weights: a list of weight
90
+ # matrices, one for each layer.
91
+ def weights
92
+ layers.collect(&:to_consistent_a)
93
+ end
94
+
95
+ # Number of neurons per layer. Although this implementation includes inputs
96
+ # in the layer counts, this methods correctly ignores the input as not having
97
+ # neurons.
98
+ # @return [Array] list of neurons per each (proper) layer (i.e. no inputs)
99
+ def layer_col_sizes
100
+ @layer_col_sizes ||= struct.drop(1)
101
+ end
102
+
103
+ # define #layer_row_sizes in child class: number of inputs per layer
104
+
105
+ # Shapes for the weight matrices, each corresponding to a layer
106
+ # @return [Array<Array[Integer, Integer]>] Weight matrix shapes
107
+ def layer_shapes
108
+ @layer_shapes ||= layer_row_sizes.zip layer_col_sizes
109
+ end
110
+
111
+ # Count the neurons in a particular layer or in the whole network.
112
+ # @param nlay [Integer, nil] the layer of interest, 1-indexed.
113
+ # `0` will return the number of inputs.
114
+ # `nil` will compute the total neurons in the network.
115
+ # @return [Integer] the number of neurons in a given layer, or in all network, or the number of inputs
116
+ def nneurs nlay=nil
117
+ nlay.nil? ? struct.reduce(:+) : struct[nlay]
118
+ end
119
+
120
+ # Loads a plain list of weights into the weight matrices (one per layer).
121
+ # Preserves order.
122
+ # @input weights [Array<Float>] weights to load
123
+ # @return [true] always true. If something's wrong it simply fails, and if
124
+ # all goes well there's nothing to return but a confirmation to the caller.
125
+ def load_weights weights
126
+ raise "Hell!" unless weights.size == nweights
127
+ weights_iter = weights.each
128
+ @layers = layer_shapes.collect do |shape|
129
+ NMatrix.new(shape, dtype: :float64) { weights_iter.next }
130
+ end
131
+ reset_state
132
+ return true
133
+ end
134
+
135
+
136
+ ## Activation
137
+
138
+ # The "fixed `1`" used in the layer's input
139
+ def bias
140
+ @bias ||= NMatrix[[1], dtype: :float64]
141
+ end
142
+
143
+ # Activate the network on a given input
144
+ # @param input [Array<Float>] the given input
145
+ # @return [Array] the activation of the output layer
146
+ def activate input
147
+ raise "Hell!" unless input.size == struct.first
148
+ raise "Hell!" unless input.is_a? Array
149
+ # load input in first state
150
+ @state[0][0, 0..-2] = input
151
+ # activate layers in sequence
152
+ (0...nlayers).each do |i|
153
+ act = activate_layer i
154
+ @state[i+1][0,0...act.size] = act
155
+ end
156
+ return out
157
+ end
158
+
159
+ # Extract and convert the output layer's activation
160
+ # @return [Array] the activation of the output layer as 1-dim Array
161
+ def out
162
+ state.last.to_flat_a
163
+ end
164
+
165
+ # define #activate_layer in child class
166
+
167
+ ## Activation functions
168
+
169
+ # Activation function caller. Allows to cleanly define the activation function as one-dimensional, by calling it over the inputs and building a NMatrix to return.
170
+ # @return [NMatrix] activations for one layer
171
+ def self.act_fn type, *args
172
+ fn = send(type,*args)
173
+ lambda do |inputs|
174
+ NMatrix.new([1, inputs.size], dtype: :float64) do |_,i|
175
+ # single-row matrix, indices are columns
176
+ fn.call inputs[i]
177
+ end
178
+ end
179
+ end
180
+
181
+ # Traditional sigmoid with variable steepness
182
+ def self.sigmoid k=0.5
183
+ # k is steepness: 0<k<1 is flatter, 1<k is flatter
184
+ # flatter makes activation less sensitive, better with large number of inputs
185
+ lambda { |x| 1.0 / (Math.exp(-k * x) + 1.0) }
186
+ end
187
+
188
+ # Traditional logistic
189
+ def self.logistic
190
+ lambda { |x|
191
+ exp = Math.exp(x)
192
+ exp.infinite? ? exp : exp / (1.0 + exp)
193
+ }
194
+ end
195
+
196
+ # LeCun hyperbolic activation
197
+ # @see http://yann.lecun.com/exdb/publis/pdf/lecun-98b.pdf Section 4.4
198
+ def self.lecun_hyperbolic
199
+ lambda { |x| 1.7159 * Math.tanh(2.0*x/3.0) + 1e-3*x }
200
+ end
201
+
202
+
203
+ # @!method interface_methods
204
+ # Declaring interface methods - implement in child class!
205
+ [:layer_row_sizes, :activate_layer].each do |sym|
206
+ define_method sym do
207
+ raise NotImplementedError, "Implement ##{sym} in child class!"
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,20 @@
1
+
2
+ module MachineLearningWorkbench::NeuralNetwork
3
+ # Feed Forward Neural Network
4
+ class FeedForward < Base
5
+
6
+ # Calculate the size of each row in a layer's weight matrix.
7
+ # Includes inputs (or previous-layer activations) and bias.
8
+ # @return [Array<Integer>] per-layer row sizes
9
+ def layer_row_sizes
10
+ @layer_row_sizes ||= struct.each_cons(2).collect {|prev, _curr| prev+1}
11
+ end
12
+
13
+ # Activates a layer of the network
14
+ # @param i [Integer] the layer to activate, zero-indexed
15
+ def activate_layer i
16
+ act_fn.call( state[i].dot layers[i] )
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+
2
+ module MachineLearningWorkbench::NeuralNetwork
3
+ # Recurrent Neural Network
4
+ class Recurrent < Base
5
+
6
+ # Calculate the size of each row in a layer's weight matrix.
7
+ # Each row holds the inputs for the next level: previous level's
8
+ # activations (or inputs), this level's last activations
9
+ # (recursion) and bias.
10
+ # @return [Array<Integer>] per-layer row sizes
11
+ def layer_row_sizes
12
+ @layer_row_sizes ||= struct.each_cons(2).collect do |prev, rec|
13
+ prev + rec + 1
14
+ end
15
+ end
16
+
17
+ # Activates a layer of the network.
18
+ # Bit more complex since it has to copy the layer's activation on
19
+ # last input to its own inputs, for recursion.
20
+ # @param i [Integer] the layer to activate, zero-indexed
21
+ def activate_layer nlay #_layer
22
+ # NOTE: current layer index corresponds to index of next state!
23
+ previous = nlay # index of previous layer (inputs)
24
+ current = nlay + 1 # index of current layer (outputs)
25
+ # Copy the level's last-time activation to the input (previous state)
26
+ # NOTE: ranges in NMatrix#[] not reliable! gotta loop :(
27
+ nneurs(current).times do |i| # for each activations to copy
28
+ # Copy output from last-time activation to recurrency in previous state
29
+ @state[previous][0, nneurs(previous) + i] = state[current][0, i]
30
+ end
31
+ act_fn.call state[previous].dot layers[nlay]
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ module MachineLearningWorkbench::Optimizer
2
+ end
3
+
4
+ require_relative 'optimizer/natural_evolution_strategies/base'
5
+ require_relative 'optimizer/natural_evolution_strategies/xnes'
6
+ require_relative 'optimizer/natural_evolution_strategies/snes'
7
+ require_relative 'optimizer/natural_evolution_strategies/bdnes'
@@ -0,0 +1,112 @@
1
+
2
+ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
3
+ # Natural Evolution Strategies base class
4
+ class Base
5
+ attr_reader :ndims, :mu, :sigma, :opt_type, :obj_fn, :id, :rng, :last_fits, :best
6
+
7
+ # NES object initialization
8
+ # @param ndims [Integer] number of parameters to optimize
9
+ # @param obj_fn [#call] any object defining a #call method (Proc, lambda, custom class)
10
+ # @param opt_type [:min, :max] select minimization / maximization of obj_fn
11
+ # @param rseed [Integer] allow for deterministic execution on rseed provided
12
+ def initialize ndims, obj_fn, opt_type, rseed: nil, mu_init: 0, sigma_init: 1
13
+ raise ArgumentError unless [:min, :max].include? opt_type
14
+ raise ArgumentError unless obj_fn.respond_to? :call
15
+ @ndims, @opt_type, @obj_fn = ndims, opt_type, obj_fn
16
+ @id = NMatrix.identity(ndims, dtype: :float64)
17
+ rseed ||= Random.new_seed
18
+ # puts "NES rseed: #{s}" # currently disabled
19
+ @rng = Random.new rseed
20
+ @best = [(opt_type==:max ? -1 : 1) * Float::INFINITY, nil]
21
+ @last_fits = []
22
+ initialize_distribution mu_init: mu_init, sigma_init: sigma_init
23
+ end
24
+
25
+ # Box-Muller transform: generates standard (unit) normal distribution samples
26
+ # @return [Float] a single sample from a standard normal distribution
27
+ def standard_normal_sample
28
+ rho = Math.sqrt(-2.0 * Math.log(rng.rand))
29
+ theta = 2 * Math::PI * rng.rand
30
+ tfn = rng.rand > 0.5 ? :cos : :sin
31
+ rho * Math.send(tfn, theta)
32
+ end
33
+
34
+ # Memoized automatic magic numbers
35
+ # NOTE: Doubling popsize and halving lrate often helps
36
+ def utils; @utilities ||= cmaes_utilities end
37
+ # (see #utils)
38
+ def popsize; @popsize ||= cmaes_popsize * 2 end
39
+ # (see #utils)
40
+ def lrate; @lrate ||= cmaes_lrate end
41
+
42
+ # Magic numbers from CMA-ES (TODO: add proper citation)
43
+ # @return [NMatrix] scale-invariant utilities
44
+ def cmaes_utilities
45
+ # Algorithm equations are meant for fitness maximization
46
+ # Match utilities with individuals sorted by INCREASING fitness
47
+ log_range = (1..popsize).collect do |v|
48
+ [0, Math.log(popsize.to_f/2 - 1) - Math.log(v)].max
49
+ end
50
+ total = log_range.reduce(:+)
51
+ buf = 1.0/popsize
52
+ vals = log_range.collect { |v| v / total - buf }.reverse
53
+ NMatrix[vals, dtype: :float64]
54
+ end
55
+
56
+ # (see #cmaes_utilities)
57
+ # @return [Float] learning rate lower bound
58
+ def cmaes_lrate
59
+ (3+Math.log(ndims)) / (5*Math.sqrt(ndims))
60
+ end
61
+
62
+ # (see #cmaes_utilities)
63
+ # @return [Integer] population size lower bound
64
+ def cmaes_popsize
65
+ [5, 4 + (3*Math.log(ndims)).floor].max
66
+ end
67
+
68
+ # Samples a standard normal distribution to construct a NMatrix of
69
+ # popsize multivariate samples of length ndims
70
+ # @return [NMatrix] standard normal samples
71
+ def standard_normal_samples
72
+ NMatrix.new([popsize, ndims], dtype: :float64) { standard_normal_sample }
73
+ end
74
+
75
+ # Move standard normal samples to current distribution
76
+ # @return [NMatrix] individuals
77
+ def move_inds inds
78
+ # TODO: can we reduce the transpositions?
79
+ # sigma.dot(inds.transpose).map(&mu.method(:+)).transpose
80
+ multi_mu = NMatrix[*inds.rows.times.collect {mu.to_a}, dtype: :float64].transpose
81
+ (multi_mu + sigma.dot(inds.transpose)).transpose
82
+ # sigma.dot(inds.transpose).transpose + inds.rows.times.collect {mu.to_a}.to_nm
83
+ end
84
+
85
+ # Sorted individuals
86
+ # NOTE: Algorithm equations are meant for fitness maximization. Utilities need to be
87
+ # matched with individuals sorted by INCREASING fitness. Then reverse order for minimization.
88
+ # @return standard normal samples sorted by the respective individuals' fitnesses
89
+ def sorted_inds
90
+ samples = standard_normal_samples
91
+ inds = move_inds(samples).to_a
92
+ fits = obj_fn.call(inds)
93
+ # Quick cure for NaN fitnesses
94
+ fits.map! { |x| x.nan? ? (opt_type==:max ? -1 : 1) * Float::INFINITY : x }
95
+ @last_fits = fits # allows checking for stagnation
96
+ sorted = [fits, inds, samples.to_a].transpose.sort_by(&:first)
97
+ sorted.reverse! if opt_type==:min
98
+ this_best = sorted.last.take(2)
99
+ opt_cmp_fn = opt_type==:min ? :< : :>
100
+ @best = this_best if this_best.first.send(opt_cmp_fn, best.first)
101
+ NMatrix[*sorted.map(&:last), dtype: :float64]
102
+ end
103
+
104
+ # @!method interface_methods
105
+ # Declaring interface methods - implement these in child class!
106
+ [:train, :initialize_distribution, :convergence].each do |mname|
107
+ define_method mname do
108
+ raise NotImplementedError, "Implement in child class!"
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,104 @@
1
+
2
+ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
3
+ # Block-Diagonal Natural Evolution Strategies
4
+ class BDNES < Base
5
+
6
+ MAX_RSEED = 10**Random.new_seed.size # same range as Random.new_seed
7
+
8
+ attr_reader :ndims_lst, :obj_fn, :opt_type, :blocks, :popsize, :rng,
9
+ :best, :last_fits
10
+
11
+ # initialize a list of XNES for each block
12
+ def initialize ndims_lst, obj_fn, opt_type, rseed: nil, **init_opts
13
+ # mu_init: 0, sigma_init: 1
14
+ # init_opts = {rseed: rseed, mu_init: mu_init, sigma_init: sigma_init}
15
+ # TODO: accept list of `mu_init`s and `sigma_init`s
16
+ @ndims_lst, @obj_fn, @opt_type = ndims_lst, obj_fn, opt_type
17
+ block_fit = -> (*args) { raise "Should never be called" }
18
+ # the BD-NES seed should ensure deterministic reproducibility
19
+ # but each block should have a different seed
20
+ rseed ||= Random.new_seed
21
+ # puts "BD-NES rseed: #{s}" # currently disabled
22
+ @rng = Random.new rseed
23
+ @blocks = ndims_lst.map do |ndims|
24
+ b_rseed = rng.rand MAX_RSEED
25
+ XNES.new ndims, block_fit, opt_type, rseed: b_rseed, **init_opts
26
+ end
27
+ # Need `popsize` to be the same for all blocks, to make complete individuals
28
+ @popsize = blocks.map(&:popsize).max
29
+ blocks.each { |xnes| xnes.instance_variable_set :@popsize, popsize }
30
+
31
+ @best = [(opt_type==:max ? -1 : 1) * Float::INFINITY, nil]
32
+ @last_fits = []
33
+ end
34
+
35
+ def sorted_inds_lst
36
+ # Build samples and inds from the list of blocks
37
+ samples_lst, inds_lst = blocks.map do |xnes|
38
+ samples = xnes.standard_normal_samples
39
+ inds = xnes.move_inds(samples)
40
+ [samples.to_a, inds]
41
+ end.transpose
42
+
43
+ # Join the individuals for evaluation
44
+ full_inds = inds_lst.reduce(&:hconcat).to_a
45
+ # Need to fix samples dimensions for sorting
46
+ # - current dims: nblocks x ninds x [block sizes]
47
+ # - for sorting: ninds x nblocks x [block sizes]
48
+ full_samples = samples_lst.transpose
49
+
50
+ # Evaluate fitness of complete individuals
51
+ fits = obj_fn.call(full_inds)
52
+ # Quick cure for NaN fitnesses
53
+ fits.map! { |x| x.nan? ? (opt_type==:max ? -1 : 1) * Float::INFINITY : x }
54
+ @last_fits = fits # allows checking for stagnation
55
+
56
+ # Sort inds based on fit and opt_type, save best
57
+ sorted = [fits, full_inds, full_samples].transpose.sort_by(&:first)
58
+ sorted.reverse! if opt_type==:min
59
+ this_best = sorted.last.take(2)
60
+ opt_cmp_fn = opt_type==:min ? :< : :>
61
+ @best = this_best if this_best.first.send(opt_cmp_fn, best.first)
62
+ sorted_samples = sorted.map(&:last)
63
+
64
+ # Need to bring back sample dimensions for each block
65
+ # - current dims: ninds x nblocks x [block sizes]
66
+ # - target blocks list: nblocks x ninds x [block sizes]
67
+ block_samples = sorted_samples.transpose
68
+
69
+ # then back to NMatrix for usage in training
70
+ block_samples.map { |sample| NMatrix[*sample, dtype: :float64] }
71
+ end
72
+
73
+ # duck-type the interface: [:train, :mu, :convergence, :save, :load]
74
+
75
+ def train picks: sorted_inds_lst
76
+ blocks.zip(sorted_inds_lst).each do |xnes, s_inds|
77
+ xnes.train picks: s_inds
78
+ end
79
+ end
80
+
81
+ def mu
82
+ blocks.map(&:mu).reduce(&:hconcat)
83
+ end
84
+
85
+ def convergence
86
+ blocks.map(&:convergence).reduce(:+)
87
+ end
88
+
89
+ def save
90
+ blocks.map &:save
91
+ end
92
+
93
+ def load data
94
+ # raise "Hell!" unless data.size == 2
95
+ fit = -> (*args) { raise "Should never be called" }
96
+ @blocks = data.map do |block_data|
97
+ ndims = block_data.first.size
98
+ XNES.new(ndims, fit, opt_type).tap do |nes|
99
+ nes.load block_data
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end