ruby-dnn 0.8.8 → 0.9.0
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/API-Reference.ja.md +83 -46
- data/examples/cifar10_example.rb +5 -5
- data/examples/mnist_conv2d_example.rb +5 -5
- data/examples/mnist_example.rb +5 -5
- data/examples/mnist_lstm_example.rb +5 -5
- data/examples/xor_example.rb +4 -3
- data/lib/dnn.rb +3 -3
- data/lib/dnn/core/activations.rb +1 -112
- data/lib/dnn/core/cnn_layers.rb +14 -14
- data/lib/dnn/core/dataset.rb +18 -0
- data/lib/dnn/core/initializers.rb +28 -8
- data/lib/dnn/core/layers.rb +62 -90
- data/lib/dnn/core/losses.rb +120 -0
- data/lib/dnn/core/model.rb +124 -66
- data/lib/dnn/core/rnn_layers.rb +17 -13
- data/lib/dnn/core/{util.rb → utils.rb} +10 -6
- data/lib/dnn/version.rb +1 -1
- metadata +5 -3
@@ -0,0 +1,120 @@
|
|
1
|
+
module DNN
|
2
|
+
module Losses
|
3
|
+
|
4
|
+
class Loss
|
5
|
+
def forward(out, y)
|
6
|
+
raise NotImplementedError.new("Class '#{self.class.name}' has implement method 'forward'")
|
7
|
+
end
|
8
|
+
|
9
|
+
def backward(y)
|
10
|
+
raise NotImplementedError.new("Class '#{self.class.name}' has implement method 'backward'")
|
11
|
+
end
|
12
|
+
|
13
|
+
def regularize(layers)
|
14
|
+
layers.select { |layer| layer.is_a?(Connection) }
|
15
|
+
.reduce(0) { |sum, layer| sum + layer.lasso + layer.ridge }
|
16
|
+
end
|
17
|
+
|
18
|
+
def d_regularize(layers)
|
19
|
+
layers.select { |layer| layer.is_a?(Connection) }.each do |layer|
|
20
|
+
layer.d_lasso
|
21
|
+
layer.d_ridge
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
{class: self.class.name}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class MeanSquaredError < Loss
|
31
|
+
def forward(out, y)
|
32
|
+
@out = out
|
33
|
+
batch_size = y.shape[0]
|
34
|
+
0.5 * ((out - y)**2).sum / batch_size
|
35
|
+
end
|
36
|
+
|
37
|
+
def backward(y)
|
38
|
+
@out - y
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
class MeanAbsoluteError < Loss
|
44
|
+
def forward(out, y)
|
45
|
+
@out = out
|
46
|
+
batch_size = y.shape[0]
|
47
|
+
(out - y).abs.sum / batch_size
|
48
|
+
end
|
49
|
+
|
50
|
+
def backward(y)
|
51
|
+
dout = @out - y
|
52
|
+
dout[dout >= 0] = 1
|
53
|
+
dout[dout < 0] = -1
|
54
|
+
dout
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class HuberLoss < Loss
|
60
|
+
def forward(out, y)
|
61
|
+
@out = out
|
62
|
+
loss = loss_l1(y)
|
63
|
+
loss = loss > 1 ? loss : loss_l2(y)
|
64
|
+
@loss = loss + regularize
|
65
|
+
end
|
66
|
+
|
67
|
+
def backward(y)
|
68
|
+
dout = @out - y
|
69
|
+
if @loss > 1
|
70
|
+
dout[dout >= 0] = 1
|
71
|
+
dout[dout < 0] = -1
|
72
|
+
end
|
73
|
+
dout
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def loss_l1(y)
|
79
|
+
batch_size = y.shape[0]
|
80
|
+
(@out - y).abs.sum / batch_size
|
81
|
+
end
|
82
|
+
|
83
|
+
def loss_l2(y)
|
84
|
+
batch_size = y.shape[0]
|
85
|
+
0.5 * ((@out - y)**2).sum / batch_size
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
class SoftmaxCrossEntropy < Loss
|
91
|
+
NMath = Xumo::NMath
|
92
|
+
|
93
|
+
def forward(x, y)
|
94
|
+
@out = Utils.softmax(x)
|
95
|
+
batch_size = y.shape[0]
|
96
|
+
-(y * NMath.log(@out + 1e-7)).sum / batch_size
|
97
|
+
end
|
98
|
+
|
99
|
+
def backward(y)
|
100
|
+
@out - y
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
class SigmoidCrossEntropy < Loss
|
106
|
+
NMath = Xumo::NMath
|
107
|
+
|
108
|
+
def forward(x, y)
|
109
|
+
@out = Utils.sigmoid(x)
|
110
|
+
batch_size = y.shape[0]
|
111
|
+
-(y * NMath.log(@out + 1e-7) + (1 - y) * NMath.log(1 - @out + 1e-7)).sum / batch_size
|
112
|
+
end
|
113
|
+
|
114
|
+
def backward(y)
|
115
|
+
@out - y
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
data/lib/dnn/core/model.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "zlib"
|
1
2
|
require "json"
|
2
3
|
require "base64"
|
3
4
|
|
@@ -9,14 +10,19 @@ module DNN
|
|
9
10
|
attr_accessor :trainable # Setting false prevents learning of parameters.
|
10
11
|
|
11
12
|
def self.load(file_name)
|
12
|
-
Marshal.load(File.binread(file_name))
|
13
|
+
Marshal.load(Zlib::Inflate.inflate(File.binread(file_name)))
|
13
14
|
end
|
14
15
|
|
15
16
|
def self.load_json(json_str)
|
16
17
|
hash = JSON.parse(json_str, symbolize_names: true)
|
18
|
+
model = self.load_hash(hash)
|
19
|
+
model.compile(Utils.load_hash(hash[:optimizer]), Utils.load_hash(hash[:loss]))
|
20
|
+
model
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.load_hash(hash)
|
17
24
|
model = self.new
|
18
|
-
model.layers = hash[:layers].map { |hash_layer|
|
19
|
-
model.compile(Util.load_hash(hash[:optimizer]))
|
25
|
+
model.layers = hash[:layers].map { |hash_layer| Utils.load_hash(hash_layer) }
|
20
26
|
model
|
21
27
|
end
|
22
28
|
|
@@ -24,15 +30,15 @@ module DNN
|
|
24
30
|
@layers = []
|
25
31
|
@trainable = true
|
26
32
|
@optimizer = nil
|
27
|
-
@training = false
|
28
33
|
@compiled = false
|
29
34
|
end
|
30
35
|
|
31
36
|
def load_json_params(json_str)
|
32
|
-
|
37
|
+
hash = JSON.parse(json_str, symbolize_names: true)
|
38
|
+
has_param_layers_params = hash[:params]
|
33
39
|
has_param_layers_index = 0
|
34
|
-
|
35
|
-
|
40
|
+
has_param_layers = get_all_layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
|
41
|
+
has_param_layers.each do |layer|
|
36
42
|
hash_params = has_param_layers_params[has_param_layers_index]
|
37
43
|
hash_params.each do |key, (shape, base64_param)|
|
38
44
|
bin = Base64.decode64(base64_param)
|
@@ -46,71 +52,100 @@ module DNN
|
|
46
52
|
has_param_layers_index += 1
|
47
53
|
end
|
48
54
|
end
|
49
|
-
|
55
|
+
|
50
56
|
def save(file_name)
|
51
|
-
|
57
|
+
bin = Zlib::Deflate.deflate(Marshal.dump(self))
|
52
58
|
begin
|
53
|
-
File.binwrite(file_name,
|
59
|
+
File.binwrite(file_name, bin)
|
54
60
|
rescue Errno::ENOENT => ex
|
55
61
|
dir_name = file_name.match(%r`(.*)/.+$`)[1]
|
56
62
|
Dir.mkdir(dir_name)
|
57
|
-
File.binwrite(file_name,
|
63
|
+
File.binwrite(file_name, bin)
|
58
64
|
end
|
59
65
|
end
|
60
66
|
|
61
67
|
def to_json
|
62
|
-
|
63
|
-
hash =
|
68
|
+
hash = self.to_hash
|
69
|
+
hash[:version] = VERSION
|
64
70
|
JSON.pretty_generate(hash)
|
65
71
|
end
|
66
72
|
|
67
73
|
def params_to_json
|
68
|
-
has_param_layers =
|
74
|
+
has_param_layers = get_all_layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
|
69
75
|
has_param_layers_params = has_param_layers.map do |layer|
|
70
76
|
layer.params.map { |key, param|
|
71
77
|
base64_data = Base64.encode64(param.data.to_binary)
|
72
78
|
[key, [param.data.shape, base64_data]]
|
73
79
|
}.to_h
|
74
80
|
end
|
75
|
-
|
81
|
+
hash = {version: VERSION, params: has_param_layers_params}
|
82
|
+
JSON.dump(hash)
|
76
83
|
end
|
77
|
-
|
84
|
+
|
78
85
|
def <<(layer)
|
79
|
-
|
80
|
-
|
86
|
+
# Due to a bug in saving nested models, temporarily prohibit model nesting.
|
87
|
+
# if !layer.is_a?(Layers::Layer) && !layer.is_a?(Model)
|
88
|
+
# raise TypeError.new("layer is not an instance of the DNN::Layers::Layer class or DNN::Model class.")
|
89
|
+
# end
|
90
|
+
unless layer.is_a?(Layers::Layer)
|
91
|
+
raise TypeError.new("layer:#{layer.class.name} is not an instance of the DNN::Layers::Layer class.")
|
81
92
|
end
|
82
93
|
@layers << layer
|
83
94
|
self
|
84
95
|
end
|
85
|
-
|
86
|
-
def compile(optimizer)
|
96
|
+
|
97
|
+
def compile(optimizer, loss)
|
87
98
|
unless optimizer.is_a?(Optimizers::Optimizer)
|
88
|
-
raise TypeError.new("optimizer is not an instance of
|
99
|
+
raise TypeError.new("optimizer:#{optimizer.class} is not an instance of DNN::Optimizers::Optimizer class.")
|
100
|
+
end
|
101
|
+
unless loss.is_a?(Losses::Loss)
|
102
|
+
raise TypeError.new("loss:#{loss.class} is not an instance of DNN::Losses::Loss class.")
|
89
103
|
end
|
90
104
|
@compiled = true
|
91
105
|
layers_check
|
92
106
|
@optimizer = optimizer
|
107
|
+
@loss = loss
|
93
108
|
build
|
94
109
|
layers_shape_check
|
95
110
|
end
|
96
111
|
|
97
112
|
def build(super_model = nil)
|
98
113
|
@super_model = super_model
|
99
|
-
|
100
|
-
|
114
|
+
shape = if super_model
|
115
|
+
super_model.output_shape
|
116
|
+
else
|
117
|
+
@layers.first.build
|
118
|
+
end
|
119
|
+
@layers[1..-1].each do |layer|
|
120
|
+
if layer.is_a?(Model)
|
121
|
+
layer.build(self)
|
122
|
+
else
|
123
|
+
layer.build(shape)
|
124
|
+
end
|
125
|
+
shape = layer.output_shape
|
101
126
|
end
|
102
127
|
end
|
103
128
|
|
129
|
+
def input_shape
|
130
|
+
@layers.first.input_shape
|
131
|
+
end
|
132
|
+
|
133
|
+
def output_shape
|
134
|
+
@layers.last.output_shape
|
135
|
+
end
|
136
|
+
|
104
137
|
def optimizer
|
138
|
+
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
105
139
|
@optimizer ? @optimizer : @super_model.optimizer
|
106
140
|
end
|
107
141
|
|
108
|
-
def
|
109
|
-
|
142
|
+
def loss
|
143
|
+
raise DNN_Error.new("The model is not compiled.") unless compiled?
|
144
|
+
@loss ? @loss : @super_model.loss
|
110
145
|
end
|
111
146
|
|
112
|
-
def
|
113
|
-
@
|
147
|
+
def compiled?
|
148
|
+
@compiled
|
114
149
|
end
|
115
150
|
|
116
151
|
def train(x, y, epochs,
|
@@ -122,29 +157,31 @@ module DNN
|
|
122
157
|
unless compiled?
|
123
158
|
raise DNN_Error.new("The model is not compiled.")
|
124
159
|
end
|
125
|
-
|
160
|
+
check_xy_type(x, y)
|
161
|
+
dataset = Dataset.new(x, y)
|
162
|
+
num_train_datas = x.shape[0]
|
126
163
|
(1..epochs).each do |epoch|
|
127
164
|
puts "【 epoch #{epoch}/#{epochs} 】" if verbose
|
128
|
-
(
|
129
|
-
x_batch, y_batch =
|
165
|
+
(num_train_datas.to_f / batch_size).ceil.times do |index|
|
166
|
+
x_batch, y_batch = dataset.get_batch(batch_size)
|
130
167
|
loss = train_on_batch(x_batch, y_batch, &batch_proc)
|
131
168
|
if loss.nan?
|
132
169
|
puts "\nloss is nan" if verbose
|
133
170
|
return
|
134
171
|
end
|
135
|
-
|
136
|
-
|
172
|
+
num_trained_datas = (index + 1) * batch_size
|
173
|
+
num_trained_datas = num_trained_datas > num_train_datas ? num_train_datas : num_trained_datas
|
137
174
|
log = "\r"
|
138
175
|
40.times do |i|
|
139
|
-
if i <
|
176
|
+
if i < num_trained_datas * 40 / num_train_datas
|
140
177
|
log << "="
|
141
|
-
elsif i ==
|
178
|
+
elsif i == num_trained_datas * 40 / num_train_datas
|
142
179
|
log << ">"
|
143
180
|
else
|
144
181
|
log << "_"
|
145
182
|
end
|
146
183
|
end
|
147
|
-
log << " #{
|
184
|
+
log << " #{num_trained_datas}/#{num_train_datas} loss: #{sprintf('%.8f', loss)}"
|
148
185
|
print log if verbose
|
149
186
|
end
|
150
187
|
if verbose && test
|
@@ -157,17 +194,20 @@ module DNN
|
|
157
194
|
end
|
158
195
|
|
159
196
|
def train_on_batch(x, y, &batch_proc)
|
197
|
+
check_xy_type(x, y)
|
160
198
|
input_data_shape_check(x, y)
|
161
199
|
x, y = batch_proc.call(x, y) if batch_proc
|
162
|
-
forward(x, true)
|
163
|
-
loss_value = loss(y)
|
164
|
-
backward(y)
|
165
|
-
|
200
|
+
out = forward(x, true)
|
201
|
+
loss_value = @loss.forward(out, y) + @loss.regularize(get_all_layers)
|
202
|
+
dout = @loss.backward(y)
|
203
|
+
backward(dout, true)
|
204
|
+
@loss.d_regularize(get_all_layers)
|
166
205
|
update
|
167
206
|
loss_value
|
168
207
|
end
|
169
208
|
|
170
209
|
def accurate(x, y, batch_size = 100, &batch_proc)
|
210
|
+
check_xy_type(x, y)
|
171
211
|
input_data_shape_check(x, y)
|
172
212
|
batch_size = batch_size >= x.shape[0] ? x.shape[0] : batch_size
|
173
213
|
correct = 0
|
@@ -183,7 +223,7 @@ module DNN
|
|
183
223
|
x_batch, y_batch = batch_proc.call(x_batch, y_batch) if batch_proc
|
184
224
|
out = forward(x_batch, false)
|
185
225
|
batch_size.times do |j|
|
186
|
-
if @layers
|
226
|
+
if @layers.last.output_shape == [1]
|
187
227
|
correct += 1 if out[j, 0].round == y_batch[j, 0].round
|
188
228
|
else
|
189
229
|
correct += 1 if out[j, true].max_index == y_batch[j, true].max_index
|
@@ -194,11 +234,13 @@ module DNN
|
|
194
234
|
end
|
195
235
|
|
196
236
|
def predict(x)
|
237
|
+
check_xy_type(x)
|
197
238
|
input_data_shape_check(x)
|
198
239
|
forward(x, false)
|
199
240
|
end
|
200
241
|
|
201
242
|
def predict1(x)
|
243
|
+
check_xy_type(x)
|
202
244
|
predict(Xumo::SFloat.cast([x]))[0, false]
|
203
245
|
end
|
204
246
|
|
@@ -222,37 +264,36 @@ module DNN
|
|
222
264
|
}.flatten
|
223
265
|
end
|
224
266
|
|
225
|
-
def forward(x,
|
226
|
-
@training = training
|
267
|
+
def forward(x, learning_phase)
|
227
268
|
@layers.each do |layer|
|
228
|
-
x = if layer.is_a?(Layers::
|
269
|
+
x = if layer.is_a?(Layers::Dropout) || layer.is_a?(Layers::BatchNormalization) || layer.is_a?(Model)
|
270
|
+
layer.forward(x, learning_phase)
|
271
|
+
else
|
229
272
|
layer.forward(x)
|
230
|
-
elsif layer.is_a?(Model)
|
231
|
-
layer.forward(x, training)
|
232
273
|
end
|
233
274
|
end
|
234
275
|
x
|
235
276
|
end
|
236
|
-
|
237
|
-
def loss(y)
|
238
|
-
@layers[-1].loss(y)
|
239
|
-
end
|
240
|
-
|
241
|
-
def dloss
|
242
|
-
@layers[-1].dloss
|
243
|
-
end
|
244
277
|
|
245
|
-
def backward(
|
246
|
-
dout = y
|
278
|
+
def backward(dout, learning_phase)
|
247
279
|
@layers.reverse.each do |layer|
|
248
|
-
|
280
|
+
if layer.is_a?(Layers::Dropout) || layer.is_a?(Layers::BatchNormalization) || layer.is_a?(Model)
|
281
|
+
dout = layer.backward(dout, learning_phase)
|
282
|
+
else
|
283
|
+
dout = layer.backward(dout)
|
284
|
+
end
|
249
285
|
end
|
250
286
|
dout
|
251
287
|
end
|
252
288
|
|
253
289
|
def update
|
290
|
+
return unless @trainable
|
254
291
|
@layers.each do |layer|
|
255
|
-
|
292
|
+
if layer.is_a?(Layers::HasParamLayer)
|
293
|
+
layer.update(@optimizer)
|
294
|
+
elsif layer.is_a?(Model)
|
295
|
+
layer.update
|
296
|
+
end
|
256
297
|
end
|
257
298
|
end
|
258
299
|
|
@@ -270,33 +311,35 @@ module DNN
|
|
270
311
|
if prev_layer.is_a?(Layers::Layer)
|
271
312
|
prev_layer
|
272
313
|
elsif prev_layer.is_a?(Model)
|
273
|
-
prev_layer.layers
|
314
|
+
prev_layer.layers.last
|
274
315
|
end
|
275
316
|
end
|
276
317
|
|
318
|
+
def to_hash
|
319
|
+
hash_layers = @layers.map { |layer| layer.to_hash }
|
320
|
+
{class: Model.name, layers: hash_layers, optimizer: @optimizer.to_hash, loss: @loss.to_hash}
|
321
|
+
end
|
322
|
+
|
277
323
|
private
|
278
324
|
|
279
325
|
def layers_check
|
280
326
|
unless @layers.first.is_a?(Layers::InputLayer)
|
281
327
|
raise TypeError.new("The first layer is not an InputLayer.")
|
282
328
|
end
|
283
|
-
unless @layers.last.is_a?(Layers::OutputLayer)
|
284
|
-
raise TypeError.new("The last layer is not an OutputLayer.")
|
285
|
-
end
|
286
329
|
end
|
287
330
|
|
288
331
|
def input_data_shape_check(x, y = nil)
|
289
|
-
unless @layers.first.
|
290
|
-
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.
|
332
|
+
unless @layers.first.input_shape == x.shape[1..-1]
|
333
|
+
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}.")
|
291
334
|
end
|
292
|
-
if y && @layers.last.
|
293
|
-
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.
|
335
|
+
if y && @layers.last.output_shape != y.shape[1..-1]
|
336
|
+
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}.")
|
294
337
|
end
|
295
338
|
end
|
296
339
|
|
297
340
|
def layers_shape_check
|
298
341
|
@layers.each.with_index do |layer, i|
|
299
|
-
prev_shape = layer.
|
342
|
+
prev_shape = layer.input_shape
|
300
343
|
if layer.is_a?(Layers::Dense)
|
301
344
|
if prev_shape.length != 1
|
302
345
|
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.")
|
@@ -313,6 +356,21 @@ module DNN
|
|
313
356
|
end
|
314
357
|
end
|
315
358
|
end
|
359
|
+
|
360
|
+
def check_xy_type(x, y = nil)
|
361
|
+
unless x.is_a?(Xumo::SFloat)
|
362
|
+
raise TypeError.new("x:#{x.class.name} is not an instance of #{Xumo::SFloat.name} class.")
|
363
|
+
end
|
364
|
+
if y && !y.is_a?(Xumo::SFloat)
|
365
|
+
raise TypeError.new("y:#{y.class.name} is not an instance of #{Xumo::SFloat.name} class.")
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def type_check(var_name, var, type)
|
370
|
+
unless var.is_a?(type)
|
371
|
+
raise TypeError.new("#{var_name}:#{var.class} is not an instance of #{type} class.")
|
372
|
+
end
|
373
|
+
end
|
316
374
|
end
|
317
375
|
|
318
376
|
end
|