machine_learning_workbench 0.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1a0550319ef523cd49f7c09b635a4e21508cf730
4
- data.tar.gz: b3fb9a716bfac1850bc5af0c8abf96f17f0292b6
3
+ metadata.gz: 743cc4c65bda521785e00dc563b20fcd5660a6af
4
+ data.tar.gz: 80924d34dc550df7b25d565022f50297b8bb72d5
5
5
  SHA512:
6
- metadata.gz: 51b05034a2fffcc135388c5760b14067728d4fcf7210ff47d4a7f58f66fb174d3940fd538e3551a78d9df24a0c14b01e0f277aba2ceb898d7206302f8c30721b
7
- data.tar.gz: 0dd1cf85fdb8577278882fe197e90032b1061a11fec9176b8d64ef38b97140876059ec964199007f52840eaab13ca2a25b2b01b8d0935c5f55c2ffc518f59124
6
+ metadata.gz: 2ef96c2884a6f43304f0dba63239a7469b80c7be46c97af7c8de283a448a8ee049b55cdc98b0aa5bfde82fc024fde891c982ec621bb4cafcd6070814e8366976
7
+ data.tar.gz: 0b5571eb26babf90015deec8425dc0a9c75bebd47ef50b4927a131e86f5f0c9e6e828e3ee1b212597222b9aa7209cd54f6b3db7a9c8ec6afcceafe94016165a1
data/bin/setup CHANGED
@@ -4,7 +4,7 @@ IFS=$'\n\t'
4
4
  set -vx
5
5
 
6
6
  # ubuntu:
7
- sudo apt install libatlas-base-dev # for nmatrix
7
+ sudo apt install libopenblas-base # for numo
8
8
 
9
9
  bundle install
10
10
 
@@ -5,7 +5,8 @@ Img = MachineLearningWorkbench::Tools::Imaging
5
5
  Norm = MachineLearningWorkbench::Tools::Normalization
6
6
 
7
7
  ncentrs = 1
8
- image_files = Dir[ENV['HOME']+'/jaffe/KA.HA*.png']
8
+ # image_files = Dir[ENV['HOME']+'/jaffe/KA.HA*.png']
9
+ image_files = Dir[ENV['HOME']+'/jaffe/*.png']
9
10
  raise "Download the JAFFE dataset in your home dir" if image_files&.empty?
10
11
  # ... and convert the `.tiff` in `.png`: `mogrify -format png jaffe/*.tiff`
11
12
  centr_range = [-1, 1]
@@ -14,14 +15,13 @@ img_range = [0, 2**16-1]
14
15
 
15
16
  puts "Loading images"
16
17
  images = image_files.map do |fname|
17
- nmat = Img.nmat_from_png fname, flat: true, dtype: :float64
18
- ret = Norm.feature_scaling nmat, from: img_range, to: centr_range
18
+ ary = Img.narr_from_png fname, flat: true
19
+ ret = Norm.feature_scaling ary, from: img_range, to: centr_range
19
20
  end
20
21
 
21
22
  puts "Initializing VQ"
22
23
  vq = VectorQuantization.new ncentrs: ncentrs,
23
- dims: images.first.shape, lrate: 0.3,
24
- dtype: images.first.dtype, vrange: centr_range
24
+ dims: images.first.shape, lrate: 0.3, vrange: centr_range
25
25
 
26
26
  puts "Training"
27
27
  vq.train images, debug: true
@@ -1,4 +1,20 @@
1
- require 'nmatrix'
1
+
2
+ gpu = false # prepare for switching to GPUs
3
+ if gpu
4
+ require 'cumo/narray'
5
+ require 'cumo/linalg'
6
+ Xumo = Cumo
7
+ else
8
+ require 'numo/narray'
9
+ require 'numo/linalg'
10
+ # currently required for Ubuntu 16.04
11
+ # depends on openblas: `sudo apt install libopenblas-base`
12
+ Fiddle.dlopen("/usr/lib/libopenblas.so")
13
+ Numo::Linalg::Lapack.dlopen("/usr/lib/liblapacke.so.3")
14
+ Xumo = Numo
15
+ end
16
+ NArray = Xumo::DFloat # set a single data type across the WB for now
17
+ NMath = Xumo::NMath # shorthand for extended math module
2
18
 
3
19
  module MachineLearningWorkbench
4
20
  module Compressor
@@ -2,13 +2,12 @@ module MachineLearningWorkbench::Compressor
2
2
 
3
3
  # Standard Vector Quantization
4
4
  class VectorQuantization
5
- attr_reader :ncentrs, :centrs, :dims, :vrange, :dtype, :lrate, :rng, :ntrains
5
+ attr_reader :ncentrs, :centrs, :dims, :vrange, :lrate, :rng, :ntrains
6
6
  Verification = MachineLearningWorkbench::Tools::Verification
7
7
 
8
- def initialize ncentrs:, dims:, vrange:, dtype:, lrate:, rseed: Random.new_seed
8
+ def initialize ncentrs:, dims:, vrange:, lrate:, rseed: Random.new_seed
9
9
  @rng = Random.new rseed
10
10
  @ncentrs = ncentrs
11
- @dtype = dtype
12
11
  @dims = Array(dims)
13
12
  check_lrate lrate # hack: so that we can overload it in online_vq
14
13
  @lrate = lrate
@@ -32,15 +31,13 @@ module MachineLearningWorkbench::Compressor
32
31
 
33
32
  # Creates a new (random) centroid
34
33
  def new_centr
35
- # TODO: this is too slow, find another way to use the rng
36
- # NMatrix.new(dims, dtype: dtype) { rng.rand Range.new *vrange }
37
- NMatrix.random dims, dtype: dtype
34
+ NArray.new(*dims).rand(*vrange)
38
35
  end
39
36
 
40
37
  # Computes similarities between vector and all centroids
41
38
  def similarities vec
42
39
  raise NotImplementedError if vec.shape.size > 1
43
- centrs.map { |c| c.dot(vec).first }
40
+ centrs.map { |c| c.dot(vec) }
44
41
  # require 'parallel'
