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/tensor.rb CHANGED
@@ -40,7 +40,7 @@ module GRX
40
40
  ones(t.shape, requires_grad: requires_grad)
41
41
  end
42
42
 
43
- # Inicialización Xavier uniform (para capas lineales con tanh/sigmoid)
43
+ # Xavier uniform initialization (optimal for linear layers with tanh/sigmoid)
44
44
  def self.xavier_uniform(shape, requires_grad: false)
45
45
  fan_in, fan_out = shape[-2] || 1, shape[-1] || 1
46
46
  n = shape.reduce(1, :*)
@@ -49,9 +49,9 @@ module GRX
49
49
  new(s, shape, requires_grad: requires_grad)
50
50
  end
51
51
 
52
- # Inicialización He normal (para capas con ReLU)
52
+ # He normal initialization (optimal for layers with ReLU)
53
53
  def self.he_normal(shape, requires_grad: false)
54
- # fan_in = número de entradas = último dim o penúltimo si es 2D
54
+ # fan_in = number of inputs = last dim or penultimate if 2D
55
55
  fan_in = shape.size >= 2 ? shape[-1] : shape[0]
56
56
  n = shape.reduce(1, :*)
57
57
  s = _alloc_raw(n)
@@ -60,13 +60,13 @@ module GRX
60
60
  end
61
61
 
62
62
  # ----------------------------------------------------------------
63
- # OPERACIONES ARITMÉTICAS (con autograd)
63
+ # ARITHMETIC OPERATIONS (with autograd)
64
64
  # ----------------------------------------------------------------
65
65
 
66
66
  def +(other)
67
67
  case other
68
68
  when Tensor
69
- raise ShapeError, "Shapes incompatibles: #{@shape} vs #{other.shape}" if @shape != other.shape
69
+ raise ShapeError, "Incompatible shapes: #{@shape} vs #{other.shape}" if @shape != other.shape
70
70
  r = Tensor.new(_binop(:grx_add, other), @shape)
71
71
  if requires_grad || other.requires_grad
72
72
  r.requires_grad = true
@@ -80,14 +80,14 @@ module GRX
80
80
  when Numeric
81
81
  add_scalar(other.to_f)
82
82
  else
83
- raise TypeError, "No se puede sumar Tensor con #{other.class}"
83
+ raise TypeError, "Cannot add Tensor with #{other.class}"
84
84
  end
85
85
  end
86
86
 
87
87
  def -(other)
88
88
  case other
89
89
  when Tensor
90
- raise ShapeError, "Shapes incompatibles: #{@shape} vs #{other.shape}" if @shape != other.shape
90
+ raise ShapeError, "Incompatible shapes: #{@shape} vs #{other.shape}" if @shape != other.shape
91
91
  r = Tensor.new(_binop(:grx_sub, other), @shape)
92
92
  if requires_grad || other.requires_grad
93
93
  r.requires_grad = true
@@ -101,14 +101,14 @@ module GRX
101
101
  when Numeric
102
102
  add_scalar(-other.to_f)
103
103
  else
104
- raise TypeError, "No se puede restar Tensor con #{other.class}"
104
+ raise TypeError, "Cannot subtract Tensor with #{other.class}"
105
105
  end
106
106
  end
107
107
 
108
108
  def *(other)
109
109
  case other
110
110
  when Tensor
111
- raise ShapeError, "Shapes incompatibles: #{@shape} vs #{other.shape}" if @shape != other.shape
111
+ raise ShapeError, "Incompatible shapes: #{@shape} vs #{other.shape}" if @shape != other.shape
112
112
  r = Tensor.new(_binop(:grx_mul, other), @shape)
113
113
  if requires_grad || other.requires_grad
114
114
  r.requires_grad = true
@@ -123,14 +123,14 @@ module GRX
123
123
  when Numeric
124
124
  scale(other.to_f)
125
125
  else
126
- raise TypeError, "No se puede multiplicar Tensor con #{other.class}"
126
+ raise TypeError, "Cannot multiply Tensor with #{other.class}"
127
127
  end
128
128
  end
129
129
 
130
130
  def /(other)
131
131
  case other
132
132
  when Tensor
