ruby-dnn 1.1.4 → 1.2.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +2 -1
- data/README.md +39 -22
- data/examples/api-examples/early_stopping_example.rb +6 -6
- data/examples/api-examples/initializer_example.rb +6 -6
- data/examples/api-examples/regularizer_example.rb +6 -6
- data/examples/api-examples/save_example.rb +6 -6
- data/examples/dcgan/dcgan.rb +27 -27
- data/examples/judge-number/README.md +29 -0
- data/examples/judge-number/capture.PNG +0 -0
- data/examples/judge-number/convnet8.rb +70 -0
- data/examples/judge-number/make_weights.rb +5 -0
- data/examples/judge-number/mnist_predict.rb +20 -0
- data/examples/judge-number/mnist_train.rb +19 -0
- data/examples/judge-number/public/httpRequest.js +44 -0
- data/examples/judge-number/public/judgeNumber.js +61 -0
- data/examples/judge-number/server.rb +19 -0
- data/examples/judge-number/trained_mnist_params.marshal +0 -0
- data/examples/judge-number/views/index.erb +7 -0
- data/examples/mnist_conv2d_example.rb +3 -3
- data/examples/mnist_define_by_run.rb +7 -7
- data/examples/mnist_gpu.rb +47 -0
- data/examples/mnist_lstm_example.rb +1 -1
- data/examples/pix2pix/dcgan.rb +54 -66
- data/examples/pix2pix/train.rb +2 -2
- data/examples/vae.rb +13 -13
- data/img/cart-pole.gif +0 -0
- data/img/cycle-gan.PNG +0 -0
- data/img/facade-pix2pix.png +0 -0
- data/lib/dnn.rb +24 -3
- data/lib/dnn/core/callbacks.rb +6 -4
- data/lib/dnn/core/layers/basic_layers.rb +40 -22
- data/lib/dnn/core/layers/cnn_layers.rb +33 -5
- data/lib/dnn/core/layers/math_layers.rb +17 -9
- data/lib/dnn/core/layers/merge_layers.rb +2 -26
- data/lib/dnn/core/layers/split_layers.rb +39 -0
- data/lib/dnn/core/link.rb +14 -33
- data/lib/dnn/core/losses.rb +6 -12
- data/lib/dnn/core/models.rb +77 -10
- data/lib/dnn/core/optimizers.rb +8 -1
- data/lib/dnn/core/utils.rb +23 -0
- data/lib/dnn/image.rb +48 -0
- data/lib/dnn/version.rb +1 -1
- data/ruby-dnn.gemspec +2 -15
- metadata +40 -20
- data/bin/console +0 -14
- data/bin/setup +0 -8
data/examples/pix2pix/train.rb
CHANGED
|
@@ -23,8 +23,8 @@ epochs = 20
|
|
|
23
23
|
batch_size = 128
|
|
24
24
|
|
|
25
25
|
if initial_epoch == 1
|
|
26
|
-
gen = Generator.new([32, 32, 1])
|
|
27
|
-
dis = Discriminator.new([32, 32, 1], [32, 32, 3])
|
|
26
|
+
gen = Generator.new([32, 32, 1], 32)
|
|
27
|
+
dis = Discriminator.new([32, 32, 1], [32, 32, 3], 32)
|
|
28
28
|
dcgan = DCGAN.new(gen, dis)
|
|
29
29
|
gen.setup(Adam.new(alpha: 0.0002, beta1: 0.5), MeanAbsoluteError.new)
|
|
30
30
|
dis.setup(Adam.new(alpha: 0.00001, beta1: 0.1), SigmoidCrossEntropy.new)
|
data/examples/vae.rb
CHANGED
|
@@ -28,24 +28,24 @@ end
|
|
|
28
28
|
class Encoder < Model
|
|
29
29
|
def initialize
|
|
30
30
|
super
|
|
31
|
-
@
|
|
32
|
-
@
|
|
33
|
-
@
|
|
34
|
-
@
|
|
31
|
+
@d1 = Dense.new(196)
|
|
32
|
+
@d2 = Dense.new(49)
|
|
33
|
+
@d3_1 = Dense.new($z_dim)
|
|
34
|
+
@d3_2 = Dense.new($z_dim)
|
|
35
35
|
@bn1 = BatchNormalization.new
|
|
36
36
|
@bn2 = BatchNormalization.new
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def forward(x)
|
|
40
40
|
x = InputLayer.new(784).(x)
|
|
41
|
-
x = @
|
|
41
|
+
x = @d1.(x)
|
|
42
42
|
x = @bn1.(x)
|
|
43
43
|
x = ReLU.(x)
|
|
44
|
-
x = @
|
|
44
|
+
x = @d2.(x)
|
|
45
45
|
x = @bn2.(x)
|
|
46
46
|
x = ReLU.(x)
|
|
47
|
-
z_mean = @
|
|
48
|
-
z_sigma = @
|
|
47
|
+
z_mean = @d3_1.(x)
|
|
48
|
+
z_sigma = @d3_2.(x)
|
|
49
49
|
[z_mean, z_sigma]
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -53,16 +53,16 @@ end
|
|
|
53
53
|
class Decoder < Model
|
|
54
54
|
def initialize
|
|
55
55
|
super
|
|
56
|
-
@
|
|
57
|
-
@
|
|
56
|
+
@d1 = Dense.new(196)
|
|
57
|
+
@d2 = Dense.new(784)
|
|
58
58
|
@bn1 = BatchNormalization.new
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def forward(z)
|
|
62
|
-
x = @
|
|
62
|
+
x = @d1.(z)
|
|
63
63
|
x = @bn1.(x)
|
|
64
64
|
x = ReLU.(x)
|
|
65
|
-
x = @
|
|
65
|
+
x = @d2.(x)
|
|
66
66
|
x
|
|
67
67
|
end
|
|
68
68
|
end
|
|
@@ -97,7 +97,7 @@ model = VAE.new
|
|
|
97
97
|
dec = model.dec
|
|
98
98
|
model.setup(Adam.new, VAELoss.new)
|
|
99
99
|
|
|
100
|
-
model.train(x_train, x_train, 10, batch_size:
|
|
100
|
+
model.train(x_train, x_train, 10, batch_size: 128)
|
|
101
101
|
|
|
102
102
|
images = []
|
|
103
103
|
10.times do |i|
|
data/img/cart-pole.gif
ADDED
|
Binary file
|
data/img/cycle-gan.PNG
ADDED
|
Binary file
|
|
Binary file
|
data/lib/dnn.rb
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
require "numo/narray"
|
|
2
|
+
|
|
1
3
|
module DNN
|
|
2
|
-
if
|
|
4
|
+
if ENV["RUBY_DNN_USE_CUMO"] == "ENABLE"
|
|
5
|
+
require "cumo/narray"
|
|
3
6
|
Xumo = ::Cumo
|
|
4
7
|
else
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
if defined? ::Cumo
|
|
9
|
+
Xumo = ::Cumo
|
|
10
|
+
else
|
|
11
|
+
Xumo = ::Numo
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.use_cumo?
|
|
16
|
+
defined? ::Cumo
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.cudnn_available?
|
|
20
|
+
return false unless defined? ::Cumo
|
|
21
|
+
Cumo::CUDA::CUDNN.available?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.use_cudnn?
|
|
25
|
+
return false unless ENV["RUBY_DNN_USE_CUDNN"] == "ENABLE"
|
|
26
|
+
cudnn_available?
|
|
7
27
|
end
|
|
8
28
|
end
|
|
9
29
|
|
|
@@ -20,6 +40,7 @@ require_relative "dnn/core/layers/basic_layers"
|
|
|
20
40
|
require_relative "dnn/core/layers/normalizations"
|
|
21
41
|
require_relative "dnn/core/layers/activations"
|
|
22
42
|
require_relative "dnn/core/layers/merge_layers"
|
|
43
|
+
require_relative "dnn/core/layers/split_layers"
|
|
23
44
|
require_relative "dnn/core/layers/cnn_layers"
|
|
24
45
|
require_relative "dnn/core/layers/embedding"
|
|
25
46
|
require_relative "dnn/core/layers/rnn_layers"
|
data/lib/dnn/core/callbacks.rb
CHANGED
|
@@ -104,6 +104,7 @@ module DNN
|
|
|
104
104
|
# A callback that save the log.
|
|
105
105
|
# The following logs will be recorded.
|
|
106
106
|
# epoch: Current epoch.
|
|
107
|
+
# step: Current step in epoch.
|
|
107
108
|
# train_loss: Batch training loss.
|
|
108
109
|
# test_loss: Mean test loss.
|
|
109
110
|
# test_accuracy: Test accuracy.
|
|
@@ -111,6 +112,7 @@ module DNN
|
|
|
111
112
|
def initialize
|
|
112
113
|
@log = {
|
|
113
114
|
epoch: [],
|
|
115
|
+
step: [],
|
|
114
116
|
train_loss: [],
|
|
115
117
|
test_loss: [],
|
|
116
118
|
test_accuracy: [],
|
|
@@ -122,7 +124,7 @@ module DNN
|
|
|
122
124
|
end
|
|
123
125
|
|
|
124
126
|
def after_train_on_batch
|
|
125
|
-
logging(:train_loss)
|
|
127
|
+
logging(:train_loss, :step)
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
# Get a log.
|
|
@@ -130,10 +132,10 @@ module DNN
|
|
|
130
132
|
# @return [Numo::NArray] Return the recorded log.
|
|
131
133
|
def get_log(tag)
|
|
132
134
|
case tag
|
|
133
|
-
when :epoch
|
|
134
|
-
|
|
135
|
+
when :epoch, :step
|
|
136
|
+
Xumo::UInt32.cast(@log[tag])
|
|
135
137
|
else
|
|
136
|
-
|
|
138
|
+
Xumo::SFloat.cast(@log[tag])
|
|
137
139
|
end
|
|
138
140
|
end
|
|
139
141
|
|
|
@@ -2,20 +2,21 @@ module DNN
|
|
|
2
2
|
module Layers
|
|
3
3
|
|
|
4
4
|
module LayerNode
|
|
5
|
-
def forward(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
def forward(*inputs)
|
|
6
|
+
xs = inputs.map(&:data)
|
|
7
|
+
prevs = inputs.map { |input| input.is_a?(Tensor) ? input.link : input }
|
|
8
|
+
ys = forward_node(*xs)
|
|
9
|
+
num_outputs = (ys.is_a?(Array) ? ys.length : 1)
|
|
10
|
+
link = Link.new(prevs: prevs, layer_node: self, num_outputs: num_outputs)
|
|
11
|
+
prevs.map { |prev| prev.next = link if prev.is_a?(Link) }
|
|
12
|
+
Tensor.convert(ys, link)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
def forward_node(
|
|
15
|
+
def forward_node(*xs)
|
|
15
16
|
raise NotImplementedError, "Class '#{self.class.name}' has implement method 'forward_node'"
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def backward_node(
|
|
19
|
+
def backward_node(*dys)
|
|
19
20
|
raise NotImplementedError, "Class '#{self.class.name}' has implement method 'backward_node'"
|
|
20
21
|
end
|
|
21
22
|
end
|
|
@@ -292,14 +293,8 @@ module DNN
|
|
|
292
293
|
end
|
|
293
294
|
|
|
294
295
|
class Flatten < Layer
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def forward_node(x)
|
|
298
|
-
x.reshape(x.shape[0], *@output_shape)
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
def backward_node(dy)
|
|
302
|
-
dy.reshape(dy.shape[0], *@input_shape)
|
|
296
|
+
def forward(x)
|
|
297
|
+
Reshape.(x, @output_shape)
|
|
303
298
|
end
|
|
304
299
|
|
|
305
300
|
def compute_output_shape
|
|
@@ -320,13 +315,37 @@ module DNN
|
|
|
320
315
|
end
|
|
321
316
|
|
|
322
317
|
def forward_node(x)
|
|
323
|
-
|
|
318
|
+
if DNN.use_cumo?
|
|
319
|
+
_forward_gpu(x)
|
|
320
|
+
else
|
|
321
|
+
_forward_cpu(x)
|
|
322
|
+
end
|
|
324
323
|
end
|
|
325
324
|
|
|
326
325
|
def backward_node(dy)
|
|
326
|
+
if DNN.use_cumo?
|
|
327
|
+
_backward_gpu(dy)
|
|
328
|
+
else
|
|
329
|
+
_backward_cpu(dy)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def _forward_cpu(x)
|
|
334
|
+
x.reshape(x.shape[0], *@output_shape)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def _backward_cpu(dy)
|
|
327
338
|
dy.reshape(dy.shape[0], *@input_shape)
|
|
328
339
|
end
|
|
329
340
|
|
|
341
|
+
def _forward_gpu(x)
|
|
342
|
+
x.flatten.reshape(x.shape[0], *@output_shape)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def _backward_gpu(dy)
|
|
346
|
+
dy.flatten.reshape(dy.shape[0], *@input_shape)
|
|
347
|
+
end
|
|
348
|
+
|
|
330
349
|
def to_hash
|
|
331
350
|
super(shape: @shape)
|
|
332
351
|
end
|
|
@@ -417,8 +436,8 @@ module DNN
|
|
|
417
436
|
def forward_node(x)
|
|
418
437
|
if DNN.learning_phase
|
|
419
438
|
Xumo::SFloat.srand(@rnd.rand(1 << 31))
|
|
420
|
-
@mask = Xumo::SFloat.new(*x.shape).rand
|
|
421
|
-
x
|
|
439
|
+
@mask = Xumo::SFloat.cast(Xumo::SFloat.new(*x.shape).rand >= @dropout_ratio)
|
|
440
|
+
x = x * @mask
|
|
422
441
|
elsif @use_scale
|
|
423
442
|
x *= (1 - @dropout_ratio)
|
|
424
443
|
end
|
|
@@ -426,8 +445,7 @@ module DNN
|
|
|
426
445
|
end
|
|
427
446
|
|
|
428
447
|
def backward_node(dy)
|
|
429
|
-
dy
|
|
430
|
-
dy
|
|
448
|
+
dy * @mask
|
|
431
449
|
end
|
|
432
450
|
|
|
433
451
|
def to_hash
|
|
@@ -6,10 +6,27 @@ module DNN
|
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
8
|
# img[bsize, out_h, out_w, ch] to col[bsize * out_h * out_w, fil_h * fil_w * ch]
|
|
9
|
-
def im2col(
|
|
9
|
+
def im2col(*args)
|
|
10
|
+
if DNN.use_cumo?
|
|
11
|
+
im2col_gpu(*args)
|
|
12
|
+
else
|
|
13
|
+
im2col_cpu(*args)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# col[bsize * out_h * out_w, fil_h * fil_w * ch] to img[bsize, out_h, out_w, ch]
|
|
18
|
+
def col2im(*args)
|
|
19
|
+
if DNN.use_cumo?
|
|
20
|
+
col2im_gpu(*args)
|
|
21
|
+
else
|
|
22
|
+
col2im_cpu(*args)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def im2col_cpu(img, out_h, out_w, fil_h, fil_w, strides)
|
|
10
27
|
bsize = img.shape[0]
|
|
11
28
|
ch = img.shape[3]
|
|
12
|
-
col =
|
|
29
|
+
col = img.class.zeros(bsize, out_h, out_w, fil_h, fil_w, ch)
|
|
13
30
|
(0...fil_h).each do |i|
|
|
14
31
|
i_range = (i...(i + strides[0] * out_h)).step(strides[0]).to_a
|
|
15
32
|
(0...fil_w).each do |j|
|
|
@@ -20,11 +37,16 @@ module DNN
|
|
|
20
37
|
col.reshape(bsize * out_h * out_w, fil_h * fil_w * ch)
|
|
21
38
|
end
|
|
22
39
|
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
def im2col_gpu(img, out_h, out_w, fil_h, fil_w, strides)
|
|
41
|
+
img = Utils.cumo2numo(img)
|
|
42
|
+
col = im2col_cpu(img, out_h, out_w, fil_h, fil_w, strides)
|
|
43
|
+
Utils.numo2cumo(col)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def col2im_cpu(col, img_shape, out_h, out_w, fil_h, fil_w, strides)
|
|
25
47
|
bsize, img_h, img_w, ch = img_shape
|
|
26
48
|
col = col.reshape(bsize, out_h, out_w, fil_h, fil_w, ch)
|
|
27
|
-
img =
|
|
49
|
+
img = col.class.zeros(bsize, img_h, img_w, ch)
|
|
28
50
|
(0...fil_h).each do |i|
|
|
29
51
|
i_range = (i...(i + strides[0] * out_h)).step(strides[0]).to_a
|
|
30
52
|
(0...fil_w).each do |j|
|
|
@@ -35,6 +57,12 @@ module DNN
|
|
|
35
57
|
img
|
|
36
58
|
end
|
|
37
59
|
|
|
60
|
+
def col2im_gpu(col, img_shape, out_h, out_w, fil_h, fil_w, strides)
|
|
61
|
+
col = Utils.cumo2numo(col)
|
|
62
|
+
img = col2im_cpu(col, img_shape, out_h, out_w, fil_h, fil_w, strides)
|
|
63
|
+
Utils.numo2cumo(img)
|
|
64
|
+
end
|
|
65
|
+
|
|
38
66
|
def zero_padding(img, pad)
|
|
39
67
|
bsize, img_h, img_w, ch = img.shape
|
|
40
68
|
img2 = Xumo::SFloat.zeros(bsize, img_h + pad[0], img_w + pad[1], ch)
|
|
@@ -61,7 +61,7 @@ module DNN
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
class Add < MergeLayer
|
|
64
|
-
include
|
|
64
|
+
include LayerNode
|
|
65
65
|
|
|
66
66
|
def forward_node(x1, x2)
|
|
67
67
|
@x1_shape = x1.shape
|
|
@@ -77,7 +77,7 @@ module DNN
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
class Sub < MergeLayer
|
|
80
|
-
include
|
|
80
|
+
include LayerNode
|
|
81
81
|
|
|
82
82
|
def forward_node(x1, x2)
|
|
83
83
|
@x1_shape = x1.shape
|
|
@@ -93,7 +93,7 @@ module DNN
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
class Mul < MergeLayer
|
|
96
|
-
include
|
|
96
|
+
include LayerNode
|
|
97
97
|
|
|
98
98
|
def forward_node(x1, x2)
|
|
99
99
|
@x1, @x2 = x1, x2
|
|
@@ -108,7 +108,7 @@ module DNN
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
class Div < MergeLayer
|
|
111
|
-
include
|
|
111
|
+
include LayerNode
|
|
112
112
|
|
|
113
113
|
def forward_node(x1, x2)
|
|
114
114
|
@x1, @x2 = x1, x2
|
|
@@ -123,7 +123,7 @@ module DNN
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
class Dot < MergeLayer
|
|
126
|
-
include
|
|
126
|
+
include LayerNode
|
|
127
127
|
|
|
128
128
|
def forward_node(x1, x2)
|
|
129
129
|
@x1, @x2 = x1, x2
|
|
@@ -205,8 +205,11 @@ module DNN
|
|
|
205
205
|
|
|
206
206
|
def forward_node(x)
|
|
207
207
|
@x_shape = x.shape
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
if @axis
|
|
209
|
+
x.sum(axis: @axis, keepdims: true)
|
|
210
|
+
else
|
|
211
|
+
x.sum
|
|
212
|
+
end
|
|
210
213
|
end
|
|
211
214
|
|
|
212
215
|
def backward_node(dy)
|
|
@@ -236,8 +239,13 @@ module DNN
|
|
|
236
239
|
|
|
237
240
|
def forward_node(x)
|
|
238
241
|
@x_shape = x.shape
|
|
239
|
-
|
|
240
|
-
|
|
242
|
+
if @axis
|
|
243
|
+
@dim = x.shape[@axis]
|
|
244
|
+
x.mean(axis: @axis, keepdims: true)
|
|
245
|
+
else
|
|
246
|
+
@dim = x.size
|
|
247
|
+
x.mean
|
|
248
|
+
end
|
|
241
249
|
end
|
|
242
250
|
|
|
243
251
|
def backward_node(dy)
|
|
@@ -1,30 +1,6 @@
|
|
|
1
1
|
module DNN
|
|
2
2
|
module Layers
|
|
3
3
|
|
|
4
|
-
module MergeLayerNode
|
|
5
|
-
def forward(input1, input2)
|
|
6
|
-
x1 = input1.data
|
|
7
|
-
x2 = input2.data
|
|
8
|
-
prev1 = (input1.is_a?(Tensor) ? input1.link : input1)
|
|
9
|
-
prev2 = (input2.is_a?(Tensor) ? input2.link : input2)
|
|
10
|
-
y = forward_node(x1, x2)
|
|
11
|
-
link = TwoInputLink.new(prev1, prev2, self)
|
|
12
|
-
Tensor.convert(y, link)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def backward(dy)
|
|
16
|
-
backward_node(dy)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def forward_node(x1, x2)
|
|
20
|
-
raise NotImplementedError, "Class '#{self.class.name}' has implement method 'forward_node'"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def backward_node(dy)
|
|
24
|
-
raise NotImplementedError, "Class '#{self.class.name}' has implement method 'backward_node'"
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
4
|
class MergeLayer < Layer
|
|
29
5
|
def self.call(x1, x2, *args)
|
|
30
6
|
new(*args).call(x1, x2)
|
|
@@ -33,7 +9,7 @@ module DNN
|
|
|
33
9
|
def call(input1, input2)
|
|
34
10
|
input1 = Tensor.convert(input1) if !input1.is_a?(Tensor) && !input1.is_a?(Param)
|
|
35
11
|
input2 = Tensor.convert(input2) if !input2.is_a?(Tensor) && !input2.is_a?(Param)
|
|
36
|
-
if input1.data.is_a?(
|
|
12
|
+
if input1.data.is_a?(Xumo::NArray)
|
|
37
13
|
build(input1.data.shape[1..-1]) unless built?
|
|
38
14
|
else
|
|
39
15
|
build([1]) unless built?
|
|
@@ -43,7 +19,7 @@ module DNN
|
|
|
43
19
|
end
|
|
44
20
|
|
|
45
21
|
class Concatenate < MergeLayer
|
|
46
|
-
include
|
|
22
|
+
include LayerNode
|
|
47
23
|
|
|
48
24
|
attr_reader :axis
|
|
49
25
|
|