Classical ML algorithms
Distance functions, k-means and k-NN, Gaussian Naive Bayes, preprocessing transforms, evaluation metrics, and a bagged decision-stump random forest across seven independent kernel modules.
Seven small, self-contained modules for the classical ML operations that sit underneath (or alongside) the single-model pages elsewhere in this section — ARIMA, SVM, HMM, gradient boosting, and the rest. Where those pages are one estimator each, this page is the toolbox: distance metrics, k-means and k-nearest-neighbors, a from-scratch Gaussian Naive Bayes classifier, one-shot feature preprocessing (scaling, one-hot, polynomial features, train/test split), the point-prediction metrics almost every other module calls to grade itself, and a bootstrap-aggregated ensemble of single-split decision stumps standing in for a random forest. None of the seven import Python or share code with each other — each module implements its own copy of anything it needs, including distance functions duplicated verbatim across three of them.
from algenta import MojoRuntime
runtime = MojoRuntime(mode="local")
runtime.execute("ml.knn", "euclidean_distance_knn", [[0.0, 0.0], [3.0, 4.0]])["result"]
# 5.0 — the classic 3-4-5 right triangle
runtime.execute("ml.metrics", "precision_recall_f1",
[[1, 1, 0, 0, 1], [1, 0, 0, 1, 1]])["result"]
# {"precision": 0.6666666666666666, "recall": 0.6666666666666666, "f1": 0.6666666666666666}The modules
ml.classifiers
Euclidean/Manhattan/cosine distance, logistic sigmoid and softmax, a majority-vote k-NN predictor, and a k-means (kmeans_cluster) that raises a catchable error on ragged input instead of aborting.
ml.clustering
A second, independent k-means (kmeans) returning a full KMeansResult (centroids, labels, inertia, iteration count), plus assign_cluster and silhouette_score.
ml.knn
The full k-NN toolbox: batched and distance-weighted classification, true mean-based regression, leave-one-out cross-validation, and optimal_k search.
ml.metrics
Accuracy, MSE/RMSE/MAE/R², an n×n confusion matrix, and binary precision/recall/F1 over Float64/Int label lists.
ml.naive_bayes
A Gaussian Naive Bayes classifier (fit, predict, batch predict) with its own priors, accuracy, confusion matrix, and per-class precision/recall/F1.
ml.preprocessing
Stateless one-shot transforms: min-max, z-score and median-IQR scaling, one-hot encoding, polynomial features, and a deterministic train/test split.
ml.random_forest
Bootstrap aggregation of single-split decision stumps: bagging, feature subsampling, out-of-bag R², permutation feature importance, and cross-tree prediction variance.
Distance functions, duplicated on purpose
Each module is self-contained, so the same handful of distance functions exist more than once rather than being imported from a shared location:
euclidean_distanceis defined identically inml.classifiersandml.clustering;ml.knnhas the same body under the nameeuclidean_distance_knn(renamed to avoid a symbol clash with its own file, not because it behaves differently).manhattan_distanceis defined identically inml.classifiersandml.knn.
All three implementations take min(len(a), len(b)) as the comparison length rather than requiring equal-length vectors, so a longer vector's trailing elements are silently ignored instead of raising.
Two k-means implementations, one guarded
ml.classifiers.kmeans_cluster(data, k, max_iter=100) and ml.clustering.kmeans(data, k, max_iter=100, tol=1e-6) are independent implementations of Lloyd's algorithm with random restart of empty clusters, not a shared core with two entry points, and they diverge in an important way:
kmeans_clusterreturns onlyList[Int]labels andraises a catchableErrorthe moment any point after the first has a different dimensionality thandata[0]. The source comment is explicit about why: a bounds-check abort here would kill the whole worker process, "the exact same bug shape asembeddings.centroid," so — unlike this same module's lenient min-length distance functions above — it is "fixed in place rather than given a_strictsibling."kmeansreturns a fullKMeansResult(centroids, labels, inertia, iterations) and adds a convergence tolerance, but has no equivalent length check. A raggeddatalist handed to it directly hits the same uncaught bounds violation thatkmeans_clusterwas fixed to avoid — validate that every row shareslen(data[0])before callingml.clustering.kmeans, or callml.classifiers.kmeans_clusterinstead if labels alone are enough.
Neither function takes a seed. Both draw initial centroids from random_float64() with no parameter to fix it, so two calls on the same data can converge to differently-numbered (though not necessarily differently-shaped) clusters. ml.random_forest.bootstrap_indices and feature_subsample share the same property and the same reason — every bootstrap sample and every feature subset is freshly unseeded.
Worked examples
Every number below was traced by hand against the real algorithm in mojo/engine/ml/*.mojo, not lifted from the module's own test file — see Verification.
k-NN classification. Two well-separated clusters:
Silhouette score. Two 1-D clusters, {0.0, 1.0} and {10.0, 11.0}:
Per-point coefficients are [0.9047619047619048, 0.8947368421052632, 0.8947368421052632, 0.9047619047619048] — each point's same-cluster neighbor is 1.0 away while the nearer of the two other-cluster points is roughly 9.5–10.5 away, so every point scores close to the 1.0 ceiling of a cleanly separated pair of clusters.
Gaussian Naive Bayes. One feature, two classes centered at 1.5 and 10.5:
Preprocessing. The textbook eight-point dataset used to teach sample standard deviation (mean 5, sample stdev ≈2.138):
normalize_zscore uses math_ext.stdev, which is the sample standard deviation (divides by n - 1), not the population standard deviation sklearn.preprocessing.StandardScaler uses by default (divides by n) — porting a pipeline that assumes StandardScaler's exact output will need to account for that difference, especially at small n.
train_test_split computes n_test = Int(Float64(n) * test_ratio), truncating toward zero rather than rounding. 10 * 0.3 happens to round to exactly 3.0 in Float64, giving the clean 7/3 split above, but Int() truncation means a test_ratio that lands a hair below an integer boundary in binary floating point (rather than in the reals) will silently produce one fewer test row than the ratio implies — there is no rounding step to correct for it.
A random forest is a bag of single-split stumps. tree_predict_simple tries five candidate thresholds per feature (fmin + t/6 * (fmax - fmin) for t in 1..5) and keeps whichever split reduces total sum-of-squared-error the most, with ties going to the first threshold tried:
Thresholds at t=2 (4.0), t=3 (5.5) and t=4 (7.0) all produce the identical {1,2,3}/{8,9,10} partition and tie at the maximum SSE reduction (73.5); because the comparison is strict >, the first one tried (4.0) wins. random_forest_predict averages many such stumps, each over a fresh unseeded bootstrap sample and feature subset — a single stump is deterministic given its inputs, but the forest built around it is not, run to run.
Two accuracy/confusion-matrix implementations
ml.metrics and ml.naive_bayes each ship their own accuracy and confusion_matrix rather than sharing one:
ml.metrics.accuracy(predicted: List[Float64], actual: List[Float64])compares by rounding each value to the nearest integer (Int(x + 0.5)), so it works on raw class-id floats or on rounded regression output.ml.naive_bayes.accuracy(predicted: List[Int], actual: List[Int])comparesInts directly, no rounding.Both modules'
confusion_matrix(predicted, actual, n_classes)takeIntlabels and are byte-for-byte the same algorithm (matrix[actual][predicted] += 1, out-of-range labels ignored).
All four — plus mse, mae, r2_score, and precision_recall_f1/precision_recall — take min(len(predicted), len(actual)) as the comparison length. A length mismatch is never an error here; the longer list's extra entries are dropped silently.
Refusal behavior
Several functions guard explicitly against a length mismatch that would otherwise index past a list's end — an uncatchable, worker-killing bounds abort in this runtime rather than a normal exception — and return a defined sentinel instead:
ml.knn.knn_predict,knn_predict_batch,weighted_knn_predict, andknn_regressionall return0/0.0iflen(y_train) == 0orlen(y_train) != len(X_train).ml.knn.leave_one_out_cvandoptimal_kreturn0.0/0iflen(y) != len(X).ml.naive_bayes.gaussian_nb_fitreturns an empty result iflen(y) != len(X);gaussian_nb_predictreturns0ifpriorsis shorter than the number of classes, or if any class's fittedparamsrow is narrower than2 * len(x).ml.preprocessing.train_test_splitreturns an emptySplitResultiflen(labels) != len(data).ml.random_forest.tree_predict_simplehas the most thorough guard of the seven modules: it checksy_train/x_testlength againstX_train, every row ofX_trainfor raggedness,isnan/isinfon everyy_trainvalue, and everyfeature_subsetindex for range — all before touching the data.random_forest_predictandoob_scorerefuse the same way ify_train/yis short, or ifn_trees <= 0.
The one module without this guard is ml.clustering.kmeans, covered above.
ml.naive_bayes.gaussian_log_likelihood floors std at MIN_STD = 1e-9 before dividing by it (and gaussian_nb_fit applies the same floor to any class/feature with zero fitted variance), so a class with a single training sample, or a constant feature, never divides by zero or takes log(0).
Verification
The seven mojo/tests/libs/test_ml_*.mojo files behind this module family (test_ml_classifiers.mojo, test_ml_clustering.mojo, test_ml_knn.mojo, test_ml_metrics.mojo, test_ml_naive_bayes.mojo, test_ml_preprocessing.mojo, test_ml_random_forest.mojo) are auto-generated coverage smoke tests: each calls every function once on a shared, arbitrary fixture vector and checks only that the result is finite (for scalars) or non-empty (for lists and structs) — they do not pin specific numeric outputs. The worked examples above were hand-traced against the real formulas in mojo/engine/ml/classifiers.mojo, clustering.mojo, knn.mojo, metrics.mojo, naive_bayes.mojo, preprocessing.mojo, and random_forest.mojo, and every number given can be reproduced by re-running the same arithmetic.
Next: compare with fit/transform pipelines for stateful, invertible preprocessing, or model ensembling for combining predictions from arbitrary base models instead of bagging decision stumps.
Last updated
Was this helpful?