45
42
  # Parallel.map(centrs) { |c| c.dot(vec).first }
46
43
  end
@@ -85,7 +82,7 @@ module MachineLearningWorkbench::Compressor
85
82
  end
86
83
 
87
84
  # Per-pixel errors in reconstructing vector
88
- # @return [NMatrix] residuals
85
+ # @return [NArray] residuals
89
86
  def reconstr_error vec
90
87
  reconstruction(vec) - vec
91
88
  end
@@ -217,12 +217,112 @@ module MachineLearningWorkbench::Monkey
217
217
  raise NotImplementedError, "There's no setter for the data pointer!"
218
218
  end
219
219
  end
220
+
221
+ module ToNArrayConvertible
222
+ def to_na
223
+ NArray[*self]
224
+ end
225
+ end
226
+
227
+ module NArrayOuterFlattable
228
+ # Flat-output generalized outer relationship. Same as `#outer`, but the
229
+ # result is a 2-dim matrix of the interactions between all the elements
230
+ # in `self` (as rows) and all the elements in `other` (as columns)
231
+ # @param other [NArray] other matrix
232
+ # @return [NArray]
233
+ def outer_flat other
234
+ # TODO: Numo::NArray should be able to implement this with `#outer` and some other
235
+ # function to flatten the right layer -- much faster
236
+ raise ArgumentError, "Need to pass an operand block" unless block_given?
237
+ self.class.zeros([self.size, other.size]).tap do |ret|
238
+ self.size.times do |r|
239
+ other.size.times do |c|
240
+ ret[r,c] = yield self[r], other[c]
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ module NArrayApproximatable
248
+ # Verifies if `self` and `other` are withing `epsilon` of each other.
249
+ # @param other [NArray]
250
+ # @param epsilon [NArray]
251
+ # @return [Boolean]
252
+ def approximates? other, epsilon=1e-5
253
+ ((self - other).abs < epsilon).all?
254
+ end
255
+ end
256
+
257
+ module Invertable
258
+ # Inverses matrix
259
+ # @return [NArray]
260
+ def invert
261
+ Numo::Linalg.inv self
262
+ end
263
+ end
264
+
265
+ module Exponentiable
266
+ # Matrix exponential: `e**self` (not to be confused with `self**n`)
267
+ # @return [NArray]
268
+ def exponential
269
+ raise ArgumentError if ndim > 2
270
+ # special case: one-dimensional matrix: just exponentiate the values
271
+ return Numo::NMath.exp(self) if (ndim == 1) || shape.include?(1)
272
+ # at this point we need to validate it is a square matrix
273
+ raise ArgumentError unless shape.reduce(&:==)
274
+
275
+ # Eigenvalue decomposition method from `scipy/linalg/matfuncs.py#expm2` (deprecated)
276
+ # https://github.com/scipy/scipy/commit/236e0740ba951cb455ba8b6a306abb32740131cf
277
+ # s, vr = eig(A)
278
+ # vri = inv(vr)
279
+ # r = dot(dot(vr, diag(exp(s))), vri)
280
+
281
+ # TODO: this is a simple but outdated method, switch to Pade approximation
282
+ # https://github.com/scipy/scipy/blob/11509c4a98edded6c59423ac44ca1b7f28fba1fd/scipy/sparse/linalg/matfuncs.py#L557
283
+
284
+ # e_values, l_e_vectors, r_e_vectors_t = Numo::Linalg.svd self
285
+ evals, _wi, _vl, r_evecs = Numo::Linalg::Lapack.call(:geev, self, jobvl: false, jobvr: true)
286
+ r_evecs_t = r_evecs#.transpose
287
+ r_evecs_inv = r_evecs_t.invert
288
+ evals_exp_dmat = Numo::NMath.exp(evals).diag
289
+
290
+ # l_e_vectors.dot(e_vals_exp_dmat).dot(l_e_vectors.invert)#.transpose
291
+ r_evecs_t.dot(evals_exp_dmat).dot(r_evecs_inv)
292
+ end
293
+ end
294
+
295
+ module Mappable
296
+ # Maps along a NArray dimension, and returns NArray
297
+ # @return [NArray]
298
+ # NOTE: this indexing is not consistent with NArray, which uses 0 to indicate
299
+ # columns rather than the 0th dimension (rows)
300
+ def map dim=0
301
+ raise ArgumentError unless dim.kind_of?(Integer) && dim.between?(0,ndim)
302
+ # TODO: return iterator instead of raise
303
+ raise NotImplementedError unless block_given?
304
+ indices = [true]*ndim
305
+ ret = []
306
+ shape[dim].times.each do |i|
307
+ indices[dim] = i
308
+ ret << yield(self[*indices])
309
+ end
310
+ self.class[*ret]
311
+ end
312
+ end
313
+
220
314
  end
221
315
 
222
316
  Array.include MachineLearningWorkbench::Monkey::Dimensionable
223
- NMatrix.extend MachineLearningWorkbench::Monkey::Buildable
224
- require 'nmatrix/lapack_plugin' # loads whichever is installed between atlas and lapacke
225
- NMatrix.include MachineLearningWorkbench::Monkey::AdvancelyOperationable
317
+ # NMatrix.extend MachineLearningWorkbench::Monkey::Buildable
318
+ # require 'nmatrix/lapack_plugin' # loads whichever is installed between atlas and lapacke
319
+ # NMatrix.include MachineLearningWorkbench::Monkey::AdvancelyOperationable
226
320
  Numeric.include MachineLearningWorkbench::Monkey::NumericallyApproximatable
