Skip to content
learn

Journal

  • IX cloned and pinned at commit 490c395; the course code depends on seven of its crates
  • Data: builds.csv and jobs.csv, extracted from the CI history and the Git history of this repository
  • CI: formatting, clippy, unit tests and every example compared with expected/ on three OSes, and a numpy and scikit-learn cross-check on Linux
  • Lesson 1: data, features and evaluation
  • Lesson 2: linear regression and gradient descent
  • Lesson 3: classification
  • Lesson 4: clustering
  • Lesson 5: principal components
  • Lesson 6: ensembles
  • Lesson 7: neural networks
  • Lesson 8: optimization
  • Lesson 9: five other reducers, with a hand MDS and independent cross-checks
  • Appendix: the API map, generated from the pinned commit
  • Lessons 10 to 21
  • French and Spanish translations

IX is somebody else’s library, pinned at 490c395. The earlier lessons recorded nineteen findings; lesson 9 adds two API or documentation findings. The first column says what a caller would expect. None is filed as an IX issue. Several are defensible choices rather than defects, and the middle column says which.

Expected What happens Where Measure Status
The test rows take no part in shaping the preprocessing With normalize on, the pipeline fits StandardScaler and PCA on all rows and splits afterwards ml_pipeline.rs 190-203 The first test row reads 1.440 against 1.042 when the split comes first Reproduced, not filed 2026-09-14
A vector of integer labels is a regression target unless told otherwise infer_task_type calls any non-negative integer vector with at most 20 distinct values a classification preprocessing.rs 234-254 Twenty distinct values is the whole rule Reproduced, not filed 2026-09-14
A k-nearest-neighbour tie breaks as scikit-learn breaks it max_by_key returns the last maximum, so a tie goes to the largest class index knn.rs 53 Accuracy 0.947 against 0.974 Reproduced, not filed 2026-09-14
An empty k-means cluster is re-seeded, as scikit-learn re-seeds it The centroid update starts from zeros and skips clusters with no rows, so the centroid stays at the origin kmeans.rs 137-152 KMeans(3) on [5, 5, 9, 9] gives [5.0, 9.0, 0.0] Reproduced, not filed 2026-09-14
The silhouette of a row alone in its cluster is 0, as Rousseeuw and scikit-learn define it silhouette_score_exact sets a = 0, so s = 1 ix-voicings/lib.rs 664-678 0.9296 against 0.5963 Reproduced, not filed 2026-09-14
The tutorials’ import lines compile use ix_unsupervised::{KMeans, Clusterer}; fails with E0432: the crate exports modules only ix-unsupervised/lib.rs 5-14 E0432 Reproduced, not filed 2026-09-14
KMeans::fit runs several starts and keeps the best It runs a single k-means++ start; the ix_kmeans MCP tool also fixes the seed at 42 kmeans.rs, handlers.rs 322 One start Reproduced, not filed 2026-09-14
ix_voicings::cluster compares the k it tries It keeps k = 5 as soon as its silhouette reaches 0.15, and only tries k = 3 below that ix-voicings/lib.rs 770-776 A threshold, not a comparison Reproduced, not filed 2026-09-14
A Gaussian mixture guards against a collapsing component GMM floors each variance at 1e-6, which lets a component collapse onto a point gmm.rs 170-171 1e-6 Reproduced, not filed 2026-09-14
explained_variance_ratio is a share of the total variance Each eigenvalue is divided by the sum of the kept eigenvalues, so the ratios always sum to 1 pca.rs 46-57 [0.5635, 0.4365] against the true [0.3867, 0.2996], which sums to 0.6863 Reproduced, not filed 2026-09-15
Power iteration finds the major axis It starts from the fixed vector (1, …, 1)/√n, and can return the minor axis pca.rs 107 A start that is not random Reproduced, not filed 2026-09-15
The eigenvalues can be read back PCA::explained_variance is private and the ratio is normalized as above, so save_state() is the only way out pca.rs One accessor, and it is a serializer Reproduced, not filed 2026-09-15
A random forest draws its features once per split, as its own doc comment promises It draws them once per tree, before the tree is built random_forest.rs 68-84 With max_features = 1: hand-written 0.9474, IX 0.7632 Reproduced, documentation and code disagree, not filed 2026-09-15
A tie in the forest vote breaks as scikit-learn breaks it max_by keeps the last maximum, so it goes to the largest class index random_forest.rs 97 Same shape as finding 3 Reproduced, not filed 2026-09-15
The analytic gradient equals a finite-difference gradient Dense::backward divides by the batch size a second time: grad_output already carries the 1/n that mse_gradient put there ix-nn/layer.rs 40-52 Ratio 65.0000 exactly against the finite-difference oracle Reproduced, not filed 2026-09-15
mse_gradient is the gradient of mse_loss It is, for one output column. mse_loss averages over rows and columns; mse_gradient divides by the rows only ix-nn/loss.rs 6-16 Ratios 1, 2, 3, 4 for 1 to 4 columns Reproduced, not filed 2026-09-15
Dense::new can be seeded It draws from the thread generator, so a network is not reproducible ix-nn/layer.rs 27 No seed parameter Reproduced, not filed 2026-09-15
A Sequential of two Dense layers can learn XOR Dense is the only type implementing Layer, so there is no activation to put between two affine maps: the whole network is one affine map ix-nn The loss floors at 0.25 after 50,000 epochs and predicts 0.5 at all four corners Reproduced, a missing feature rather than a defect, not filed 2026-09-15
minimize tells a diverged run apart from one that ran out of iterations It returns the caller’s own starting point, labelled as the best seen, with the same converged: false as an ordinary non-convergence ix-optimize/gradient.rs 124-165 Rosenbrock at rate 0.01 reports best f 24.200000, the value at the start Reproduced, not filed 2026-09-15
TSNE::transform(new_rows) places those rows on the fitted map The argument is ignored: it returns the stored 12 × 2 embedding even for a 1 × 1 input tsne.rs 249-253 Output exactly equals the original embedding Reproduced, not filed 2026-09-24
The t-SNE module’s Barnes-Hut header describes its implementation compute_q and the gradient each loop over all point pairs tsne.rs 160-178, 205-217 Quadratic loops visible in source; scaling not benchmarked Source-confirmed, not filed 2026-09-24

