rubygrad 1.1.2 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/lib/nn.rb +77 -20
  3. data/mlp_example.rb +21 -6
  4. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2e2b978ae8290e86a85ad55bdda2c86ceed5fb38d19f8713e6104ce5d1c447d
4
- data.tar.gz: 5da44a923ca6d8ed8bcbc55a7a8ac937b1d71d1b8b560854320290d853b2450d
3
+ metadata.gz: f05df7e74616deb2db9a7c7340e105e0997268d9d2938d07dbabf51395684fe5
4
+ data.tar.gz: 6179a5c6a57c37411784195a9ca3a15f73debdd06aaeae46a754ff5857e60770
5
5
  SHA512:
6
- metadata.gz: 68f06f445e60e400c863485ee63c3e919a39df86dc7da2f9c5febdb57c4bcf5475877fbca9356a32d0e9cd5130e2ec290ff910a4f155a7565cb08d0040ac7993
7
- data.tar.gz: c0947a0b25cd505594e7d30a1d5da7e48c47f5899e6683047251ba33b720db7ed9456e251826f02ec68947007cf88db802d4ee1c0630c2edaebf4746e2794be5
6
+ metadata.gz: 2187327cf7c8b2a8e323b90138c49b7bd6cb5c84ed0e784a5c288556525cf0f8c619dd2aef024b6afaa27aad5b4f2a104457e67dd7158a121f41e9b5dfa631c3
7
+ data.tar.gz: ebb19b5217fb756fa9072dcf9e2b39ff503f93ac94f782cc45e2ffdc73ee85e6f5c9e471d7995d1017aa0697b97437cf5eb56be1cee34d79e30b0aaf4d5818b9
data/lib/nn.rb CHANGED
@@ -2,14 +2,18 @@ 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
 
15
+ attr_reader :weights, :bias
16
+
13
17
  def reset_params
14
18
  @initial_weights.each_with_index do |w,i|
15
19
  @weights[i].value = w
@@ -24,13 +28,15 @@ class Neuron
24
28
  (1...params.size).each { |i| @weights[i - 1].value = params[i] }
25
29
  end
26
30
 
27
- attr_reader :weights, :bias
31
+ def set_activation_function(activation_function)
32
+ @activation_function = activation_function
33
+ end
28
34
 
29
35
  def parameters
30
36
  self.weights + [self.bias]
31
37
  end
32
38
 
33
- def calc(inputs, activation)
39
+ def calc(inputs)
34
40
  # xw + b
35
41
  n = self.weights.size
36
42
  raise "Wrong number of inputs! #{inputs.size} expected #{n}" unless n == inputs.size
@@ -38,25 +44,26 @@ class Neuron
38
44
  n.times do |index|
39
45
  sum += self.weights[index] * inputs[index]
40
46
  end
41
- if activation == :tanh
47
+ if @activation_function == :tanh
42
48
  sum.tanh
43
- elsif activation == :relu
49
+ elsif @activation_function == :relu
44
50
  sum.relu
45
- elsif activation == :sigmoid
51
+ elsif @activation_function == :sigmoid
46
52
  sum.sigmoid
47
53
  else
48
- raise "Unsupported activation function: #{activation}"
54
+ raise "Unsupported activation function: #{activation_function}"
49
55
  end
50
56
  end
51
57
  end
52
58
 
53
59
  class Layer
54
60
 
55
- def initialize(number_of_inputs, number_of_outputs)
56
- @neurons = Array.new(number_of_outputs) { Neuron.new(number_of_inputs) }
61
+ def initialize(number_of_inputs, number_of_outputs, activation_function)
62
+ @neurons = Array.new(number_of_outputs) { Neuron.new(number_of_inputs, activation_function) }
63
+ @activation_function = activation_function
57
64
  end
58
65
 
59
- attr_reader :neurons
66
+ attr_reader :neurons, :activation_function
60
67
 
61
68
  def parameters