227
- NMatrix.include MachineLearningWorkbench::Monkey::MatrixApproximatable
228
- NMatrix.include MachineLearningWorkbench::Monkey::CPtrDumpable
321
+ # NMatrix.include MachineLearningWorkbench::Monkey::MatrixApproximatable
322
+ # NMatrix.include MachineLearningWorkbench::Monkey::CPtrDumpable
323
+ Array.include MachineLearningWorkbench::Monkey::ToNArrayConvertible
324
+ NArray.include MachineLearningWorkbench::Monkey::NArrayApproximatable
325
+ NArray.include MachineLearningWorkbench::Monkey::NArrayOuterFlattable
326
+ NArray.include MachineLearningWorkbench::Monkey::Exponentiable
327
+ NArray.include MachineLearningWorkbench::Monkey::Invertable
328
+ NArray.prepend MachineLearningWorkbench::Monkey::Mappable
@@ -7,51 +7,52 @@ module MachineLearningWorkbench::NeuralNetwork
7
7
  # List of matrices, each being the weights
8
8
  # connecting a layer's inputs (rows) to a layer's neurons (columns),
9
9
  # hence its shape is `[ninputs, nneurs]`
10
- # @return [Array<NMatrix>] list of weight matrices, each uniquely describing a layer
10
+ # @return [Array<NArray>] list of weight matrices, each uniquely describing a layer
11
+ # TODO: return a NArray after the usage of `#map` is figured out
11
12
  # @!attribute [r] state
12
13
  # 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
+ # TODO: return a NArray after the usage of `#map` is figured out
15
+ # @return [Array<NArray>] current state of the network.
14
16
  # @!attribute [r] act_fn
15
17
  # activation function, common to all neurons (for now)
16
18
  # @return [#call] activation function
17
19
  # @!attribute [r] struct
18
20
  # list of number of (inputs or) neurons in each layer
19
21
  # @return [Array<Integer>] structure of the network
20
- attr_reader :layers, :state, :act_fn, :struct, :dtype
22
+ attr_reader :layers, :state, :act_fn, :act_fn_name, :struct
21
23
 
22
24
 
23
25
  ## Initialization
24
26
 
25
27
  # @param struct [Array<Integer>] list of layer sizes
26
28
  # @param act_fn [Symbol] choice of activation function for the neurons
27
- # @param dtype [NMatrix dtype] NMatrix dtype for weights and states
28
- def initialize struct, act_fn: nil, dtype: :float32
29
+ def initialize struct, act_fn: nil
29
30
  @struct = struct
30
- @act_fn = self.get_act_fn(act_fn || :sigmoid)
31
+ @act_fn_name = act_fn || :sigmoid
32
+ @act_fn = send(act_fn_name)
31
33
  # @state holds both inputs, possibly recurrency, and bias
32
34
  # it is a complete input for the next layer, hence size from layer sizes
33
35
  @state = layer_row_sizes.collect do |size|
34
- NMatrix.zeros([1, size], dtype: dtype)
36
+ NArray.zeros [1, size]
35
37
  end
36
38
  # to this, append a matrix to hold the final network output
37
- @state.push NMatrix.zeros([1, nneurs(-1)], dtype: dtype)
39
+ @state.push NArray.zeros [1, nneurs(-1)]
38
40
  reset_state
39
41
  end
40
42
 
41
43
  # Reset the network to the initial state
42
44
  def reset_state
43
- @state.each do |m| # state has only single-row matrices
44
- # reset all to zero
45
- m[0,0..-1] = 0
46
- # add bias to all but output
47
- m[0,-1] = 1 unless m.object_id == @state.last.object_id
45
+ state.each do |s|
46
+ s.fill 0 # reset state to zero
47
+ s[0,-1] = 1 # add bias
48
48
  end
49
+ state[-1][0,-1] = 0 # last layer has no bias
49
50
  end
50
51
 
51
52
  # Initialize the network with random weights
52
53
  def init_random
53
- # Will only be used for testing, no sense optimizing it (NMatrix#rand)
54
- # Reusing #load_weights instead helps catching bugs
54
+ # Will only be used for testing, no sense optimizing it now (NArray#rand)
55
+ # Reusing `#load_weights` instead helps catching bugs
55
56
  load_weights nweights.times.collect { rand(-1.0..1.0) }
56
57
  end
57
58
 
@@ -90,7 +91,7 @@ module MachineLearningWorkbench::NeuralNetwork
90
91
  # @return [Array] three-dimensional Array of weights: a list of weight
91
92
  # matrices, one for each layer.
92
93
  def weights
93
- layers.collect(&:to_consistent_a)
94
+ layers.collect(&:to_a)
94
95
  end
95
96
 
96
97
  # Number of neurons per layer. Although this implementation includes inputs
@@ -126,10 +127,10 @@ module MachineLearningWorkbench::NeuralNetwork
126
127
  def load_weights weights
127
128
  raise ArgumentError unless weights.size == nweights
128
129
  weights_iter = weights.each
129
- @layers ||= layer_shapes.collect { |shape| NMatrix.new shape, dtype: dtype }
130
- layers.each do |nmat|
131
- nmat.each_with_indices do |_val, *idxs|
132
- nmat[*idxs] = weights_iter.next
130
+ @layers ||= layer_shapes.collect { |shape| NArray.zeros shape }
131
+ layers.each do |narr|
132
+ narr.each_with_index do |_val, *idxs|
133
+ narr[*idxs] = weights_iter.next
133
134
  end
134
135
  end
135
136
  reset_state
@@ -139,11 +140,6 @@ module MachineLearningWorkbench::NeuralNetwork
139
140
 
140
141
  ## Activation
141
142
 
142
- # The "fixed `1`" used in the layer's input
143
- def bias
144
- @bias ||= NMatrix[[1], dtype: dtype]
145
- end
146
-
147
143
  # Activate the network on a given input
148
144
  # @param input [Array<Float>] the given input
149
145
  # @return [Array] the activation of the output layer
@@ -153,9 +149,9 @@ module MachineLearningWorkbench::NeuralNetwork
153
149
  # load input in first state
154
150
  @state[0][0, 0..-2] = input
155
151
  # activate layers in sequence
156
- (0...nlayers).each do |i|
152
+ nlayers.times.each do |i|
157
153
  act = activate_layer i
158
- @state[i+1][0,0...act.size] = act
154
+ @state[i+1][0, 0...act.size] = act
159
155
  end
