Class: DNN::Model

Inherits:
Object
  • Object
show all
Defined in:
lib/dnn/core/model.rb

Overview

This class deals with the model of the network.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ Model

Returns a new instance of Model.



36
37
38
39
40
41
# File 'lib/dnn/core/model.rb', line 36

def initialize
  @layers = []
  @trainable = true
  @optimizer = nil
  @compiled = false
end

Instance Attribute Details

#layers ⇒ Array

Returns All layers possessed by the model.

Returns:

  • (Array) —

    All layers possessed by the model.



10
11
12
# File 'lib/dnn/core/model.rb', line 10

def layers
  @layers
end

#trainable ⇒ Bool

Returns Setting false prevents learning of parameters.

Returns:

  • (Bool) —

    Setting false prevents learning of parameters.



12
13
14
# File 'lib/dnn/core/model.rb', line 12

def trainable
  @trainable
end

Class Method Details

.from_hash(hash) ⇒ Object



30
31
32
33
34
# File 'lib/dnn/core/model.rb', line 30

def self.from_hash(hash)
  model = self.new
  model.layers = hash[:layers].map { |hash_layer| Utils.from_hash(hash_layer) }
  model
end

.load(file_name) ⇒ Object

Load marshal model.

Parameters:

  • file_name (String) —

    File name of marshal model to load.



16
17
18
# File 'lib/dnn/core/model.rb', line 16

def self.load(file_name)
  Marshal.load(Zlib::Inflate.inflate(File.binread(file_name)))
end

.load_json(json_str) ⇒ DNN::Model

Load json model.

Parameters:

  • json_str (String) —

    json string to load model.

Returns:



23
24
25
26
27
28
# File 'lib/dnn/core/model.rb', line 23

def self.load_json(json_str)
  hash = JSON.parse(json_str, symbolize_names: true)
  model = self.from_hash(hash)
  model.compile(Utils.from_hash(hash[:optimizer]), Utils.from_hash(hash[:loss]))
  model
end

Instance Method Details

#<<(layer) ⇒ DNN::Model

Add layer to the model.

Parameters:

Returns:



99
100
101
102
103
104
105
# File 'lib/dnn/core/model.rb', line 99

def <<(layer)
  if !layer.is_a?(Layers::Layer) && !layer.is_a?(Model)
    raise TypeError.new("layer is not an instance of the DNN::Layers::Layer class or DNN::Model class.")
  end
  @layers << layer
  self
end

#accurate(x, y, batch_size = 100, before_batch_cbk: nil, after_batch_cbk: nil) ⇒ Array

Evaluate model and get accurate of test data.

Parameters:

  • x (Numo::SFloat) —

    Input test data.

  • y (Numo::SFloat) —

    Output test data.

  • before_batch_cbk (Lambda) (defaults to: nil) —

    Set the proc to be performed before batch processing.

  • after_batch_cbk (Lambda) (defaults to: nil) —

    Set the proc to be performed after batch processing.

Returns:

  • (Array) —

    Returns the test data accurate and mean loss in the form [accurate, mean_loss].



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/dnn/core/model.rb', line 279

def accurate(x, y, batch_size = 100, before_batch_cbk: nil, after_batch_cbk: nil)
  check_xy_type(x, y)
  input_data_shape_check(x, y)
  batch_size = batch_size >= x.shape[0] ? x.shape[0] : batch_size
  dataset = Dataset.new(x, y, false)
  correct = 0
  sum_loss = 0
  (x.shape[0].to_f / batch_size).ceil.times do |i|
    x_batch, y_batch = dataset.next_batch(batch_size)
    x_batch, y_batch = before_batch_cbk.call(x_batch, y_batch, false) if before_batch_cbk
    x_batch = forward(x_batch, false)
    sigmoid = Sigmoid.new
    batch_size.times do |j|
      if @layers.last.output_shape == [1]
        if @loss_func.is_a?(SigmoidCrossEntropy)
          correct += 1 if sigmoid.forward(x_batch[j, 0]).round == y_batch[j, 0].round
        else
          correct += 1 if x_batch[j, 0].round == y_batch[j, 0].round
        end
      else
        correct += 1 if x_batch[j, true].max_index == y_batch[j, true].max_index
      end
    end
    loss_value = @loss_func.forward(x_batch, y_batch, get_all_layers)
    after_batch_cbk.call(loss_value, false) if after_batch_cbk
    sum_loss += loss_value.is_a?(Numo::SFloat) ? loss_value.mean : loss_value
  end
  mean_loss = sum_loss / batch_size
  [correct.to_f / x.shape[0], mean_loss]
end

#backward(dy) ⇒ Object



