ruby-dnn 0.10.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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