Ir al contenido
learn

Diario

  • IX clonado y fijado en el commit 490c395; el código del curso depende de siete de sus crates
  • Datos: builds.csv y jobs.csv, extraídos del historial de CI y del historial de Git de este repositorio
  • CI: formato, clippy, pruebas unitarias y cada ejemplo comparado con expected/ en tres sistemas operativos, y una comprobación cruzada con numpy y scikit-learn en Linux
  • Lección 1: datos, características y evaluación
  • Lección 2: regresión lineal y descenso de gradiente
  • Lección 3: clasificación
  • Lección 4: agrupamiento
  • Lección 5: componentes principales
  • Lección 6: conjuntos
  • Lección 7: redes neuronales
  • Lección 8: optimización
  • Lección 9: cinco reductores más, con MDS escrita a mano y comprobaciones independientes
  • Apéndice: el mapa de API, generado desde el commit fijado
  • Lecciones 10 a 21
  • Traducciones al francés y al español

IX es la biblioteca de otros, fijada en 490c395. Las lecciones anteriores registraron diecinueve hallazgos; la lección 9 añade dos hallazgos de API o documentación. La primera columna indica lo que esperaría quien llama. Ninguno se ha reportado como incidencia de IX. Varias son decisiones defendibles más que defectos, y la columna del medio dice cuáles.

Esperado Lo que pasa Dónde Medición Estado
Las filas de prueba no participan en dar forma al preprocesado Con normalize, la tubería ajusta StandardScaler y el PCA sobre todas las filas y divide después ml_pipeline.rs 190-203 La primera fila de prueba vale 1,440 frente a 1,042 si la división va primero Reproducido, no reportado 2026-09-14
Un vector de etiquetas enteras es un objetivo de regresión salvo indicación contraria infer_task_type llama clasificación a todo vector de enteros no negativos con a lo sumo 20 valores distintos preprocessing.rs 234-254 Veinte valores distintos es toda la regla Reproducido, no reportado 2026-09-14
Un empate entre vecinos más cercanos se rompe como en scikit-learn max_by_key devuelve el último máximo: el empate va al índice de clase mayor knn.rs 53 Exactitud 0,947 frente a 0,974 Reproducido, no reportado 2026-09-14
Un clúster k-means vacío se resiembra, como hace scikit-learn La actualización de centroides parte de ceros y salta los clústeres sin filas: el centroide se queda en el origen kmeans.rs 137-152 KMeans(3) sobre [5, 5, 9, 9] da [5.0, 9.0, 0.0] Reproducido, no reportado 2026-09-14
La silueta de una fila sola en su clúster vale 0, como la definen Rousseeuw y scikit-learn silhouette_score_exact pone a = 0, de donde s = 1 ix-voicings/lib.rs 664-678 0,9296 frente a 0,5963 Reproducido, no reportado 2026-09-14
Las líneas de import de los tutoriales compilan use ix_unsupervised::{KMeans, Clusterer}; falla con E0432: la crate solo exporta módulos ix-unsupervised/lib.rs 5-14 E0432 Reproducido, no reportado 2026-09-14
KMeans::fit hace varios arranques y se queda con el mejor Hace un único arranque k-means++; la herramienta MCP ix_kmeans además fija la semilla en 42 kmeans.rs, handlers.rs 322 Un solo arranque Reproducido, no reportado 2026-09-14
ix_voicings::cluster compara las k que prueba Se queda con k = 5 en cuanto su silueta llega a 0,15, y solo prueba k = 3 por debajo ix-voicings/lib.rs 770-776 Un umbral, no una comparación Reproducido, no reportado 2026-09-14
Una mezcla gaussiana se protege del colapso de una componente GMM pone un suelo de 1e-6 a cada varianza, lo que deja que una componente colapse sobre un punto gmm.rs 170-171 1e-6 Reproducido, no reportado 2026-09-14
explained_variance_ratio es una parte de la varianza total Cada autovalor se divide por la suma de los autovalores conservados, así que las proporciones siempre suman 1 pca.rs 46-57 [0,5635, 0,4365] frente a las verdaderas [0,3867, 0,2996], que suman 0,6863 Reproducido, no reportado 2026-09-15
La iteración de la potencia encuentra el eje principal Parte del vector fijo (1, …, 1)/√n, y puede devolver el eje secundario pca.rs 107 Un arranque que no tiene nada de aleatorio Reproducido, no reportado 2026-09-15
Los autovalores se pueden volver a leer PCA::explained_variance es privado y la proporción está normalizada como arriba: save_state() es la única salida pca.rs Un solo acceso, y es un serializador Reproducido, no reportado 2026-09-15
Un bosque aleatorio sortea sus variables en cada corte, como promete su propio comentario Las sortea una vez por árbol, antes de construirlo random_forest.rs 68-84 Con max_features = 1: versión a mano 0,9474, IX 0,7632 Reproducido, el comentario y el código se contradicen, no reportado 2026-09-15
Un empate en el voto del bosque se rompe como en scikit-learn max_by conserva el último máximo: va al índice de clase mayor random_forest.rs 97 La misma forma que el hallazgo 3 Reproducido, no reportado 2026-09-15
El gradiente analítico es igual a uno por diferencias finitas Dense::backward divide una segunda vez por el tamaño del lote: grad_output ya lleva el 1/n que puso mse_gradient ix-nn/layer.rs 40-52 Razón de 65,0000 exactamente frente al oráculo por diferencias finitas Reproducido, no reportado 2026-09-15
mse_gradient es el gradiente de mse_loss Lo es para una columna de salida. mse_loss promedia sobre filas y columnas; mse_gradient solo divide por las filas ix-nn/loss.rs 6-16 Razones 1, 2, 3, 4 para 1 a 4 columnas Reproducido, no reportado 2026-09-15
Dense::new admite una semilla Sortea del generador del hilo: una red no es reproducible ix-nn/layer.rs 27 Ningún parámetro de semilla Reproducido, no reportado 2026-09-15
Un Sequential de dos capas Dense puede aprender el XOR Dense es el único tipo que implementa Layer: no hay activación que meter entre dos aplicaciones afines, y toda la red es una sola aplicación afín ix-nn La pérdida se estanca en 0,25 tras 50 000 épocas y predice 0,5 en las cuatro esquinas Reproducido, funcionalidad ausente más que defecto, no reportado 2026-09-15
minimize distingue una ejecución divergida de una que agotó iteraciones Devuelve el punto de partida de quien llama, etiquetado como el mejor visto, con el mismo converged: false que una no convergencia corriente ix-optimize/gradient.rs 124-165 Rosenbrock con paso 0,01 informa best f 24.200000, el valor de partida Reproducido, no reportado 2026-09-15
TSNE::transform(new_rows) coloca esas filas en el mapa ajustado El argumento se ignora: devuelve la proyección 12 × 2 guardada incluso con una entrada 1 × 1 tsne.rs 249-253 La salida es exactamente la proyección original Reproducido, no reportado 2026-09-24
El encabezado Barnes-Hut del módulo t-SNE describe la implementación compute_q y el gradiente recorren todos los pares de puntos tsne.rs 160-178, 205-217 Bucles cuadráticos visibles en el código; rendimiento no medido Confirmado en código, no reportado 2026-09-24