A course that finds divergences must show what it checked and found right. The original six checks remain below; the fifth failed and produced findings 15 and 16. Lesson 9 adds two exploratory geometry checks, not preregistered experiments.

Question Hypothesis Result Verdict Where
Do IX’s SGD, Momentum and Adam match hand-written implementations step for step? A correct implementation should match exactly, not approximately Same step counts — 5000, 4129, 2822 — the same final points and the same first step Confirmed 2026-09-15
Does IX’s PCA eigendecomposition match Jacobi and scikit-learn’s SVD? Implied by the five variances it prints Jacobi to 1.28e-10, scikit-learn to the printed digits Confirmed — only the ratio and the fixed start are wrong 2026-09-15
Does GradientBoostedClassifier match a hand-rolled and a numpy cross-check? Implied It matches on every test row at every round count, with the same smoothed log priors and the same first-round stumps Confirmed 2026-09-15
Can a replayable 64-bit xorshift stand in for IX’s unreplayable StdRng in a bootstrap check? Written in advance: a replayable generator used identically in Rust and Python lands on the same out-of-bag rows The numpy check lands on the same 48 rows left out Confirmed 2026-09-15
Does ix_nn’s analytic gradient equal its finite-difference gradient? Written in advance: the ratio should be 1 65.0000, and then 1, 2, 3, 4 by column count — findings 15 and 16 Refuted 2026-09-15
Does macOS-ARM float summation change any claim the lessons make? Written in advance: only the sign of an already-zero value should differ -0.0000 against 0.0000 at two corners, every other value identical to four decimals (run 35039180659) Confirmed 2026-09-16
Does hand-written classical MDS preserve the same square distances as IX? Both should reproduce the six distances, irrespective of axis orientation Both have maximum distance error below 1e-10; numpy agrees Confirmed, exploratory 2026-09-24, l09_reducers.rs
Does the first RBF kernel-PCA axis separate concentric rings? Nonlinear similarity should put the radial contrast first Both IX and scikit-learn put it on axis 4; gap 0.5798 in IX, zero on axis 1 Refuted, exploratory 2026-09-24, l09_reducers.rs
  • IX is cloned apart from my working copy, with git clone --filter=blob:none, and checked out at 490c39533627d296bf9f8f050e6fafc14d7a20c2, a commit of main from 2026-09-14, 21:40 UTC. The workspace has 83 folders under crates/ (its README says 81 crates); the course reads ix-math, ix-supervised, ix-optimize, ix-unsupervised, ix-voicings, ix-io and ix-agent, and depends on the first five.
  • Cargo.toml names each crate with git and rev, and Cargo.lock is committed. Cargo fetches the whole IX repository once for the five crates, including its governance/demerzel submodule.
  • The course code shares ndarray 0.17 with IX: the matrices go from the hand versions to IX’s functions without conversion.
  • The IX MCP tools of my Claude Code session helped me find the algorithms; every result in the lessons comes from the compiled examples, not from the tools.
  • data/extract.py reads code/duckdb/data/runs.json and jobs.json, the export the DuckDB course made on 2026-09-14, and runs git ls-tree at each built commit to count its .md and .mdx files.
  • builds.csv: the successful build jobs of Deploy to GitHub Pages, 65 rows, with the seconds of the step whose name starts with Install, build.
  • jobs.csv: the jobs that ended in success or failure and have both a checkout step and its post step, 186 rows. The OS is the runner label without -latest. queue_s is the time from the creation of the job to its start.
  • Every timing is a whole number of seconds (the difference of two timestamps, truncated). Lesson 1 shows what that does to IX’s task inference, lesson 4 what it does to a Gaussian mixture.
  • No data set was downloaded: both files are derived from this public repository.
  • ml-ix-examples.yml runs check.sh on ubuntu-latest, windows-latest and macos-latest: cargo fmt --check, cargo clippy --release --all-targets -- -D warnings, cargo test --release (unit tests and doctests, including the compile_fail import of lesson 4), then each example, whose output and exit code are compared with expected/ by diff --strip-trailing-cr. UPDATE=1 rewrites expected/, CROSSCHECK=1 adds the Python check locally.
  • The cross-check runs on Linux only, with Python 3.13, numpy 2.4.2 and scikit-learn 1.8.0: it recomputes the metrics, the regression line, the decision tree, the k nearest neighbour accuracies, the silhouettes, DBSCAN and a Gaussian mixture, and compares its output with expected/crosscheck.txt. It can’t replay IX’s random number generator, so it doesn’t reproduce IX’s k-means starts or random splits.
  • Floating-point values are printed with a fixed number of decimals (fmt_vec in src/lib.rs), so the outputs are identical on the three systems; none needed a per-OS file.
  • Runs 34903197003 (the lessons’ examples, commit d9ef7fb) and 34903462618 (the exercise solutions, commit 15cde43) passed on all four jobs at the first push.

