Nota: Esta es el 3er post de la serie "Entrenando mi red neuronal sin frameworks".
Si te perdiste el inicio:
Parte 1: La Idea y la Arquitectura
Parte 2: Ingeniería de datos
Introducción: Cuando "un ratito" se convierte en magia
Ayer domingo me desperté con una intención modesta: dedicarle "un par de horas" a avanzar con Prize, mi proyecto de red neuronal casera. Mi plan era simplemente configurar algunos archivos y quizás dejar todo listo para trabajar con los hiperparámetros y el entrenamiento durante la semana.
Pero esta intención se convirtió en 4 horas de "vibe coding".
Lo primero fue convertir los ~11.000 items del data set en embeddings, usando la API de OpenRouter y el excelente modelo qwen/qwen3-embedding-8b.
Luego, con la ayuda de Google Gemini Pro, empezó la sesión de "vibe coding": Lo que iba a ser una sesión de configuración se convirtió en una maratón de ingeniería donde resolvimos problemas de formatos, ajustamos dimensiones vectoriales y depuramos matrices de Numpy.
El resultado no fue solo "un avance". Fue el nacimiento oficial de Prize.
El Entrenamiento: 99% en la primera vuelta
Corrí el script de entrenamiento main.py esperando ver una curva de aprendizaje lenta. Aquí les dejo el código:
prize_loader.py
import numpy as np
import random
import argparse
import sys
import os
INPUT_SIZE = 1024
OUTPUT_SIZE = 2 # Clase 0: Farmacia, Clase 1: Laboratorio
def vectorizar_entrada(lista_floats):
"""Convierte lista a matriz columna (1024, 1) para Numpy"""
return np.reshape(lista_floats, (INPUT_SIZE, 1))
def vectorizar_salida(indice_clase):
"""One-Hot Encoding: Clase 0 -> [[1], [0]], Clase 1 -> [[0], [1]]"""
y = np.zeros((OUTPUT_SIZE, 1))
y[indice_clase] = 1.0
return y
def cargar_csv_embeddings(ruta):
"""
Lee un archivo de texto donde cada línea son 1024 floats separados por coma.
Ej: 0.029,0.022,...
"""
if not os.path.exists(ruta):
print(f"Error: No se encuentra el archivo '{ruta}'")
sys.exit(1)
dataset = []
print(f" -> Leyendo CSV: {ruta}...")
with open(ruta, 'r', encoding='utf-8') as f:
for i, linea in enumerate(f):
linea = linea.strip()
if not linea: continue # Saltar líneas vacías
try:
# Convertir "0.1, 0.2" -> [0.1, 0.2]
vector = [float(x) for x in linea.split(',')]
# Validación rápida de dimensiones
if len(vector) == INPUT_SIZE:
dataset.append(vector)
else:
# Opcional: Avisar si una línea tiene tamaño incorrecto
if i < 5: print(f" ⚠️ Línea {i+1} ignorada: tiene {len(vector)} dimensiones (se esperan {INPUT_SIZE})")
except ValueError:
print(f" ⚠️ Error de formato en línea {i+1}")
continue
return dataset
def create_prize_datasets(ruta_farmacia, ruta_lab):
print(f"--- Cargando Datos (Formato CSV) ---")
# 1. Cargar vectores crudos
raw_farmacia = cargar_csv_embeddings(ruta_farmacia)
raw_lab = cargar_csv_embeddings(ruta_lab)
print(f"✅ Leídos: {len(raw_farmacia)} Farmacia | {len(raw_lab)} Laboratorio")
dataset_completo = []
# 2. Etiquetar FARMACIA (Clase 0)
label_farmacia = vectorizar_salida(0)
for vector in raw_farmacia:
x = vectorizar_entrada(vector)
dataset_completo.append((x, label_farmacia, 0))
# 3. Etiquetar LABORATORIO (Clase 1)
label_lab = vectorizar_salida(1)
for vector in raw_lab:
x = vectorizar_entrada(vector)
dataset_completo.append((x, label_lab, 1))
# 4. Barajar y Dividir
random.shuffle(dataset_completo)
split_index = int(len(dataset_completo) * 0.8)
train_slice = dataset_completo[:split_index]
test_slice = dataset_completo[split_index:]
# Formatear para Nielsen
training_data = [(x, y) for x, y, _ in train_slice]
test_data = [(x, idx) for x, _, idx in test_slice]
print(f"📊 Dataset Final: {len(training_data)} Train / {len(test_data)} Test")
return training_data, test_data
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--farmacia", required=True)
parser.add_argument("-l", "--laboratorio", required=True)
args = parser.parse_args()
create_prize_datasets(args.farmacia, args.laboratorio)
main.py
import argparse
import network2
import prize_loader
import sys
def main():
parser = argparse.ArgumentParser(description="Entrenar Red Neuronal Prize (Network2)")
parser.add_argument("-f", "--farmacia", required=True, help="JSON Farmacia")
parser.add_argument("-l", "--laboratorio", required=True, help="JSON Laboratorio")
parser.add_argument("-e", "--epochs", type=int, default=30, help="Épocas")
parser.add_argument("-r", "--rate", type=float, default=0.5, help="Learning Rate")
parser.add_argument("-x", "--lmbda", type=float, default=5.0, help="Regularización Lambda")
parser.add_argument("-b", "--batch", type=int, default=10, help="Mini-batch size")
# Nuevo argumento para el nombre del archivo de salida
parser.add_argument("-o", "--output", default="prize_model.json", help="Archivo donde guardar el cerebro (JSON)")
args = parser.parse_args()
# Cargar datos
training_data, test_data = prize_loader.create_prize_datasets(
args.farmacia,
args.laboratorio
)
print(f"\n🧠 Inicializando Prize Network2 [1024 -> 50 -> 2]")
net = network2.Network([1024, 50, 2], cost=network2.CrossEntropyCost)
print(f"🚀 Iniciando entrenamiento. Pulsa Ctrl+C para detener y guardar anticipadamente.")
# --- BLOQUE DE SEGURIDAD ---
try:
net.SGD(training_data,
args.epochs,
args.batch,
args.rate,
lmbda=args.lmbda,
evaluation_data=test_data,
monitor_evaluation_accuracy=True,
monitor_evaluation_cost=True)
print("\n✅ Entrenamiento finalizado correctamente.")
except KeyboardInterrupt:
print("\n\n⚠️ Interrupción detectada (Ctrl+C).")
print("🛑 Deteniendo entrenamiento...")
finally:
# Esto se ejecuta SIEMPRE: si termina bien O si das Ctrl+C
print(f"💾 Guardando el cerebro de Prize en '{args.output}'...")
net.save(args.output)
print("¡Guardado! Tu red está segura.")
if __name__ == "__main__":
main()
Me preparé para esperar 30 épocas para ver resultados decentes. Pero Prize tenía otros planes. Miren el log de la terminal apenas segundos después de iniciar:
¿Lo ven? En la Época 0, la red ya tenía una precisión del 99.47%. Para la Época 4, alcanzó el 99.9%.
Suerte de principiante supongo.
¿Por qué tan rápido?
Según Gemini, la clave fue usar Embeddings como entrada, ya que de esta manera la red no tuvo que aprender a leer español desde cero. Ya "sabía" que la palabra "amoxicilina" y "prótesis" viven en planetas diferentes dentro del universo semántico. Prize solo tuvo que aprender a trazar la frontera entre esos dos mundos.
La Prueba de Fuego: Prize en la Terminal
Con los pesos guardados en prize_model.json, escribimos un último script: prize_inference.py. Quería ver a la red funcionando en la terminal.
import sys
import os
import argparse
import json
import numpy as np
import requests
import network2 # El archivo del libro que contiene la clase Network
# --- CONFIGURACIÓN CRÍTICA ---
# 1. API KEY de OpenRouter
API_KEY = "XXXXXXX"
# 2. El nombre EXACTO del modelo de embeddings
MODEL_ID = "qwen/qwen3-embedding-8b" # <--- AJUSTA ESTO AL MODELO DE EMBEDDINGS CORRECTO
# URL de OpenRouter para embeddings
API_URL = "https://openrouter.ai/api/v1/embeddings"
# Definición de Clases
CLASES = {
0: "💊 FARMACIA",
1: "🦷 LABORATORIO DENTAL"
}
def get_embedding_from_api(text):
"""
Envía el texto a la API y devuelve el vector de 1024 floats.
"""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"HTTP-Referer": "https://prize.local",
}
payload = {
"model": MODEL_ID,
"input": text,
"dimensions": 1024
}
try:
response = requests.post(API_URL, headers=headers, json=payload, timeout=10)
if response.status_code != 200:
print(f"❌ Error API ({response.status_code}): {response.text}")
sys.exit(1)
result = response.json()
# OpenRouter/OpenAI devuelven data en: data[0]['embedding']
if 'data' in result and len(result['data']) > 0:
vector = result['data'][0]['embedding']
return vector
else:
print(f"❌ Respuesta API inesperada: {result}")
sys.exit(1)
except Exception as e:
print(f"❌ Error de conexión: {e}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Prize: Clasificador IA Todo-en-Uno")
parser.add_argument("query", type=str, help="El texto que quieres clasificar (entre comillas)")
parser.add_argument("-m", "--model", default="prize_model.json", help="Ruta al archivo del cerebro entrenado")
args = parser.parse_args()
# 1. Validaciones Iniciales
if not os.path.exists(args.model):
print(f"❌ Error: No encuentro el archivo '{args.model}'. ¿Ya entrenaste a Prize?")
sys.exit(1)
if API_KEY.startswith("sk-or-v1-..."):
print("⚠️ ADVERTENCIA: No has configurado tu API_KEY en el script.")
print(f"\n🤖 Prize System v1.0")
print(f"Query: '{args.query}'")
# 2. Generar Embedding (Reemplaza al script de Go)
print("📡 Conectando con OpenRouter para vectorizar...")
vector_lista = get_embedding_from_api(args.query)
# Verificar dimensiones
dim = len(vector_lista)
if dim != 1024:
print(f"❌ ERROR CRÍTICO: El modelo devolvió {dim} dimensiones, pero Prize espera 1024.")
print("Solución: Verifica que MODEL_ID en el script sea el mismo que usaste para entrenar.")
sys.exit(1)
# Convertir a formato Numpy para Nielsen (Columna 1024x1)
vector_numpy = np.reshape(vector_lista, (1024, 1))
# 3. Cargar el Cerebro
print("🧠 Cargando red neuronal...")
net = network2.load(args.model)
# 4. Inferencia
print("🔮 Clasificando...")
output = net.feedforward(vector_numpy)
# Extraer probabilidades
prob_farmacia = output[0][0]
prob_lab = output[1][0]
# 5. Visualización de Resultados
print("\n" + "="*40)
print(f" RESULTADOS PARA: '{args.query}'")
print("="*40)
# Barra Farmacia
len_bar = 30
fill_f = int(len_bar * prob_farmacia)
bar_f = "█" * fill_f + "░" * (len_bar - fill_f)
print(f"{CLASES[0]}: {bar_f} {prob_farmacia*100:.1f}%")
# Barra Laboratorio
fill_l = int(len_bar * prob_lab)
bar_l = "█" * fill_l + "░" * (len_bar - fill_l)
print(f"{CLASES[1]}: {bar_l} {prob_lab*100:.1f}%")
print("-" * 40)
# Decisión Final
ganador = np.argmax(output)
print(f"🏆 VEREDICTO: {CLASES[ganador]}")
print("="*40 + "\n")
if __name__ == "__main__":
main()
Aquí está el resultado de una prueba real:

Conclusión: Misión Cumplida
Lo que empezó hace una semana como una frustración por la latencia de 5 segundos de una API externa, hoy es la base para una solución propia.
Este fin de semana me enseñó que no siempre necesitamos la IA más grande y costosa (GPT-5) para resolver nuestros problemas. A veces, un poco de Numpy, unos buenos datos y unas sesiones de vibe coding son todo lo que necesitas.
Durante las siguientes semanas seguiré aumentando la complejidad de Prize para buscar reemplazar la API externa que uso actualmente en mi aplicación.

Top comments (0)