rubygrad 1.1.2 → 1.2.1
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/lib/nn.rb +56 -19
- data/mlp_example.rb +15 -6
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8f74bc1d833eda69afa375ab9cbdbca37fd201a3635e9c517264b1255102c613
|
4
|
+
data.tar.gz: 72461737f3c24099121b35ddbf26300898ce825b1b18625b7c899ac50941fa18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7e42b0f1f0e0895635ec37cccfc950d302690cddc53c432f4a3ce8970b9b3e9f80b58e2dc1d7136ab35cf49277e56ec929eaa473466257aca152611dcaee0d4
|
7
|
+
data.tar.gz: 6d5ba4a6b70f91e7909c6b878c99a8bd8534ddec279f70ac7a0afe395a7c849b9b687101ef0304ccd50fd234e38109be27e1280d63e998cfe6a0f9396315d5f0
|
data/lib/nn.rb
CHANGED
@@ -2,12 +2,14 @@ require_relative "value.rb"
|
|
2
2
|
|
3
3
|
class Neuron
|
4
4
|
|
5
|
-
def initialize(number_of_inputs)
|
5
|
+
def initialize(number_of_inputs, activation_function)
|
6
6
|
@initial_weights = Array.new(number_of_inputs) { rand(-1.0..1.0) }
|
7
7
|
@initial_bias = rand(-1.0..1.0)
|
8
8
|
|
9
9
|
@weights = @initial_weights.map { |w| Value.new(w) }
|
10
10
|
@bias = Value.new(@initial_bias)
|
11
|
+
|
12
|
+
@activation_function = activation_function
|
11
13
|
end
|
12
14
|
|
13
15
|
def reset_params
|
@@ -30,7 +32,7 @@ class Neuron
|
|
30
32
|
self.weights + [self.bias]
|
31
33
|
end
|
32
34
|
|
33
|
-
def calc(inputs
|
35
|
+
def calc(inputs)
|
34
36
|
# xw + b
|
35
37
|
n = self.weights.size
|
36
38
|
raise "Wrong number of inputs! #{inputs.size} expected #{n}" unless n == inputs.size
|
@@ -38,25 +40,26 @@ class Neuron
|
|
38
40
|
n.times do |index|
|
39
41
|
sum += self.weights[index] * inputs[index]
|
40
42
|
end
|
41
|
-
if
|
43
|
+
if @activation_function == :tanh
|
42
44
|
sum.tanh
|
43
|
-
elsif
|
45
|
+
elsif @activation_function == :relu
|
44
46
|
sum.relu
|
45
|
-
elsif
|
47
|
+
elsif @activation_function == :sigmoid
|
46
48
|
sum.sigmoid
|
47
49
|
else
|
48
|
-
raise "Unsupported activation function: #{
|
50
|
+
raise "Unsupported activation function: #{activation_function}"
|
49
51
|
end
|
50
52
|
end
|
51
53
|
end
|
52
54
|
|
53
55
|
class Layer
|
54
56
|
|
55
|
-
def initialize(number_of_inputs, number_of_outputs)
|
56
|
-
@neurons = Array.new(number_of_outputs) { Neuron.new(number_of_inputs) }
|
57
|
+
def initialize(number_of_inputs, number_of_outputs, activation_function)
|
58
|
+
@neurons = Array.new(number_of_outputs) { Neuron.new(number_of_inputs, activation_function) }
|
59
|
+
@activation_function = activation_function
|
57
60
|
end
|
58
61
|
|
59
|
-
attr_reader :neurons
|
62
|
+
attr_reader :neurons, :activation_function
|
60
63
|
|
61
64
|
def parameters
|
62
65
|
params = []
|
@@ -68,10 +71,10 @@ class Layer
|
|
68
71
|
self.neurons.each { |n| n.reset_params }
|
69
72
|
end
|
70
73
|
|
71
|
-
def calc(inputs
|
74
|
+
def calc(inputs)
|
72
75
|
outs = []
|
73
76
|
self.neurons.each do |neuron|
|
74
|
-
outs << neuron.calc(inputs
|
77
|
+
outs << neuron.calc(inputs)
|
75
78
|
end
|
76
79
|
outs
|
77
80
|
end
|
@@ -80,18 +83,52 @@ end
|
|
80
83
|
class MLP
|
81
84
|
|
82
85
|
def initialize(*layers_config)
|
83
|
-
|
86
|
+
|
87
|
+
number_of_layers = layers_config.size - 1 # last param is the activation function
|
88
|
+
|
89
|
+
act = layers_config.last
|
90
|
+
|
91
|
+
if !act.is_a?(Symbol) and !act.is_a?(Array)
|
92
|
+
raise "Activation function must be passed as the last parameter: #{act.class} expected Symbol or Array of Symbols"
|
93
|
+
end
|
94
|
+
|
95
|
+
single_activation_function = nil
|
96
|
+
|
97
|
+
if act.is_a?(Symbol)
|
98
|
+
|
99
|
+
single_activation_function = act
|
100
|
+
|
101
|
+
else # is Array
|
102
|
+
|
103
|
+
if not act.all? { |item| item.is_a?(Symbol) }
|
104
|
+
raise "Array with activation functions must contain symbols: #{act}"
|
105
|
+
end
|
106
|
+
|
107
|
+
if act.size == 1
|
108
|
+
single_activation_function = act.first
|
109
|
+
elsif act.size != number_of_layers - 1
|
110
|
+
raise "Array size does not match number of layers with activation functions: #{act.size} expected #{number_of_layers - 1}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
84
114
|
@layers = Array.new(number_of_layers - 1) # input layer is not really a layer object
|
85
115
|
(number_of_layers - 1).times do |i|
|
86
|
-
@layers[i] = Layer.new(layers_config[i], layers_config[i + 1])
|
116
|
+
@layers[i] = Layer.new(layers_config[i], layers_config[i + 1], single_activation_function.nil? ? act[i] : single_activation_function)
|
87
117
|
end
|
118
|
+
|
88
119
|
@layers_config = layers_config
|
89
120
|
end
|
90
121
|
|
91
122
|
attr_reader :layers
|
92
123
|
|
93
124
|
def inspect
|
94
|
-
|
125
|
+
lay = @layers_config[0..-2].join(", ") # slice to remove last element
|
126
|
+
act = @layers_config.last.inspect
|
127
|
+
"MLP(#{lay}, #{act})"
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_s
|
131
|
+
inspect
|
95
132
|
end
|
96
133
|
|
97
134
|
def parameters
|
@@ -102,11 +139,11 @@ class MLP
|
|
102
139
|
|
103
140
|
def show_params(in_words = false)
|
104
141
|
if in_words
|
105
|
-
n = @layers_config
|
142
|
+
n = @layers_config.first
|
106
143
|
puts "Layer 0: (#{n} input#{n > 1 ? "s" : ""})"
|
107
144
|
self.layers.each_with_index do |layer, i|
|
108
145
|
n = layer.neurons.size
|
109
|
-
puts "Layer #{i + 1}: (#{n} neuron#{n > 1 ? "s" : ""})"
|
146
|
+
puts "Layer #{i + 1}: (#{n} neuron#{n > 1 ? "s" : ""}, #{layer.activation_function.inspect} activation)"
|
110
147
|
layer.neurons.each_with_index do |neuron, ii|
|
111
148
|
n = neuron.weights.size
|
112
149
|
puts "\tNeuron #{ii + 1}: (#{n} weight#{n > 1 ? "s" : ""})"
|
@@ -116,7 +153,7 @@ class MLP
|
|
116
153
|
end
|
117
154
|
end
|
118
155
|
else
|
119
|
-
n = @layers_config
|
156
|
+
n = @layers_config.first
|
120
157
|
self.layers.each_with_index do |layer, i|
|
121
158
|
n = layer.neurons.size
|
122
159
|
puts "["
|
@@ -146,10 +183,10 @@ class MLP
|
|
146
183
|
self.parameters.each { |p| p.grad = 0.0 }
|
147
184
|
end
|
148
185
|
|
149
|
-
def calc(inputs
|
186
|
+
def calc(inputs)
|
150
187
|
out = inputs
|
151
188
|
self.layers.each do |layer|
|
152
|
-
out = layer.calc(out
|
189
|
+
out = layer.calc(out) # chain the results forward, layer by layer
|
153
190
|
end
|
154
191
|
out.size == 1 ? out[0] : out # for convenience
|
155
192
|
end
|
data/mlp_example.rb
CHANGED
@@ -1,14 +1,22 @@
|
|
1
|
-
|
1
|
+
require 'rubygrad'
|
2
2
|
|
3
|
-
|
3
|
+
# Build a Machine Learning Perceptron with 4 layers
|
4
|
+
# First Layer (Layer 0) => Input Layer => 3 Neurons => 3 Inputs
|
5
|
+
# Second Layer (Layer 1) => Hidden Layer => 4 Neurons
|
6
|
+
# Third Layer (Layer 2) => Hidden Layer => 4 Neurons
|
7
|
+
# Fourth Layer (Layer 3) => Output Layer => 1 Neuron => 1 Output
|
8
|
+
nn = MLP.new(3, 4, 4, 1, :tanh)
|
4
9
|
|
10
|
+
# 4 input samples
|
5
11
|
x_inputs = [
|
6
12
|
[2.0, 3.0, -1.0],
|
7
13
|
[3.0, -1.0, 0.5],
|
8
14
|
[0.5, 1.0, 1.0],
|
9
15
|
[1.0, 1.0, -1.0]
|
10
16
|
]
|
11
|
-
|
17
|
+
|
18
|
+
# expected output for each of the 4 inputs above
|
19
|
+
y_expected = [1.0, -1.0, -1.0, 1.0]
|
12
20
|
|
13
21
|
passes = 2000
|
14
22
|
learning_rate = 0.2
|
@@ -20,7 +28,7 @@ _loss_format = "%.#{_loss_precision}f"
|
|
20
28
|
(0...passes).each do |pass|
|
21
29
|
|
22
30
|
# forward pass (calculate output)
|
23
|
-
y_calculated = x_inputs.map { |x| nn.calc(x
|
31
|
+
y_calculated = x_inputs.map { |x| nn.calc(x) }
|
24
32
|
|
25
33
|
# loss function (check how good the neural net is)
|
26
34
|
loss = 0.0
|
@@ -38,6 +46,7 @@ _loss_format = "%.#{_loss_precision}f"
|
|
38
46
|
break if loss.value == 0 # just for fun and just in case
|
39
47
|
end
|
40
48
|
|
41
|
-
y_calculated = x_inputs.map { |x| nn.calc(x
|
49
|
+
y_calculated = x_inputs.map { |x| nn.calc(x) }
|
42
50
|
puts
|
43
|
-
puts
|
51
|
+
puts "Final NN results:"
|
52
|
+
y_calculated.each_with_index { |y_c, i| puts "Output: #{y_c} => Expected: #{y_expected[i]}" }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubygrad
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergio Oliveira Jr
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
11
|
+
date: 2023-03-22 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: sergio.oliveira.jr@gmail.com
|