Aller au contenu
learn

Journal

  • IX cloné et épinglé au commit 490c395 ; le code du cours dépend de sept de ses crates
  • Données : builds.csv et jobs.csv, extraits de l’historique CI et de l’historique Git de ce dépôt
  • CI : formatage, clippy, tests unitaires et chaque exemple comparé avec expected/ sur trois OS, plus une vérification croisée avec numpy et scikit-learn sous Linux
  • Leçon 1 : données, caractéristiques et évaluation
  • Leçon 2 : régression linéaire et descente de gradient
  • Leçon 3 : classification
  • Leçon 4 : partitionnement
  • Leçon 5 : composantes principales
  • Leçon 6 : ensembles
  • Leçon 7 : réseaux de neurones
  • Leçon 8 : optimisation
  • Leçon 9 : cinq autres réducteurs, avec une MDS écrite à la main et des contrôles indépendants
  • Annexe : la carte d’API, générée depuis le commit épinglé
  • Leçons 10 à 21
  • Traductions française et espagnole

IX est la bibliothèque de quelqu’un d’autre, épinglée à 490c395. Les leçons précédentes ont recensé dix-neuf constats ; la leçon 9 ajoute deux constats d’API ou de documentation. La première colonne indique ce qu’attendrait un appelant. Aucun constat n’est déclaré comme ticket IX. Plusieurs sont des choix défendables plutôt que des défauts, et la colonne du milieu dit lesquels.

