ruby-dnn 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/examples/cifar100_example.rb +71 -71
- data/examples/cifar10_example.rb +68 -68
- data/examples/iris_example.rb +34 -34
- data/examples/mnist_conv2d_example.rb +50 -50
- data/examples/mnist_example.rb +39 -39
- data/examples/mnist_lstm_example.rb +36 -36
- data/examples/xor_example.rb +24 -24
- data/lib/dnn.rb +27 -26
- data/lib/dnn/cifar10.rb +51 -51
- data/lib/dnn/cifar100.rb +49 -49
- data/lib/dnn/core/activations.rb +148 -148
- data/lib/dnn/core/cnn_layers.rb +464 -464
- data/lib/dnn/core/dataset.rb +34 -34
- data/lib/dnn/core/embedding.rb +56 -0
- data/lib/dnn/core/error.rb +5 -5
- data/lib/dnn/core/initializers.rb +126 -126
- data/lib/dnn/core/layers.rb +307 -307
- data/lib/dnn/core/losses.rb +175 -175
- data/lib/dnn/core/model.rb +461 -461
- data/lib/dnn/core/normalizations.rb +72 -72
- data/lib/dnn/core/optimizers.rb +283 -283
- data/lib/dnn/core/param.rb +9 -9
- data/lib/dnn/core/regularizers.rb +106 -106
- data/lib/dnn/core/rnn_layers.rb +464 -464
- data/lib/dnn/core/utils.rb +34 -34
- data/lib/dnn/downloader.rb +50 -50
- data/lib/dnn/image.rb +41 -41
- data/lib/dnn/iris.rb +60 -60
- data/lib/dnn/mnist.rb +84 -84
- data/lib/dnn/version.rb +3 -3
- metadata +2 -1
data/lib/dnn/core/losses.rb
CHANGED
@@ -1,175 +1,175 @@
|
|
1
|
-
module DNN
|
2
|
-
module Losses
|
3
|
-
|
4
|
-
class Loss
|
5
|
-
def forward(x, y, layers)
|
6
|
-
loss_value = forward_loss(x, y)
|
7
|
-
regularizers = layers.select { |layer| layer.is_a?(Connection) }
|
8
|
-
.map { |layer| layer.regularizers }.flatten
|
9
|
-
|
10
|
-
regularizers.each do |regularizer|
|
11
|
-
loss_value = regularizer.forward(loss_value)
|
12
|
-
end
|
13
|
-
loss_value
|
14
|
-
end
|
15
|
-
|
16
|
-
def backward(y, layers)
|
17
|
-
layers.select { |layer| layer.is_a?(Connection) }.each do |layer|
|
18
|
-
layer.regularizers.each do |regularizer|
|
19
|
-
regularizer.backward
|
20
|
-
end
|
21
|
-
end
|
22
|
-
backward_loss(y)
|
23
|
-
end
|
24
|
-
|
25
|
-
def to_hash(merge_hash = nil)
|
26
|
-
hash = {class: self.class.name}
|
27
|
-
hash.merge!(merge_hash) if merge_hash
|
28
|
-
hash
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def forward_loss(x, y)
|
34
|
-
raise NotImplementedError.new("Class '#{self.class.name}' has implement method 'forward_loss'")
|
35
|
-
end
|
36
|
-
|
37
|
-
def backward_loss(y)
|
38
|
-
raise NotImplementedError.new("Class '#{self.class.name}' has implement method 'backward_loss'")
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
class MeanSquaredError < Loss
|
43
|
-
private
|
44
|
-
|
45
|
-
def forward_loss(x, y)
|
46
|
-
@x = x
|
47
|
-
batch_size = y.shape[0]
|
48
|
-
0.5 * ((x - y)**2).sum / batch_size
|
49
|
-
end
|
50
|
-
|
51
|
-
def backward_loss(y)
|
52
|
-
@x - y
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
|
57
|
-
class MeanAbsoluteError < Loss
|
58
|
-
private
|
59
|
-
|
60
|
-
def forward_loss(x, y)
|
61
|
-
@x = x
|
62
|
-
batch_size = y.shape[0]
|
63
|
-
(x - y).abs.sum / batch_size
|
64
|
-
end
|
65
|
-
|
66
|
-
def backward_loss(y)
|
67
|
-
dy = @x - y
|
68
|
-
dy[dy >= 0] = 1
|
69
|
-
dy[dy < 0] = -1
|
70
|
-
dy
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
|
75
|
-
class HuberLoss < Loss
|
76
|
-
def forward(x, y, layers)
|
77
|
-
@loss_value = super(x, y, layers)
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
|
82
|
-
def forward_loss(x, y)
|
83
|
-
@x = x
|
84
|
-
loss_value = loss_l1(y)
|
85
|
-
loss_value > 1 ? loss_value : loss_l2(y)
|
86
|
-
end
|
87
|
-
|
88
|
-
def backward_loss(y)
|
89
|
-
dy = @x - y
|
90
|
-
if @loss_value > 1
|
91
|
-
dy[dy >= 0] = 1
|
92
|
-
dy[dy < 0] = -1
|
93
|
-
end
|
94
|
-
dy
|
95
|
-
end
|
96
|
-
|
97
|
-
def loss_l1(y)
|
98
|
-
batch_size = y.shape[0]
|
99
|
-
(@x - y).abs.sum / batch_size
|
100
|
-
end
|
101
|
-
|
102
|
-
def loss_l2(y)
|
103
|
-
batch_size = y.shape[0]
|
104
|
-
0.5 * ((@x - y)**2).sum / batch_size
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
|
109
|
-
class SoftmaxCrossEntropy < Loss
|
110
|
-
# @return [Float] Return the eps value.
|
111
|
-
attr_accessor :eps
|
112
|
-
|
113
|
-
def self.from_hash(hash)
|
114
|
-
SoftmaxCrossEntropy.new(eps: hash[:eps])
|
115
|
-
end
|
116
|
-
|
117
|
-
def self.softmax(x)
|
118
|
-
NMath.exp(x) / NMath.exp(x).sum(1).reshape(x.shape[0], 1)
|
119
|
-
end
|
120
|
-
|
121
|
-
# @param [Float] eps Value to avoid nan.
|
122
|
-
def initialize(eps: 1e-7)
|
123
|
-
@eps = eps
|
124
|
-
end
|
125
|
-
|
126
|
-
def to_hash
|
127
|
-
super(eps: @eps)
|
128
|
-
end
|
129
|
-
|
130
|
-
private
|
131
|
-
|
132
|
-
def forward_loss(x, y)
|
133
|
-
@x = SoftmaxCrossEntropy.softmax(x)
|
134
|
-
batch_size = y.shape[0]
|
135
|
-
-(y * NMath.log(@x + @eps)).sum / batch_size
|
136
|
-
end
|
137
|
-
|
138
|
-
def backward_loss(y)
|
139
|
-
@x - y
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
|
144
|
-
class SigmoidCrossEntropy < Loss
|
145
|
-
# @return [Float] Return the eps value.
|
146
|
-
attr_accessor :eps
|
147
|
-
|
148
|
-
def self.from_hash(hash)
|
149
|
-
SigmoidCrossEntropy.new(eps: hash[:eps])
|
150
|
-
end
|
151
|
-
|
152
|
-
# @param [Float] eps Value to avoid nan.
|
153
|
-
def initialize(eps: 1e-7)
|
154
|
-
@eps = eps
|
155
|
-
end
|
156
|
-
|
157
|
-
def to_hash
|
158
|
-
super(eps: @eps)
|
159
|
-
end
|
160
|
-
|
161
|
-
private
|
162
|
-
|
163
|
-
def forward_loss(x, y)
|
164
|
-
@x = Sigmoid.new.forward(x)
|
165
|
-
batch_size = y.shape[0]
|
166
|
-
-(y * NMath.log(@x) + (1 - y) * NMath.log(1 - @x))
|
167
|
-
end
|
168
|
-
|
169
|
-
def backward_loss(y)
|
170
|
-
@x - y
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
end
|
175
|
-
end
|
1
|
+
module DNN
|
2
|
+
module Losses
|
3
|
+
|
4
|
+
class Loss
|
5
|
+
def forward(x, y, layers)
|
6
|
+
loss_value = forward_loss(x, y)
|
7
|
+
regularizers = layers.select { |layer| layer.is_a?(Connection) }
|
8
|
+
.map { |layer| layer.regularizers }.flatten
|
9
|
+
|
10
|
+
regularizers.each do |regularizer|
|
11
|
+
loss_value = regularizer.forward(loss_value)
|
12
|
+
end
|
13
|
+
loss_value
|
14
|
+
end
|
15
|
+
|
16
|
+
def backward(y, layers)
|
17
|
+
layers.select { |layer| layer.is_a?(Connection) }.each do |layer|
|
18
|
+
layer.regularizers.each do |regularizer|
|
19
|
+
regularizer.backward
|
20
|
+
end
|
21
|
+
end
|
22
|
+
backward_loss(y)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash(merge_hash = nil)
|
26
|
+
hash = {class: self.class.name}
|
27
|
+
hash.merge!(merge_hash) if merge_hash
|
28
|
+
hash
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def forward_loss(x, y)
|
34
|
+
raise NotImplementedError.new("Class '#{self.class.name}' has implement method 'forward_loss'")
|
35
|
+
end
|
36
|
+
|
37
|
+
def backward_loss(y)
|
38
|
+
raise NotImplementedError.new("Class '#{self.class.name}' has implement method 'backward_loss'")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class MeanSquaredError < Loss
|
43
|
+
private
|
44
|
+
|
45
|
+
def forward_loss(x, y)
|
46
|
+
@x = x
|
47
|
+
batch_size = y.shape[0]
|
48
|
+
0.5 * ((x - y)**2).sum / batch_size
|
49
|
+
end
|
50
|
+
|
51
|
+
def backward_loss(y)
|
52
|
+
@x - y
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
class MeanAbsoluteError < Loss
|
58
|
+
private
|
59
|
+
|
60
|
+
def forward_loss(x, y)
|
61
|
+
@x = x
|
62
|
+
batch_size = y.shape[0]
|
63
|
+
(x - y).abs.sum / batch_size
|
64
|
+
end
|
65
|
+
|
66
|
+
def backward_loss(y)
|
67
|
+
dy = @x - y
|
68
|
+
dy[dy >= 0] = 1
|
69
|
+
dy[dy < 0] = -1
|
70
|
+
dy
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
class HuberLoss < Loss
|
76
|
+
def forward(x, y, layers)
|
77
|
+
@loss_value = super(x, y, layers)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def forward_loss(x, y)
|
83
|
+
@x = x
|
84
|
+
loss_value = loss_l1(y)
|
85
|
+
loss_value > 1 ? loss_value : loss_l2(y)
|
86
|
+
end
|
87
|
+
|
88
|
+
def backward_loss(y)
|
89
|
+
dy = @x - y
|
90
|
+
if @loss_value > 1
|
91
|
+
dy[dy >= 0] = 1
|
92
|
+
dy[dy < 0] = -1
|
93
|
+
end
|
94
|
+
dy
|
95
|
+
end
|
96
|
+
|
97
|
+
def loss_l1(y)
|
98
|
+
batch_size = y.shape[0]
|
99
|
+
(@x - y).abs.sum / batch_size
|
100
|
+
end
|
101
|
+
|
102
|
+
def loss_l2(y)
|
103
|
+
batch_size = y.shape[0]
|
104
|
+
0.5 * ((@x - y)**2).sum / batch_size
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
class SoftmaxCrossEntropy < Loss
|
110
|
+
# @return [Float] Return the eps value.
|
111
|
+
attr_accessor :eps
|
112
|
+
|
113
|
+
def self.from_hash(hash)
|
114
|
+
SoftmaxCrossEntropy.new(eps: hash[:eps])
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.softmax(x)
|
118
|
+
NMath.exp(x) / NMath.exp(x).sum(1).reshape(x.shape[0], 1)
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param [Float] eps Value to avoid nan.
|
122
|
+
def initialize(eps: 1e-7)
|
123
|
+
@eps = eps
|
124
|
+
end
|
125
|
+
|
126
|
+
def to_hash
|
127
|
+
super(eps: @eps)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def forward_loss(x, y)
|
133
|
+
@x = SoftmaxCrossEntropy.softmax(x)
|
134
|
+
batch_size = y.shape[0]
|
135
|
+
-(y * NMath.log(@x + @eps)).sum / batch_size
|
136
|
+
end
|
137
|
+
|
138
|
+
def backward_loss(y)
|
139
|
+
@x - y
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
class SigmoidCrossEntropy < Loss
|
145
|
+
# @return [Float] Return the eps value.
|
146
|
+
attr_accessor :eps
|
147
|
+
|
148
|
+
def self.from_hash(hash)
|
149
|
+
SigmoidCrossEntropy.new(eps: hash[:eps])
|
150
|
+
end
|
151
|
+
|
152
|
+
# @param [Float] eps Value to avoid nan.
|
153
|
+
def initialize(eps: 1e-7)
|
154
|
+
@eps = eps
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_hash
|
158
|
+
super(eps: @eps)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def forward_loss(x, y)
|
164
|
+
@x = Sigmoid.new.forward(x)
|
165
|
+
batch_size = y.shape[0]
|
166
|
+
-(y * NMath.log(@x) + (1 - y) * NMath.log(1 - @x))
|
167
|
+
end
|
168
|
+
|
169
|
+
def backward_loss(y)
|
170
|
+
@x - y
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
data/lib/dnn/core/model.rb
CHANGED
@@ -1,461 +1,461 @@
|
|
1
|
-
require "zlib"
|
2
|
-
require "json"
|
3
|
-
require "base64"
|
4
|
-
|
5
|
-
module DNN
|
6
|
-
|
7
|
-
# This class deals with the model of the network.
|
8
|
-
class Model
|
9
|
-
# @return [Array] All layers possessed by the model.
|
10
|
-
attr_accessor :layers
|
11
|
-
# @return [Bool] Setting false prevents learning of parameters.
|
12
|
-
attr_accessor :trainable
|
13
|
-
|
14
|
-
# Load marshal model.
|
15
|
-
# @param [String] file_name File name of marshal model to load.
|
16
|
-
def self.load(file_name)
|
17
|
-
Marshal.load(Zlib::Inflate.inflate(File.binread(file_name)))
|
18
|
-
end
|
19
|
-
|
20
|
-
# Load json model.
|
21
|
-
# @param [String] json_str json string to load model.
|
22
|
-
# @return [DNN::Model]
|
23
|
-
def self.load_json(json_str)
|
24
|
-
hash = JSON.parse(json_str, symbolize_names: true)
|
25
|
-
model = self.from_hash(hash)
|
26
|
-
model.compile(Utils.from_hash(hash[:optimizer]), Utils.from_hash(hash[:loss]))
|
27
|
-
model
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.from_hash(hash)
|
31
|
-
model = self.new
|
32
|
-
model.layers = hash[:layers].map { |hash_layer| Utils.from_hash(hash_layer) }
|
33
|
-
model
|
34
|
-
end
|
35
|
-
|
36
|
-
def initialize
|
37
|
-
@layers = []
|
38
|
-
@trainable = true
|
39
|
-
@optimizer = nil
|
40
|
-
@compiled = false
|
41
|
-
end
|
42
|
-
|
43
|
-
# Load json model parameters.
|
44
|
-
# @param [String] json_str json string to load model parameters.
|
45
|
-
def load_json_params(json_str)
|
46
|
-
hash = JSON.parse(json_str, symbolize_names: true)
|
47
|
-
has_param_layers_params = hash[:params]
|
48
|
-
has_param_layers_index = 0
|
49
|
-
has_param_layers = get_all_layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
|
50
|
-
has_param_layers.each do |layer|
|
51
|
-
hash_params = has_param_layers_params[has_param_layers_index]
|
52
|
-
hash_params.each do |key, (shape, base64_param)|
|
53
|
-
bin = Base64.decode64(base64_param)
|
54
|
-
data = Xumo::SFloat.from_binary(bin).reshape(*shape)
|
55
|
-
layer.params[key].data = data
|
56
|
-
end
|
57
|
-
has_param_layers_index += 1
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# Save the model in marshal format.
|
62
|
-
# @param [String] file_name name to save model.
|
63
|
-
def save(file_name)
|
64
|
-
bin = Zlib::Deflate.deflate(Marshal.dump(self))
|
65
|
-
begin
|
66
|
-
File.binwrite(file_name, bin)
|
67
|
-
rescue Errno::ENOENT => ex
|
68
|
-
dir_name = file_name.match(%r`(.*)/.+$`)[1]
|
69
|
-
Dir.mkdir(dir_name)
|
70
|
-
File.binwrite(file_name, bin)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
# Convert model to json string.
|
75
|
-
# @return [String] json string.
|
76
|
-
def to_json
|
77
|
-
hash = self.to_hash
|
78
|
-
hash[:version] = VERSION
|
79
|
-
JSON.pretty_generate(hash)
|
80
|
-
end
|
81
|
-
|
82
|
-
# Convert model parameters to json string.
|
83
|
-
# @return [String] json string.
|
84
|
-
def params_to_json
|
85
|
-
has_param_layers = get_all_layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
|
86
|
-
has_param_layers_params = has_param_layers.map do |layer|
|
87
|
-
layer.params.map { |key, param|
|
88
|
-
base64_data = Base64.encode64(param.data.to_binary)
|
89
|
-
[key, [param.data.shape, base64_data]]
|
90
|
-
}.to_h
|
91
|
-
end
|
92
|
-
hash = {version: VERSION, params: has_param_layers_params}
|
93
|
-
JSON.dump(hash)
|
94
|
-
end
|
95
|
-
|
96
|
-
# Add layer to the model.
|
97
|
-
# @param [DNN::Layers::Layer] layer Layer to add to the model.
|
98
|
-
# @return [DNN::Model] return self.
|
99
|
-
def <<(layer)
|
100
|
-
if !layer.is_a?(Layers::Layer) && !layer.is_a?(Model)
|
101
|
-
raise TypeError.new("layer is not an instance of the DNN::Layers::Layer class or DNN::Model class.")
|
102
|
-
end
|
103
|
-
@layers << layer
|
104
|
-
self
|
105
|
-
end
|
106
|
-
|
107
|
-
# Set optimizer and loss_func to model and build all layers.
|
108
|
-
# @param [DNN::Optimizers::Optimizer] optimizer Optimizer to use for learning.
|
109
|
-
# @param [DNN::Losses::Loss] loss_func Loss function to use for learning.
|
110
|
-
def compile(optimizer, loss_func)
|
111
|
-
raise DNN_Error.new("The model is already compiled.") if compiled?
|
112
|
-
unless optimizer.is_a?(Optimizers::Optimizer)
|
113
|
-
raise TypeError.new("optimizer:#{optimizer.class} is not an instance of DNN::Optimizers::Optimizer class.")
|
114
|
-
end
|
115
|
-
unless loss_func.is_a?(Losses::Loss)
|
116
|
-
raise TypeError.new("loss_func:#{loss_func.class} is not an instance of DNN::Losses::Loss class.")
|
117
|
-
end
|
118
|
-
@compiled = true
|
119
|
-
layers_check
|
120
|
-
@optimizer = optimizer
|
121
|
-
@loss_func = loss_func
|
122
|
-
build
|
123
|
-
layers_shape_check
|
124
|
-
end
|
125
|
-
|
126
|
-
# Set optimizer and loss_func to model and recompile. But does not build layers.
|
127
|
-
# @param [DNN::Optimizers::Optimizer] optimizer Optimizer to use for learning.
|
128
|
-
# @param [DNN::Losses::Loss] loss_func Loss function to use for learning.
|
129
|
-
def recompile(optimizer, loss_func)
|
130
|
-
unless optimizer.is_a?(Optimizers::Optimizer)
|
131
|
-
raise TypeError.new("optimizer:#{optimizer.class} is not an instance of DNN::Optimizers::Optimizer class.")
|
132
|
-
end
|
133
|
-
unless loss_func.is_a?(Losses::Loss)
|
134
|
-
raise TypeError.new("loss_func:#{loss_func.class} is not an instance of DNN::Losses::Loss class.")
|
135
|
-
end
|
136
|
-
@compiled = true
|
137
|
-
layers_check
|
138
|
-
@optimizer = optimizer
|
139
|
-
@loss_func = loss_func
|
140
|
-
layers_shape_check
|
141
|
-
end
|
142
|
-
|
143
|
-
def build(super_model = nil)
|
144
|
-
@super_model = super_model
|
145
|
-
shape = if super_model
|
146
|
-
super_model.get_prev_layer(self).output_shape
|
147
|
-
else
|
148
|
-
@layers.first.build
|
149
|
-
end
|
150
|
-
layers = super_model ? @layers : @layers[1..-1]
|
151
|
-
layers.each do |layer|
|
152
|
-
if layer.is_a?(Model)
|
153
|
-
layer.build(self)
|
154
|
-
layer.recompile(@optimizer, @loss_func)
|
155
|
-
else
|
156
|
-
layer.build(shape)
|
157
|
-
end
|
158
|
-
shape = layer.output_shape
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
# @return [Array] Return the input shape of the model.
|
163
|
-
def input_shape
|
164
|
-
@layers.first.input_shape
|
165
|
-
end
|
166
|
-
|
167
|
-
# @return [Array] Return the output shape of the model.
|
168
|
-
def output_shape
|
169
|
-
@layers.last.output_shape
|
170
|
-
end
|
171
|
-
|
172
|
-
# @return [DNN::Optimizers::Optimizer] optimizer Return the optimizer to use for learning.
|
173
|
-
def optimizer
|
174
|
-
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
175
|
-
@optimizer
|
176
|
-
end
|
177
|
-
|
178
|
-
# @return [DNN::Losses::Loss] loss Return the loss to use for learning.
|
179
|
-
def loss_func
|
180
|
-
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
181
|
-
@loss_func
|
182
|
-
end
|
183
|
-
|
184
|
-
# @return [Bool] Returns whether the model is learning.
|
185
|
-
def compiled?
|
186
|
-
@compiled
|
187
|
-
end
|
188
|
-
|
189
|
-
# Start training.
|
190
|
-
# Compile the model before use this method.
|
191
|
-
# @param [Numo::SFloat] x Input training data.
|
192
|
-
# @param [Numo::SFloat] y Output training data.
|
193
|
-
# @param [Integer] epochs Number of training.
|
194
|
-
# @param [Integer] batch_size Batch size used for one training.
|
195
|
-
# @param [Array or NilClass] test If you to test the model for every 1 epoch,
|
196
|
-
# specify [x_test, y_test]. Don't test to the model, specify nil.
|
197
|
-
# @param [Bool] verbose Set true to display the log. If false is set, the log is not displayed.
|
198
|
-
# @param [Lambda] before_epoch_cbk Process performed before one training.
|
199
|
-
# @param [Lambda] after_epoch_cbk Process performed after one training.
|
200
|
-
# @param [Lambda] before_batch_cbk Set the proc to be performed before batch processing.
|
201
|
-
# @param [Lambda] after_batch_cbk Set the proc to be performed after batch processing.
|
202
|
-
def train(x, y, epochs,
|
203
|
-
batch_size: 1,
|
204
|
-
test: nil,
|
205
|
-
verbose: true,
|
206
|
-
before_epoch_cbk: nil,
|
207
|
-
after_epoch_cbk: nil,
|
208
|
-
before_batch_cbk: nil,
|
209
|
-
after_batch_cbk: nil)
|
210
|
-
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
211
|
-
check_xy_type(x, y)
|
212
|
-
dataset = Dataset.new(x, y)
|
213
|
-
num_train_datas = x.shape[0]
|
214
|
-
(1..epochs).each do |epoch|
|
215
|
-
before_epoch_cbk.call(epoch) if before_epoch_cbk
|
216
|
-
puts "【 epoch #{epoch}/#{epochs} 】" if verbose
|
217
|
-
(num_train_datas.to_f / batch_size).ceil.times do |index|
|
218
|
-
x_batch, y_batch = dataset.next_batch(batch_size)
|
219
|
-
loss_value = train_on_batch(x_batch, y_batch,
|
220
|
-
before_batch_cbk: before_batch_cbk, after_batch_cbk: after_batch_cbk)
|
221
|
-
if loss_value.is_a?(Numo::SFloat)
|
222
|
-
loss_value = loss_value.mean
|
223
|
-
elsif loss_value.nan?
|
224
|
-
puts "\nloss is nan" if verbose
|
225
|
-
return
|
226
|
-
end
|
227
|
-
num_trained_datas = (index + 1) * batch_size
|
228
|
-
num_trained_datas = num_trained_datas > num_train_datas ? num_train_datas : num_trained_datas
|
229
|
-
log = "\r"
|
230
|
-
40.times do |i|
|
231
|
-
if i < num_trained_datas * 40 / num_train_datas
|
232
|
-
log << "="
|
233
|
-
elsif i == num_trained_datas * 40 / num_train_datas
|
234
|
-
log << ">"
|
235
|
-
else
|
236
|
-
log << "_"
|
237
|
-
end
|
238
|
-
end
|
239
|
-
log << " #{num_trained_datas}/#{num_train_datas} loss: #{sprintf('%.8f', loss_value)}"
|
240
|
-
print log if verbose
|
241
|
-
end
|
242
|
-
if verbose && test
|
243
|
-
acc, test_loss = accurate(test[0], test[1], batch_size,
|
244
|
-
before_batch_cbk: before_batch_cbk, after_batch_cbk: after_batch_cbk)
|
245
|
-
print " accurate: #{acc}, test loss: #{sprintf('%.8f', test_loss)}"
|
246
|
-
end
|
247
|
-
puts "" if verbose
|
248
|
-
after_epoch_cbk.call(epoch) if after_epoch_cbk
|
249
|
-
end
|
250
|
-
end
|
251
|
-
|
252
|
-
# Training once.
|
253
|
-
# Compile the model before use this method.
|
254
|
-
# @param [Numo::SFloat] x Input training data.
|
255
|
-
# @param [Numo::SFloat] y Output training data.
|
256
|
-
# @param [Lambda] before_batch_cbk Set the proc to be performed before batch processing.
|
257
|
-
# @param [Lambda] after_batch_cbk Set the proc to be performed after batch processing.
|
258
|
-
# @return [Float | Numo::SFloat] Return loss value in the form of Float or Numo::SFloat.
|
259
|
-
def train_on_batch(x, y, before_batch_cbk: nil, after_batch_cbk: nil)
|
260
|
-
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
261
|
-
check_xy_type(x, y)
|
262
|
-
input_data_shape_check(x, y)
|
263
|
-
x, y = before_batch_cbk.call(x, y, true) if before_batch_cbk
|
264
|
-
x = forward(x, true)
|
265
|
-
loss_value = @loss_func.forward(x, y, get_all_layers)
|
266
|
-
dy = @loss_func.backward(y, get_all_layers)
|
267
|
-
backward(dy)
|
268
|
-
update
|
269
|
-
after_batch_cbk.call(loss_value, true) if after_batch_cbk
|
270
|
-
loss_value
|
271
|
-
end
|
272
|
-
|
273
|
-
# Evaluate model and get accurate of test data.
|
274
|
-
# @param [Numo::SFloat] x Input test data.
|
275
|
-
# @param [Numo::SFloat] y Output test data.
|
276
|
-
# @param [Lambda] before_batch_cbk Set the proc to be performed before batch processing.
|
277
|
-
# @param [Lambda] after_batch_cbk Set the proc to be performed after batch processing.
|
278
|
-
# @return [Array] Returns the test data accurate and mean loss in the form [accurate, mean_loss].
|
279
|
-
def accurate(x, y, batch_size = 100, before_batch_cbk: nil, after_batch_cbk: nil)
|
280
|
-
check_xy_type(x, y)
|
281
|
-
input_data_shape_check(x, y)
|
282
|
-
batch_size = batch_size >= x.shape[0] ? x.shape[0] : batch_size
|
283
|
-
dataset = Dataset.new(x, y, false)
|
284
|
-
correct = 0
|
285
|
-
sum_loss = 0
|
286
|
-
(x.shape[0].to_f / batch_size).ceil.times do |i|
|
287
|
-
x_batch, y_batch = dataset.next_batch(batch_size)
|
288
|
-
x_batch, y_batch = before_batch_cbk.call(x_batch, y_batch, false) if before_batch_cbk
|
289
|
-
x_batch = forward(x_batch, false)
|
290
|
-
sigmoid = Sigmoid.new
|
291
|
-
batch_size.times do |j|
|
292
|
-
if @layers.last.output_shape == [1]
|
293
|
-
if @loss_func.is_a?(SigmoidCrossEntropy)
|
294
|
-
correct += 1 if sigmoid.forward(x_batch[j, 0]).round == y_batch[j, 0].round
|
295
|
-
else
|
296
|
-
correct += 1 if x_batch[j, 0].round == y_batch[j, 0].round
|
297
|
-
end
|
298
|
-
else
|
299
|
-
correct += 1 if x_batch[j, true].max_index == y_batch[j, true].max_index
|
300
|
-
end
|
301
|
-
end
|
302
|
-
loss_value = @loss_func.forward(x_batch, y_batch, get_all_layers)
|
303
|
-
after_batch_cbk.call(loss_value, false) if after_batch_cbk
|
304
|
-
sum_loss += loss_value.is_a?(Numo::SFloat) ? loss_value.mean : loss_value
|
305
|
-
end
|
306
|
-
mean_loss = sum_loss / batch_size
|
307
|
-
[correct.to_f / x.shape[0], mean_loss]
|
308
|
-
end
|
309
|
-
|
310
|
-
# Predict data.
|
311
|
-
# @param [Numo::SFloat] x Input data.
|
312
|
-
def predict(x)
|
313
|
-
check_xy_type(x)
|
314
|
-
input_data_shape_check(x)
|
315
|
-
forward(x, false)
|
316
|
-
end
|
317
|
-
|
318
|
-
# Predict one data.
|
319
|
-
# @param [Numo::SFloat] x Input data. However, x is single data.
|
320
|
-
def predict1(x)
|
321
|
-
check_xy_type(x)
|
322
|
-
predict(x.reshape(1, *x.shape))[0, false]
|
323
|
-
end
|
324
|
-
|
325
|
-
# Get loss value.
|
326
|
-
# @param [Numo::SFloat] x Input data.
|
327
|
-
# @param [Numo::SFloat] y Output data.
|
328
|
-
# @return [Float | Numo::SFloat] Return loss value in the form of Float or Numo::SFloat.
|
329
|
-
def loss(x, y)
|
330
|
-
check_xy_type(x, y)
|
331
|
-
input_data_shape_check(x, y)
|
332
|
-
x = forward(x, false)
|
333
|
-
@loss_func.forward(x, y, get_all_layers)
|
334
|
-
end
|
335
|
-
|
336
|
-
# @return [DNN::Model] Copy this model.
|
337
|
-
def copy
|
338
|
-
Marshal.load(Marshal.dump(self))
|
339
|
-
end
|
340
|
-
|
341
|
-
# Get the layer that the model has.
|
342
|
-
def get_layer(*args)
|
343
|
-
if args.length == 1
|
344
|
-
index = args[0]
|
345
|
-
@layers[index]
|
346
|
-
else
|
347
|
-
layer_class, index = args
|
348
|
-
@layers.select { |layer| layer.is_a?(layer_class) }[index]
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
# Get the all layers.
|
353
|
-
# @return [Array] all layers array.
|
354
|
-
def get_all_layers
|
355
|
-
@layers.map { |layer|
|
356
|
-
layer.is_a?(Model) ? layer.get_all_layers : layer
|
357
|
-
}.flatten
|
358
|
-
end
|
359
|
-
|
360
|
-
def forward(x, learning_phase)
|
361
|
-
@layers.each do |layer|
|
362
|
-
x = if layer.is_a?(Model)
|
363
|
-
layer.forward(x, learning_phase)
|
364
|
-
else
|
365
|
-
layer.learning_phase = learning_phase
|
366
|
-
layer.forward(x)
|
367
|
-
end
|
368
|
-
end
|
369
|
-
x
|
370
|
-
end
|
371
|
-
|
372
|
-
def backward(dy)
|
373
|
-
@layers.reverse.each do |layer|
|
374
|
-
dy = layer.backward(dy)
|
375
|
-
end
|
376
|
-
dy
|
377
|
-
end
|
378
|
-
|
379
|
-
def update
|
380
|
-
return unless @trainable
|
381
|
-
all_trainable_layers = @layers.map { |layer|
|
382
|
-
if layer.is_a?(Model)
|
383
|
-
layer.trainable ? layer.get_all_layers : nil
|
384
|
-
else
|
385
|
-
layer
|
386
|
-
end
|
387
|
-
}.flatten.compact.uniq
|
388
|
-
@optimizer.update(all_trainable_layers)
|
389
|
-
end
|
390
|
-
|
391
|
-
def get_prev_layer(layer)
|
392
|
-
layer_index = @layers.index(layer)
|
393
|
-
prev_layer = if layer_index == 0
|
394
|
-
if @super_model
|
395
|
-
@super_model.layers[@super_model.layers.index(self) - 1]
|
396
|
-
else
|
397
|
-
self
|
398
|
-
end
|
399
|
-
else
|
400
|
-
@layers[layer_index - 1]
|
401
|
-
end
|
402
|
-
if prev_layer.is_a?(Layers::Layer)
|
403
|
-
prev_layer
|
404
|
-
elsif prev_layer.is_a?(Model)
|
405
|
-
prev_layer.layers.last
|
406
|
-
end
|
407
|
-
end
|
408
|
-
|
409
|
-
def to_hash
|
410
|
-
hash_layers = @layers.map { |layer| layer.to_hash }
|
411
|
-
{class: Model.name, layers: hash_layers, optimizer: @optimizer.to_hash, loss: @loss_func.to_hash}
|
412
|
-
end
|
413
|
-
|
414
|
-
private
|
415
|
-
|
416
|
-
def layers_check
|
417
|
-
if !@layers.first.is_a?(Layers::InputLayer) && !@super_model
|
418
|
-
raise TypeError.new("The first layer is not an InputLayer.")
|
419
|
-
end
|
420
|
-
end
|
421
|
-
|
422
|
-
def input_data_shape_check(x, y = nil)
|
423
|
-
unless @layers.first.input_shape == x.shape[1..-1]
|
424
|
-
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}.")
|
425
|
-
end
|
426
|
-
if y && @layers.last.output_shape != y.shape[1..-1]
|
427
|
-
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}.")
|
428
|
-
end
|
429
|
-
end
|
430
|
-
|
431
|
-
def layers_shape_check
|
432
|
-
@layers.each.with_index do |layer, i|
|
433
|
-
prev_shape = layer.input_shape
|
434
|
-
if layer.is_a?(Layers::Dense)
|
435
|
-
if prev_shape.length != 1
|
436
|
-
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.")
|
437
|
-
end
|
438
|
-
elsif layer.is_a?(Layers::Conv2D) || layer.is_a?(Layers::MaxPool2D)
|
439
|
-
if prev_shape.length != 3
|
440
|
-
raise DNN_ShapeError.new("layer index(#{i}) Conv2D: The shape of the previous layer is #{prev_shape}. The shape of the previous layer must be 3 dimensional.")
|
441
|
-
end
|
442
|
-
elsif layer.is_a?(Layers::RNN)
|
443
|
-
if prev_shape.length != 2
|
444
|
-
layer_name = layer.class.name.match("\:\:(.+)$")[1]
|
445
|
-
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.")
|
446
|
-
end
|
447
|
-
end
|
448
|
-
end
|
449
|
-
end
|
450
|
-
|
451
|
-
def check_xy_type(x, y = nil)
|
452
|
-
unless x.is_a?(Xumo::SFloat)
|
453
|
-
raise TypeError.new("x:#{x.class.name} is not an instance of #{Xumo::SFloat.name} class.")
|
454
|
-
end
|
455
|
-
if y && !y.is_a?(Xumo::SFloat)
|
456
|
-
raise TypeError.new("y:#{y.class.name} is not an instance of #{Xumo::SFloat.name} class.")
|
457
|
-
end
|
458
|
-
end
|
459
|
-
end
|
460
|
-
|
461
|
-
end
|
1
|
+
require "zlib"
|
2
|
+
require "json"
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module DNN
|
6
|
+
|
7
|
+
# This class deals with the model of the network.
|
8
|
+
class Model
|
9
|
+
# @return [Array] All layers possessed by the model.
|
10
|
+
attr_accessor :layers
|
11
|
+
# @return [Bool] Setting false prevents learning of parameters.
|
12
|
+
attr_accessor :trainable
|
13
|
+
|
14
|
+
# Load marshal model.
|
15
|
+
# @param [String] file_name File name of marshal model to load.
|
16
|
+
def self.load(file_name)
|
17
|
+
Marshal.load(Zlib::Inflate.inflate(File.binread(file_name)))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Load json model.
|
21
|
+
# @param [String] json_str json string to load model.
|
22
|
+
# @return [DNN::Model]
|
23
|
+
def self.load_json(json_str)
|
24
|
+
hash = JSON.parse(json_str, symbolize_names: true)
|
25
|
+
model = self.from_hash(hash)
|
26
|
+
model.compile(Utils.from_hash(hash[:optimizer]), Utils.from_hash(hash[:loss]))
|
27
|
+
model
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.from_hash(hash)
|
31
|
+
model = self.new
|
32
|
+
model.layers = hash[:layers].map { |hash_layer| Utils.from_hash(hash_layer) }
|
33
|
+
model
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@layers = []
|
38
|
+
@trainable = true
|
39
|
+
@optimizer = nil
|
40
|
+
@compiled = false
|
41
|
+
end
|
42
|
+
|
43
|
+
# Load json model parameters.
|
44
|
+
# @param [String] json_str json string to load model parameters.
|
45
|
+
def load_json_params(json_str)
|
46
|
+
hash = JSON.parse(json_str, symbolize_names: true)
|
47
|
+
has_param_layers_params = hash[:params]
|
48
|
+
has_param_layers_index = 0
|
49
|
+
has_param_layers = get_all_layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
|
50
|
+
has_param_layers.each do |layer|
|
51
|
+
hash_params = has_param_layers_params[has_param_layers_index]
|
52
|
+
hash_params.each do |key, (shape, base64_param)|
|
53
|
+
bin = Base64.decode64(base64_param)
|
54
|
+
data = Xumo::SFloat.from_binary(bin).reshape(*shape)
|
55
|
+
layer.params[key].data = data
|
56
|
+
end
|
57
|
+
has_param_layers_index += 1
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Save the model in marshal format.
|
62
|
+
# @param [String] file_name name to save model.
|
63
|
+
def save(file_name)
|
64
|
+
bin = Zlib::Deflate.deflate(Marshal.dump(self))
|
65
|
+
begin
|
66
|
+
File.binwrite(file_name, bin)
|
67
|
+
rescue Errno::ENOENT => ex
|
68
|
+
dir_name = file_name.match(%r`(.*)/.+$`)[1]
|
69
|
+
Dir.mkdir(dir_name)
|
70
|
+
File.binwrite(file_name, bin)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Convert model to json string.
|
75
|
+
# @return [String] json string.
|
76
|
+
def to_json
|
77
|
+
hash = self.to_hash
|
78
|
+
hash[:version] = VERSION
|
79
|
+
JSON.pretty_generate(hash)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Convert model parameters to json string.
|
83
|
+
# @return [String] json string.
|
84
|
+
def params_to_json
|
85
|
+
has_param_layers = get_all_layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
|
86
|
+
has_param_layers_params = has_param_layers.map do |layer|
|
87
|
+
layer.params.map { |key, param|
|
88
|
+
base64_data = Base64.encode64(param.data.to_binary)
|
89
|
+
[key, [param.data.shape, base64_data]]
|
90
|
+
}.to_h
|
91
|
+
end
|
92
|
+
hash = {version: VERSION, params: has_param_layers_params}
|
93
|
+
JSON.dump(hash)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Add layer to the model.
|
97
|
+
# @param [DNN::Layers::Layer] layer Layer to add to the model.
|
98
|
+
# @return [DNN::Model] return self.
|
99
|
+
def <<(layer)
|
100
|
+
if !layer.is_a?(Layers::Layer) && !layer.is_a?(Model)
|
101
|
+
raise TypeError.new("layer is not an instance of the DNN::Layers::Layer class or DNN::Model class.")
|
102
|
+
end
|
103
|
+
@layers << layer
|
104
|
+
self
|
105
|
+
end
|
106
|
+
|
107
|
+
# Set optimizer and loss_func to model and build all layers.
|
108
|
+
# @param [DNN::Optimizers::Optimizer] optimizer Optimizer to use for learning.
|
109
|
+
# @param [DNN::Losses::Loss] loss_func Loss function to use for learning.
|
110
|
+
def compile(optimizer, loss_func)
|
111
|
+
raise DNN_Error.new("The model is already compiled.") if compiled?
|
112
|
+
unless optimizer.is_a?(Optimizers::Optimizer)
|
113
|
+
raise TypeError.new("optimizer:#{optimizer.class} is not an instance of DNN::Optimizers::Optimizer class.")
|
114
|
+
end
|
115
|
+
unless loss_func.is_a?(Losses::Loss)
|
116
|
+
raise TypeError.new("loss_func:#{loss_func.class} is not an instance of DNN::Losses::Loss class.")
|
117
|
+
end
|
118
|
+
@compiled = true
|
119
|
+
layers_check
|
120
|
+
@optimizer = optimizer
|
121
|
+
@loss_func = loss_func
|
122
|
+
build
|
123
|
+
layers_shape_check
|
124
|
+
end
|
125
|
+
|
126
|
+
# Set optimizer and loss_func to model and recompile. But does not build layers.
|
127
|
+
# @param [DNN::Optimizers::Optimizer] optimizer Optimizer to use for learning.
|
128
|
+
# @param [DNN::Losses::Loss] loss_func Loss function to use for learning.
|
129
|
+
def recompile(optimizer, loss_func)
|
130
|
+
unless optimizer.is_a?(Optimizers::Optimizer)
|
131
|
+
raise TypeError.new("optimizer:#{optimizer.class} is not an instance of DNN::Optimizers::Optimizer class.")
|
132
|
+
end
|
133
|
+
unless loss_func.is_a?(Losses::Loss)
|
134
|
+
raise TypeError.new("loss_func:#{loss_func.class} is not an instance of DNN::Losses::Loss class.")
|
135
|
+
end
|
136
|
+
@compiled = true
|
137
|
+
layers_check
|
138
|
+
@optimizer = optimizer
|
139
|
+
@loss_func = loss_func
|
140
|
+
layers_shape_check
|
141
|
+
end
|
142
|
+
|
143
|
+
def build(super_model = nil)
|
144
|
+
@super_model = super_model
|
145
|
+
shape = if super_model
|
146
|
+
super_model.get_prev_layer(self).output_shape
|
147
|
+
else
|
148
|
+
@layers.first.build
|
149
|
+
end
|
150
|
+
layers = super_model ? @layers : @layers[1..-1]
|
151
|
+
layers.each do |layer|
|
152
|
+
if layer.is_a?(Model)
|
153
|
+
layer.build(self)
|
154
|
+
layer.recompile(@optimizer, @loss_func)
|
155
|
+
else
|
156
|
+
layer.build(shape)
|
157
|
+
end
|
158
|
+
shape = layer.output_shape
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [Array] Return the input shape of the model.
|
163
|
+
def input_shape
|
164
|
+
@layers.first.input_shape
|
165
|
+
end
|
166
|
+
|
167
|
+
# @return [Array] Return the output shape of the model.
|
168
|
+
def output_shape
|
169
|
+
@layers.last.output_shape
|
170
|
+
end
|
171
|
+
|
172
|
+
# @return [DNN::Optimizers::Optimizer] optimizer Return the optimizer to use for learning.
|
173
|
+
def optimizer
|
174
|
+
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
175
|
+
@optimizer
|
176
|
+
end
|
177
|
+
|
178
|
+
# @return [DNN::Losses::Loss] loss Return the loss to use for learning.
|
179
|
+
def loss_func
|
180
|
+
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
181
|
+
@loss_func
|
182
|
+
end
|
183
|
+
|
184
|
+
# @return [Bool] Returns whether the model is learning.
|
185
|
+
def compiled?
|
186
|
+
@compiled
|
187
|
+
end
|
188
|
+
|
189
|
+
# Start training.
|
190
|
+
# Compile the model before use this method.
|
191
|
+
# @param [Numo::SFloat] x Input training data.
|
192
|
+
# @param [Numo::SFloat] y Output training data.
|
193
|
+
# @param [Integer] epochs Number of training.
|
194
|
+
# @param [Integer] batch_size Batch size used for one training.
|
195
|
+
# @param [Array or NilClass] test If you to test the model for every 1 epoch,
|
196
|
+
# specify [x_test, y_test]. Don't test to the model, specify nil.
|
197
|
+
# @param [Bool] verbose Set true to display the log. If false is set, the log is not displayed.
|
198
|
+
# @param [Lambda] before_epoch_cbk Process performed before one training.
|
199
|
+
# @param [Lambda] after_epoch_cbk Process performed after one training.
|
200
|
+
# @param [Lambda] before_batch_cbk Set the proc to be performed before batch processing.
|
201
|
+
# @param [Lambda] after_batch_cbk Set the proc to be performed after batch processing.
|
202
|
+
def train(x, y, epochs,
|
203
|
+
batch_size: 1,
|
204
|
+
test: nil,
|
205
|
+
verbose: true,
|
206
|
+
before_epoch_cbk: nil,
|
207
|
+
after_epoch_cbk: nil,
|
208
|
+
before_batch_cbk: nil,
|
209
|
+
after_batch_cbk: nil)
|
210
|
+
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
211
|
+
check_xy_type(x, y)
|
212
|
+
dataset = Dataset.new(x, y)
|
213
|
+
num_train_datas = x.shape[0]
|
214
|
+
(1..epochs).each do |epoch|
|
215
|
+
before_epoch_cbk.call(epoch) if before_epoch_cbk
|
216
|
+
puts "【 epoch #{epoch}/#{epochs} 】" if verbose
|
217
|
+
(num_train_datas.to_f / batch_size).ceil.times do |index|
|
218
|
+
x_batch, y_batch = dataset.next_batch(batch_size)
|
219
|
+
loss_value = train_on_batch(x_batch, y_batch,
|
220
|
+
before_batch_cbk: before_batch_cbk, after_batch_cbk: after_batch_cbk)
|
221
|
+
if loss_value.is_a?(Numo::SFloat)
|
222
|
+
loss_value = loss_value.mean
|
223
|
+
elsif loss_value.nan?
|
224
|
+
puts "\nloss is nan" if verbose
|
225
|
+
return
|
226
|
+
end
|
227
|
+
num_trained_datas = (index + 1) * batch_size
|
228
|
+
num_trained_datas = num_trained_datas > num_train_datas ? num_train_datas : num_trained_datas
|
229
|
+
log = "\r"
|
230
|
+
40.times do |i|
|
231
|
+
if i < num_trained_datas * 40 / num_train_datas
|
232
|
+
log << "="
|
233
|
+
elsif i == num_trained_datas * 40 / num_train_datas
|
234
|
+
log << ">"
|
235
|
+
else
|
236
|
+
log << "_"
|
237
|
+
end
|
238
|
+
end
|
239
|
+
log << " #{num_trained_datas}/#{num_train_datas} loss: #{sprintf('%.8f', loss_value)}"
|
240
|
+
print log if verbose
|
241
|
+
end
|
242
|
+
if verbose && test
|
243
|
+
acc, test_loss = accurate(test[0], test[1], batch_size,
|
244
|
+
before_batch_cbk: before_batch_cbk, after_batch_cbk: after_batch_cbk)
|
245
|
+
print " accurate: #{acc}, test loss: #{sprintf('%.8f', test_loss)}"
|
246
|
+
end
|
247
|
+
puts "" if verbose
|
248
|
+
after_epoch_cbk.call(epoch) if after_epoch_cbk
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Training once.
|
253
|
+
# Compile the model before use this method.
|
254
|
+
# @param [Numo::SFloat] x Input training data.
|
255
|
+
# @param [Numo::SFloat] y Output training data.
|
256
|
+
# @param [Lambda] before_batch_cbk Set the proc to be performed before batch processing.
|
257
|
+
# @param [Lambda] after_batch_cbk Set the proc to be performed after batch processing.
|
258
|
+
# @return [Float | Numo::SFloat] Return loss value in the form of Float or Numo::SFloat.
|
259
|
+
def train_on_batch(x, y, before_batch_cbk: nil, after_batch_cbk: nil)
|
260
|
+
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
261
|
+
check_xy_type(x, y)
|
262
|
+
input_data_shape_check(x, y)
|
263
|
+
x, y = before_batch_cbk.call(x, y, true) if before_batch_cbk
|
264
|
+
x = forward(x, true)
|
265
|
+
loss_value = @loss_func.forward(x, y, get_all_layers)
|
266
|
+
dy = @loss_func.backward(y, get_all_layers)
|
267
|
+
backward(dy)
|
268
|
+
update
|
269
|
+
after_batch_cbk.call(loss_value, true) if after_batch_cbk
|
270
|
+
loss_value
|
271
|
+
end
|
272
|
+
|
273
|
+
# Evaluate model and get accurate of test data.
|
274
|
+
# @param [Numo::SFloat] x Input test data.
|
275
|
+
# @param [Numo::SFloat] y Output test data.
|
276
|
+
# @param [Lambda] before_batch_cbk Set the proc to be performed before batch processing.
|
277
|
+
# @param [Lambda] after_batch_cbk Set the proc to be performed after batch processing.
|
278
|
+
# @return [Array] Returns the test data accurate and mean loss in the form [accurate, mean_loss].
|
279
|
+
def accurate(x, y, batch_size = 100, before_batch_cbk: nil, after_batch_cbk: nil)
|
280
|
+
check_xy_type(x, y)
|
281
|
+
input_data_shape_check(x, y)
|
282
|
+
batch_size = batch_size >= x.shape[0] ? x.shape[0] : batch_size
|
283
|
+
dataset = Dataset.new(x, y, false)
|
284
|
+
correct = 0
|
285
|
+
sum_loss = 0
|
286
|
+
(x.shape[0].to_f / batch_size).ceil.times do |i|
|
287
|
+
x_batch, y_batch = dataset.next_batch(batch_size)
|
288
|
+
x_batch, y_batch = before_batch_cbk.call(x_batch, y_batch, false) if before_batch_cbk
|
289
|
+
x_batch = forward(x_batch, false)
|
290
|
+
sigmoid = Sigmoid.new
|
291
|
+
batch_size.times do |j|
|
292
|
+
if @layers.last.output_shape == [1]
|
293
|
+
if @loss_func.is_a?(SigmoidCrossEntropy)
|
294
|
+
correct += 1 if sigmoid.forward(x_batch[j, 0]).round == y_batch[j, 0].round
|
295
|
+
else
|
296
|
+
correct += 1 if x_batch[j, 0].round == y_batch[j, 0].round
|
297
|
+
end
|
298
|
+
else
|
299
|
+
correct += 1 if x_batch[j, true].max_index == y_batch[j, true].max_index
|
300
|
+
end
|
301
|
+
end
|
302
|
+
loss_value = @loss_func.forward(x_batch, y_batch, get_all_layers)
|
303
|
+
after_batch_cbk.call(loss_value, false) if after_batch_cbk
|
304
|
+
sum_loss += loss_value.is_a?(Numo::SFloat) ? loss_value.mean : loss_value
|
305
|
+
end
|
306
|
+
mean_loss = sum_loss / batch_size
|
307
|
+
[correct.to_f / x.shape[0], mean_loss]
|
308
|
+
end
|
309
|
+
|
310
|
+
# Predict data.
|
311
|
+
# @param [Numo::SFloat] x Input data.
|
312
|
+
def predict(x)
|
313
|
+
check_xy_type(x)
|
314
|
+
input_data_shape_check(x)
|
315
|
+
forward(x, false)
|
316
|
+
end
|
317
|
+
|
318
|
+
# Predict one data.
|
319
|
+
# @param [Numo::SFloat] x Input data. However, x is single data.
|
320
|
+
def predict1(x)
|
321
|
+
check_xy_type(x)
|
322
|
+
predict(x.reshape(1, *x.shape))[0, false]
|
323
|
+
end
|
324
|
+
|
325
|
+
# Get loss value.
|
326
|
+
# @param [Numo::SFloat] x Input data.
|
327
|
+
# @param [Numo::SFloat] y Output data.
|
328
|
+
# @return [Float | Numo::SFloat] Return loss value in the form of Float or Numo::SFloat.
|
329
|
+
def loss(x, y)
|
330
|
+
check_xy_type(x, y)
|
331
|
+
input_data_shape_check(x, y)
|
332
|
+
x = forward(x, false)
|
333
|
+
@loss_func.forward(x, y, get_all_layers)
|
334
|
+
end
|
335
|
+
|
336
|
+
# @return [DNN::Model] Copy this model.
|
337
|
+
def copy
|
338
|
+
Marshal.load(Marshal.dump(self))
|
339
|
+
end
|
340
|
+
|
341
|
+
# Get the layer that the model has.
|
342
|
+
def get_layer(*args)
|
343
|
+
if args.length == 1
|
344
|
+
index = args[0]
|
345
|
+
@layers[index]
|
346
|
+
else
|
347
|
+
layer_class, index = args
|
348
|
+
@layers.select { |layer| layer.is_a?(layer_class) }[index]
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Get the all layers.
|
353
|
+
# @return [Array] all layers array.
|
354
|
+
def get_all_layers
|
355
|
+
@layers.map { |layer|
|
356
|
+
layer.is_a?(Model) ? layer.get_all_layers : layer
|
357
|
+
}.flatten
|
358
|
+
end
|
359
|
+
|
360
|
+
def forward(x, learning_phase)
|
361
|
+
@layers.each do |layer|
|
362
|
+
x = if layer.is_a?(Model)
|
363
|
+
layer.forward(x, learning_phase)
|
364
|
+
else
|
365
|
+
layer.learning_phase = learning_phase
|
366
|
+
layer.forward(x)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
x
|
370
|
+
end
|
371
|
+
|
372
|
+
def backward(dy)
|
373
|
+
@layers.reverse.each do |layer|
|
374
|
+
dy = layer.backward(dy)
|
375
|
+
end
|
376
|
+
dy
|
377
|
+
end
|
378
|
+
|
379
|
+
def update
|
380
|
+
return unless @trainable
|
381
|
+
all_trainable_layers = @layers.map { |layer|
|
382
|
+
if layer.is_a?(Model)
|
383
|
+
layer.trainable ? layer.get_all_layers : nil
|
384
|
+
else
|
385
|
+
layer
|
386
|
+
end
|
387
|
+
}.flatten.compact.uniq
|
388
|
+
@optimizer.update(all_trainable_layers)
|
389
|
+
end
|
390
|
+
|
391
|
+
def get_prev_layer(layer)
|
392
|
+
layer_index = @layers.index(layer)
|
393
|
+
prev_layer = if layer_index == 0
|
394
|
+
if @super_model
|
395
|
+
@super_model.layers[@super_model.layers.index(self) - 1]
|
396
|
+
else
|
397
|
+
self
|
398
|
+
end
|
399
|
+
else
|
400
|
+
@layers[layer_index - 1]
|
401
|
+
end
|
402
|
+
if prev_layer.is_a?(Layers::Layer)
|
403
|
+
prev_layer
|
404
|
+
elsif prev_layer.is_a?(Model)
|
405
|
+
prev_layer.layers.last
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def to_hash
|
410
|
+
hash_layers = @layers.map { |layer| layer.to_hash }
|
411
|
+
{class: Model.name, layers: hash_layers, optimizer: @optimizer.to_hash, loss: @loss_func.to_hash}
|
412
|
+
end
|
413
|
+
|
414
|
+
private
|
415
|
+
|
416
|
+
def layers_check
|
417
|
+
if !@layers.first.is_a?(Layers::InputLayer) && !@layers.first.is_a?(Layers::Embedding) && !@super_model
|
418
|
+
raise TypeError.new("The first layer is not an InputLayer or Embedding.")
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def input_data_shape_check(x, y = nil)
|
423
|
+
unless @layers.first.input_shape == x.shape[1..-1]
|
424
|
+
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}.")
|
425
|
+
end
|
426
|
+
if y && @layers.last.output_shape != y.shape[1..-1]
|
427
|
+
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}.")
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def layers_shape_check
|
432
|
+
@layers.each.with_index do |layer, i|
|
433
|
+
prev_shape = layer.input_shape
|
434
|
+
if layer.is_a?(Layers::Dense)
|
435
|
+
if prev_shape.length != 1
|
436
|
+
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.")
|
437
|
+
end
|
438
|
+
elsif layer.is_a?(Layers::Conv2D) || layer.is_a?(Layers::MaxPool2D)
|
439
|
+
if prev_shape.length != 3
|
440
|
+
raise DNN_ShapeError.new("layer index(#{i}) Conv2D: The shape of the previous layer is #{prev_shape}. The shape of the previous layer must be 3 dimensional.")
|
441
|
+
end
|
442
|
+
elsif layer.is_a?(Layers::RNN)
|
443
|
+
if prev_shape.length != 2
|
444
|
+
layer_name = layer.class.name.match("\:\:(.+)$")[1]
|
445
|
+
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.")
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def check_xy_type(x, y = nil)
|
452
|
+
unless x.is_a?(Xumo::SFloat)
|
453
|
+
raise TypeError.new("x:#{x.class.name} is not an instance of #{Xumo::SFloat.name} class.")
|
454
|
+
end
|
455
|
+
if y && !y.is_a?(Xumo::SFloat)
|
456
|
+
raise TypeError.new("y:#{y.class.name} is not an instance of #{Xumo::SFloat.name} class.")
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
end
|