2026-09-14 — Choices while writing the code

Section titled “2026-09-14 — Choices while writing the code”
  • IX’s train_test_split, k-means++, GMM start and StratifiedKFold draw from Rust’s StdRng, which Python can’t replay. For those, the hand version starts from IX’s fitted result and checks that one more step changes nothing (k-means, EM), and the cross-check compares what doesn’t depend on the start: sizes, best-of-n inertias, log-likelihoods.
  • Lesson 3 tests on every fifth job, a split both languages can make.
  • Lesson 3 runs k = 4 next to k = 5 for IX’s tie-breaking: the tie I first saw disappeared when the test set became every fifth job, and standardized k = 4 shows one again, on a real test row.
  • The first Gaussian mixture comparison didn’t match scikit-learn. Tracing it led to the collapsed complete_s variances, which became a section of lesson 4 instead of a footnote.

Nine places where IX’s answer, or its documentation, differs from the textbook or from scikit-learn, each shown by compiled code in the course unless marked. None is filed as an IX issue.

  1. Scaling before splitting. With normalize on, the ML pipeline fits StandardScaler (and PCA) on all rows, then splits: the test rows shape the scaling (ml_pipeline.rs, lines 190-203, then 537-538 and 704-705). Read in the code; the mechanism is shown in lesson 1 (first test row 1.440 against 1.042), the tool itself not run.
  2. Integer targets become classes. infer_task_type calls any label vector of non-negative integers with at most 20 distinct values classification (preprocessing.rs, lines 234-254): the build seconds give MulticlassClassification { n_classes: 18 }, and adding 0.5 to one row gives Regression. The pipeline uses it when the task is auto.
  3. k nearest neighbours ties. KNN::predict takes max_by_key, which returns the last maximum: a tie goes to the largest class index (knn.rs, line 53). On the standardized jobs with k = 4, test row 27 votes [2, 0, 2]: IX says macOS, the hand version and scikit-learn Ubuntu, the true OS; accuracy 0.947 against 0.974.
  4. An empty k-means cluster goes to the origin. The centroid update starts from zeros and skips clusters without rows (kmeans.rs, lines 137-152). KMeans(3) on [5, 5, 9, 9] gives centroids [5.0, 9.0, 0.0], and predict([1.0]) returns the empty cluster; scikit-learn gives [5.0, 9.0, 9.0] with a warning.
  5. The silhouette of a singleton is 1. silhouette_score_exact in ix-voicings sets a = 0 for a row alone in its cluster, so s = 1, where Rousseeuw and scikit-learn use 0 (lib.rs, lines 664-678): 0.9296 against 0.5963 on [0, 1 | 10].
  6. The tutorials’ imports don’t compile. use ix_unsupervised::{KMeans, Clusterer}; fails with E0432, because the crate exports modules only (lib.rs, lines 5-14). The same form is in docs/unsupervised-learning/kmeans.md (lines 60 and 91), dbscan.md (line 50), pca.md (lines 63 and 101), docs/use-cases/fraud-detection.md (line 43), gis-spatial-analysis.md (lines 79, 375, 427), docs/foundations/rust-for-ml.md (line 141), and their French versions. The course’s doctest checks the first one.
  7. One k-means start. KMeans::fit runs a single k-means++ start, and the ix_kmeans MCP tool fixes the seed at 42 (handlers.rs, line 322). On the jobs, seeds 0 to 9 give inertias from 411.23 to 618.02; for k = 6, seed 42 gives 223.366 where scikit-learn’s best of 50 gives 214.040, with a silhouette of 0.3646 against 0.4400.
  8. The voicings rule doesn’t compare. ix_voicings::cluster keeps k = 5 as soon as its silhouette reaches 0.15, and only tries k = 3 below that (lib.rs, lines 770-776). On the jobs, it keeps k = 5 (0.4350) over k = 3 (0.4997). A design choice rather than an error, but the doc comment calls the threshold one for “accepting the clustering”, not for choosing k.
  9. Gaussian mixture collapse. GMM floors each variance at 1e-6 (gmm.rs, lines 170-171) and runs one random start. On the whole-second timings, components collapse on complete_s, and the log-likelihood depends on the seed, from −102.666 to 552.879; scikit-learn’s best of 10 starts is −102.667. Comparing IX’s log-likelihoods picks the most collapsed model.