Attendu Ce qui se passe Où Mesure État
Les lignes de test ne participent pas au façonnage du prétraitement Avec normalize, le pipeline ajuste StandardScaler et la PCA sur toutes les lignes, puis découpe ml_pipeline.rs 190-203 La première ligne de test vaut 1,440 contre 1,042 si le découpage vient d’abord Reproduit, non déclaré 2026-09-14
Un vecteur d’étiquettes entières est une cible de régression sauf mention contraire infer_task_type qualifie de classification tout vecteur d’entiers positifs comptant au plus 20 valeurs distinctes preprocessing.rs 234-254 Vingt valeurs distinctes, c’est toute la règle Reproduit, non déclaré 2026-09-14
Une égalité entre plus proches voisins se tranche comme chez scikit-learn max_by_key rend le dernier maximum : l’égalité va donc à l’indice de classe le plus grand knn.rs 53 Exactitude 0,947 contre 0,974 Reproduit, non déclaré 2026-09-14
Un cluster k-means vide est réensemencé, comme le fait scikit-learn La mise à jour des centroïdes part de zéros et saute les clusters sans lignes : le centroïde reste à l’origine kmeans.rs 137-152 KMeans(3) sur [5, 5, 9, 9] donne [5.0, 9.0, 0.0] Reproduit, non déclaré 2026-09-14
La silhouette d’une ligne seule dans son cluster vaut 0, comme la définissent Rousseeuw et scikit-learn silhouette_score_exact pose a = 0, d’où s = 1 ix-voicings/lib.rs 664-678 0,9296 contre 0,5963 Reproduit, non déclaré 2026-09-14
Les lignes d’import des tutoriels compilent use ix_unsupervised::{KMeans, Clusterer}; échoue en E0432 : la crate n’exporte que des modules ix-unsupervised/lib.rs 5-14 E0432 Reproduit, non déclaré 2026-09-14
KMeans::fit fait plusieurs départs et garde le meilleur Il fait un unique départ k-means++ ; l’outil MCP ix_kmeans fixe en plus la graine à 42 kmeans.rs, handlers.rs 322 Un seul départ Reproduit, non déclaré 2026-09-14
ix_voicings::cluster compare les k qu’il essaie Il garde k = 5 dès que sa silhouette atteint 0,15, et n’essaie k = 3 qu’en dessous ix-voicings/lib.rs 770-776 Un seuil, pas une comparaison Reproduit, non déclaré 2026-09-14
Un mélange gaussien se prémunit contre l’effondrement d’une composante GMM plancher chaque variance à 1e-6, ce qui laisse une composante s’effondrer sur un point gmm.rs 170-171 1e-6 Reproduit, non déclaré 2026-09-14
explained_variance_ratio est une part de la variance totale Chaque valeur propre est divisée par la somme des valeurs propres conservées : les ratios somment donc toujours à 1 pca.rs 46-57 [0,5635, 0,4365] contre les vraies [0,3867, 0,2996], dont la somme fait 0,6863 Reproduit, non déclaré 2026-09-15
L’itération de la puissance trouve l’axe principal Elle part du vecteur fixe (1, …, 1)/√n, et peut rendre l’axe secondaire pca.rs 107 Un départ qui n’a rien d’aléatoire Reproduit, non déclaré 2026-09-15
Les valeurs propres sont relisibles PCA::explained_variance est privé et le ratio est normalisé comme ci-dessus : save_state() est la seule sortie pca.rs Un seul accès, et c’est un sérialiseur Reproduit, non déclaré 2026-09-15
Une forêt aléatoire tire ses variables à chaque coupe, comme son propre commentaire le promet Elle les tire une fois par arbre, avant de le construire random_forest.rs 68-84 Avec max_features = 1 : version à la main 0,9474, IX 0,7632 Reproduit, le commentaire et le code se contredisent, non déclaré 2026-09-15
Une égalité dans le vote de la forêt se tranche comme chez scikit-learn max_by garde le dernier maximum : elle va à l’indice de classe le plus grand random_forest.rs 97 Même forme que le constat 3 Reproduit, non déclaré 2026-09-15
Le gradient analytique égale un gradient par différences finies Dense::backward divise une seconde fois par la taille du lot : grad_output porte déjà le 1/n que mse_gradient y a mis ix-nn/layer.rs 40-52 Rapport de 65,0000 exactement face à l’oracle par différences finies Reproduit, non déclaré 2026-09-15
mse_gradient est le gradient de mse_loss Il l’est pour une colonne de sortie. mse_loss moyenne sur les lignes et les colonnes ; mse_gradient ne divise que par les lignes ix-nn/loss.rs 6-16 Rapports 1, 2, 3, 4 pour 1 à 4 colonnes Reproduit, non déclaré 2026-09-15
Dense::new accepte une graine Il tire du générateur du thread : un réseau n’est donc pas reproductible ix-nn/layer.rs 27 Aucun paramètre de graine Reproduit, non déclaré 2026-09-15
Un Sequential de deux couches Dense peut apprendre le XOR Dense est le seul type qui implémente Layer : il n’existe aucune activation à glisser entre deux applications affines, et tout le réseau est une seule application affine ix-nn La perte plafonne à 0,25 après 50 000 époques et prédit 0,5 aux quatre coins Reproduit, fonctionnalité manquante plutôt que défaut, non déclaré 2026-09-15
minimize distingue une exécution divergée d’une exécution à court d’itérations Il rend le point de départ de l’appelant, étiqueté comme le meilleur vu, avec le même converged: false qu’une non-convergence ordinaire ix-optimize/gradient.rs 124-165 Rosenbrock au pas 0,01 rapporte best f 24.200000, la valeur au départ Reproduit, non déclaré 2026-09-15
TSNE::transform(new_rows) place ces lignes sur la carte ajustée L’argument est ignoré : la méthode rend l’embedding 12 × 2 mémorisé même pour une entrée 1 × 1 tsne.rs 249-253 La sortie est exactement l’embedding d’origine Reproduit, non déclaré 2026-09-24
L’en-tête Barnes-Hut du module t-SNE décrit l’implémentation compute_q et le gradient parcourent toutes les paires de points tsne.rs 160-178, 205-217 Boucles quadratiques visibles dans le code ; débit non mesuré Confirmé dans le code, non déclaré 2026-09-24

Un cours qui trouve des divergences doit montrer ce qu’il a vérifié et trouvé juste. Les six contrôles initiaux restent ci-dessous ; le cinquième a échoué et produit les constats 15 et 16. La leçon 9 ajoute deux contrôles géométriques exploratoires, non pré-enregistrés.

