ztzoff.tech

30 jul 2026

Escribí la guía de redes neuronales que me hubiera gustado encontrar (y está en español)

2 152 líneas en español que explican por qué existe cada pieza de una red neuronal, con dos ejemplos resueltos a mano y un repositorio que verifica su propio backward pass.

Cuando aprendí cómo funciona por dentro una red neuronal, el material que encontré se dividía en dos montones. Uno explicaba la intuición sin llegar nunca a las ecuaciones. El otro daba las ecuaciones dándolas por evidentes. Casi nada explicaba por qué existe cada pieza: por qué hay una función de activación, por qué la regla de la cadena aparece donde aparece, por qué el orden en que se guardan los pesos en memoria cambia el tiempo de ejecución.

Así que escribí esa guía mientras construía la implementación. Son 2 152 líneas en español, viven junto al código que describen, y cada afirmación de rendimiento enlaza a la medición que la respalda.

Cómo está organizada la guía

Tres partes, y el orden importa.

Parte I — los conceptos, sin código. Desde qué problema resuelve realmente una red neuronal hasta la regla de la cadena, pasando por qué es una neurona, qué añade una capa oculta y por qué aprender es descenso de gradiente. La regla de la cadena aparece cuando hace falta y aplicada al caso concreto que se está construyendo, no como preámbulo matemático.

Parte II — la implementación en C#. Cada idea de la Parte I trasladada al código real: la disposición de los datos —«la decisión más trascendente»—, las activaciones, el forward pass, SIMD, el backward pass, la inicialización de los pesos, el bucle de entrenamiento y el gradient check.

Parte III — práctica. Resultados, un manual de depuración, dieciséis ejercicios, lo que la implementación no hace y hacia dónde seguir.

Y hay una recomendación explícita al principio para quien tenga prisa: si solo vas a leer tres secciones, lee la §7, la §8 y la §21.

backpropagation explicado a mano, con una calculadora

Las §7 y §8 son ejemplos resueltos completos: una neurona a mano, y después la cadena entera a través de dos capas. Números pequeños, todos los pasos escritos, calculados y verificados.

Ese formato importa más de lo que parece. La diferencia entre entender la propagación hacia atrás (backpropagation) y creer que se entiende suele estar en si uno es capaz de calcular a mano un gradiente de dos capas. Si los números salen, la abstracción se sostiene después. Si no, uno arrastra durante meses una intuición aproximada que se rompe en cuanto algo no funciona.

La §21 — gradient check, la sección que casi nadie escribe

Un backward pass sutilmente incorrecto normalmente sigue entrenando. La pérdida baja, nada revienta, y los resultados son mediocres de una forma indistinguible de «hay que ajustar los hiperparámetros». Esos errores cuestan días.

La defensa es comparar el gradiente analítico contra una estimación numérica que no use tu código: mover un peso en ±ε, medir cómo se mueve la pérdida de verdad, y comparar. Lento —dos forward passes completos por parámetro— pero definitivo.

Lo que hace útil a esa sección no es la fórmula, es la tabla:

εerror relativo máximodominado por
1e-19,1e-3truncamiento — ε demasiado grueso
1e-22,4e-4equilibrado ← el mejor
1e-31,7e-3el redondeo empieza a colarse
1e-41,5e-2redondeo de float32

La forma de U es la evidencia. Dos fuentes de error luchan entre sí: un ε grande aproxima mal una derivada, y un ε pequeño resta dos float casi iguales y pierde precisión. Un gradiente correcto muestra ese compromiso con un óptimo en el medio. Un gradiente incorrecto muestra error O(1) con cualquier ε, porque no está aproximando nada. No hace falta creerse un umbral: se mira la forma.

Y viene con su excepción documentada, que es la parte que más me habría ahorrado en su momento: ReLU puede hacer fallar la comprobación siendo el código correcto. Las diferencias finitas suponen que la pérdida es suave entre w−ε y w+ε, y ReLU tiene un codo en z = 0. Si el paso cruza esa esquina, la estimación numérica mide el promedio de dos pendientes distintas. Medido sobre exactamente ese montaje —una unidad con z a 0,001 del codo— el error es de 2,9e-1 con ε = 1e-2 y baja a 8,8e-2 con ε = 1e-4. Ahí el gradiente analítico es correcto y lo que está mal es la estimación.

De eso salen dos diagnósticos para distinguir un codo de un error real: reduce ε —un artefacto mejora bruscamente, un error no se mueve— y reconstruye la misma red con tanh, que no tiene esquina. Ambos comportamientos están fijados por pruebas.

