qoa 0.0.2 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ca1c9d06ebd0907eb52f52d4a34c1f44e53b8674efda61731d5ff397f2650a6
4
- data.tar.gz: cd31fa3f0bafef6c043d7b22b368ca84c6a1240ce2c782d058d9c032d6c16be9
3
+ metadata.gz: 8541feace7b5d8df6418672899fbba8289cc63624d7876c640a28e3143a450f3
4
+ data.tar.gz: 3944346b90a422a1ea843aef9b0a5bd18c8067e0bc075fa897daab0a1cf9373c
5
5
  SHA512:
6
- metadata.gz: d4d293ad9e455a5f9017583ba5d78e717210900226eb72f776d69fbad6d7ac16b99abdaf956707310f11b02b60e2bc78a43118ed84045b5cda7a115d0936f143
7
- data.tar.gz: 257b167c2aaa3e9206c67b5c3ff3bee78a1d29aa355a5c64e43cd091c7edd3bec17a435105947a1001424d43521baa35e503bd5c02c7d7927fbebfb50a78f153
6
+ metadata.gz: 8e0afee3076f2d47ff091c2d943c53710e080b015f2773733aabaf90fc4b2ccb26507d24344d1e3b6915e488df875b093577098572c889a1a621064600467037
7
+ data.tar.gz: aaf6f8e7e9280630fd6218d72a6dd47a4e68602b193a127be8b59462e3204267a28e490e61e8dc19523b8aa463f2d9611fd81617b9da9f79d0e83b1c1942a638
data/README.md CHANGED
@@ -10,15 +10,22 @@ Qoa is a simple and customizable neural network library for Ruby. It allows you
10
10
  - Weight initialization using Xavier initialization
11
11
  - Customizable learning parameters
12
12
  - Parallelized backward pass for faster training
13
+ - Supports convolutional and pooling layers
13
14
 
14
15
  ## Installation
15
16
 
16
- Simply copy the `neural_network.rb`, `activation_functions.rb`, and `matrix_helpers.rb` files into your project and require them.
17
+ ### Install via RubyGems
18
+
19
+ You can install the gem via RubyGems:
20
+
21
+ ```
22
+ gem install qoa
23
+ ```
24
+
25
+ Then, require the gem in your project:
17
26
 
18
27
  ```ruby
19
- require_relative 'neural_network'
20
- require_relative 'activation_functions'
21
- require_relative 'matrix_helpers'
28
+ require 'qoa'
22
29
  ```
23
30
 
24
31
  ## Usage
@@ -28,7 +35,7 @@ require_relative 'matrix_helpers'
28
35
  To create a new neural network, you can initialize an instance of `Qoa::NeuralNetwork` with the following parameters:
29
36
 
30
37
  - `input_nodes`: The number of input nodes.
31
- - `hidden_layers`: An array of the number of nodes in each hidden layer.
38
+ - `hidden_layers`: An array of the number of nodes in each hidden layer, or an array of `[:conv, nodes, kernel_size, stride]` for a convolutional layer or `[:pool, nodes, pool_size, stride]` for a pooling layer.
32
39
  - `output_nodes`: The number of output nodes.
33
40
  - `learning_rate`: The learning rate for the gradient descent optimization.
34
41
  - `dropout_rate`: The dropout rate for regularization.
@@ -36,11 +43,36 @@ To create a new neural network, you can initialize an instance of `Qoa::NeuralNe
36
43
  - `decay_rate`: The decay rate for the RMSProp optimizer (default is `0.9`).
37
44
  - `epsilon`: A small value to prevent division by zero in the RMSProp optimizer (default is `1e-8`).
38
45
  - `batch_size`: The size of the mini-batches used for training (default is `10`).
46
+ - `l1_lambda`: The L1 regularization parameter (default is `0.0`).
47
+ - `l2_lambda`: The L2 regularization parameter (default is `0.0`).
39
48
 
40
49
  Example:
41
50
 
42
51
  ```ruby
