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)