62
69
  params = []
@@ -68,10 +75,15 @@ class Layer
68
75
  self.neurons.each { |n| n.reset_params }
69
76
  end
70
77
 
71
- def calc(inputs, activation)
78
+ def set_activation_function(activation_function)
79
+ @activation_function = activation_function
80
+ self.neurons.each { |n| n.set_activation_function(activation_function) }
81
+ end
82
+
83
+ def calc(inputs)
72
84
  outs = []
73
85
  self.neurons.each do |neuron|
74
- outs << neuron.calc(inputs, activation)
86
+ outs << neuron.calc(inputs)
75
87
  end
76
88
  outs
77
89
  end
@@ -80,18 +92,58 @@ end
80
92
  class MLP
81
93
 
82
94
  def initialize(*layers_config)
83
- number_of_layers = layers_config.size
95
+
96
+ number_of_layers = layers_config.size - 1 # last param is the activation function
97
+
98
+ act_array = validate_act_array(layers_config.last, number_of_layers)
99
+
84
100
  @layers = Array.new(number_of_layers - 1) # input layer is not really a layer object
85
101
  (number_of_layers - 1).times do |i|
86
- @layers[i] = Layer.new(layers_config[i], layers_config[i + 1])
102
+ @layers[i] = Layer.new(layers_config[i], layers_config[i + 1], act_array[i])
87
103
  end
104
+
88
105
  @layers_config = layers_config
89
106
  end
90
107
 
108
+ private def validate_act_array(act, number_of_layers)
109
+
110
+ if !act.is_a?(Symbol) and !act.is_a?(Array)
111
+ raise "Activation function must be passed as the last parameter: #{act.class} expected Symbol or Array of Symbols"
112
+ end
113
+
114
+ if act.is_a?(Array)
115
+
116
+ if not act.all? { |item| item.is_a?(Symbol) }
117
+ raise "Array with activation functions must contain symbols: #{act}"
118
+ end
119
+
120
+ if act.size == 1
121
+ return Array.new(number_of_layers - 1) { act.first }
122
+ end
123
+
124
+ if act.size != number_of_layers - 1
125
+ raise "Array size does not match number of layers with activation functions: #{act.size} expected #{number_of_layers - 1}"
126
+ end
127
+
128
+ return act
129
+
130
+ else # is a Symbol
131
+
132
+ return Array.new(number_of_layers - 1) { act }
133
+
134
+ end
135
+ end
136
+
91
137
  attr_reader :layers
92
138
 
93
139
  def inspect
94
- "MLP(#{@layers_config.join(", ")})"
140
+ lay = @layers_config[0..-2].join(", ") # slice to remove last element
141
+ act = @layers_config.last.inspect
142
+ "MLP(#{lay}, #{act})"
143
+ end
144
+
145
+ def to_s
146
+ inspect
95
147
  end
96
148
 
97
149
  def parameters
@@ -102,11 +154,11 @@ class MLP
102
154
 
103
155
  def show_params(in_words = false)
104
156
  if in_words
105
- n = @layers_config[0]
157
+ n = @layers_config.first
106
158
  puts "Layer 0: (#{n} input#{n > 1 ? "s" : ""})"
107
159
  self.layers.each_with_index do |layer, i|
108
160
  n = layer.neurons.size
109
- puts "Layer #{i + 1}: (#{n} neuron#{n > 1 ? "s" : ""})"
161
+ puts "Layer #{i + 1}: (#{n} neuron#{n > 1 ? "s" : ""}, #{layer.activation_function.inspect} activation)"
110
162
  layer.neurons.each_with_index do |neuron, ii|
111
163
  n = neuron.weights.size
112
164
  puts "\tNeuron #{ii + 1}: (#{n} weight#{n > 1 ? "s" : ""})"
@@ -116,7 +168,7 @@ class MLP
116
168
  end
117
169
  end
118
170
  else