160
156
  return out
161
157
  end
@@ -163,49 +159,38 @@ module MachineLearningWorkbench::NeuralNetwork
163
159
  # Extract and convert the output layer's activation
164
160
  # @return [Array] the activation of the output layer as 1-dim Array
165
161
  def out
166
- state.last.to_flat_a
162
+ state.last.to_a.flatten
167
163
  end
168
164
 
169
165
  # define #activate_layer in child class
170
166
 
171
167
  ## Activation functions
172
168
 
173
- # 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.
174
- # @return [NMatrix] activations for one layer
175
- def get_act_fn type, *args
176
- fn = send(type,*args)
177
- lambda do |inputs|
178
- NMatrix.new([1, inputs.size], dtype: dtype) do |_,i|
179
- # single-row matrix, indices are columns
180
- fn.call inputs[i]
181
- end
182
- end
183
- end
184
-
185
169
  # Traditional sigmoid with variable steepness
186
170
  def sigmoid k=0.5
187
171
  # k is steepness: 0<k<1 is flatter, 1<k is flatter
188
172
  # flatter makes activation less sensitive, better with large number of inputs
189
- lambda { |x| 1.0 / (Math.exp(-k * x) + 1.0) }
173
+ lambda { |x| 1.0 / (Numo::NMath.exp(-k * x) + 1.0) }
190
174
  end
191
175
 
192
176
  # Traditional logistic
193
177
  def logistic
194
178
  lambda { |x|
195
- exp = Math.exp(x)
196
- exp.infinite? ? exp : exp / (1.0 + exp)
179
+ exp = Numo::NMath.exp(x)
180
+ # exp.infinite? ? exp : exp / (1.0 + exp)
181
+ exp / (1.0 + exp)
197
182
  }
198
183
  end
199
184
 
200
185
  # LeCun hyperbolic activation
201
186
  # @see http://yann.lecun.com/exdb/publis/pdf/lecun-98b.pdf Section 4.4
202
187
  def lecun_hyperbolic
203
- lambda { |x| 1.7159 * Math.tanh(2.0*x/3.0) + 1e-3*x }
188
+ lambda { |x| 1.7159 * Numo::NMath.tanh(2.0*x/3.0) + 1e-3*x }
204
189
  end
205
190
 
206
191
  # Rectified Linear Unit (ReLU)
207
192
  def relu
208
- lambda { |x| x>0 && x || 0 }
193
+ lambda { |x| (x>0).all? && x || x.class.zeros(x.shape) }
209
194
  end
210
195
 
211
196
 
@@ -23,7 +23,7 @@ module MachineLearningWorkbench::NeuralNetwork
23
23
  previous = nlay # index of previous layer (inputs)
24
24
  current = nlay + 1 # index of current layer (outputs)
25
25
  # Copy the level's last-time activation to the input (previous state)
26
- # NOTE: ranges in NMatrix#[] not reliable! gotta loop :(
26
+ # TODO: ranges in `NArray#[]` should be reliable, get rid of loop
27
27
  nneurs(current).times do |i| # for each activations to copy
28
28
  # Copy output from last-time activation to recurrency in previous state
29
29
  @state[previous][0, nneurs(previous) + i] = state[current][0, i]
@@ -2,7 +2,7 @@
2
2
  module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
3
3
  # Natural Evolution Strategies base class
4
4
  class Base
5
- attr_reader :ndims, :mu, :sigma, :opt_type, :obj_fn, :parallel_fit, :id, :rng, :last_fits, :best, :rescale_popsize, :rescale_lrate, :dtype
5
+ attr_reader :ndims, :mu, :sigma, :opt_type, :obj_fn, :parallel_fit, :eye, :rng, :last_fits, :best, :rescale_popsize, :rescale_lrate
6
6
 
7
7
  # NES object initialization
8
8
  # @param ndims [Integer] number of parameters to optimize
@@ -19,24 +19,23 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
19
19
  # a single instance.
20
20
  # @param rescale_popsize [Float] scaling for the default population size
21
21
  # @param rescale_lrate [Float] scaling for the default learning rate
22
- # @param dtype [NMatrix dtype] NMatrix dtype for all matrix computation
23
- def initialize ndims, obj_fn, opt_type, rseed: nil, mu_init: 0, sigma_init: 1, parallel_fit: false, rescale_popsize: 1, rescale_lrate: 1, dtype: :float64
22
+ def initialize ndims, obj_fn, opt_type, rseed: nil, mu_init: 0, sigma_init: 1, parallel_fit: false, rescale_popsize: 1, rescale_lrate: 1
24
23
  raise ArgumentError unless [:min, :max].include? opt_type
25
24
  raise ArgumentError unless obj_fn.respond_to? :call
26
25
  @ndims, @opt_type, @obj_fn, @parallel_fit = ndims, opt_type, obj_fn, parallel_fit
27
26
  @rescale_popsize, @rescale_lrate = rescale_popsize, rescale_lrate
28
- @id = NMatrix.identity(ndims, dtype: dtype)
27
+ @eye = NArray.eye(ndims)
29
28
  rseed ||= Random.new_seed
30
29
  # puts "NES rseed: #{s}" # currently disabled
31
30
  @rng = Random.new rseed
32
31
  @best = [(opt_type==:max ? -1 : 1) * Float::INFINITY, nil]
33
32
  @last_fits = []
34
- @dtype = dtype
35
33
  initialize_distribution mu_init: mu_init, sigma_init: sigma_init
36
34
  end
37
35
 
38
36
  # Box-Muller transform: generates standard (unit) normal distribution samples
39
37
  # @return [Float] a single sample from a standard normal distribution
38
+ # @note Numo::NArray implements this :) glad to have switched!
40
39
  def standard_normal_sample
41
40
  rho = Math.sqrt(-2.0 * Math.log(rng.rand))
42
41
  theta = 2 * Math::PI * rng.rand
@@ -53,7 +52,7 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
53
52
  def lrate; @lrate ||= cmaes_lrate * rescale_lrate end
54
53
 