Un curso que encuentra divergencias debe mostrar qué comprobó y halló correcto. Las seis comprobaciones iniciales siguen abajo; la quinta falló y produjo los hallazgos 15 y 16. La lección 9 añade dos comprobaciones geométricas exploratorias, no prerregistradas.

Pregunta Hipótesis Resultado Veredicto Dónde
¿Siguen los SGD, Momentum y Adam de IX paso a paso a implementaciones escritas a mano? Una implementación correcta debe coincidir exactamente, no aproximadamente Los mismos recuentos de pasos — 5000, 4129, 2822 —, los mismos puntos finales y el mismo primer paso Confirmada 2026-09-15
¿Coincide la descomposición propia del PCA de IX con Jacobi y con la SVD de scikit-learn? Sobreentendida por las cinco varianzas que imprime Jacobi hasta 1,28e-10, scikit-learn hasta los dígitos impresos Confirmada — solo la proporción y el arranque fijo están mal 2026-09-15
¿Coincide GradientBoostedClassifier con una versión a mano y una comprobación con numpy? Sobreentendida Coincide en cada fila de prueba y con cualquier número de rondas, con los mismos log-priors suavizados y los mismos tocones en la primera ronda Confirmada 2026-09-15
¿Puede un xorshift de 64 bits repetible sustituir al StdRng no repetible de IX en una comprobación por bootstrap? Escrita de antemano: un generador repetible usado igual en Rust y en Python cae sobre las mismas filas fuera de bolsa La comprobación con numpy cae sobre las mismas 48 filas dejadas fuera Confirmada 2026-09-15
¿Es el gradiente analítico de ix_nn igual a su gradiente por diferencias finitas? Escrita de antemano: la razón debe valer 1 65,0000, y luego 1, 2, 3, 4 según el número de columnas — hallazgos 15 y 16 Refutada 2026-09-15
¿Cambia la suma en coma flotante en macOS-ARM alguna afirmación de las lecciones? Escrita de antemano: solo debe diferir el signo de un valor que ya era cero -0.0000 frente a 0.0000 en dos esquinas, todos los demás valores idénticos a cuatro decimales (ejecución 35039180659) Confirmada 2026-09-16
¿Conserva la MDS clásica escrita a mano las mismas distancias del cuadrado que IX? Ambas deberían recuperar las seis distancias, independientemente de la orientación de los ejes Error máximo menor de 1e-10 en ambas; numpy coincide Confirmada, exploratoria 2026-09-24, l09_reducers.rs
¿Separa el primer eje de PCA con núcleo RBF dos anillos concéntricos? La similitud no lineal debería poner primero el contraste radial IX y scikit-learn lo sitúan en el eje 4; separación de 0.5798 en IX, cero en el eje 1 Refutada, exploratoria 2026-09-24, l09_reducers.rs
  • IX está clonado aparte de mi copia de trabajo, con git clone --filter=blob:none, y en el checkout de 490c39533627d296bf9f8f050e6fafc14d7a20c2, un commit de main del 2026-09-14, 21:40 UTC. El workspace tiene 83 carpetas bajo crates/ (su README dice 81 crates); el curso lee ix-math, ix-supervised, ix-optimize, ix-unsupervised, ix-voicings, ix-io e ix-agent, y depende de los cinco primeros.
  • Cargo.toml nombra cada crate con git y rev, y Cargo.lock está en el commit. Cargo descarga una sola vez el repositorio completo de IX para los cinco crates, incluido su submódulo governance/demerzel.
  • El código del curso comparte ndarray 0.17 con IX: las matrices pasan de las versiones a mano a las funciones de IX sin conversión.
  • Las herramientas MCP de IX de mi sesión de Claude Code me ayudaron a encontrar los algoritmos; cada resultado de las lecciones viene de los ejemplos compilados, no de las herramientas.
  • data/extract.py lee code/duckdb/data/runs.json y jobs.json, la exportación que hizo el curso de DuckDB el 2026-09-14, y ejecuta git ls-tree en cada commit compilado para contar sus archivos .md y .mdx.
  • builds.csv: los jobs build exitosos de Deploy to GitHub Pages, 65 filas, con los segundos del paso cuyo nombre empieza por Install, build.
  • jobs.csv: los jobs que terminaron con éxito o con fallo y tienen a la vez un paso de checkout y su paso posterior, 186 filas. El sistema operativo es la etiqueta del runner sin -latest. queue_s es el tiempo desde la creación del job hasta su inicio.
  • Cada tiempo es un número entero de segundos (la diferencia de dos marcas de tiempo, truncada). La lección 1 muestra lo que eso le hace a la inferencia de tarea de IX, la lección 4 lo que le hace a una mezcla gaussiana.
  • No se descargó ningún conjunto de datos: ambos archivos se derivan de este repositorio público.
  • ml-ix-examples.yml ejecuta check.sh en ubuntu-latest, windows-latest y macos-latest: cargo fmt --check, cargo clippy --release --all-targets -- -D warnings, cargo test --release (pruebas unitarias y doctests, incluida la importación compile_fail de la lección 4), y luego cada ejemplo, cuya salida y código de salida se comparan con expected/ mediante diff --strip-trailing-cr. UPDATE=1 reescribe expected/, CROSSCHECK=1 añade la comprobación en Python en local.
  • La comprobación cruzada solo se ejecuta en Linux, con Python 3.13, numpy 2.4.2 y scikit-learn 1.8.0: recalcula las métricas, la recta de regresión, el árbol de decisión, las exactitudes de los k vecinos más cercanos, las siluetas, DBSCAN y una mezcla gaussiana, y compara su salida con expected/crosscheck.txt. No puede reproducir el generador de números aleatorios de IX, así que no reproduce los inicios de k-means ni las divisiones aleatorias de IX.
  • Los valores de punto flotante se imprimen con un número fijo de decimales (fmt_vec en src/lib.rs), así que las salidas son idénticas en los tres sistemas; ninguna necesitó un archivo por sistema operativo.
  • Las ejecuciones 34903197003 (los ejemplos de las lecciones, commit d9ef7fb) y 34903462618 (las soluciones de los ejercicios, commit 15cde43) pasaron en los cuatro jobs al primer push.

