ruby-dnn 0.8.8 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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| Util.load_hash(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
- has_param_layers_params = JSON.parse(json_str, symbolize_names: true)
37
+ hash = JSON.parse(json_str, symbolize_names: true)
38
+ has_param_layers_params = hash[:params]
33
39
  has_param_layers_index = 0
34
- @layers.each do |layer|
35
- next unless layer.is_a?(HasParamLayer)
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
- marshal = Marshal.dump(self)
57
+ bin = Zlib::Deflate.deflate(Marshal.dump(self))
52
58
  begin
53
- File.binwrite(file_name, marshal)
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, marshal)
63
+ File.binwrite(file_name, bin)
58
64
  end
59
65
  end
60
66
 
61
67
  def to_json
62
- hash_layers = @layers.map { |layer| layer.to_hash }
63
- hash = {version: VERSION, layers: hash_layers, optimizer: @optimizer.to_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 = @layers.select { |layer| layer.is_a?(Layers::HasParamLayer) }
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
- JSON.dump(has_param_layers_params)
81
+ hash = {version: VERSION, params: has_param_layers_params}
82
+ JSON.dump(hash)
76
83
  end
77
-
84
+
78
85
  def <<(layer)
79
- if !layer.is_a?(Layers::Layer) && !layer.is_a?(Model)
80
- raise TypeError.new("layer is not an instance of the DNN::Layers::Layer class or DNN::Model class.")
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 the DNN::Optimizers::Optimizer class.")
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
- @layers.each do |layer|
100
- layer.build(self)
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 compiled?
109
- @compiled
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 training?
113
- @training
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
- num_train_data = x.shape[0]
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
- (num_train_data.to_f / batch_size).ceil.times do |index|
129
- x_batch, y_batch = Util.get_minibatch(x, y, batch_size)
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
- num_trained_data = (index + 1) * batch_size
136
- num_trained_data = num_trained_data > num_train_data ? num_train_data : num_trained_data
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 < num_trained_data * 40 / num_train_data
176
+ if i < num_trained_datas * 40 / num_train_datas
140
177
  log << "="
141
- elsif i == num_trained_data * 40 / num_train_data
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 << " #{num_trained_data}/#{num_train_data} loss: #{sprintf('%.8f', loss)}"
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
- dloss
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[-1].shape == [1]
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, training)
226
- @training = training
267
+ def forward(x, learning_phase)
227
268
  @layers.each do |layer|
228
- x = if layer.is_a?(Layers::Layer)
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(y)
246
- dout = y
278
+ def backward(dout, learning_phase)
247
279
  @layers.reverse.each do |layer|
248
- dout = layer.backward(dout)
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
- layer.update if @trainable && (layer.is_a?(Layers::HasParamLayer) || layer.is_a?(Model))
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[-1]
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.shape == x.shape[1..-1]
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.shape}.")
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.shape != y.shape[1..-1]
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.shape}.")
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.is_a?(Layers::Layer) ? layer.prev_layer.shape : layer.layers[-1]
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