55
54
  # Magic numbers from CMA-ES (TODO: add proper citation)
56
- # @return [NMatrix] scale-invariant utilities
55
+ # @return [NArray] scale-invariant utilities
57
56
  def cmaes_utilities
58
57
  # Algorithm equations are meant for fitness maximization
59
58
  # Match utilities with individuals sorted by INCREASING fitness
@@ -63,7 +62,7 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
63
62
  total = log_range.reduce(:+)
64
63
  buf = 1.0/popsize
65
64
  vals = log_range.collect { |v| v / total - buf }.reverse
66
- NMatrix[vals, dtype: dtype]
65
+ NArray[vals]
67
66
  end
68
67
 
69
68
  # (see #cmaes_utilities)
@@ -78,21 +77,26 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
78
77
  [5, 4 + (3*Math.log(ndims)).floor].max
79
78
  end
80
79
 
81
- # Samples a standard normal distribution to construct a NMatrix of
80
+ # Samples a standard normal distribution to construct a NArray of
82
81
  # popsize multivariate samples of length ndims
83
- # @return [NMatrix] standard normal samples
82
+ # @return [NArray] standard normal samples
83
+ # @note Numo::NArray implements this :) glad to have switched!
84
84
  def standard_normal_samples
85
- NMatrix.new([popsize, ndims], dtype: dtype) { standard_normal_sample }
85
+ NArray.zeros([popsize, ndims]).tap do |ret|
86
+ ret.each_with_index { |_,*i| ret[*i] = standard_normal_sample }
87
+ end
86
88
  end
87
89
 
88
90
  # Move standard normal samples to current distribution
89
- # @return [NMatrix] individuals
91
+ # @return [NArray] individuals
90
92
  def move_inds inds
91
93
  # TODO: can we reduce the transpositions?
92
- # sigma.dot(inds.transpose).map(&mu.method(:+)).transpose
93
- multi_mu = NMatrix[*inds.rows.times.collect {mu.to_a}, dtype: dtype].transpose
94
- (multi_mu + sigma.dot(inds.transpose)).transpose
95
- # sigma.dot(inds.transpose).transpose + inds.rows.times.collect {mu.to_a}.to_nm
94
+
95
+ # multi_mu = NMatrix[*inds.rows.times.collect {mu.to_a}, dtype: dtype].transpose
96
+ # (multi_mu + sigma.dot(inds.transpose)).transpose
97
+
98
+ mu_tile = mu.tile(inds.shape.first, 1).transpose
99
+ (mu_tile + sigma.dot(inds.transpose)).transpose
96
100
  end
97
101
 
98
102
  # Sorted individuals
@@ -100,18 +104,27 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
100
104
  # matched with individuals sorted by INCREASING fitness. Then reverse order for minimization.
101
105
  # @return standard normal samples sorted by the respective individuals' fitnesses
102
106
  def sorted_inds
103
- samples = standard_normal_samples
104
- inds = move_inds(samples).to_a
107
+ # samples = standard_normal_samples # Numo::NArray implements the Box-Muller :)
108
+ samples = NArray.new([popsize, ndims]).rand_norm(0,1)
109
+ inds = move_inds(samples)
105
110
  fits = parallel_fit ? obj_fn.call(inds) : inds.map(&obj_fn)
106
111
  # Quick cure for NaN fitnesses
107
- fits.map! { |x| x.nan? ? (opt_type==:max ? -1 : 1) * Float::INFINITY : x }
112
+ fits.map { |x| x.nan? ? (opt_type==:max ? -1 : 1) * Float::INFINITY : x }
108
113
  @last_fits = fits # allows checking for stagnation
109
- sorted = [fits, inds, samples.to_a].transpose.sort_by(&:first)
110
- sorted.reverse! if opt_type==:min
111
- this_best = sorted.last.take(2)
114
+
115
+ # sorted = [fits.to_a, inds, samples.to_a].transpose.sort_by(&:first)
116
+ # sorted.reverse! if opt_type==:min
117
+ # this_best = sorted.last.take(2)
118
+ # NArray[*sorted.map(&:last)]
119
+
120
+ sort_idxs = fits.sort_index
121
+ sort_idxs = sort_idxs.reverse if opt_type == :min
122
+ this_best = [fits[sort_idxs[-1]], inds[sort_idxs[-1]]]
123
+
112
124
  opt_cmp_fn = opt_type==:min ? :< : :>
113
125
  @best = this_best if this_best.first.send(opt_cmp_fn, best.first)
114
- NMatrix[*sorted.map(&:last), dtype: dtype]
126
+
127
+ samples[sort_idxs,true]
115
128
  end
116
129
 
117
130
  # @!method interface_methods
@@ -46,8 +46,8 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
46
46
  end.transpose
47
47
 
48
48
  # Join the individuals for evaluation
49
- full_inds = inds_lst.reduce(&:hconcat).to_a
50
- # Need to fix samples dimensions for sorting
49
+ full_inds = inds_lst.reduce { |mem, var| mem.concatenate var, axis: 1 }
50
+ # Need to fix sample dimensions for sorting
51
51
  # - current dims: nblocks x ninds x [block sizes]
52
52
  # - for sorting: ninds x nblocks x [block sizes]
53
53
  full_samples = samples_lst.transpose
@@ -55,24 +55,30 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
55
55
  # Evaluate fitness of complete individuals
56
56
  fits = parallel_fit ? obj_fn.call(full_inds) : full_inds.map(&obj_fn)
57
57
  # Quick cure for NaN fitnesses
58
- fits.map! { |x| x.nan? ? (opt_type==:max ? -1 : 1) * Float::INFINITY : x }
58
+ fits.map { |x| x.nan? ? (opt_type==:max ? -1 : 1) * Float::INFINITY : x }
59
59
  @last_fits = fits # allows checking for stagnation
60
60
 
61
61
  # Sort inds based on fit and opt_type, save best
