Skip to content

Commit 33f8d10

Browse files
committed
refactor(v0.4.0): caches externos, from_logits, JSON+NPZ, shape fail-fast
BREAKING CHANGE: refactor arquitectónico que resuelve los 5 riesgos estructurales de v0.3 identificados en el code review. Arreglos: - [#1] Acoplamiento matemático oculto CCE↔Softmax: * Softmax.backward ahora implementa el Jacobiano-vector completo (s * (d_output - sum(d_output * s))). Matemáticamente correcto con cualquier función de pérdida. * BinaryCrossEntropy y CategoricalCrossEntropy aceptan from_logits. Con from_logits=True: capa final Linear, loss aplica sigmoid/ softmax internamente con el atajo estable (pred - y) / N. * Eliminada la suposición silenciosa 'la capa previa es Softmax'. - [#2] Estado mutable en capas: * forward(x, training) -> (output, cache). Las capas ya NO guardan self.inputs / self.z. El cache viaja explícitamente. * backward(d_output, cache) -> (d_input, grads_dict). * Activaciones también stateless: forward -> (out, cache). * Habilita arquitecturas multi-entrada (siamesas, triplet loss) con pesos compartidos sin corromperse. - [#3] Serialización pickle-frágil: * save(directory) produce topology.json + weights.npz. * JSON es legible, no ejecutable, sobrevive a refactors. * NPZ es formato NumPy estándar, incluye estado no entrenable (running_mean/running_var de BatchNorm). * get_config / from_config en TODOS los componentes (layers, optimizers, losses, activations, initializers, regularizers, metrics). * save_model/load_model (pickle) mantenidos con warning. - [#4] Inferencia de shapes tardía: * NeuralNetwork.build(input_shape) propaga shapes a lo largo de toda la red y valida compatibilidad ANTES de entrenar. * Se invoca automáticamente en compile() si la primera capa tiene input_size definido, o en fit() como fallback. * Cada capa tiene input_shape / output_shape accesibles. * Mismatches dimensionales fallan fail-fast con mensaje claro. - [#5] Fugas de abstracción en optimizadores: * Capas: parameters() -> Dict[str, ndarray] con nombres arbitrarios. * Optimizer.apply_gradients(list_of_tuples) donde cada tupla es (layer_id, param_name, param_ref, grad). No conoce 'weights' ni 'biases' hardcodeados. * BatchNormalization declara {'gamma', 'beta'} como parámetros entrenables y {'running_mean', 'running_var'} como estado no entrenable. Se eliminan los @Property falsos que 'engañaban' al optimizer en v0.3. * Capas futuras pueden tener N parámetros con cualquier nombre sin tocar el código de los optimizers. Nuevo: - examples/siamese_network.py: encoder con pesos compartidos que demuestra state isolation entre forwards intercalados. - tests/test_activations.py: valida stateless y Jacobiano de Softmax contra gradiente numérico. - tests/test_gradient_check.py: ahora cubre Softmax+CCE con Jacobiano real y el camino Linear+CCE(from_logits=True). - 68 tests (antes 51). Migración desde v0.3: # Persistencia - model.save_model('file.pkl') -> model.save('dir/') - NeuralNetwork.load_model('file.pkl') -> NeuralNetwork.load('dir/') # Clasificación (path recomendado, numéricamente estable) - Dense(n_classes, activation='softmax') -> Dense(n_classes, activation='linear') - loss='cce' -> CategoricalCrossEntropy(from_logits=True) - probs = model.predict(X) -> logits = model.predict(X) probs = softmax(logits) # API de capas - layer.get_params() -> layer.parameters() (dict) - output = layer.forward(x) -> output, cache = layer.forward(x) - d_in = layer.backward(d_out) -> d_in, grads = layer.backward(d_out, cache) # API de optimizadores - optimizer.update(layers) -> optimizer.apply_gradients(grad_tuples) (el modelo lo maneja internamente; usuarios de fit() no se ven afectados) Los archivos .pkl de v0.3 no son compatibles. Re-entrenar y guardar con model.save('dir/') para persistencia portable futura. Resultados: - XOR: 100% accuracy (path from_logits estable) - Multiclass blobs: 100% test accuracy con BatchNorm + Dropout + callbacks - Siamese encoder: converge con pesos compartidos (loss 0.34 -> 0.005) - Gradient check: todos los caminos validados numéricamente
1 parent 2d7eb83 commit 33f8d10

20 files changed

Lines changed: 1978 additions & 1228 deletions

README.md

Lines changed: 185 additions & 182 deletions
Large diffs are not rendered by default.

examples/multiclass_classification.py

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
11
"""
2-
Demo avanzado: clasificación multiclase con dataset sintético.
3-
4-
Muestra:
5-
- BatchNormalization y Dropout combinados.
6-
- Optimizer Adam con gradient clipping.
7-
- Regularización L2 sobre los pesos.
8-
- validation_split para monitorear sobreajuste.
9-
- EarlyStopping y ReduceLROnPlateau.
10-
- Predicción y evaluación final.
2+
Demo multiclase con BatchNorm, Dropout, callbacks y from_logits.
3+
4+
- Capa final Linear + CategoricalCrossEntropy(from_logits=True):
5+
camino numéricamente estable que evita el acoplamiento Softmax/Loss.
6+
- L2 regularización, gradient clipping, EarlyStopping, ReduceLROnPlateau.
7+
- save(dir) + load(dir) con topology JSON + pesos NPZ.
118
"""
129
import os
1310
import sys
11+
import tempfile
1412
import numpy as np
1513

1614
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
1715

1816
from src import (
1917
NeuralNetwork, Dense, Dropout, BatchNormalization,
20-
Adam, L2,
18+
Adam, L2, CategoricalCrossEntropy,
2119
EarlyStopping, ReduceLROnPlateau,
2220
train_test_split, to_categorical, standardize,
2321
)
2422

2523

26-
def make_blobs(n_per_class: int = 200, n_features: int = 4, n_classes: int = 3, seed: int = 0):
27-
"""Genera clusters gaussianos en distintos centros (dataset sintético)."""
24+
def make_blobs(n_per_class=300, n_features=8, n_classes=4, seed=0):
2825
rng = np.random.default_rng(seed)
2926
centers = rng.uniform(-5, 5, size=(n_classes, n_features))
3027
X, y = [], []
@@ -37,50 +34,55 @@ def make_blobs(n_per_class: int = 200, n_features: int = 4, n_classes: int = 3,
3734
def main():
3835
np.random.seed(0)
3936

40-
# Datos
4137
X, y = make_blobs(n_per_class=300, n_features=8, n_classes=4, seed=0)
4238
X = standardize(X)
4339
y_oh = to_categorical(y, num_classes=4)
40+
X_train, X_test, y_train, y_test = train_test_split(X, y_oh, test_size=0.2, random_state=0)
4441

45-
X_train, X_test, y_train, y_test = train_test_split(
46-
X, y_oh, test_size=0.2, random_state=0
47-
)
48-
49-
# Modelo profundo con regularización
42+
# Capa final 'linear' — la loss aplica softmax internamente
5043
model = NeuralNetwork()
5144
model.add(Dense(32, input_size=8, activation="relu", kernel_regularizer=L2(0.001)))
5245
model.add(BatchNormalization(32))
5346
model.add(Dropout(0.2))
5447
model.add(Dense(16, activation="relu", kernel_regularizer=L2(0.001)))
5548
model.add(Dropout(0.2))
56-
model.add(Dense(4, activation="softmax"))
49+
model.add(Dense(4, activation="linear")) # logits
5750

5851
model.compile(
5952
optimizer=Adam(learning_rate=0.01, clip_norm=1.0),
60-
loss="cce",
61-
metrics=["categorical_accuracy"],
53+
loss=CategoricalCrossEntropy(from_logits=True),
6254
)
6355
model.summary()
6456

65-
# Callbacks
6657
callbacks = [
6758
EarlyStopping(monitor="val_loss", patience=15, restore_best_weights=True),
6859
ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=5),
6960
]
7061

71-
# Entrenar
72-
history = model.fit(
62+
model.fit(
7363
X_train, y_train,
74-
epochs=100,
75-
batch_size=32,
64+
epochs=100, batch_size=32,
7665
validation_split=0.2,
7766
callbacks=callbacks,
7867
verbose=1,
7968
)
8069

81-
# Evaluar en test set
82-
print("\n--- Test set ---")
83-
model.evaluate(X_test, y_test)
70+
# Evaluación: necesitamos accuracy, así que aplicamos softmax manualmente
71+
logits_test = model.predict(X_test)
72+
probs = np.exp(logits_test - logits_test.max(axis=1, keepdims=True))
73+
probs = probs / probs.sum(axis=1, keepdims=True)
74+
acc = np.mean(np.argmax(probs, axis=1) == np.argmax(y_test, axis=1))
75+
print(f"\n--- Test accuracy: {acc:.4f}")
76+
77+
# Persistencia portable
78+
with tempfile.TemporaryDirectory() as tmp:
79+
path = os.path.join(tmp, "multiclass_model")
80+
model.save(path)
81+
print(f"Modelo guardado en {path}/")
82+
loaded = NeuralNetwork.load(path)
83+
loaded_logits = loaded.predict(X_test)
84+
assert np.allclose(logits_test, loaded_logits)
85+
print("save/load JSON+NPZ: OK")
8486

8587

8688
if __name__ == "__main__":

main.py

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,69 @@
1-
# Nuevo main.py de ejemplo
1+
"""
2+
Demo XOR — v0.4.
3+
4+
Muestra el camino recomendado de producción:
5+
- capa final 'linear' + BinaryCrossEntropy(from_logits=True).
6+
Es numéricamente estable y evita el acoplamiento Softmax/Loss.
7+
- build() propaga shapes antes de entrenar (fail-fast).
8+
- save() genera topology.json + weights.npz sin pickle.
9+
"""
10+
import os
11+
import tempfile
212
import numpy as np
3-
from src.neural_network import NeuralNetwork
4-
from src.activations import LeakyReLU, Sigmoid
5-
from src.losses import BinaryCrossEntropy
6-
from src.optimizers import SGD
13+
14+
from src import (
15+
NeuralNetwork, Dense,
16+
Adam, BinaryCrossEntropy,
17+
EarlyStopping,
18+
)
19+
20+
21+
def main():
22+
np.random.seed(42)
23+
24+
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
25+
y = np.array([[0], [1], [1], [0]], dtype=float)
26+
27+
model = NeuralNetwork()
28+
model.add(Dense(8, input_size=2, activation="relu"))
29+
model.add(Dense(1, activation="linear")) # logits
30+
31+
model.compile(
32+
optimizer=Adam(learning_rate=0.05),
33+
loss=BinaryCrossEntropy(from_logits=True),
34+
metrics=[],
35+
)
36+
37+
model.summary()
38+
39+
model.fit(
40+
X, y,
41+
epochs=500,
42+
batch_size=4,
43+
callbacks=[EarlyStopping(monitor="loss", patience=50)],
44+
verbose=0,
45+
)
46+
47+
# Inferencia: aplicar sigmoid al logit
48+
logits = model.predict(X)
49+
probs = 1.0 / (1.0 + np.exp(-logits))
50+
51+
print("\n--- Predicciones ---")
52+
for i in range(len(X)):
53+
print(
54+
f"Input: {X[i]}, Esperado: {int(y[i, 0])}, "
55+
f"Prob: {float(probs[i, 0]):.4f}, "
56+
f"Clase: {int(probs[i, 0] >= 0.5)}"
57+
)
58+
59+
# Persistencia portable (JSON + NPZ)
60+
with tempfile.TemporaryDirectory() as tmp:
61+
path = os.path.join(tmp, "xor_model")
62+
model.save(path)
63+
loaded = NeuralNetwork.load(path)
64+
assert np.allclose(model.predict(X), loaded.predict(X))
65+
print("\nsave/load JSON+NPZ: OK")
66+
767

868
if __name__ == "__main__":
9-
X = np.array([[0,0], [0,1], [1,0], [1,1]])
10-
y = np.array([[0], [1], [1], [0]])
11-
12-
# Definimos optimizador con Momentum (Acelera el entrenamiento)
13-
optimizer = SGD(learning_rate=0.1, momentum=0.9)
14-
15-
nn = NeuralNetwork(loss_function=BinaryCrossEntropy(), optimizer=optimizer)
16-
17-
# Arquitectura
18-
nn.add_layer(num_neurons=4, input_size=2, activation=LeakyReLU())
19-
nn.add_layer(num_neurons=1, activation=Sigmoid())
20-
21-
# Entrenar (Batch size 4 es todo el dataset en este caso pequeño)
22-
nn.train(X, y, epochs=100000, batch_size=4)
23-
24-
print(nn.predict(X))
69+
main()

setup.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,23 @@
88

99
setup(
1010
name="NeuralNetwork",
11-
version="0.3.0",
11+
version="0.4.0",
1212
packages=find_packages(),
13-
install_requires=[
14-
"numpy>=1.21.0",
15-
],
16-
extras_require={
17-
"dev": [
18-
"matplotlib>=3.5.0",
19-
"python-dotenv",
20-
],
21-
},
13+
install_requires=["numpy>=1.21.0"],
14+
extras_require={"dev": ["matplotlib>=3.5.0", "python-dotenv"]},
2215
author="elJulioDev",
23-
description="Librería de Deep Learning vectorizada con API estilo Keras",
16+
description="Deep Learning vectorizado con API Keras, caches externos y persistencia portable",
2417
long_description=long_description,
2518
long_description_content_type="text/markdown",
2619
python_requires=">=3.8",
2720
classifiers=[
2821
"Programming Language :: Python :: 3",
2922
"Programming Language :: Python :: 3.8",
30-
"Programming Language :: Python :: 3.9",
3123
"Programming Language :: Python :: 3.10",
32-
"Programming Language :: Python :: 3.11",
3324
"Programming Language :: Python :: 3.12",
3425
"License :: OSI Approved :: MIT License",
3526
"Operating System :: OS Independent",
3627
"Topic :: Scientific/Engineering :: Artificial Intelligence",
37-
"Topic :: Software Development :: Libraries :: Python Modules",
3828
"Intended Audience :: Developers",
3929
"Intended Audience :: Science/Research",
4030
],

src/__init__.py

Lines changed: 15 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,65 @@
1-
"""
2-
NeuralNetwork - librería de Deep Learning vectorizada.
3-
4-
Exports principales para uso ergonómico:
5-
from src import NeuralNetwork, Dense, Dropout, Adam, ...
6-
"""
1+
"""NeuralNetwork v0.4 — librería de Deep Learning vectorizada."""
72
from .neural_network import NeuralNetwork
83

94
from .layer import (
10-
BaseLayer,
11-
Layer,
12-
Dense,
13-
Dropout,
14-
BatchNormalization,
5+
BaseLayer, Layer, Dense, Dropout, BatchNormalization, layer_from_config,
156
)
167

178
from .activations import (
18-
Activation,
19-
Sigmoid,
20-
ReLU,
21-
LeakyReLU,
22-
ELU,
23-
Tanh,
24-
Softmax,
25-
Linear,
9+
Activation, Sigmoid, ReLU, LeakyReLU, ELU, Tanh, Softmax, Linear,
2610
get_activation,
2711
)
2812

2913
from .losses import (
30-
Loss,
31-
MSE,
32-
MAE,
33-
Huber,
34-
BinaryCrossEntropy,
35-
CategoricalCrossEntropy,
36-
SparseCategoricalCrossEntropy,
14+
Loss, MSE, MAE, Huber,
15+
BinaryCrossEntropy, CategoricalCrossEntropy, SparseCategoricalCrossEntropy,
3716
get_loss,
3817
)
3918

4019
from .optimizers import (
41-
Optimizer,
42-
SGD,
43-
AdaGrad,
44-
RMSprop,
45-
Adam,
46-
get_optimizer,
20+
Optimizer, SGD, AdaGrad, RMSprop, Adam, get_optimizer,
4721
)
4822

4923
from .initializers import (
50-
Initializer,
51-
HeNormal,
52-
XavierNormal,
53-
XavierUniform,
54-
Zeros,
55-
Ones,
24+
Initializer, HeNormal, XavierNormal, XavierUniform, Zeros, Ones,
5625
get_initializer,
5726
)
5827

5928
from .regularizers import (
60-
Regularizer,
61-
L1,
62-
L2,
63-
L1L2,
64-
get_regularizer,
29+
Regularizer, L1, L2, L1L2, get_regularizer,
6530
)
6631

6732
from .metrics import (
68-
Metric,
69-
BinaryAccuracy,
70-
CategoricalAccuracy,
71-
SparseCategoricalAccuracy,
72-
MeanAbsoluteError,
73-
RootMeanSquaredError,
74-
R2Score,
75-
get_metric,
33+
Metric, BinaryAccuracy, CategoricalAccuracy, SparseCategoricalAccuracy,
34+
MeanAbsoluteError, RootMeanSquaredError, R2Score, get_metric,
7635
)
7736

7837
from .callbacks import (
79-
Callback,
80-
History,
81-
EarlyStopping,
82-
ModelCheckpoint,
83-
ReduceLROnPlateau,
38+
Callback, History, EarlyStopping, ModelCheckpoint, ReduceLROnPlateau,
8439
)
8540

8641
from .utils import (
87-
train_test_split,
88-
to_categorical,
89-
normalize,
90-
standardize,
91-
shuffle_arrays,
92-
batch_iterator,
42+
train_test_split, to_categorical, normalize, standardize,
43+
shuffle_arrays, batch_iterator,
9344
)
9445

95-
__version__ = "0.3.0"
46+
__version__ = "0.4.0"
9647

9748
__all__ = [
9849
"NeuralNetwork",
99-
# Layers
100-
"BaseLayer", "Layer", "Dense", "Dropout", "BatchNormalization",
101-
# Activations
50+
"BaseLayer", "Layer", "Dense", "Dropout", "BatchNormalization", "layer_from_config",
10251
"Activation", "Sigmoid", "ReLU", "LeakyReLU", "ELU", "Tanh", "Softmax", "Linear",
10352
"get_activation",
104-
# Losses
10553
"Loss", "MSE", "MAE", "Huber",
10654
"BinaryCrossEntropy", "CategoricalCrossEntropy", "SparseCategoricalCrossEntropy",
10755
"get_loss",
108-
# Optimizers
10956
"Optimizer", "SGD", "AdaGrad", "RMSprop", "Adam", "get_optimizer",
110-
# Initializers
11157
"Initializer", "HeNormal", "XavierNormal", "XavierUniform", "Zeros", "Ones",
11258
"get_initializer",
113-
# Regularizers
11459
"Regularizer", "L1", "L2", "L1L2", "get_regularizer",
115-
# Metrics
11660
"Metric", "BinaryAccuracy", "CategoricalAccuracy", "SparseCategoricalAccuracy",
11761
"MeanAbsoluteError", "RootMeanSquaredError", "R2Score", "get_metric",
118-
# Callbacks
11962
"Callback", "History", "EarlyStopping", "ModelCheckpoint", "ReduceLROnPlateau",
120-
# Utils
12163
"train_test_split", "to_categorical", "normalize", "standardize",
12264
"shuffle_arrays", "batch_iterator",
12365
]

0 commit comments

Comments
 (0)