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

.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_hash(hash) ⇒ Object



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

def self.load_hash(hash)
  model = self.new
  model.layers = hash[:layers].map { |hash_layer| Utils.load_hash(hash_layer) }
  model
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.load_hash(hash)
  model.compile(Utils.load_hash(hash[:optimizer]), Utils.load_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
106
107
108
109
# File 'lib/dnn/core/model.rb', line 99

def <<(layer)
  # Due to a bug in saving nested models, temporarily prohibit model nesting.
  # 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
  unless layer.is_a?(Layers::Layer)
    raise TypeError.new("layer:#{layer.class.name} is not an instance of the DNN::Layers::Layer class.")
  end
  @layers << layer
  self
end

#accurate(x, y, batch_size = 100) {|x, y| ... } ⇒ Object

Evaluate model and get accurate of test data.

Parameters:

  • x (Numo::SFloat) —

    Input test data.

  • y (Numo::SFloat) —

    Output test data.

Yields:

  • (x, y) —

    batch_proc Set proc to process per batch.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/dnn/core/model.rb', line 271

def accurate(x, y, batch_size = 100, &batch_proc)
  check_xy_type(x, y)
  input_data_shape_check(x, y)
  batch_size = batch_size >= x.shape[0] ? x.shape[0] : batch_size
  correct = 0
  (x.shape[0].to_f / batch_size).ceil.times do |i|
    x_batch = Xumo::SFloat.zeros(batch_size, *x.shape[1..-1])
    y_batch = Xumo::SFloat.zeros(batch_size, *y.shape[1..-1])
    batch_size.times do |j|
      k = i * batch_size + j
      break if k >= x.shape[0]
      x_batch[j, false] = x[k, false]
      y_batch[j, false] = y[k, false]
    end
    x_batch, y_batch = batch_proc.call(x_batch, y_batch) if batch_proc
    out = forward(x_batch, false)
    batch_size.times do |j|
      if @layers.last.output_shape == [1]
        correct += 1 if out[j, 0].round == y_batch[j, 0].round
      else
        correct += 1 if out[j, true].max_index == y_batch[j, true].max_index
      end
    end
  end
  correct.to_f / x.shape[0]
end

#backward(dout) ⇒ Object



350
351
352
353
354
355
# File 'lib/dnn/core/model.rb', line 350

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

#build(super_model = nil) ⇒ Object



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

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

#compile(optimizer, loss) ⇒ Object

Set optimizer and loss to model and build all layers.

Parameters:

Raises:



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/dnn/core/model.rb', line 114

def compile(optimizer, loss)
  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.is_a?(Losses::Loss)
    raise TypeError.new("loss:#{loss.class} is not an instance of DNN::Losses::Loss class.")
  end
  @compiled = true
  layers_check
  @optimizer = optimizer
  @loss = loss
  build
  layers_shape_check
end

#compiled? ⇒ Bool

Returns whether the model is learning.

Returns:

  • (Bool) —

    Returns whether the model is learning.



187
188
189
# File 'lib/dnn/core/model.rb', line 187

def compiled?
  @compiled
end

#copy ⇒ DNN::Model

Returns Copy this model.

Returns:



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

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

#forward(x, learning_phase) ⇒ Object

TODO It is not good to write the Layer class name directly in the Model class. I will fix it later.



339
340
341
342
343
344
345
346
347
348
# File 'lib/dnn/core/model.rb', line 339

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

#get_all_layers ⇒ Array

Get the all layers.

Returns:

  • (Array) —

    all layers array.



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

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.



319
320
321
322
323
324
325
326
327
# File 'lib/dnn/core/model.rb', line 319

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



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/dnn/core/model.rb', line 368

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.



165
166
167
# File 'lib/dnn/core/model.rb', line 165

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 ⇒ DNN::Losses::Loss

Returns loss Return the loss to use for learning.

Returns:

Raises:



181
182
183
184
# File 'lib/dnn/core/model.rb', line 181

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

#optimizer ⇒ DNN::Optimizers::Optimizer

Returns optimizer Return the optimizer to use for learning.

Returns:

Raises:



175
176
177
178
# File 'lib/dnn/core/model.rb', line 175

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

#output_shape ⇒ Array

Return the output shape of the model.

Returns:

  • (Array) —

    Return the output shape of the model.



170
171
172
# File 'lib/dnn/core/model.rb', line 170

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.



300
301
302
303
304
# File 'lib/dnn/core/model.rb', line 300

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.



308
309
310
311
# File 'lib/dnn/core/model.rb', line 308

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

#recompile(optimizer, loss) ⇒ Object

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

Parameters:



133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/dnn/core/model.rb', line 133

def recompile(optimizer, loss)
  unless optimizer.is_a?(Optimizers::Optimizer)
    raise TypeError.new("optimizer:#{optimizer.class} is not an instance of DNN::Optimizers::Optimizer class.")
  end
  unless loss.is_a?(Losses::Loss)
    raise TypeError.new("loss:#{loss.class} is not an instance of DNN::Losses::Loss class.")
  end
  @compiled = true
  layers_check
  @optimizer = optimizer
  @loss = loss
  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



386
387
388
389
# File 'lib/dnn/core/model.rb', line 386

def to_hash
  hash_layers = @layers.map { |layer| layer.to_hash }
  {class: Model.name, layers: hash_layers, optimizer: @optimizer.to_hash, loss: @loss.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, batch_proc: nil) {|epoch| ... } ⇒ 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.

  • batch_proc (Proc) (defaults to: nil) —

    Set proc to process per batch.

Yields:

  • (epoch) —

    Process performed before one training.

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
# File 'lib/dnn/core/model.rb', line 202

def train(x, y, epochs,
          batch_size: 1,
          test: nil,
          verbose: true,
          batch_proc: nil,
          &epoch_proc)
  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|
    puts "【 epoch #{epoch}/#{epochs} 】" if verbose
    (num_train_datas.to_f / batch_size).ceil.times do |index|
      x_batch, y_batch = dataset.get_batch(batch_size)
      loss_value = train_on_batch(x_batch, y_batch, &batch_proc)
      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 = accurate(test[0], test[1], batch_size, &batch_proc)
      print "  accurate: #{acc}"
    end
    puts "" if verbose
    epoch_proc.call(epoch) if epoch_proc
  end
end

#train_on_batch(x, y) {|x, y| ... } ⇒ 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.

Yields:

  • (x, y) —

    batch_proc Set proc to process per batch.

Returns:

  • (Float | Numo::SFloat) —

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

Raises:



253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/dnn/core/model.rb', line 253

def train_on_batch(x, y, &batch_proc)
  raise DNN_Error.new("The model is not compiled.") unless compiled?
  check_xy_type(x, y)
  input_data_shape_check(x, y)
  x, y = batch_proc.call(x, y) if batch_proc
  out = forward(x, true)
  loss_value = @loss.forward(out, y, get_all_layers)
  dout = @loss.backward(y)
  backward(dout)
  @loss.regularizes_backward(get_all_layers)
  update
  loss_value
end

#update ⇒ Object



357
358
359
360
361
362
363
364
365
366
# File 'lib/dnn/core/model.rb', line 357

def update
  return unless @trainable
  @layers.each do |layer|
    if layer.is_a?(Layers::HasParamLayer)
      layer.update(@optimizer)
    elsif layer.is_a?(Model)
      layer.update
    end
  end
end