133
- raise ShapeError, "Shapes incompatibles: #{@shape} vs #{other.shape}" if @shape != other.shape
133
+ raise ShapeError, "Incompatible shapes: #{@shape} vs #{other.shape}" if @shape != other.shape
134
134
  r = Tensor.new(_binop(:grx_div, other), @shape)
135
135
  if requires_grad || other.requires_grad
136
136
  r.requires_grad = true
@@ -146,7 +146,7 @@ module GRX
146
146
  when Numeric
147
147
  scale(1.0 / other.to_f)
148
148
  else
149
- raise TypeError, "No se puede dividir Tensor con #{other.class}"
149
+ raise TypeError, "Cannot divide Tensor with #{other.class}"
150
150
  end
151
151
  end
152
152
 
@@ -155,23 +155,51 @@ module GRX
155
155
  end
156
156
 
157
157
  # ----------------------------------------------------------------
158
- # OPERACIONES ESCALARES
158
+ # SCALAR OPERATIONS
159
159
  # ----------------------------------------------------------------
160
160
 
161
+ def coerce(other)
162
+ case other
163
+ when Numeric
164
+ # Returns reversed [self, other] wrapper to enable 2.0 * tensor
165
+ [Tensor.new(Storage.new(Array.new(numel, other.to_f)), @shape), self]
166
+ else
167
+ raise TypeError, "#{self.class} cannot be coerced with #{other.class}"
168
+ end
169
+ end
170
+
161
171
  def scale(s)
162
- _unary_c(:grx_scale, s) { |v| v * s }
172
+ r = _unary_c(:grx_scale, s) { |v| v * s }
173
+ if requires_grad
174
+ r.requires_grad = true; r._grafo_hijos << self
175
+ src = self; factor = s.to_f
176
+ r.backward_fn = ->(g) { src.agregar_gradiente(g.scale(factor)) }
177
+ end
178
+ r
163
179
  end
164
180
 
165
181
  def add_scalar(s)
166
- _unary_c(:grx_add_scalar, s) { |v| v + s }
182
+ r = _unary_c(:grx_add_scalar, s) { |v| v + s }
183
+ if requires_grad
184
+ r.requires_grad = true; r._grafo_hijos << self
185
+ src = self
186
+ r.backward_fn = ->(g) { src.agregar_gradiente(g) }
187
+ end
188
+ r
167
189
  end
168
190
 
169
191
  def negate
170
- _unary_c(:grx_negate) { |v| -v }
192
+ r = _unary_c(:grx_negate) { |v| -v }
193
+ if requires_grad
194
+ r.requires_grad = true; r._grafo_hijos << self
195
+ src = self
196
+ r.backward_fn = ->(g) { src.agregar_gradiente(g.negate) }
197
+ end
198
+ r
171
199
  end
172
200
 
173
201
  # ----------------------------------------------------------------
174
- # MATEMÁTICAS ELEMENT-WISE (con autograd)
202
+ # ELEMENT-WISE MATH (with autograd)
175
203
  # ----------------------------------------------------------------
176
204
 
177
205
  def abs
@@ -249,29 +277,56 @@ module GRX
249
277
  CAPI.grx_clip(@storage.ptr, lo.to_f, hi.to_f, out.ptr, numel)
250
278
  else
251
279
  data = to_a.map { |v| v < lo ? lo : (v > hi ? hi : v) }
252
- return Tensor.create(data, @shape)
280
+ return Tensor.create(data, @shape, requires_grad: @requires_grad)
253
281
  end
254
- Tensor.new(out, @shape)
282
+ r = Tensor.new(out, @shape)
283
+ if @requires_grad
284
+ r.requires_grad = true; r._grafo_hijos << self
285
+ src = self; l = lo.to_f; h = hi.to_f
286
+ r.backward_fn = ->(g) {
287
+ mask = Tensor.create(src.to_a.map { |v| (v >= l && v <= h) ? 1.0 : 0.0 }, src.shape)
288
+ src.agregar_gradiente(g * mask)
289
+ }
290
+ end
291
+ r
255
292
  end
256
293
 
257
294
  # ----------------------------------------------------------------