119
- n = @layers_config[0]
171
+ n = @layers_config.first
120
172
  self.layers.each_with_index do |layer, i|
121
173
  n = layer.neurons.size
122
174
  puts "["
@@ -142,14 +194,19 @@ class MLP
142
194
  end
143
195
  end
144
196
 
197
+ def set_activation_function(activation_function)
198
+ act_array = validate_act_array(activation_function, @layers_config.size - 1)
199
+ self.layers.each_with_index { |layer, i| layer.set_activation_function(act_array[i]) }
200
+ end
201
+
145
202
  def zero_grad
146
203
  self.parameters.each { |p| p.grad = 0.0 }
147
204
  end
148
205
 
149
- def calc(inputs, activation)
206
+ def calc(inputs)
150
207
  out = inputs
151
208
  self.layers.each do |layer|
152
- out = layer.calc(out, activation) # chain the results forward, layer by layer
209
+ out = layer.calc(out) # chain the results forward, layer by layer
153
210
  end
154
211
  out.size == 1 ? out[0] : out # for convenience
155
212
  end
data/mlp_example.rb CHANGED
@@ -1,14 +1,28 @@
1
- require_relative 'lib/nn.rb'
1
+ require 'rubygrad'
2
+ #require_relative 'lib/nn.rb'
2
3
 
3
- nn = MLP.new(3, 4, 4, 1)
4
+ # Build a Machine Learning Perceptron with 4 layers
5
+ # First Layer (Layer 0) => Input Layer => 3 Neurons => 3 Inputs
6
+ # Second Layer (Layer 1) => Hidden Layer => 4 Neurons
7
+ # Third Layer (Layer 2) => Hidden Layer => 4 Neurons
8
+ # Fourth Layer (Layer 3) => Output Layer => 1 Neuron => 1 Output
9
+ nn = MLP.new(3, 4, 4, 1, :tanh)
4
10
 
11
+ nn.show_params
12
+ puts
13
+ nn.show_params(in_words = true)
14
+ puts
15
+
16
+ # 4 input samples
5
17
  x_inputs = [
6
18
  [2.0, 3.0, -1.0],
7
19
  [3.0, -1.0, 0.5],
8
20
  [0.5, 1.0, 1.0],
9
21
  [1.0, 1.0, -1.0]
10
22
  ]
11
- y_expected = [1.0, -1.0, -1.0, 1.0] # desired
23
+
24
+ # expected output for each of the 4 inputs above
25
+ y_expected = [1.0, -1.0, -1.0, 1.0]
12
26
 
13
27
  passes = 2000
14
28
  learning_rate = 0.2
@@ -20,7 +34,7 @@ _loss_format = "%.#{_loss_precision}f"
20
34
  (0...passes).each do |pass|
21
35
 
22
36
  # forward pass (calculate output)
23
- y_calculated = x_inputs.map { |x| nn.calc(x, :tanh) }
37
+ y_calculated = x_inputs.map { |x| nn.calc(x) }
24
38
 
25
39
  # loss function (check how good the neural net is)
26
40
  loss = 0.0
@@ -38,6 +52,7 @@ _loss_format = "%.#{_loss_precision}f"
38
52
  break if loss.value == 0 # just for fun and just in case
39
53
  end
40
54
 
41
- y_calculated = x_inputs.map { |x| nn.calc(x, :tanh) }
55
+ y_calculated = x_inputs.map { |x| nn.calc(x) }
42
56
  puts
43
- puts y_calculated
57
+ puts "Final NN results:"
58
+ 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.2
4
+ version: 1.2.2
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-21 00:00:00.000000000 Z
11
+ date: 2023-03-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: sergio.oliveira.jr@gmail.com
@@ -40,7 +40,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
42
  requirements: []
43
- rubygems_version: 3.3.26
43
+ rubygems_version: 3.4.9
44
44
  signing_key:
45
45
  specification_version: 4
46
46
  summary: A port of Andrej Karpathy's micrograd to Ruby.