lib/dnn/core/model.rb in ruby-dnn-0.8.8 vs lib/dnn/core/model.rb in ruby-dnn-0.9.0

- old
+ new

@@ -1,5 +1,6 @@ +require "zlib" require "json" require "base64" module DNN @@ -7,34 +8,39 @@ class Model attr_accessor :layers # All layers possessed by the model attr_accessor :trainable # Setting false prevents learning of parameters. def self.load(file_name) - Marshal.load(File.binread(file_name)) + Marshal.load(Zlib::Inflate.inflate(File.binread(file_name))) end 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 + + def self.load_hash(hash) model = self.new - model.layers = hash[:layers].map { |hash_layer| Util.load_hash(hash_layer) } - model.compile(Util.load_hash(hash[:optimizer])) + model.layers = hash[:layers].map { |hash_layer| Utils.load_hash(hash_layer) } model end def initialize @layers = [] @trainable = true @optimizer = nil - @training = false @compiled = false end def load_json_params(json_str) - has_param_layers_params = JSON.parse(json_str, symbolize_names: true) + hash = JSON.parse(json_str, symbolize_names: true) + has_param_layers_params = hash[:params] has_param_layers_index = 0 - @layers.each do |layer| - next unless layer.is_a?(HasParamLayer) + 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) if layer.params[key].is_a?(Param) @@ -44,109 +50,140 @@ end end has_param_layers_index += 1 end end - + def save(file_name) - marshal = Marshal.dump(self) + bin = Zlib::Deflate.deflate(Marshal.dump(self)) begin - File.binwrite(file_name, marshal) + File.binwrite(file_name, bin) rescue Errno::ENOENT => ex dir_name = file_name.match(%r`(.*)/.+$`)[1] Dir.mkdir(dir_name) - File.binwrite(file_name, marshal) + File.binwrite(file_name, bin) end end def to_json - hash_layers = @layers.map { |layer| layer.to_hash } - hash = {version: VERSION, layers: hash_layers, optimizer: @optimizer.to_hash} + hash = self.to_hash + hash[:version] = VERSION JSON.pretty_generate(hash) end def params_to_json - has_param_layers = @layers.select { |layer| layer.is_a?(Layers::HasParamLayer) } + 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 - JSON.dump(has_param_layers_params) + hash = {version: VERSION, params: has_param_layers_params} + JSON.dump(hash) end - + 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.") + # 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 - - def compile(optimizer) + + def compile(optimizer, loss) unless optimizer.is_a?(Optimizers::Optimizer) - raise TypeError.new("optimizer is not an instance of the DNN::Optimizers::Optimizer class.") + 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 def build(super_model = nil) @super_model = super_model - @layers.each do |layer| - layer.build(self) + 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 + def input_shape + @layers.first.input_shape + end + + def output_shape + @layers.last.output_shape + end + def optimizer + raise DNN_Error.new("The model is not compiled.") unless compiled? @optimizer ? @optimizer : @super_model.optimizer end + def loss + raise DNN_Error.new("The model is not compiled.") unless compiled? + @loss ? @loss : @super_model.loss + end + def compiled? @compiled end - def training? - @training - end - def train(x, y, epochs, batch_size: 1, test: nil, verbose: true, batch_proc: nil, &epoch_proc) unless compiled? raise DNN_Error.new("The model is not compiled.") end - num_train_data = x.shape[0] + 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_data.to_f / batch_size).ceil.times do |index| - x_batch, y_batch = Util.get_minibatch(x, y, batch_size) + (num_train_datas.to_f / batch_size).ceil.times do |index| + x_batch, y_batch = dataset.get_batch(batch_size) loss = train_on_batch(x_batch, y_batch, &batch_proc) if loss.nan? puts "\nloss is nan" if verbose return end - num_trained_data = (index + 1) * batch_size - num_trained_data = num_trained_data > num_train_data ? num_train_data : num_trained_data + 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_data * 40 / num_train_data + if i < num_trained_datas * 40 / num_train_datas log << "=" - elsif i == num_trained_data * 40 / num_train_data + elsif i == num_trained_datas * 40 / num_train_datas log << ">" else log << "_" end end - log << " #{num_trained_data}/#{num_train_data} loss: #{sprintf('%.8f', loss)}" + log << " #{num_trained_datas}/#{num_train_datas} loss: #{sprintf('%.8f', loss)}" print log if verbose end if verbose && test acc = accurate(test[0], test[1], batch_size, &batch_proc) print " accurate: #{acc}" @@ -155,21 +192,24 @@ epoch_proc.call(epoch) if epoch_proc end end def train_on_batch(x, y, &batch_proc) + check_xy_type(x, y) input_data_shape_check(x, y) x, y = batch_proc.call(x, y) if batch_proc - forward(x, true) - loss_value = loss(y) - backward(y) - dloss + out = forward(x, true) + loss_value = @loss.forward(out, y) + @loss.regularize(get_all_layers) + dout = @loss.backward(y) + backward(dout, true) + @loss.d_regularize(get_all_layers) update loss_value end 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]) @@ -181,26 +221,28 @@ 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[-1].shape == [1] + 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 def predict(x) + check_xy_type(x) input_data_shape_check(x) forward(x, false) end def predict1(x) + check_xy_type(x) predict(Xumo::SFloat.cast([x]))[0, false] end def copy Marshal.load(Marshal.dump(self)) @@ -220,41 +262,40 @@ @layers.map { |layer| layer.is_a?(Model) ? layer.get_all_layers : layer }.flatten end - def forward(x, training) - @training = training + def forward(x, learning_phase) @layers.each do |layer| - x = if layer.is_a?(Layers::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) - elsif layer.is_a?(Model) - layer.forward(x, training) end end x end - - def loss(y) - @layers[-1].loss(y) - end - - def dloss - @layers[-1].dloss - end - def backward(y) - dout = y + def backward(dout, learning_phase) @layers.reverse.each do |layer| - dout = layer.backward(dout) + if layer.is_a?(Layers::Dropout) || layer.is_a?(Layers::BatchNormalization) || layer.is_a?(Model) + dout = layer.backward(dout, learning_phase) + else + dout = layer.backward(dout) + end end dout end def update + return unless @trainable @layers.each do |layer| - layer.update if @trainable && (layer.is_a?(Layers::HasParamLayer) || layer.is_a?(Model)) + if layer.is_a?(Layers::HasParamLayer) + layer.update(@optimizer) + elsif layer.is_a?(Model) + layer.update + end end end def get_prev_layer(layer) layer_index = @layers.index(layer) @@ -268,37 +309,39 @@ @layers[layer_index - 1] end if prev_layer.is_a?(Layers::Layer) prev_layer elsif prev_layer.is_a?(Model) - prev_layer.layers[-1] + prev_layer.layers.last end end + 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 + private def layers_check unless @layers.first.is_a?(Layers::InputLayer) raise TypeError.new("The first layer is not an InputLayer.") end - unless @layers.last.is_a?(Layers::OutputLayer) - raise TypeError.new("The last layer is not an OutputLayer.") - end end def input_data_shape_check(x, y = nil) - unless @layers.first.shape == x.shape[1..-1] - raise DNN_ShapeError.new("The shape of x does not match the input shape. x shape is #{x.shape[1..-1]}, but input shape is #{@layers.first.shape}.") + unless @layers.first.input_shape == x.shape[1..-1] + raise DNN_ShapeError.new("The shape of x does not match the input shape. x shape is #{x.shape[1..-1]}, but input shape is #{@layers.first.input_shape}.") end - if y && @layers.last.shape != y.shape[1..-1] - raise DNN_ShapeError.new("The shape of y does not match the input shape. y shape is #{y.shape[1..-1]}, but output shape is #{@layers.last.shape}.") + if y && @layers.last.output_shape != y.shape[1..-1] + raise DNN_ShapeError.new("The shape of y does not match the input shape. y shape is #{y.shape[1..-1]}, but output shape is #{@layers.last.output_shape}.") end end def layers_shape_check @layers.each.with_index do |layer, i| - prev_shape = layer.is_a?(Layers::Layer) ? layer.prev_layer.shape : layer.layers[-1] + prev_shape = layer.input_shape if layer.is_a?(Layers::Dense) if prev_shape.length != 1 raise DNN_ShapeError.new("layer index(#{i}) Dense: The shape of the previous layer is #{prev_shape}. The shape of the previous layer must be 1 dimensional.") end elsif layer.is_a?(Layers::Conv2D) || layer.is_a?(Layers::MaxPool2D) @@ -309,9 +352,24 @@ if prev_shape.length != 2 layer_name = layer.class.name.match("\:\:(.+)$")[1] raise DNN_ShapeError.new("layer index(#{i}) #{layer_name}: The shape of the previous layer is #{prev_shape}. The shape of the previous layer must be 3 dimensional.") end end + end + end + + def check_xy_type(x, y = nil) + unless x.is_a?(Xumo::SFloat) + raise TypeError.new("x:#{x.class.name} is not an instance of #{Xumo::SFloat.name} class.") + end + if y && !y.is_a?(Xumo::SFloat) + raise TypeError.new("y:#{y.class.name} is not an instance of #{Xumo::SFloat.name} class.") + end + end + + def type_check(var_name, var, type) + unless var.is_a?(type) + raise TypeError.new("#{var_name}:#{var.class} is not an instance of #{type} class.") end end end end