Question Hypothèse Résultat Verdict Où
Les SGD, Momentum et Adam d’IX suivent-ils pas à pas des implémentations écrites à la main ? Une implémentation correcte doit coïncider exactement, pas approximativement Mêmes nombres de pas — 5000, 4129, 2822 — mêmes points finaux et même premier pas Confirmée 2026-09-15
La décomposition propre de la PCA d’IX coïncide-t-elle avec Jacobi et la SVD de scikit-learn ? Sous-entendue par les cinq variances qu’elle affiche Jacobi à 1,28e-10, scikit-learn aux décimales affichées Confirmée — seuls le ratio et le départ fixe sont faux 2026-09-15
GradientBoostedClassifier coïncide-t-il avec une version à la main et un contrôle numpy ? Sous-entendue Il coïncide sur chaque ligne de test à chaque nombre de tours, avec les mêmes log-priors lissés et les mêmes souches au premier tour Confirmée 2026-09-15
Un xorshift 64 bits rejouable peut-il remplacer le StdRng non rejouable d’IX dans un contrôle par bootstrap ? Écrite à l’avance : un générateur rejouable employé à l’identique en Rust et en Python retombe sur les mêmes lignes hors sac Le contrôle numpy retombe sur les mêmes 48 lignes laissées de côté Confirmée 2026-09-15
Le gradient analytique d’ix_nn égale-t-il son gradient par différences finies ? Écrite à l’avance : le rapport doit valoir 1 65,0000, puis 1, 2, 3, 4 selon le nombre de colonnes — constats 15 et 16 Réfutée 2026-09-15
La sommation flottante sur macOS-ARM change-t-elle une affirmation des leçons ? Écrite à l’avance : seul le signe d’une valeur déjà nulle doit différer -0.0000 contre 0.0000 à deux coins, toutes les autres valeurs identiques à quatre décimales (exécution 35039180659) Confirmée 2026-09-16
La MDS classique écrite à la main conserve-t-elle les mêmes distances du carré qu’IX ? Les deux devraient retrouver les six distances, quelle que soit l’orientation des axes Erreur maximale inférieure à 1e-10 pour les deux ; numpy concorde Confirmée, exploratoire 2026-09-24, l09_reducers.rs
Le premier axe de la PCA à noyau RBF sépare-t-il des anneaux concentriques ? La similarité non linéaire devrait placer le contraste radial en premier IX et scikit-learn le placent sur l’axe 4 ; écart de 0.5798 dans IX, nul sur l’axe 1 Réfutée, exploratoire 2026-09-24, l09_reducers.rs
  • IX est cloné à part de ma copie de travail, avec git clone --filter=blob:none, et extrait à 490c39533627d296bf9f8f050e6fafc14d7a20c2, un commit de main du 2026-09-14, 21:40 UTC. Le workspace a 83 dossiers sous crates/ (son README annonce 81 crates) ; le cours lit ix-math, ix-supervised, ix-optimize, ix-unsupervised, ix-voicings, ix-io et ix-agent, et dépend des cinq premières.
  • Cargo.toml nomme chaque crate avec git et rev, et Cargo.lock est commité. Cargo récupère une seule fois tout le dépôt IX pour les cinq crates, y compris son sous-module governance/demerzel.
  • Le code du cours partage ndarray 0.17 avec IX : les matrices passent des versions à la main aux fonctions d’IX sans conversion.
  • Les outils MCP d’IX de ma session Claude Code m’ont aidé à trouver les algorithmes ; chaque résultat des leçons vient des exemples compilés, pas des outils.
  • data/extract.py lit code/duckdb/data/runs.json et jobs.json, l’export que le cours DuckDB a fait le 2026-09-14, et exécute git ls-tree à chaque commit construit pour compter ses fichiers .md et .mdx.
  • builds.csv : les jobs build réussis de Deploy to GitHub Pages, 65 lignes, avec les secondes de l’étape dont le nom commence par Install, build.
  • jobs.csv : les jobs terminés en succès ou en échec qui ont à la fois une étape de checkout et son étape post, 186 lignes. L’OS est le label du runner sans -latest. queue_s est le temps entre la création du job et son démarrage.
  • Chaque durée est un nombre entier de secondes (la différence de deux horodatages, tronquée). La leçon 1 montre ce que cela fait à l’inférence de tâche d’IX, la leçon 4 ce que cela fait à un mélange gaussien.
  • Aucun jeu de données n’a été téléchargé : les deux fichiers sont dérivés de ce dépôt public.
  • ml-ix-examples.yml exécute check.sh sur ubuntu-latest, windows-latest et macos-latest : cargo fmt --check, cargo clippy --release --all-targets -- -D warnings, cargo test --release (tests unitaires et doctests, y compris l’import compile_fail de la leçon 4), puis chaque exemple, dont la sortie et le code de sortie sont comparés avec expected/ par diff --strip-trailing-cr. UPDATE=1 réécrit expected/, CROSSCHECK=1 ajoute la vérification Python en local.
  • La vérification croisée ne tourne que sous Linux, avec Python 3.13, numpy 2.4.2 et scikit-learn 1.8.0 : elle recalcule les métriques, la droite de régression, l’arbre de décision, les exactitudes des k plus proches voisins, les silhouettes, DBSCAN et un mélange gaussien, et compare sa sortie avec expected/crosscheck.txt. Elle ne peut pas rejouer le générateur de nombres aléatoires d’IX, et ne reproduit donc ni les départs de k-means d’IX ni ses découpages aléatoires.
  • Les valeurs à virgule flottante sont affichées avec un nombre fixe de décimales (fmt_vec dans src/lib.rs), si bien que les sorties sont identiques sur les trois systèmes ; aucune n’a eu besoin d’un fichier par OS.
  • Les exécutions 34903197003 (les exemples des leçons, commit d9ef7fb) et 34903462618 (les solutions des exercices, commit 15cde43) ont réussi sur les quatre jobs dès le premier push.
  • Le train_test_split, le k-means++, le départ du GMM et le StratifiedKFold d’IX tirent leurs nombres du StdRng de Rust, que Python ne peut pas rejouer. Pour ceux-là, la version à la main part du résultat ajusté d’IX et vérifie qu’un pas de plus ne change rien (k-means, EM), et la vérification croisée compare ce qui ne dépend pas du départ : tailles, meilleures inerties sur n départs, log-vraisemblances.
  • La leçon 3 teste sur un job sur cinq, un découpage que les deux langages savent faire.
  • La leçon 3 exécute k = 4 à côté de k = 5 pour le départage des égalités d’IX : l’égalité que j’avais d’abord vue a disparu quand le jeu de test est devenu un job sur cinq, et k = 4 standardisé en montre de nouveau une, sur une vraie ligne de test.
  • La première comparaison de mélange gaussien ne correspondait pas à scikit-learn. En remontant la piste, je suis tombé sur les variances effondrées de complete_s, qui sont devenues une section de la leçon 4 au lieu d’une note de bas de page.