2026-09-14 — Decisiones al escribir el código

Sección titulada «2026-09-14 — Decisiones al escribir el código»
  • train_test_split, k-means++, el inicio del GMM y StratifiedKFold de IX sacan sus números de StdRng de Rust, que Python no puede reproducir. Para ellos, la versión a mano parte del resultado ajustado de IX y comprueba que un paso más no cambia nada (k-means, EM), y la comprobación cruzada compara lo que no depende del inicio: tamaños, inercias del mejor de n, log-verosimilitudes.
  • La lección 3 prueba con uno de cada cinco jobs, una división que ambos lenguajes pueden hacer.
  • La lección 3 ejecuta k = 4 junto a k = 5 por el desempate de IX: el empate que vi primero desapareció cuando el conjunto de prueba pasó a ser uno de cada cinco jobs, y k = 4 estandarizado vuelve a mostrar uno, en una fila de prueba real.
  • La primera comparación de la mezcla gaussiana no coincidía con scikit-learn. Rastrearla llevó a las varianzas colapsadas de complete_s, que se convirtieron en una sección de la lección 4 en lugar de una nota al pie.

Nueve puntos en los que la respuesta de IX, o su documentación, difiere del libro de texto o de scikit-learn, cada uno mostrado por código compilado en el curso salvo que se indique lo contrario. Ninguno está registrado como issue de IX.

  1. Escalar antes de dividir. Con normalize activado, el pipeline de ML ajusta StandardScaler (y PCA) sobre todas las filas y después divide: las filas de prueba influyen en el escalado (ml_pipeline.rs, líneas 190-203, luego 537-538 y 704-705). Leído en el código; el mecanismo se muestra en la lección 1 (primera fila de prueba 1.440 frente a 1.042), sin ejecutar la herramienta en sí.
  2. Las etiquetas enteras se vuelven clases. infer_task_type considera clasificación cualquier vector de etiquetas de enteros no negativos con como mucho 20 valores distintos (preprocessing.rs, líneas 234-254): los segundos de build dan MulticlassClassification { n_classes: 18 }, y sumar 0.5 a una fila da Regression. El pipeline lo usa cuando la tarea es auto.
  3. Empates en los k vecinos más cercanos. KNN::predict usa max_by_key, que devuelve el último máximo: un empate va al índice de clase más alto (knn.rs, línea 53). En los jobs estandarizados con k = 4, la fila de prueba 27 vota [2, 0, 2]: IX dice macOS, la versión a mano y scikit-learn Ubuntu, el sistema operativo real; exactitud 0.947 frente a 0.974.
  4. Un cluster vacío de k-means se va al origen. La actualización de centroides parte de ceros y omite los clusters sin filas (kmeans.rs, líneas 137-152). KMeans(3) sobre [5, 5, 9, 9] da los centroides [5.0, 9.0, 0.0], y predict([1.0]) devuelve el cluster vacío; scikit-learn da [5.0, 9.0, 9.0] con una advertencia.
  5. La silueta de un singleton es 1. silhouette_score_exact en ix-voicings pone a = 0 para una fila sola en su cluster, así que s = 1, donde Rousseeuw y scikit-learn usan 0 (lib.rs, líneas 664-678): 0.9296 frente a 0.5963 en [0, 1 | 10].
  6. Las importaciones de los tutoriales no compilan. use ix_unsupervised::{KMeans, Clusterer}; falla con E0432, porque el crate solo exporta módulos (lib.rs, líneas 5-14). La misma forma aparece en docs/unsupervised-learning/kmeans.md (líneas 60 y 91), dbscan.md (línea 50), pca.md (líneas 63 y 101), docs/use-cases/fraud-detection.md (línea 43), gis-spatial-analysis.md (líneas 79, 375, 427), docs/foundations/rust-for-ml.md (línea 141), y en sus versiones en francés. El doctest del curso comprueba la primera.
  7. Un solo inicio de k-means. KMeans::fit ejecuta un único inicio k-means++, y la herramienta MCP ix_kmeans fija la semilla en 42 (handlers.rs, línea 322). En los jobs, las semillas 0 a 9 dan inercias de 411.23 a 618.02; para k = 6, la semilla 42 da 223.366 donde el mejor de 50 de scikit-learn da 214.040, con una silueta de 0.3646 frente a 0.4400.
  8. La regla de voicings no compara. ix_voicings::cluster se queda con k = 5 en cuanto su silueta alcanza 0.15, y solo prueba k = 3 por debajo de eso (lib.rs, líneas 770-776). En los jobs, se queda con k = 5 (0.4350) en lugar de k = 3 (0.4997). Una decisión de diseño más que un error, pero el comentario de documentación llama al umbral uno para «aceptar el agrupamiento», no para elegir k.
  9. Colapso de la mezcla gaussiana. GMM pone un mínimo de 1e-6 a cada varianza (gmm.rs, líneas 170-171) y ejecuta un solo inicio aleatorio. Con los tiempos en segundos enteros, las componentes colapsan sobre complete_s, y la log-verosimilitud depende de la semilla, de −102.666 a 552.879; el mejor de 10 inicios de scikit-learn es −102.667. Comparar las log-verosimilitudes de IX elige el modelo más colapsado.

