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.
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
- LIB_NAME = case RUBY_PLATFORM
11
- when /mingw|mswin|windows/i then "grx_core.dll"
12
- when /darwin/i then "libgrx_core.dylib"
13
- else "libgrx_core.so"
14
- end
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
- # rake-compiler siempre genera el archivo como "grx_core.so" / "grx_core.bundle" / "grx_core.dll"
17
- # (sin el prefijo "lib"), y lo pone un nivel arriba de lib/grx/
18
- RAKE_COMPILER_NAME = case RUBY_PLATFORM
19
- when /mingw|mswin|windows/i then "grx_core.dll"
20
- when /darwin/i then "grx_core.bundle"
21
- else "grx_core.so"
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
- raise Fiddle::DLError, "No se encontró #{LIB_NAME} en #{LIB_PATHS.inspect}" unless path
38
- dlload path
39
- true
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] Extensión C no disponible: #{e.message}\n" \
42
- " → Ejecuta: make -C ext/unix install\n" \
43
- " → Corriendo en modo Ruby puro (sin SIMD)."
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
- # Memoria
52
+ # Memory management
49
53
  extern "double* grx_alloc(size_t)"
50
54
  extern "void grx_free(double*)"
51
55
 
52
- # Aritmética element-wise
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
- # Matemáticas element-wise
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
- # Reducciones
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
- # Álgebra lineal
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
- # Activaciones
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
- # Optimizadores
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
- # Inicialización de pesos
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) retorna Float
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, "Shapes incompatibles" if pred.shape != target.shape
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, "Shapes incompatibles" if pred.shape != target.shape
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 debe estar en (0,1) — aplica sigmoid antes si usas logits.
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, "Shapes incompatibles" if pred.shape != target.shape
37
- p_data = pred.to_a.map { |v| v < EPS ? EPS : (v > 1-EPS ? 1-EPS : v) }
38
- t_data = target.to_a
39
- total = p_data.size.to_f
40
- loss = p_data.each_with_index.sum do |p, i|
41
- t = t_data[i]
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 = -mean(sum(target * log(softmax(logits))))
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, "Shapes incompatibles" if logits.shape != target.shape
57
- probs = logits.softmax.to_a.map { |v| v < EPS ? EPS : v }
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
- loss = probs.each_with_index.sum { |p, i| -t_data[i] * Math.log(p) }
60
- loss / probs.size.to_f
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
- diffs = (pred - target).abs.to_a
76
- loss = diffs.sum { |v| v <= d ? 0.5 * v * v : d * (v - 0.5 * d) }
77
- loss / diffs.size.to_f
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 — clase base para todas las capas
6
+ # Module — Base class for all neural network layers
7
7
  # ================================================================
8
8
  class Module
9
- # Retorna todos los parámetros entrenables (para pasarlos al optimizador)
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
- # Subclases implementan forward
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 — Capa densa (fully connected)
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
- # Pesos: Xavier uniform (bueno para tanh/sigmoid)
58
+ # Weights: Xavier uniform initialization
51
59
  @weight = Tensor.xavier_uniform([out_features, in_features], requires_grad: true)
52
60
 
53
- # Bias: ceros
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] out: [batch, out_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
- # Acumulamos gradiente en bias: suma sobre el batch
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} #{@out_features}, bias: #{@use_bias})"
105
+ "Linear(#{@in_features} -> #{@out_features}, bias: #{@use_bias})"
110
106
  end
111
107
  end
112
108
 
113
109
  # ================================================================
114
- # Sequential — Contenedor de capas en secuencia
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
- # Activaciones como capas (para usar en Sequential)
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 — regularización durante entrenamiento
173
+ # Dropout — Random feature dropout during training mode
168
174
  # ================================================================
169
175
  class Dropout < Module
170
176
  def initialize(p = 0.5)
171
- @p = 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
- # BatchNorm1dNormalización por batch
194
- # Estabiliza el entrenamiento de redes profundas.
197
+ # EmbeddingDense 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)