Neuf endroits où la réponse d’IX, ou sa documentation, diffère du manuel ou de scikit-learn, chacun montré par du code compilé dans le cours sauf mention contraire. Aucun n’a fait l’objet d’une issue IX.

  1. Mise à l’échelle avant le découpage. Avec normalize activé, le pipeline ML ajuste StandardScaler (et la PCA) sur toutes les lignes, puis découpe : les lignes de test façonnent la mise à l’échelle (ml_pipeline.rs, lignes 190-203, puis 537-538 et 704-705). Lu dans le code ; le mécanisme est montré dans la leçon 1 (première ligne de test à 1.440 contre 1.042), l’outil lui-même n’a pas été exécuté.
  2. Les cibles entières deviennent des classes. infer_task_type qualifie de classification tout vecteur d’étiquettes entières positives ou nulles ayant au plus 20 valeurs distinctes (preprocessing.rs, lignes 234-254) : les secondes de build donnent MulticlassClassification { n_classes: 18 }, et ajouter 0.5 à une ligne donne Regression. Le pipeline l’utilise quand la tâche vaut auto.
  3. Égalités des k plus proches voisins. KNN::predict utilise max_by_key, qui renvoie le dernier maximum : une égalité va au plus grand indice de classe (knn.rs, ligne 53). Sur les jobs standardisés avec k = 4, la ligne de test 27 vote [2, 0, 2] : IX dit macOS, la version à la main et scikit-learn Ubuntu, le vrai OS ; exactitude de 0.947 contre 0.974.
  4. Un cluster k-means vide part à l’origine. La mise à jour des centroïdes part de zéros et saute les clusters sans lignes (kmeans.rs, lignes 137-152). KMeans(3) sur [5, 5, 9, 9] donne les centroïdes [5.0, 9.0, 0.0], et predict([1.0]) renvoie le cluster vide ; scikit-learn donne [5.0, 9.0, 9.0] avec un avertissement.
  5. La silhouette d’un singleton vaut 1. silhouette_score_exact dans ix-voicings fixe a = 0 pour une ligne seule dans son cluster, donc s = 1, là où Rousseeuw et scikit-learn utilisent 0 (lib.rs, lignes 664-678) : 0.9296 contre 0.5963 sur [0, 1 | 10].
  6. Les imports des tutoriels ne compilent pas. use ix_unsupervised::{KMeans, Clusterer}; échoue avec E0432, parce que la crate n’exporte que des modules (lib.rs, lignes 5-14). La même forme se trouve dans docs/unsupervised-learning/kmeans.md (lignes 60 et 91), dbscan.md (ligne 50), pca.md (lignes 63 et 101), docs/use-cases/fraud-detection.md (ligne 43), gis-spatial-analysis.md (lignes 79, 375, 427), docs/foundations/rust-for-ml.md (ligne 141), et dans leurs versions françaises. Le doctest du cours vérifie la première.
  7. Un seul départ pour k-means. KMeans::fit lance un seul départ k-means++, et l’outil MCP ix_kmeans fixe la graine à 42 (handlers.rs, ligne 322). Sur les jobs, les graines 0 à 9 donnent des inerties de 411.23 à 618.02 ; pour k = 6, la graine 42 donne 223.366 là où le meilleur de 50 départs de scikit-learn donne 214.040, avec une silhouette de 0.3646 contre 0.4400.
  8. La règle des voicings ne compare pas. ix_voicings::cluster garde k = 5 dès que sa silhouette atteint 0.15, et n’essaie k = 3 qu’en dessous (lib.rs, lignes 770-776). Sur les jobs, elle garde k = 5 (0.4350) plutôt que k = 3 (0.4997). Un choix de conception plutôt qu’une erreur, mais le commentaire de documentation présente le seuil comme celui d’« acceptation du partitionnement » (accepting the clustering), pas comme celui du choix de k.
  9. Effondrement du mélange gaussien. GMM impose à chaque variance un plancher de 1e-6 (gmm.rs, lignes 170-171) et lance un seul départ aléatoire. Sur les durées en secondes entières, des composantes s’effondrent sur complete_s, et la log-vraisemblance dépend de la graine, de −102.666 à 552.879 ; le meilleur de 10 départs de scikit-learn est −102.667. Comparer les log-vraisemblances d’IX choisit le modèle le plus effondré.

