grydra 0.1.2
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/lib/gr/core.rb +698 -0
- data/lib/gr/version.rb +3 -0
- data/lib/grydra.rb +2 -0
- metadata +44 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e449d883a411813be922b8336a3e8044d33c36cbecc7590472cf98ba83d7bd24
|
|
4
|
+
data.tar.gz: d95118b578b70a82f539153a203be2afb11be345fc5bf317d010ea476b43225c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 83d5f6bad1790e03bd60cf59f061c4e1c3b23eef6dbd4e7a04d475b1262c9543a1cbea0188f6d84fde6de04e927142ef8f5d1f220784e12d7416ad002c06caab
|
|
7
|
+
data.tar.gz: 64b5c314cb50f51cc0c01ac6763c2a643442be772b96acbeed26f0c5cb3b54248452c31f4549e06008aa070fc53a9ac07217834b6fd93a6dd3e3b8aa9b1f27ec
|
data/lib/gr/core.rb
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
module GR
|
|
2
|
+
require 'set'
|
|
3
|
+
|
|
4
|
+
### FUNCIONES DE ACTIVACIÓN ###
|
|
5
|
+
def self.tanh(x)
|
|
6
|
+
Math.tanh(x)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.derivada_tanh(x)
|
|
10
|
+
1 - tanh(x)**2
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.relu(x)
|
|
14
|
+
x > 0 ? x : 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.derivada_relu(x)
|
|
18
|
+
x > 0 ? 1 : 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.sigmoid(x)
|
|
22
|
+
1.0 / (1.0 + Math.exp(-x))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.derivada_sigmoid(x)
|
|
26
|
+
s = sigmoid(x)
|
|
27
|
+
s * (1 - s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.softmax(vector)
|
|
31
|
+
max = vector.max
|
|
32
|
+
exps = vector.map { |v| Math.exp(v - max) }
|
|
33
|
+
sum = exps.sum
|
|
34
|
+
exps.map { |v| v / sum }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
### INICIALIZACIÓN XAVIER ###
|
|
38
|
+
def self.xavier_init(num_inputs)
|
|
39
|
+
limit = Math.sqrt(6.0 / num_inputs)
|
|
40
|
+
rand * 2 * limit - limit
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
### NORMALIZACIÓN Z-SCORE ###
|
|
44
|
+
def self.normalizar_zscore(datos)
|
|
45
|
+
n = datos.size
|
|
46
|
+
medias = datos.first.size.times.map { |i| datos.map { |fila| fila[i] }.sum.to_f / n }
|
|
47
|
+
desviaciones = datos.first.size.times.map do |i|
|
|
48
|
+
m = medias[i]
|
|
49
|
+
Math.sqrt(datos.map { |fila| (fila[i] - m)**2 }.sum.to_f / n)
|
|
50
|
+
end
|
|
51
|
+
normalizados = datos.map do |fila|
|
|
52
|
+
fila.each_with_index.map { |valor, i| desviaciones[i] != 0 ? (valor - medias[i]) / desviaciones[i] : 0 }
|
|
53
|
+
end
|
|
54
|
+
return normalizados, medias, desviaciones
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.desnormalizar_zscore(normalizados, medias, desviaciones)
|
|
58
|
+
normalizados.map do |fila|
|
|
59
|
+
fila.each_with_index.map { |valor, i| valor * desviaciones[i] + medias[i] }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
### MÉTRICAS DE EVALUACIÓN ###
|
|
64
|
+
def self.mse(predicciones, reales)
|
|
65
|
+
n = predicciones.size
|
|
66
|
+
sum = predicciones.zip(reales).map { |p, r| (p - r)**2 }.sum
|
|
67
|
+
sum / n.to_f
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.mae(predicciones, reales)
|
|
71
|
+
n = predicciones.size
|
|
72
|
+
sum = predicciones.zip(reales).map { |p, r| (p - r).abs }.sum
|
|
73
|
+
sum / n.to_f
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.precision(tp, fp)
|
|
77
|
+
tp.to_f / (tp + fp)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.recall(tp, fn)
|
|
81
|
+
tp.to_f / (tp + fn)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.f1(precision, recall)
|
|
85
|
+
2 * (precision * recall) / (precision + recall)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
### CLASE NEURONA ###
|
|
89
|
+
class Neurona
|
|
90
|
+
attr_accessor :pesos, :sesgo, :salida, :delta
|
|
91
|
+
|
|
92
|
+
def initialize(entradas, activacion=:tanh)
|
|
93
|
+
raise ArgumentError, "El número de entradas debe ser un entero positivo" unless entradas.is_a?(Integer) && entradas > 0
|
|
94
|
+
@pesos = Array.new(entradas) { GR.xavier_init(entradas) }
|
|
95
|
+
@sesgo = GR.xavier_init(entradas)
|
|
96
|
+
@salida = 0
|
|
97
|
+
@delta = 0
|
|
98
|
+
@activacion = activacion
|
|
99
|
+
@suma = 0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def calcular_salida(entradas)
|
|
103
|
+
raise ArgumentError, "Las entradas deben ser un array de números" unless entradas.is_a?(Array) && entradas.all? { |e| e.is_a?(Numeric) }
|
|
104
|
+
if @pesos.size != entradas.size
|
|
105
|
+
raise "Error: entradas (#{entradas.size}) no coinciden con pesos (#{@pesos.size})"
|
|
106
|
+
end
|
|
107
|
+
@suma = @pesos.zip(entradas).map { |peso, entrada| peso * entrada }.sum + @sesgo
|
|
108
|
+
@salida = case @activacion
|
|
109
|
+
when :tanh then GR.tanh(@suma)
|
|
110
|
+
when :relu then GR.relu(@suma)
|
|
111
|
+
when :sigmoid then GR.sigmoid(@suma)
|
|
112
|
+
else @suma
|
|
113
|
+
end
|
|
114
|
+
@salida
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def derivada_activacion
|
|
118
|
+
case @activacion
|
|
119
|
+
when :tanh then GR.derivada_tanh(@salida)
|
|
120
|
+
when :relu then GR.derivada_relu(@salida)
|
|
121
|
+
when :sigmoid then GR.derivada_sigmoid(@suma)
|
|
122
|
+
else 1
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
### CLASE CAPA BASE ###
|
|
128
|
+
class Capa
|
|
129
|
+
def calcular_salidas(entradas)
|
|
130
|
+
raise NotImplementedError, "Implementar en subclase"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
### CLASE CAPA DENSE ###
|
|
135
|
+
class CapaDensa < Capa
|
|
136
|
+
attr_accessor :neuronas, :activacion
|
|
137
|
+
|
|
138
|
+
def initialize(numero_neuronas, entradas_por_neurona, activacion=:tanh)
|
|
139
|
+
@activacion = activacion
|
|
140
|
+
@neuronas = Array.new(numero_neuronas) { Neurona.new(entradas_por_neurona, activacion) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def calcular_salidas(entradas)
|
|
144
|
+
@neuronas.map { |neurona| neurona.calcular_salida(entradas) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
### CLASE RED NEURONAL ###
|
|
149
|
+
class RedNeuronal
|
|
150
|
+
attr_accessor :capas
|
|
151
|
+
|
|
152
|
+
def initialize(estructura, imprimir_epocas = false, activaciones = nil)
|
|
153
|
+
@imprimir_epocas = imprimir_epocas
|
|
154
|
+
@capas = []
|
|
155
|
+
activaciones ||= Array.new(estructura.size - 1, :tanh)
|
|
156
|
+
estructura.each_cons(2).with_index do |(entradas, salidas), i|
|
|
157
|
+
@capas << CapaDensa.new(salidas, entradas, activaciones[i])
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def calcular_salidas(entradas)
|
|
162
|
+
raise ArgumentError, "Entradas deben ser array de números" unless entradas.is_a?(Array) && entradas.all? { |e| e.is_a?(Numeric) }
|
|
163
|
+
@capas.inject(entradas) { |salidas, capa| capa.calcular_salidas(salidas) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Entrenamiento con mini-batch, early stopping, decay learning rate
|
|
167
|
+
def entrenar(datos_entrada, datos_salida, tasa_aprendizaje, epocas, umbral_error = nil, batch_size: 1, paciencia: 5, decay: 0.95)
|
|
168
|
+
mejor_error = Float::INFINITY
|
|
169
|
+
contador_paciencia = 0
|
|
170
|
+
|
|
171
|
+
epocas.times do |epoca|
|
|
172
|
+
error_total = 0
|
|
173
|
+
|
|
174
|
+
# Shuffle datos
|
|
175
|
+
indices = (0...datos_entrada.size).to_a.shuffle
|
|
176
|
+
datos_entrada = indices.map { |i| datos_entrada[i] }
|
|
177
|
+
datos_salida = indices.map { |i| datos_salida[i] }
|
|
178
|
+
|
|
179
|
+
datos_entrada.each_slice(batch_size).with_index do |batch_entradas, batch_idx|
|
|
180
|
+
batch_salidas_reales = datos_salida[batch_idx*batch_size, batch_size]
|
|
181
|
+
|
|
182
|
+
batch_entradas.zip(batch_salidas_reales).each do |entrada, salida_real|
|
|
183
|
+
salidas = calcular_salidas(entrada)
|
|
184
|
+
errores = salidas.zip(salida_real).map { |salida, real| real - salida }
|
|
185
|
+
error_total += errores.map { |e| e**2 }.sum / errores.size
|
|
186
|
+
|
|
187
|
+
# Capa salida
|
|
188
|
+
@capas.last.neuronas.each_with_index do |neurona, i|
|
|
189
|
+
neurona.delta = errores[i] * neurona.derivada_activacion
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Backpropagation capas ocultas
|
|
193
|
+
(@capas.size - 2).downto(0) do |i|
|
|
194
|
+
@capas[i].neuronas.each_with_index do |neurona, j|
|
|
195
|
+
sum_deltas = @capas[i + 1].neuronas.sum { |n| n.pesos[j] * n.delta }
|
|
196
|
+
neurona.delta = sum_deltas * neurona.derivada_activacion
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Actualización pesos y sesgo
|
|
201
|
+
@capas.each_with_index do |capa, idx|
|
|
202
|
+
entradas_capa = idx.zero? ? entrada : @capas[idx - 1].neuronas.map(&:salida)
|
|
203
|
+
capa.neuronas.each do |neurona|
|
|
204
|
+
neurona.pesos.each_with_index do |peso, i|
|
|
205
|
+
neurona.pesos[i] += tasa_aprendizaje * neurona.delta * entradas_capa[i]
|
|
206
|
+
end
|
|
207
|
+
neurona.sesgo += tasa_aprendizaje * neurona.delta
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if umbral_error && error_total < umbral_error
|
|
214
|
+
puts "Error umbral alcanzado en época #{epoca+1}: #{error_total}"
|
|
215
|
+
break
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if error_total < mejor_error
|
|
219
|
+
mejor_error = error_total
|
|
220
|
+
contador_paciencia = 0
|
|
221
|
+
else
|
|
222
|
+
contador_paciencia += 1
|
|
223
|
+
if contador_paciencia >= paciencia
|
|
224
|
+
puts "Early stopping en época #{epoca+1}, error no mejora desde hace #{paciencia} épocas."
|
|
225
|
+
break
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
tasa_aprendizaje *= decay
|
|
230
|
+
|
|
231
|
+
puts "Época #{epoca+1}, Error total: #{error_total.round(6)}, tasa_aprendizaje: #{tasa_aprendizaje.round(6)}" if @imprimir_epocas
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def info_red
|
|
236
|
+
puts "Red neuronal con #{@capas.size} capas:"
|
|
237
|
+
@capas.each_with_index do |capa, i|
|
|
238
|
+
puts " Capa #{i+1}: #{capa.neuronas.size} neuronas, activación: #{capa.activacion}"
|
|
239
|
+
capa.neuronas.each_with_index do |neurona, j|
|
|
240
|
+
puts " Neurona #{j+1}: Pesos=#{neurona.pesos.map { |p| p.round(3) }}, Sesgo=#{neurona.sesgo.round(3)}"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Exportar a DOT para Graphviz
|
|
246
|
+
def exportar_graphviz(nombre_archivo = "red_neuronal.dot")
|
|
247
|
+
File.open(nombre_archivo, "w") do |f|
|
|
248
|
+
f.puts "digraph RedNeuronal {"
|
|
249
|
+
@capas.each_with_index do |capa, i|
|
|
250
|
+
capa.neuronas.each_with_index do |neurona, j|
|
|
251
|
+
nodo = "C#{i}_N#{j}"
|
|
252
|
+
f.puts " #{nodo} [label=\"N#{j+1}\"];"
|
|
253
|
+
if i < @capas.size - 1
|
|
254
|
+
@capas[i+1].neuronas.each_with_index do |sig_neurona, k|
|
|
255
|
+
peso = sig_neurona.pesos[j].round(3)
|
|
256
|
+
f.puts " #{nodo} -> C#{i+1}_N#{k} [label=\"#{peso}\"];"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
f.puts "}"
|
|
262
|
+
end
|
|
263
|
+
puts "Red exportada a #{nombre_archivo} (Graphviz DOT)"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
### RED PRINCIPAL ###
|
|
268
|
+
class RedPrincipal
|
|
269
|
+
attr_accessor :subredes
|
|
270
|
+
|
|
271
|
+
def initialize(imprimir_epocas = false)
|
|
272
|
+
@subredes = []
|
|
273
|
+
@imprimir_epocas = imprimir_epocas
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def agregar_subred(estructura, activaciones=nil)
|
|
277
|
+
@subredes << RedNeuronal.new(estructura, @imprimir_epocas, activaciones)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def entrenar_subredes(datos, tasa_aprendizaje, epocas, **opts)
|
|
281
|
+
datos.each_with_index do |datos_subred, index|
|
|
282
|
+
puts "Entrenando Subred #{index + 1}..."
|
|
283
|
+
@subredes[index].entrenar(datos_subred[:entrada], datos_subred[:salida], tasa_aprendizaje, epocas, **opts)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def combinar_resultados(entrada_principal)
|
|
288
|
+
salidas_subredes = @subredes.map { |subred| subred.calcular_salidas(entrada_principal) }
|
|
289
|
+
salidas_subredes.transpose.map { |salidas| salidas.sum / salidas.size }
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
### GUARDAR Y CARGAR MODELO Y VOCABULARIO ###
|
|
294
|
+
def self.guardar_modelo(modelo, nombre, path = File.dirname(__FILE__), vocabulario=nil)
|
|
295
|
+
File.open("#{nombre}.red", "wb") { |f| Marshal.dump(modelo, f) }
|
|
296
|
+
puts "\e[33mModelo guardado en '#{path}'\e[0m"
|
|
297
|
+
if vocabulario
|
|
298
|
+
guardar_vocabulario(vocabulario, nombre, path)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def self.cargar_modelo(nombre, path = File.dirname(__FILE__))
|
|
303
|
+
modelo = nil
|
|
304
|
+
File.open("#{nombre}.red", "rb") { |f| modelo = Marshal.load(f) }
|
|
305
|
+
modelo
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def self.guardar_vocabulario(vocabulario, nombre, path = File.dirname(__FILE__))
|
|
309
|
+
File.open("#{nombre}_vocab.bin", "wb") { |f| Marshal.dump(vocabulario, f) }
|
|
310
|
+
puts "\e[33mVocabulario guardado en '#{path}'\e[0m"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def self.cargar_vocabulario(nombre, path = File.dirname(__FILE__))
|
|
314
|
+
vocabulario = nil
|
|
315
|
+
File.open("#{nombre}_vocab.bin", "rb") { |f| vocabulario = Marshal.load(f) }
|
|
316
|
+
vocabulario
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
### PREPROCESAMIENTO ACTUALIZADO ###
|
|
320
|
+
def self.normalizar_varios(datos, maximos, metodo=:max)
|
|
321
|
+
case metodo
|
|
322
|
+
when :max
|
|
323
|
+
datos.map do |fila|
|
|
324
|
+
fila.each_with_index.map { |valor, idx| valor.to_f / maximos[idx] }
|
|
325
|
+
end
|
|
326
|
+
when :zscore
|
|
327
|
+
medias = maximos[:medias]
|
|
328
|
+
desviaciones = maximos[:desviaciones]
|
|
329
|
+
datos.map do |fila|
|
|
330
|
+
fila.each_with_index.map do |valor, idx|
|
|
331
|
+
desviaciones[idx] != 0 ? (valor.to_f - medias[idx]) / desviaciones[idx] : 0
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
else
|
|
335
|
+
raise "Método de normalización desconocido"
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def self.calcular_maximos(datos, metodo=:max)
|
|
340
|
+
if metodo == :max
|
|
341
|
+
maximos = {}
|
|
342
|
+
datos.first.size.times do |i|
|
|
343
|
+
maximos[i] = datos.map { |fila| fila[i] }.max.to_f
|
|
344
|
+
end
|
|
345
|
+
maximos
|
|
346
|
+
elsif metodo == :zscore
|
|
347
|
+
medias = []
|
|
348
|
+
desviaciones = []
|
|
349
|
+
n = datos.size
|
|
350
|
+
|
|
351
|
+
medias = datos.first.size.times.map do |i|
|
|
352
|
+
datos.map { |fila| fila[i] }.sum.to_f / n
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
desviaciones = datos.first.size.times.map do |i|
|
|
356
|
+
m = medias[i]
|
|
357
|
+
Math.sqrt(datos.map { |fila| (fila[i] - m)**2 }.sum.to_f / n)
|
|
358
|
+
end
|
|
359
|
+
{ medias: medias, desviaciones: desviaciones }
|
|
360
|
+
else
|
|
361
|
+
raise "Método desconocido para calcular máximos"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
### FUNCIONES DE TEXTO ###
|
|
366
|
+
def self.crear_vocabulario(textos)
|
|
367
|
+
textos.map(&:split).flatten.map(&:downcase).uniq
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def self.vectorizar_texto(texto, vocabulario)
|
|
371
|
+
vector = Array.new(vocabulario.size, 0)
|
|
372
|
+
palabras = texto.downcase.split
|
|
373
|
+
palabras.each do |palabra|
|
|
374
|
+
indice = vocabulario.index(palabra)
|
|
375
|
+
vector[indice] = 1 if indice
|
|
376
|
+
end
|
|
377
|
+
vector
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def self.normalizar_con_vocabulario(datos, vocabulario)
|
|
381
|
+
max_valor = vocabulario.size
|
|
382
|
+
datos.map { |vector| vector.map { |v| v.to_f / max_valor } }
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
### CLASE FacilRed (sin cambios, solo agregando opción normalización zscore y activaciones) ###
|
|
386
|
+
class FacilRed
|
|
387
|
+
attr_accessor :red, :vocabulario, :max_valores, :max_valores_salida
|
|
388
|
+
|
|
389
|
+
def initialize(imprimir_epocas = false)
|
|
390
|
+
@red = GR::RedPrincipal.new(imprimir_epocas)
|
|
391
|
+
@vocabulario = nil
|
|
392
|
+
@max_valores = {}
|
|
393
|
+
@max_valores_salida = {}
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# --------- Para datos tipo hash ---------
|
|
397
|
+
def entrenar_hashes(datos_hash, claves_entrada, clave_etiqueta, estructuras, tasa, epocas, normalizacion=:max)
|
|
398
|
+
@red.subredes.clear # limpiar subredes previas
|
|
399
|
+
|
|
400
|
+
entradas = datos_hash.map do |item|
|
|
401
|
+
claves_entrada.map do |clave|
|
|
402
|
+
valor = item[clave]
|
|
403
|
+
valor == true ? 1.0 : valor == false ? 0.0 : valor.to_f
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
@max_valores = GR.calcular_maximos(entradas, normalizacion)
|
|
408
|
+
|
|
409
|
+
datos_normalizados = GR.normalizar_varios(entradas, @max_valores, normalizacion)
|
|
410
|
+
|
|
411
|
+
etiquetas = datos_hash.map { |item| [item[clave_etiqueta].to_f] }
|
|
412
|
+
|
|
413
|
+
@max_valores_salida = GR.calcular_maximos(etiquetas, normalizacion)
|
|
414
|
+
etiquetas_no = GR.normalizar_varios(etiquetas, @max_valores_salida, normalizacion)
|
|
415
|
+
|
|
416
|
+
estructuras.each do |estructura|
|
|
417
|
+
@red.agregar_subred([claves_entrada.size, *estructura])
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
datos_para_subredes = estructuras.map do |_|
|
|
421
|
+
{ entrada: datos_normalizados, salida: etiquetas_no }
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
@red.entrenar_subredes(datos_para_subredes, tasa, epocas)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def predecir_hashes(nuevos_hashes, claves_entrada, normalizacion=:max)
|
|
428
|
+
entradas = nuevos_hashes.map do |item|
|
|
429
|
+
claves_entrada.map do |clave|
|
|
430
|
+
valor = item[clave]
|
|
431
|
+
valor == true ? 1.0 : valor == false ? 0.0 : valor.to_f
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
datos_normalizados = GR.normalizar_varios(entradas, @max_valores, normalizacion)
|
|
436
|
+
|
|
437
|
+
datos_normalizados.map do |entrada|
|
|
438
|
+
pred_norm = @red.combinar_resultados(entrada)
|
|
439
|
+
if normalizacion == :zscore && @max_valores_salida.is_a?(Hash)
|
|
440
|
+
pred_norm.map.with_index do |val, idx|
|
|
441
|
+
val * @max_valores_salida[:desviaciones][idx] + @max_valores_salida[:medias][idx]
|
|
442
|
+
end
|
|
443
|
+
else
|
|
444
|
+
pred_norm.map.with_index { |val, idx| val * @max_valores_salida[idx] }
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# --------- Para datos numéricos ---------
|
|
451
|
+
def entrenar_numericos(datos_entrada, datos_salida, estructuras, tasa, epocas, normalizacion=:max)
|
|
452
|
+
@red.subredes.clear # limpiar subredes previas
|
|
453
|
+
|
|
454
|
+
@max_valores = GR.calcular_maximos(datos_entrada, normalizacion)
|
|
455
|
+
@max_valores_salida = GR.calcular_maximos(datos_salida, normalizacion)
|
|
456
|
+
|
|
457
|
+
datos_entrada_no = GR.normalizar_varios(datos_entrada, @max_valores, normalizacion)
|
|
458
|
+
datos_salida_no = GR.normalizar_varios(datos_salida, @max_valores_salida, normalizacion)
|
|
459
|
+
|
|
460
|
+
estructuras.each do |estructura|
|
|
461
|
+
@red.agregar_subred([datos_entrada.first.size, *estructura])
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
datos_para_subredes = estructuras.map do |_|
|
|
465
|
+
{ entrada: datos_entrada_no, salida: datos_salida_no }
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
@red.entrenar_subredes(datos_para_subredes, tasa, epocas)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def predecir_numericos(nuevos_datos, normalizacion=:max)
|
|
472
|
+
datos_normalizados = GR.normalizar_varios(nuevos_datos, @max_valores, normalizacion)
|
|
473
|
+
datos_normalizados.map do |entrada|
|
|
474
|
+
pred_norm = @red.combinar_resultados(entrada)
|
|
475
|
+
if normalizacion == :zscore && @max_valores_salida.is_a?(Hash)
|
|
476
|
+
pred_norm.map.with_index do |val, idx|
|
|
477
|
+
val * @max_valores_salida[:desviaciones][idx] + @max_valores_salida[:medias][idx]
|
|
478
|
+
end
|
|
479
|
+
else
|
|
480
|
+
pred_norm.map.with_index { |val, idx| val * @max_valores_salida[idx] }
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# --------- Para texto ---------
|
|
486
|
+
def entrenar_texto(textos, etiquetas, estructuras, tasa, epocas, normalizacion=:max)
|
|
487
|
+
@red.subredes.clear # limpiar subredes previas
|
|
488
|
+
|
|
489
|
+
@vocabulario = GR.crear_vocabulario(textos)
|
|
490
|
+
entradas = textos.map { |texto| GR.vectorizar_texto(texto, @vocabulario) }
|
|
491
|
+
@max_valores = { 0 => @vocabulario.size } # Solo tamaño vocabulario para texto
|
|
492
|
+
|
|
493
|
+
datos_normalizados = GR.normalizar_varios(entradas, @max_valores, normalizacion)
|
|
494
|
+
|
|
495
|
+
@max_valores_salida = GR.calcular_maximos(etiquetas, normalizacion)
|
|
496
|
+
etiquetas_no = GR.normalizar_varios(etiquetas, @max_valores_salida, normalizacion)
|
|
497
|
+
|
|
498
|
+
estructuras.each do |estructura|
|
|
499
|
+
@red.agregar_subred([@vocabulario.size, *estructura])
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
datos_para_subredes = estructuras.map do |_|
|
|
503
|
+
{ entrada: datos_normalizados, salida: etiquetas_no }
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
@red.entrenar_subredes(datos_para_subredes, tasa, epocas)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def predecir_texto(nuevos_textos, normalizacion=:max)
|
|
510
|
+
entradas = nuevos_textos.map { |texto| GR.vectorizar_texto(texto, @vocabulario) }
|
|
511
|
+
datos_normalizados = GR.normalizar_varios(entradas, @max_valores, normalizacion)
|
|
512
|
+
|
|
513
|
+
datos_normalizados.map do |entrada|
|
|
514
|
+
pred_norm = @red.combinar_resultados(entrada)
|
|
515
|
+
if normalizacion == :zscore && @max_valores_salida.is_a?(Hash)
|
|
516
|
+
pred_norm.map.with_index do |val, idx|
|
|
517
|
+
val * @max_valores_salida[:desviaciones][idx] + @max_valores_salida[:medias][idx]
|
|
518
|
+
end
|
|
519
|
+
else
|
|
520
|
+
pred_norm.map.with_index { |val, idx| val * @max_valores_salida[idx] }
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
DESCRIPCIONES_METODOS = {
|
|
526
|
+
# RedPrincipal
|
|
527
|
+
'RedPrincipal.agregar_subred' => {
|
|
528
|
+
descripcion: "Agrega una subred a la red principal con la estructura dada. La estructura define el número de neuronas por capa (incluyendo entradas).",
|
|
529
|
+
ejemplo: <<~EX
|
|
530
|
+
red = GR::RedPrincipal.new
|
|
531
|
+
red.agregar_subred([2, 4, 1]) # 2 entradas, 4 neuronas capa oculta, 1 salida
|
|
532
|
+
EX
|
|
533
|
+
}, #34,36
|
|
534
|
+
'RedPrincipal.entrenar_subredes' => {
|
|
535
|
+
descripcion: "Entrena todas las subredes usando los datos de entrada y salida, con tasa de aprendizaje, épocas y opciones como paciencia para early stopping.",
|
|
536
|
+
ejemplo: <<~EX
|
|
537
|
+
datos = [
|
|
538
|
+
{entrada: [[0.1, 0.2]], salida: [[0.3]]},
|
|
539
|
+
{entrada: [[0.5, 0.6]], salida: [[0.7]]}
|
|
540
|
+
]
|
|
541
|
+
red = GR::RedPrincipal.new(true) <-- (true) es para que impriam el umbral de error
|
|
542
|
+
red.agregar_subred([2, 3, 1])
|
|
543
|
+
red.agregar_subred([2, 2, 1])
|
|
544
|
+
red.entrenar_subredes(datos, 0.01, 1000, paciencia: 5)
|
|
545
|
+
EX
|
|
546
|
+
},
|
|
547
|
+
'RedPrincipal.combinar_resultados' => {
|
|
548
|
+
descripcion: 'Promedia las salidas de todas las subredes para una entrada dada, generando la predicción final.',
|
|
549
|
+
ejemplo: <<~EX
|
|
550
|
+
resultado = red.combinar_resultados([0.2, 0.8])
|
|
551
|
+
EX
|
|
552
|
+
},
|
|
553
|
+
|
|
554
|
+
# FacilRed (interfaz más sencilla)
|
|
555
|
+
'FacilRed.entrenar_numericos' => {
|
|
556
|
+
descripcion: 'Entrena la red con datos numéricos (arrays de números) para entrada y salida. Normaliza, crea subredes y entrena.',
|
|
557
|
+
ejemplo: <<~EX
|
|
558
|
+
datos_entrada = [[170, 25], [160, 30], [180, 22]]
|
|
559
|
+
datos_salida = [[65], [60], [75]]
|
|
560
|
+
estructuras = [[4, 1], [3, 1]]
|
|
561
|
+
red = GR::FacilRed.new(true)
|
|
562
|
+
red.entrenar_numericos(datos_entrada, datos_salida, estructuras, 0.05, 15000, :max)
|
|
563
|
+
EX
|
|
564
|
+
},
|
|
565
|
+
'FacilRed.predecir_numericos' => {
|
|
566
|
+
descripcion: 'Predice valores con nuevos datos numéricos normalizados igual que el entrenamiento.',
|
|
567
|
+
ejemplo: <<~EX
|
|
568
|
+
nuevos_datos = [[172, 26]]
|
|
569
|
+
predicciones = red.predecir_numericos(nuevos_datos, :max)
|
|
570
|
+
EX
|
|
571
|
+
},
|
|
572
|
+
'FacilRed.entrenar_hashes' => {
|
|
573
|
+
descripcion: 'Entrena la red con datos de entrada en formato hash, especificando las claves a usar y la clave para la etiqueta.',
|
|
574
|
+
ejemplo: <<~EX
|
|
575
|
+
datos_hash = [
|
|
576
|
+
{altura: 170, edad: 25, peso: 65},
|
|
577
|
+
{altura: 160, edad: 30, peso: 60}
|
|
578
|
+
]
|
|
579
|
+
red = GR::FacilRed.new(true) <-- (true) es para que impriam el umbral de error
|
|
580
|
+
red.entrenar_hashes(datos_hash, [:altura, :edad], :peso, [[4, 1]], 0.05, 15000, :max)
|
|
581
|
+
EX
|
|
582
|
+
},
|
|
583
|
+
'FacilRed.predecir_hashes' => {
|
|
584
|
+
descripcion: 'Predice usando datos hash con las claves especificadas para entrada.',
|
|
585
|
+
ejemplo: <<~EX
|
|
586
|
+
nuevos_hashes = [{altura: 172, edad: 26}]
|
|
587
|
+
predicciones = red.predecir_hashes(nuevos_hashes, [:altura, :edad], :max)
|
|
588
|
+
EX
|
|
589
|
+
},
|
|
590
|
+
'FacilRed.entrenar_texto' => {
|
|
591
|
+
descripcion: 'Entrena la red con textos y etiquetas numéricas, creando un vocabulario para vectorizar textos.',
|
|
592
|
+
ejemplo: <<~EX
|
|
593
|
+
textos = ["hola mundo", "buen día"]
|
|
594
|
+
etiquetas = [[1], [0]]
|
|
595
|
+
estructuras = [[5, 1]]
|
|
596
|
+
red = GR::FacilRed.new(true)
|
|
597
|
+
red.entrenar_texto(textos, etiquetas, estructuras, 0.01, 5000)
|
|
598
|
+
EX
|
|
599
|
+
},
|
|
600
|
+
'FacilRed.predecir_texto' => {
|
|
601
|
+
descripcion: 'Predice con nuevos textos, vectorizando según el vocabulario aprendido.',
|
|
602
|
+
ejemplo: <<~EX
|
|
603
|
+
nuevos_textos = ["hola"]
|
|
604
|
+
predicciones = red.predecir_texto(nuevos_textos)
|
|
605
|
+
EX
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
# Guardar y cargar modelos y vocabularios
|
|
609
|
+
'GR.guardar_modelo' => {
|
|
610
|
+
descripcion: 'Guarda el modelo entrenado en un archivo binario para poder cargarlo después. Opcionalmente guarda también el vocabulario.',
|
|
611
|
+
ejemplo: <<~EX
|
|
612
|
+
GR.guardar_modelo(modelo, "mi_modelo", "./modelos", vocabulario) El path por defecto es la carpeta donde se este ejecunatdo
|
|
613
|
+
EX
|
|
614
|
+
},
|
|
615
|
+
'GR.cargar_modelo' => {
|
|
616
|
+
descripcion: 'Carga un modelo guardado desde un archivo binario para usarlo sin entrenar de nuevo.',
|
|
617
|
+
ejemplo: <<~EX
|
|
618
|
+
modelo = GR.cargar_modelo("mi_modelo", "./modelos") El path por defecto es la carpeta donde se este ejecunatdo
|
|
619
|
+
EX
|
|
620
|
+
},
|
|
621
|
+
'GR.guardar_vocabulario' => {
|
|
622
|
+
descripcion: 'Guarda el vocabulario en un archivo binario para su posterior carga.',
|
|
623
|
+
ejemplo: <<~EX
|
|
624
|
+
GR.guardar_vocabulario(vocabulario, "mi_modelo", "./modelos") El path por defecto es la carpeta donde se este ejecunatdo
|
|
625
|
+
EX
|
|
626
|
+
},
|
|
627
|
+
'GR.cargar_vocabulario' => {
|
|
628
|
+
descripcion: 'Carga el vocabulario desde un archivo binario guardado.',
|
|
629
|
+
ejemplo: <<~EX
|
|
630
|
+
vocabulario = GR.cargar_vocabulario("mi_modelo", "./modelos") El path por defecto es la carpeta donde se este ejecunatdo
|
|
631
|
+
EX
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
# Normalización y preprocesamiento
|
|
635
|
+
'GR.normalizar_varios' => {
|
|
636
|
+
descripcion: 'Normaliza un conjunto de datos según el método especificado (:max o :zscore).',
|
|
637
|
+
ejemplo: <<~EX
|
|
638
|
+
maximos = GR.calcular_maximos(datos, :max)
|
|
639
|
+
datos_norm = GR.normalizar_varios(datos, maximos, :max)
|
|
640
|
+
EX
|
|
641
|
+
},
|
|
642
|
+
'GR.calcular_maximos' => {
|
|
643
|
+
descripcion: 'Calcula valores máximos o medias y desviaciones según el método para normalizar datos.',
|
|
644
|
+
ejemplo: <<~EX
|
|
645
|
+
maximos = GR.calcular_maximos(datos, :max)
|
|
646
|
+
estadisticas = GR.calcular_maximos(datos, :zscore)
|
|
647
|
+
EX
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
# Funciones de texto
|
|
651
|
+
'GR.crear_vocabulario' => {
|
|
652
|
+
descripcion: 'Crea un vocabulario único a partir de una lista de textos, separando palabras.',
|
|
653
|
+
ejemplo: <<~EX
|
|
654
|
+
textos = ["hola mundo", "buen día"]
|
|
655
|
+
vocabulario = GR.crear_vocabulario(textos)
|
|
656
|
+
EX
|
|
657
|
+
},
|
|
658
|
+
'GR.vectorizar_texto' => {
|
|
659
|
+
descripcion: 'Convierte un texto en un vector binario basado en la presencia de palabras en el vocabulario.',
|
|
660
|
+
ejemplo: <<~EX
|
|
661
|
+
vector = GR.vectorizar_texto("hola mundo", vocabulario)
|
|
662
|
+
EX
|
|
663
|
+
},
|
|
664
|
+
'GR.normalizar_con_vocabulario' => {
|
|
665
|
+
descripcion: 'Normaliza vectores generados con el vocabulario dividiendo entre el tamaño del vocabulario.',
|
|
666
|
+
ejemplo: <<~EX
|
|
667
|
+
vectores_norm = GR.normalizar_con_vocabulario(vectores, vocabulario)
|
|
668
|
+
EX
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
# Función para mostrar descripción y ejemplo de método dado clase y método (strings)
|
|
673
|
+
def self.describe_metodo(clase, metodo)
|
|
674
|
+
clave = "#{clase}.#{metodo}"
|
|
675
|
+
info = DESCRIPCIONES_METODOS[clave]
|
|
676
|
+
if info
|
|
677
|
+
puts "\e[1;3;5;41;37mDescripción de #{clave}:"
|
|
678
|
+
puts info[:descripcion]
|
|
679
|
+
puts "\nEjemplo de uso:"
|
|
680
|
+
puts "#{info[:ejemplo]}\e[0m"
|
|
681
|
+
else
|
|
682
|
+
puts "\e[31;1mNo se encontró descripción para el método '#{clave}'"
|
|
683
|
+
puts "\e[31mAsegúrate de usar el nombre exacto de clase y método (como strings)"
|
|
684
|
+
puts "\e[36mPuedes llamar el metodo para verificar: listar_metodos_disponibles\e[0m"
|
|
685
|
+
end
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Función que lista todos los métodos públicos documentados en DESCRIPCIONES_METODOS
|
|
689
|
+
def self.listar_metodos_disponibles
|
|
690
|
+
puts "\e[1;3;5;41;37mMétodos públicos documentados:"
|
|
691
|
+
grouped = DESCRIPCIONES_METODOS.keys.group_by { |k| k.split('.').first }
|
|
692
|
+
grouped.each do |clase, metodos|
|
|
693
|
+
puts " #{clase}:"
|
|
694
|
+
metodos.each { |m| puts " - #{m.split('.').last}" }
|
|
695
|
+
end
|
|
696
|
+
print "\e[0m"
|
|
697
|
+
end
|
|
698
|
+
end
|
data/lib/gr/version.rb
ADDED
data/lib/grydra.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: grydra
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Razo
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Implementación de redes neuronales con clases para neuronas y poder crear
|
|
13
|
+
redes neuronales, con diferentes metodos de activacion como Tanh, Relu y Sigmoid
|
|
14
|
+
email:
|
|
15
|
+
- garabatoangelopolis@email.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/gr/core.rb
|
|
21
|
+
- lib/gr/version.rb
|
|
22
|
+
- lib/grydra.rb
|
|
23
|
+
homepage: https://rubygems.org/gems/gr
|
|
24
|
+
licenses:
|
|
25
|
+
- MIT
|
|
26
|
+
metadata: {}
|
|
27
|
+
rdoc_options: []
|
|
28
|
+
require_paths:
|
|
29
|
+
- lib
|
|
30
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - ">="
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: 2.7.0
|
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
requirements: []
|
|
41
|
+
rubygems_version: 3.6.9
|
|
42
|
+
specification_version: 4
|
|
43
|
+
summary: Librería para redes neuronales en Ruby
|
|
44
|
+
test_files: []
|