Differences worth knowing, which I don’t count as errors:

  • LogisticRegression has no regularization and no stopping test: on separable data, its weights grow with the iteration count (lesson 3).
  • ix_optimize::gradient::SGD is full-batch gradient descent: minimize passes it the gradient over all rows.
  • DBSCAN labels noise 0 and clusters from 1, as its documentation says; code written for scikit-learn’s -1 merges the noise into a cluster.
  • ix_io::csv_io::read_csv turns text fields into NaN without an error.
  • train_test_split always shuffles, and takes classification labels as f64; there’s no stratified or time-ordered option.
  • Four lessons in one batch: principal components, ensembles, neural networks, optimization. The course now depends on seven IX crates, having added ix-ensemble and ix-nn at the same pinned commit, and runs sixteen examples in CI.
  • The cross-check grew with them. Lesson 5 is the first where scikit-learn can check IX directly and completely — PCA is deterministic, takes no seed, and has a documented sign convention — so every number in that lesson has three independent sources.
  • Lesson 6 introduced a 64-bit xorshift generator in the course code. IX draws from StdRng, which Python cannot replay; three shifts and three XORs can be replayed exactly, and the numpy check lands on the same 48 rows left out of the same bootstrap. That closes the gap the 2026-09-14 note complained about, for the algorithms the course writes itself.
  • Lesson 7 is the first to use finite differences as the oracle rather than scikit-learn. Every claim about what ix_nn computes was measured by moving a weight and watching the loss, not derived by reading — which turned out to matter, because two of the three findings are off by a factor nobody would guess from the code.
  • Lesson 8 found nothing wrong with ix_optimize’s update rules: SGD, Momentum and Adam match the hand versions step for step and land on the same points. The finding there is about minimize’s result type, not its arithmetic.