Diferencias que vale la pena conocer, que no cuento como errores:

  • LogisticRegression no tiene regularización ni criterio de parada: con datos separables, sus pesos crecen con el número de iteraciones (lección 3).
  • ix_optimize::gradient::SGD es descenso de gradiente por lote completo: minimize le pasa el gradiente sobre todas las filas.
  • DBSCAN etiqueta el ruido con 0 y los clusters desde 1, como dice su documentación; el código escrito para el -1 de scikit-learn fusiona el ruido con un cluster.
  • ix_io::csv_io::read_csv convierte los campos de texto en NaN sin error.
  • train_test_split siempre baraja, y toma las etiquetas de clasificación como f64; no hay opción estratificada ni por orden temporal.
  • Cuatro lecciones de una tanda: componentes principales, conjuntos, redes neuronales, optimización. El curso depende ahora de siete crates de IX, tras añadir ix-ensemble e ix-nn en el mismo commit fijado, y ejecuta dieciséis ejemplos en el CI.
  • La comprobación cruzada creció con ellas. La lección 5 es la primera en la que scikit-learn puede comprobar IX directa y completamente — PCA es determinista, no toma semilla y tiene una convención de signo documentada — así que cada número de esa lección tiene tres fuentes independientes.
  • La lección 6 introdujo un generador xorshift de 64 bits en el código del curso. IX extrae de StdRng, que Python no puede reproducir; tres desplazamientos y tres XOR sí se reproducen exactamente, y la comprobación con numpy cae en las mismas 48 filas que dejó fuera el mismo bootstrap. Eso cierra la brecha de la que se quejaba la nota del 2026-09-14, para los algoritmos que el curso escribe él mismo.
  • La lección 7 es la primera que usa las diferencias finitas como oráculo en lugar de scikit-learn. Cada afirmación sobre lo que calcula ix_nn se midió moviendo un peso y observando la pérdida, no se dedujo leyendo — lo cual resultó importar, porque dos de los tres hallazgos se desvían por un factor que nadie adivinaría leyendo el código.
  • La lección 8 no encontró nada malo en las reglas de actualización de ix_optimize: SGD, Momentum y Adam igualan a las versiones a mano paso a paso y aterrizan en los mismos puntos. El hallazgo allí es sobre el tipo de resultado de minimize, no sobre su aritmética.