372
373
374
375
376
377
# File 'lib/dnn/core/model.rb', line 372

def backward(dy)
  @layers.reverse.each do |layer|
    dy = layer.backward(dy)
  end
  dy
end

#build(super_model = nil) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/dnn/core/model.rb', line 143

def build(super_model = nil)
  @super_model = super_model
  shape = if super_model
    super_model.get_prev_layer(self).output_shape
  else
    @layers.first.build
  end
  layers = super_model ? @layers : @layers[1..-1]
  layers.each do |layer|
    if layer.is_a?(Model)
      layer.build(self)
      layer.recompile(@optimizer, @loss_func)
    else
      layer.build(shape)
    end
    shape = layer.output_shape
  end
end

#compile(optimizer, loss_func) ⇒ Object

Set optimizer and loss_func to model and build all layers.

Parameters:

Raises:



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/dnn/core/model.rb', line 110

def compile(optimizer, loss_func)
  raise DNN_Error.new("The model is already compiled.") if compiled?
  unless optimizer.is_a?(Optimizers::Optimizer)
    raise TypeError.new("optimizer:#{optimizer.class} is not an instance of DNN::Optimizers::Optimizer class.")
  end
  unless loss_func.is_a?(Losses::Loss)
    raise TypeError.new("loss_func:#{loss_func.class} is not an instance of DNN::Losses::Loss class.")
  end
  @compiled = true
  layers_check
  @optimizer = optimizer
  @loss_func = loss_func
  build
  layers_shape_check
end

#compiled? ⇒ Bool

Returns whether the model is learning.

Returns:

  • (Bool) —

    Returns whether the model is learning.



185
186
187
# File 'lib/dnn/core/model.rb', line 185

def compiled?
  @compiled
end

#copy ⇒ DNN::Model

Returns Copy this model.

Returns:



337
338
339
# File 'lib/dnn/core/model.rb', line 337

def copy
  Marshal.load(Marshal.dump(self))
end

#forward(x, learning_phase) ⇒ Object



360
361
362
363
364
365
366
367
368
369
370
# File 'lib/dnn/core/model.rb', line 360

def forward(x, learning_phase)
  @layers.each do |layer|
    x = if layer.is_a?(Model)
      layer.forward(x, learning_phase)
    else
      layer.learning_phase = learning_phase
      layer.forward(x)
    end
  end
  x
end

#get_all_layers ⇒ Array

Get the all layers.

Returns:

  • (Array) —

    all layers array.



354
355
356
357
358
# File 'lib/dnn/core/model.rb', line 354

def get_all_layers
  @layers.map { |layer|
    layer.is_a?(Model) ? layer.get_all_layers : layer
  }.flatten
end

#get_layer(*args) ⇒ Object

Get the layer that the model has.



342
343
344
345
346
347
348
349
350
# File 'lib/dnn/core/model.rb', line 342

def get_layer(*args)
  if args.length == 1
    index = args[0]
    @layers[index]
  else
    layer_class, index = args
    @layers.select { |layer| layer.is_a?(layer_class) }[index]
  end
end

#get_prev_layer(layer) ⇒ Object



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/dnn/core/model.rb', line 391

def get_prev_layer(layer)
  layer_index = @layers.index(layer)
  prev_layer = if layer_index == 0
    if @super_model
      @super_model.layers[@super_model.layers.index(self) - 1]
    else
      self
    end
  else
    @layers[layer_index - 1]
  end
  if prev_layer.is_a?(Layers::Layer)
    prev_layer
  elsif prev_layer.is_a?(Model)
    prev_layer.layers.last
  end
end

#input_shape ⇒ Array

Return the input shape of the model.

Returns:

  • (Array) —

    Return the input shape of the model.



163
164
165
# File 'lib/dnn/core/model.rb', line 163

def input_shape
  @layers.first.input_shape
end

#load_json_params(json_str) ⇒ Object

Load json model parameters.

Parameters:

  • json_str (String) —

    json string to load model parameters.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/dnn/core/model.rb', line 45

def load_json_params(json_str)
  hash = JSON.parse(json_str, symbolize_names: true)
  has_param_layers_params = hash[:params]
  has_param_layers_index = 0
  has_param_layers = get_all_layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
  has_param_layers.each do |layer|
    hash_params = has_param_layers_params[has_param_layers_index]
    hash_params.each do |key, (shape, base64_param)|
      bin = Base64.decode64(base64_param)
      data = Xumo::SFloat.from_binary(bin).reshape(*shape)
      layer.params[key].data = data
    end
    has_param_layers_index += 1
  end
end

#loss(x, y) ⇒ Float | Numo::SFloat

Get loss value.

Parameters:

  • x (Numo::SFloat) —

    Input data.

  • y (Numo::SFloat) —

    Output data.

