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.
@@ -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