2026-09-15 — Donde IX difiere, continuación

Sección titulada «2026-09-15 — Donde IX difiere, continuación»

Diez puntos más en los que la respuesta de IX, o su documentación, difiere del libro de texto o de scikit-learn, cada uno mostrado por código compilado en el curso. Con los nueve del 2026-09-14 son diecinueve. Ninguno está registrado como issue de IX.

  1. La razón de varianza explicada está normalizada sobre las componentes retenidas. explained_variance_ratio divide cada valor propio por la suma de los valores propios retenidos (pca.rs, líneas 46-57), así que las razones suman 1 para cualquier n_components y el «proportion of total variance» del comentario de documentación solo se cumple si se guardan todas las componentes. Sobre los jobs estandarizados, PCA::new(2) informa de [0.5635, 0.4365] donde la verdad, y scikit-learn, dicen [0.3867, 0.2996], con suma 0.6863. La pregunta habitual «¿cuántas componentes para el 90 %?» no se le puede hacer.
  2. La iteración de potencia parte de un vector fijo y puede devolver el eje menor. El inicio es (1, …, 1)/√n (pca.rs, línea 107). En una nube de seis puntos estirada a lo largo de (1, -1), cuya covarianza es [[2.4, -1.6], [-1.6, 2.4]], ese inicio es el vector propio menor, así que PCA::new(1) devuelve varianza 0.8 a lo largo de (1, 1) en lugar de 4.0 a lo largo de (1, -1). PCA::new(2) es peor: la deflación deja una matriz que envía el inicio a cero, salta la protección norm < 1e-15, y el modelo acaba con la misma componente dos veces y una varianza de 0. La SVD de scikit-learn devuelve 4.0 y (0.7071, -0.7071). Un caso al filo de la navaja — necesita varianzas exactamente iguales — pero nada en la salida dice en qué caso estás.
  3. Las varianzas son inalcanzables. PCA::explained_variance es un campo privado, y explained_variance_ratio() está normalizado como en el hallazgo 10, así que save_state() es la única forma de sacar los valores propios de un modelo ajustado.
  4. Un bosque aleatorio sortea sus variables una vez por árbol, no una vez por división. El comentario de documentación promete «random feature subsets (sqrt(n_features) features per split)»; el código las sortea antes de hacer crecer el árbol y construye todo el árbol con esas columnas (random_forest.rs, líneas 68-84). Con el valor por defecto de tres variables de cinco ambos coinciden, llegando los dos a 0.9211 en los jobs de prueba; con max_features = 1 la versión a mano, que sortea en cada división, llega a 0.9474 e IX a 0.7632, porque cincuenta árboles unidimensionales no pueden combinar variables.
  5. Un empate en el voto del bosque va al mayor índice de clase. predict usa max_by, que se queda con el último máximo (random_forest.rs, línea 97). Dos tocones con la semilla 6 dan probabilidades [0.500, 0.500], e IX responde la clase 1 donde scikit-learn y np.argmax responden 0. La misma forma que el hallazgo 3 sobre KNN::predict.
  6. Dense::backward divide dos veces por el tamaño del lote. grad_output ya lleva el 1/n que puso ahí mse_gradient, y backward vuelve a dividir por n (layer.rs, líneas 40-52). Medido contra el gradiente por diferencias centradas de mse_loss sobre las 65 compilaciones, la razón es 65.0000 exactamente. El entrenamiento converge igualmente, ya que cada capa divide por el mismo n, pero la tasa de aprendizaje que pasas es la tasa de aprendizaje dividida por el tamaño del lote.
  7. mse_gradient no es el gradiente de mse_loss más allá de una columna de salida. mse_loss promedia sobre filas y columnas, mse_gradient divide solo por las filas (loss.rs, líneas 6-16). Medido de una a cuatro columnas, la razón es 1, 2, 3, 4. Combinado con el hallazgo 15, una capa de dos salidas sobre 65 filas se mueve por el gradiente verdadero dividido por 32.5.
  8. Dense::new no toma semilla. Extrae del generador del hilo (layer.rs, línea 27), así que dos capas construidas en la misma ejecución ya difieren y ningún resultado de Sequential puede reproducirse ni someterse a pruebas de regresión. Todos los demás algoritmos aleatorios de IX toman una — KMeans, RandomForest, Dropout, transformer::FeedForward. Los campos weights y bias son públicos, que es la salida y lo que hace el curso.
  9. Un Sequential de capas Dense es una sola aplicación afín. Dense es el único tipo que implementa el trait Layer: ix-nn no tiene capa de activación que Sequential::push pudiera aceptar entre dos aplicaciones afines. En el o exclusivo, dos capas Dense convergen a una pérdida de 0.25 — la varianza de los objetivos, lo mejor que puede hacer una constante — y predicen 0.5 en las cuatro esquinas, mientras que la red a mano del mismo tamaño con una sigmoide entre sus capas llega a 0. Tras 50 000 épocas la pila sigue satisfaciendo f(0,0) + f(1,1) - f(0,1) - f(1,0) = 0 hasta el último bit: no está poco entrenada, no puede representar la función. Las no linealidades reales del crate viven en transformer::gelu, dentro de FeedForward, que trabaja sobre Array3 y no es un Layer.
  10. minimize devuelve el punto de partida para una ejecución que divergió. Recuerda el mejor punto por el que ha pasado y devuelve ese (gradient.rs, líneas 124-165). El descenso simple a tasa 0.01 sobre Rosenbrock se va a NaN en sus primeros pasos; el resultado informa de best f 24.200000, que es f en el punto que pasó el llamante, con converged: false — la misma bandera que recibe una ejecución perfectamente buena que simplemente agotó sus iteraciones. Ni best_value ni converged separan ambos casos.