2026-09-15 — Where IX differs, continued

Section titled “2026-09-15 — Where IX differs, continued”

Ten more places where IX’s answer, or its documentation, differs from the textbook or from scikit-learn, each shown by compiled code in the course. With the nine of 2026-09-14 that makes nineteen. None is filed as an IX issue.

  1. The explained-variance ratio is normalized over the components kept. explained_variance_ratio divides each eigenvalue by the sum of the kept eigenvalues (pca.rs, lines 46-57), so the ratios sum to 1 for any n_components and the doc comment’s “proportion of total variance” holds only when every component is kept. On the standardized jobs, PCA::new(2) reports [0.5635, 0.4365] where the truth, and scikit-learn, say [0.3867, 0.2996] summing to 0.6863. The usual “how many components for 90 %?” question cannot be asked of it.
  2. Power iteration starts from a fixed vector and can return the minor axis. The start is (1, …, 1)/√n (pca.rs, line 107). On a six-point cloud stretched along (1, -1), whose covariance is [[2.4, -1.6], [-1.6, 2.4]], that start is the minor eigenvector, so PCA::new(1) returns variance 0.8 along (1, 1) instead of 4.0 along (1, -1). PCA::new(2) is worse: deflation leaves a matrix that sends the start to zero, the norm < 1e-15 guard fires, and the model ends with the same component twice and a variance of 0. scikit-learn’s SVD returns 4.0 and (0.7071, -0.7071). A knife-edge case — it needs exactly equal variances — but nothing in the output says which case you are in.
  3. The variances are unreachable. PCA::explained_variance is a private field, and explained_variance_ratio() is normalized as in finding 10, so save_state() is the only way to get the eigenvalues out of a fitted model.
  4. A random forest draws its features once per tree, not once per split. The doc comment promises “random feature subsets (sqrt(n_features) features per split)”; the code draws them before the tree is grown and builds the whole tree from those columns (random_forest.rs, lines 68-84). At the default of three features of five the two agree, both reaching 0.9211 on the test jobs; at max_features = 1 the hand version, drawing at every split, reaches 0.9474 and IX 0.7632, because fifty one-dimensional trees cannot combine features.
  5. A tie in the forest vote goes to the largest class index. predict takes max_by, which keeps the last maximum (random_forest.rs, line 97). Two stumps with seed 6 give probabilities [0.500, 0.500], and IX answers class 1 where scikit-learn and np.argmax answer 0. The same shape as finding 3 about KNN::predict.
  6. Dense::backward divides by the batch size twice. grad_output already carries the 1/n that mse_gradient put there, and backward divides by n again (layer.rs, lines 40-52). Measured against the central-difference gradient of mse_loss on the 65 builds, the ratio is 65.0000 exactly. Training still converges, since every layer divides by the same n, but the learning rate you pass is the learning rate divided by the batch size.
  7. mse_gradient is not the gradient of mse_loss beyond one output column. mse_loss averages over rows and columns, mse_gradient divides by the rows only (loss.rs, lines 6-16). Measured for one to four columns, the ratio is 1, 2, 3, 4. Combined with finding 15, a two-output layer on 65 rows moves by the true gradient divided by 32.5.
  8. Dense::new takes no seed. It draws from the thread generator (layer.rs, line 27), so two layers built in the same run already differ and no Sequential result can be reproduced or regression-tested. Every other random algorithm in IX takes one — KMeans, RandomForest, Dropout, transformer::FeedForward. The weights and bias fields are public, which is the way out and what the course does.
  9. A Sequential of Dense layers is one affine map. Dense is the only type implementing the Layer trait: ix-nn has no activation layer that Sequential::push could accept between two affine maps. On exclusive or, two Dense layers converge to loss 0.25 — the variance of the targets, the best a constant can do — and predict 0.5 at all four corners, while the same-sized hand network with one sigmoid between its layers reaches 0. After 50 000 epochs the stack still satisfies f(0,0) + f(1,1) - f(0,1) - f(1,0) = 0 to the last bit: it is not undertrained, it cannot represent the function. The crate’s real non-linearities live in transformer::gelu, inside FeedForward, which works on Array3 and is not a Layer.
  10. minimize returns the starting point for a run that diverged. It tracks the best point it has passed and returns that (gradient.rs, lines 124-165). Plain descent at rate 0.01 on Rosenbrock runs to NaN on its first few steps; the result reports best f 24.200000, which is f at the point the caller passed in, with converged: false — the same flag a perfectly good run that merely ran out of iterations gets. Neither best_value nor converged separates the two.