Des différences bonnes à connaître, que je ne compte pas comme des erreurs :

  • LogisticRegression n’a ni régularisation ni test d’arrêt : sur des données séparables, ses poids grandissent avec le nombre d’itérations (leçon 3).
  • ix_optimize::gradient::SGD est une descente de gradient sur le lot complet : minimize lui passe le gradient sur toutes les lignes.
  • DBSCAN étiquette le bruit 0 et les clusters à partir de 1, comme le dit sa documentation ; un code écrit pour le -1 de scikit-learn fond le bruit dans un cluster.
  • ix_io::csv_io::read_csv transforme les champs texte en NaN sans erreur.
  • train_test_split mélange toujours, et prend les étiquettes de classification en f64 ; il n’y a pas d’option stratifiée ni chronologique.
  • Quatre leçons d’un coup : composantes principales, ensembles, réseaux de neurones, optimisation. Le cours dépend désormais de sept crates IX, ix-ensemble et ix-nn ayant été ajoutées au même commit épinglé, et exécute seize exemples en CI.
  • La vérification croisée a grandi avec elles. La leçon 5 est la première où scikit-learn peut vérifier IX directement et complètement — PCA est déterministe, ne prend pas de graine, et a une convention de signe documentée — si bien que chaque nombre de cette leçon a trois sources indépendantes.
  • La leçon 6 a introduit un générateur xorshift 64 bits dans le code du cours. IX tire de StdRng, que Python ne peut pas rejouer ; trois décalages et trois XOR se rejouent exactement, et la vérification numpy retombe sur les mêmes 48 lignes laissées de côté par le même bootstrap. Cela comble le manque dont se plaignait la note du 2026-09-14, pour les algorithmes que le cours écrit lui-même.
  • La leçon 7 est la première à utiliser les différences finies comme oracle plutôt que scikit-learn. Chaque affirmation sur ce que calcule ix_nn a été mesurée en bougeant un poids et en regardant la perte, pas déduite d’une lecture — ce qui s’est avéré compter, puisque deux des trois constats se trompent d’un facteur que personne ne devinerait à la lecture du code.
  • La leçon 8 n’a rien trouvé à redire aux règles de mise à jour d’ix_optimize : SGD, Momentum et Adam suivent les versions à la main pas à pas et atterrissent sur les mêmes points. Le constat, là, porte sur le type de résultat de minimize, pas sur son arithmétique.