Returns:

  • (Float | Numo::SFloat) —

    Return loss value in the form of Float or Numo::SFloat.



329
330
331
332
333
334
# File 'lib/dnn/core/model.rb', line 329

def loss(x, y)
  check_xy_type(x, y)
  input_data_shape_check(x, y)
  x = forward(x, false)
  @loss_func.forward(x, y, get_all_layers)
end

#loss_func ⇒ DNN::Losses::Loss

Returns loss Return the loss to use for learning.

Returns:

Raises:



179
180
181
182
# File 'lib/dnn/core/model.rb', line 179

def loss_func
  raise DNN_Error.new("The model is not compiled.") unless compiled?
  @loss_func
end

#optimizer ⇒ DNN::Optimizers::Optimizer

Returns optimizer Return the optimizer to use for learning.

Returns:

Raises:



173
174
175
176
# File 'lib/dnn/core/model.rb', line 173

def optimizer
  raise DNN_Error.new("The model is not compiled.") unless compiled?
  @optimizer
end

#output_shape ⇒ Array

Return the output shape of the model.

Returns:

  • (Array) —

    Return the output shape of the model.



168
169
170
# File 'lib/dnn/core/model.rb', line 168

def output_shape
  @layers.last.output_shape
end

#params_to_json ⇒ String

Convert model parameters to json string.

Returns:

  • (String) —

    json string.



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/dnn/core/model.rb', line 84

def params_to_json
  has_param_layers = get_all_layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
  has_param_layers_params = has_param_layers.map do |layer|
    layer.params.map { |key, param|
      base64_data = Base64.encode64(param.data.to_binary)
      [key, [param.data.shape, base64_data]]
    }.to_h
  end
  hash = {version: VERSION, params: has_param_layers_params}
  JSON.dump(hash)
end

#predict(x) ⇒ Object

Predict data.

Parameters:

  • x (Numo::SFloat) —

    Input data.



312
313
314
315
316
# File 'lib/dnn/core/model.rb', line 312

def predict(x)
  check_xy_type(x)
  input_data_shape_check(x)
  forward(x, false)
end

#predict1(x) ⇒ Object

Predict one data.

Parameters:

  • x (Numo::SFloat) —

    Input data. However, x is single data.



320
321
322
323
# File 'lib/dnn/core/model.rb', line 320

def predict1(x)
  check_xy_type(x)
  predict(x.reshape(1, *x.shape))[0, false]
end

#recompile(optimizer, loss_func) ⇒ Object

Set optimizer and loss_func to model and recompile. But does not build layers.

Parameters:



129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/dnn/core/model.rb', line 129

def recompile(optimizer, loss_func)
  unless optimizer.is_a?(Optimizers::Optimizer)
    raise TypeError.new("optimizer:#{optimizer.class} is not an instance of DNN::Optimizers::Optimizer class.")
  end
  unless loss_func.is_a?(Losses::Loss)
    raise TypeError.new("loss_func:#{loss_func.class} is not an instance of DNN::Losses::Loss class.")
  end
  @compiled = true
  layers_check
  @optimizer = optimizer
  @loss_func = loss_func
  layers_shape_check
end

#save(file_name) ⇒ Object

Save the model in marshal format.

Parameters:

  • file_name (String) —

    name to save model.



63
64
65
66
67
68
69
70
71
72
# File 'lib/dnn/core/model.rb', line 63

def save(file_name)
  bin = Zlib::Deflate.deflate(Marshal.dump(self))
  begin
    File.binwrite(file_name, bin)
  rescue Errno::ENOENT => ex
    dir_name = file_name.match(%r`(.*)/.+$`)[1]
    Dir.mkdir(dir_name)
    File.binwrite(file_name, bin)
  end
end

#to_hash ⇒ Object



409
410
411
412
# File 'lib/dnn/core/model.rb', line 409

def to_hash
  hash_layers = @layers.map { |layer| layer.to_hash }
  {class: Model.name, layers: hash_layers, optimizer: @optimizer.to_hash, loss: @loss_func.to_hash}
end

#to_json ⇒ String

Convert model to json string.

Returns:

  • (String) —

    json string.



76
77
78
79
80
# File 'lib/dnn/core/model.rb', line 76

def to_json
  hash = self.to_hash
  hash[:version] = VERSION
  JSON.pretty_generate(hash)
end

#train(x, y, epochs, batch_size: 1, test: nil, verbose: true, before_epoch_cbk: nil, after_epoch_cbk: nil, before_batch_cbk: nil, after_batch_cbk: nil) ⇒ Object

Start training. Compile the model before use this method.

