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/optim.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GRX
|
|
4
|
+
module Optim
|
|
5
|
+
# ================================================================
|
|
6
|
+
# SGD — Stochastic Gradient Descent (con momentum opcional)
|
|
7
|
+
# ================================================================
|
|
8
|
+
class SGD
|
|
9
|
+
def initialize(params, lr: 0.01, momentum: 0.0, weight_decay: 0.0)
|
|
10
|
+
@params = params
|
|
11
|
+
@lr = lr
|
|
12
|
+
@momentum = momentum
|
|
13
|
+
@weight_decay = weight_decay
|
|
14
|
+
# Buffer de velocidad para momentum
|
|
15
|
+
@velocity = params.map { |p| Tensor.zeros_like(p) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def step
|
|
19
|
+
@params.each_with_index do |param, i|
|
|
20
|
+
next unless param.grad
|
|
21
|
+
|
|
22
|
+
grad = param.grad
|
|
23
|
+
|
|
24
|
+
# L2 regularización (weight decay)
|
|
25
|
+
if @weight_decay > 0
|
|
26
|
+
grad = grad + param.scale(@weight_decay)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if @momentum > 0
|
|
30
|
+
# v = momentum*v + grad
|
|
31
|
+
@velocity[i] = @velocity[i].scale(@momentum) + grad
|
|
32
|
+
grad = @velocity[i]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if CAPI::LOADED
|
|
36
|
+
CAPI.grx_sgd_step(param.storage.ptr, grad.storage.ptr, @lr, param.numel)
|
|
37
|
+
else
|
|
38
|
+
# Fallback Ruby
|
|
39
|
+
param_data = param.to_a
|
|
40
|
+
grad_data = grad.to_a
|
|
41
|
+
param_data.each_with_index { |v, j| param_data[j] = v - @lr * grad_data[j] }
|
|
42
|
+
param.storage.instance_variable_set(:@data, param_data)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def zero_grad
|
|
48
|
+
@params.each(&:zero_grad!)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# ================================================================
|
|
53
|
+
# Adam — Adaptive Moment Estimation (Kingma & Ba, 2015)
|
|
54
|
+
# El optimizador estándar para redes neuronales profundas.
|
|
55
|
+
# ================================================================
|
|
56
|
+
class Adam
|
|
57
|
+
def initialize(params, lr: 0.001, beta1: 0.9, beta2: 0.999,
|
|
58
|
+
epsilon: 1e-8, weight_decay: 0.0)
|
|
59
|
+
@params = params
|
|
60
|
+
@lr = lr
|
|
61
|
+
@beta1 = beta1
|
|
62
|
+
@beta2 = beta2
|
|
63
|
+
@epsilon = epsilon
|
|
64
|
+
@weight_decay = weight_decay
|
|
65
|
+
@t = 0 # paso actual
|
|
66
|
+
|
|
67
|
+
# Momentos de primer y segundo orden (inicializados en cero)
|
|
68
|
+
@m = params.map { |p| Tensor.zeros_like(p) }
|
|
69
|
+
@v = params.map { |p| Tensor.zeros_like(p) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def step
|
|
73
|
+
@t += 1
|
|
74
|
+
beta1t = @beta1 ** @t # beta1^t para corrección de bias
|
|
75
|
+
beta2t = @beta2 ** @t
|
|
76
|
+
|
|
77
|
+
@params.each_with_index do |param, i|
|
|
78
|
+
next unless param.grad
|
|
79
|
+
|
|
80
|
+
grad = param.grad
|
|
81
|
+
|
|
82
|
+
if @weight_decay > 0
|
|
83
|
+
grad = grad + param.scale(@weight_decay)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if CAPI::LOADED
|
|
87
|
+
CAPI.grx_adam_step(
|
|
88
|
+
param.storage.ptr,
|
|
89
|
+
@m[i].storage.ptr,
|
|
90
|
+
@v[i].storage.ptr,
|
|
91
|
+
grad.storage.ptr,
|
|
92
|
+
@lr, @beta1, @beta2, @epsilon,
|
|
93
|
+
beta1t, beta2t,
|
|
94
|
+
param.numel
|
|
95
|
+
)
|
|
96
|
+
else
|
|
97
|
+
# Fallback Ruby puro
|
|
98
|
+
p_data = param.to_a
|
|
99
|
+
m_data = @m[i].to_a
|
|
100
|
+
v_data = @v[i].to_a
|
|
101
|
+
g_data = grad.to_a
|
|
102
|
+
p_data.each_with_index do |_, j|
|
|
103
|
+
m_data[j] = @beta1 * m_data[j] + (1 - @beta1) * g_data[j]
|
|
104
|
+
v_data[j] = @beta2 * v_data[j] + (1 - @beta2) * g_data[j] ** 2
|
|
105
|
+
mh = m_data[j] / (1 - beta1t)
|
|
106
|
+
vh = v_data[j] / (1 - beta2t)
|
|
107
|
+
p_data[j] -= @lr * mh / (Math.sqrt(vh) + @epsilon)
|
|
108
|
+
end
|
|
109
|
+
param.storage.instance_variable_set(:@data, p_data)
|
|
110
|
+
@m[i].storage.instance_variable_set(:@data, m_data)
|
|
111
|
+
@v[i].storage.instance_variable_set(:@data, v_data)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def zero_grad
|
|
117
|
+
@params.each(&:zero_grad!)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/grx/storage.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
|
|
5
|
+
module GRX
|
|
6
|
+
# ===================================================================
|
|
7
|
+
# Storage — Buffer de memoria nativa
|
|
8
|
+
#
|
|
9
|
+
# Cuando CAPI está cargado:
|
|
10
|
+
# @ptr → Fiddle::Pointer a un bloque de doubles alineado a 32 bytes
|
|
11
|
+
# reservado con grx_alloc() (malloc alineado en C).
|
|
12
|
+
# Los datos viven en el heap de C, NO en el GC de Ruby.
|
|
13
|
+
#
|
|
14
|
+
# Cuando CAPI NO está cargado (fallback):
|
|
15
|
+
# @data → Array de Ruby normal (lento pero siempre correcto).
|
|
16
|
+
#
|
|
17
|
+
# La separación entre los dos modos es transparente para Tensor.
|
|
18
|
+
# ===================================================================
|
|
19
|
+
class Storage
|
|
20
|
+
attr_reader :size
|
|
21
|
+
|
|
22
|
+
# ptr expuesto para que CAPI pueda leerlo directamente
|
|
23
|
+
attr_reader :ptr
|
|
24
|
+
|
|
25
|
+
def initialize(array_plano)
|
|
26
|
+
@size = array_plano.size
|
|
27
|
+
|
|
28
|
+
if CAPI::LOADED
|
|
29
|
+
# --- MODO RÁPIDO: memoria C alineada ---
|
|
30
|
+
# grx_alloc devuelve un double* alineado a 32 bytes
|
|
31
|
+
@ptr = CAPI.grx_alloc(@size)
|
|
32
|
+
raise StorageError, "grx_alloc falló (OOM)" if @ptr.null?
|
|
33
|
+
|
|
34
|
+
# Empaquetamos el Array de Ruby en el buffer C como doubles (little-endian)
|
|
35
|
+
# Array#pack("d*") → String binaria de IEEE 754 doubles
|
|
36
|
+
bytes = array_plano.pack("d*")
|
|
37
|
+
@ptr[0, bytes.bytesize] = bytes
|
|
38
|
+
|
|
39
|
+
# Registramos un finalizer para liberar la memoria C cuando el objeto
|
|
40
|
+
# Ruby sea recolectado por el GC. Usamos ObjectSpace para evitar
|
|
41
|
+
# que el closure capture self (lo que impediría la recolección).
|
|
42
|
+
ptr_to_free = @ptr
|
|
43
|
+
ObjectSpace.define_finalizer(self, self.class.make_finalizer(ptr_to_free))
|
|
44
|
+
else
|
|
45
|
+
# --- MODO FALLBACK: Array de Ruby ---
|
|
46
|
+
@data = array_plano.map(&:to_f)
|
|
47
|
+
@ptr = nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.make_finalizer(ptr)
|
|
52
|
+
proc { CAPI.grx_free(ptr) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# Lectura / escritura — usadas solo en modo fallback y por get()
|
|
57
|
+
# Las operaciones masivas (add, mul, etc.) van directo por @ptr en C.
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
def read(indice)
|
|
60
|
+
if CAPI::LOADED
|
|
61
|
+
# Leemos 8 bytes desde el offset correcto y los desempaquetamos como double
|
|
62
|
+
@ptr[indice * 8, 8].unpack1("d")
|
|
63
|
+
else
|
|
64
|
+
@data[indice]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def write(indice, valor)
|
|
69
|
+
if CAPI::LOADED
|
|
70
|
+
@ptr[indice * 8, 8] = [valor.to_f].pack("d")
|
|
71
|
+
else
|
|
72
|
+
@data[indice] = valor.to_f
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Vuelca todo el buffer a un Array de Ruby (para to_a, inspect, tests)
|
|
77
|
+
def to_ruby_array
|
|
78
|
+
if CAPI::LOADED
|
|
79
|
+
@ptr[0, @size * 8].unpack("d#{@size}")
|
|
80
|
+
else
|
|
81
|
+
@data.dup
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|