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 +4 -4
- data/README.md +39 -7
- data/lib/qoa/err/validations.rb +27 -0
- data/lib/qoa/layers/convolutional_layer.rb +18 -0
- data/lib/qoa/layers/layer.rb +22 -0
- data/lib/qoa/layers/pooling_layer.rb +13 -0
- data/lib/qoa/loss_functions.rb +30 -0
- data/lib/qoa/matrix_helpers.rb +5 -1
- data/lib/qoa/neural_network.rb +48 -89
- data/lib/qoa/training.rb +206 -0
- data/lib/qoa/utils.rb +43 -0
- data/lib/qoa/version.rb +1 -1
- metadata +23 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8541feace7b5d8df6418672899fbba8289cc63624d7876c640a28e3143a450f3
|
4
|
+
data.tar.gz: 3944346b90a422a1ea843aef9b0a5bd18c8067e0bc075fa897daab0a1cf9373c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
|
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 [
|
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
|
data/lib/qoa/matrix_helpers.rb
CHANGED
@@ -24,7 +24,11 @@ module Qoa
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def apply_function(matrix, func)
|
27
|
-
matrix.map
|
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)
|
data/lib/qoa/neural_network.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
|
-
|
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 '
|
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
|
8
|
-
|
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
|
-
@
|
22
|
-
@
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
77
|
-
inputs
|
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
|
92
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
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
|
data/lib/qoa/training.rb
ADDED
@@ -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
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.
|
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-
|
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:
|