62
- sorted = [fits, full_inds, full_samples].transpose.sort_by(&:first)
63
- sorted.reverse! if opt_type==:min
64
- this_best = sorted.last.take(2)
62
+ # sorted = [fits, full_inds, full_samples].transpose.sort_by(&:first)
63
+ # sorted.reverse! if opt_type==:min
64
+ # this_best = sorted.last.take(2)
65
+ # opt_cmp_fn = opt_type==:min ? :< : :>
66
+ # @best = this_best if this_best.first.send(opt_cmp_fn, best.first)
67
+ # sorted_samples = sorted.map(&:last)
68
+ sort_idxs = fits.sort_index
69
+ sort_idxs = sort_idxs.reverse if opt_type == :min
70
+ this_best = [fits[sort_idxs[-1]], full_inds[sort_idxs[-1]]]
65
71
  opt_cmp_fn = opt_type==:min ? :< : :>
66
72
  @best = this_best if this_best.first.send(opt_cmp_fn, best.first)
67
- sorted_samples = sorted.map(&:last)
73
+ sorted_samples = full_samples.values_at *sort_idxs
68
74
 
69
75
  # Need to bring back sample dimensions for each block
70
76
  # - current dims: ninds x nblocks x [block sizes]
71
77
  # - target blocks list: nblocks x ninds x [block sizes]
72
78
  block_samples = sorted_samples.transpose
73
79
 
74
- # then back to NMatrix for usage in training
75
- block_samples.map { |sample| NMatrix[*sample, dtype: dtype] }
80
+ # then back to NArray for usage in training
81
+ block_samples.map &:to_na
76
82
  end
77
83
 
78
84
  # duck-type the interface: [:train, :mu, :convergence, :save, :load]
@@ -84,7 +90,7 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
84
90
  end
85
91
 
86
92
  def mu
87
- blocks.map(&:mu).reduce(&:hconcat)
93
+ blocks.map(&:mu).reduce { |mem, var| mem.concatenate var, axis: 1 }
88
94
  end
89
95
 
90
96
  def convergence
@@ -5,18 +5,34 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
5
5
  attr_reader :variance
6
6
 
7
7
  def initialize_distribution mu_init: 0, sigma_init: 1
8
- @mu = NMatrix.new([1, ndims], mu_init, dtype: dtype)
9
- raise ArgumentError unless sigma_init.kind_of? Numeric
8
+ @mu = case mu_init
9
+ when Array
10
+ raise ArgumentError unless mu_init.size == ndims
11
+ NArray[mu_init]
12
+ when Numeric
13
+ NArray.new([1,ndims]).fill mu_init
14
+ else
15
+ raise ArgumentError, "Something is wrong with mu_init: #{mu_init}"
16
+ end
10
17
  @variance = sigma_init
11
- @sigma = id * variance
18
+ @sigma = case sigma_init
19
+ when Array
20
+ raise ArgumentError "RNES uses single global variance"
21
+ when Numeric
22
+ NArray.new([ndims]).fill(variance).diag
23
+ else
24
+ raise ArgumentError, "Something is wrong with sigma_init: #{sigma_init}"
25
+ end
12
26
  end
13
27
 
14
28
  def train picks: sorted_inds
15
29
  g_mu = utils.dot(picks)
16
- g_sigma = utils.dot(picks.row_norms**2 - ndims).first # back to scalar
30
+ # g_sigma = utils.dot(picks.row_norms**2 - ndims).first # back to scalar
31
+ row_norms = Numo::Linalg.norm picks, 2, axis:1
32
+ g_sigma = utils.dot(row_norms**2 - ndims)[0] # back to scalar
17
33
  @mu += sigma.dot(g_mu.transpose).transpose * lrate
18
34
  @variance *= Math.exp(g_sigma * lrate / 2)
19
- @sigma = id * variance
35
+ @sigma = NArray.new([ndims]).fill(variance).diag
20
36
  end
21
37
 
22
38
  # Estimate algorithm convergence based on variance
@@ -25,14 +41,14 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
25
41
  end
26
42
 
27
43
  def save
28
- [mu.to_consistent_a, variance]
44
+ [mu.to_a, variance]
29
45
  end
30
46
 
31
47
  def load data
32
48
  raise ArgumentError unless data.size == 2
33
49
  mu_ary, @variance = data
34
- @mu = NMatrix[*mu_ary, dtype: dtype]
35
- @sigma = id * variance
50
+ @mu = mu_ary.to_na
51
+ @sigma = eye * variance
36
52
  end
37
53
  end
38
54
  end
@@ -6,35 +6,48 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
6
6
  attr_reader :variances
7
7
 
8
8
  def initialize_distribution mu_init: 0, sigma_init: 1
9
- @mu = NMatrix.new([1, ndims], mu_init, dtype: dtype)
10
- sigma_init = [sigma_init]*ndims unless sigma_init.kind_of? Enumerable
11
- @variances = NMatrix.new([1,ndims], sigma_init, dtype: dtype)
12
- @sigma = NMatrix.diagonal(variances, dtype: dtype)
9
+ @mu = case mu_init
10
+ when Array
11
+ raise ArgumentError unless mu_init.size == ndims
12
+ NArray[mu_init]
13
+ when Numeric
14
+ NArray.new([1,ndims]).fill mu_init
15
+ else
16
+ raise ArgumentError, "Something is wrong with mu_init: #{mu_init}"
17
+ end
18
+ @variances = case sigma_init
19
+ when Array
20
+ raise ArgumentError unless sigma_init.size == ndims
21
+ NArray[*sigma_init]
22
+ when Numeric
23
+ NArray.new([ndims]).fill(sigma_init)
24
+ else
25
+ raise ArgumentError, "Something is wrong with sigma_init: #{sigma_init}"
26
+ end
27
+ @sigma = @variances.diag
13
28
  end
14
29
 
15
30
  def train picks: sorted_inds
16
31
  g_mu = utils.dot(picks)
17
32
  g_sigma = utils.dot(picks**2 - 1)
18
33
  @mu += sigma.dot(g_mu.transpose).transpose * lrate