43
- nn = Qoa::NeuralNetwork.new(784, [128, 64], 10, 0.001, 0.5, :relu, 0.9, 1e-8, 32)
52
+ require 'qoa'
53
+
54
+ input_nodes = 784 # Number of input features (e.g., 28x28 pixels for MNIST dataset)
55
+ hidden_layers = [128, [:conv, 64, 3, 1]] # One hidden layer with 128 nodes and one convolutional layer with 64 nodes, kernel size 3, and stride 1
56
+ output_nodes = 10 # Number of output classes (e.g., 10 for MNIST dataset)
57
+ learning_rate = 0.01
58
+ dropout_rate = 0.5
59
+ activation_func = :relu
60
+
61
+ nn = Qoa::NeuralNetwork.new(input_nodes, hidden_layers, output_nodes, learning_rate, dropout_rate, activation_func)
62
+ ```
63
+
64
+ ### Saving and Loading Models
65
+
66
+ To save the trained model to a file, call the `save_model` method:
67
+
68
+ ```ruby
69
+ nn.save_model('model.json')
70
+ ```
71
+
72
+ To load a previously saved model, call the `load_model` method:
73
+
74
+ ```ruby
75
+ nn.load_model('model.json')
44
76
  ```
45
77
 
46
78
  ### Training the Neural Network
@@ -98,4 +130,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/mmaton
98
130
 
99
131
  ## License
100
132
 
101
- The library is available as open source under the terms of the [MIT License](http://opensource.org/licenses/Apache-2.0).
133
+ The library is available as open source under the terms of the [Apache-2.0 License](http://opensource.org/licenses/Apache-2.0).
@@ -0,0 +1,27 @@
1
+ module Qoa
2
+ module Err
3
+ module Validations
4
+ def validate_constructor_args(input_nodes, hidden_layers, output_nodes, learning_rate, dropout_rate, activation_func, decay_rate, epsilon, batch_size, l1_lambda, l2_lambda)
5
+ raise ArgumentError, 'input_nodes, hidden_layers, and output_nodes must be positive integers' unless [input_nodes, output_nodes].all? { |x| x.is_a?(Integer) && x > 0 } && hidden_layers.is_a?(Array) && hidden_layers.all? { |x| x.is_a?(Integer) && x > 0 }
6
+ raise ArgumentError, 'learning_rate, dropout_rate, decay_rate, epsilon, l1_lambda, and l2_lambda must be positive numbers' unless [learning_rate, dropout_rate, decay_rate, epsilon, l1_lambda, l2_lambda].all? { |x| x.is_a?(Numeric) && x >= 0 }
7
+ raise ArgumentError, 'activation_func must be a valid symbol' unless ActivationFunctions.methods.include?(activation_func)
8
+ raise ArgumentError, 'batch_size must be a positive integer' unless batch_size.is_a?(Integer) && batch_size > 0
9
+ end
10
+
11
+ def validate_query_args(inputs)
12
+ raise ArgumentError, 'inputs must be an array of numbers' unless inputs.is_a?(Array) && inputs.all? { |x| x.is_a?(Numeric) }
13
+ end
14
+
15
+ def validate_calculate_loss_args(inputs, targets, loss_function)
16
+ raise ArgumentError, 'inputs and targets must have the same length' if inputs.size != targets.size
17
+ raise ArgumentError, 'inputs and targets must be arrays of arrays of numbers' unless inputs.is_a?(Array) && targets.is_a?(Array) && inputs.all? { |x| x.is_a?(Array) && x.all? { |y| y.is_a?(Numeric) } } && targets.all? { |x| x.is_a?(Array) && x.all? { |y| y.is_a?(Numeric) } }
18
+ raise ArgumentError, 'loss_function must be a valid symbol' unless LossFunctions.methods.include?(loss_function)
19
+ end
20
+
21
+ def validate_train_args(inputs, targets)
22
+ raise ArgumentError, 'inputs and targets must have the same length' if inputs.size != targets.size
23
+ raise ArgumentError, 'inputs and targets must be arrays of arrays of numbers' unless inputs.is_a?(Array) && targets.is_a?(Array) && inputs.all? { |x| x.is_a?(Array) && x.all? { |y| y.is_a?(Numeric) } } && targets.all? { |x| x.is_a?(Array) && x.all? { |y| y.is_a?(Numeric) } }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module Qoa
2
+ module Layers
3
+ class ConvolutionalLayer < Qoa::Layers::Layer
4
+ attr_reader :kernel_size, :stride
5
+
6
+ def initialize(input_size, output_size, kernel_size, stride = 1)
7
+ super(input_size, output_size)
8
+ @kernel_size = kernel_size
9
+ @stride = stride
10
+ end
11
+
12
+ def random_matrix(rows, cols)
13
+ limit = Math.sqrt(6.0 / (rows + cols))
14
+ Array.new(rows) { Array.new(cols) { rand(-limit..limit) } }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ module Qoa
2
+ module Layers
3
+ class Layer
4
+ attr_reader :input_size, :output_size, :weights
5
+
6
+ def initialize(input_size, output_size)
7
+ @input_size = input_size
8
+ @output_size = output_size
9
+ @weights = random_matrix(output_size, input_size)
10
+ end
11
+
12
+ def random_matrix(rows, cols)
13
+ limit = Math.sqrt(6.0 / (rows + cols))
14
+ Array.new(rows) { Array.new(cols) { rand(-limit..limit) } }
15
+ end
16
+
17
+ def weights=(new_weights)
18
+ @weights = new_weights
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ module Qoa
2
+ module Layers
3
+ class PoolingLayer < Qoa::Layers::Layer
4
+ attr_reader :pool_size, :stride
5
+
6
+ def initialize(input_size, output_size, pool_size, stride = 1)
7
+ super(input_size, output_size)
8
+ @pool_size = pool_size
9
+ @stride = stride
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ module Qoa
2
+ module LossFunctions
3
+ class << self
4
+ def mean_squared_error(prediction, target)
5
+ raise ArgumentError, 'prediction and target must have the same length' if prediction.size != target.size
6
+ prediction.zip(target).map { |p, t| (p - t) ** 2 }.sum / prediction.size
7
+ end
8
+
9
+ def cross_entropy_loss(prediction, target)
10
+ raise ArgumentError, 'prediction and target must have the same length' if prediction.size != target.size
11
+ -prediction.zip(target).map { |p, t| t * Math.log(p) }.sum / prediction.size
12
+ end
13
+
14
+ def binary_cross_entropy(prediction, target)
15
+ raise ArgumentError, 'prediction and target must have the same length' if prediction.size != target.size
16
+ -prediction.zip(target).map { |p, t| t * Math.log(p) + (1 - t) * Math.log(1 - p) }.sum / prediction.size
17
+ end
18
+
19
+ def categorical_cross_entropy(prediction, target)
20
+ raise ArgumentError, 'prediction and target must have the same length' if prediction.size != target.size
21
+ -prediction.zip(target).map { |p, t| t * Math.log(p) }.sum / prediction.size
22
+ end
23
+
24
+ def mean_absolute_error(prediction, target)
25
+ raise ArgumentError, 'prediction and target must have the same length' if prediction.size != target.size
26
+ prediction.zip(target).map { |p, t| (p - t).abs }.sum / prediction.size
27
+ end
28
+ end
29
+ end
30
+ end
@@ -24,7 +24,11 @@ module Qoa
24
24
  end
25
25
 
26
26
  def apply_function(matrix, func)
27
- matrix.map { |row| row.map { |x| func.call(x) } }
27
+ matrix.map do |row|
28
+ row.map do |x|
29
+ x.nil? ? nil : func.call(x) # Add a check for nil values
30
+ end
31
+ end
28
32
  end
29
33
 
30
34
  def transpose(matrix)
@@ -1,13 +1,24 @@
1
- require 'concurrent'
1
+ require_relative 'layers/layer'
2
+ require_relative 'layers/convolutional_layer'
3
+ require_relative 'layers/pooling_layer'
2
4
  require_relative 'activation_functions'
3
- require_relative 'matrix_helpers'
5
+ require_relative 'training'
6
+ require_relative 'utils'
7
+ require_relative 'loss_functions'
8
+ require_relative 'err/validations'
4
9
 
5
10
  module Qoa
6
11
  class NeuralNetwork
7
- include MatrixHelpers
8
- attr_reader :input_nodes, :hidden_layers, :output_nodes, :learning_rate, :activation_func, :dropout_rate, :decay_rate, :epsilon, :batch_size
12
+ include Training
13
+ include Utils
14
+ include LossFunctions
15
+ include Err::Validations
16
+
17
+ attr_reader :input_nodes, :hidden_layers, :output_nodes, :learning_rate, :activation_func, :dropout_rate, :decay_rate, :epsilon, :batch_size, :l1_lambda, :l2_lambda
18
+
19
+ def initialize(input_nodes, hidden_layers, output_nodes, learning_rate, dropout_rate, activation_func = :leaky_relu, decay_rate = 0.9, epsilon = 1e-8, batch_size = 10, l1_lambda = 0.0, l2_lambda = 0.0)
20
+ # validate_constructor_args(input_nodes, hidden_layers, output_nodes, learning_rate, dropout_rate, activation_func, decay_rate, epsilon, batch_size, l1_lambda, l2_lambda)
9
21
 
10
- def initialize(input_nodes, hidden_layers, output_nodes, learning_rate, dropout_rate, activation_func = :sigmoid, decay_rate = 0.9, epsilon = 1e-8, batch_size = 10)
11
22
  @input_nodes = input_nodes
12
23
  @hidden_layers = hidden_layers
13
24
  @output_nodes = output_nodes
@@ -17,102 +28,50 @@ module Qoa
17
28
  @decay_rate = decay_rate
18
29
  @epsilon = epsilon
19
30
  @batch_size = batch_size
31
+ @l1_lambda = l1_lambda
32
+ @l2_lambda = l2_lambda
20
33
 
21
- @weights = []
22
- @weights << random_matrix(hidden_layers[0], input_nodes)
23
- hidden_layers.each_cons(2) do |l1, l2|
24
- @weights << random_matrix(l2, l1)
25
- end
26
- @weights << random_matrix(output_nodes, hidden_layers[-1])
27
- end
28
-
29
- def random_matrix(rows, cols)
30
- limit = Math.sqrt(6.0 / (rows + cols))
31
- Array.new(rows) { Array.new(cols) { rand(-limit..limit) } }
32
- end
33
-
34
- def train(inputs, targets)
35
- raise ArgumentError, 'inputs and targets must have the same length' if inputs.size != targets.size
34
+ @layers = []
35
+ @layers << Qoa::Layers::Layer.new(input_nodes, hidden_layers[0].is_a?(Array) ? hidden_layers[0][1] : hidden_layers[0])
36
36
 
37
- inputs.zip(targets).each_slice(@batch_size) do |batch|
38
- train_batch(batch)
39
- end
40
- end
41
-
42
- def train_batch(batch)
43
- derivative_func = "#{@activation_func}_derivative"
44
- batch_inputs = batch.map { |x, _| x }
45
- batch_targets = batch.map { |_, y| y }
46
-
47
- # Forward pass
48
- layer_outputs = batch_inputs.map { |inputs| forward_pass(inputs) }
49
-
50
- # Backward pass
51
- # Using thread pool to parallelize the backward pass for each input in the batch
52
- pool = Concurrent::FixedThreadPool.new(4)
53
- weight_deltas = Array.new(@weights.size) { Array.new(@weights[0].size) { Array.new(@weights[0][0].size, 0) } }
54
- mutex = Mutex.new
55
-
56
- batch.zip(layer_outputs).each do |(inputs, targets), outputs|
57
- pool.post do
58
- deltas = backward_pass(inputs, targets, outputs)
59
- mutex.synchronize do
60
- @weights.each_with_index do |_, i|
61
- weight_deltas[i] = matrix_add(weight_deltas[i], deltas[i])
62
- end
63
- end
37
+ hidden_layers.each_cons(2) do |l1, l2|
38
+ l1_size = l1.is_a?(Array) ? l1[1] : l1
39
+ l2_size = l2.is_a?(Array) ? l2[1] : l2
40
+
41
+ if l1.is_a?(Array) && l2.is_a?(Array) && l1[0] == :conv && l2[0] == :conv
42
+ @layers << Qoa::Layers::ConvolutionalLayer.new(l1_size, l2_size, l1[2], l1[3])
43
+ elsif l1.is_a?(Array) && l1[0] == :conv && l2.is_a?(Numeric)
44
+ @layers << Qoa::Layers::ConvolutionalLayer.new(l1_size, l2_size, l1[2], l1[3])
45
+ elsif l1.is_a?(Numeric) && l2.is_a?(Array) && l2[0] == :conv
46
+ @layers << Qoa::Layers::ConvolutionalLayer.new(l1_size, l2_size, l2[2], l2[3])
47
+ elsif l1.is_a?(Array) && l1[0] == :pool && l2.is_a?(Numeric)
48
+ @layers << Qoa::Layers::PoolingLayer.new(l1_size, l2_size, l1[2], l1[3])
49
+ elsif l1.is_a?(Numeric) && l2.is_a?(Array) && l2[0] == :pool
50
+ @layers << Qoa::Layers::PoolingLayer.new(l1_size, l2_size, l2[2], l2[3])
51
+ else
52
+ @layers << Qoa::Layers::Layer.new(l1_size, l2_size)
64
53
  end
65
54
  end
66
-
67
- pool.shutdown
68
- pool.wait_for_termination
69
-
70
- # Update weights
71
- @weights.each_with_index do |w, i|
72
- @weights[i] = matrix_add(w, scalar_multiply(@learning_rate / batch.size, weight_deltas[i]))
73
- end
55
+ @layers << Qoa::Layers::Layer.new(hidden_layers[-1].is_a?(Array) ? hidden_layers[-1][1] : hidden_layers[-1], output_nodes)
74
56
  end
75
57
 
76
- def forward_pass(inputs)
77
- inputs = inputs.map { |x| [x] } # Convert to column vector
78
-
79
- layer_outputs = [inputs]
80
- @weights.each_with_index do |w, i|
81
- layer_inputs = matrix_multiply(w, layer_outputs[-1])
82
- layer_outputs << apply_function(layer_inputs, ActivationFunctions.method(@activation_func))
83
-
84
- # Apply dropout to hidden layers
85
- layer_outputs[-1] = apply_dropout(layer_outputs[-1], @dropout_rate) if i < @weights.size - 1
86
- end
58
+ def query(inputs)
59
+ validate_query_args(inputs)
87
60
 
88
- layer_outputs
61
+ layer_outputs = forward_pass(inputs)
62
+ layer_outputs.last.flatten
89
63
  end
90
64
 
91
- def backward_pass(inputs, targets, layer_outputs)
92
- derivative_func = "#{@activation_func}_derivative"
93
- inputs = inputs.map { |x| [x] } # Convert to column vector
94
- targets = targets.map { |x| [x] } # Convert to column vector
65
+ def calculate_loss(inputs, targets, loss_function = :cross_entropy_loss)
66
+ validate_calculate_loss_args(inputs, targets, loss_function)
95
67
 
96
- # Compute errors
97
- errors = [matrix_subtract(targets, layer_outputs.last)]
98
- (@weights.size - 1).downto(1) do |i|
99
- errors << matrix_multiply(transpose(@weights[i]), errors.last)
68
+ total_loss = 0.0
69
+ inputs.zip(targets).each do |input, target|
70
+ prediction = query(input)
71
+ total_loss += LossFunctions.send(loss_function, prediction, target)
100
72
  end
101
73
 
102
- # Compute weight deltas
103
- weight_deltas = []
104
- @weights.each_with_index do |w, i|
105
- gradients = matrix_multiply_element_wise(errors[i], apply_function(layer_outputs[i + 1], ActivationFunctions.method(derivative_func)))
106
- w_delta = matrix_multiply(gradients, transpose(layer_outputs[i]))
107
- weight_deltas << w_delta
108
- end
109
-
110
- weight_deltas
111
- end
112
-
113
- def query(inputs)
114
- layer_outputs = forward_pass(inputs)
115
- layer_outputs.last.flatten
74
+ total_loss / inputs.size
116
75
  end
117
76
  end
118
77
  end
@@ -0,0 +1,206 @@
1
+ require 'concurrent'
2
+ require_relative 'matrix_helpers'
3
+ require_relative 'err/validations'
4
+
5
+ module Qoa
6
+ module Training
7
+ include MatrixHelpers
8
+ include Err::Validations
9
+
10
+ def train(inputs, targets)
11
+ validate_train_args(inputs, targets)
12
+
13
+ inputs.zip(targets).each_slice(@batch_size) do |batch|
14
+ train_batch(batch)
15
+ end
16
+ end
17
+
18
+ def train_batch(batch)
19
+ derivative_func = "#{@activation_func}_derivative"
20
+ batch_inputs = batch.map { |x, _| x }
21
+ batch_targets = batch.map { |_, y| y }
22
+
23
+ # Forward pass
24
+ layer_outputs = batch_inputs.map { |inputs| forward_pass(inputs) }
25
+
26
+ # Backward pass
27
+ # Using thread pool to parallelize the backward pass for each input in the batch
28
+ pool = Concurrent::FixedThreadPool.new(4)
29
+ # weight_deltas = Array.new(@layers.size - 1) { |i| Array.new(@layers[i].output_size) { Array.new(@layers[i].input_size, 0) } }
30
+ weight_deltas = Array.new(@layers.size) { |i| Array.new(@layers[i].output_size) { Array.new(@layers[i].input_size, 0) } }
31
+ mutex = Mutex.new
32
+
33
+ batch.zip(layer_outputs).each do |(inputs, targets), outputs|
34
+ pool.post do
35
+ deltas = backward_pass(inputs, targets, outputs)
36
+ mutex.synchronize do
37
+ @layers.each_with_index do |_, i|
38
+ weight_deltas[i] = matrix_add(weight_deltas[i], deltas[i])
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ pool.shutdown
45
+ pool.wait_for_termination
46
+
47
+ # Update weights
48
+ @layers.each_with_index do |layer, i|
49
+ regularization_penalty = calculate_regularization_penalty(layer.weights, @l1_lambda, @l2_lambda)
50
+ layer.weights = matrix_add(layer.weights, scalar_multiply(@learning_rate / batch.size, matrix_add(weight_deltas[i], regularization_penalty)))
51
+ end
52
+ end
53
+
54
+ def train_with_early_stopping(inputs, targets, validation_inputs, validation_targets, max_epochs, patience)
55
+ best_validation_loss = Float::INFINITY
56
+ patience_left = patience
57
+ epoch = 0
58
+
59
+ while epoch < max_epochs && patience_left > 0
60
+ train(inputs, targets)
61
+ validation_loss = calculate_loss(validation_inputs, validation_targets)
62
+ puts "Epoch #{epoch + 1}: Validation loss = #{validation_loss}"
63
+
64
+ if validation_loss < best_validation_loss
65
+ best_validation_loss = validation_loss
66
+ save_model('best_model.json')
67
+ patience_left = patience
68
+ else
69
+ patience_left -= 1
70
+ end
71
+
72
+ epoch += 1
73
+ end
74
+
75
+ puts "Training stopped. Best validation loss = #{best_validation_loss}"
76
+ load_model('best_model.json')
77
+ end
78
+
79
+ def forward_pass(inputs)
80
+ inputs = inputs.map { |x| [x] } # Convert to column vector
81
+
82
+ layer_outputs = [inputs]
83
+ @layers.each_with_index do |layer, i|
84
+ if layer.is_a?(Qoa::Layers::ConvolutionalLayer)
85
+ layer_inputs = convolution(layer, layer_outputs[-1])
86
+ elsif layer.is_a?(Qoa::Layers::PoolingLayer)
87
+ layer_inputs = pooling(layer, layer_outputs[-1])
88
+ else
89
+ layer_inputs = matrix_multiply(layer.weights, layer_outputs[-1])
90
+ end
91
+
92
+ layer_outputs << apply_function(layer_inputs, ActivationFunctions.method(@activation_func))
93
+
94
+ # Apply dropout to hidden layers
95
+ layer_outputs[-1] = apply_dropout(layer_outputs[-1], @dropout_rate) if i < @layers.size - 2
96
+ end
97
+
98
+ layer_outputs
99
+ end
100
+
101
+ def backward_pass(inputs, targets, layer_outputs)
102
+ derivative_func = "#{@activation_func}_derivative"
103
+ inputs = inputs.map { |x| [x] } # Convert to column vector
104
+ targets = targets.map { |x| [x] } # Convert to column vector
105
+
106
+ # Compute errors
107
+ errors = [matrix_subtract(targets, layer_outputs.last)]
108
+ (@layers.size - 2).downto(0) do |i|
109
+ errors << matrix_multiply(transpose(@layers[i + 1].weights), errors.last)
110
+ end
111
+
112
+ # Compute weight deltas
113
+ weight_deltas = []
114
+ @layers.each_with_index do |layer, i|
115
+ gradients = matrix_multiply_element_wise(errors[i], apply_function(layer_outputs[i + 1], ActivationFunctions.method(derivative_func)))
116
+ if layer.is_a?(Qoa::Layers::ConvolutionalLayer)
117
+ w_delta = conv_weight_delta(layer, gradients, layer_outputs[i])
118
+ elsif layer.is_a?(Qoa::Layers::PoolingLayer)
119
+ w_delta = pool_weight_delta(layer, gradients, layer_outputs[i])
120
+ else
121
+ w_delta = matrix_multiply(gradients, transpose(layer_outputs[i]))
122
+ end
123
+ weight_deltas << w_delta
124
+ end
125
+
126
+ weight_deltas
127
+ end
128
+
129
+ def calculate_regularization_penalty(weights, l1_lambda, l2_lambda)
130
+ l1_penalty = weights.map do |row|
131
+ row.nil? ? nil : row.map { |x| x.nil? ? nil : (x < 0 ? -1 : 1) }
132
+ end
133
+ l1_penalty = scalar_multiply(l1_lambda, l1_penalty)
134
+
135
+ l2_penalty = scalar_multiply(l2_lambda, weights)
136
+
137
+ matrix_add(l1_penalty, l2_penalty)
138
+ end
139
+
140
+ def convolution(layer, inputs)
141
+ output_size = layer.output_size
142
+ kernel_size = layer.kernel_size
143
+ stride = layer.stride
144
+
145
+ output = Array.new(output_size) { Array.new(inputs.length - kernel_size + 1) }
146
+ layer.weights.each_with_index do |row, i|
147
+ inputs.each_cons(kernel_size).each_with_index do |input_slice, j|
148
+ output[i][j] = row.zip(input_slice).map { |a, b| a * b }.reduce(:+)
149
+ end
150
+ end
151
+
152
+ output
153
+ end
154
+
155
+ def pooling(layer, inputs)
156
+ output_size = layer.output_size
157
+ pool_size = layer.pool_size
158
+ stride = layer.stride || 1
159
+
160
+ # Calculate the number of columns in the output array
161
+ output_columns = inputs[0].length - pool_size
162
+ output_columns = output_columns <= 0 ? 1 : ((output_columns) / stride.to_f).ceil + 1
163
+
164
+ output = Array.new(output_size) { Array.new(output_columns) }
165
+
166
+ (0...output_size).each do |i|
167
+ (0...output_columns).each do |j|
168
+ start_idx = j * stride
169
+ end_idx = start_idx + pool_size - 1
170
+ next if inputs[i].nil? || inputs[i].length < end_idx + 1 # Add this check to avoid accessing an index that does not exist
171
+ pool_slice = inputs[i].slice(start_idx..end_idx)
172
+ output[i][j] = pool_slice.max
173
+ end
174
+ end
175
+
176
+ output
177
+ end
178
+
179
+ def conv_weight_delta(layer, gradients, inputs)
180
+ kernel_size = layer.kernel_size
181
+ stride = layer.stride
182
+
183
+ deltas = layer.weights.map do |row|
184
+ inputs.each_cons(kernel_size).map do |input_slice|
185
+ row.zip(input_slice).map { |a, b| a * b }.reduce(:+)
186
+ end
187
+ end
188
+
189
+ deltas
190
+ end
191
+
192
+ def pool_weight_delta(layer, gradients, inputs)
193
+ pool_size = layer.pool_size
194
+ stride = layer.stride
195
+
196
+ deltas = inputs.each_slice(stride).map do |input_slice|
197
+ input_slice.each_cons(pool_size).map do |pool_slice|
198
+ max_index = pool_slice.each_with_index.max[1]
199
+ pool_slice.map.with_index { |v, i| (i == max_index) ? v * gradients[i] : 0 }
200
+ end
201
+ end
202
+
203
+ deltas
204
+ end
205
+ end
206
+ end
data/lib/qoa/utils.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'json'
2
+
3
+ module Qoa
4
+ module Utils
5
+ def save_model(file_path)
6
+ model_data = {
7
+ input_nodes: @input_nodes,
8
+ hidden_layers: @hidden_layers,
9
+ output_nodes: @output_nodes,
10
+ learning_rate: @learning_rate,
11
+ activation_func: @activation_func,
12
+ dropout_rate: @dropout_rate,
13
+ decay_rate: @decay_rate,
14
+ epsilon: @epsilon,
15
+ batch_size: @batch_size,
16
+ weights: @layers.map(&:weights),
17
+ }
18
+
19
+ File.open(file_path, 'w') do |f|
20
+ f.write(JSON.pretty_generate(model_data))
21
+ end
22
+ end
23
+
24
+ def load_model(file_path)
25
+ model_data = JSON.parse(File.read(file_path), symbolize_names: true)
26
+
27
+ @input_nodes = model_data[:input_nodes]
28
+ @hidden_layers = model_data[:hidden_layers]
29
+ @output_nodes = model_data[:output_nodes]
30
+ @learning_rate = model_data[:learning_rate]
31
+ @activation_func = model_data[:activation_func].to_sym
32
+ @dropout_rate = model_data[:dropout_rate]
33
+ @decay_rate = model_data[:decay_rate]
34
+ @epsilon = model_data[:epsilon]
35
+ @batch_size = model_data[:batch_size]
36
+
37
+ @layers = model_data[:weights].map { |w| Qoa::Layers::Layer.new(w.first.size, w.size) }
38
+ @layers.each_with_index do |layer, i|
39
+ layer.weights = model_data[:weights][i]
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/qoa/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Qoa
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.4'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qoa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel M. Matongo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-29 00:00:00.000000000 Z
11
+ date: 2023-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: concurrent-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.1'
55
69
  description: Qoa is a simple machine learning library for Ruby, including a basic
56
70
  feedforward neural network implementation with backpropagation.
57
71
  email:
@@ -65,8 +79,15 @@ files:
65
79
  - code_of_conduct.md
66
80
  - lib/qoa.rb
67
81
  - lib/qoa/activation_functions.rb
82
+ - lib/qoa/err/validations.rb
83
+ - lib/qoa/layers/convolutional_layer.rb
84
+ - lib/qoa/layers/layer.rb
85
+ - lib/qoa/layers/pooling_layer.rb
86
+ - lib/qoa/loss_functions.rb
68
87
  - lib/qoa/matrix_helpers.rb
69
88
  - lib/qoa/neural_network.rb
89
+ - lib/qoa/training.rb
90
+ - lib/qoa/utils.rb
70
91
  - lib/qoa/version.rb
71
92
  homepage: https://github.com/mmatongo/qoa
72
93
  licenses: