# GRNexus Python - Neural Network Library

![GRNexus](https://img.shields.io/badge/GRNexus-v0.1.7-blue?style=for-the-badge)
![Python](https://img.shields.io/badge/Python-3.8+-green?style=for-the-badge&logo=python)
![License](https://img.shields.io/badge/License-GPL--3.0-yellow?style=for-the-badge)
![PyPI](https://img.shields.io/pypi/v/grnexus?style=for-the-badge)

**A high-performance neural network library with Keras-like API**

---

## 📑 Table of Contents

- [Features](#-features)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Architecture](#-architecture)
- [Step-by-Step Guide](#-step-by-step-guide)
  - [1. Create a Model](#1-create-a-model)
  - [2. Add Layers](#2-add-layers)
  - [3. Compile](#3-compile)
  - [4. Train](#4-train)
  - [5. Predict](#5-predict)
  - [6. Save/Load](#6-saveload)
- [Complete API](#-complete-api)
  - [NeuralNetwork Class](#neuralnetwork-class)
  - [Available Layers](#available-layers)
  - [Activation Functions](#activation-functions)
  - [Optimizers](#optimizers)
  - [Loss Functions](#loss-functions)
- [Practical Examples](#-practical-examples)
- [Interactive Tests](#-interactive-tests)
- [Troubleshooting](#-troubleshooting)

---

## ✨ Features

- 🚀 **High Performance**: Optimized operations with native C libraries
- 🎯 **Keras-like API**: Familiar and easy to use for ML developers
- 🧩 **Modular**: Flexible architecture with decoupled layers
- 💾 **Persistence**: Save and load models in `.nexus` format
- 🎨 **50+ Activation Functions**: From ReLU to Snake, Mish, GELU, etc.
- 🔧 **Multiple Optimizers**: SGD, Momentum, Adam, RMSprop
- 📊 **Regularization**: Dropout, Batch Normalization
- 🤖 **ML Algorithms**: KNN, K-Means, Linear/Logistic Regression, Naive Bayes
- 🌍 **Cross-Platform**: Windows, macOS, Linux

---

## 📦 Installation

```bash
pip install grnexus
```

### Instalación desde Código Fuente

Para desarrolladores que quieran contribuir o usar la última versión:

```bash
# Clonar el repositorio
git clone https://github.com/grcodedigitalsolutions/GRNexus.git
cd GRNexus/python

# Instalar en modo desarrollo
pip install -e .

# O instalar dependencias opcionales para tests
pip install flet numpy
```

### Requisitos

- Python 3.8 o superior
- Sin dependencias externas (100% Python puro)
- Flet (opcional, solo para tests interactivos)
- NumPy (opcional, para operaciones matriciales avanzadas)

### Verificar Instalación

```python
from grnexus import NeuralNetwork

# Crear un modelo simple
model = NeuralNetwork()
print(f"✅ GRNexus v{grnexus.__version__} instalado correctamente!")
```

---

## 🚀 Inicio Rápido

```python
from grnexus import NeuralNetwork, DenseLayer, ReLU, DropoutLayer

# 1. Crear modelo
model = NeuralNetwork()

# 2. Agregar capas
model.add(DenseLayer(128, input_dim=784, activation=ReLU()))
model.add(DropoutLayer(0.2))
model.add(DenseLayer(64, activation=ReLU()))
model.add(DenseLayer(10))  # Capa de salida

# 3. Compilar
model.compile(
    optimizer='adam',
    loss='cross_entropy',
    learning_rate=0.001
)

# 4. Entrenar
history = model.train(
    x_train,  # Datos de entrenamiento
    y_train,  # Etiquetas (one-hot encoded)
    epochs=10,
    batch_size=32
)

# 5. Predecir
predictions = model.predict(x_test)

# 6. Guardar modelo
model.save('mi_modelo.nexus')

# Cargar modelo
model_cargado = NeuralNetwork.load('mi_modelo.nexus')
```

---

## 🏗️ Arquitectura

### Estructura del Proyecto

```
GRNexus/python/
├── grnexus.py              # Clase principal GRNexus
├── lib/
│   ├── grnexus_layers.py   # Implementación de capas
│   ├── grnexus_activations.py  # Funciones de activación
│   └── grnexus_optimizers.py   # Optimizers
├── test/
│   ├── test_digitos.py     # Test de reconocimiento de dígitos
│   ├── test_sentiment.py   # Test de análisis de sentimientos
│   ├── test_iris.py        # Test de clasificación Iris
│   ├── test_housing.py     # Test de predicción de precios
│   └── test_suite.py       # Launcher de todos los tests
└── README.md               # Este archivo
```

### Flujo de Datos

```
Input → DenseLayer → Activation → Dropout → ... → Softmax → Output
   ↓                                                            ↑
   └──────────── Backpropagation (gradientes) ─────────────────┘
```

---

## 🧠 Cómo Funciona una Red Neuronal

<details>
<summary><strong>📖 Explicación Básica para Principiantes (Click para expandir)</strong></summary>

### ¿Qué es una Neurona Artificial?

Imagina una neurona como una **pequeña calculadora** que:
1. Recibe varios números de entrada
2. Los multiplica por "pesos" (importancia de cada entrada)
3. Los suma todos
4. Aplica una función de activación (decide si "activarse" o no)
5. Produce un número de salida

```
Ejemplo simple:
Entradas: [2, 3, 1]
Pesos:    [0.5, 0.3, 0.2]

Cálculo: (2 × 0.5) + (3 × 0.3) + (1 × 0.2) = 1.0 + 0.9 + 0.2 = 2.1
Activación ReLU: max(0, 2.1) = 2.1
Salida: 2.1
```

### ¿Qué son los Pesos y Bias?

- **Pesos**: Son como "perillas de volumen" que controlan qué tan importante es cada entrada
  - Al inicio son aleatorios
  - Durante el entrenamiento se ajustan automáticamente
  - Pesos grandes = entrada muy importante
  - Pesos pequeños = entrada poco importante

- **Bias**: Es como un "ajuste fino" adicional
  - Permite a la neurona activarse incluso sin entradas
  - Ayuda a que la red sea más flexible

### ¿Cómo Aprende la Red?

El aprendizaje es un proceso de **prueba y error automatizado**:

```
1. PREDICCIÓN (Forward Pass):
   Entrada → Capa 1 → Capa 2 → ... → Salida
   
2. COMPARACIÓN:
   ¿Qué predijo la red? vs ¿Cuál era la respuesta correcta?
   Error = |Predicción - Respuesta Correcta|
   
3. AJUSTE (Backward Pass / Backpropagation):
   - La red calcula: "¿Qué pesos causaron este error?"
   - Ajusta los pesos en la dirección que reduce el error
   - Usa el "learning rate" para controlar qué tan grandes son los ajustes
   
4. REPETIR:
   - Hace esto miles de veces con muchos ejemplos
   - Gradualmente los pesos mejoran
   - El error disminuye
   - ¡La red aprende!
```

### ¿Qué hace la Función de Activación?

Las funciones de activación añaden **no-linealidad** (capacidad de aprender patrones complejos):

- **Sin activación**: La red solo puede aprender líneas rectas (muy limitado)
- **Con activación**: La red puede aprender curvas, formas complejas, patrones

**Analogía**: 
- Sin activación = Solo puedes dibujar con una regla
- Con activación = Puedes dibujar cualquier forma libremente

**Funciones comunes**:
- `ReLU`: Si el número es negativo → 0, si es positivo → déjalo igual
- `Sigmoid`: Convierte cualquier número a rango 0-1
- `Softmax`: Convierte números en probabilidades que suman 1

### Ejemplo Completo: Reconocimiento de Dígitos

```
PASO 1: PREPARAR DATOS
Imagen 28×28 → Aplanar → Vector de 784 números
[pixel1, pixel2, ..., pixel784]

PASO 2: FORWARD PASS (Predicción)
784 números → Capa 1 (128 neuronas con ReLU)
           → Capa 2 (64 neuronas con ReLU)  
           → Capa 3 (10 neuronas, una por dígito)
           → Softmax (convierte a probabilidades)
           → [0.01, 0.02, 0.85, 0.03, ...] ← "85% seguro que es un 2"

PASO 3: CALCULAR ERROR
Predicción: [0.01, 0.02, 0.85, 0.03, 0.01, 0.02, 0.01, 0.03, 0.01, 0.01]
Correcto:   [0,    0,    1,    0,    0,    0,    0,    0,    0,    0   ] (es un 2)
Error: Categorical Cross-Entropy = 0.16 (bastante bajo, buena predicción)

PASO 4: BACKWARD PASS (Aprendizaje)
- Calcular gradientes: "¿Qué pesos causaron el error?"
- Actualizar pesos: peso_nuevo = peso_viejo - (learning_rate × gradiente)
- Los pesos se ajustan para que la próxima vez prediga mejor

PASO 5: REPETIR
- Entrenar con miles de imágenes
- Cada vez los pesos mejoran un poquito
- Después de muchas épocas, ¡la red reconoce dígitos con 95%+ precisión!
```

### Conceptos Clave

| Concepto | Explicación Simple | Analogía |
|----------|-------------------|----------|
| **Neurona** | Calculadora que suma entradas ponderadas | Una persona votando (cada voto tiene diferente peso) |
| **Peso** | Importancia de una conexión | Volumen de un canal de audio |
| **Bias** | Ajuste fino adicional | Temperatura base de un termostato |
| **Activación** | Función que decide si la neurona "se activa" | Interruptor que decide si pasa la señal |
| **Forward Pass** | Calcular la predicción | Resolver un problema de matemáticas |
| **Loss** | Qué tan equivocada está la predicción | Distancia entre tu respuesta y la correcta |
| **Backward Pass** | Ajustar pesos para reducir error | Aprender de tus errores |
| **Epoch** | Una pasada completa por todos los datos | Leer un libro completo una vez |
| **Batch** | Grupo pequeño de ejemplos | Estudiar un capítulo del libro |
| **Learning Rate** | Qué tan grandes son los ajustes | Tamaño de los pasos cuando caminas |

</details>

### Flujo Detallado de Entrenamiento

```mermaid
graph TD
    A[Datos de Entrada] --> B[Normalización]
    B --> C[Forward Pass]
    C --> D[Capa Dense 1]
    D --> E[Activación ReLU]
    E --> F[Dropout]
    F --> G[Capa Dense 2]
    G --> H[Activación]
    H --> I[Capa de Salida]
    I --> J[Softmax/Output]
    J --> K[Calcular Loss]
    K --> L[Backward Pass]
    L --> M[Calcular Gradientes]
    M --> N[Actualizar Pesos]
    N --> O{¿Más Epochs?}
    O -->|Sí| C
    O -->|No| P[Modelo Entrenado]
```

### Proceso de Predicción vs Entrenamiento

| Fase | Forward Pass | Backward Pass | Actualizar Pesos |
|------|--------------|---------------|------------------|
| **Entrenamiento** | ✅ Sí | ✅ Sí | ✅ Sí |
| **Predicción** | ✅ Sí | ❌ No | ❌ No |

**Durante Entrenamiento:**
1. Forward: Calcular predicción
2. Comparar con etiqueta real
3. Backward: Calcular gradientes
4. Actualizar pesos con optimizer

**Durante Predicción:**
1. Forward: Calcular predicción
2. Retornar resultado
3. ¡Listo!

---

## 📚 Guía Paso a Paso

### 1. Crear un Modelo

```python
from grnexus import GRNexus

# Especificar la dimensión de entrada
model = GRNexus(input_dim=784)
```

**Parámetros:**
- `input_dim` (int): Número de características de entrada

**¿Cómo determinar input_dim?**
- Para imágenes 28x28: `input_dim = 28 * 28 = 784`
- Para texto vectorizado: `input_dim = tamaño_vocabulario`
- Para datos tabulares: `input_dim = número_de_columnas`

---

### 2. Agregar Capas

#### 2.1 Capa Densa (Dense Layer)

```python
model.add_dense(
    units=128,              # Número de neuronas
    activation='ReLU',      # Función de activación
    use_bias=True,          # Usar bias
    weight_init='he'        # Inicialización de pesos
)
```

**Parámetros:**
- `units` (int): Número de neuronas en la capa
- `activation` (str, opcional): Nombre de la función de activación
  - Opciones: `'ReLU'`, `'Sigmoid'`, `'Tanh'`, `'Swish'`, `'GELU'`, `'Mish'`, etc.
- `use_bias` (bool): Si usar bias (default: `True`)
- `weight_init` (str): Método de inicialización
  - `'xavier'`: Para Sigmoid/Tanh
  - `'he'`: Para ReLU y variantes (recomendado)
  - `'normal'`: Distribución normal
  - `'uniform'`: Distribución uniforme

#### 2.2 Capa de Dropout

```python
model.add_dropout(rate=0.2)  # Desactivar 20% de neuronas durante entrenamiento
```

**Parámetros:**
- `rate` (float): Proporción de neuronas a desactivar (0.0 - 1.0)

**¿Cuándo usar Dropout?**
- Para prevenir overfitting
- Típicamente después de capas densas
- Valores comunes: 0.2 - 0.5

#### 2.3 Capa de Batch Normalization

```python
model.add_batch_norm(
    momentum=0.99,
    epsilon=1e-5
)
```

**Parámetros:**
- `momentum` (float): Momentum para actualizar media y varianza móviles
- `epsilon` (float): Valor pequeño para estabilidad numérica

#### 2.4 Capa de Activación

```python
model.add_activation('ReLU')
```

**¿Cuándo usar?**
- Cuando necesitas aplicar una activación sin una capa densa
- Para arquitecturas más complejas

#### 2.5 Capa Softmax

```python
model.add_softmax()  # Para clasificación multiclase
```

**Importante:**
- Solo usar en la capa de salida
- Para problemas de clasificación
- Convierte logits en probabilidades que suman 1.0

---

### 3. Compilar

```python
model.compile(
    optimizer='adam',        # Optimizer a usar
    loss='categorical_crossentropy',  # Función de pérdida
    learning_rate=0.01,      # Tasa de aprendizaje
    metrics=['accuracy']     # Métricas a monitorear
)
```

**Parámetros:**
- `optimizer` (str): Algoritmo de optimización
  - `'sgd'`: Stochastic Gradient Descent
  - `'momentum'`: SGD con Momentum
  - `'adam'`: Adaptive Moment Estimation (recomendado)
  - `'rmsprop'`: RMSprop
- `loss` (str): Función de pérdida
  - `'categorical_crossentropy'`: Para clasificación multiclase
  - `'mse'`: Mean Squared Error (para regresión)
- `learning_rate` (float): Tasa de aprendizaje
  - Valores típicos: 0.001 - 0.1
  - Valores más bajos: aprendizaje más estable pero lento
  - Valores más altos: aprendizaje más rápido pero puede divergir
- `metrics` (list, opcional): Métricas a calcular

**🎯 Guía de Learning Rate:**
| Optimizer | LR Recomendado |
|-----------|----------------|
| SGD       | 0.01 - 0.1     |
| Momentum  | 0.01 - 0.05    |
| Adam      | 0.001 - 0.01   |
| RMSprop   | 0.001 - 0.01   |

---

### 4. Entrenar

#### 4.1 Método `fit()` (Recomendado)

```python
history = model.fit(
    x_train,           # Datos de entrenamiento
    y_train,           # Etiquetas
    epochs=10,         # Número de épocas
    batch_size=32,     # Tamaño del batch
    validation_data=(x_val, y_val),  # Datos de validación (opcional)
    verbose=1          # Nivel de detalle (0, 1, 2)
)
```

**Parámetros:**
- `x_train` (list): Datos de entrada (lista de listas)
  - Ejemplo: `[[0.1, 0.2, ...], [0.3, 0.4, ...], ...]`
- `y_train` (list): Etiquetas (one-hot encoded para clasificación)
  - Ejemplo: `[[1, 0, 0], [0, 1, 0], ...]` para 3 clases
- `epochs` (int): Número de veces que pasar por todo el dataset
- `batch_size` (int): Número de muestras por actualización de pesos
- `validation_data` (tuple, opcional): (x_val, y_val) para validación
- `verbose` (int): 0=silencioso, 1=barra de progreso, 2=una línea por época

**Retorna:**
- `history` (dict): Historial de entrenamiento con pérdidas por época

#### 4.2 Método `train_on_batch()` (Para control fino)

```python
for epoch in range(epochs):
    for i in range(0, len(x_train), batch_size):
        x_batch = x_train[i:i+batch_size]
        y_batch = y_train[i:i+batch_size]
        
        loss = model.train_on_batch(x_batch, y_batch)
        print(f"Batch loss: {loss}")
```

---

### 5. Predecir

#### 5.1 Predicción Simple

```python
# Predecir una muestra
prediction = model.predict(x_sample)  # x_sample es una lista de números

# Predecir múltiples muestras
predictions = model.predict(x_test)   # x_test es lista de listas
```

#### 5.2 Obtener Clase Predicha

```python
# Para clasificación
prediction = model.predict(x_sample)
predicted_class = prediction.index(max(prediction))  # Índice de máxima probabilidad
confidence = max(prediction)  # Confianza de la predicción

print(f"Clase predicha: {predicted_class}")
print(f"Confianza: {confidence:.2%}")
```

---

### 6. Guardar/Cargar

#### 6.1 Guardar Modelo

```python
model.save('mi_modelo.nexus')
```

**Formato `.nexus`:**
- Guarda arquitectura completa del modelo
- Guarda todos los pesos y biases
- Guarda configuración del optimizer
- Compatible entre plataformas

#### 6.2 Cargar Modelo

```python
from grnexus import GRNexus

modelo_cargado = GRNexus.load('mi_modelo.nexus')

# Usar inmediatamente sin recompilar
predicciones = modelo_cargado.predict(x_test)
```

---

## 📊 Entendiendo los Datos

### Preparación de Datos para Entrenamiento

Los datos son el combustible de las redes neuronales. Aquí aprenderás cómo prepararlos correctamente.

#### 1. Formato de Entrada

Las redes neuronales solo entienden **números**. Debes convertir tus datos a listas de números.

**Ejemplos por tipo de dato:**

```python
# IMÁGENES (28x28 píxeles)
imagen_2d = [
    [0.0, 0.1, 0.2, ...],  # Fila 1
    [0.3, 0.4, 0.5, ...],  # Fila 2
    ...
]
# Aplanar a 1D
imagen_1d = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, ...]  # 784 números

# TEXTO
texto = "This movie is great"
# Convertir a bag-of-words o embeddings
vector_texto = [0.0, 0.2, 0.0, 0.5, ...]  # Cada posición = una palabra

# DATOS TABULARES
casa = {
    'habitaciones': 3,
    'baños': 2,
    'metros': 120,
    'año': 2010,
    'distancia': 5
}
# Convertir a lista
vector_casa = [3, 2, 120, 2010, 5]
```

#### 2. Normalización de Datos

**¿Por qué normalizar?**
- Las redes aprenden mejor cuando todos los valores están en rangos similares
- Evita que características con valores grandes dominen el aprendizaje
- Acelera la convergencia

**Métodos de normalización:**

```python
# MIN-MAX NORMALIZATION (escala a [0, 1])
def normalizar_min_max(valor, min_val, max_val):
    return (valor - min_val) / (max_val - min_val)

# Ejemplo:
edad = 25  # rango: 18-80
edad_norm = (25 - 18) / (80 - 18) = 0.113

# Z-SCORE NORMALIZATION (media=0, std=1)
def normalizar_z_score(valor, media, std):
    return (valor - media) / std

# PARA IMÁGENES (ya están en [0, 255])
pixel = 128
pixel_norm = pixel / 255.0  # → 0.502
```

**Ejemplo completo:**

```python
# Datos sin normalizar
datos_crudos = [
    [3, 2, 120, 2010, 5],    # habitaciones, baños, m², año, km
    [4, 3, 180, 2015, 3],
    [2, 1, 80, 1990, 10]
]

# Encontrar min y max de cada característica
mins = [2, 1, 70, 1985, 1]
maxs = [6, 5, 320, 2024, 15]

# Normalizar
datos_normalizados = []
for muestra in datos_crudos:
    muestra_norm = []
    for i, valor in enumerate(muestra):
        norm = (valor - mins[i]) / (maxs[i] - mins[i])
        muestra_norm.append(norm)
    datos_normalizados.append(muestra_norm)

# Resultado:
# [[0.25, 0.25, 0.2, 0.64, 0.29],
#  [0.5, 0.5, 0.44, 0.77, 0.14],
#  [0.0, 0.0, 0.04, 0.13, 0.64]]
```

#### 3. One-Hot Encoding

Para **clasificación**, las etiquetas deben estar en formato one-hot.

**¿Qué es One-Hot Encoding?**

Convertir una categoría en un vector donde:
- Todas las posiciones son 0
- Excepto la posición de la categoría correcta que es 1

```python
# EJEMPLO: Clasificación de dígitos (0-9)
digito = 3

# One-hot encoding:
one_hot = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
#          0  1  2  3  4  5  6  7  8  9
#                 ↑
#            posición 3 = 1

# Función para crear one-hot
def crear_one_hot(etiqueta, num_clases):
    vector = [0.0] * num_clases
    vector[etiqueta] = 1.0
    return vector

# Ejemplos:
crear_one_hot(0, 10)  # → [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
crear_one_hot(5, 10)  # → [0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
crear_one_hot(9, 10)  # → [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
```

**Para múltiples muestras:**

```python
# Etiquetas originales
etiquetas = [3, 7, 2, 9, 1]

# Convertir a one-hot
y_train = []
for etiqueta in etiquetas:
    one_hot = [0.0] * 10
    one_hot[etiqueta] = 1.0
    y_train.append(one_hot)

# Resultado:
# [[0,0,0,1,0,0,0,0,0,0],  # 3
#  [0,0,0,0,0,0,0,1,0,0],  # 7
#  [0,0,1,0,0,0,0,0,0,0],  # 2
#  [0,0,0,0,0,0,0,0,0,1],  # 9
#  [0,1,0,0,0,0,0,0,0,0]]  # 1
```

#### 4. Formato de Entrada/Salida

**Para Clasificación:**

```python
# ENTRADA: Lista de listas (cada muestra es una lista)
x_train = [
    [0.1, 0.2, 0.3, ...],  # Muestra 1
    [0.4, 0.5, 0.6, ...],  # Muestra 2
    [0.7, 0.8, 0.9, ...]   # Muestra 3
]

# SALIDA: One-hot encoded
y_train = [
    [1, 0, 0],  # Clase 0
    [0, 1, 0],  # Clase 1
    [1, 0, 0]   # Clase 0
]

# ENTRENAR
model.fit(x_train, y_train, epochs=10, batch_size=32)
```

**Para Regresión:**

```python
# ENTRADA: Igual que clasificación
x_train = [
    [3, 2, 120, 2010, 5],   # Casa 1
    [4, 3, 180, 2015, 3],   # Casa 2
    [2, 1, 80, 1990, 10]    # Casa 3
]

# SALIDA: Valores continuos (NO one-hot)
y_train = [
    [250],  # Precio casa 1: $250k
    [380],  # Precio casa 2: $380k
    [150]   # Precio casa 3: $150k
]

# ENTRENAR
model.fit(x_train, y_train, epochs=100, batch_size=8)
```

#### 5. Validación de Datos

Antes de entrenar, verifica que tus datos estén correctos:

```python
# Verificar dimensiones
print(f"Muestras de entrenamiento: {len(x_train)}")
print(f"Dimensión de entrada: {len(x_train[0])}")
print(f"Dimensión de salida: {len(y_train[0])}")

# Verificar que coincidan
assert len(x_train) == len(y_train), "¡x_train y y_train deben tener mismo tamaño!"

# Verificar que input_dim sea correcto
assert len(x_train[0]) == model.input_dim, f"¡input_dim debe ser {len(x_train[0])}!"

# Verificar rangos (después de normalizar)
import statistics
muestra = x_train[0]
print(f"Min: {min(muestra)}, Max: {max(muestra)}")
# Idealmente debe estar en [0, 1] o [-1, 1]

# Verificar one-hot (para clasificación)
for y in y_train:
    assert sum(y) == 1.0, "¡One-hot debe sumar 1!"
    assert max(y) == 1.0, "¡One-hot debe tener un 1!"
```

#### 6. División Train/Test

Siempre separa tus datos en entrenamiento y prueba:

```python
import random

# Mezclar datos
combined = list(zip(x_data, y_data))
random.shuffle(combined)
x_data, y_data = zip(*combined)

# Dividir 80% train, 20% test
split_idx = int(0.8 * len(x_data))

x_train = list(x_data[:split_idx])
y_train = list(y_data[:split_idx])

x_test = list(x_data[split_idx:])
y_test = list(y_data[split_idx:])

print(f"Train: {len(x_train)} muestras")
print(f"Test: {len(x_test)} muestras")

# Entrenar solo con train
model.fit(x_train, y_train, epochs=50)

# Evaluar con test
for x, y in zip(x_test, y_test):
    pred = model.predict(x)
    # Comparar pred con y
```

---

## 🔧 API Completa

### Clase GRNexus

#### Constructor

```python
GRNexus(input_dim: int)
```

#### Métodos de Construcción

| Método | Descripción |
|--------|-------------|
| `add_dense(units, activation=None, use_bias=True, weight_init='xavier')` | Agregar capa densa |
| `add_activation(activation)` | Agregar capa de activación |
| `add_dropout(rate)` | Agregar dropout |
| `add_batch_norm(momentum=0.99, epsilon=1e-5)` | Agregar batch normalization |
| `add_softmax()` | Agregar softmax (salida) |

#### Métodos de Configuración

| Método | Descripción |
|--------|-------------|
| `compile(optimizer, loss, learning_rate, metrics=None)` | Compilar modelo |

#### Métodos de Entrenamiento

| Método | Descripción |
|--------|-------------|
| `fit(x_train, y_train, epochs, batch_size, validation_data=None, verbose=1)` | Entrenar modelo |
| `train_on_batch(x_batch, y_batch)` | Entrenar en un batch |

#### Métodos de Predicción

| Método | Descripción |
|--------|-------------|
| `predict(x, batch_size=32)` | Hacer predicciones |
| `forward(x, training=False)` | Forward pass (bajo nivel) |

#### Métodos de Persistencia

| Método | Descripción |
|--------|-------------|
| `save(filename)` | Guardar modelo |
| `load(filename)` (estático) | Cargar modelo |

---

### Capas Disponibles

#### DenseLayer
```python
DenseLayer(units, input_dim, activation=None, use_bias=True, weight_init='xavier')
```
Capa completamente conectada.

#### DropoutLayer
```python
DropoutLayer(rate)
```
Desactiva aleatoriamente neuronas durante entrenamiento.

#### BatchNormLayer
```python
BatchNormLayer(input_dim, momentum=0.99, epsilon=1e-5)
```
Normaliza activaciones para acelerar entrenamiento.

#### ActivationLayer
```python
ActivationLayer(activation)
```
Aplica función de activación no lineal.

#### SoftmaxLayer
```python
SoftmaxLayer()
```
Convierte logits en probabilidades (suma = 1).

---

### Funciones de Activación

#### Básicas
- `Linear`: f(x) = x
- `Step`: Función escalón
- `Sigmoid`: f(x) = 1 / (1 + e^(-x))
- `Tanh`: f(x) = tanh(x)

#### ReLU y Variantes
- `ReLU`: f(x) = max(0, x)
- `LeakyReLU`: f(x) = max(αx, x)
- `PReLU`: ReLU paramétrico
- `ELU`: Exponential Linear Unit
- `SELU`: Scaled ELU
- `ReLU6`: min(max(0, x), 6)

#### Modernas
- `Swish`: f(x) = x · sigmoid(x)
- `Mish`: f(x) = x · tanh(softplus(x))
- `GELU`: Gaussian Error Linear Unit
- `LiSHT`: Linearly Scaled Hyperbolic Tangent

#### Especializadas
- `Softplus`: f(x) = log(1 + e^x)
- `Softsign`: f(x) = x / (1 + |x|)
- `HardSigmoid`: Aproximación rápida de sigmoid
- `HardSwish`: Aproximación rápida de swish
- `HardTanh`: Versión acotada de tanh

#### Exóticas
- `Snake`: Función periódica
- `ISRU`: Inverse Square Root Unit
- `FReLU`: Funnel ReLU
- **Y más de 40 adicionales...**

**Ver lista completa en:** `lib/grnexus_activations.py`

---

### Optimizers

#### SGD (Stochastic Gradient Descent)
```python
model.compile(optimizer='sgd', learning_rate=0.01)
```
El optimizer más básico. Actualiza pesos en dirección opuesta al gradiente.

#### Momentum
```python
model.compile(optimizer='momentum', learning_rate=0.01)
```
SGD con momento. Acelera convergencia y reduce oscilaciones.

#### Adam (Recomendado)
```python
model.compile(optimizer='adam', learning_rate=0.001)
```
Adaptive Moment Estimation. Combina momentum con tasas de aprendizaje adaptativas.

#### RMSprop
```python
model.compile(optimizer='rmsprop', learning_rate=0.001)
```
Root Mean Square Propagation. Bueno para RNNs.

---

### Funciones de Pérdida

#### Categorical Cross-Entropy
```python
model.compile(loss='categorical_crossentropy')
```
Para clasificación multiclase con one-hot encoding.

#### MSE (Mean Squared Error)
```python
model.compile(loss='mse')
```
Para problemas de regresión.

---

## 💡 Ejemplos Prácticos

### Ejemplo 1: Clasificación de Dígitos (MNIST-like)

```python
from grnexus import GRNexus

# ============================================
# PASO 1: CREAR EL MODELO
# ============================================
# input_dim=784 porque las imágenes son 28x28 píxeles
# 28 × 28 = 784 valores de entrada
model = GRNexus(input_dim=784)

# ============================================
# PASO 2: CONSTRUIR LA ARQUITECTURA
# ============================================
# Primera capa oculta: 784 → 128 neuronas
# - ReLU: Función de activación que elimina valores negativos
# - he: Inicialización de pesos óptima para ReLU
model.add_dense(128, activation='ReLU', weight_init='he')

# Dropout: Desactiva aleatoriamente 20% de neuronas durante entrenamiento
# - Previene overfitting (memorización de datos)
# - Solo activo durante entrenamiento, no durante predicción
model.add_dropout(0.2)

# Segunda capa oculta: 128 → 64 neuronas
model.add_dense(64, activation='ReLU')
model.add_dropout(0.2)

# Capa de salida: 64 → 10 neuronas (una por cada dígito 0-9)
model.add_dense(10)

# Softmax: Convierte las 10 salidas en probabilidades que suman 1.0
# Ejemplo: [0.01, 0.02, 0.85, 0.03, ...] → "85% seguro que es un 2"
model.add_softmax()

# ============================================
# PASO 3: COMPILAR EL MODELO
# ============================================
model.compile(
    optimizer='adam',  # Adam: optimizer adaptativo, generalmente el mejor
    loss='categorical_crossentropy',  # Para clasificación multiclase
    learning_rate=0.01  # Qué tan grandes son los ajustes de pesos
)

# ============================================
# PASO 4: PREPARAR DATOS
# ============================================
# Ejemplo de cómo preparar datos (debes tener tus propios datos)
x_train = []  # Lista de imágenes aplanadas (cada una con 784 valores)
y_train = []  # Lista de etiquetas one-hot encoded

# Ejemplo: Agregar una imagen del dígito "3"
imagen_3 = [0.0] * 784  # Imagen de 28x28 aplanada
# ... (aquí irían los valores reales de los píxeles)

etiqueta_3 = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]  # One-hot para el dígito 3

x_train.append(imagen_3)
y_train.append(etiqueta_3)

# ... agregar más ejemplos ...

# ============================================
# PASO 5: ENTRENAR
# ============================================
print("Iniciando entrenamiento...")
history = model.fit(
    x_train, y_train,
    epochs=10,      # Número de veces que ver todo el dataset
    batch_size=32,  # Procesar 32 imágenes a la vez
    verbose=1       # Mostrar progreso
)

# ============================================
# PASO 6: HACER PREDICCIONES
# ============================================
# Predecir una imagen de prueba
test_image = [...]  # Imagen de prueba 784D

# Obtener probabilidades para cada dígito
prediction = model.predict(test_image)
# Resultado: [0.01, 0.02, 0.85, 0.03, 0.01, 0.02, 0.01, 0.03, 0.01, 0.01]

# Encontrar el dígito con mayor probabilidad
digit = prediction.index(max(prediction))
confidence = max(prediction)

print(f"Dígito predicho: {digit}")
print(f"Confianza: {confidence:.1%}")
# Salida: "Dígito predicho: 2, Confianza: 85.0%"

# ============================================
# PASO 7: GUARDAR MODELO
# ============================================
model.save('mnist_model.nexus')
print("Modelo guardado exitosamente")

# ============================================
# PASO 8: CARGAR Y USAR MODELO GUARDADO
# ============================================
# En otra sesión, puedes cargar el modelo entrenado
loaded_model = GRNexus.load('mnist_model.nexus')
new_prediction = loaded_model.predict(test_image)
```

**Resultados Esperados:**
- Precisión en entrenamiento: ~90-95% después de 10 épocas
- Loss final: ~0.2-0.3
- Tiempo de entrenamiento: Depende del tamaño del dataset

---

### Ejemplo 2: Análisis de Sentimientos

```python
from grnexus import GRNexus

# ============================================
# PREPARAR DATOS DE TEXTO
# ============================================
# Construir vocabulario (palabras únicas)
reviews = [
    "This movie is amazing and wonderful",
    "Terrible film, waste of time",
    "Great acting and beautiful scenes"
]

# Crear diccionario de palabras
vocab = {}
for review in reviews:
    words = review.lower().split()
    for word in words:
        if word not in vocab:
            vocab[word] = len(vocab)

vocab_size = len(vocab)
print(f"Vocabulario: {vocab_size} palabras")

# ============================================
# CONVERTIR TEXTO A VECTORES (BAG-OF-WORDS)
# ============================================
def text_to_vector(text, vocab):
    """
    Convierte texto en vector numérico
    Cada posición representa una palabra del vocabulario
    El valor es la frecuencia de esa palabra
    """
    vector = [0.0] * len(vocab)
    words = text.lower().split()
    
    # Contar ocurrencias
    for word in words:
        if word in vocab:
            vector[vocab[word]] += 1.0
    
    # Normalizar (convertir conteos a frecuencias)
    total = sum(vector)
    if total > 0:
        vector = [v / total for v in vector]
    
    return vector

# ============================================
# CREAR MODELO
# ============================================
model = GRNexus(input_dim=vocab_size)

# Arquitectura para análisis de sentimientos
model.add_dense(64, activation='ReLU')
model.add_dropout(0.3)  # Dropout más alto para texto
model.add_dense(32, activation='Swish')  # Swish: activación moderna
model.add_dense(2)  # 2 clases: Positivo/Negativo
model.add_softmax()

model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    learning_rate=0.005  # Learning rate más bajo para mejor convergencia
)

# ============================================
# PREPARAR DATASET
# ============================================
x_train = [
    text_to_vector("This movie is amazing", vocab),
    text_to_vector("Terrible waste of time", vocab),
    # ... más ejemplos ...
]

y_train = [
    [0, 1],  # Positivo
    [1, 0],  # Negativo
    # ... más etiquetas ...
]

# ============================================
# ENTRENAR
# ============================================
model.fit(x_train, y_train, epochs=20, batch_size=16)

# ============================================
# PREDECIR SENTIMIENTO
# ============================================
new_review = "This film is absolutely fantastic"
vector = text_to_vector(new_review, vocab)
prediction = model.predict(vector)

sentiment = "Positivo" if prediction[1] > prediction[0] else "Negativo"
confidence = max(prediction)

print(f"Sentimiento: {sentiment} ({confidence:.1%} confianza)")
```

**Resultados Esperados:**
- Precisión: ~80-90% con dataset pequeño
- El modelo aprende asociaciones palabra-sentimiento
- Palabras como "amazing", "great" → Positivo
- Palabras como "terrible", "waste" → Negativo

---

### Ejemplo 3: Clasificación Multiclase (Iris)

```python
from grnexus import GRNexus

# ============================================
# DATASET IRIS
# ============================================
# 4 características: largo sépalo, ancho sépalo, largo pétalo, ancho pétalo
# 3 especies: Setosa (0), Versicolor (1), Virginica (2)

# Datos de ejemplo (valores reales del dataset Iris)
iris_data = [
    [5.1, 3.5, 1.4, 0.2, 0],  # Setosa
    [7.0, 3.2, 4.7, 1.4, 1],  # Versicolor
    [6.3, 3.3, 6.0, 2.5, 2],  # Virginica
    # ... más muestras ...
]

# ============================================
# NORMALIZAR DATOS
# ============================================
def normalize_iris(data):
    """Normaliza características al rango [0, 1]"""
    # Valores min/max del dataset Iris
    mins = [4.3, 2.0, 1.0, 0.1]
    maxs = [7.9, 4.4, 6.9, 2.5]
    
    normalized = []
    for i, val in enumerate(data[:4]):  # Solo las 4 características
        norm = (val - mins[i]) / (maxs[i] - mins[i])
        normalized.append(norm)
    
    return normalized

# ============================================
# PREPARAR DATOS
# ============================================
x_train = [normalize_iris(sample) for sample in iris_data]

y_train = []
for sample in iris_data:
    species = sample[4]  # Última columna es la especie
    one_hot = [0.0, 0.0, 0.0]
    one_hot[species] = 1.0
    y_train.append(one_hot)

# ============================================
# CREAR MODELO
# ============================================
model = GRNexus(input_dim=4)  # 4 características

# Arquitectura pequeña (dataset pequeño)
model.add_dense(16, activation='ReLU')
model.add_dense(8, activation='Swish')
model.add_dense(3)  # 3 especies
model.add_softmax()

model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',
    learning_rate=0.01
)

# ============================================
# ENTRENAR
# ============================================
model.fit(x_train, y_train, epochs=100, batch_size=16)

# ============================================
# PREDECIR ESPECIE
# ============================================
# Nueva flor con medidas: [6.0, 3.0, 4.5, 1.5]
new_flower = normalize_iris([6.0, 3.0, 4.5, 1.5, 0])
prediction = model.predict(new_flower)

species_names = ["Setosa", "Versicolor", "Virginica"]
predicted_species = prediction.index(max(prediction))

print(f"Especie predicha: {species_names[predicted_species]}")
print(f"Probabilidades:")
for i, name in enumerate(species_names):
    print(f"  {name}: {prediction[i]:.1%}")
```

**Resultados Esperados:**
- Precisión: ~95-98% (dataset Iris es muy separable)
- El modelo aprende patrones en las medidas de las flores
- Setosa es fácilmente distinguible (pétalos pequeños)
- Versicolor y Virginica son más similares

---

### Ejemplo 4: Regresión (Predicción de Precios)

```python
from grnexus import GRNexus

# ============================================
# DATOS DE CASAS
# ============================================
# [habitaciones, baños, m², año, distancia_km, precio_miles]
housing_data = [
    [3, 2, 120, 2010, 5, 250],
    [4, 3, 180, 2015, 3, 380],
    [2, 1, 80, 1990, 10, 150],
    # ... más casas ...
]

# ============================================
# NORMALIZAR ENTRADA Y SALIDA
# ============================================
def normalize_features(features):
    """Normaliza las 5 características"""
    mins = [2, 1, 70, 1985, 1]
    maxs = [6, 5, 320, 2024, 15]
    
    return [(f - mins[i]) / (maxs[i] - mins[i]) 
            for i, f in enumerate(features)]

def normalize_price(price):
    """Normaliza precio a [0, 1]"""
    return (price - 100) / (750 - 100)

def denormalize_price(norm_price):
    """Convierte precio normalizado a valor real"""
    return norm_price * (750 - 100) + 100

# ============================================
# PREPARAR DATOS
# ============================================
x_train = [normalize_features(house[:5]) for house in housing_data]
y_train = [[normalize_price(house[5])] for house in housing_data]

# ============================================
# CREAR MODELO (REGRESIÓN)
# ============================================
model = GRNexus(input_dim=5)

# Arquitectura para regresión
model.add_dense(32, activation='ReLU')
model.add_dropout(0.2)
model.add_dense(16, activation='Swish')
model.add_dense(8, activation='ReLU')
model.add_dense(1)  # UNA sola neurona de salida (precio)
# NO usar Softmax en regresión

model.compile(
    optimizer='adam',
    loss='categorical_crossentropy',  # Usaremos MSE manualmente
    learning_rate=0.001  # Learning rate bajo para regresión
)

# ============================================
# ENTRENAR
# ============================================
print("Entrenando modelo de regresión...")
for epoch in range(150):
    total_loss = 0
    for i in range(0, len(x_train), 4):
        batch_x = x_train[i:i+4]
        batch_y = y_train[i:i+4]
        loss = model.train_on_batch(batch_x, batch_y)
        total_loss += loss
    
    if (epoch + 1) % 30 == 0:
        avg_loss = total_loss / (len(x_train) / 4)
        print(f"Epoch {epoch+1}/150 - loss: {avg_loss:.4f}")

# ============================================
# PREDECIR PRECIO
# ============================================
# Casa nueva: 3 hab, 2 baños, 140m², año 2012, 6km del centro
new_house = normalize_features([3, 2, 140, 2012, 6])
prediction_norm = model.predict(new_house)

# Desnormalizar para obtener precio real
predicted_price = denormalize_price(prediction_norm[0])

print(f"\nCasa: 3 hab, 2 baños, 140m², 2012, 6km centro")
print(f"Precio predicho: ${predicted_price:.0f}k")

# ============================================
# EVALUAR MODELO
# ============================================
errors = []
for house in housing_data:
    features = normalize_features(house[:5])
    real_price = house[5]
    
    pred_norm = model.predict(features)
    pred_price = denormalize_price(pred_norm[0])
    
    error = abs(real_price - pred_price)
    errors.append(error)

avg_error = sum(errors) / len(errors)
print(f"\nError promedio: ${avg_error:.1f}k")
```

**Resultados Esperados:**
- Error promedio: $20-40k (depende del dataset)
- El modelo aprende relaciones:
  - Más habitaciones → Precio más alto
  - Más metros cuadrados → Precio más alto
  - Más cerca del centro → Precio más alto
  - Más nuevo → Precio más alto

---

## 🎮 Tests Interactivos

GRNexus incluye 5 aplicaciones interactivas con Flet para probar las capacidades de la biblioteca:

### 1. test_digitos.py - Reconocimiento de Dígitos
```bash
python3 test/test_digitos.py
```
- Dibuja dígitos con el mouse
- Entrena la red en tiempo real
- Visualiza probabilidades por clase
- Guarda/carga modelos

### 2. test_sentiment.py - Análisis de Sentimientos
```bash
python3 test/test_sentiment.py
```
- Analiza reseñas de películas
- 30 reseñas reales de IMDb
- Clasifica como positivo/negativo
- Bag-of-words + red neuronal

### 3. test_iris.py - Clasificación Iris
```bash
python3 test/test_iris.py
```
- Dataset clásico de ML
- 90 muestras de 3 especies de flores
- Sliders para ajustar características
- Visualización de probabilidades

### 4. test_housing.py - Predicción de Precios
```bash
python3 test/test_housing.py
```
- Regresión con datos reales
- 42 casas con precios
- Sliders para habitaciones, m², etc.
- Compara precio real vs predicho

### 5. test_suite.py - Launcher Maestro
```bash
python3 test/test_suite.py
```
- Ejecuta todos los tests desde una interfaz
- Descripción de cada test
- Ejecutar individual o todos juntos

---

## 🔍 Troubleshooting

### Problema: "ModuleNotFoundError: No module named 'grnexus'"

**Solución:**
```python
import sys
import os
sys.path.insert(0, os.path.abspath('path/to/GRNexus/python'))
```

### Problema: "TypeError: layer.backward() takes 2 positional arguments but 3 were given"

**Causa:** Código desactualizado de GRNexus v1.0

**Solución:** Actualizar a GRNexus v2.0. El método `backward` ya no acepta `learning_rate`.

### Problema: Pérdida (loss) no disminuye

**Posibles causas:**
1. Learning rate muy alto → Reducir a 0.001 o menos
2. Learning rate muy bajo → Aumentar a 0.01
3. Datos no normalizados → Normalizar a rango [0, 1] o [-1, 1]
4. Arquitectura inadecuada → Agregar más capas/neuronas
5. Inicialización incorrecta → Usar 'he' para ReLU, 'xavier' para Sigmoid/Tanh

### Problema: Overfitting (buena pérdida en entrenamiento, mala en validación)

**Soluciones:**
- Agregar Dropout: `model.add_dropout(0.2)`
- Reducir tamaño del modelo (menos neuronas/capas)
- Aumentar tamaño del dataset
- Agregar regularización

### Problema: Predicciones siempre retornan la misma clase

**Causas:**
- Clases desbalanceadas en el dataset
- Learning rate muy alto provocando divergencia
- Pesos no inicializados correctamente

**Soluciones:**
- Balancear clases en el dataset
- Reducir learning rate
- Verificar que `compile()` fue llamado antes de entrenar

### Problema: "IndexError: list index out of range" durante entrenamiento

**Causa:** Dimensiones incorrectas en los datos

**Solución:**
- Verificar que `x_train` tenga dimensión `input_dim`
- Verificar que `y_train` sea one-hot encoded con `num_classes` elementos
- Ejemplo: Si 3 clases, usar `[[1,0,0], [0,1,0], [0,0,1]]`

---

## 📊 Mejores Prácticas

### 1. Preparación de Datos

```python
# Normalizar datos
def normalize(data):
    min_val = min(data)
    max_val = max(data)
    return [(x - min_val) / (max_val - min_val) for x in data]

# One-hot encoding
def one_hot(label, num_classes):
    encoding = [0.0] * num_classes
    encoding[label] = 1.0
    return encoding
```

### 2. Arquitectura

- **Clasificación simple:** 1-2 capas ocultas
- **Clasificación compleja:** 3-5 capas con Dropout
- **Regresión:** 2-4 capas sin Softmax final

### 3. Hiperparámetros

| Tamaño Dataset | Batch Size | Learning Rate | Epochs |
|----------------|------------|---------------|--------|
| < 1000         | 16-32      | 0.01          | 50-100 |
| 1000-10000     | 32-64      | 0.01-0.001    | 20-50  |
| > 10000        | 64-128     | 0.001         | 10-30  |

### 4. Monitoreo

```python
history = model.fit(x_train, y_train, epochs=10, verbose=1)

# Graficar pérdida por época
import matplotlib.pyplot as plt
plt.plot(history['loss'])
plt.xlabel('Época')
plt.ylabel('Pérdida')
plt.title('Curva de Entrenamiento')
plt.show()
```

---

<div align="center">

**Hecho con ❤️ por el equipo de GRNexus**

[GitHub](https://github.com/tu-usuario/GRNexus) • [Documentación](docs/) • [Examples](test/)

</div>
