grx-tensor 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/GUIA_PRINCIPIANTES.md +1046 -0
- data/README.es.md +1199 -0
- data/README.md +1026 -283
- data/ext/grx/extconf.rb +4 -18
- data/ext/grx/grx_core.c +411 -331
- data/ext/grx/grx_core.h +23 -13
- data/ext/unix/Makefile +3 -27
- data/ext/windows/Makefile.mingw +3 -23
- data/grx-tensor.gemspec +41 -35
- data/lib/grx/c_api.rb +75 -39
- data/lib/grx/data.rb +87 -0
- data/lib/grx/loss.rb +55 -25
- data/lib/grx/nn.rb +160 -41
- data/lib/grx/optim.rb +21 -16
- data/lib/grx/serialization.rb +66 -0
- data/lib/grx/storage.rb +19 -27
- data/lib/grx/tensor.rb +234 -65
- data/lib/grx/utils.rb +43 -0
- data/lib/grx/version.rb +1 -1
- data/lib/grx.rb +22 -6
- metadata +37 -28
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
|
|
@@ -141,8 +147,10 @@ module GRX
|
|
|
141
147
|
end
|
|
142
148
|
|
|
143
149
|
class LeakyReLU < Module
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
attr_reader :alpha
|
|
151
|
+
|
|
152
|
+
def initialize(alpha_arg = nil, alpha: nil)
|
|
153
|
+
@alpha = (alpha || alpha_arg || 0.01).to_f
|
|
146
154
|
end
|
|
147
155
|
def forward(x) = x.leaky_relu(@alpha)
|
|
148
156
|
def to_s = "LeakyReLU(alpha=#{@alpha})"
|
|
@@ -164,11 +172,11 @@ module GRX
|
|
|
164
172
|
end
|
|
165
173
|
|
|
166
174
|
# ================================================================
|
|
167
|
-
# Dropout —
|
|
175
|
+
# Dropout — Random feature dropout during training mode
|
|
168
176
|
# ================================================================
|
|
169
177
|
class Dropout < Module
|
|
170
178
|
def initialize(p = 0.5)
|
|
171
|
-
@p
|
|
179
|
+
@p = p
|
|
172
180
|
@training = true
|
|
173
181
|
end
|
|
174
182
|
|
|
@@ -178,8 +186,6 @@ module GRX
|
|
|
178
186
|
def forward(x)
|
|
179
187
|
return x unless @training && @p > 0
|
|
180
188
|
|
|
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
189
|
scale = 1.0 / (1.0 - @p)
|
|
184
190
|
mask_data = x.to_a.map { rand > @p ? scale : 0.0 }
|
|
185
191
|
mask = Tensor.create(mask_data, x.shape)
|
|
@@ -190,21 +196,139 @@ module GRX
|
|
|
190
196
|
end
|
|
191
197
|
|
|
192
198
|
# ================================================================
|
|
193
|
-
#
|
|
194
|
-
#
|
|
199
|
+
# Embedding — Dense vector lookup table for token indices
|
|
200
|
+
# ================================================================
|
|
201
|
+
class Embedding < Module
|
|
202
|
+
attr_reader :weight, :num_embeddings, :embedding_dim
|
|
203
|
+
|
|
204
|
+
def initialize(num_embeddings, embedding_dim)
|
|
205
|
+
@num_embeddings = num_embeddings
|
|
206
|
+
@embedding_dim = embedding_dim
|
|
207
|
+
@weight = Tensor.he_normal([num_embeddings, embedding_dim], requires_grad: true)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def forward(indices)
|
|
211
|
+
ids = indices.is_a?(Tensor) ? indices.to_a.map(&:to_i) : indices.map(&:to_i)
|
|
212
|
+
batch_size = ids.size
|
|
213
|
+
out_data = ids.flat_map do |id|
|
|
214
|
+
raise IndexError, "Token index #{id} out of range [0, #{@num_embeddings})" if id < 0 || id >= @num_embeddings
|
|
215
|
+
@weight.to_a.slice(id * @embedding_dim, @embedding_dim)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
out = Tensor.create(out_data, [batch_size, @embedding_dim])
|
|
219
|
+
if @weight.requires_grad
|
|
220
|
+
out.requires_grad = true
|
|
221
|
+
out._grafo_hijos << @weight
|
|
222
|
+
w = @weight; dim = @embedding_dim; num_emb = @num_embeddings
|
|
223
|
+
out.backward_fn = ->(g) {
|
|
224
|
+
grad_w = Array.new(num_emb * dim, 0.0)
|
|
225
|
+
g_data = g.to_a
|
|
226
|
+
ids.each_with_index do |id, i|
|
|
227
|
+
slice = g_data.slice(i * dim, dim)
|
|
228
|
+
dim.times { |d| grad_w[id * dim + d] += slice[d] }
|
|
229
|
+
end
|
|
230
|
+
w.agregar_gradiente(Tensor.create(grad_w, w.shape))
|
|
231
|
+
}
|
|
232
|
+
end
|
|
233
|
+
out
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def to_s
|
|
237
|
+
"Embedding(#{@num_embeddings}, #{@embedding_dim})"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# ================================================================
|
|
242
|
+
# LayerNorm — Layer normalization across channel dimensions
|
|
243
|
+
# ================================================================
|
|
244
|
+
class LayerNorm < Module
|
|
245
|
+
attr_reader :gamma, :beta, :normalized_shape, :epsilon
|
|
246
|
+
|
|
247
|
+
def initialize(normalized_shape, eps: nil, epsilon: 1e-5)
|
|
248
|
+
@normalized_shape = normalized_shape.is_a?(Array) ? normalized_shape : [normalized_shape]
|
|
249
|
+
@dim = @normalized_shape.reduce(1, :*)
|
|
250
|
+
@epsilon = (eps || epsilon).to_f
|
|
251
|
+
|
|
252
|
+
@gamma = Tensor.ones(@normalized_shape, requires_grad: true)
|
|
253
|
+
@beta = Tensor.zeros(@normalized_shape, requires_grad: true)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def forward(x)
|
|
257
|
+
batch_size = x.shape[0]
|
|
258
|
+
x_data = x.to_a
|
|
259
|
+
|
|
260
|
+
means = Array.new(batch_size) do |b|
|
|
261
|
+
x_data.slice(b * @dim, @dim).sum / @dim.to_f
|
|
262
|
+
end
|
|
263
|
+
vars = Array.new(batch_size) do |b|
|
|
264
|
+
m = means[b]
|
|
265
|
+
x_data.slice(b * @dim, @dim).sum { |v| (v - m)**2 } / @dim.to_f
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
gamma_data = @gamma.to_a
|
|
269
|
+
beta_data = @beta.to_a
|
|
270
|
+
norm_data = Array.new(batch_size * @dim)
|
|
271
|
+
|
|
272
|
+
batch_size.times do |b|
|
|
273
|
+
m = means[b]
|
|
274
|
+
inv_std = 1.0 / Math.sqrt(vars[b] + @epsilon)
|
|
275
|
+
@dim.times do |j|
|
|
276
|
+
norm_data[b * @dim + j] = gamma_data[j] * (x_data[b * @dim + j] - m) * inv_std + beta_data[j]
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
out = Tensor.create(norm_data, x.shape)
|
|
281
|
+
if x.requires_grad || @gamma.requires_grad || @beta.requires_grad
|
|
282
|
+
out.requires_grad = true
|
|
283
|
+
out._grafo_hijos.push(x, @gamma, @beta)
|
|
284
|
+
g_param = @gamma; b_param = @beta; d = @dim; eps = @epsilon
|
|
285
|
+
out.backward_fn = ->(g) {
|
|
286
|
+
g_data = g.to_a
|
|
287
|
+
grad_gamma = Array.new(d, 0.0)
|
|
288
|
+
grad_beta = Array.new(d, 0.0)
|
|
289
|
+
grad_x = Array.new(batch_size * d, 0.0)
|
|
290
|
+
|
|
291
|
+
batch_size.times do |b|
|
|
292
|
+
m = means[b]; v = vars[b]
|
|
293
|
+
inv_std = 1.0 / Math.sqrt(v + eps)
|
|
294
|
+
x_hat = Array.new(d) { |j| (x_data[b * d + j] - m) * inv_std }
|
|
295
|
+
dl_dxhat = Array.new(d) { |j| g_data[b * d + j] * gamma_data[j] }
|
|
296
|
+
sum_dl = dl_dxhat.sum
|
|
297
|
+
sum_dl_x = dl_dxhat.zip(x_hat).sum { |a, c| a * c }
|
|
298
|
+
|
|
299
|
+
d.times do |j|
|
|
300
|
+
grad_gamma[j] += g_data[b * d + j] * x_hat[j]
|
|
301
|
+
grad_beta[j] += g_data[b * d + j]
|
|
302
|
+
grad_x[b * d + j] = (inv_std / d.to_f) * (d.to_f * dl_dxhat[j] - sum_dl - x_hat[j] * sum_dl_x)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
g_param.agregar_gradiente(Tensor.create(grad_gamma, g_param.shape)) if g_param.requires_grad
|
|
307
|
+
b_param.agregar_gradiente(Tensor.create(grad_beta, b_param.shape)) if b_param.requires_grad
|
|
308
|
+
x.agregar_gradiente(Tensor.create(grad_x, x.shape)) if x.requires_grad
|
|
309
|
+
}
|
|
310
|
+
end
|
|
311
|
+
out
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def to_s
|
|
315
|
+
"LayerNorm(#{@normalized_shape})"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# ================================================================
|
|
320
|
+
# BatchNorm1d — Normalizacion por batch
|
|
195
321
|
# ================================================================
|
|
196
322
|
class BatchNorm1d < Module
|
|
197
|
-
def initialize(num_features, epsilon: 1e-5, momentum: 0.1)
|
|
323
|
+
def initialize(num_features, eps: nil, epsilon: 1e-5, momentum: 0.1)
|
|
198
324
|
@num_features = num_features
|
|
199
|
-
@epsilon = epsilon
|
|
200
|
-
@momentum = momentum
|
|
325
|
+
@epsilon = (eps || epsilon).to_f
|
|
326
|
+
@momentum = momentum.to_f
|
|
201
327
|
@training = true
|
|
202
328
|
|
|
203
|
-
# Parámetros entrenables
|
|
204
329
|
@gamma = Tensor.ones([num_features], requires_grad: true)
|
|
205
330
|
@beta = Tensor.zeros([num_features], requires_grad: true)
|
|
206
331
|
|
|
207
|
-
# Estadísticas corrientes (no entrenables, para inferencia)
|
|
208
332
|
@running_mean = Tensor.zeros([num_features])
|
|
209
333
|
@running_var = Tensor.ones([num_features])
|
|
210
334
|
end
|
|
@@ -213,11 +337,9 @@ module GRX
|
|
|
213
337
|
def eval!; @training = false; self; end
|
|
214
338
|
|
|
215
339
|
def forward(x)
|
|
216
|
-
# x: [batch, num_features]
|
|
217
340
|
batch_size = x.shape[0]
|
|
218
341
|
|
|
219
342
|
if @training
|
|
220
|
-
# Calculamos media y varianza del batch
|
|
221
343
|
batch_data = x.to_a
|
|
222
344
|
means = Array.new(@num_features) do |j|
|
|
223
345
|
batch_data.each_slice(@num_features).map { |row| row[j] }.sum / batch_size
|
|
@@ -227,7 +349,6 @@ module GRX
|
|
|
227
349
|
col.sum { |v| (v - means[j]) ** 2 } / batch_size
|
|
228
350
|
end
|
|
229
351
|
|
|
230
|
-
# Actualizamos estadísticas corrientes
|
|
231
352
|
means.each_with_index do |m, j|
|
|
232
353
|
rm = @running_mean.to_a; rm[j] = (1 - @momentum) * rm[j] + @momentum * m
|
|
233
354
|
@running_mean = Tensor.create(rm, [@num_features])
|
|
@@ -244,8 +365,6 @@ module GRX
|
|
|
244
365
|
var_t = @running_var
|
|
245
366
|
end
|
|
246
367
|
|
|
247
|
-
# Normalizamos: x_hat = (x - mean) / sqrt(var + eps)
|
|
248
|
-
# Luego escalamos: y = gamma * x_hat + beta
|
|
249
368
|
norm_data = x.to_a.each_slice(@num_features).flat_map do |row|
|
|
250
369
|
row.each_with_index.map do |v, j|
|
|
251
370
|
x_hat = (v - mean_t.to_a[j]) / Math.sqrt(var_t.to_a[j] + @epsilon)
|
data/lib/grx/optim.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module GRX
|
|
4
4
|
module Optim
|
|
5
5
|
# ================================================================
|
|
6
|
-
# SGD — Stochastic Gradient Descent (
|
|
6
|
+
# SGD — Stochastic Gradient Descent (with optional momentum)
|
|
7
7
|
# ================================================================
|
|
8
8
|
class SGD
|
|
9
9
|
def initialize(params, lr: 0.01, momentum: 0.0, weight_decay: 0.0)
|
|
@@ -11,7 +11,7 @@ module GRX
|
|
|
11
11
|
@lr = lr
|
|
12
12
|
@momentum = momentum
|
|
13
13
|
@weight_decay = weight_decay
|
|
14
|
-
#
|
|
14
|
+
# Velocity buffer for momentum
|
|
15
15
|
@velocity = params.map { |p| Tensor.zeros_like(p) }
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -21,7 +21,7 @@ module GRX
|
|
|
21
21
|
|
|
22
22
|
grad = param.grad
|
|
23
23
|
|
|
24
|
-
# L2
|
|
24
|
+
# L2 regularization (weight decay)
|
|
25
25
|
if @weight_decay > 0
|
|
26
26
|
grad = grad + param.scale(@weight_decay)
|
|
27
27
|
end
|
|
@@ -35,7 +35,7 @@ module GRX
|
|
|
35
35
|
if CAPI::LOADED
|
|
36
36
|
CAPI.grx_sgd_step(param.storage.ptr, grad.storage.ptr, @lr, param.numel)
|
|
37
37
|
else
|
|
38
|
-
#
|
|
38
|
+
# Ruby fallback
|
|
39
39
|
param_data = param.to_a
|
|
40
40
|
grad_data = grad.to_a
|
|
41
41
|
param_data.each_with_index { |v, j| param_data[j] = v - @lr * grad_data[j] }
|
|
@@ -51,27 +51,32 @@ module GRX
|
|
|
51
51
|
|
|
52
52
|
# ================================================================
|
|
53
53
|
# Adam — Adaptive Moment Estimation (Kingma & Ba, 2015)
|
|
54
|
-
#
|
|
54
|
+
# The standard optimizer for deep neural networks.
|
|
55
55
|
# ================================================================
|
|
56
56
|
class Adam
|
|
57
|
-
def initialize(params, lr: 0.001, beta1: 0.9, beta2: 0.999,
|
|
58
|
-
epsilon: 1e-8, weight_decay: 0.0)
|
|
57
|
+
def initialize(params, lr: 0.001, betas: nil, beta1: 0.9, beta2: 0.999,
|
|
58
|
+
eps: nil, epsilon: 1e-8, weight_decay: 0.0)
|
|
59
59
|
@params = params
|
|
60
|
-
@lr = lr
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
@lr = lr.to_f
|
|
61
|
+
if betas
|
|
62
|
+
@beta1 = betas[0].to_f
|
|
63
|
+
@beta2 = betas[1].to_f
|
|
64
|
+
else
|
|
65
|
+
@beta1 = beta1.to_f
|
|
66
|
+
@beta2 = beta2.to_f
|
|
67
|
+
end
|
|
68
|
+
@epsilon = eps ? eps.to_f : epsilon.to_f
|
|
69
|
+
@weight_decay = weight_decay.to_f
|
|
70
|
+
@t = 0 # current step
|
|
66
71
|
|
|
67
|
-
#
|
|
72
|
+
# First and second order moment vectors (zero-initialized)
|
|
68
73
|
@m = params.map { |p| Tensor.zeros_like(p) }
|
|
69
74
|
@v = params.map { |p| Tensor.zeros_like(p) }
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
def step
|
|
73
78
|
@t += 1
|
|
74
|
-
beta1t = @beta1 ** @t # beta1^t
|
|
79
|
+
beta1t = @beta1 ** @t # beta1^t for bias correction
|
|
75
80
|
beta2t = @beta2 ** @t
|
|
76
81
|
|
|
77
82
|
@params.each_with_index do |param, i|
|
|
@@ -94,7 +99,7 @@ module GRX
|
|
|
94
99
|
param.numel
|
|
95
100
|
)
|
|
96
101
|
else
|
|
97
|
-
#
|
|
102
|
+
# Pure Ruby fallback
|
|
98
103
|
p_data = param.to_a
|
|
99
104
|
m_data = @m[i].to_a
|
|
100
105
|
v_data = @v[i].to_a
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GRX
|
|
4
|
+
# ===================================================================
|
|
5
|
+
# Serialization — Native binary .grx format
|
|
6
|
+
#
|
|
7
|
+
# Binary layout:
|
|
8
|
+
# - 8 bytes: Magic header "GRX1\0\0\0\0"
|
|
9
|
+
# - 4 bytes: Number of parameter tensors (unsigned 32-bit big-endian)
|
|
10
|
+
# For each tensor:
|
|
11
|
+
# - 2 bytes: Rank (number of dimensions)
|
|
12
|
+
# - 4 bytes * rank: Dimensions of the shape (uint32 big-endian)
|
|
13
|
+
# - 8 bytes: Total numel (uint64 big-endian)
|
|
14
|
+
# - numel * 8 bytes: Direct binary packed IEEE 754 doubles
|
|
15
|
+
# ===================================================================
|
|
16
|
+
module Serialization
|
|
17
|
+
MAGIC = "GRX1\x00\x00\x00\x00".b
|
|
18
|
+
|
|
19
|
+
def self.save(model, path)
|
|
20
|
+
params = model.parameters
|
|
21
|
+
File.open(path, "wb") do |f|
|
|
22
|
+
f.write(MAGIC)
|
|
23
|
+
f.write([params.size].pack("N"))
|
|
24
|
+
params.each do |p|
|
|
25
|
+
shape = p.shape
|
|
26
|
+
f.write([shape.size].pack("n"))
|
|
27
|
+
f.write(shape.pack("N*"))
|
|
28
|
+
f.write([p.numel].pack("Q>"))
|
|
29
|
+
# Direct binary copy from native C buffer
|
|
30
|
+
bytes = if p.storage.ptr
|
|
31
|
+
p.storage.ptr[0, p.numel * 8]
|
|
32
|
+
else
|
|
33
|
+
p.to_a.pack("d*")
|
|
34
|
+
end
|
|
35
|
+
f.write(bytes)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.load(model, path)
|
|
42
|
+
params = model.parameters
|
|
43
|
+
File.open(path, "rb") do |f|
|
|
44
|
+
magic = f.read(8)
|
|
45
|
+
raise StorageError, "Invalid format: not a valid .grx binary file" unless magic == MAGIC
|
|
46
|
+
count = f.read(4).unpack1("N")
|
|
47
|
+
raise StorageError, "Parameter count mismatch: model has #{params.size}, file has #{count}" unless count == params.size
|
|
48
|
+
|
|
49
|
+
params.each_with_index do |p, idx|
|
|
50
|
+
rank = f.read(2).unpack1("n")
|
|
51
|
+
shape = f.read(rank * 4).unpack("N*")
|
|
52
|
+
numel = f.read(8).unpack1("Q>")
|
|
53
|
+
raise ShapeError, "Shape mismatch for parameter #{idx}: expected #{p.shape}, got #{shape}" unless shape == p.shape
|
|
54
|
+
|
|
55
|
+
bytes = f.read(numel * 8)
|
|
56
|
+
if p.storage.ptr
|
|
57
|
+
p.storage.ptr[0, bytes.bytesize] = bytes
|
|
58
|
+
else
|
|
59
|
+
p.storage.instance_variable_set(:@data, bytes.unpack("d*"))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
model
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|