lib/dnn/core/models.rb in ruby-dnn-0.14.3 vs lib/dnn/core/models.rb in ruby-dnn-0.15.0
- old
+ new
@@ -1,18 +1,108 @@
module DNN
module Models
+
+ class LayersList < Array
+ def self.from_hash_list(hash_list)
+ layers_list = new
+ hash_list.each do |hash|
+ obj_class = DNN.const_get(hash[:class])
+ obj = obj_class.allocate
+ if obj.is_a?(Chain)
+ obj = obj_class.new
+ obj.load_hash(hash)
+ else
+ obj = Layers::Layer.from_hash(hash)
+ end
+ layers_list << obj
+ end
+ layers_list
+ end
+
+ def to_hash_list
+ map { |layer| layer.to_hash }
+ end
+
+ # Get the all layers.
+ # @return [Array] All layers array.
+ def layers
+ layers_array = []
+ each do |layer|
+ if layer.is_a?(Layers::Layer)
+ layers_array << layer
+ elsif layer.is_a?(Chain) || layer.is_a?(LayersList)
+ layers_array.concat(layer.layers)
+ end
+ end
+ layers_array
+ end
+ end
+
+ class Chain
+ def call(x)
+ raise NotImplementedError, "Class '#{self.class.name}' has implement method 'call'"
+ end
+
+ # Get the all layers.
+ # @return [Array] All layers array.
+ def layers
+ layers_array = []
+ instance_variables.sort.each do |ivar|
+ obj = instance_variable_get(ivar)
+ if obj.is_a?(Layers::Layer)
+ layers_array << obj
+ elsif obj.is_a?(Chain) || obj.is_a?(LayersList)
+ layers_array.concat(obj.layers)
+ end
+ end
+ layers_array
+ end
+
+ def to_hash
+ layers_hash = { class: self.class.name }
+ instance_variables.sort.each do |ivar|
+ obj = instance_variable_get(ivar)
+ if obj.is_a?(Layers::Layer) || obj.is_a?(Chain)
+ layers_hash[ivar] = obj.to_hash
+ elsif obj.is_a?(LayersList)
+ layers_hash[ivar] = obj.to_hash_list
+ end
+ end
+ layers_hash
+ end
+
+ def load_hash(layers_hash)
+ instance_variables.sort.each do |ivar|
+ hash_or_array = layers_hash[ivar]
+ if hash_or_array.is_a?(Array)
+ instance_variable_set(ivar, LayersList.from_hash_list(hash_or_array))
+ elsif hash_or_array.is_a?(Hash)
+ obj_class = DNN.const_get(hash_or_array[:class])
+ obj = obj_class.allocate
+ if obj.is_a?(Chain)
+ obj = obj_class.new
+ obj.load_hash(hash_or_array)
+ instance_variable_set(ivar, obj)
+ else
+ instance_variable_set(ivar, Layers::Layer.from_hash(hash_or_array))
+ end
+ end
+ end
+ end
+ end
+
# This class deals with the model of the network.
- class Model
+ class Model < Chain
attr_accessor :optimizer
attr_accessor :loss_func
attr_reader :last_log
# Load marshal model.
# @param [String] file_name File name of marshal model to load.
# @return [DNN::Models::Model] Return the loaded model.
def self.load(file_name)
- model = new
+ model = self.allocate
loader = Loaders::MarshalLoader.new(model)
loader.load(file_name)
model
end
@@ -111,12 +201,16 @@
log << metrics_to_str(train_step_met)
print log if verbose
end
if test
- test_met = test(test[0], test[1], batch_size: batch_size)
- print " " + metrics_to_str(test_met) if verbose
+ acc, loss = if test.is_a?(Array)
+ evaluate(test[0], test[1], batch_size: batch_size)
+ else
+ evaluate_by_iterator(test, batch_size: batch_size)
+ end
+ print " " + metrics_to_str({ accuracy: acc, test_loss: loss }) if verbose
end
puts "" if verbose
call_callbacks(:after_epoch)
end
nil
@@ -136,20 +230,10 @@
private def train_step(x, y)
loss_value = train_on_batch(x, y)
{ loss: loss_value }
end
- # Implement the test process to be performed.
- # @param [Numo::SFloat] x Input training data.
- # @param [Numo::SFloat] y Output training data.
- # @param [Integer] batch_size Batch size used for one test.
- # @return [Hash] Hash of contents to be output to log.
- private def test(x, y, batch_size: 100)
- acc, test_loss = accuracy(x, y, batch_size: batch_size)
- { accuracy: acc, test_loss: test_loss }
- end
-
# Training once.
# Setup the model before use this method.
# @param [Numo::SFloat] x Input training data.
# @param [Numo::SFloat] y Output training data.
# @return [Float | Numo::SFloat] Return loss value in the form of Float or Numo::SFloat.
@@ -167,24 +251,28 @@
@last_log[:train_loss] = loss_value
call_callbacks(:after_train_on_batch)
loss_value
end
- # Evaluate model and get accuracy of test data.
+ # Evaluate model and get accuracy and loss of test data.
# @param [Numo::SFloat] x Input test data.
# @param [Numo::SFloat] y Output test data.
# @param [Integer] batch_size Batch size used for one test.
# @return [Array] Returns the test data accuracy and mean loss in the form [accuracy, mean_loss].
- def accuracy(x, y, batch_size: 100)
+ def evaluate(x, y, batch_size: 100)
check_xy_type(x, y)
- num_test_datas = x.is_a?(Array) ? x[0].shape[0] : x.shape[0]
+ evaluate_by_iterator(Iterator.new(x, y, random: false))
+ end
+
+ # Evaluate model by iterator
+ def evaluate_by_iterator(test_iterator, batch_size: 100)
+ num_test_datas = test_iterator.num_datas
batch_size = batch_size >= num_test_datas[0] ? num_test_datas : batch_size
- iter = Iterator.new(x, y, random: false)
total_correct = 0
sum_loss = 0
max_steps = (num_test_datas.to_f / batch_size).ceil
- iter.foreach(batch_size) do |x_batch, y_batch|
+ test_iterator.foreach(batch_size) do |x_batch, y_batch|
correct, loss_value = test_on_batch(x_batch, y_batch)
total_correct += correct
sum_loss += loss_value
end
mean_loss = sum_loss / max_steps
@@ -199,20 +287,20 @@
# @param [Numo::SFloat] y Output test data.
# @return [Array] Returns the test data accuracy and mean loss in the form [accuracy, mean_loss].
def test_on_batch(x, y)
call_callbacks(:before_test_on_batch)
x = forward(x, false)
- correct = evaluate(x, y)
+ correct = accuracy(x, y)
loss_value = @loss_func.loss(x, y)
call_callbacks(:after_test_on_batch)
[correct, loss_value]
end
- # Implement the process to evaluate this model.
+ # Implement the process to accuracy this model.
# @param [Numo::SFloat] x Input test data.
# @param [Numo::SFloat] y Output test data.
- private def evaluate(x, y)
+ private def accuracy(x, y)
if x.shape[1..-1] == [1]
correct = 0
x.shape[0].times do |i|
if @loss_func.is_a?(Losses::SigmoidCrossEntropy)
correct += 1 if (x[i, 0] < 0 && y[i, 0] < 0.5) || (x[i, 0] >= 0 && y[i, 0] >= 0.5)
@@ -255,61 +343,83 @@
# Clear the callback function registered for each event.
def clear_callbacks
@callbacks = []
end
+ # Load marshal params.
+ # @param [String] file_name File name of marshal model to load.
+ def load_params(file_name)
+ loader = Loaders::MarshalLoader.new(self)
+ loader.load(file_name)
+ end
+
# Save the model in marshal format.
# @param [String] file_name Name to save model.
- # @param [Boolean] include_optimizer Set true to save data included optimizer status.
- def save(file_name, include_optimizer: true)
- saver = Savers::MarshalSaver.new(self, include_optimizer: include_optimizer)
+ def save(file_name)
+ saver = Savers::MarshalSaver.new(self, include_model: true)
saver.save(file_name)
end
+ # Save the params in marshal format.
+ # @param [String] file_name Name to save model.
+ def save_params(file_name)
+ saver = Savers::MarshalSaver.new(self, include_model: false)
+ saver.save(file_name)
+ end
+
# @return [DNN::Models::Model] Return the copy this model.
def copy
Marshal.load(Marshal.dump(self))
end
- # Get the all layers.
- # @return [Array] All layers array.
- def layers
- raise DNN_Error, "This model is not built. You need build this model using predict or train." unless built?
- return @layers_cache if @layers_cache
- layers = []
- get_layers = -> link do
- return unless link
- layers.unshift(link.layer)
- if link.is_a?(TwoInputLink)
- get_layers.(link.prev1)
- get_layers.(link.prev2)
- else
- get_layers.(link.prev)
- end
- end
- get_layers.(@last_link)
- @layers_cache = layers.uniq
- end
-
- # Get the all has param layers.
+ # Get the all trainable layers.
# @return [Array] All has param layers array.
- def has_param_layers
- layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
+ def trainable_layers
+ layers.select { |layer| layer.is_a?(Layers::TrainableLayer) }
end
# Get the layer that the model has.
# @param [Symbol] name The name of the layer to get.
# @return [DNN::Layers::Layer] Return the layer.
def get_layer(name)
- layers.find { |layer| layer.name == name }
+ layer = instance_variable_get("@#{name}")
+ if layer.is_a?(Layers::Layer) || layer.is_a?(Chain) || layer.is_a?(LayersList)
+ return layer
+ end
+ nil
end
# @return [Boolean] If model have already been built then return true.
def built?
@built
end
+ def clean_layers
+ layers.each do |layer|
+ layer.clean
+ end
+ @loss_func.clean
+ @last_link = nil
+ @layers_cache = nil
+ end
+
+ def get_all_params_data
+ trainable_layers.map do |layer|
+ layer.get_params.to_h do |key, param|
+ [key, param.data]
+ end
+ end
+ end
+
+ def set_all_params_data(params_data)
+ trainable_layers.each.with_index do |layer, i|
+ params_data[i].each do |(key, data)|
+ layer.get_params[key].data = data
+ end
+ end
+ end
+
private
def forward(x, learning_phase)
DNN.learning_phase = learning_phase
@layers_cache = nil
@@ -320,11 +430,10 @@
end
output_tensor = call(inputs)
@last_link = output_tensor.link
unless @built
@built = true
- naming
end
output_tensor.data
end
def backward(dy)
@@ -335,23 +444,10 @@
@callbacks.each do |callback|
callback.send(event) if callback.respond_to?(event)
end
end
- def naming
- layers.each do |layer|
- id = layers.select { |l| l.is_a?(layer.class) }.index(layer)
- class_name = layer.class.name.split("::").last
- layer.name = "#{class_name}_#{id}".to_sym unless layer.name
- if layer.is_a?(Layers::HasParamLayer)
- layer.get_params.each do |param_key, param|
- param.name = "#{layer.name}__#{param_key}".to_sym unless param.name
- end
- end
- end
- end
-
def metrics_to_str(mertics)
mertics.map { |key, num| "#{key}: #{sprintf('%.4f', num)}" }.join(", ")
end
def check_xy_type(x, y = nil)
@@ -368,22 +464,22 @@
attr_reader :stack
# @param [Array] stack All layers possessed by the model.
def initialize(stack = [])
super()
- @stack = []
+ @stack = LayersList.new
stack.each do |layer|
add(layer)
end
end
# Add layer to the model.
# @param [DNN::Layers::Layer] layer Layer to add to the model.
# @return [DNN::Models::Model] Return self.
def add(layer)
- if layer.is_a?(MergeLayers::MergeLayer)
- raise TypeError, "layer: #{layer.class.name} should not be a DNN::MergeLayers::MergeLayer class."
+ if layer.is_a?(Layers::MergeLayer)
+ raise TypeError, "layer: #{layer.class.name} should not be a DNN::Layers::MergeLayer class."
end
unless layer.is_a?(Layers::Layer) || layer.is_a?(Model)
raise TypeError, "layer: #{layer.class.name} is not an instance of the DNN::Layers::Layer class or DNN::Models::Model class."
end
@stack << layer
@@ -394,11 +490,11 @@
# Insert layer to the model by index position.
# @param [DNN::Layers::Layer] layer Layer to add to the model.
# @return [DNN::Models::Model] Return self.
def insert(index, layer)
- if layer.is_a?(MergeLayers::MergeLayer)
- raise TypeError, "layer: #{layer.class.name} should not be a DNN::MergeLayers::MergeLayer class."
+ if layer.is_a?(Layers::MergeLayer)
+ raise TypeError, "layer: #{layer.class.name} should not be a DNN::Layers::MergeLayer class."
end
unless layer.is_a?(Layers::Layer) || layer.is_a?(Model)
raise TypeError, "layer: #{layer.class.name} is not an instance of the DNN::Layers::Layer class or DNN::Models::Model class."
end
@stack.insert(index, layer)