Tres cosas que distinguen a esta red neuronal desde cero en C#

1. Verifica su propio backward pass, y demuestra que la verificación funciona. La batería incluye a propósito una derivada incorrecta —una variante de Tanh que usa 1 + a² en lugar de 1 - a²— que debe producir error O(1). Sin eso, las pruebas que pasan podrían estar pasando vacuamente: una comprobación que no puede fallar no demuestra nada.

2. Mide sus propias afirmaciones y publica la que resultó falsa. El repositorio afirmaba que una activación invocada mediante un delegado sería claramente más lenta que una resuelta por genéricos. La medición dice que no: queda dentro de ±4 % en todos los tamaños realistas, y el signo ni siquiera es consistente. La documentación lo corrige en lugar de borrar la frase en silencio.

Esa fue la afirmación más barata de corregir. He escrito antes sobre las dos que costaron más: una optimización que dejó una capa 6,3× más lenta y de la que solo el desensamblado dio cuenta, y un benchmark que resultó ser ruido mientras yo ya había escrito una explicación convincente de ese ruido. También está el caso en que la biblioteca estándar ganó una operación y perdió la otra.

3. Es honesta sobre sus límites. La §25 enumera ocho cosas que la implementación no hace, ordenadas por cuánto cuestan. La primera es la más incómoda: ForwardBatch mide 0,98× frente a un bucle manual —un resultado nulo deliberado, publicado como tal— y un GEMM real por bloques probablemente dominaría cualquier otra optimización del código actual. Es decir, el eje entero por el que he estado optimizando es el secundario, y eso se dice antes que nada.

Los ejercicios de «rómpelo»

Los dieciséis ejercicios están ordenados por valor, y los que más enseñan son los que rompen algo a propósito. Poner los pesos a cero y ver a XOR congelarse en una pérdida de exactamente 0,250000, prediciendo 0,5000 para siempre. Cambiar la derivada de Tanh y ver saltar la comprobación a O(1) mientras el entrenamiento sigue funcionando en parte. Volver a activar la cache del forward pass en el momento equivocado y comprobar que la pérdida sigue bajando mientras la red entrena con el ejemplo equivocado.

Ese último es, creo, el argumento central de todo el proyecto: «sigue entrenando» no demuestra nada.

El resultado: MNIST al 98,02 % en 37 segundos

MNIST — precisión en test98,02 %
Tiempo de entrenamiento37 s
Parámetros101 770
Tamaño del modelo guardado397 KB
Tiempo de recarga del modelo4 ms

Entorno: Apple M3 Pro (11 núcleos), macOS 26.3, .NET 10.0.10, Arm64 RyuJIT armv8.0-a.

Treinta y siete segundos importa por una razón concreta: se puede cambiar algo, reentrenar y ver el efecto dentro del mismo minuto. Ese ciclo corto es lo que convierte una guía en algo que se puede usar mientras se lee. Y la referencia está publicada precisamente para que se pueda batir: el ejercicio 10 pide implementar momentum y medirlo contra ese 98,02 % en 37 s.

Por qué en español

La documentación técnica de esta profundidad sobre .NET y rendimiento es escasa en español. No inexistente, pero escasa, y casi siempre a medias: el texto traducido y todo lo interesante —los desensamblados, las tablas de benchmarks— apuntando a fuentes en inglés.

El proyecto está completamente duplicado: README.md / README.es.md, STUDY-GUIDE.md / STUDY-GUIDE.es.md, bench/README.md / bench/README.es.md. La guía tiene 2 052 líneas en inglés y 2 152 en español; no es un resumen. Lo que sí se conserva en inglés a propósito es el código, los identificadores, las banderas de línea de comandos y la salida de los programas: son los artefactos reales del repositorio, y traducirlos describiría un programa que no existe.

La advertencia

Este proyecto es didáctico. Es de un solo hilo, no tiene procesamiento por lotes real, no tiene GEMM por bloques, usa SGD simple sin momentum ni Adam, y no pretende competir con ninguna herramienta especializada. Está optimizado solo hasta donde optimizar enseñaba algo.

Lo que sí ofrece es una propiedad que no siempre acompaña al material didáctico: todo lo que afirma está medido, y lo que resultó falso sigue publicado. Esa es la parte que me hubiera gustado encontrar.

El código

Todo lo anterior es reproducible: neural-network-csharp. La guía de estudio está en STUDY-GUIDE.es.md, y los benchmarks en bench/, con los resultados comentados en bench/README.es.md.

Agenda una llamada