19
- @variances *= (g_sigma * lrate / 2).exponential
20
- @sigma = NMatrix.diagonal(variances, dtype: dtype)
34
+ @variances *= (g_sigma * lrate / 2).exponential.flatten
35
+ @sigma = @variances.diag
21
36
  end
22
37
 
23
38
  # Estimate algorithm convergence as total variance
24
39
  def convergence
25
- variances.reduce :+
40
+ variances.sum
26
41
  end
27
42
 
28
43
  def save
29
- [mu.to_consistent_a, variances.to_consistent_a]
44
+ [mu.to_a, variances.to_a]
30
45
  end
31
46
 
32
47
  def load data
33
48
  raise ArgumentError unless data.size == 2
34
- mu_ary, variances_ary = data
35
- @mu = NMatrix[*mu_ary, dtype: dtype]
36
- @variances = NMatrix[*variances_ary, dtype: dtype]
37
- @sigma = NMatrix.diagonal(variances, dtype: dtype)
49
+ @mu, @variances = data.map &:to_na
50
+ @sigma = variances.diag
38
51
  end
39
52
  end
40
53
  end
@@ -5,21 +5,35 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
5
5
  attr_reader :log_sigma
6
6
 
7
7
  def initialize_distribution mu_init: 0, sigma_init: 1
8
- @mu = NMatrix.new([1, ndims], mu_init, dtype: dtype)
9
- sigma_init = [sigma_init]*ndims unless sigma_init.kind_of? Enumerable
10
- @sigma = NMatrix.diag(sigma_init, dtype: dtype)
8
+ @mu = case mu_init
9
+ when Array
10
+ raise ArgumentError unless mu_init.size == ndims
11
+ NArray[mu_init]
12
+ when Numeric
13
+ NArray.new([1,ndims]).fill mu_init
14
+ else
15
+ raise ArgumentError, "Something is wrong with mu_init: #{mu_init}"
16
+ end
17
+ @sigma = case sigma_init
18
+ when Array
19
+ raise ArgumentError unless sigma_init.size == ndims
20
+ NArray[*sigma_init].diag
21
+ when Numeric
22
+ NArray.new([ndims]).fill(sigma_init).diag
23
+ else
24
+ raise ArgumentError, "Something is wrong with sigma_init: #{sigma_init}"
25
+ end
11
26
  # Works with the log of sigma to avoid continuous decompositions (thanks Sun Yi)
12
- log_sigma_init = sigma_init.map &Math.method(:log)
13
- @log_sigma = NMatrix.diag(log_sigma_init, dtype: dtype)
27
+ @log_sigma = Numo::NMath.log(sigma.diagonal).diag
14
28
  end
15
29
 
16
30
  def train picks: sorted_inds
17
31
  g_mu = utils.dot(picks)
18
- g_log_sigma = popsize.times.inject(NMatrix.zeros_like sigma) do |sum, i|
32
+ g_log_sigma = popsize.times.inject(NArray.zeros sigma.shape) do |sum, i|
19
33
  u = utils[i]
20
- ind = picks.row(i)
34
+ ind = picks[i, true]
21
35
  ind_sq = ind.outer_flat(ind, &:*)
22
- sum + (ind_sq - id) * u
36
+ sum + (ind_sq - eye) * u
23
37
  end
24
38
  @mu += sigma.dot(g_mu.transpose).transpose * lrate
25
39
  @log_sigma += g_log_sigma * (lrate/2)
@@ -32,14 +46,12 @@ module MachineLearningWorkbench::Optimizer::NaturalEvolutionStrategies
32
46
  end
33
47
 
34
48
  def save
35
- [mu.to_consistent_a, log_sigma.to_consistent_a]
49
+ [mu.to_a, log_sigma.to_a]
36
50
  end
37
51
 
38
52
  def load data
39
53
  raise ArgumentError unless data.size == 2
40
- mu_ary, log_sigma_ary = data
41
- @mu = NMatrix[*mu_ary, dtype: dtype]
42
- @log_sigma = NMatrix[*log_sigma_ary, dtype: dtype]
54
+ @mu, @log_sigma = data.map &:to_na
43
55
  @sigma = log_sigma.exponential
44
56
  end
45
57
  end
@@ -3,26 +3,32 @@ module MachineLearningWorkbench::Tools
3
3
  Forkable = MachineLearningWorkbench::Tools::Execution
4
4
  Norm = MachineLearningWorkbench::Tools::Normalization
5
5
 
6
- # Create RMagick::Image from NMatrix data
7
- def self.nmat_to_img nmat, shape: nil
8
- shape ||= nmat.shape
6
+ # Create RMagick::Image from numeric matrix data
7
+ # @param narr [NArray] numeric matrix to display
8
+ # @param shape [Array<Integer>] optional reshaping
9
+ def self.narr_to_img narr, shape: nil
10
+ shape ||= narr.shape
9
11
  shape = [1, shape] if shape.kind_of?(Integer) || shape.size == 1
10
12
  # `Image::constitute` requires Float pixels to be in [0,1]
11
- pixels = Norm.feature_scaling nmat.round(4), to: [0,1]
12
- Magick::Image.constitute *shape, "I", pixels.to_flat_a
13
+ pixels = Norm.feature_scaling narr, to: [0,1]
14
+ Magick::Image.constitute *shape, "I", pixels.to_a.flatten
13
15
  end
14
16
 
15
- # Create PNG file from NMatrix data
16
- def self.nmat_to_png nmat, fname, shape: nil
17
- nmat_to_img(nmat, shape: shape).write fname
17
+ # Create PNG file from numeric matrix data
18
+ # @param narr [NArray] numeric matrix to display
19
+ # @param fname [String] path to save PNG
20
+ # @param shape [Array<Integer>] optional reshaping before saving
21
+ def self.narr_to_png narr, fname, shape: nil
22
+ narr_to_img(narr, shape: shape).write fname
18
23
  end
19
24
 
20
- # Show a NMatrix as image in a RMagick window
25
+ # Show a numeric matrix as image in a RMagick window
26
+ # @param narr [NArray] numeric matrix to display
21
27
  # @param disp_size [Array] the size of the image to display
