machine_learning_workbench 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +37 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/machine_learning_workbench.rb +19 -0
- data/lib/machine_learning_workbench/compressor.rb +1 -0
- data/lib/machine_learning_workbench/compressor/vector_quantization.rb +74 -0
- data/lib/machine_learning_workbench/monkey.rb +197 -0
- data/lib/machine_learning_workbench/neural_network.rb +3 -0
- data/lib/machine_learning_workbench/neural_network/base.rb +211 -0
- data/lib/machine_learning_workbench/neural_network/feed_forward.rb +20 -0
- data/lib/machine_learning_workbench/neural_network/recurrent.rb +35 -0
- data/lib/machine_learning_workbench/optimizer.rb +7 -0
- data/lib/machine_learning_workbench/optimizer/natural_evolution_strategies/base.rb +112 -0
- data/lib/machine_learning_workbench/optimizer/natural_evolution_strategies/bdnes.rb +104 -0
- data/lib/machine_learning_workbench/optimizer/natural_evolution_strategies/snes.rb +40 -0
- data/lib/machine_learning_workbench/optimizer/natural_evolution_strategies/xnes.rb +46 -0
- data/lib/machine_learning_workbench/tools.rb +4 -0
- data/lib/machine_learning_workbench/tools/execution.rb +18 -0
- data/lib/machine_learning_workbench/tools/imaging.rb +48 -0
- data/lib/machine_learning_workbench/tools/normalization.rb +22 -0
- data/lib/machine_learning_workbench/tools/verification.rb +11 -0
- data/machine_learning_workbench.gemspec +36 -0
- metadata +216 -0
@@ -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
|