Dix autres endroits où la réponse d’IX, ou sa documentation, diffère du manuel ou de scikit-learn, chacun montré par du code compilé dans le cours. Avec les neuf du 2026-09-14, cela fait dix-neuf. Aucun n’a fait l’objet d’une issue IX.

  1. Le ratio de variance expliquée est normalisé sur les composantes retenues. explained_variance_ratio divise chaque valeur propre par la somme des valeurs propres retenues (pca.rs, lignes 46-57), si bien que les ratios somment à 1 pour tout n_components et que le « proportion of total variance » du commentaire de documentation n’est vrai que si toutes les composantes sont gardées. Sur les jobs standardisés, PCA::new(2) rapporte [0.5635, 0.4365] là où la vérité, et scikit-learn, disent [0.3867, 0.2996], de somme 0.6863. La question habituelle « combien de composantes pour 90 % ? » ne peut pas lui être posée.
  2. L’itération de puissance part d’un vecteur fixe et peut renvoyer l’axe mineur. Le départ est (1, …, 1)/√n (pca.rs, ligne 107). Sur un nuage de six points étiré le long de (1, -1), de covariance [[2.4, -1.6], [-1.6, 2.4]], ce départ est le vecteur propre mineur, donc PCA::new(1) renvoie une variance de 0.8 le long de (1, 1) au lieu de 4.0 le long de (1, -1). PCA::new(2) fait pire : la déflation laisse une matrice qui envoie le départ sur zéro, le garde-fou norm < 1e-15 se déclenche, et le modèle finit avec deux fois la même composante et une variance de 0. La SVD de scikit-learn renvoie 4.0 et (0.7071, -0.7071). Un cas sur le fil du rasoir — il faut des variances exactement égales — mais rien dans la sortie ne dit dans quel cas on est.
  3. Les variances sont inaccessibles. PCA::explained_variance est un champ privé, et explained_variance_ratio() est normalisé comme au constat 10, si bien que save_state() est le seul moyen de sortir les valeurs propres d’un modèle ajusté.
  4. Une forêt aléatoire tire ses variables une fois par arbre, pas une fois par division. Le commentaire de documentation promet « random feature subsets (sqrt(n_features) features per split) » ; le code les tire avant la croissance de l’arbre et construit tout l’arbre à partir de ces colonnes (random_forest.rs, lignes 68-84). Au défaut de trois variables sur cinq, les deux s’accordent, atteignant toutes deux 0.9211 sur les jobs de test ; à max_features = 1, la version à la main, qui tire à chaque division, atteint 0.9474 et IX 0.7632, parce que cinquante arbres unidimensionnels ne peuvent pas combiner de variables.
  5. Une égalité dans le vote de la forêt revient au plus grand indice de classe. predict utilise max_by, qui garde le dernier maximum (random_forest.rs, ligne 97). Deux souches avec la graine 6 donnent des probabilités [0.500, 0.500], et IX répond la classe 1 là où scikit-learn et np.argmax répondent 0. Même forme que le constat 3 sur KNN::predict.
  6. Dense::backward divise deux fois par la taille du lot. grad_output porte déjà le 1/n que mse_gradient y a mis, et backward divise encore par n (layer.rs, lignes 40-52). Mesuré contre le gradient par différences centrées de mse_loss sur les 65 builds, le rapport vaut exactement 65.0000. L’entraînement converge quand même, puisque chaque couche divise par le même n, mais le taux d’apprentissage que vous passez est le taux d’apprentissage divisé par la taille du lot.
  7. mse_gradient n’est pas le gradient de mse_loss au-delà d’une colonne de sortie. mse_loss moyenne sur les lignes et les colonnes, mse_gradient ne divise que par les lignes (loss.rs, lignes 6-16). Mesuré de une à quatre colonnes, le rapport vaut 1, 2, 3, 4. Combiné au constat 15, une couche à deux sorties sur 65 lignes bouge du vrai gradient divisé par 32.5.
  8. Dense::new ne prend pas de graine. Il tire du générateur du fil d’exécution (layer.rs, ligne 27), si bien que deux couches construites dans la même exécution diffèrent déjà et qu’aucun résultat de Sequential ne peut être reproduit ni testé contre les régressions. Tous les autres algorithmes aléatoires d’IX en prennent une — KMeans, RandomForest, Dropout, transformer::FeedForward. Les champs weights et bias sont publics, c’est la sortie de secours et c’est ce que fait le cours.
  9. Un Sequential de couches Dense est une seule application affine. Dense est le seul type qui implémente le trait Layer : ix-nn n’a aucune couche d’activation que Sequential::push pourrait accepter entre deux applications affines. Sur le ou exclusif, deux couches Dense convergent vers une perte de 0.25 — la variance des cibles, le mieux qu’une constante puisse faire — et prédisent 0.5 aux quatre coins, tandis que le réseau à la main de même taille, avec une sigmoïde entre ses couches, atteint 0. Après 50 000 époques, la pile satisfait encore f(0,0) + f(1,1) - f(0,1) - f(1,0) = 0 au dernier bit : elle n’est pas sous-entraînée, elle ne peut pas représenter la fonction. Les vraies non-linéarités du crate vivent dans transformer::gelu, à l’intérieur de FeedForward, qui travaille sur Array3 et n’est pas un Layer.
  10. minimize renvoie le point de départ pour une exécution qui a divergé. Elle retient le meilleur point traversé et renvoie celui-là (gradient.rs, lignes 124-165). La descente simple au taux 0.01 sur Rosenbrock part en NaN dès ses premiers pas ; le résultat annonce best f 24.200000, qui est f au point fourni par l’appelant, avec converged: false — le même drapeau qu’obtient une exécution parfaitement bonne qui a simplement épuisé ses itérations. Ni best_value ni converged ne sépare les deux.