Differences worth knowing, which I don’t count as errors:

  • RegressionStump puts the mean of the residuals in each leaf, where Friedman’s recipe and scikit-learn use a Newton step. It is a defensible simplification, and the hand version copies it so the two can be compared; with it, IX and the hand version agree on every one of the 38 test rows at 1, 5, 10, 25 and 50 rounds.
  • GradientBoostedClassifier has no early stopping and no held-out score. On the jobs it peaks at 0.9474 after five rounds and settles at 0.9211 by twenty-five, and nothing in the API measures that.
  • ix-ensemble has no out-of-bag score, which bagging gives away for free and scikit-learn exposes as oob_score=True.
  • ClosureObjective never overrides ObjectiveFunction::gradient, so wrapping a closure silently buys a measured gradient at two extra evaluations per coordinate per step — 1001 evaluations against 201 for 200 Adam steps in two dimensions. On a smooth objective it costs nothing in accuracy: both runs ended at 1.061e-16.

Worth recording next to the findings, because the lessons so far have mostly found the opposite:

  • ix_optimize’s SGD, Momentum and Adam match the hand versions exactly — same step counts (5000, 4129, 2822), same final points, same first step from the same gradient.
  • GradientBoostedClassifier matches the hand version on every test row at every number of rounds, and numpy reproduces the same accuracies, the same smoothed log priors and the same first-round stumps.
  • PCA’s five variances match Jacobi’s to 1.28e-10 and scikit-learn’s to the printed digits; only the ratio and the fixed starting vector are at issue.
  • The outline promises one lesson per family of algorithms, which will still leave most of the workspace untouched. scripts/sync-ix-api-map.mjs fills the gap from the other side: one git grep over the pinned commit, and a generated page per locale listing every pub declaration under crates/*/src/, crate by crate and module by module. 80 crates, 3398 declarations, of which 2307 are functions.
  • It is generated, never hand-written, and npm run sync:ix-api-map rebuilds it — so it cannot drift from the pin, which is what usually rots a page like this. The revision is read out of the course’s Cargo.toml rather than written twice.
  • It counts what the source says, not what a caller can reach: pub inside a private module is listed and is invisible from outside the crate. Finding 12 — a private field where the ratio is unusable — is exactly the kind of thing the page cannot tell you, and the lessons can.
  • The first CI run of the lesson 5 to 8 code was red on macos-latest alone, in l07_networks: the exclusive-or network printed predictions [-0.0000, 1.0000, 1.0000, -0.0000] where Windows and Linux print 0.0000 at those two corners. Run 35039180659; the other three jobs passed, and so did the fifteen other examples.
  • At the two zero corners the network lands below the fourth decimal, and only the sign differs — the same additions taken in a different order on ARM. -0.0 and 0.0 are the same number and compare equal, so the sign is not a result, and no lesson claim changes.
  • fmt_vec exists so that “the outputs don’t depend on how each OS prints the last digits”, which makes it where the fix belongs: anything that rounds to zero now prints 0.0000, never -0.0000, and every other sign survives. A unit test pins both halves. No expected/ file changed, so no lesson had to be requoted.
  • The 2026-09-14 note — fixed decimals, so the outputs are identical on the three systems — was right about the remedy and one case short of complete. Fixed decimals do not settle the sign of a zero, and a course written on one machine cannot find that out.

2026-09-24 — Five reducers, three inputs

Section titled “2026-09-24 — Five reducers, three inputs”
  • l09_reducers.rs ran on Windows with the pinned IX commit: a square for MDS, two rings for kernel PCA, and the 186-job CI data for NMF and LDA (first twelve standardized rows for t-SNE). Hand MDS and IX each reproduced the square’s six distances within 1e-10.
  • On the rings, the linear first-axis gap was 0.0000. The first RBF gap was also 0.0000; the fourth was 0.5798. This refutes the exploratory first-axis expectation. Eigenvector signs and rotations are arbitrary, so the comparison uses absolute ring-mean gaps and does not infer a class score.
  • Rank-two NMF reconstructed the raw timing matrix at MSE 0.505 seconds squared per cell and rejected standardized negative entries. LDA produced two finite axes for three OS labels, with no held-out classification claim. Seeded t-SNE produced a finite 12 × 2 arrangement but returned exactly the same arrangement when transform received an unrelated 1 × 1 input.
  • The numpy/scikit-learn 1.8.0 cross-check reproduced the square’s distances, the ring-axis outcome, finite NMF reconstruction, LDA’s two axes, and a finite t-SNE embedding. These are invariant checks, not claims of equal NMF factors or t-SNE coordinates across implementations. The hypotheses were not recorded before this run, so these are exploratory findings, not preregistered confirmations. Three-OS CI for lesson 9 is still pending.
  • The ix_ml_pipeline tool end to end: the scaling order of finding 1, the task inference of finding 2 on a CSV file, and the All rows contain NaN values error for a file with a text column. All three are read in the code, not run.
  • LinearRegression::fit on two identical features: whether ix_math::linalg::inverse returns an error, and fit panics, or returns a wrong answer.
  • A k-means cluster that empties in the middle of a real run, rather than at the start as in finding 4.
  • ix_voicings::cluster on GA’s voicings: the export needs GA’s FretboardVoicingsCLI, which the course doesn’t build.
  • The cause of the 48-second build of 95a3830.
  • Whether findings 1 to 9 are already known upstream: I haven’t searched IX’s issues.
  • The examples on Linux ARM runners: CI covers ubuntu-latest (x64), windows-latest and macos-latest (ARM) only.
  • Whether PCA ever returns unsorted variances on real data, rather than on the constructed cloud of finding 11.
  • Whether a Sequential deeper than two Dense layers is used anywhere in IX, where finding 18 would bite.
  • ix-autograd: its tape is the crate that should make finding 15 unnecessary, and lesson 12 will measure it.
  • Whether findings 10 to 19 are already known upstream: I still have not searched IX issues.
  • The API map counts pub declarations, not reachable ones; how far apart the two numbers are is unmeasured.
  • The values printed by a direct println!("{:.6}") rather than through fmt_vec — lesson 8’s intercept -0.000000 is one — carry the same signed-zero hazard and are not normalized. That one agreed on the three systems in run 35039180659; the others have not been enumerated.
  • Repeat lesson 9’s numeric snapshots on Linux and macOS CI; check whether IX’s NMF MSE and the RBF fourth-axis gap round identically there. Benchmark t-SNE’s scaling separately before assigning a runtime cost.