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