Des différences bonnes à connaître, que je ne compte pas comme des erreurs :

  • RegressionStump met la moyenne des résidus dans chaque feuille, là où la recette de Friedman et scikit-learn emploient un pas de Newton. C’est une simplification défendable, et la version à la main la copie pour que les deux soient comparables ; avec elle, IX et la version à la main s’accordent sur chacune des 38 lignes de test à 1, 5, 10, 25 et 50 tours.
  • GradientBoostedClassifier n’a ni arrêt anticipé ni score sur un jeu réservé. Sur les jobs il culmine à 0.9474 après cinq tours et se stabilise à 0.9211 dès vingt-cinq, et rien dans l’API ne le mesure.
  • ix-ensemble n’a pas de score out-of-bag, que le bagging offre gratuitement et que scikit-learn expose par oob_score=True.
  • ClosureObjective ne redéfinit jamais ObjectiveFunction::gradient, si bien qu’envelopper une closure achète silencieusement un gradient mesuré au prix de deux évaluations supplémentaires par coordonnée et par pas — 1001 évaluations contre 201 pour 200 pas d’Adam en deux dimensions. Sur un objectif lisse, cela ne coûte rien en précision : les deux exécutions ont fini à 1.061e-16.

Cela mérite d’être consigné à côté des constats, car les leçons ont surtout trouvé l’inverse jusqu’ici :

  • Les SGD, Momentum et Adam d’ix_optimize suivent exactement les versions à la main — mêmes nombres de pas (5000, 4129, 2822), mêmes points finaux, même premier pas depuis le même gradient.
  • GradientBoostedClassifier suit la version à la main sur chaque ligne de test à chaque nombre de tours, et numpy reproduit les mêmes exactitudes, les mêmes log-priors lissés et les mêmes souches du premier tour.
  • Les cinq variances de PCA correspondent à celles de Jacobi à 1.28e-10 près et à celles de scikit-learn aux chiffres affichés ; seuls le ratio et le vecteur de départ fixe sont en cause.
  • Le plan promet une leçon par famille d’algorithmes, ce qui laissera encore l’essentiel du workspace intouché. scripts/sync-ix-api-map.mjs comble le manque par l’autre bout : un seul git grep sur le commit épinglé, et une page générée par locale listant chaque déclaration pub sous crates/*/src/, crate par crate et module par module. 80 crates, 3398 déclarations, dont 2307 fonctions.
  • Elle est générée, jamais écrite à la main, et npm run sync:ix-api-map la reconstruit — elle ne peut donc pas s’écarter du pin, ce qui est d’ordinaire ce qui pourrit une page de ce genre. La révision est lue dans le Cargo.toml du cours plutôt qu’écrite deux fois.
  • Elle compte ce que dit la source, pas ce qu’un appelant peut atteindre : un pub dans un module privé est listé et reste invisible depuis l’extérieur du crate. Le constat 12 — un champ privé qui rend le ratio inutilisable — est exactement le genre de chose que la page ne peut pas dire, et que les leçons peuvent.
  • La première exécution CI du code des leçons 5 à 8 était rouge sur macos-latest seulement, dans l07_networks : le réseau du ou exclusif affichait predictions [-0.0000, 1.0000, 1.0000, -0.0000] là où Windows et Linux affichent 0.0000 à ces deux coins. Exécution 35039180659 ; les trois autres jobs ont réussi, ainsi que les quinze autres exemples.
  • Aux deux coins nuls, le réseau atterrit sous la quatrième décimale, et seul le signe diffère — les mêmes additions prises dans un autre ordre sur ARM. -0.0 et 0.0 sont le même nombre et se comparent égaux : le signe n’est pas un résultat, et aucune affirmation des leçons ne change.
  • fmt_vec existe pour que « les sorties ne dépendent pas de la façon dont chaque OS imprime les derniers chiffres », ce qui en fait l’endroit du correctif : tout ce qui s’arrondit à zéro affiche désormais 0.0000, jamais -0.0000, et tout autre signe survit. Un test unitaire fixe les deux moitiés. Aucun fichier expected/ n’a changé, donc aucune leçon n’a eu à être recitée.
  • La note du 2026-09-14 — des décimales fixes, donc des sorties identiques sur les trois systèmes — avait raison sur le remède et lui manquait un cas. Des décimales fixes ne tranchent pas le signe d’un zéro, et un cours écrit sur une seule machine ne peut pas l’apprendre.
  • l09_reducers.rs a tourné sous Windows avec le commit IX épinglé : un carré pour la MDS, deux anneaux pour la PCA à noyau, et les données CI de 186 jobs pour la NMF et la LDA (les douze premières lignes standardisées pour t-SNE). La MDS écrite à la main et IX retrouvent les six distances du carré à 1e-10 près.
  • Sur les anneaux, l’écart du premier axe linéaire est 0.0000. Celui du premier axe RBF est lui aussi 0.0000 ; le quatrième atteint 0.5798. L’hypothèse exploratoire sur le premier axe est donc réfutée. Le signe et la rotation des vecteurs propres étant arbitraires, la comparaison prend les écarts absolus entre moyennes sans en déduire un score de classification.
  • La NMF de rang deux reconstruit la matrice brute des durées avec une erreur quadratique moyenne de 0.505 secondes carrées par cellule et refuse les entrées standardisées négatives. La LDA produit deux axes finis pour trois étiquettes de système d’exploitation, sans prétention de classification hors échantillon. t-SNE avec graine donne un arrangement fini de taille 12 × 2, mais transform renvoie exactement le même arrangement avec une entrée sans rapport de taille 1 × 1.
  • Le contrôle indépendant numpy/scikit-learn 1.8.0 confirme les distances du carré, le résultat des axes des anneaux, la reconstruction NMF finie, les deux axes LDA et l’embedding t-SNE fini. Ce sont des contrôles d’invariants, pas une égalité des facteurs NMF ni des coordonnées t-SNE entre implémentations. Les hypothèses n’ont pas été consignées avant l’exécution : il s’agit de résultats exploratoires, non de confirmations pré-enregistrées. La CI sur trois systèmes pour la leçon 9 reste à faire.
  • L’outil ix_ml_pipeline de bout en bout : l’ordre de mise à l’échelle du constat 1, l’inférence de tâche du constat 2 sur un fichier CSV, et l’erreur All rows contain NaN values pour un fichier avec une colonne texte. Les trois sont lus dans le code, pas exécutés.
  • LinearRegression::fit sur deux caractéristiques identiques : si ix_math::linalg::inverse renvoie une erreur et que fit panique, ou s’il renvoie une mauvaise réponse.
  • Un cluster k-means qui se vide au milieu d’une exécution réelle, plutôt qu’au départ comme dans le constat 4.
  • ix_voicings::cluster sur les voicings de GA : l’export demande le FretboardVoicingsCLI de GA, que le cours ne construit pas.
  • La cause du build de 48 secondes de 95a3830.
  • Si les constats 1 à 9 sont déjà connus en amont : je n’ai pas cherché dans les issues d’IX.
  • Les exemples sur les runners Linux ARM : la CI couvre seulement ubuntu-latest (x64), windows-latest et macos-latest (ARM).
  • Si PCA renvoie parfois des variances non triées sur de vraies données, plutôt que sur le nuage construit du constat 11.
  • Si un Sequential plus profond que deux couches Dense est utilisé quelque part dans IX, où le constat 18 mordrait.
  • ix-autograd : son ruban est le crate qui devrait rendre le constat 15 inutile, et la leçon 12 le mesurera.
  • Si les constats 10 à 19 sont déjà connus en amont : je n’ai toujours pas cherché dans les issues d’IX.
  • La carte d’API compte les déclarations pub, pas celles qui sont atteignables ; l’écart entre les deux nombres n’est pas mesuré.
  • Les valeurs affichées par un println!("{:.6}") direct plutôt que par fmt_vec — l’intercept -0.000000 de la leçon 8 en est une — portent le même risque de zéro signé et ne sont pas normalisées. Celle-là concordait sur les trois systèmes dans l’exécution 35039180659 ; les autres n’ont pas été recensées.
  • Rejouer les sorties numériques de la leçon 9 sur les CI Linux et macOS ; vérifier que l’erreur NMF et l’écart du quatrième axe RBF s’arrondissent de la même façon. Mesurer séparément l’évolution du temps de t-SNE avant d’en chiffrer le coût.