258
- # REDUCCIONES (retornan Float o Tensor escalar)
295
+ # REDUCTIONS (return differentiable scalar Tensor with autograd)
259
296
  # ----------------------------------------------------------------
260
297
 
261
298
  def sum
262
- if CAPI::LOADED
299
+ val = if CAPI::LOADED
263
300
  CAPI.grx_sum(@storage.ptr, numel)
264
301
  else
265
302
  to_a.sum
266
303
  end
304
+ r = Tensor.create([val], [1], requires_grad: @requires_grad)
305
+ if @requires_grad
306
+ r._grafo_hijos << self
307
+ src = self
308
+ r.backward_fn = ->(g) {
309
+ src.agregar_gradiente(Tensor.create(Array.new(src.numel, g.item), src.shape))
310
+ }
311
+ end
312
+ r
267
313
  end
268
314
 
269
315
  def mean
270
- if CAPI::LOADED
316
+ val = if CAPI::LOADED
271
317
  CAPI.grx_mean(@storage.ptr, numel)
272
318
  else
273
319
  to_a.sum.to_f / numel
274
320
  end
321
+ r = Tensor.create([val], [1], requires_grad: @requires_grad)
322
+ if @requires_grad
323
+ r._grafo_hijos << self
324
+ src = self; n = numel.to_f
325
+ r.backward_fn = ->(g) {
326
+ src.agregar_gradiente(Tensor.create(Array.new(src.numel, g.item / n), src.shape))
327
+ }
328
+ end
329
+ r
275
330
  end
276
331
 
277
332
  def max
@@ -291,11 +346,11 @@ module GRX
291
346
  end
292
347
 
293
348
  # ----------------------------------------------------------------
294
- # ÁLGEBRA LINEAL
349
+ # LINEAR ALGEBRA
295
350
  # ----------------------------------------------------------------
296
351
 
297
352
  def dot(other)
298
- raise ShapeError, "dot requiere mismo shape" if @shape != other.shape
353
+ raise ShapeError, "dot requires matching shape" if @shape != other.shape
299
354
  if CAPI::LOADED
300
355
  CAPI.grx_dot(@storage.ptr, other.storage.ptr, numel)
301
356
  else
@@ -304,9 +359,9 @@ module GRX
304
359
  end
305
360
 
306
361
  def matmul(other)
307
- raise DimensionError, "matmul requiere tensores 2D" unless @shape.size == 2 && other.shape.size == 2
362
+ raise DimensionError, "matmul requires 2D tensors" unless @shape.size == 2 && other.shape.size == 2
308
363
  m, k = @shape; k2, n = other.shape
309
- raise ShapeError, "Dimensiones incompatibles: #{@shape} × #{other.shape}" if k != k2
364
+ raise ShapeError, "Incompatible dimensions: #{@shape} × #{other.shape}" if k != k2
310
365
  out = _alloc_storage(m * n)
311
366
  if CAPI::LOADED
312
367
  CAPI.grx_matmul(@storage.ptr, other.storage.ptr, out.ptr, m, k, n)
@@ -323,7 +378,7 @@ module GRX
323
378
  r._grafo_hijos.push(a, b)
324
379
  r.backward_fn = ->(g) {
325
380
  # dL/dA = dL/dC × B^T, dL/dB = A^T × dL/dC
326
- # Usamos _matmul_no_grad y _transpose_view para no crear nodos en el grafo
381
+ # Uses _matmul_no_grad and _transpose_view to avoid graph recursion
327
382
  a.agregar_gradiente(g._matmul_no_grad(b._transpose_view)) if a.requires_grad
328
383
  b.agregar_gradiente(a._transpose_view._matmul_no_grad(g)) if b.requires_grad
329
384
  }
@@ -332,7 +387,7 @@ module GRX
332
387
  end
333
388
 
334
389
  # ----------------------------------------------------------------
335
- # ACTIVACIONES (con autograd)
390
+ # ACTIVATIONS (with autograd)
336
391
  # ----------------------------------------------------------------
337
392
 
338
393
  def relu
@@ -388,10 +443,39 @@ module GRX
388
443
  end
389
444
 
390
445
  def softmax
391
- r = _unary_c(:grx_softmax) do
392
- vals = to_a; max_v = vals.max
393
- exps = vals.map { |v| Math.exp(v - max_v) }; s = exps.sum
394
- exps.map { |e| e / s }
446
+ dim = @shape[-1]
447
+ batch = numel / dim
448
+ raw = to_a
449
+ out_vals = Array.new(numel)
450
+
451
+ batch.times do |b|
452
+ slice = raw.slice(b * dim, dim)
453
+ max_v = slice.max
454
+ exps = slice.map { |v| Math.exp(v - max_v) }
455
+ sum_e = exps.sum
456
+ dim.times { |j| out_vals[b * dim + j] = exps[j] / sum_e }
457
+ end
458
+
459
+ r = Tensor.create(out_vals, @shape, requires_grad: @requires_grad)
460
+ if @requires_grad
461
+ r._grafo_hijos << self
462
+ res = r; src = self
463
+ r.backward_fn = ->(g) {
464
+ s_data = res.to_a
465
+ g_data = g.to_a
466
+ grad_x = Array.new(src.numel, 0.0)
467
+
468
+ batch.times do |b|
469
+ s_row = s_data.slice(b * dim, dim)
470
+ g_row = g_data.slice(b * dim, dim)
471
+ dot = s_row.zip(g_row).sum { |s_val, g_val| s_val * g_val }
472
+ dim.times do |j|
473
+ grad_x[b * dim + j] = s_row[j] * (g_row[j] - dot)
474
+ end
475
+ end
476
+
477
+ src.agregar_gradiente(Tensor.create(grad_x, src.shape))
478
+ }
395
479
  end
396
480
  r
397
481
  end
@@ -411,7 +495,7 @@ module GRX
411
495
  agregar_gradiente(grad_inicial)
412
496
  end
413
497
 
414
- # Orden topológico via DFS iterativo post-order (evita stack overflow en grafos profundos)
498
+ # Topological sorting via iterative post-order DFS (prevents stack overflow on deep graphs)
415
499
  orden = []
416
500
  visitados = {}
417
501
  stack = [[self, false]]
@@ -428,7 +512,7 @@ module GRX
428
512
  end
429
513
  end
430
514
 
431
- # orden ya está en post-order reverse = raíz primero, hojas al final
515
+ # Topological order in post-order: reverse traverses root first down to leaves
432
516
  orden.reverse_each do |nodo|
433
517
  next unless nodo.grad && nodo.backward_fn
434
518
  nodo.backward_fn.call(nodo.grad)
@@ -447,20 +531,37 @@ module GRX
447
531
  end
448
532
 
449
533
  # ----------------------------------------------------------------
450
- # GEOMETRÍA (zero-copy)
534
+ # GEOMETRY (zero-copy)
451
535
  # ----------------------------------------------------------------
452
536
 
453
537
  def get(*coords)
454
538
  @storage.read(_calc_flat_index(coords))
455
539
  end
456
540
 
541
+ def contiguous
542
+ return self if _contiguous?
543
+ c = Tensor.create(to_a, @shape, requires_grad: @requires_grad)
544
+ if @requires_grad
545
+ c._grafo_hijos << self
546
+ src = self
547
+ c.backward_fn = ->(g) { src.agregar_gradiente(g) }
548
+ end
549
+ c
550
+ end
551
+
457
552
  def reshape(nueva_forma)
458
- raise ArgumentError, "Reshape incompatible" if numel != nueva_forma.reduce(1,:*)
459
- Tensor.new(@storage, nueva_forma, offset: @offset, requires_grad: @requires_grad)
553
+ raise ArgumentError, "Incompatible reshape" if numel != nueva_forma.reduce(1,:*)
554
+ r = Tensor.new(@storage, nueva_forma, offset: @offset, requires_grad: @requires_grad)
555
+ if @requires_grad
556
+ r._grafo_hijos << self
557
+ src = self; orig_shape = @shape
558
+ r.backward_fn = ->(g) { src.agregar_gradiente(g.reshape(orig_shape)) }
559
+ end
560
+ r
460
561
  end
461
562
 
462
563
  def transpose
463
- raise DimensionError, "transpose solo soporta 2D" if @shape.size != 2
564
+ raise DimensionError, "transpose only supports 2D tensors" if @shape.size != 2
464
565
  t = Tensor.new(@storage, [@shape[1], @shape[0]],
465
566
  strides: [@strides[1], @strides[0]],
466
567
  offset: @offset, requires_grad: @requires_grad)
@@ -468,32 +569,34 @@ module GRX
468
569
  t._grafo_hijos << self
469
570
  src = self
470
571
  t.backward_fn = ->(g) {
471
- src.agregar_gradiente(g._transpose_view)
572
+ src.agregar_gradiente(g.transpose)
472
573
  }
473
574
  end
474
575
  t
475
576
  end
476
577
 
477
- # Transpose sin autograd — solo para uso interno en backward
578
+ # Transpose view without autograd — for internal backward pass
478
579
  def _transpose_view
479
- raise DimensionError, "transpose solo soporta 2D" if @shape.size != 2
580
+ raise DimensionError, "transpose only supports 2D tensors" if @shape.size != 2
480
581
  Tensor.new(@storage, [@shape[1], @shape[0]],
481
582
  strides: [@strides[1], @strides[0]],
482
583
  offset: @offset, requires_grad: false)
483
584
  end
484
585
 
485
- # Matmul sin autograd — para uso interno en backward_fn
586
+ # Matmul without autograd — for internal backward_fn usage
486
587
  def _matmul_no_grad(other)
487
- raise DimensionError, "matmul requiere tensores 2D" unless @shape.size == 2 && other.shape.size == 2
588
+ raise DimensionError, "matmul requires 2D tensors" unless @shape.size == 2 && other.shape.size == 2
488
589
  m, k = @shape; k2, n = other.shape
489
- raise ShapeError, "Dimensiones incompatibles" if k != k2
590
+ raise ShapeError, "Incompatible dimensions" if k != k2
591
+ a_c = _contiguous? ? self : contiguous
592
+ b_c = other._contiguous? ? other : other.contiguous
490
593
  out = _alloc_storage(m * n)
491
594
  if CAPI::LOADED
492
- CAPI.grx_matmul(@storage.ptr, other.storage.ptr, out.ptr, m, k, n)
595
+ CAPI.grx_matmul(a_c.storage.ptr, b_c.storage.ptr, out.ptr, m, k, n)
493
596
  else
494
597
  result = Array.new(m * n, 0.0)
495
- m.times { |i| k.times { |kk| aik = @storage.read(i*k+kk)
496
- n.times { |j| result[i*n+j] += aik * other.storage.read(kk*n+j) } } }
598
+ m.times { |i| k.times { |kk| aik = a_c.storage.read(i*k+kk)
599
+ n.times { |j| result[i*n+j] += aik * b_c.storage.read(kk*n+j) } } }
497
600
  return Tensor.new(Storage.new(result), [m, n])
498
601
  end
499
602
  Tensor.new(out, [m, n])
@@ -504,7 +607,7 @@ module GRX
504
607
  end
505
608
 
506
609
  # ----------------------------------------------------------------
507
- # UTILIDADES
610
+ # UTILITIES
508
611
  # ----------------------------------------------------------------
509
612
 
510
613
  def numel
@@ -512,8 +615,8 @@ module GRX
512
615
  end
513
616
 
514
617
  def to_a
515
- # Si los strides son contiguos (tensor normal, reshape), leemos el buffer directo.
516
- # Si no (transpose, vistas con strides custom), recorremos con strides.
618
+ # If strides are contiguous (normal tensor, reshape), read buffer directly.
619
+ # Otherwise (transpose, strided views), traverse with custom strides.
517
620
  if _contiguous?
518
621
  @storage.to_ruby_array
519
622
  else
@@ -521,13 +624,14 @@ module GRX
521
624
  end
522
625
  end
523
626
 
524
- private
525
-
526
- # Un tensor es contiguo si sus strides coinciden con los strides row-major estándar
527
- def _contiguous?
627
+ # A tensor is contiguous if its strides match standard row-major order
628
+ def contiguous?
528
629
  expected = _calc_strides(@shape)
529
630
  @strides == expected && @offset == 0
530
631
  end
632
+ alias _contiguous? contiguous?
633
+
634
+ private
531
635
 
532
636
  def _collect_elements(shape, strides, offset)
533
637
  if shape.size == 1
@@ -541,18 +645,46 @@ module GRX
541
645
 
542
646
  public
543
647
 
648
+ include Comparable
649
+
650
+ def <=>(other)
651
+ case other
652
+ when Tensor
653
+ (numel == 1 && other.numel == 1) ? item <=> other.item : nil
654
+ when Numeric
655
+ numel == 1 ? item <=> other.to_f : nil
656
+ else
657
+ nil
658
+ end
659
+ end
660
+
544
661
  def item
545
- raise "item() solo para tensores de 1 elemento" if numel != 1
662
+ raise "item() only supported for 1-element tensors" if numel != 1
663
+ to_a[0]
664
+ end
665
+
666
+ def to_f
667
+ raise "to_f only supported for 1-element tensors" if numel != 1
546
668
  to_a[0]
547
669
  end
548
670
 
671
+ def to_i
672
+ raise "to_i only supported for 1-element tensors" if numel != 1
673
+ to_a[0].to_i
674
+ end
675
+
676
+ def nan?
677
+ raise "nan? only supported for 1-element tensors" if numel != 1
678
+ to_a[0].nan?
679
+ end
680
+
549
681
  def to_s
550
682
  "#<GRX::Tensor shape=#{@shape} data=#{to_a}>"
551
683
  end
552
684
  alias inspect to_s
553
685
 
554
686
  # ----------------------------------------------------------------
555
- # PRIVADO
687
+ # PRIVATE
556
688
  # ----------------------------------------------------------------
557
689
 
558
690
  private
@@ -575,33 +707,34 @@ module GRX
575
707
  end
576
708
  end
577
709
 
578
- # Operación binaria element-wise: llama a CAPI o fallback Ruby
710
+ # Element-wise binary op: delegates to CAPI or Ruby fallback
579
711
  def _binop(op, other)
712
+ a_c = _contiguous? ? self : contiguous
713
+ b_c = other._contiguous? ? other : other.contiguous
580
714
  out = _alloc_storage(numel)
581
715
  if CAPI::LOADED
582
- CAPI.public_send(op, @storage.ptr, other.storage.ptr, out.ptr, numel)
716
+ CAPI.public_send(op, a_c.storage.ptr, b_c.storage.ptr, out.ptr, numel)
583
717
  else
584
718
  rb = { grx_add: :+, grx_sub: :-, grx_mul: :*, grx_div: :/ }[op]
585
719
  data = (0...numel).map { |i|
586
- @storage.read(@offset + i).public_send(rb, other.storage.read(other.offset + i))
720
+ a_c.storage.read(a_c.offset + i).public_send(rb, b_c.storage.read(b_c.offset + i))
587
721
  }
588
722
  return Storage.new(data)
589
723
  end
590
724
  out
591
725
  end
592
726
 
593
- # Operación unaria: llama a CAPI con args opcionales o fallback con bloque
594
- # Si el bloque acepta un elemento (arity == 1) → map element-wise
595
- # Si el bloque no acepta argumentos (arity == 0) → lo llama una vez (para softmax, etc.)
727
+ # Unary op: delegates to CAPI with optional args or Ruby fallback block
596
728
  def _unary_c(op, *args, &fallback)
729
+ a_c = _contiguous? ? self : contiguous
597
730
  out = _alloc_storage(numel)
598
731
  if CAPI::LOADED
599
- CAPI.public_send(op, @storage.ptr, *args, out.ptr, numel)
732
+ CAPI.public_send(op, a_c.storage.ptr, *args, out.ptr, numel)
600
733
  else
601
734
  vals = if fallback
602
- fallback.arity == 0 ? fallback.call : to_a.map(&fallback)
735
+ fallback.arity == 0 ? fallback.call : a_c.to_a.map(&fallback)
603
736
  else
604
- to_a
737
+ a_c.to_a
605
738
  end
606
739
  return Tensor.create(vals, @shape)
607
740
  end
data/lib/grx/utils.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRX
4
+ module Utils
5
+ # ================================================================
6
+ # clip_grad_norm! — Clips gradients of parameter collection so their
7
+ # combined L2 norm does not exceed max_norm.
8
+ # Prevents exploding gradient problems during deep network training.
9
+ # ================================================================
10
+ def self.clip_grad_norm!(parameters, max_norm)
11
+ max_norm = max_norm.to_f
12
+ total_norm_sq = 0.0
13
+ parameters.each do |p|
14
+ next unless p.grad
15
+ total_norm_sq += p.grad.square.to_a.sum
16
+ end
17
+ total_norm = Math.sqrt(total_norm_sq)
18
+ clip_coef = max_norm / (total_norm + 1e-6)
19
+ if clip_coef < 1.0
20
+ parameters.each do |p|
21
+ next unless p.grad
22
+ p.grad = p.grad.scale(clip_coef)
23
+ end
24
+ end
25
+ total_norm
26
+ end
27
+ end
28
+ end
data/lib/grx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GRX
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/grx.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # =====================================================================
4
- # GRX — Framework de tensores con autograd y núcleo C+SIMD
4
+ # GRX — Tensor framework with autograd and C+SIMD compute core
5
5
  # require "grx"
6
6
  # =====================================================================
7
7
 
@@ -10,12 +10,15 @@ require_relative "grx/errors"
10
10
  require_relative "grx/c_api"
11
11
  require_relative "grx/storage"
12
12
  require_relative "grx/tensor"
13
+ require_relative "grx/serialization"
13
14
  require_relative "grx/nn"
14
15
  require_relative "grx/optim"
15
16
  require_relative "grx/loss"
17
+ require_relative "grx/data"
18
+ require_relative "grx/utils"
16
19
 
17
20
  module GRX
18
- # Acceso rápido
21
+ # Quick factory helpers
19
22
  def self.tensor(data, shape, requires_grad: false)
20
23
  Tensor.create(data, shape, requires_grad: requires_grad)
21
24
  end
@@ -34,7 +37,7 @@ module GRX
34
37
  end
35
38
 
36
39
  def self.randn(shape, requires_grad: false)
37
- # Box-Muller desde Ruby (el C lo hace más rápido vía he_normal)
40
+ # Box-Muller from Ruby (C backend executes faster via he_normal)
38
41
  n = shape.reduce(1, :*)
39
42
  data = []
40
43
  (n / 2.0).ceil.times do
@@ -46,4 +49,12 @@ module GRX
46
49
  end
47
50
  Tensor.create(data.first(n), shape, requires_grad: requires_grad)
48
51
  end
52
+
53
+ def self.c_loaded?
54
+ CAPI::LOADED
55
+ end
56
+
57
+ def self.mode
58
+ CAPI::LOADED ? :c : :ruby
59
+ end
49
60
  end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grx-tensor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - Angel Gabriel Garcia Razo
7
+ - Razo
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -81,7 +81,9 @@ extensions:
81
81
  extra_rdoc_files: []
82
82
  files:
83
83
  - CHANGELOG.md
84
+ - GUIA_PRINCIPIANTES.md
84
85
  - LICENSE.txt
86
+ - README.es.md
85
87
  - README.md
86
88
  - ext/grx/extconf.rb
87
89
  - ext/grx/grx_core.c
@@ -91,12 +93,15 @@ files:
91
93
  - grx-tensor.gemspec
92
94
  - lib/grx.rb
93
95
  - lib/grx/c_api.rb
96
+ - lib/grx/data.rb
94
97
  - lib/grx/errors.rb
95
98
  - lib/grx/loss.rb
96
99
  - lib/grx/nn.rb
97
100
  - lib/grx/optim.rb
101
+ - lib/grx/serialization.rb
98
102
  - lib/grx/storage.rb
99
103
  - lib/grx/tensor.rb
104
+ - lib/grx/utils.rb
100
105
  - lib/grx/version.rb
101
106
  homepage: https://github.com/Gabo-Razo/grx-tensor
102
107
  licenses:
@@ -108,7 +113,7 @@ metadata:
108
113
  post_install_message: |2+
109
114
 
110
115
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
111
- GRX-Tensor 0.1.0 installed
116
+ GRX-Tensor 0.2.0 installed
112
117
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
113
118
 
114
119
  Compile the C extension to enable AVX2+FMA SIMD: