Machine Learning
Four classic ML algorithms built from Nx primitives: SVD, broadcasting, reductions, and scalar loops.
| File | Algorithm | Key Nx operations |
|---|---|---|
pca.ml |
PCA | svd, mean, matmul, cumsum |
kmeans.ml |
K-Means | broadcasting, argmin, categorical, sq_distances |
dbscan.ml |
DBSCAN | pairwise distances, less_equal_s, boolean item in BFS |
tsne.ml |
t-SNE | exp, log, Student-t kernel, momentum gradient descent |
Running
dune exec nx/examples/10-machine-learning/pca.exe
dune exec nx/examples/10-machine-learning/kmeans.exe
dune exec nx/examples/10-machine-learning/dbscan.exe
dune exec nx/examples/10-machine-learning/tsne.exe
dbscan.ml
(** DBSCAN density-based clustering.
Generate two dense clusters with scattered noise, find clusters using
neighbourhood density, and report cluster sizes and noise count. *)
open Nx
let () =
let eps = 1.5 in
let min_samples = 5 in
(* Two tight blobs plus uniform noise *)
let c1 = add_s (mul_s (randn Float64 [| 80; 2 |]) 0.6) 3.0 in
let c2 = sub_s (mul_s (randn Float64 [| 80; 2 |]) 0.6) 3.0 in
let noise = sub_s (mul_s (rand Float64 [| 20; 2 |]) 14.0) 7.0 in
let data = concatenate ~axis:0 [ c1; c2; noise ] in
let n = (shape data).(0) in
Printf.printf "Data: %d points (eps=%.1f, min_samples=%d)\n\n" n eps
min_samples;
(* Pairwise Euclidean distance matrix [n, n] *)
let diff = sub (expand_dims [ 1 ] data) (expand_dims [ 0 ] data) in
let dist = sqrt (sum ~axes:[ 2 ] (square diff)) in
(* Neighbour adjacency and core-point mask *)
let neighbours = less_equal_s dist eps in
let counts = sum ~axes:[ 1 ] (cast Float64 neighbours) in
let core = greater_equal_s counts (Float.of_int min_samples) in
(* BFS cluster expansion *)
let labels = Array.make n (-1) in
let cluster_id = ref 0 in
for i = 0 to n - 1 do
if labels.(i) = -1 && item [ i ] core then begin
let c = !cluster_id in
incr cluster_id;
labels.(i) <- c;
let q = Queue.create () in
Queue.push i q;
while not (Queue.is_empty q) do
let p = Queue.pop q in
for j = 0 to n - 1 do
if labels.(j) = -1 && item [ p; j ] neighbours then begin
labels.(j) <- c;
if item [ j ] core then Queue.push j q
end
done
done
end
done;
let n_clusters = !cluster_id in
let n_noise =
Array.fold_left (fun acc l -> if l = -1 then acc + 1 else acc) 0 labels
in
Printf.printf "Clusters found: %d\n" n_clusters;
Printf.printf "Noise points: %d\n\n" n_noise;
for c = 0 to n_clusters - 1 do
let count =
Array.fold_left (fun acc l -> if l = c then acc + 1 else acc) 0 labels
in
Printf.printf " Cluster %d: %d points\n" c count
done
kmeans.ml
(** K-means clustering with kmeans++ initialisation.
Generate synthetic blobs, cluster them with Lloyd's algorithm, and report
centroid positions and inertia. *)
open Nx
(* Pairwise squared L2 distances: [n, d] x [k, d] -> [n, k] *)
let sq_distances a b =
sum ~axes:[ 2 ] (square (sub (expand_dims [ 1 ] a) (expand_dims [ 0 ] b)))
(* Isotropic Gaussian blobs around given centres. *)
let make_blobs ~samples_per_cluster centers =
let d = (shape centers).(1) in
let blobs =
List.init
(shape centers).(0)
(fun c ->
add (randn Float64 [| samples_per_cluster; d |]) (get [ c ] centers))
in
shuffle (concatenate ~axis:0 blobs)
(* Kmeans++ initialisation: pick k centres from data. *)
let kmeanspp data k =
let n = (shape data).(0) in
let d = (shape data).(1) in
let centroids = zeros Float64 [| k; d |] in
let idx = Int32.to_int (item [] (randint Int32 ~high:n [||] 0)) in
set [ 0 ] centroids (get [ idx ] data);
for c = 1 to k - 1 do
let current = slice [ R (0, c); A ] centroids in
let min_d = min ~axes:[ 1 ] (sq_distances data current) in
let chosen =
Int32.to_int (item [] (categorical (log (clamp ~min:1e-30 min_d))))
in
set [ c ] centroids (get [ chosen ] data)
done;
centroids
let () =
let true_centers =
create Float64 [| 3; 2 |] [| 0.0; 0.0; 7.0; 7.0; -5.0; 10.0 |]
in
let data = make_blobs ~samples_per_cluster:100 true_centers in
let n = (shape data).(0) in
let d = (shape data).(1) in
let k = 3 in
Printf.printf "Data: %d points, %d features, %d clusters\n\n" n d k;
let centroids = kmeanspp data k in
let labels = ref (zeros Int32 [| n |]) in
let max_iter = 100 in
let tol = 1e-6 in
let converged = ref false in
let iter = ref 0 in
while !iter < max_iter && not !converged do
labels := argmin ~axis:1 (sq_distances data centroids);
let old = copy centroids in
for c = 0 to k - 1 do
let mask = cast Float64 (equal !labels (scalar Int32 (Int32.of_int c))) in
let count = item [] (sum mask) in
if count > 0.0 then begin
let total = sum ~axes:[ 0 ] (mul data (expand_dims [ 1 ] mask)) in
set [ c ] centroids (div_s total count)
end
done;
let shift = item [] (max (abs (sub centroids old))) in
converged := shift < tol;
incr iter
done;
Printf.printf "Converged after %d iterations\n\n" !iter;
Printf.printf "Centroids:\n%s\n" (data_to_string centroids);
for c = 0 to k - 1 do
let count =
item []
(sum (cast Float64 (equal !labels (scalar Int32 (Int32.of_int c)))))
in
Printf.printf " Cluster %d: %.0f points\n" c count
done;
let inertia = item [] (sum (min ~axes:[ 1 ] (sq_distances data centroids))) in
Printf.printf "\nInertia: %.2f\n" inertia
pca.ml
(** Principal component analysis via SVD.
Generate synthetic data with known structure, project to lower dimensions,
and verify the explained variance captures the signal. *)
open Nx
open Nx.Infix
let () =
(* 200 points in 5D: most variance along axes 0 and 1 *)
let n = 200 in
let scale = create Float64 [| 1; 5 |] [| 10.0; 5.0; 1.0; 1.0; 1.0 |] in
let data = randn Float64 [| n; 5 |] * scale in
Printf.printf "Data shape: [%d; %d]\n\n" (shape data).(0) (shape data).(1);
(* Center *)
let mu = mean ~axes:[ 0 ] ~keepdims:true data in
let centered = data - mu in
(* Economy SVD: centered = U diag(S) Vt *)
let _u, s, vt = svd ~full_matrices:false centered in
(* Explained variance ratio: s_i^2 / sum(s^2) *)
let s2 = square s in
let ratios = s2 /$ item [] (sum s2) in
Printf.printf "Singular values: %s\n" (data_to_string s);
Printf.printf "Explained variance ratio: %s\n" (data_to_string ratios);
Printf.printf "Cumulative: %s\n\n"
(data_to_string (cumsum ratios));
(* Project to 2 components *)
let n_components = 2 in
let components = slice [ R (0, n_components); A ] vt in
let projected = matmul centered (matrix_transpose components) in
Printf.printf "Projected shape: [%d; %d]\n"
(shape projected).(0)
(shape projected).(1);
(* Reconstruct and measure error *)
let reconstructed = matmul projected components + mu in
let rmse = sqrt (mean (square (data - reconstructed))) in
Printf.printf "Reconstruction RMSE (2 of 5 components): %.4f\n" (item [] rmse)
tsne.ml
(** t-SNE dimensionality reduction.
Embed three 10-dimensional clusters into 2D using the exact t-SNE algorithm.
Reports KL divergence and per-cluster spread. *)
open Nx
(* Pairwise squared distances: [n, d] -> [n, n] *)
let pairwise_sq data =
let diff = sub (expand_dims [ 1 ] data) (expand_dims [ 0 ] data) in
sum ~axes:[ 2 ] (square diff)
(* Off-diagonal mask: 1 everywhere except the diagonal. *)
let off_diag n =
sub (full Float64 [| n; n |] 1.0) (cast Float64 (eye Float64 n))
(* Compute symmetric P matrix via binary search for each row's bandwidth. *)
let compute_p dist_sq ~perplexity =
let n = (shape dist_sq).(0) in
let target = Float.log perplexity in
let p = zeros Float64 [| n; n |] in
for i = 0 to n - 1 do
let di = get [ i ] dist_sq in
let lo = ref 1e-10 in
let hi = ref 1e4 in
let row = ref (zeros Float64 [| n |]) in
for _ = 0 to 50 do
let sigma = (!lo +. !hi) /. 2.0 in
let beta = 1.0 /. (2.0 *. sigma *. sigma) in
let pi = exp (mul_s di (-.beta)) in
set_item [ i ] 0.0 pi;
let s = item [] (sum pi) in
let pi = div_s pi (Float.max s 1e-30) in
let h = -.item [] (sum (mul pi (log (clamp ~min:1e-30 pi)))) in
row := pi;
if h > target then hi := sigma else lo := sigma
done;
set [ i ] p !row
done;
(* Symmetrise: P = (P + P^T) / (2n) *)
let p = div_s (add p (matrix_transpose p)) (2.0 *. Float.of_int n) in
clamp ~min:1e-12 p
let () =
let n_per = 50 in
let dim = 10 in
let perplexity = 20.0 in
let max_iter = 500 in
let lr = 100.0 in
(* Three well-separated clusters in 10D *)
let c0 = randn Float64 [| n_per; dim |] in
let c1 = add_s (randn Float64 [| n_per; dim |]) 8.0 in
let c2 = sub_s (randn Float64 [| n_per; dim |]) 8.0 in
let data = concatenate ~axis:0 [ c0; c1; c2 ] in
let n = (shape data).(0) in
Printf.printf "Data: %d points in %dD, perplexity=%.0f\n\n" n dim perplexity;
let dist_sq = pairwise_sq data in
let p = compute_p dist_sq ~perplexity in
let y = ref (mul_s (randn Float64 [| n; 2 |]) 1e-4) in
let vel = ref (zeros Float64 [| n; 2 |]) in
let mask = off_diag n in
for iter = 1 to max_iter do
let y_diff = sub (expand_dims [ 1 ] !y) (expand_dims [ 0 ] !y) in
let y_dsq = sum ~axes:[ 2 ] (square y_diff) in
let inv_d = mul (div (scalar Float64 1.0) (add_s y_dsq 1.0)) mask in
let q_sum = Float.max (item [] (sum inv_d)) 1e-30 in
let q = clamp ~min:1e-12 (div_s inv_d q_sum) in
let p_eff = if iter <= 100 then mul_s p 4.0 else p in
(* Gradient: 4 sum_j (p_ij - q_ij)(y_i - y_j)(1+||y_i-y_j||^2)^{-1} *)
let mult = mul (sub p_eff q) inv_d in
let grad =
mul_s (sum ~axes:[ 1 ] (mul (expand_dims [ 2 ] mult) y_diff)) 4.0
in
let momentum = if iter <= 100 then 0.5 else 0.8 in
vel := sub (mul_s !vel momentum) (mul_s grad lr);
y := add !y !vel;
if iter = 1 || iter mod 100 = 0 then begin
let kl = item [] (sum (mul p (log (div p q)))) in
Printf.printf " iter %4d KL = %.4f\n" iter kl
end
done;
Printf.printf "\nPer-cluster spread (mean std of embedded coordinates):\n";
for c = 0 to 2 do
let lo = c * n_per in
let cluster = slice [ R (lo, lo + n_per); A ] !y in
let sx = item [] (mean (std ~axes:[ 0 ] cluster)) in
Printf.printf " Cluster %d: %.4f\n" c sx
done