Diferencias que vale la pena conocer, que no cuento como errores:

  • RegressionStump pone la media de los residuos en cada hoja, donde la receta de Friedman y scikit-learn usan un paso de Newton. Es una simplificación defendible, y la versión a mano la copia para que ambas se puedan comparar; con ella, IX y la versión a mano coinciden en cada una de las 38 filas de prueba con 1, 5, 10, 25 y 50 rondas.
  • GradientBoostedClassifier no tiene parada temprana ni puntuación sobre un conjunto reservado. En los jobs alcanza un máximo de 0.9474 tras cinco rondas y se asienta en 0.9211 hacia las veinticinco, y nada en la API lo mide.
  • ix-ensemble no tiene puntuación out-of-bag, que el bagging regala y scikit-learn expone como oob_score=True.
  • ClosureObjective nunca redefine ObjectiveFunction::gradient, así que envolver una closure compra en silencio un gradiente medido a costa de dos evaluaciones extra por coordenada y por paso — 1001 evaluaciones frente a 201 para 200 pasos de Adam en dos dimensiones. Sobre un objetivo suave no cuesta nada en precisión: ambas ejecuciones terminaron en 1.061e-16.

Merece anotarse junto a los hallazgos, porque las lecciones hasta ahora han encontrado sobre todo lo contrario:

  • SGD, Momentum y Adam de ix_optimize igualan exactamente a las versiones a mano — mismos recuentos de pasos (5000, 4129, 2822), mismos puntos finales, mismo primer paso desde el mismo gradiente.
  • GradientBoostedClassifier iguala a la versión a mano en cada fila de prueba con cualquier número de rondas, y numpy reproduce las mismas exactitudes, los mismos log-priores suavizados y los mismos tocones de la primera ronda.
  • Las cinco varianzas de PCA coinciden con las de Jacobi hasta 1.28e-10 y con las de scikit-learn hasta los dígitos impresos; solo la razón y el vector inicial fijo están en cuestión.
  • El plan promete una lección por familia de algoritmos, lo que aún dejará intacta la mayor parte del workspace. scripts/sync-ix-api-map.mjs llena el hueco por el otro lado: un solo git grep sobre el commit fijado, y una página generada por idioma que lista cada declaración pub bajo crates/*/src/, crate a crate y módulo a módulo. 80 crates, 3398 declaraciones, de las cuales 2307 son funciones.
  • Está generada, nunca escrita a mano, y npm run sync:ix-api-map la reconstruye — así que no puede alejarse del commit fijado, que es lo que suele pudrir una página así. La revisión se lee del Cargo.toml del curso en vez de escribirse dos veces.
  • Cuenta lo que dice el código fuente, no lo que un llamante puede alcanzar: un pub dentro de un módulo privado aparece listado y es invisible desde fuera del crate. El hallazgo 12 — un campo privado que deja la razón inservible — es exactamente el tipo de cosa que la página no puede contarte, y las lecciones sí.
  • La primera ejecución de CI del código de las lecciones 5 a 8 salió roja solo en macos-latest, en l07_networks: la red del o exclusivo imprimía predictions [-0.0000, 1.0000, 1.0000, -0.0000] donde Windows y Linux imprimen 0.0000 en esas dos esquinas. Ejecución 35039180659; los otros tres jobs pasaron, y también los otros quince ejemplos.
  • En las dos esquinas nulas la red aterriza por debajo del cuarto decimal, y solo difiere el signo — las mismas sumas tomadas en otro orden en ARM. -0.0 y 0.0 son el mismo número y se comparan iguales: el signo no es un resultado, y ninguna afirmación de las lecciones cambia.
  • fmt_vec existe para que «las salidas no dependan de cómo imprime cada sistema los últimos dígitos», lo que lo convierte en el lugar del arreglo: todo lo que se redondea a cero imprime ahora 0.0000, nunca -0.0000, y cualquier otro signo sobrevive. Una prueba unitaria fija ambas mitades. Ningún archivo expected/ cambió, así que ninguna lección tuvo que volver a citarse.
  • La nota del 2026-09-14 — decimales fijos, así que las salidas son idénticas en los tres sistemas — acertaba con el remedio y se quedaba a un caso de estar completa. Los decimales fijos no deciden el signo de un cero, y un curso escrito en una sola máquina no puede descubrirlo.

2026-09-24 — Cinco reductores, tres entradas

Sección titulada «2026-09-24 — Cinco reductores, tres entradas»
  • l09_reducers.rs se ejecutó en Windows con el commit IX fijado: un cuadrado para MDS, dos anillos para PCA con núcleo y los datos CI de 186 jobs para NMF y LDA (las primeras doce filas estandarizadas para t-SNE). MDS escrita a mano e IX recuperan las seis distancias del cuadrado con error menor de 1e-10.
  • En los anillos, la separación del primer eje lineal es 0.0000. La del primer eje RBF también es 0.0000; el cuarto llega a 0.5798. Esto refuta la expectativa exploratoria sobre el primer eje. Como el signo y la rotación de los autovectores son arbitrarios, la comparación usa diferencias absolutas de medias sin inferir una puntuación de clasificación.
  • NMF de rango dos reconstruye la matriz bruta de tiempos con error cuadrático medio de 0.505 segundos cuadrados por celda y rechaza las entradas estandarizadas negativas. LDA produce dos ejes finitos para tres etiquetas de sistema operativo, sin afirmación de clasificación fuera de muestra. t-SNE con semilla genera una proyección finita 12 × 2, pero transform devuelve exactamente la misma proyección al recibir una entrada no relacionada 1 × 1.
  • La comprobación independiente con numpy/scikit-learn 1.8.0 reproduce las distancias del cuadrado, el resultado de los ejes de los anillos, la reconstrucción NMF finita, los dos ejes LDA y la proyección t-SNE finita. Son comprobaciones de invariantes, no afirmaciones de igualdad de factores NMF o coordenadas t-SNE entre implementaciones. Las hipótesis no se anotaron antes de la ejecución: son hallazgos exploratorios, no confirmaciones prerregistradas. La CI en tres sistemas para la lección 9 sigue pendiente.
  • La herramienta ix_ml_pipeline de principio a fin: el orden del escalado del hallazgo 1, la inferencia de tarea del hallazgo 2 sobre un archivo CSV, y el error All rows contain NaN values para un archivo con una columna de texto. Los tres están leídos en el código, no ejecutados.
  • LinearRegression::fit con dos características idénticas: si ix_math::linalg::inverse devuelve un error y fit hace panic, o devuelve una respuesta errónea.
  • Un cluster de k-means que se vacía a mitad de una ejecución real, y no al inicio como en el hallazgo 4.
  • ix_voicings::cluster sobre los voicings de GA: la exportación necesita FretboardVoicingsCLI de GA, que el curso no compila.
  • La causa del build de 48 segundos de 95a3830.
  • Si los hallazgos 1 a 9 ya se conocen en el proyecto original: no he buscado en los issues de IX.
  • Los ejemplos en runners Linux ARM: el CI solo cubre ubuntu-latest (x64), windows-latest y macos-latest (ARM).
  • Si PCA devuelve alguna vez varianzas sin ordenar con datos reales, y no con la nube construida del hallazgo 11.
  • Si un Sequential más profundo que dos capas Dense se usa en algún sitio de IX, donde mordería el hallazgo 18.
  • ix-autograd: su cinta es el crate que debería hacer innecesario el hallazgo 15, y la lección 12 lo medirá.
  • Si los hallazgos 10 a 19 ya se conocen en el proyecto original: sigo sin buscar en los issues de IX.
  • El mapa de API cuenta declaraciones pub, no las alcanzables; cuánto se separan ambos números está sin medir.
  • Los valores impresos por un println!("{:.6}") directo y no a través de fmt_vec — el intercept -0.000000 de la lección 8 es uno — llevan el mismo riesgo de cero con signo y no están normalizados. Ese coincidió en los tres sistemas en la ejecución 35039180659; los demás no se han enumerado.
  • Repetir las salidas numéricas de la lección 9 en CI Linux y macOS; comprobar si el error NMF y la separación del cuarto eje RBF se redondean igual. Medir por separado cómo escala el tiempo de t-SNE antes de asignarle un coste.