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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# =============================================================
|
|
2
|
+
# Makefile.mingw — Windows (MinGW-w64 / MSYS2)
|
|
3
|
+
#
|
|
4
|
+
# Compila grx_core.c DIRECTAMENTE en lib/grx/ (sin archivo intermedio).
|
|
5
|
+
# El único .dll vive en lib/grx/grx_core.dll
|
|
6
|
+
#
|
|
7
|
+
# Requisitos:
|
|
8
|
+
# MSYS2: pacman -S mingw-w64-x86_64-gcc
|
|
9
|
+
#
|
|
10
|
+
# Uso:
|
|
11
|
+
# make -f Makefile.mingw → compila
|
|
12
|
+
# make -f Makefile.mingw clean → elimina el .dll de lib/grx/
|
|
13
|
+
# =============================================================
|
|
14
|
+
|
|
15
|
+
CC = x86_64-w64-mingw32-gcc
|
|
16
|
+
CFLAGS = -O3 -march=native -ffast-math -funroll-loops \
|
|
17
|
+
-Wall -Wextra -std=c11
|
|
18
|
+
LDFLAGS = -lm
|
|
19
|
+
SRC_DIR = ../grx
|
|
20
|
+
SRC = $(SRC_DIR)/grx_core.c
|
|
21
|
+
HEADER = $(SRC_DIR)/grx_core.h
|
|
22
|
+
|
|
23
|
+
# Destino final — directamente en lib/grx/
|
|
24
|
+
OUT_DIR = ../../lib/grx
|
|
25
|
+
LIB = grx_core.dll
|
|
26
|
+
TARGET = $(OUT_DIR)/$(LIB)
|
|
27
|
+
|
|
28
|
+
# __declspec(dllexport) ya está en el header vía GRX_API
|
|
29
|
+
SHARED = -shared -Wl,--out-implib,$(OUT_DIR)/libgrx_core.a
|
|
30
|
+
|
|
31
|
+
AVX2_TEST := $(shell echo 'int main(){}' | $(CC) -mavx2 -mfma -x c - -o NUL 2>&1)
|
|
32
|
+
ifeq ($(AVX2_TEST),)
|
|
33
|
+
CFLAGS += -mavx2 -mfma
|
|
34
|
+
$(info [GRX] AVX2 + FMA habilitados)
|
|
35
|
+
else
|
|
36
|
+
$(info [GRX] Sin AVX2 — modo escalar)
|
|
37
|
+
endif
|
|
38
|
+
|
|
39
|
+
.PHONY: all clean
|
|
40
|
+
|
|
41
|
+
all: $(TARGET)
|
|
42
|
+
|
|
43
|
+
$(TARGET): $(SRC) $(HEADER)
|
|
44
|
+
mkdir -p $(OUT_DIR)
|
|
45
|
+
$(CC) $(CFLAGS) $(SHARED) $(SRC) -o $(TARGET) $(LDFLAGS)
|
|
46
|
+
@echo [GRX] Compilado → $(TARGET)
|
|
47
|
+
|
|
48
|
+
clean:
|
|
49
|
+
rm -f "$(TARGET)" "$(OUT_DIR)/libgrx_core.a" 2>/dev/null || true
|
|
50
|
+
@echo [GRX] Limpiado: $(TARGET)
|
data/grx-tensor.gemspec
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/grx/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "grx-tensor"
|
|
7
|
+
spec.version = GRX::VERSION
|
|
8
|
+
spec.authors = ["Angel Gabriel Garcia Razo"]
|
|
9
|
+
spec.email = ["garabatoangelopolis@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Tensor framework for Ruby with autograd and a C+SIMD compute core"
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
GRX brings PyTorch-style tensor operations to Ruby. Every arithmetic op,
|
|
14
|
+
activation, and optimizer step runs through a native C library compiled
|
|
15
|
+
with AVX2+FMA SIMD. Ruby is the interface — C does the work.
|
|
16
|
+
|
|
17
|
+
Features: autograd, SGD/Adam optimizers, Linear/Sequential/Dropout/BatchNorm
|
|
18
|
+
layers, MSE/BCE/CrossEntropy loss functions, Xavier and He weight init.
|
|
19
|
+
Cross-platform: .so on Linux, .dylib on macOS, .dll on Windows.
|
|
20
|
+
DESC
|
|
21
|
+
|
|
22
|
+
spec.homepage = "https://github.com/Gabo-Razo/grx-tensor"
|
|
23
|
+
spec.license = "MIT"
|
|
24
|
+
|
|
25
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
26
|
+
|
|
27
|
+
spec.metadata = {
|
|
28
|
+
"homepage_uri" => spec.homepage,
|
|
29
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md",
|
|
30
|
+
"bug_tracker_uri" => "#{spec.homepage}/issues"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
spec.files = Dir[
|
|
34
|
+
"lib/**/*.rb",
|
|
35
|
+
"ext/grx/**/*.{c,h,rb}",
|
|
36
|
+
"ext/unix/Makefile",
|
|
37
|
+
"ext/windows/Makefile.mingw",
|
|
38
|
+
"*.gemspec",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE.txt",
|
|
41
|
+
"CHANGELOG.md"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
spec.require_paths = ["lib"]
|
|
45
|
+
|
|
46
|
+
# rake-compiler compiles ext/grx/extconf.rb on `gem install`
|
|
47
|
+
spec.extensions = ["ext/grx/extconf.rb"]
|
|
48
|
+
|
|
49
|
+
spec.post_install_message = <<~MSG
|
|
50
|
+
|
|
51
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
52
|
+
GRX-Tensor #{GRX::VERSION} installed
|
|
53
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
54
|
+
|
|
55
|
+
Compile the C extension to enable AVX2+FMA SIMD:
|
|
56
|
+
|
|
57
|
+
Linux / macOS: make -C ext/unix
|
|
58
|
+
Windows: make -C ext/windows -f Makefile.mingw
|
|
59
|
+
|
|
60
|
+
Without it, GRX runs in pure Ruby fallback mode (slower but correct).
|
|
61
|
+
|
|
62
|
+
Quick start:
|
|
63
|
+
|
|
64
|
+
require "grx"
|
|
65
|
+
|
|
66
|
+
a = GRX.tensor([1.0, 2.0, 3.0], [3], requires_grad: true)
|
|
67
|
+
b = GRX.tensor([4.0, 5.0, 6.0], [3], requires_grad: true)
|
|
68
|
+
c = a + b
|
|
69
|
+
c.backward
|
|
70
|
+
puts a.grad.to_a # [1.0, 1.0, 1.0]
|
|
71
|
+
|
|
72
|
+
net = GRX::NN::Sequential.new(
|
|
73
|
+
GRX::NN::Linear.new(4, 16),
|
|
74
|
+
GRX::NN::ReLU.new,
|
|
75
|
+
GRX::NN::Linear.new(16, 1)
|
|
76
|
+
)
|
|
77
|
+
opt = GRX::Optim::Adam.new(net.parameters, lr: 0.001)
|
|
78
|
+
|
|
79
|
+
Docs: https://github.com/Gabo-Razo/grx-tensor
|
|
80
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
81
|
+
|
|
82
|
+
MSG
|
|
83
|
+
|
|
84
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
85
|
+
spec.add_development_dependency "rake-compiler", "~> 1.2"
|
|
86
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
87
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
88
|
+
end
|
data/lib/grx/c_api.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
require "fiddle/import"
|
|
5
|
+
|
|
6
|
+
module GRX
|
|
7
|
+
module CAPI
|
|
8
|
+
extend Fiddle::Importer
|
|
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
|
|
15
|
+
|
|
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__),
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
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
|
|
40
|
+
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)."
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if LOADED
|
|
48
|
+
# Memoria
|
|
49
|
+
extern "double* grx_alloc(size_t)"
|
|
50
|
+
extern "void grx_free(double*)"
|
|
51
|
+
|
|
52
|
+
# Aritmética element-wise
|
|
53
|
+
extern "void grx_add (double*, double*, double*, size_t)"
|
|
54
|
+
extern "void grx_sub (double*, double*, double*, size_t)"
|
|
55
|
+
extern "void grx_mul (double*, double*, double*, size_t)"
|
|
56
|
+
extern "void grx_div (double*, double*, double*, size_t)"
|
|
57
|
+
extern "void grx_scale (double*, double, double*, size_t)"
|
|
58
|
+
extern "void grx_add_scalar(double*, double, double*, size_t)"
|
|
59
|
+
extern "void grx_negate (double*, double*, size_t)"
|
|
60
|
+
|
|
61
|
+
# Matemáticas element-wise
|
|
62
|
+
extern "void grx_abs (double*, double*, size_t)"
|
|
63
|
+
extern "void grx_sqrt (double*, double*, size_t)"
|
|
64
|
+
extern "void grx_square(double*, double*, size_t)"
|
|
65
|
+
extern "void grx_log (double*, double*, size_t)"
|
|
66
|
+
extern "void grx_exp (double*, double*, size_t)"
|
|
67
|
+
extern "void grx_pow (double*, double, double*, size_t)"
|
|
68
|
+
extern "void grx_clip (double*, double, double, double*, size_t)"
|
|
69
|
+
|
|
70
|
+
# Reducciones
|
|
71
|
+
extern "double grx_sum (double*, size_t)"
|
|
72
|
+
extern "double grx_mean(double*, size_t)"
|
|
73
|
+
extern "double grx_max (double*, size_t)"
|
|
74
|
+
extern "double grx_min (double*, size_t)"
|
|
75
|
+
|
|
76
|
+
# Álgebra lineal
|
|
77
|
+
extern "double grx_dot (double*, double*, size_t)"
|
|
78
|
+
extern "void grx_matmul (double*, double*, double*, size_t, size_t, size_t)"
|
|
79
|
+
|
|
80
|
+
# Activaciones
|
|
81
|
+
extern "void grx_relu (double*, double*, size_t)"
|
|
82
|
+
extern "void grx_leaky_relu (double*, double, double*, size_t)"
|
|
83
|
+
extern "void grx_tanh_act (double*, double*, size_t)"
|
|
84
|
+
extern "void grx_sigmoid (double*, double*, size_t)"
|
|
85
|
+
extern "void grx_softmax (double*, double*, size_t)"
|
|
86
|
+
|
|
87
|
+
# Optimizadores
|
|
88
|
+
extern "void grx_sgd_step (double*, double*, double, size_t)"
|
|
89
|
+
extern "void grx_adam_step(double*, double*, double*, double*, double, double, double, double, double, double, size_t)"
|
|
90
|
+
|
|
91
|
+
# Inicialización de pesos
|
|
92
|
+
extern "void grx_init_xavier_uniform(double*, size_t, size_t, size_t)"
|
|
93
|
+
extern "void grx_init_he_normal (double*, size_t, size_t)"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/grx/errors.rb
ADDED
data/lib/grx/loss.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GRX
|
|
4
|
+
module Loss
|
|
5
|
+
# ================================================================
|
|
6
|
+
# MSELoss — Mean Squared Error
|
|
7
|
+
# L = mean((pred - target)^2) → retorna Float
|
|
8
|
+
# ================================================================
|
|
9
|
+
class MSELoss
|
|
10
|
+
def call(pred, target)
|
|
11
|
+
raise ShapeError, "Shapes incompatibles" if pred.shape != target.shape
|
|
12
|
+
(pred - target).square.mean
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# ================================================================
|
|
17
|
+
# MAELoss — Mean Absolute Error
|
|
18
|
+
# L = mean(|pred - target|)
|
|
19
|
+
# ================================================================
|
|
20
|
+
class MAELoss
|
|
21
|
+
def call(pred, target)
|
|
22
|
+
raise ShapeError, "Shapes incompatibles" if pred.shape != target.shape
|
|
23
|
+
(pred - target).abs.mean
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# ================================================================
|
|
28
|
+
# BCELoss — Binary Cross-Entropy
|
|
29
|
+
# L = -mean(t*log(p) + (1-t)*log(1-p))
|
|
30
|
+
# pred debe estar en (0,1) — aplica sigmoid antes si usas logits.
|
|
31
|
+
# ================================================================
|
|
32
|
+
class BCELoss
|
|
33
|
+
EPS = 1e-7
|
|
34
|
+
|
|
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
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ================================================================
|
|
49
|
+
# CrossEntropyLoss — Softmax + NLL
|
|
50
|
+
# L = -mean(sum(target * log(softmax(logits))))
|
|
51
|
+
# ================================================================
|
|
52
|
+
class CrossEntropyLoss
|
|
53
|
+
EPS = 1e-7
|
|
54
|
+
|
|
55
|
+
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 }
|
|
58
|
+
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
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ================================================================
|
|
65
|
+
# HuberLoss — Smooth L1
|
|
66
|
+
# ================================================================
|
|
67
|
+
class HuberLoss
|
|
68
|
+
def initialize(delta: 1.0)
|
|
69
|
+
@delta = delta
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def call(pred, target)
|
|
73
|
+
raise ShapeError, "Shapes incompatibles" if pred.shape != target.shape
|
|
74
|
+
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
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/grx/nn.rb
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GRX
|
|
4
|
+
module NN
|
|
5
|
+
# ================================================================
|
|
6
|
+
# Module — clase base para todas las capas
|
|
7
|
+
# ================================================================
|
|
8
|
+
class Module
|
|
9
|
+
# Retorna todos los parámetros entrenables (para pasarlos al optimizador)
|
|
10
|
+
def parameters
|
|
11
|
+
instance_variables.flat_map do |var|
|
|
12
|
+
val = instance_variable_get(var)
|
|
13
|
+
case val
|
|
14
|
+
when Tensor then val.requires_grad ? [val] : []
|
|
15
|
+
when Module then val.parameters
|
|
16
|
+
when Array then val.flat_map { |v|
|
|
17
|
+
case v
|
|
18
|
+
when Tensor then v.requires_grad ? [v] : []
|
|
19
|
+
when Module then v.parameters
|
|
20
|
+
else []
|
|
21
|
+
end
|
|
22
|
+
}
|
|
23
|
+
else []
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def zero_grad
|
|
29
|
+
parameters.each(&:zero_grad!)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Subclases implementan forward
|
|
33
|
+
def call(*args)
|
|
34
|
+
forward(*args)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# ================================================================
|
|
39
|
+
# Linear — Capa densa (fully connected)
|
|
40
|
+
# y = x @ W^T + b
|
|
41
|
+
# ================================================================
|
|
42
|
+
class Linear < Module
|
|
43
|
+
attr_reader :weight, :bias
|
|
44
|
+
|
|
45
|
+
def initialize(in_features, out_features, bias: true)
|
|
46
|
+
@in_features = in_features
|
|
47
|
+
@out_features = out_features
|
|
48
|
+
@use_bias = bias
|
|
49
|
+
|
|
50
|
+
# Pesos: Xavier uniform (bueno para tanh/sigmoid)
|
|
51
|
+
@weight = Tensor.xavier_uniform([out_features, in_features], requires_grad: true)
|
|
52
|
+
|
|
53
|
+
# Bias: ceros
|
|
54
|
+
@bias = bias ? Tensor.zeros([out_features], requires_grad: true) : nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def forward(x)
|
|
58
|
+
# x: [batch, in_features] → out: [batch, out_features]
|
|
59
|
+
# out = x @ W^T
|
|
60
|
+
out = x.matmul(@weight.transpose)
|
|
61
|
+
|
|
62
|
+
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
|
+
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
|
+
bias_tiled = _tile_bias(@bias, batch_size, @out_features)
|
|
70
|
+
out + bias_tiled
|
|
71
|
+
else
|
|
72
|
+
out
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
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
|
+
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
|
+
data = Array.new(batch_size) { bias.to_a }.flatten
|
|
84
|
+
tiled = GRX::Tensor.create(data, [batch_size, out_features])
|
|
85
|
+
# Conectamos al bias original via suma con ceros — mantiene el grafo
|
|
86
|
+
zero_row = GRX::Tensor.zeros([batch_size, out_features])
|
|
87
|
+
result = zero_row + tiled
|
|
88
|
+
# Registramos manualmente la conexión al bias para backprop
|
|
89
|
+
if bias.requires_grad
|
|
90
|
+
result.requires_grad = true
|
|
91
|
+
result._grafo_hijos << bias
|
|
92
|
+
b = bias
|
|
93
|
+
bf = result.backward_fn
|
|
94
|
+
result.backward_fn = ->(g) {
|
|
95
|
+
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|
|
|
98
|
+
acc.zip(row).map { |a, r| a + r }
|
|
99
|
+
}
|
|
100
|
+
b.agregar_gradiente(GRX::Tensor.create(grad_data, [out_features]))
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
public
|
|
107
|
+
|
|
108
|
+
def to_s
|
|
109
|
+
"Linear(#{@in_features} → #{@out_features}, bias: #{@use_bias})"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ================================================================
|
|
114
|
+
# Sequential — Contenedor de capas en secuencia
|
|
115
|
+
# ================================================================
|
|
116
|
+
class Sequential < Module
|
|
117
|
+
def initialize(*layers)
|
|
118
|
+
@layers = layers
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def forward(x)
|
|
122
|
+
@layers.reduce(x) { |input, layer| layer.call(input) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parameters
|
|
126
|
+
@layers.flat_map(&:parameters)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def to_s
|
|
130
|
+
layers_str = @layers.each_with_index.map { |l, i| " (#{i}): #{l}" }.join("\n")
|
|
131
|
+
"Sequential(\n#{layers_str}\n)"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# ================================================================
|
|
136
|
+
# Activaciones como capas (para usar en Sequential)
|
|
137
|
+
# ================================================================
|
|
138
|
+
class ReLU < Module
|
|
139
|
+
def forward(x) = x.relu
|
|
140
|
+
def to_s = "ReLU()"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class LeakyReLU < Module
|
|
144
|
+
def initialize(alpha = 0.01)
|
|
145
|
+
@alpha = alpha
|
|
146
|
+
end
|
|
147
|
+
def forward(x) = x.leaky_relu(@alpha)
|
|
148
|
+
def to_s = "LeakyReLU(alpha=#{@alpha})"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class Tanh < Module
|
|
152
|
+
def forward(x) = x.tanh
|
|
153
|
+
def to_s = "Tanh()"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
class Sigmoid < Module
|
|
157
|
+
def forward(x) = x.sigmoid
|
|
158
|
+
def to_s = "Sigmoid()"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class Softmax < Module
|
|
162
|
+
def forward(x) = x.softmax
|
|
163
|
+
def to_s = "Softmax()"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# ================================================================
|
|
167
|
+
# Dropout — regularización durante entrenamiento
|
|
168
|
+
# ================================================================
|
|
169
|
+
class Dropout < Module
|
|
170
|
+
def initialize(p = 0.5)
|
|
171
|
+
@p = p
|
|
172
|
+
@training = true
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def train!; @training = true; self; end
|
|
176
|
+
def eval!; @training = false; self; end
|
|
177
|
+
|
|
178
|
+
def forward(x)
|
|
179
|
+
return x unless @training && @p > 0
|
|
180
|
+
|
|
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
|
+
scale = 1.0 / (1.0 - @p)
|
|
184
|
+
mask_data = x.to_a.map { rand > @p ? scale : 0.0 }
|
|
185
|
+
mask = Tensor.create(mask_data, x.shape)
|
|
186
|
+
x * mask
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def to_s = "Dropout(p=#{@p})"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# ================================================================
|
|
193
|
+
# BatchNorm1d — Normalización por batch
|
|
194
|
+
# Estabiliza el entrenamiento de redes profundas.
|
|
195
|
+
# ================================================================
|
|
196
|
+
class BatchNorm1d < Module
|
|
197
|
+
def initialize(num_features, epsilon: 1e-5, momentum: 0.1)
|
|
198
|
+
@num_features = num_features
|
|
199
|
+
@epsilon = epsilon
|
|
200
|
+
@momentum = momentum
|
|
201
|
+
@training = true
|
|
202
|
+
|
|
203
|
+
# Parámetros entrenables
|
|
204
|
+
@gamma = Tensor.ones([num_features], requires_grad: true)
|
|
205
|
+
@beta = Tensor.zeros([num_features], requires_grad: true)
|
|
206
|
+
|
|
207
|
+
# Estadísticas corrientes (no entrenables, para inferencia)
|
|
208
|
+
@running_mean = Tensor.zeros([num_features])
|
|
209
|
+
@running_var = Tensor.ones([num_features])
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def train!; @training = true; self; end
|
|
213
|
+
def eval!; @training = false; self; end
|
|
214
|
+
|
|
215
|
+
def forward(x)
|
|
216
|
+
# x: [batch, num_features]
|
|
217
|
+
batch_size = x.shape[0]
|
|
218
|
+
|
|
219
|
+
if @training
|
|
220
|
+
# Calculamos media y varianza del batch
|
|
221
|
+
batch_data = x.to_a
|
|
222
|
+
means = Array.new(@num_features) do |j|
|
|
223
|
+
batch_data.each_slice(@num_features).map { |row| row[j] }.sum / batch_size
|
|
224
|
+
end
|
|
225
|
+
vars = Array.new(@num_features) do |j|
|
|
226
|
+
col = batch_data.each_slice(@num_features).map { |row| row[j] }
|
|
227
|
+
col.sum { |v| (v - means[j]) ** 2 } / batch_size
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Actualizamos estadísticas corrientes
|
|
231
|
+
means.each_with_index do |m, j|
|
|
232
|
+
rm = @running_mean.to_a; rm[j] = (1 - @momentum) * rm[j] + @momentum * m
|
|
233
|
+
@running_mean = Tensor.create(rm, [@num_features])
|
|
234
|
+
end
|
|
235
|
+
vars.each_with_index do |v, j|
|
|
236
|
+
rv = @running_var.to_a; rv[j] = (1 - @momentum) * rv[j] + @momentum * v
|
|
237
|
+
@running_var = Tensor.create(rv, [@num_features])
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
mean_t = Tensor.create(means, [@num_features])
|
|
241
|
+
var_t = Tensor.create(vars, [@num_features])
|
|
242
|
+
else
|
|
243
|
+
mean_t = @running_mean
|
|
244
|
+
var_t = @running_var
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Normalizamos: x_hat = (x - mean) / sqrt(var + eps)
|
|
248
|
+
# Luego escalamos: y = gamma * x_hat + beta
|
|
249
|
+
norm_data = x.to_a.each_slice(@num_features).flat_map do |row|
|
|
250
|
+
row.each_with_index.map do |v, j|
|
|
251
|
+
x_hat = (v - mean_t.to_a[j]) / Math.sqrt(var_t.to_a[j] + @epsilon)
|
|
252
|
+
@gamma.to_a[j] * x_hat + @beta.to_a[j]
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
Tensor.create(norm_data, x.shape)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def to_s = "BatchNorm1d(#{@num_features})"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|