Parameters:

  • x (Numo::SFloat) —

    Input training data.

  • y (Numo::SFloat) —

    Output training data.

  • epochs (Integer) —

    Number of training.

  • batch_size (Integer) (defaults to: 1) —

    Batch size used for one training.

  • test (Array or NilClass) (defaults to: nil) —

    If you to test the model for every 1 epoch, specify [x_test, y_test]. Don't test to the model, specify nil.

  • verbose (Bool) (defaults to: true) —

    Set true to display the log. If false is set, the log is not displayed.

  • before_epoch_cbk (Lambda) (defaults to: nil) —

    Process performed before one training.

  • after_epoch_cbk (Lambda) (defaults to: nil) —

    Process performed after one training.

  • before_batch_cbk (Lambda) (defaults to: nil) —

    Set the proc to be performed before batch processing.

  • after_batch_cbk (Lambda) (defaults to: nil) —

    Set the proc to be performed after batch processing.

Raises:



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/dnn/core/model.rb', line 202

def train(x, y, epochs,
          batch_size: 1,
          test: nil,
          verbose: true,
          before_epoch_cbk: nil,
          after_epoch_cbk: nil,
          before_batch_cbk: nil,
          after_batch_cbk: nil)
  raise DNN_Error.new("The model is not compiled.") unless compiled?
  check_xy_type(x, y)
  dataset = Dataset.new(x, y)
  num_train_datas = x.shape[0]
  (1..epochs).each do |epoch|
    before_epoch_cbk.call(epoch) if before_epoch_cbk
    puts "【 epoch #{epoch}/#{epochs} 】" if verbose
    (num_train_datas.to_f / batch_size).ceil.times do |index|
      x_batch, y_batch = dataset.next_batch(batch_size)
      loss_value = train_on_batch(x_batch, y_batch,
                                  before_batch_cbk: before_batch_cbk, after_batch_cbk: after_batch_cbk)
      if loss_value.is_a?(Numo::SFloat)
        loss_value = loss_value.mean
      elsif loss_value.nan?
        puts "\nloss is nan" if verbose
        return
      end
      num_trained_datas = (index + 1) * batch_size
      num_trained_datas = num_trained_datas > num_train_datas ? num_train_datas : num_trained_datas
      log = "\r"
      40.times do |i|
        if i < num_trained_datas * 40 / num_train_datas
          log << "="
        elsif i == num_trained_datas * 40 / num_train_datas
          log << ">"
        else
          log << "_"
        end
      end
      log << "  #{num_trained_datas}/#{num_train_datas} loss: #{sprintf('%.8f', loss_value)}"
      print log if verbose
    end
    if verbose && test
      acc, test_loss = accurate(test[0], test[1], batch_size,
                                before_batch_cbk: before_batch_cbk, after_batch_cbk: after_batch_cbk)
      print "  accurate: #{acc}, test loss: #{sprintf('%.8f', test_loss)}"
    end
    puts "" if verbose
    after_epoch_cbk.call(epoch) if after_epoch_cbk
  end
end

#train_on_batch(x, y, before_batch_cbk: nil, after_batch_cbk: nil) ⇒ Float | Numo::SFloat

Training once. Compile the model before use this method.

Parameters:

  • x (Numo::SFloat) —

    Input training data.

  • y (Numo::SFloat) —

    Output training data.

  • before_batch_cbk (Lambda) (defaults to: nil) —

    Set the proc to be performed before batch processing.

  • after_batch_cbk (Lambda) (defaults to: nil) —

    Set the proc to be performed after batch processing.

Returns:

  • (Float | Numo::SFloat) —

    Return loss value in the form of Float or Numo::SFloat.

Raises:



259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/dnn/core/model.rb', line 259

def train_on_batch(x, y, before_batch_cbk: nil, after_batch_cbk: nil)
  raise DNN_Error.new("The model is not compiled.") unless compiled?
  check_xy_type(x, y)
  input_data_shape_check(x, y)
  x, y = before_batch_cbk.call(x, y, true) if before_batch_cbk
  x = forward(x, true)
  loss_value = @loss_func.forward(x, y, get_all_layers)
  dy = @loss_func.backward(y, get_all_layers)
  backward(dy)
  update
  after_batch_cbk.call(loss_value, true) if after_batch_cbk
  loss_value
end

#update ⇒ Object



379
380
381
382
383
384
385
386
387
388
389
# File 'lib/dnn/core/model.rb', line 379

def update
  return unless @trainable
  all_trainable_layers = @layers.map { |layer|
    if layer.is_a?(Model)
      layer.trainable ? layer.get_all_layers : nil
    else
      layer
    end
  }.flatten.compact.uniq
  @optimizer.update(all_trainable_layers)
end