22
- # @param shape [Array] the true shape of the image (NMatrix could be flattened)
28
+ # @param shape [Array] the true shape of the image (numeric matrix could be flattened)
23
29
  # @param in_fork [bool] whether to execute the display in fork (and continue running)
24
- def self.display nmat, disp_size: nil, shape: nil, in_fork: true
25
- img = nmat_to_img nmat, shape: shape
30
+ def self.display narr, disp_size: nil, shape: nil, in_fork: true
31
+ img = narr_to_img narr, shape: shape
26
32
  img.resize!(*disp_size, Magick::TriangleFilter,0.51) if disp_size
27
33
  if in_fork
28
34
  MachineLearningWorkbench::Tools::Execution.in_fork { img.display }
@@ -31,19 +37,19 @@ module MachineLearningWorkbench::Tools
31
37
  end
32
38
  end
33
39
 
34
- # Create NMatrix from png by filename.
40
+ # Create numeric matrix from png by filename.
35
41
  # @param fname the file name
36
42
  # @param scale optional rescaling of the image
37
43
  # @param flat [bool] whether to return a flat array
38
- # @param dtype dtype for the NMatrix, leave `nil` for automatic detection
39
- def self.nmat_from_png fname, scale: nil, flat: false, dtype: nil
44
+ # @param dtype dtype for the numeric matrix, leave `nil` for automatic detection
45
+ def self.narr_from_png fname, scale: nil, flat: false
40
46
  img = Magick::ImageList.new(fname).first
41
47
  img.scale!(scale) if scale
42
48
  shape = [img.columns, img.rows]
43
49
  pixels = img.export_pixels(0, 0, *shape, 'I') # 'I' for intensity
44
50
  raise "Sanity check" unless shape.reduce(:*)==pixels.size
45
- return pixels.to_nm(nil, dtype) if flat
46
- NMatrix.new shape, pixels, dtype: dtype
51
+ return pixels.to_na if flat
52
+ pixels.to_na.to_dimensions shape
47
53
  end
48
54
  end
49
55
  end
@@ -1,22 +1,23 @@
1
1
  module MachineLearningWorkbench::Tools
2
2
  module Normalization
3
- def self.feature_scaling nmat, from: nil, to: [0,1]
4
- from ||= nmat.minmax
3
+ def self.feature_scaling narr, from: nil, to: [0,1]
4
+ from ||= narr.minmax
5
5
  old_min, old_max = from
6
6
  new_min, new_max = to
7
- (nmat-old_min)*(new_max-new_min)/(old_max-old_min)+new_min
7
+ (narr-old_min)*(new_max-new_min)/(old_max-old_min)+new_min
8
8
  end
9
9
 
10
10
  # @param per_column [bool] wheather to compute stats per-column or matrix-wise
11
- def self.z_score nmat, per_column: true
11
+ def self.z_score narr, per_column: true
12
12
  raise NotImplementedError unless per_column
13
- means = nmat.mean
14
- stddevs = nmat.std
13
+ raise "this would be a good time to test this implementation"
14
+ means = narr.mean
15
+ stddevs = narr.std
15
16
  # address edge case of zero variance
16
17
  stddevs.map! { |v| v.zero? ? 1 : v }
17
- mean_mat = means.repeat nmat.rows, 0
18
- stddev_mat = stddevs.repeat nmat.rows, 0
19
- (nmat - mean_mat) / stddev_mat
18
+ mean_mat = means.repeat narr.rows, 0
19
+ stddev_mat = stddevs.repeat narr.rows, 0
20
+ (narr - mean_mat) / stddev_mat
20
21
  end
21
22
  end
22
23
  end
@@ -9,8 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.email = "giuseppe.cuccu@gmail.com"
10
10
 
11
11
  spec.summary = %q[Workbench for practical machine learning in Ruby.]
12
- spec.description = %q[\
13
- This workbench holds a collection of machine learning
12
+ spec.description = %q[This workbench holds a collection of machine learning
14
13
  methods in Ruby. Rather than specializing on a single task or method, this
15
14
  gem aims at providing an encompassing framework for any machine learning
16
15
  application.].gsub(' ', '')
@@ -44,7 +43,12 @@ Gem::Specification.new do |spec|
44
43
  spec.add_development_dependency "pry-stack_explorer", "~> 0.4"
45
44
 
46
45
  # Run
47
- spec.requirements << "libatlas-base-dev" # library for following dependency
48
- spec.add_dependency "nmatrix-atlas", "~> 0.2"
46
+ spec.requirements << "libopenblas-base" # library for following dependency
47
+ spec.add_dependency "numo-linalg"
49
48
  spec.add_dependency "parallel", "~> 1.12"
49
+
50
+
51
+
52
+ # DELETEME
53
+ spec.add_dependency "nmatrix-atlas"
50
54
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: machine_learning_workbench
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.3'
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Giuseppe Cuccu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-19 00:00:00.000000000 Z
11
+ date: 2018-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -123,19 +123,19 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0.4'
125
125
  - !ruby/object:Gem::Dependency
126
- name: nmatrix-atlas
126
+ name: numo-linalg
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - "~>"
129
+ - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '0.2'
131
+ version: '0'
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - "~>"
136
+ - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '0.2'
138
+ version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: parallel
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -150,8 +150,21 @@ dependencies:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
152
  version: '1.12'
153
+ - !ruby/object:Gem::Dependency
154
+ name: nmatrix-atlas
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
153
167
  description: |-
154
- \
155
168
  This workbench holds a collection of machine learning
156
169
  methods in Ruby. Rather than specializing on a single task or method, this
157
170
  gem aims at providing an encompassing framework for any machine learning
@@ -219,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
219
232
  - !ruby/object:Gem::Version
220
233
  version: '0'
221
234
  requirements:
222
- - libatlas-base-dev
235
+ - libopenblas-base
223
236
  rubyforge_project:
224
237
  rubygems_version: 2.6.13
225
238
  signing_key: