grx-tensor 0.1.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 ADDED
@@ -0,0 +1,623 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRX
4
+ class Tensor
5
+ attr_reader :storage, :shape, :strides, :offset
6
+ attr_accessor :grad, :requires_grad, :backward_fn
7
+
8
+ def initialize(storage, shape, strides: nil, offset: 0, requires_grad: false)
9
+ @storage = storage
10
+ @shape = shape
11
+ @offset = offset
12
+ @strides = strides || _calc_strides(shape)
13
+ @requires_grad = requires_grad
14
+ @grad = nil
15
+ @backward_fn = nil
16
+ @_grafo_hijos = []
17
+ end
18
+
19
+ # ----------------------------------------------------------------
20
+ # FACTORIES
21
+ # ----------------------------------------------------------------
22
+
23
+ def self.create(array_valores, shape, requires_grad: false)
24
+ new(Storage.new(array_valores), shape, requires_grad: requires_grad)
25
+ end
26
+
27
+ def self.zeros(shape, requires_grad: false)
28
+ create(Array.new(shape.reduce(1,:*), 0.0), shape, requires_grad: requires_grad)
29
+ end
30
+
31
+ def self.ones(shape, requires_grad: false)
32
+ create(Array.new(shape.reduce(1,:*), 1.0), shape, requires_grad: requires_grad)
33
+ end
34
+
35
+ def self.zeros_like(t, requires_grad: false)
36
+ zeros(t.shape, requires_grad: requires_grad)
37
+ end
38
+
39
+ def self.ones_like(t, requires_grad: false)
40
+ ones(t.shape, requires_grad: requires_grad)
41
+ end
42
+
43
+ # Inicialización Xavier uniform (para capas lineales con tanh/sigmoid)
44
+ def self.xavier_uniform(shape, requires_grad: false)
45
+ fan_in, fan_out = shape[-2] || 1, shape[-1] || 1
46
+ n = shape.reduce(1, :*)
47
+ s = _alloc_raw(n)
48
+ CAPI.grx_init_xavier_uniform(s.ptr, n, fan_in, fan_out) if CAPI::LOADED
49
+ new(s, shape, requires_grad: requires_grad)
50
+ end
51
+
52
+ # Inicialización He normal (para capas con ReLU)
53
+ def self.he_normal(shape, requires_grad: false)
54
+ # fan_in = número de entradas = último dim o penúltimo si es 2D
55
+ fan_in = shape.size >= 2 ? shape[-1] : shape[0]
56
+ n = shape.reduce(1, :*)
57
+ s = _alloc_raw(n)
58
+ CAPI.grx_init_he_normal(s.ptr, n, fan_in) if CAPI::LOADED
59
+ new(s, shape, requires_grad: requires_grad)
60
+ end
61
+
62
+ # ----------------------------------------------------------------
63
+ # OPERACIONES ARITMÉTICAS (con autograd)
64
+ # ----------------------------------------------------------------
65
+
66
+ def +(other)
67
+ case other
68
+ when Tensor
69
+ raise ShapeError, "Shapes incompatibles: #{@shape} vs #{other.shape}" if @shape != other.shape
70
+ r = Tensor.new(_binop(:grx_add, other), @shape)
71
+ if requires_grad || other.requires_grad
72
+ r.requires_grad = true
73
+ r._grafo_hijos.push(self, other)
74
+ r.backward_fn = ->(g) {
75
+ agregar_gradiente(g) if requires_grad
76
+ other.agregar_gradiente(g) if other.requires_grad
77
+ }
78
+ end
79
+ r
80
+ when Numeric
81
+ add_scalar(other.to_f)
82
+ else
83
+ raise TypeError, "No se puede sumar Tensor con #{other.class}"
84
+ end
85
+ end
86
+
87
+ def -(other)
88
+ case other
89
+ when Tensor
90
+ raise ShapeError, "Shapes incompatibles: #{@shape} vs #{other.shape}" if @shape != other.shape
91
+ r = Tensor.new(_binop(:grx_sub, other), @shape)
92
+ if requires_grad || other.requires_grad
93
+ r.requires_grad = true
94
+ r._grafo_hijos.push(self, other)
95
+ r.backward_fn = ->(g) {
96
+ agregar_gradiente(g) if requires_grad
97
+ other.agregar_gradiente(g.negate) if other.requires_grad
98
+ }
99
+ end
100
+ r
101
+ when Numeric
102
+ add_scalar(-other.to_f)
103
+ else
104
+ raise TypeError, "No se puede restar Tensor con #{other.class}"
105
+ end
106
+ end
107
+
108
+ def *(other)
109
+ case other
110
+ when Tensor
111
+ raise ShapeError, "Shapes incompatibles: #{@shape} vs #{other.shape}" if @shape != other.shape
112
+ r = Tensor.new(_binop(:grx_mul, other), @shape)
113
+ if requires_grad || other.requires_grad
114
+ r.requires_grad = true
115
+ a, b = self, other
116
+ r._grafo_hijos.push(a, b)
117
+ r.backward_fn = ->(g) {
118
+ a.agregar_gradiente(g * b) if a.requires_grad
119
+ b.agregar_gradiente(g * a) if b.requires_grad
120
+ }
121
+ end
122
+ r
123
+ when Numeric
124
+ scale(other.to_f)
125
+ else
126
+ raise TypeError, "No se puede multiplicar Tensor con #{other.class}"
127
+ end
128
+ end
129
+
130
+ def /(other)
131
+ case other
132
+ when Tensor
133
+ raise ShapeError, "Shapes incompatibles: #{@shape} vs #{other.shape}" if @shape != other.shape
134
+ r = Tensor.new(_binop(:grx_div, other), @shape)
135
+ if requires_grad || other.requires_grad
136
+ r.requires_grad = true
137
+ a, b = self, other
138
+ r._grafo_hijos.push(a, b)
139
+ r.backward_fn = ->(g) {
140
+ # d(a/b)/da = 1/b, d(a/b)/db = -a/b^2
141
+ a.agregar_gradiente(g / b) if a.requires_grad
142
+ b.agregar_gradiente((g * a).negate / (b * b)) if b.requires_grad
143
+ }
144
+ end
145
+ r
146
+ when Numeric
147
+ scale(1.0 / other.to_f)
148
+ else
149
+ raise TypeError, "No se puede dividir Tensor con #{other.class}"
150
+ end
151
+ end
152
+
153
+ def -@
154
+ negate
155
+ end
156
+
157
+ # ----------------------------------------------------------------
158
+ # OPERACIONES ESCALARES
159
+ # ----------------------------------------------------------------
160
+
161
+ def scale(s)
162
+ _unary_c(:grx_scale, s) { |v| v * s }
163
+ end
164
+
165
+ def add_scalar(s)
166
+ _unary_c(:grx_add_scalar, s) { |v| v + s }
167
+ end
168
+
169
+ def negate
170
+ _unary_c(:grx_negate) { |v| -v }
171
+ end
172
+
173
+ # ----------------------------------------------------------------
174
+ # MATEMÁTICAS ELEMENT-WISE (con autograd)
175
+ # ----------------------------------------------------------------
176
+
177
+ def abs
178
+ r = _unary_c(:grx_abs) { |v| v.abs }
179
+ if requires_grad
180
+ r.requires_grad = true; r._grafo_hijos << self
181
+ src = self
182
+ r.backward_fn = ->(g) {
183
+ # d|x|/dx = sign(x)
184
+ sign = Tensor.create(src.to_a.map { |v| v >= 0 ? 1.0 : -1.0 }, src.shape)
185
+ src.agregar_gradiente(g * sign)
186
+ }
187
+ end
188
+ r
189
+ end
190
+
191
+ def sqrt
192
+ r = _unary_c(:grx_sqrt) { |v| Math.sqrt(v) }
193
+ if requires_grad
194
+ r.requires_grad = true; r._grafo_hijos << self
195
+ res = r; src = self
196
+ r.backward_fn = ->(g) {
197
+ # d(sqrt(x))/dx = 1 / (2*sqrt(x))
198
+ src.agregar_gradiente(g / (res.scale(2.0)))
199
+ }
200
+ end
201
+ r
202
+ end
203
+
204
+ def square
205
+ r = _unary_c(:grx_square) { |v| v * v }
206
+ if requires_grad
207
+ r.requires_grad = true; r._grafo_hijos << self
208
+ src = self
209
+ r.backward_fn = ->(g) { src.agregar_gradiente(g * src.scale(2.0)) }
210
+ end
211
+ r
212
+ end
213
+
214
+ def log
215
+ r = _unary_c(:grx_log) { |v| Math.log(v) }
216
+ if requires_grad
217
+ r.requires_grad = true; r._grafo_hijos << self
218
+ src = self
219
+ r.backward_fn = ->(g) { src.agregar_gradiente(g / src) }
220
+ end
221
+ r
222
+ end
223
+
224
+ def exp
225
+ r = _unary_c(:grx_exp) { |v| Math.exp(v) }
226
+ if requires_grad
227
+ r.requires_grad = true; r._grafo_hijos << self
228
+ res = r; src = self
229
+ r.backward_fn = ->(g) { src.agregar_gradiente(g * res) }
230
+ end
231
+ r
232
+ end
233
+
234
+ def pow(e)
235
+ r = _unary_c(:grx_pow, e.to_f) { |v| v ** e }
236
+ if requires_grad
237
+ r.requires_grad = true; r._grafo_hijos << self
238
+ src = self
239
+ r.backward_fn = ->(g) {
240
+ src.agregar_gradiente(g * src.pow(e - 1).scale(e.to_f))
241
+ }
242
+ end
243
+ r
244
+ end
245
+
246
+ def clip(lo, hi)
247
+ out = _alloc_storage(numel)
248
+ if CAPI::LOADED
249
+ CAPI.grx_clip(@storage.ptr, lo.to_f, hi.to_f, out.ptr, numel)
250
+ else
251
+ data = to_a.map { |v| v < lo ? lo : (v > hi ? hi : v) }
252
+ return Tensor.create(data, @shape)
253
+ end
254
+ Tensor.new(out, @shape)
255
+ end
256
+
257
+ # ----------------------------------------------------------------
258
+ # REDUCCIONES (retornan Float o Tensor escalar)
259
+ # ----------------------------------------------------------------
260
+
261
+ def sum
262
+ if CAPI::LOADED
263
+ CAPI.grx_sum(@storage.ptr, numel)
264
+ else
265
+ to_a.sum
266
+ end
267
+ end
268
+
269
+ def mean
270
+ if CAPI::LOADED
271
+ CAPI.grx_mean(@storage.ptr, numel)
272
+ else
273
+ to_a.sum.to_f / numel
274
+ end
275
+ end
276
+
277
+ def max
278
+ if CAPI::LOADED
279
+ CAPI.grx_max(@storage.ptr, numel)
280
+ else
281
+ to_a.max
282
+ end
283
+ end
284
+
285
+ def min
286
+ if CAPI::LOADED
287
+ CAPI.grx_min(@storage.ptr, numel)
288
+ else
289
+ to_a.min
290
+ end
291
+ end
292
+
293
+ # ----------------------------------------------------------------
294
+ # ÁLGEBRA LINEAL
295
+ # ----------------------------------------------------------------
296
+
297
+ def dot(other)
298
+ raise ShapeError, "dot requiere mismo shape" if @shape != other.shape
299
+ if CAPI::LOADED
300
+ CAPI.grx_dot(@storage.ptr, other.storage.ptr, numel)
301
+ else
302
+ to_a.zip(other.to_a).sum { |a, b| a * b }
303
+ end
304
+ end
305
+
306
+ def matmul(other)
307
+ raise DimensionError, "matmul requiere tensores 2D" unless @shape.size == 2 && other.shape.size == 2
308
+ m, k = @shape; k2, n = other.shape
309
+ raise ShapeError, "Dimensiones incompatibles: #{@shape} × #{other.shape}" if k != k2
310
+ out = _alloc_storage(m * n)
311
+ if CAPI::LOADED
312
+ CAPI.grx_matmul(@storage.ptr, other.storage.ptr, out.ptr, m, k, n)
313
+ else
314
+ result = Array.new(m * n, 0.0)
315
+ m.times { |i| k.times { |kk| aik = @storage.read(i*k+kk)
316
+ n.times { |j| result[i*n+j] += aik * other.storage.read(kk*n+j) } } }
317
+ return Tensor.create(result, [m, n])
318
+ end
319
+ r = Tensor.new(out, [m, n])
320
+ if requires_grad || other.requires_grad
321
+ r.requires_grad = true
322
+ a, b = self, other
323
+ r._grafo_hijos.push(a, b)
324
+ r.backward_fn = ->(g) {
325
+ # 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
327
+ a.agregar_gradiente(g._matmul_no_grad(b._transpose_view)) if a.requires_grad
328
+ b.agregar_gradiente(a._transpose_view._matmul_no_grad(g)) if b.requires_grad
329
+ }
330
+ end
331
+ r
332
+ end
333
+
334
+ # ----------------------------------------------------------------
335
+ # ACTIVACIONES (con autograd)
336
+ # ----------------------------------------------------------------
337
+
338
+ def relu
339
+ r = _unary_c(:grx_relu) { |v| v > 0 ? v : 0.0 }
340
+ if requires_grad
341
+ r.requires_grad = true; r._grafo_hijos << self
342
+ src = self
343
+ r.backward_fn = ->(g) {
344
+ mask = Tensor.create(src.to_a.map { |v| v > 0 ? 1.0 : 0.0 }, src.shape)
345
+ src.agregar_gradiente(g * mask)
346
+ }
347
+ end
348
+ r
349
+ end
350
+
351
+ def leaky_relu(alpha = 0.01)
352
+ r = _unary_c(:grx_leaky_relu, alpha.to_f) { |v| v > 0 ? v : alpha * v }
353
+ if requires_grad
354
+ r.requires_grad = true; r._grafo_hijos << self
355
+ src = self
356
+ r.backward_fn = ->(g) {
357
+ mask = Tensor.create(src.to_a.map { |v| v > 0 ? 1.0 : alpha }, src.shape)
358
+ src.agregar_gradiente(g * mask)
359
+ }
360
+ end
361
+ r
362
+ end
363
+
364
+ def tanh
365
+ r = _unary_c(:grx_tanh_act) { |v| Math.tanh(v) }
366
+ if requires_grad
367
+ r.requires_grad = true; r._grafo_hijos << self
368
+ res = r; src = self
369
+ r.backward_fn = ->(g) {
370
+ # d(tanh)/dx = 1 - tanh(x)^2
371
+ src.agregar_gradiente(g * (Tensor.ones_like(res) - res.square))
372
+ }
373
+ end
374
+ r
375
+ end
376
+
377
+ def sigmoid
378
+ r = _unary_c(:grx_sigmoid) { |v| 1.0 / (1.0 + Math.exp(-v)) }
379
+ if requires_grad
380
+ r.requires_grad = true; r._grafo_hijos << self
381
+ res = r; src = self
382
+ r.backward_fn = ->(g) {
383
+ # d(sigmoid)/dx = sigmoid * (1 - sigmoid)
384
+ src.agregar_gradiente(g * res * (Tensor.ones_like(res) - res))
385
+ }
386
+ end
387
+ r
388
+ end
389
+
390
+ 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 }
395
+ end
396
+ r
397
+ end
398
+
399
+ # ----------------------------------------------------------------
400
+ # AUTOGRAD
401
+ # ----------------------------------------------------------------
402
+
403
+ def agregar_gradiente(g)
404
+ @grad = @grad.nil? ? g : @grad + g
405
+ end
406
+
407
+ def backward(grad_inicial = nil)
408
+ if grad_inicial.nil? && @grad.nil?
409
+ agregar_gradiente(Tensor.ones(@shape))
410
+ elsif !grad_inicial.nil?
411
+ agregar_gradiente(grad_inicial)
412
+ end
413
+
414
+ # Orden topológico via DFS iterativo post-order (evita stack overflow en grafos profundos)
415
+ orden = []
416
+ visitados = {}
417
+ stack = [[self, false]]
418
+
419
+ until stack.empty?
420
+ nodo, procesado = stack.pop
421
+ if procesado
422
+ orden << nodo unless visitados[nodo.object_id]
423
+ visitados[nodo.object_id] = true
424
+ else
425
+ next if visitados[nodo.object_id]
426
+ stack.push([nodo, true])
427
+ nodo._grafo_hijos.each { |h| stack.push([h, false]) unless visitados[h.object_id] }
428
+ end
429
+ end
430
+
431
+ # orden ya está en post-order → reverse = raíz primero, hojas al final
432
+ orden.reverse_each do |nodo|
433
+ next unless nodo.grad && nodo.backward_fn
434
+ nodo.backward_fn.call(nodo.grad)
435
+ nodo.backward_fn = nil
436
+ end
437
+ end
438
+
439
+ def zero_grad!
440
+ @grad = nil
441
+ @_grafo_hijos = []
442
+ @backward_fn = nil
443
+ end
444
+
445
+ def _grafo_hijos
446
+ @_grafo_hijos
447
+ end
448
+
449
+ # ----------------------------------------------------------------
450
+ # GEOMETRÍA (zero-copy)
451
+ # ----------------------------------------------------------------
452
+
453
+ def get(*coords)
454
+ @storage.read(_calc_flat_index(coords))
455
+ end
456
+
457
+ 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)
460
+ end
461
+
462
+ def transpose
463
+ raise DimensionError, "transpose solo soporta 2D" if @shape.size != 2
464
+ t = Tensor.new(@storage, [@shape[1], @shape[0]],
465
+ strides: [@strides[1], @strides[0]],
466
+ offset: @offset, requires_grad: @requires_grad)
467
+ if @requires_grad
468
+ t._grafo_hijos << self
469
+ src = self
470
+ t.backward_fn = ->(g) {
471
+ src.agregar_gradiente(g._transpose_view)
472
+ }
473
+ end
474
+ t
475
+ end
476
+
477
+ # Transpose sin autograd — solo para uso interno en backward
478
+ def _transpose_view
479
+ raise DimensionError, "transpose solo soporta 2D" if @shape.size != 2
480
+ Tensor.new(@storage, [@shape[1], @shape[0]],
481
+ strides: [@strides[1], @strides[0]],
482
+ offset: @offset, requires_grad: false)
483
+ end
484
+
485
+ # Matmul sin autograd — para uso interno en backward_fn
486
+ def _matmul_no_grad(other)
487
+ raise DimensionError, "matmul requiere tensores 2D" unless @shape.size == 2 && other.shape.size == 2
488
+ m, k = @shape; k2, n = other.shape
489
+ raise ShapeError, "Dimensiones incompatibles" if k != k2
490
+ out = _alloc_storage(m * n)
491
+ if CAPI::LOADED
492
+ CAPI.grx_matmul(@storage.ptr, other.storage.ptr, out.ptr, m, k, n)
493
+ else
494
+ 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) } } }
497
+ return Tensor.new(Storage.new(result), [m, n])
498
+ end
499
+ Tensor.new(out, [m, n])
500
+ end
501
+
502
+ def flatten
503
+ reshape([numel])
504
+ end
505
+
506
+ # ----------------------------------------------------------------
507
+ # UTILIDADES
508
+ # ----------------------------------------------------------------
509
+
510
+ def numel
511
+ @shape.reduce(1, :*)
512
+ end
513
+
514
+ 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.
517
+ if _contiguous?
518
+ @storage.to_ruby_array
519
+ else
520
+ _collect_elements(@shape, @strides, @offset)
521
+ end
522
+ end
523
+
524
+ private
525
+
526
+ # Un tensor es contiguo si sus strides coinciden con los strides row-major estándar
527
+ def _contiguous?
528
+ expected = _calc_strides(@shape)
529
+ @strides == expected && @offset == 0
530
+ end
531
+
532
+ def _collect_elements(shape, strides, offset)
533
+ if shape.size == 1
534
+ Array.new(shape[0]) { |i| @storage.read(offset + i * strides[0]) }
535
+ else
536
+ Array.new(shape[0]) { |i|
537
+ _collect_elements(shape[1..], strides[1..], offset + i * strides[0])
538
+ }.flatten
539
+ end
540
+ end
541
+
542
+ public
543
+
544
+ def item
545
+ raise "item() solo para tensores de 1 elemento" if numel != 1
546
+ to_a[0]
547
+ end
548
+
549
+ def to_s
550
+ "#<GRX::Tensor shape=#{@shape} data=#{to_a}>"
551
+ end
552
+ alias inspect to_s
553
+
554
+ # ----------------------------------------------------------------
555
+ # PRIVADO
556
+ # ----------------------------------------------------------------
557
+
558
+ private
559
+
560
+ def _alloc_storage(n)
561
+ self.class._alloc_raw(n)
562
+ end
563
+
564
+ def self._alloc_raw(n)
565
+ if CAPI::LOADED
566
+ ptr = CAPI.grx_alloc(n)
567
+ raise StorageError, "grx_alloc OOM" if ptr.null?
568
+ s = Storage.allocate
569
+ s.instance_variable_set(:@size, n)
570
+ s.instance_variable_set(:@ptr, ptr)
571
+ ObjectSpace.define_finalizer(s, Storage.make_finalizer(ptr))
572
+ s
573
+ else
574
+ Storage.new(Array.new(n, 0.0))
575
+ end
576
+ end
577
+
578
+ # Operación binaria element-wise: llama a CAPI o fallback Ruby
579
+ def _binop(op, other)
580
+ out = _alloc_storage(numel)
581
+ if CAPI::LOADED
582
+ CAPI.public_send(op, @storage.ptr, other.storage.ptr, out.ptr, numel)
583
+ else
584
+ rb = { grx_add: :+, grx_sub: :-, grx_mul: :*, grx_div: :/ }[op]
585
+ data = (0...numel).map { |i|
586
+ @storage.read(@offset + i).public_send(rb, other.storage.read(other.offset + i))
587
+ }
588
+ return Storage.new(data)
589
+ end
590
+ out
591
+ end
592
+
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.)
596
+ def _unary_c(op, *args, &fallback)
597
+ out = _alloc_storage(numel)
598
+ if CAPI::LOADED
599
+ CAPI.public_send(op, @storage.ptr, *args, out.ptr, numel)
600
+ else
601
+ vals = if fallback
602
+ fallback.arity == 0 ? fallback.call : to_a.map(&fallback)
603
+ else
604
+ to_a
605
+ end
606
+ return Tensor.create(vals, @shape)
607
+ end
608
+ Tensor.new(out, @shape)
609
+ end
610
+
611
+ def _calc_flat_index(coords)
612
+ idx = @offset
613
+ coords.each_with_index { |c, i| idx += c * @strides[i] }
614
+ idx
615
+ end
616
+
617
+ def _calc_strides(shape)
618
+ s = 1; saltos = []
619
+ shape.reverse_each { |d| saltos.unshift(s); s *= d }
620
+ saltos
621
+ end
622
+ end
623
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GRX
4
+ VERSION = "0.1.0"
5
+ end
data/lib/grx.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # =====================================================================
4
+ # GRX — Framework de tensores con autograd y núcleo C+SIMD
5
+ # require "grx"
6
+ # =====================================================================
7
+
8
+ require_relative "grx/version"
9
+ require_relative "grx/errors"
10
+ require_relative "grx/c_api"
11
+ require_relative "grx/storage"
12
+ require_relative "grx/tensor"
13
+ require_relative "grx/nn"
14
+ require_relative "grx/optim"
15
+ require_relative "grx/loss"
16
+
17
+ module GRX
18
+ # Acceso rápido
19
+ def self.tensor(data, shape, requires_grad: false)
20
+ Tensor.create(data, shape, requires_grad: requires_grad)
21
+ end
22
+
23
+ def self.zeros(shape, requires_grad: false)
24
+ Tensor.zeros(shape, requires_grad: requires_grad)
25
+ end
26
+
27
+ def self.ones(shape, requires_grad: false)
28
+ Tensor.ones(shape, requires_grad: requires_grad)
29
+ end
30
+
31
+ def self.rand(shape, requires_grad: false)
32
+ n = shape.reduce(1, :*)
33
+ Tensor.create(Array.new(n) { ::Kernel.rand }, shape, requires_grad: requires_grad)
34
+ end
35
+
36
+ def self.randn(shape, requires_grad: false)
37
+ # Box-Muller desde Ruby (el C lo hace más rápido vía he_normal)
38
+ n = shape.reduce(1, :*)
39
+ data = []
40
+ (n / 2.0).ceil.times do
41
+ u1 = ::Kernel.rand; u1 = ::Kernel.rand while u1 < 1e-15
42
+ u2 = ::Kernel.rand
43
+ r = Math.sqrt(-2.0 * Math.log(u1))
44
+ data << r * Math.cos(2 * Math::PI * u2)
45
+ data << r * Math.sin(2 * Math::PI * u2)
46
+ end
47
+ Tensor.create(data.first(n), shape, requires_grad: requires_grad)
48
+ end
49
+ end