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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +54 -0
- data/LICENSE.txt +21 -0
- data/README.md +471 -0
- data/ext/grx/extconf.rb +31 -0
- data/ext/grx/grx_core.c +534 -0
- data/ext/grx/grx_core.h +85 -0
- data/ext/unix/Makefile +66 -0
- data/ext/windows/Makefile.mingw +50 -0
- data/grx-tensor.gemspec +88 -0
- data/lib/grx/c_api.rb +96 -0
- data/lib/grx/errors.rb +8 -0
- data/lib/grx/loss.rb +81 -0
- data/lib/grx/nn.rb +262 -0
- data/lib/grx/optim.rb +121 -0
- data/lib/grx/storage.rb +85 -0
- data/lib/grx/tensor.rb +623 -0
- data/lib/grx/version.rb +5 -0
- data/lib/grx.rb +49 -0
- metadata +159 -0
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
|
data/lib/grx/version.rb
ADDED
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
|