Skip to content

Commit 3a7bdf0

Browse files
committed
- Se elimino implementación obsoleta de Neuron (preparando vectorización)
- Se Implemento vectorización en capas y sistema de optimizadores con Momentum. - Se actualizo tests unitarios y script de ejemplo para nueva arquitectura.
1 parent 602cfc2 commit 3a7bdf0

11 files changed

Lines changed: 256 additions & 205 deletions

main.py

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,24 @@
1+
# Nuevo main.py de ejemplo
12
import numpy as np
23
from src.neural_network import NeuralNetwork
3-
from src.activations import Sigmoid, LeakyReLU
4+
from src.activations import LeakyReLU, Sigmoid
45
from src.losses import BinaryCrossEntropy
6+
from src.optimizers import SGD
57

68
if __name__ == "__main__":
7-
# --- Datos de entrenamiento (Compuerta XOR) ---
8-
# Entradas: [0,0], [0,1], [1,0], [1,1]
9-
X = np.array([
10-
[0, 0],
11-
[0, 1],
12-
[1, 0],
13-
[1, 1]
14-
])
15-
16-
# Salidas esperadas: 0, 1, 1, 0
17-
y = np.array([
18-
[0],
19-
[1],
20-
[1],
21-
[0]
22-
])
23-
24-
print("--- Inicializando Red Neuronal ---")
25-
nn = NeuralNetwork(loss_function=BinaryCrossEntropy())
9+
X = np.array([[0,0], [0,1], [1,0], [1,1]])
10+
y = np.array([[0], [1], [1], [0]])
2611

27-
# Estructura: 2 entradas -> Capa Oculta (4 neuronas) -> Salida (1 neurona)
12+
# Definimos optimizador con Momentum (Acelera el entrenamiento)
13+
optimizer = SGD(learning_rate=0.1, momentum=0.9)
2814

29-
# CAMBIO: Usamos LeakyReLU en la oculta para mayor seguridad
30-
nn.add_layer(num_neurons=4, input_size=2, activation=LeakyReLU())
15+
nn = NeuralNetwork(loss_function=BinaryCrossEntropy(), optimizer=optimizer)
16+
17+
# Arquitectura
18+
nn.add_layer(num_neurons=4, input_size=2, activation=LeakyReLU())
3119
nn.add_layer(num_neurons=1, activation=Sigmoid())
3220

33-
print("--- Iniciando Entrenamiento ---")
34-
# Entrenamos con una tasa de aprendizaje baja para ver la convergencia
35-
nn.train(X, y, epochs=10000, learning_rate=0.1)
36-
37-
print("\n--- Resultados Finales ---")
38-
predictions = nn.predict(X)
21+
# Entrenar (Batch size 4 es todo el dataset en este caso pequeño)
22+
nn.train(X, y, epochs=100000, batch_size=4)
3923

40-
for i in range(len(X)):
41-
print(f"Entrada: {X[i]} | Predicción: {predictions[i][0]:.4f} | Esperado: {y[i][0]}")
24+
print(nn.predict(X))

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
numpy
2-
python-dotenv
3-
setuptools
1+
numpy>=2.4.0
2+
python-dotenv>=1.2.1
3+
matplotlib>=3.10.8

src/activations.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,25 @@ def derivative(self, x):
2020
class LeakyReLU(Activation):
2121
def __init__(self, alpha=0.01):
2222
self.alpha = alpha
23-
2423
def forward(self, x):
25-
# Si x > 0 devuelve x, si no, devuelve x * 0.01
2624
return np.where(x > 0, x, x * self.alpha)
25+
def derivative(self, x):
26+
return np.where(x > 0, 1, self.alpha)
27+
28+
class Softmax(Activation):
29+
def forward(self, x):
30+
# Estabilidad numérica: restar el max
31+
exp_vals = np.exp(x - np.max(x, axis=1, keepdims=True))
32+
return exp_vals / np.sum(exp_vals, axis=1, keepdims=True)
2733

2834
def derivative(self, x):
29-
# Si x > 0 la derivada es 1, si no, es 0.01
30-
return np.where(x > 0, 1, self.alpha)
35+
# Truco: Usualmente Softmax se usa con CategoricalCrossEntropy
36+
# y la derivada simplificada de ambas juntas es (pred - y).
37+
# Por lo tanto, aquí retornamos 1 para que la Loss maneje el gradiente.
38+
return 1
39+
40+
class Linear(Activation):
41+
def forward(self, x):
42+
return x
43+
def derivative(self, x):
44+
return np.ones_like(x)

src/layer.py

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,60 @@
11
import numpy as np
2-
from .neuron import Neuron
3-
from .activations import Sigmoid
42

53
class Layer:
6-
def __init__(self, num_neurons, inputs_size, activation=Sigmoid()):
4+
def __init__(self, n_neurons, input_size, activation):
5+
self.n_neurons = n_neurons
6+
self.input_size = input_size
77
self.activation = activation
8-
# Pasamos 'activation' a la Neurona
9-
self.neurons = [Neuron(inputs_size, activation) for _ in range(num_neurons)]
8+
9+
# Inicialización de He (Estándar profesional)
10+
# Weights: matriz (input_size x n_neurons)
11+
self.weights = np.random.randn(input_size, n_neurons) * np.sqrt(2 / input_size)
12+
self.biases = np.zeros((1, n_neurons))
13+
14+
# Cache para backward pass
15+
self.inputs = None
16+
self.z = None
17+
self.dweights = None
18+
self.dbiases = None
1019

11-
def forward(self, inputs):
12-
return np.array([neuron.forward(inputs) for neuron in self.neurons])
13-
14-
def backward(self, d_outputs, learning_rate):
15-
d_inputs = np.zeros_like(self.neurons[0].inputs, dtype=float)
16-
for i, neuron in enumerate(self.neurons):
17-
d_inputs += neuron.backward(d_outputs[i], learning_rate)
18-
return d_inputs
20+
def forward(self, inputs, training=True):
21+
self.inputs = inputs
22+
# Operación matricial: (Batch x Input) dot (Input x Neurons) = (Batch x Neurons)
23+
self.z = np.dot(inputs, self.weights) + self.biases
24+
return self.activation.forward(self.z)
25+
26+
def backward(self, d_output):
27+
# d_output viene de la capa siguiente: dL/dA
28+
# Multiplicamos por la derivada de la activación: dA/dZ
29+
d_activation = d_output * self.activation.derivative(self.z)
30+
31+
# Calculamos gradientes
32+
m = self.inputs.shape[0] # Tamaño del batch
33+
34+
# dW = X.T dot dZ
35+
self.dweights = np.dot(self.inputs.T, d_activation)
36+
37+
# db = sum(dZ)
38+
self.dbiases = np.sum(d_activation, axis=0, keepdims=True)
39+
40+
# dX (Error para la capa anterior) = dZ dot W.T
41+
d_input = np.dot(d_activation, self.weights.T)
42+
43+
return d_input
44+
45+
class Dropout(Layer):
46+
def __init__(self, rate):
47+
self.rate = rate
48+
self.mask = None
49+
self.n_neurons = 0 # No tiene neuronas propias
50+
51+
def forward(self, inputs, training=True):
52+
if training:
53+
# Máscara binomial (escala por 1/(1-rate) para mantener magnitud)
54+
self.mask = np.random.binomial(1, 1 - self.rate, size=inputs.shape) / (1 - self.rate)
55+
return inputs * self.mask
56+
return inputs
57+
58+
def backward(self, d_output):
59+
# El gradiente solo pasa por donde la máscara era 1
60+
return d_output * self.mask

src/losses.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@ def derivative(self, output, y): pass
77
class MSE(Loss):
88
def calculate(self, output, y):
99
return np.mean((y - output) ** 2)
10-
1110
def derivative(self, output, y):
12-
return 2 * (output - y) / y.size # Normalizado por tamaño
11+
return 2 * (output - y) / y.shape[0]
1312

1413
class BinaryCrossEntropy(Loss):
1514
def calculate(self, output, y):
16-
# Clip para evitar log(0)
1715
output = np.clip(output, 1e-15, 1 - 1e-15)
1816
return -np.mean(y * np.log(output) + (1 - y) * np.log(1 - output))
19-
2017
def derivative(self, output, y):
2118
output = np.clip(output, 1e-15, 1 - 1e-15)
22-
return (output - y) / (output * (1 - output)) / y.size
19+
return (output - y) / (output * (1 - output)) / y.shape[0]
20+
21+
class CategoricalCrossEntropy(Loss):
22+
def calculate(self, output, y):
23+
# y debe ser one-hot encoded
24+
output = np.clip(output, 1e-15, 1 - 1e-15)
25+
return -np.sum(y * np.log(output)) / y.shape[0]
26+
27+
def derivative(self, output, y):
28+
# Asumiendo combinación con Softmax
29+
return (output - y) / y.shape[0]

src/neural_network.py

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import numpy as np
22
import pickle
3-
from .layer import Layer
4-
from .activations import Sigmoid
3+
from .layer import Layer, Dropout
4+
from .activations import Sigmoid
55
from .losses import MSE
6+
from .optimizers import SGD
67

78
class NeuralNetwork:
8-
def __init__(self, loss_function=MSE()):
9+
def __init__(self, loss_function=MSE(), optimizer=None):
910
self.layers = []
10-
self.loss_list = []
1111
self.loss_function = loss_function
12+
# Por defecto usamos SGD si no se pasa uno
13+
self.optimizer = optimizer if optimizer else SGD(learning_rate=0.01)
1214

1315
def add_layer(self, num_neurons, input_size=None, activation=None):
1416
if activation is None:
@@ -19,59 +21,65 @@ def add_layer(self, num_neurons, input_size=None, activation=None):
1921
raise ValueError("Debes definir input_size para la primera capa.")
2022
self.layers.append(Layer(num_neurons, input_size, activation))
2123
else:
22-
previous_output_size = len(self.layers[-1].neurons)
23-
self.layers.append(Layer(num_neurons, previous_output_size, activation))
24-
25-
def forward(self, inputs):
24+
# Busca la última capa que tenga neuronas (ignora Dropout para contar outputs)
25+
last_layer_size = 0
26+
for layer in reversed(self.layers):
27+
if hasattr(layer, 'n_neurons') and layer.n_neurons > 0:
28+
last_layer_size = layer.n_neurons
29+
break
30+
self.layers.append(Layer(num_neurons, last_layer_size, activation))
31+
32+
def add_dropout(self, rate):
33+
self.layers.append(Dropout(rate))
34+
35+
def forward(self, inputs, training=True):
2636
for layer in self.layers:
27-
inputs = layer.forward(inputs)
37+
inputs = layer.forward(inputs, training=training)
2838
return inputs
2939

30-
def backward(self, loss_gradient, learning_rate):
40+
def backward(self, loss_gradient):
41+
# Propagar el error hacia atrás
3142
for layer in reversed(self.layers):
32-
loss_gradient = layer.backward(loss_gradient, learning_rate)
43+
loss_gradient = layer.backward(loss_gradient)
3344

34-
def train(self, x, y, epochs=1000, learning_rate=0.1):
35-
indices = np.arange(len(x))
36-
45+
def train(self, x, y, epochs=1000, batch_size=32):
3746
for epoch in range(epochs):
38-
epoch_loss = 0
47+
# 1. Shuffle (Barajado)
48+
permutation = np.random.permutation(x.shape[0])
49+
x_shuffled = x[permutation]
50+
y_shuffled = y[permutation]
3951

40-
# Barajado de datos (Shuffle)
41-
np.random.shuffle(indices)
52+
epoch_loss = 0
4253

43-
for i in indices:
44-
# 1. Forward
45-
output = self.forward(x[i])
54+
# 2. Mini-Batch Gradient Descent
55+
for i in range(0, len(x), batch_size):
56+
# Crear batch
57+
x_batch = x_shuffled[i:i+batch_size]
58+
y_batch = y_shuffled[i:i+batch_size]
4659

47-
# 2. Calcular Loss
48-
epoch_loss += self.loss_function.calculate(output, y[i])
60+
# Forward
61+
output = self.forward(x_batch, training=True)
4962

50-
# 3. Calcular gradiente
51-
loss_gradient = self.loss_function.derivative(output, y[i])
63+
# Calcular Loss y Gradiente de Loss
64+
loss = self.loss_function.calculate(output, y_batch)
65+
epoch_loss += loss
66+
grad = self.loss_function.derivative(output, y_batch)
5267

53-
# 4. Backward
54-
self.backward(loss_gradient, learning_rate)
55-
56-
# --- ESTAS LÍNEAS FALTABAN ---
57-
# Calcular promedio del error en este epoch
58-
epoch_loss /= len(x)
59-
self.loss_list.append(epoch_loss)
68+
# Backward (Calcula gradientes dW, db)
69+
self.backward(grad)
70+
71+
# Optimizer Step (Actualiza pesos)
72+
self.optimizer.update(self.layers)
6073

61-
# Imprimir progreso cada 100 épocas
6274
if epoch % 100 == 0:
63-
print(f"Epoch: {epoch}, Loss: {epoch_loss:.6f}")
75+
print(f"Epoch: {epoch}, Loss promedio: {epoch_loss / (len(x)/batch_size):.6f}")
6476

6577
def predict(self, x):
66-
predictions = []
67-
for i in range(len(x)):
68-
predictions.append(self.forward(x[i]))
69-
return np.array(predictions)
78+
return self.forward(x, training=False)
7079

7180
def save_model(self, filename):
7281
with open(filename, 'wb') as file:
7382
pickle.dump(self, file)
74-
print(f"Modelo guardado en {filename}")
7583

7684
@staticmethod
7785
def load_model(filename):

src/neuron.py

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)