grx-tensor 0.1.0 → 0.2.0
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/CHANGELOG.md +17 -0
- data/GUIA_PRINCIPIANTES.md +763 -0
- data/README.es.md +270 -0
- data/README.md +178 -375
- data/grx-tensor.gemspec +4 -2
- data/lib/grx/c_api.rb +41 -37
- data/lib/grx/data.rb +87 -0
- data/lib/grx/loss.rb +55 -25
- data/lib/grx/nn.rb +153 -36
- data/lib/grx/optim.rb +9 -9
- data/lib/grx/serialization.rb +66 -0
- data/lib/grx/storage.rb +15 -24
- data/lib/grx/tensor.rb +198 -65
- data/lib/grx/utils.rb +28 -0
- data/lib/grx/version.rb +1 -1
- data/lib/grx.rb +14 -3
- metadata +8 -3
data/lib/grx/c_api.rb
CHANGED
|
@@ -7,49 +7,53 @@ module GRX
|
|
|
7
7
|
module CAPI
|
|
8
8
|
extend Fiddle::Importer
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
CANDIDATE_NAMES = case RUBY_PLATFORM
|
|
11
|
+
when /mingw|mswin|windows/i
|
|
12
|
+
["grx_core.dll", "libgrx_core.dll", "libgrx_core.so", "grx_core.so"]
|
|
13
|
+
when /darwin/i
|
|
14
|
+
["libgrx_core.dylib", "grx_core.bundle", "libgrx_core.so", "grx_core.so"]
|
|
15
|
+
else
|
|
16
|
+
["libgrx_core.so", "grx_core.so", "libgrx_core.dylib", "grx_core.dll"]
|
|
17
|
+
end
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
LIB_PATHS = [
|
|
25
|
-
# 1. make -C ext/unix → lib/grx/libgrx_core.so
|
|
26
|
-
File.expand_path(LIB_NAME, __dir__),
|
|
27
|
-
# 2. gem install (rake-compiler) → lib/grx_core.so (un nivel arriba)
|
|
28
|
-
File.expand_path("../#{RAKE_COMPILER_NAME}", __dir__),
|
|
29
|
-
# 3. gem install en Ruby versioned path → lib/ruby/X.X.X/grx_core.so
|
|
30
|
-
File.expand_path("../../#{RAKE_COMPILER_NAME}", __dir__),
|
|
31
|
-
# 4. desarrollo local sin instalar
|
|
32
|
-
File.expand_path("../../ext/grx/#{LIB_NAME}", __dir__),
|
|
19
|
+
SEARCH_DIRS = [
|
|
20
|
+
File.expand_path(__dir__), # lib/grx/
|
|
21
|
+
File.expand_path("..", __dir__), # lib/
|
|
22
|
+
File.expand_path("../../ext/grx", __dir__), # ext/grx/
|
|
23
|
+
File.expand_path("../../ext/unix", __dir__), # ext/unix/
|
|
24
|
+
File.expand_path("../../ext/windows", __dir__) # ext/windows/
|
|
33
25
|
].freeze
|
|
34
26
|
|
|
27
|
+
LIB_PATHS = SEARCH_DIRS.flat_map do |dir|
|
|
28
|
+
CANDIDATE_NAMES.flat_map do |name|
|
|
29
|
+
[
|
|
30
|
+
File.join(dir, name),
|
|
31
|
+
File.join(dir, name).tr("/", "\\")
|
|
32
|
+
]
|
|
33
|
+
end
|
|
34
|
+
end.uniq.freeze
|
|
35
|
+
|
|
35
36
|
LOADED = begin
|
|
36
|
-
path = LIB_PATHS.find { |p| File.exist?(p) }
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
path = LIB_PATHS.find { |p| File.file?(p) && File.exist?(p) }
|
|
38
|
+
if path
|
|
39
|
+
dlload path
|
|
40
|
+
true
|
|
41
|
+
else
|
|
42
|
+
raise Fiddle::DLError, "Binary library not found (#{CANDIDATE_NAMES.join(', ')}) in #{SEARCH_DIRS.inspect}"
|
|
43
|
+
end
|
|
40
44
|
rescue Fiddle::DLError => e
|
|
41
|
-
warn "[GRX]
|
|
42
|
-
" →
|
|
43
|
-
" →
|
|
45
|
+
warn "[GRX] C extension unavailable: #{e.message}\n" \
|
|
46
|
+
" → Run: make -C ext/unix all (Linux/macOS) or make -C ext/windows -f Makefile.mingw all (Windows)\n" \
|
|
47
|
+
" → Running in pure Ruby fallback mode (without SIMD)."
|
|
44
48
|
false
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
if LOADED
|
|
48
|
-
#
|
|
52
|
+
# Memory management
|
|
49
53
|
extern "double* grx_alloc(size_t)"
|
|
50
54
|
extern "void grx_free(double*)"
|
|
51
55
|
|
|
52
|
-
#
|
|
56
|
+
# Element-wise arithmetic
|
|
53
57
|
extern "void grx_add (double*, double*, double*, size_t)"
|
|
54
58
|
extern "void grx_sub (double*, double*, double*, size_t)"
|
|
55
59
|
extern "void grx_mul (double*, double*, double*, size_t)"
|
|
@@ -58,7 +62,7 @@ module GRX
|
|
|
58
62
|
extern "void grx_add_scalar(double*, double, double*, size_t)"
|
|
59
63
|
extern "void grx_negate (double*, double*, size_t)"
|
|
60
64
|
|
|
61
|
-
#
|
|
65
|
+
# Element-wise math
|
|
62
66
|
extern "void grx_abs (double*, double*, size_t)"
|
|
63
67
|
extern "void grx_sqrt (double*, double*, size_t)"
|
|
64
68
|
extern "void grx_square(double*, double*, size_t)"
|
|
@@ -67,28 +71,28 @@ module GRX
|
|
|
67
71
|
extern "void grx_pow (double*, double, double*, size_t)"
|
|
68
72
|
extern "void grx_clip (double*, double, double, double*, size_t)"
|
|
69
73
|
|
|
70
|
-
#
|
|
74
|
+
# Reductions
|
|
71
75
|
extern "double grx_sum (double*, size_t)"
|
|
72
76
|
extern "double grx_mean(double*, size_t)"
|
|
73
77
|
extern "double grx_max (double*, size_t)"
|
|
74
78
|
extern "double grx_min (double*, size_t)"
|
|
75
79
|
|
|
76
|
-
#
|
|
80
|
+
# Linear algebra
|
|
77
81
|
extern "double grx_dot (double*, double*, size_t)"
|
|
78
82
|
extern "void grx_matmul (double*, double*, double*, size_t, size_t, size_t)"
|
|
79
83
|
|
|
80
|
-
#
|
|
84
|
+
# Activations
|
|
81
85
|
extern "void grx_relu (double*, double*, size_t)"
|
|
82
86
|
extern "void grx_leaky_relu (double*, double, double*, size_t)"
|
|
83
87
|
extern "void grx_tanh_act (double*, double*, size_t)"
|
|
84
88
|
extern "void grx_sigmoid (double*, double*, size_t)"
|
|
85
89
|
extern "void grx_softmax (double*, double*, size_t)"
|
|
86
90
|
|
|
87
|
-
#
|
|
91
|
+
# Optimizers
|
|
88
92
|
extern "void grx_sgd_step (double*, double*, double, size_t)"
|
|
89
93
|
extern "void grx_adam_step(double*, double*, double*, double*, double, double, double, double, double, double, size_t)"
|
|
90
94
|
|
|
91
|
-
#
|
|
95
|
+
# Weight initialization
|
|
92
96
|
extern "void grx_init_xavier_uniform(double*, size_t, size_t, size_t)"
|
|
93
97
|
extern "void grx_init_he_normal (double*, size_t, size_t)"
|
|
94
98
|
end
|
data/lib/grx/data.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GRX
|
|
4
|
+
module Data
|
|
5
|
+
# ================================================================
|
|
6
|
+
# Dataset — Base class for data collections
|
|
7
|
+
# ================================================================
|
|
8
|
+
class Dataset
|
|
9
|
+
def size
|
|
10
|
+
raise NotImplementedError, "#{self.class}#size must be implemented"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def [](index)
|
|
14
|
+
raise NotImplementedError, "#{self.class}#[] must be implemented"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# ================================================================
|
|
19
|
+
# TensorDataset — Dataset wrapping parallel tensors (e.g. X and Y)
|
|
20
|
+
# ================================================================
|
|
21
|
+
class TensorDataset < Dataset
|
|
22
|
+
attr_reader :tensors, :size
|
|
23
|
+
|
|
24
|
+
def initialize(*tensors)
|
|
25
|
+
raise ArgumentError, "Must provide at least one tensor" if tensors.empty?
|
|
26
|
+
first_dim = tensors.first.shape[0]
|
|
27
|
+
unless tensors.all? { |t| t.shape[0] == first_dim }
|
|
28
|
+
raise ArgumentError, "All tensors must have the same size in batch dimension (dimension 0)"
|
|
29
|
+
end
|
|
30
|
+
@tensors = tensors
|
|
31
|
+
@size = first_dim
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def [](index)
|
|
35
|
+
@tensors.map do |t|
|
|
36
|
+
cols = t.numel / @size
|
|
37
|
+
offset = index * cols
|
|
38
|
+
data = t.to_a.slice(offset, cols)
|
|
39
|
+
new_shape = t.shape.size == 1 ? [1] : [1] + t.shape[1..]
|
|
40
|
+
Tensor.create(data, new_shape)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ================================================================
|
|
46
|
+
# DataLoader — Mini-batch iterator with optional shuffling
|
|
47
|
+
# ================================================================
|
|
48
|
+
class DataLoader
|
|
49
|
+
include Enumerable
|
|
50
|
+
|
|
51
|
+
attr_reader :dataset, :batch_size, :shuffle
|
|
52
|
+
|
|
53
|
+
def initialize(dataset, batch_size: 32, shuffle: true)
|
|
54
|
+
@dataset = dataset
|
|
55
|
+
@batch_size = batch_size
|
|
56
|
+
@shuffle = shuffle
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def each
|
|
60
|
+
return to_enum(:each) unless block_given?
|
|
61
|
+
|
|
62
|
+
indices = (0...@dataset.size).to_a
|
|
63
|
+
indices.shuffle! if @shuffle
|
|
64
|
+
|
|
65
|
+
indices.each_slice(@batch_size) do |batch_indices|
|
|
66
|
+
batch_samples = batch_indices.map { |i| @dataset[i] }
|
|
67
|
+
num_tensors = batch_samples.first.size
|
|
68
|
+
|
|
69
|
+
batched = (0...num_tensors).map do |t_idx|
|
|
70
|
+
slices = batch_samples.map { |sample| sample[t_idx].to_a }
|
|
71
|
+
flat_data = slices.flatten
|
|
72
|
+
sample_shape = batch_samples.first[t_idx].shape
|
|
73
|
+
batch_dim = batch_indices.size
|
|
74
|
+
target_shape = [batch_dim] + sample_shape[1..]
|
|
75
|
+
Tensor.create(flat_data, target_shape)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
yield(*batched)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def size
|
|
83
|
+
(@dataset.size.to_f / @batch_size).ceil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/grx/loss.rb
CHANGED
|
@@ -4,11 +4,11 @@ module GRX
|
|
|
4
4
|
module Loss
|
|
5
5
|
# ================================================================
|
|
6
6
|
# MSELoss — Mean Squared Error
|
|
7
|
-
# L = mean((pred - target)^2)
|
|
7
|
+
# L = mean((pred - target)^2) -> returns differentiable scalar Tensor
|
|
8
8
|
# ================================================================
|
|
9
9
|
class MSELoss
|
|
10
10
|
def call(pred, target)
|
|
11
|
-
raise ShapeError, "
|
|
11
|
+
raise ShapeError, "Incompatible shapes: #{pred.shape} vs #{target.shape}" if pred.shape != target.shape
|
|
12
12
|
(pred - target).square.mean
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -19,7 +19,7 @@ module GRX
|
|
|
19
19
|
# ================================================================
|
|
20
20
|
class MAELoss
|
|
21
21
|
def call(pred, target)
|
|
22
|
-
raise ShapeError, "
|
|
22
|
+
raise ShapeError, "Incompatible shapes: #{pred.shape} vs #{target.shape}" if pred.shape != target.shape
|
|
23
23
|
(pred - target).abs.mean
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -27,54 +27,84 @@ module GRX
|
|
|
27
27
|
# ================================================================
|
|
28
28
|
# BCELoss — Binary Cross-Entropy
|
|
29
29
|
# L = -mean(t*log(p) + (1-t)*log(1-p))
|
|
30
|
-
# pred
|
|
30
|
+
# pred must be in (0,1) — apply Sigmoid before if using logits.
|
|
31
31
|
# ================================================================
|
|
32
32
|
class BCELoss
|
|
33
33
|
EPS = 1e-7
|
|
34
34
|
|
|
35
35
|
def call(pred, target)
|
|
36
|
-
raise ShapeError, "
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-(t * Math.log(p) + (1 - t) * Math.log(1 - p))
|
|
43
|
-
end
|
|
44
|
-
loss / total
|
|
36
|
+
raise ShapeError, "Incompatible shapes: #{pred.shape} vs #{target.shape}" if pred.shape != target.shape
|
|
37
|
+
p_clamped = pred.clip(EPS, 1.0 - EPS)
|
|
38
|
+
ones = Tensor.ones_like(target)
|
|
39
|
+
term1 = target * p_clamped.log
|
|
40
|
+
term2 = (ones - target) * (ones - p_clamped).log
|
|
41
|
+
(-(term1 + term2)).mean
|
|
45
42
|
end
|
|
46
43
|
end
|
|
47
44
|
|
|
48
45
|
# ================================================================
|
|
49
|
-
# CrossEntropyLoss — Softmax + NLL
|
|
50
|
-
# L = -
|
|
46
|
+
# CrossEntropyLoss — Softmax + NLL (multi-class)
|
|
47
|
+
# L = -sum(target * log(softmax(logits))) / batch_size
|
|
51
48
|
# ================================================================
|
|
52
49
|
class CrossEntropyLoss
|
|
53
50
|
EPS = 1e-7
|
|
54
51
|
|
|
55
52
|
def call(logits, target)
|
|
56
|
-
raise ShapeError, "
|
|
57
|
-
probs
|
|
53
|
+
raise ShapeError, "Incompatible shapes: #{logits.shape} vs #{target.shape}" if logits.shape != target.shape
|
|
54
|
+
probs = logits.softmax
|
|
55
|
+
p_data = probs.to_a
|
|
58
56
|
t_data = target.to_a
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
batch_size = logits.shape[0].to_f
|
|
58
|
+
|
|
59
|
+
loss_val = t_data.each_with_index.sum do |t, i|
|
|
60
|
+
next 0.0 if t == 0.0
|
|
61
|
+
p = [p_data[i], EPS].max
|
|
62
|
+
-t * Math.log(p)
|
|
63
|
+
end / batch_size
|
|
64
|
+
|
|
65
|
+
out = Tensor.create([loss_val], [1], requires_grad: logits.requires_grad || target.requires_grad)
|
|
66
|
+
if logits.requires_grad || target.requires_grad
|
|
67
|
+
out._grafo_hijos.push(logits, target)
|
|
68
|
+
out.backward_fn = ->(g) {
|
|
69
|
+
scale = g.item / batch_size
|
|
70
|
+
grad_logits = p_data.zip(t_data).map { |p, t| (p - t) * scale }
|
|
71
|
+
logits.agregar_gradiente(Tensor.create(grad_logits, logits.shape)) if logits.requires_grad
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
out
|
|
61
75
|
end
|
|
62
76
|
end
|
|
63
77
|
|
|
64
78
|
# ================================================================
|
|
65
|
-
# HuberLoss — Smooth L1
|
|
79
|
+
# HuberLoss — Smooth L1 (robust against outliers)
|
|
66
80
|
# ================================================================
|
|
67
81
|
class HuberLoss
|
|
82
|
+
attr_reader :delta
|
|
83
|
+
|
|
68
84
|
def initialize(delta: 1.0)
|
|
69
|
-
@delta = delta
|
|
85
|
+
@delta = delta.to_f
|
|
70
86
|
end
|
|
71
87
|
|
|
72
88
|
def call(pred, target)
|
|
73
|
-
raise ShapeError, "Shapes incompatibles" if pred.shape != target.shape
|
|
89
|
+
raise ShapeError, "Shapes incompatibles: #{pred.shape} vs #{target.shape}" if pred.shape != target.shape
|
|
90
|
+
diff_data = (pred - target).abs.to_a
|
|
74
91
|
d = @delta
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
loss_val = diff_data.sum { |v| v <= d ? 0.5 * v * v : d * (v - 0.5 * d) } / diff_data.size.to_f
|
|
93
|
+
|
|
94
|
+
out = Tensor.create([loss_val], [1], requires_grad: pred.requires_grad || target.requires_grad)
|
|
95
|
+
if pred.requires_grad || target.requires_grad
|
|
96
|
+
out._grafo_hijos.push(pred, target)
|
|
97
|
+
n = diff_data.size.to_f
|
|
98
|
+
out.backward_fn = ->(g) {
|
|
99
|
+
grad_pred = (pred - target).to_a.map do |err|
|
|
100
|
+
abs_err = err.abs
|
|
101
|
+
(abs_err <= d ? err : d * (err > 0 ? 1.0 : -1.0)) * (g.item / n)
|
|
102
|
+
end
|
|
103
|
+
pred.agregar_gradiente(Tensor.create(grad_pred, pred.shape)) if pred.requires_grad
|
|
104
|
+
target.agregar_gradiente(Tensor.create(grad_pred.map(&:-@), target.shape)) if target.requires_grad
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
out
|
|
78
108
|
end
|
|
79
109
|
end
|
|
80
110
|
end
|
data/lib/grx/nn.rb
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
module GRX
|
|
4
4
|
module NN
|
|
5
5
|
# ================================================================
|
|
6
|
-
# Module —
|
|
6
|
+
# Module — Base class for all neural network layers
|
|
7
7
|
# ================================================================
|
|
8
8
|
class Module
|
|
9
|
-
#
|
|
9
|
+
# Returns all trainable parameters for optimizer registration
|
|
10
10
|
def parameters
|
|
11
11
|
instance_variables.flat_map do |var|
|
|
12
12
|
val = instance_variable_get(var)
|
|
@@ -29,14 +29,22 @@ module GRX
|
|
|
29
29
|
parameters.each(&:zero_grad!)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
def save_weights(path)
|
|
33
|
+
GRX::Serialization.save(self, path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load_weights(path)
|
|
37
|
+
GRX::Serialization.load(self, path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Subclasses implement forward computation
|
|
33
41
|
def call(*args)
|
|
34
42
|
forward(*args)
|
|
35
43
|
end
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# ================================================================
|
|
39
|
-
# Linear —
|
|
47
|
+
# Linear — Dense fully connected layer
|
|
40
48
|
# y = x @ W^T + b
|
|
41
49
|
# ================================================================
|
|
42
50
|
class Linear < Module
|
|
@@ -47,25 +55,20 @@ module GRX
|
|
|
47
55
|
@out_features = out_features
|
|
48
56
|
@use_bias = bias
|
|
49
57
|
|
|
50
|
-
#
|
|
58
|
+
# Weights: Xavier uniform initialization
|
|
51
59
|
@weight = Tensor.xavier_uniform([out_features, in_features], requires_grad: true)
|
|
52
60
|
|
|
53
|
-
# Bias:
|
|
61
|
+
# Bias: initialized to zeros
|
|
54
62
|
@bias = bias ? Tensor.zeros([out_features], requires_grad: true) : nil
|
|
55
63
|
end
|
|
56
64
|
|
|
57
65
|
def forward(x)
|
|
58
|
-
# x: [batch, in_features]
|
|
66
|
+
# x: [batch, in_features] -> out: [batch, out_features]
|
|
59
67
|
# out = x @ W^T
|
|
60
68
|
out = x.matmul(@weight.transpose)
|
|
61
69
|
|
|
62
70
|
if @use_bias
|
|
63
|
-
# Sumamos bias fila por fila.
|
|
64
|
-
# Repetimos @bias batch_size veces para crear un tensor [batch, out_features]
|
|
65
|
-
# que comparte el grafo con @bias original.
|
|
66
71
|
batch_size = x.shape[0]
|
|
67
|
-
# Tile del bias: concatenamos el mismo tensor bias_size veces
|
|
68
|
-
# usando operaciones que mantienen el grafo conectado
|
|
69
72
|
bias_tiled = _tile_bias(@bias, batch_size, @out_features)
|
|
70
73
|
out + bias_tiled
|
|
71
74
|
else
|
|
@@ -75,17 +78,11 @@ module GRX
|
|
|
75
78
|
|
|
76
79
|
private
|
|
77
80
|
|
|
78
|
-
# Crea un tensor [batch, out_features] repitiendo @bias batch veces.
|
|
79
|
-
# Usa add_scalar(0) para crear un nuevo nodo conectado al bias en el grafo.
|
|
80
81
|
def _tile_bias(bias, batch_size, out_features)
|
|
81
|
-
# Construimos el tensor tileado sumando el bias a un tensor de ceros
|
|
82
|
-
# de la forma correcta. Esto conecta el grafo al bias original.
|
|
83
82
|
data = Array.new(batch_size) { bias.to_a }.flatten
|
|
84
83
|
tiled = GRX::Tensor.create(data, [batch_size, out_features])
|
|
85
|
-
# Conectamos al bias original via suma con ceros — mantiene el grafo
|
|
86
84
|
zero_row = GRX::Tensor.zeros([batch_size, out_features])
|
|
87
85
|
result = zero_row + tiled
|
|
88
|
-
# Registramos manualmente la conexión al bias para backprop
|
|
89
86
|
if bias.requires_grad
|
|
90
87
|
result.requires_grad = true
|
|
91
88
|
result._grafo_hijos << bias
|
|
@@ -93,8 +90,7 @@ module GRX
|
|
|
93
90
|
bf = result.backward_fn
|
|
94
91
|
result.backward_fn = ->(g) {
|
|
95
92
|
bf&.call(g)
|
|
96
|
-
|
|
97
|
-
grad_data = g.to_a.each_slice(out_features).reduce([0.0]*out_features) { |acc, row|
|
|
93
|
+
grad_data = g.to_a.each_slice(out_features).reduce([0.0] * out_features) { |acc, row|
|
|
98
94
|
acc.zip(row).map { |a, r| a + r }
|
|
99
95
|
}
|
|
100
96
|
b.agregar_gradiente(GRX::Tensor.create(grad_data, [out_features]))
|
|
@@ -106,12 +102,12 @@ module GRX
|
|
|
106
102
|
public
|
|
107
103
|
|
|
108
104
|
def to_s
|
|
109
|
-
"Linear(#{@in_features}
|
|
105
|
+
"Linear(#{@in_features} -> #{@out_features}, bias: #{@use_bias})"
|
|
110
106
|
end
|
|
111
107
|
end
|
|
112
108
|
|
|
113
109
|
# ================================================================
|
|
114
|
-
# Sequential —
|
|
110
|
+
# Sequential — Chains layers sequentially in order
|
|
115
111
|
# ================================================================
|
|
116
112
|
class Sequential < Module
|
|
117
113
|
def initialize(*layers)
|
|
@@ -126,6 +122,16 @@ module GRX
|
|
|
126
122
|
@layers.flat_map(&:parameters)
|
|
127
123
|
end
|
|
128
124
|
|
|
125
|
+
def train!
|
|
126
|
+
@layers.each { |l| l.train! if l.respond_to?(:train!) }
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def eval!
|
|
131
|
+
@layers.each { |l| l.eval! if l.respond_to?(:eval!) }
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
129
135
|
def to_s
|
|
130
136
|
layers_str = @layers.each_with_index.map { |l, i| " (#{i}): #{l}" }.join("\n")
|
|
131
137
|
"Sequential(\n#{layers_str}\n)"
|
|
@@ -133,7 +139,7 @@ module GRX
|
|
|
133
139
|
end
|
|
134
140
|
|
|
135
141
|
# ================================================================
|
|
136
|
-
#
|
|
142
|
+
# Activation layers (for use inside Sequential pipelines)
|
|
137
143
|
# ================================================================
|
|
138
144
|
class ReLU < Module
|
|
139
145
|
def forward(x) = x.relu
|
|
@@ -164,11 +170,11 @@ module GRX
|
|
|
164
170
|
end
|
|
165
171
|
|
|
166
172
|
# ================================================================
|
|
167
|
-
# Dropout —
|
|
173
|
+
# Dropout — Random feature dropout during training mode
|
|
168
174
|
# ================================================================
|
|
169
175
|
class Dropout < Module
|
|
170
176
|
def initialize(p = 0.5)
|
|
171
|
-
@p
|
|
177
|
+
@p = p
|
|
172
178
|
@training = true
|
|
173
179
|
end
|
|
174
180
|
|
|
@@ -178,8 +184,6 @@ module GRX
|
|
|
178
184
|
def forward(x)
|
|
179
185
|
return x unless @training && @p > 0
|
|
180
186
|
|
|
181
|
-
# Máscara binaria: 1 con prob (1-p), 0 con prob p
|
|
182
|
-
# Escalamos por 1/(1-p) para mantener la esperanza (inverted dropout)
|
|
183
187
|
scale = 1.0 / (1.0 - @p)
|
|
184
188
|
mask_data = x.to_a.map { rand > @p ? scale : 0.0 }
|
|
185
189
|
mask = Tensor.create(mask_data, x.shape)
|
|
@@ -190,8 +194,128 @@ module GRX
|
|
|
190
194
|
end
|
|
191
195
|
|
|
192
196
|
# ================================================================
|
|
193
|
-
#
|
|
194
|
-
#
|
|
197
|
+
# Embedding — Dense vector lookup table for token indices
|
|
198
|
+
# ================================================================
|
|
199
|
+
class Embedding < Module
|
|
200
|
+
attr_reader :weight, :num_embeddings, :embedding_dim
|
|
201
|
+
|
|
202
|
+
def initialize(num_embeddings, embedding_dim)
|
|
203
|
+
@num_embeddings = num_embeddings
|
|
204
|
+
@embedding_dim = embedding_dim
|
|
205
|
+
@weight = Tensor.he_normal([num_embeddings, embedding_dim], requires_grad: true)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def forward(indices)
|
|
209
|
+
ids = indices.is_a?(Tensor) ? indices.to_a.map(&:to_i) : indices.map(&:to_i)
|
|
210
|
+
batch_size = ids.size
|
|
211
|
+
out_data = ids.flat_map do |id|
|
|
212
|
+
raise IndexError, "Token index #{id} out of range [0, #{@num_embeddings})" if id < 0 || id >= @num_embeddings
|
|
213
|
+
@weight.to_a.slice(id * @embedding_dim, @embedding_dim)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
out = Tensor.create(out_data, [batch_size, @embedding_dim])
|
|
217
|
+
if @weight.requires_grad
|
|
218
|
+
out.requires_grad = true
|
|
219
|
+
out._grafo_hijos << @weight
|
|
220
|
+
w = @weight; dim = @embedding_dim; num_emb = @num_embeddings
|
|
221
|
+
out.backward_fn = ->(g) {
|
|
222
|
+
grad_w = Array.new(num_emb * dim, 0.0)
|
|
223
|
+
g_data = g.to_a
|
|
224
|
+
ids.each_with_index do |id, i|
|
|
225
|
+
slice = g_data.slice(i * dim, dim)
|
|
226
|
+
dim.times { |d| grad_w[id * dim + d] += slice[d] }
|
|
227
|
+
end
|
|
228
|
+
w.agregar_gradiente(Tensor.create(grad_w, w.shape))
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
out
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def to_s
|
|
235
|
+
"Embedding(#{@num_embeddings}, #{@embedding_dim})"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# ================================================================
|
|
240
|
+
# LayerNorm — Layer normalization across channel dimensions
|
|
241
|
+
# ================================================================
|
|
242
|
+
class LayerNorm < Module
|
|
243
|
+
attr_reader :gamma, :beta, :normalized_shape, :epsilon
|
|
244
|
+
|
|
245
|
+
def initialize(normalized_shape, epsilon: 1e-5)
|
|
246
|
+
@normalized_shape = normalized_shape.is_a?(Array) ? normalized_shape : [normalized_shape]
|
|
247
|
+
@dim = @normalized_shape.reduce(1, :*)
|
|
248
|
+
@epsilon = epsilon
|
|
249
|
+
|
|
250
|
+
@gamma = Tensor.ones(@normalized_shape, requires_grad: true)
|
|
251
|
+
@beta = Tensor.zeros(@normalized_shape, requires_grad: true)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def forward(x)
|
|
255
|
+
batch_size = x.shape[0]
|
|
256
|
+
x_data = x.to_a
|
|
257
|
+
|
|
258
|
+
means = Array.new(batch_size) do |b|
|
|
259
|
+
x_data.slice(b * @dim, @dim).sum / @dim.to_f
|
|
260
|
+
end
|
|
261
|
+
vars = Array.new(batch_size) do |b|
|
|
262
|
+
m = means[b]
|
|
263
|
+
x_data.slice(b * @dim, @dim).sum { |v| (v - m)**2 } / @dim.to_f
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
gamma_data = @gamma.to_a
|
|
267
|
+
beta_data = @beta.to_a
|
|
268
|
+
norm_data = Array.new(batch_size * @dim)
|
|
269
|
+
|
|
270
|
+
batch_size.times do |b|
|
|
271
|
+
m = means[b]
|
|
272
|
+
inv_std = 1.0 / Math.sqrt(vars[b] + @epsilon)
|
|
273
|
+
@dim.times do |j|
|
|
274
|
+
norm_data[b * @dim + j] = gamma_data[j] * (x_data[b * @dim + j] - m) * inv_std + beta_data[j]
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
out = Tensor.create(norm_data, x.shape)
|
|
279
|
+
if x.requires_grad || @gamma.requires_grad || @beta.requires_grad
|
|
280
|
+
out.requires_grad = true
|
|
281
|
+
out._grafo_hijos.push(x, @gamma, @beta)
|
|
282
|
+
g_param = @gamma; b_param = @beta; d = @dim; eps = @epsilon
|
|
283
|
+
out.backward_fn = ->(g) {
|
|
284
|
+
g_data = g.to_a
|
|
285
|
+
grad_gamma = Array.new(d, 0.0)
|
|
286
|
+
grad_beta = Array.new(d, 0.0)
|
|
287
|
+
grad_x = Array.new(batch_size * d, 0.0)
|
|
288
|
+
|
|
289
|
+
batch_size.times do |b|
|
|
290
|
+
m = means[b]; v = vars[b]
|
|
291
|
+
inv_std = 1.0 / Math.sqrt(v + eps)
|
|
292
|
+
x_hat = Array.new(d) { |j| (x_data[b * d + j] - m) * inv_std }
|
|
293
|
+
dl_dxhat = Array.new(d) { |j| g_data[b * d + j] * gamma_data[j] }
|
|
294
|
+
sum_dl = dl_dxhat.sum
|
|
295
|
+
sum_dl_x = dl_dxhat.zip(x_hat).sum { |a, c| a * c }
|
|
296
|
+
|
|
297
|
+
d.times do |j|
|
|
298
|
+
grad_gamma[j] += g_data[b * d + j] * x_hat[j]
|
|
299
|
+
grad_beta[j] += g_data[b * d + j]
|
|
300
|
+
grad_x[b * d + j] = (inv_std / d.to_f) * (d.to_f * dl_dxhat[j] - sum_dl - x_hat[j] * sum_dl_x)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
g_param.agregar_gradiente(Tensor.create(grad_gamma, g_param.shape)) if g_param.requires_grad
|
|
305
|
+
b_param.agregar_gradiente(Tensor.create(grad_beta, b_param.shape)) if b_param.requires_grad
|
|
306
|
+
x.agregar_gradiente(Tensor.create(grad_x, x.shape)) if x.requires_grad
|
|
307
|
+
}
|
|
308
|
+
end
|
|
309
|
+
out
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def to_s
|
|
313
|
+
"LayerNorm(#{@normalized_shape})"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# ================================================================
|
|
318
|
+
# BatchNorm1d — Normalizacion por batch
|
|
195
319
|
# ================================================================
|
|
196
320
|
class BatchNorm1d < Module
|
|
197
321
|
def initialize(num_features, epsilon: 1e-5, momentum: 0.1)
|
|
@@ -200,11 +324,9 @@ module GRX
|
|
|
200
324
|
@momentum = momentum
|
|
201
325
|
@training = true
|
|
202
326
|
|
|
203
|
-
# Parámetros entrenables
|
|
204
327
|
@gamma = Tensor.ones([num_features], requires_grad: true)
|
|
205
328
|
@beta = Tensor.zeros([num_features], requires_grad: true)
|
|
206
329
|
|
|
207
|
-
# Estadísticas corrientes (no entrenables, para inferencia)
|
|
208
330
|
@running_mean = Tensor.zeros([num_features])
|
|
209
331
|
@running_var = Tensor.ones([num_features])
|
|
210
332
|
end
|
|
@@ -213,11 +335,9 @@ module GRX
|
|
|
213
335
|
def eval!; @training = false; self; end
|
|
214
336
|
|
|
215
337
|
def forward(x)
|
|
216
|
-
# x: [batch, num_features]
|
|
217
338
|
batch_size = x.shape[0]
|
|
218
339
|
|
|
219
340
|
if @training
|
|
220
|
-
# Calculamos media y varianza del batch
|
|
221
341
|
batch_data = x.to_a
|
|
222
342
|
means = Array.new(@num_features) do |j|
|
|
223
343
|
batch_data.each_slice(@num_features).map { |row| row[j] }.sum / batch_size
|
|
@@ -227,7 +347,6 @@ module GRX
|
|
|
227
347
|
col.sum { |v| (v - means[j]) ** 2 } / batch_size
|
|
228
348
|
end
|
|
229
349
|
|
|
230
|
-
# Actualizamos estadísticas corrientes
|
|
231
350
|
means.each_with_index do |m, j|
|
|
232
351
|
rm = @running_mean.to_a; rm[j] = (1 - @momentum) * rm[j] + @momentum * m
|
|
233
352
|
@running_mean = Tensor.create(rm, [@num_features])
|
|
@@ -244,8 +363,6 @@ module GRX
|
|
|
244
363
|
var_t = @running_var
|
|
245
364
|
end
|
|
246
365
|
|
|
247
|
-
# Normalizamos: x_hat = (x - mean) / sqrt(var + eps)
|
|
248
|
-
# Luego escalamos: y = gamma * x_hat + beta
|
|
249
366
|
norm_data = x.to_a.each_slice(@num_features).flat_map do |row|
|
|
250
367
|
row.each_with_index.map do |v, j|
|
|
251
368
|
x_hat = (v - mean_t.to_a[j]) / Math.sqrt(var_t.to_a[j] + @epsilon)
|