From ce8a815796dd2215ac5e630f5969786bfa6c0581 Mon Sep 17 00:00:00 2001 From: Lorenz Schmidt Date: Wed, 28 Apr 2021 22:46:46 +0200 Subject: [PATCH] Linfa's 0.4.0 release (#127) * Bump version 0.4.0, add release notes * Add t-SNE example to release notes * Add example for t-SNE and start writing preprocessing example * Fix typo * Add example for TF-IDF text preprocessing * Explain that target construction is omitted and traits changes * Add contribute section on the new lapack traits * Link to crates.io in the base crate documentation * Link to crates.io in the base crate documentation * Add mnist example * Run rustfmt * Fix mnist example --- CONTRIBUTE.md | 12 ++ Cargo.toml | 2 +- algorithms/linfa-bayes/Cargo.toml | 6 +- algorithms/linfa-clustering/Cargo.toml | 5 +- .../src/gaussian_mixture/algorithm.rs | 2 - algorithms/linfa-elasticnet/Cargo.toml | 6 +- algorithms/linfa-hierarchical/Cargo.toml | 8 +- algorithms/linfa-ica/Cargo.toml | 4 +- algorithms/linfa-kernel/Cargo.toml | 4 +- algorithms/linfa-linear/Cargo.toml | 6 +- algorithms/linfa-logistic/Cargo.toml | 6 +- algorithms/linfa-pls/Cargo.toml | 6 +- algorithms/linfa-pls/README.md | 27 +++ algorithms/linfa-preprocessing/Cargo.toml | 8 +- algorithms/linfa-reduction/Cargo.toml | 8 +- algorithms/linfa-svm/Cargo.toml | 8 +- algorithms/linfa-trees/Cargo.toml | 6 +- algorithms/linfa-tsne/Cargo.toml | 11 +- algorithms/linfa-tsne/examples/mnist.rs | 60 ++++++ .../{iris_plot.plt => mnist_plot.plt} | 0 datasets/Cargo.toml | 4 +- docs/website/content/news/release040/index.md | 177 ++++++++++++++++++ docs/website/content/news/release040/tsne.png | Bin 0 -> 48732 bytes docs/website/content/snippets/multi-class.md | 20 ++ src/lib.rs | 6 +- 25 files changed, 349 insertions(+), 53 deletions(-) create mode 100644 algorithms/linfa-pls/README.md create mode 100644 algorithms/linfa-tsne/examples/mnist.rs rename algorithms/linfa-tsne/examples/{iris_plot.plt => mnist_plot.plt} (100%) create mode 100644 docs/website/content/news/release040/index.md create mode 100644 docs/website/content/news/release040/tsne.png create mode 100644 docs/website/content/snippets/multi-class.md diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index 06d5621fa..2f4598c33 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -128,3 +128,15 @@ fn main() { /// ... } ``` + +## Use the lapack trait bound + +When you want to implement an algorithm which requires the [Lapack](https://docs.rs/ndarray-linalg/0.13.1/ndarray_linalg/types/trait.Lapack.html) bound, then you could add the trait bound to the `linfa::Float` standard bound, e.g. `F: Float + Scalar + Lapack`. If you do that you're currently running into conflicting function definitions of [num_traits::Float](https://docs.rs/num-traits/0.2.14/num_traits/float/trait.Float.html) and [cauchy::Scalar](https://docs.rs/cauchy/0.4.0/cauchy/trait.Scalar.html) with the first defined for real-valued values and the second for complex values. + +If you want to avoid that you can use the `linfa::dataset::{WithLapack, WithoutLapack}` traits, which basically adds the lapack trait bound for a block and then removes it again so that the conflicts can be avoided. For example: +```rust +let decomp = covariance.with_lapack().cholesky(UPLO::Lower)?; +let sol = decomp + .solve_triangular(UPLO::Lower, Diag::NonUnit, &Array::eye(n_features))? + .without_lapack(); +``` diff --git a/Cargo.toml b/Cargo.toml index de8686d3a..6788607bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa" -version = "0.3.1" +version = "0.4.0" authors = [ "Luca Palmieri ", "Lorenz Schmidt ", diff --git a/algorithms/linfa-bayes/Cargo.toml b/algorithms/linfa-bayes/Cargo.toml index 4d904db43..d1001ae4b 100644 --- a/algorithms/linfa-bayes/Cargo.toml +++ b/algorithms/linfa-bayes/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-bayes" -version = "0.3.1" +version = "0.4.0" authors = ["VasanthakumarV "] description = "Collection of Naive Bayes Algorithms" edition = "2018" @@ -15,8 +15,8 @@ ndarray = { version = "0.14" , features = ["blas", "approx"]} ndarray-stats = "0.4" thiserror = "1" -linfa = { version = "0.3.1", path = "../.." } +linfa = { version = "0.4.0", path = "../.." } [dev-dependencies] approx = "0.4" -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["winequality"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["winequality"] } diff --git a/algorithms/linfa-clustering/Cargo.toml b/algorithms/linfa-clustering/Cargo.toml index bd8d38005..23299340f 100644 --- a/algorithms/linfa-clustering/Cargo.toml +++ b/algorithms/linfa-clustering/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-clustering" -version = "0.3.1" +version = "0.4.0" edition = "2018" authors = [ "Luca Palmieri ", @@ -36,7 +36,8 @@ num-traits = "0.2" rand_isaac = "0.3" thiserror = "1" partitions = "0.2.4" -linfa = { version = "0.3.1", path = "../..", features = ["ndarray-linalg"] } + +linfa = { version = "0.4.0", path = "../..", features = ["ndarray-linalg"] } [dev-dependencies] ndarray-npy = { version = "0.7", default-features = false } diff --git a/algorithms/linfa-clustering/src/gaussian_mixture/algorithm.rs b/algorithms/linfa-clustering/src/gaussian_mixture/algorithm.rs index 2388d3006..4cadb3bc7 100644 --- a/algorithms/linfa-clustering/src/gaussian_mixture/algorithm.rs +++ b/algorithms/linfa-clustering/src/gaussian_mixture/algorithm.rs @@ -257,8 +257,6 @@ impl GaussianMixtureModel { let n_features = covariances.shape()[1]; let mut precisions_chol = Array::zeros((n_clusters, n_features, n_features)); for (k, covariance) in covariances.outer_iter().enumerate() { - dbg!(&covariance.shape()); - dbg!(&covariance.with_lapack().shape()); let decomp = covariance.with_lapack().cholesky(UPLO::Lower)?; let sol = decomp .solve_triangular(UPLO::Lower, Diag::NonUnit, &Array::eye(n_features))? diff --git a/algorithms/linfa-elasticnet/Cargo.toml b/algorithms/linfa-elasticnet/Cargo.toml index 912e82d26..c86ffc84b 100644 --- a/algorithms/linfa-elasticnet/Cargo.toml +++ b/algorithms/linfa-elasticnet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-elasticnet" -version = "0.3.1" +version = "0.4.0" authors = [ "Paul Körbitz / Google ", "Lorenz Schmidt " @@ -35,9 +35,9 @@ num-traits = "0.2" approx = "0.4" thiserror = "1" -linfa = { version = "0.3.1", path = "../.." } +linfa = { version = "0.4.0", path = "../.." } [dev-dependencies] -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["diabetes"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["diabetes"] } ndarray-rand = "0.13" rand_isaac = "0.3" diff --git a/algorithms/linfa-hierarchical/Cargo.toml b/algorithms/linfa-hierarchical/Cargo.toml index da8443321..35c05b307 100644 --- a/algorithms/linfa-hierarchical/Cargo.toml +++ b/algorithms/linfa-hierarchical/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-hierarchical" -version = "0.3.1" +version = "0.4.0" authors = ["Lorenz Schmidt "] edition = "2018" @@ -17,10 +17,10 @@ categories = ["algorithms", "mathematics", "science"] ndarray = { version = "0.14", default-features = false } kodama = "0.2" -linfa = { version = "0.3.1", path = "../.." } -linfa-kernel = { version = "0.3.1", path = "../linfa-kernel" } +linfa = { version = "0.4.0", path = "../.." } +linfa-kernel = { version = "0.4.0", path = "../linfa-kernel" } [dev-dependencies] rand = "0.8" ndarray-rand = "0.13" -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["iris"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["iris"] } diff --git a/algorithms/linfa-ica/Cargo.toml b/algorithms/linfa-ica/Cargo.toml index 4eb0f7290..6a77ce37b 100644 --- a/algorithms/linfa-ica/Cargo.toml +++ b/algorithms/linfa-ica/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-ica" -version = "0.3.1" +version = "0.4.0" authors = ["VasanthakumarV "] description = "A collection of Independent Component Analysis (ICA) algorithms" edition = "2018" @@ -32,7 +32,7 @@ num-traits = "0.2" rand_isaac = "0.3" thiserror = "1" -linfa = { version = "0.3.1", path = "../..", features = ["ndarray-linalg"] } +linfa = { version = "0.4.0", path = "../..", features = ["ndarray-linalg"] } [dev-dependencies] ndarray-npy = { version = "0.7", default-features = false } diff --git a/algorithms/linfa-kernel/Cargo.toml b/algorithms/linfa-kernel/Cargo.toml index da6dbdec8..7e25c3719 100644 --- a/algorithms/linfa-kernel/Cargo.toml +++ b/algorithms/linfa-kernel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-kernel" -version = "0.3.1" +version = "0.4.0" authors = ["Lorenz Schmidt "] description = "Kernel methods for non-linear algorithms" edition = "2018" @@ -30,4 +30,4 @@ sprs = { version="0.9.4", default-features = false } hnsw = "0.6" space = "0.10" -linfa = { version = "0.3.1", path = "../.." } +linfa = { version = "0.4.0", path = "../.." } diff --git a/algorithms/linfa-linear/Cargo.toml b/algorithms/linfa-linear/Cargo.toml index a5fefe8c8..1aecf7357 100644 --- a/algorithms/linfa-linear/Cargo.toml +++ b/algorithms/linfa-linear/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-linear" -version = "0.3.1" +version = "0.4.0" authors = [ "Paul Körbitz / Google ", "VasanthakumarV " @@ -25,8 +25,8 @@ argmin = { version = "0.4", features = ["ndarrayl"] } serde = { version = "1.0", default-features = false, features = ["derive"] } thiserror = "1" -linfa = { version = "0.3.1", path = "../.." } +linfa = { version = "0.4.0", path = "../.." } [dev-dependencies] -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["diabetes"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["diabetes"] } approx = "0.4" diff --git a/algorithms/linfa-logistic/Cargo.toml b/algorithms/linfa-logistic/Cargo.toml index 495533637..2064e8eb0 100644 --- a/algorithms/linfa-logistic/Cargo.toml +++ b/algorithms/linfa-logistic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-logistic" -version = "0.3.1" +version = "0.4.0" authors = ["Paul Körbitz / Google "] description = "A Machine Learning framework for Rust" @@ -21,8 +21,8 @@ argmin = { version = "0.4", features = ["ndarrayl"] } serde = "1.0" thiserror = "1" -linfa = { version = "0.3.1", path = "../.." } +linfa = { version = "0.4.0", path = "../.." } [dev-dependencies] approx = "0.4" -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["winequality"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["winequality"] } diff --git a/algorithms/linfa-pls/Cargo.toml b/algorithms/linfa-pls/Cargo.toml index 1221010c1..e0a96b99a 100644 --- a/algorithms/linfa-pls/Cargo.toml +++ b/algorithms/linfa-pls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-pls" -version = "0.3.1" +version = "0.4.0" edition = "2018" authors = ["relf "] description = "Partial Least Squares family methods" @@ -32,9 +32,9 @@ rand_isaac = "0.3" num-traits = "0.2" paste = "1.0" thiserror = "1" -linfa = { version = "0.3.1", path = "../..", features = ["ndarray-linalg"] } +linfa = { version = "0.4.0", path = "../..", features = ["ndarray-linalg"] } [dev-dependencies] -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["linnerud"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["linnerud"] } rand_isaac = "0.3" approx = "0.4" diff --git a/algorithms/linfa-pls/README.md b/algorithms/linfa-pls/README.md new file mode 100644 index 000000000..19b042ed5 --- /dev/null +++ b/algorithms/linfa-pls/README.md @@ -0,0 +1,27 @@ +# Partial Least Squares + +`linfa-pls` provides a pure Rust implementation of the partial least squares algorithm family. + +## The Big Picture + +`linfa-pls` is a crate in the [`linfa`](https://crates.io/crates/linfa) ecosystem, an effort to create a toolkit for classical Machine Learning implemented in pure Rust, akin to Python's `scikit-learn`. + +## Current state + +`linfa-pls` currently provides an implementation of the following methods: + + - Partial Least Squares + +## Examples + +There is an usage example in the `examples/` directory. The example uses a BLAS backend, to run it and use the `intel-mkl` library do: + +```bash +$ cargo run --example pls_regression --features linfa/intel-mkl-system +``` + +## License +Dual-licensed to be compatible with the Rust project. + +Licensed under the Apache License, Version 2.0 or the MIT license , at your option. This file may not be copied, modified, or distributed except according to those terms. + diff --git a/algorithms/linfa-preprocessing/Cargo.toml b/algorithms/linfa-preprocessing/Cargo.toml index 894b29958..8cfea99db 100644 --- a/algorithms/linfa-preprocessing/Cargo.toml +++ b/algorithms/linfa-preprocessing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-preprocessing" -version = "0.3.1" +version = "0.4.0" authors = ["Sauro98 "] description = "A Machine Learning framework for Rust" @@ -17,7 +17,7 @@ categories = ["algorithms", "mathematics", "science"] [dependencies] -linfa = { version = "0.3.1", path = "../..", features = ["ndarray-linalg"] } +linfa = { version = "0.4.0", path = "../..", features = ["ndarray-linalg"] } ndarray = { version = "0.14", default-features = false, features = ["approx", "blas"] } ndarray-linalg = { version = "0.13" } ndarray-stats = "0.4" @@ -30,8 +30,8 @@ encoding = "0.2" sprs = { version="0.9.4", default-features = false } [dev-dependencies] -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["diabetes", "winequality"] } -linfa-bayes = { version = "0.3.1", path = "../linfa-bayes" } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["diabetes", "winequality"] } +linfa-bayes = { version = "0.4.0", path = "../linfa-bayes" } iai = "0.1" curl = "0.4.35" flate2 = "1.0.20" diff --git a/algorithms/linfa-reduction/Cargo.toml b/algorithms/linfa-reduction/Cargo.toml index 16e8f0a0d..bdf10ea27 100644 --- a/algorithms/linfa-reduction/Cargo.toml +++ b/algorithms/linfa-reduction/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-reduction" -version = "0.3.1" +version = "0.4.0" authors = ["Lorenz Schmidt "] description = "A collection of dimensionality reduction techniques" edition = "2018" @@ -31,11 +31,11 @@ ndarray-rand = "0.13" num-traits = "0.2" thiserror = "1" -linfa = { version = "0.3.1", path = "../..", features = ["ndarray-linalg"] } -linfa-kernel = { version = "0.3.1", path = "../linfa-kernel" } +linfa = { version = "0.4.0", path = "../..", features = ["ndarray-linalg"] } +linfa-kernel = { version = "0.4.0", path = "../linfa-kernel" } [dev-dependencies] rand = { version = "0.8", features = ["small_rng"] } ndarray-npy = { version = "0.7", default-features = false } -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["iris"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["iris"] } approx = { version = "0.4", default-features = false, features = ["std"] } diff --git a/algorithms/linfa-svm/Cargo.toml b/algorithms/linfa-svm/Cargo.toml index 2c41cdd4b..68790bc7e 100644 --- a/algorithms/linfa-svm/Cargo.toml +++ b/algorithms/linfa-svm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-svm" -version = "0.3.1" +version = "0.4.0" edition = "2018" authors = ["Lorenz Schmidt "] description = "Support Vector Machines" @@ -29,9 +29,9 @@ ndarray-rand = "0.13" num-traits = "0.2" thiserror = "1" -linfa = { version = "0.3.1", path = "../.." } -linfa-kernel = { version = "0.3.1", path = "../linfa-kernel" } +linfa = { version = "0.4.0", path = "../.." } +linfa-kernel = { version = "0.4.0", path = "../linfa-kernel" } [dev-dependencies] -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["winequality", "diabetes"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["winequality", "diabetes"] } rand_isaac = "0.3" diff --git a/algorithms/linfa-trees/Cargo.toml b/algorithms/linfa-trees/Cargo.toml index 4655987c2..4396f175a 100644 --- a/algorithms/linfa-trees/Cargo.toml +++ b/algorithms/linfa-trees/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-trees" -version = "0.3.1" +version = "0.4.0" edition = "2018" authors = ["Moss Ebeling "] description = "A collection of tree-based algorithms" @@ -27,14 +27,14 @@ features = ["std", "derive"] ndarray = { version = "0.14" , features = ["rayon", "approx"]} ndarray-rand = "0.13" -linfa = { version = "0.3.1", path = "../.." } +linfa = { version = "0.4.0", path = "../.." } [dev-dependencies] rand = { version = "0.8", features = ["small_rng"] } criterion = "0.3" approx = "0.4" -linfa-datasets = { version = "0.3.1", path = "../../datasets/", features = ["iris"] } +linfa-datasets = { version = "0.4.0", path = "../../datasets/", features = ["iris"] } [[bench]] name = "decision_tree" diff --git a/algorithms/linfa-tsne/Cargo.toml b/algorithms/linfa-tsne/Cargo.toml index 09a0c2377..9fa227ab9 100644 --- a/algorithms/linfa-tsne/Cargo.toml +++ b/algorithms/linfa-tsne/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-tsne" -version = "0.3.1" +version = "0.4.0" authors = ["Lorenz Schmidt "] edition = "2018" @@ -10,7 +10,7 @@ license = "MIT/Apache-2.0" repository = "https://github.com/rust-ml/linfa" readme = "README.md" -keywords = ["tsne", "data visualization", "clustering", "machine-learning", "linfa"] +keywords = ["tsne", "visualization", "clustering", "machine-learning", "linfa"] categories = ["algorithms", "mathematics", "science"] [dependencies] @@ -19,11 +19,12 @@ ndarray = { version = "0.14", default-features = false } ndarray-rand = "0.13" bhtsne = "0.4.0" -linfa = { version = "0.3.1", path = "../.." } +linfa = { version = "0.4.0", path = "../.." } [dev-dependencies] rand = "0.8" approx = "0.4" +mnist = { version = "0.4", features = ["download"] } -linfa-datasets = { version = "0.3.1", path = "../../datasets", features = ["iris"] } -linfa-reduction = { version = "0.3.1", path = "../linfa-reduction" } +linfa-datasets = { version = "0.4.0", path = "../../datasets", features = ["iris"] } +linfa-reduction = { version = "0.4.0", path = "../linfa-reduction" } diff --git a/algorithms/linfa-tsne/examples/mnist.rs b/algorithms/linfa-tsne/examples/mnist.rs new file mode 100644 index 000000000..ca8228a2f --- /dev/null +++ b/algorithms/linfa-tsne/examples/mnist.rs @@ -0,0 +1,60 @@ +use linfa::traits::{Fit, Transformer}; +use linfa::Dataset; +use linfa_reduction::Pca; +use linfa_tsne::{Result, TSne}; +use mnist::{Mnist, MnistBuilder}; +use ndarray::Array; +use std::{io::Write, process::Command}; + +fn main() -> Result<()> { + // use 50k samples from the MNIST dataset + let (trn_size, rows, cols) = (50_000usize, 28, 28); + + // download and extract it into a dataset + let Mnist { + trn_img, trn_lbl, .. + } = MnistBuilder::new() + .label_format_digit() + .training_set_length(trn_size as u32) + .download_and_extract() + .finalize(); + + // create a dataset from it + let ds = Dataset::new( + Array::from_shape_vec((trn_size, rows * cols), trn_img)?.mapv(|x| (x as f64) / 255.), + Array::from_shape_vec((trn_size, 1), trn_lbl)?, + ); + + // reduce to 50 dimension without whitening + let ds = Pca::params(50) + .whiten(false) + .fit(&ds) + .unwrap() + .transform(ds); + + // calculate a two-dimensional embedding with Barnes-Hut t-SNE + let ds = TSne::embedding_size(2) + .perplexity(50.0) + .approx_threshold(0.5) + .max_iter(1000) + .transform(ds)?; + + // write out + let mut f = std::fs::File::create("examples/mnist.dat").unwrap(); + + for (x, y) in ds.sample_iter() { + f.write(format!("{} {} {}\n", x[0], x[1], y[0]).as_bytes()) + .unwrap(); + } + + // and plot with gnuplot + Command::new("gnuplot") + .arg("-p") + .arg("examples/mnist_plot.plt") + .spawn() + .expect( + "Failed to launch gnuplot. Pleasure ensure that gnuplot is installed and on the $PATH.", + ); + + Ok(()) +} diff --git a/algorithms/linfa-tsne/examples/iris_plot.plt b/algorithms/linfa-tsne/examples/mnist_plot.plt similarity index 100% rename from algorithms/linfa-tsne/examples/iris_plot.plt rename to algorithms/linfa-tsne/examples/mnist_plot.plt diff --git a/datasets/Cargo.toml b/datasets/Cargo.toml index 9406109f5..d9517a9cd 100644 --- a/datasets/Cargo.toml +++ b/datasets/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "linfa-datasets" -version = "0.3.1" +version = "0.4.0" authors = ["Lorenz Schmidt "] description = "Collection of small datasets for Linfa" edition = "2018" @@ -8,7 +8,7 @@ license = "MIT/Apache-2.0" repository = "https://github.com/rust-ml/linfa" [dependencies] -linfa = { version = "0.3.1", path = ".." } +linfa = { version = "0.4.0", path = ".." } ndarray = { version = "0.14", default-features = false } ndarray-csv = "=0.5.0" csv = "1.1" diff --git a/docs/website/content/news/release040/index.md b/docs/website/content/news/release040/index.md new file mode 100644 index 000000000..971aed749 --- /dev/null +++ b/docs/website/content/news/release040/index.md @@ -0,0 +1,177 @@ ++++ +title = "Release 0.4.0" +date = "2021-04-28" ++++ + +Linfa's 0.4.0 release introduces four new algorithms, improves documentation of the ICA and K-means implementations, adds more benchmarks to K-Means and updates to ndarray's 0.14 version. + + + +## New algorithms + +The [Partial Least Squares Regression](https://en.wikipedia.org/wiki/Partial_least_squares_regression) model family is added in this release. It projects the observable, as well as predicted variables to a latent space and maximizes the correlation for them. For problems with a large number of targets or collinear predictors it gives a better performance when compared to standard regression. For more information look into the documentation of `linfa-pls`. + +A wrapper for Barnes-Hut t-SNE is also added in this release. The t-SNE algorithm is often used for data visualization and projects data in a high-dimensional space to a similar representation in two/three dimension. It does so by maximizing the Kullback-Leibler Divergence between the high dimensional source distribution to the target distribution. The Barnes-Hut approximation improves the runtime drastically while retaining the performance. Kudos to [github/frjnn](https://github.com/frjnn/) for providing an implementation! + +A new preprocessing crate makes working with textual data and data normalization easy. It implements _count-vectorizer_ and _IT-IDF_ normalization for text pre-processing. Normalizations for signals include linear scaling, norm scaling and whitening with PCA/ZCA/choelsky. An example with a Naive Bayes model achieves 84% F1 score for predicting categories `alt.atheism`, `talk.religion.misc`, `comp.graphics` and `sci.space` on a news dataset. + +[Platt scaling](https://en.wikipedia.org/wiki/Platt_scaling) calibrates a real-valued classification model to probabilities over two classes. This is used for the SV classification when probabilities are required. Further a multi class model, combining multiple binary models (e.g. calibrated SVM models) into a single multi-class model is also added. These composing models are moved to the `linfa/src/composing/` subfolder. + +## Improvements + +Numerous improvements are added to the KMeans implementation, thanks to @YuhanLiin. The implementation is optimized for offline training, an incremental training model is added and KMeans++/KMeans|| initialization gives good initial cluster means for medium and large datasets. + +We also moved to ndarray's version 0.14 and introduced `F::cast` for simpler floating point casting. The trait signature of `linfa::Fit` is changed such that it always returns a `Result` and error handling is added for the `linfa-logistic` and `linfa-reduction` subcrates. + +You often have to compare several model parametrization with k-folding. For this a new function `cross_validate` is added which takes the number of folds, model parameters and a closure for the evaluation metric. It automatically calls k-folding and averages the metric over the folds. To compare different L1 ratios of an elasticnet model, you can use it in the following way: +```rust +// L1 ratios to compare +let ratios = vec![0.1, 0.2, 0.5, 0.7, 1.0]; + +// create a model for each parameter +let models = ratios + .iter() + .map(|ratio| ElasticNet::params().penalty(0.3).l1_ratio(*ratio)) + .collect::>(); + +// get the mean r2 validation score across 5 folds for each model +let r2_values = + dataset.cross_validate(5, &models, |prediction, truth| prediction.r2(&truth))?; + +// show the mean r2 score for each parameter choice +for (ratio, r2) in ratios.iter().zip(r2_values.iter()) { + println!("L1 ratio: {}, r2 score: {}", ratio, r2); +} +``` + +### Other changes + + * fix for border points in the DBSCAN implementation + * improved documentation of the ICA subcrate + * prevent overflowing code example in website + +## Barnes-Hut t-SNE + +This example shows the use of `linfa-tsne` with the MNIST digits dataset. We are going to load the MNIST dataset, then reduce the dimensionality with PCA to an embedding of 50 dimension and finally apply Barnes-Hut t-SNE for a two-dimensional embedding. This embedding can be plotted to give the following image: + + + +I won't go into details how to load the MNIST dataset, but we are using the excellent [crates.io/mnist](https://crates.io/crates/mnist) crate here to help us downloading and representing the images in a proper vector representation. + +```rust +// use 50k samples from the MNIST dataset +let (trn_size, rows, cols) = (50_000, 28, 28); + +// download and extract it into a dataset +let Mnist { images, labels, .. } = MnistBuilder::new() + .label_format_digit() + .training_set_length(trn_size as u32) + .download_and_extract() + .finalize(); +``` + +The image brightness information `images` and corresponding `labels` are then used to construct a dataset. + +```rust +// create a dataset from magnitudes and targets +let ds = Dataset::new( + Array::from_shape_vec((trn_size, rows * cols), images)?.mapv(|x| (x as f64) / 255.), + Array::from_shape_vec((trn_size, 1), labels)? +); +``` + +In a preliminary step this brightness information is transformed from a 784 dimensional vector representation to a 50 dimensional embedding with maximized variance. The Principal Component Analysis uses LOBPCG for an efficient implementation. No whitening is performed as this hurts the results. + +```rust +let ds = Pca::params(50).whiten(false).fit(&ds).transform(ds); +``` + +Then t-SNE is used to project those 50 dimensions in a non-linear way to retain as much of the structural information as possible. We will use a Barnes-Hut approximation with `theta=0.5`. This performs a space partitioning and combines regions very far away from the corresponding point to reduce the required runtime. The value theta can go from zero to one with one the original non-approximate t-SNE algorithm. We will also cap the runtime to a thousand iterations: + +```rust +let ds = TSne::embedding_size(2) + .perplexity(50.0) + .approx_threshold(0.5) + .max_iter(1000) + .transform(ds)?; +``` + +The resulting embedding can then be written out to a file and plotted with `gnuplot`: + +```rust +let mut f = File::create("examples/mnist.dat").unwrap(); + +for (x, y) in ds.sample_iter() { + f.write(format!("{} {} {}\n", x[0], x[1], y[0]).as_bytes()) + .unwrap(); +} +``` + +You can find the full example at [algorithms/linfa-tsne/examples/mnist.rs](https://github.com/rust-ml/linfa/blob/master/algorithms/linfa-tsne/examples/mnist.rs) and run it with +``` +$ cargo run --example mnist --features linfa/intel-mkl-system --release +``` + +## Preprocessing text data with TF-IDF and `linfa-preprocessing` + +Let's move to a different example. This release sees the publication of the first `linfa-preprocessing` version which already includes many algorithms suitable for text processing. We will try to predict the topic of a newspaper article with Gaussian Naive Bayes algorithm. Prior to training such a model, we need to somehow extract continuous embeddings from the text. With a number of sample files `training_filenames` we can use `linfa-preprocessing` to construct a vocabulary by calling: + +```rust +let vectorizer = TfIdfVectorizer::default() + .fit_files(&training_filenames, ISO_8859_1, Strict)?; + +println!( + "We obtain a vocabulary with {} entries", + vectorizer.nentries() +); + +// construction of targets and dataset omitted here +let training_dataset = //... +``` + +This vocabulary can then be used to extract an embedding for a text file. The Naive Bayes algorithm does not work with sparse matrices, so we have to make the embedding matrix dense. + +```rust +let training_records = vectorizer + .transform_files(&training_filenames, ISO_8859_1, Strict) + .to_dense(); +``` + +The Gaussian Naive Bayes is trained with the default parameters and the dataset passed for training: (the construction of the targets is omitted here) +```rust +let model = GaussianNbParams::params().fit(&training_dataset)?; +let training_prediction = model.predict(&training_dataset); + +let cm = training_prediction + .confusion_matrix(&training_dataset)?; + +// this gives an F1 score of 0.9994 +println!("The fitted model has a training f1 score of {}", cm.f1_score()); +``` + +To evaluate the model we have a second set of `test_filenames` which are again transformed to its dense embedding representation. The Gaussian Naive Bayes model is then used to predict the targets. The confusion matrix and F1 score measures its performance. + +```rust +let test_records = vectorizer + .transform_files(&test_filenames, ISO_8859_1, Strict) + .to_dense(); + +// get targets and construct testing dataset +// ... + +// predict the testing targets +let test_prediction: Array1 = model.predict(&test_dataset); + +// create a confusion matrix and print F1 score +let cm = test_prediction.confusion_matrix(&test_dataset)? +println!("{:?}", cm); + +// the evaluation gives an F1 score of 0.8402 +println!("The model has a test f1 score of {}", cm.f1_score()); +``` + +You can find the full example at [algorithms/linfa-preprocessing/examples/tfidf_vectorizer.rs](https://github.com/rust-ml/linfa/blob/master/algorithms/linfa-preprocessing/examples/tfidf_vectorization.rs) and run it with +``` +$ cargo run --example tfidf_vectorizer --release + +``` diff --git a/docs/website/content/news/release040/tsne.png b/docs/website/content/news/release040/tsne.png new file mode 100644 index 0000000000000000000000000000000000000000..ae8ddcbec9bc1ec503901b0e8df93d612871e9b5 GIT binary patch literal 48732 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 Uo}7O84ERx&e::params() + .gaussian_kernel(30.0); + +// assume we have a binary decision model (here SVM) +// predicting probability. We can merge them into a +// multi-class model by collecting several of them +// into a `MultiClassModel` +let model = train + .one_vs_all()? + .into_iter() + .map(|(l, x)| (l, params.fit(&x).unwrap())) + .collect::>(); + +// predict multi-class label +let pred = model.predict(&valid); +``` diff --git a/src/lib.rs b/src/lib.rs index b678bdb55..08bde0bdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,9 +32,9 @@ //! | [hierarchical](https://docs.rs/linfa-hierarchical/) | Agglomerative hierarchical clustering | Tested | Unsupervised learning | Cluster and build hierarchy of clusters | //! | [bayes](https://docs.rs/linfa-bayes/) | Naive Bayes | Tested | Supervised learning | Contains Gaussian Naive Bayes | //! | [ica](https://docs.rs/linfa-ica/) | Independent component analysis | Tested | Unsupervised learning | Contains FastICA implementation | -//! | [pls](algorithms/linfa-pls/) | Partial Least Squares | Tested | Supervised learning | Contains PLS estimators for dimensionality reduction and regression | -//! | [tsne](algorithms/linfa-tsne/) | Dimensionality reduction| Tested | Unsupervised learning | Contains exact solution and Barnes-Hut approximation t-SNE | -//! | [preprocessing](algorithms/linfa-preprocessing/) |Normalization & Vectorization| Tested | Pre-processing | Contains data normalization/whitening and count vectorization/tf-idf| +//! | [pls](https://docs.rs/linfa-pls/) | Partial Least Squares | Tested | Supervised learning | Contains PLS estimators for dimensionality reduction and regression | +//! | [tsne](https://docs.rs/linfa-tsne/) | Dimensionality reduction| Tested | Unsupervised learning | Contains exact solution and Barnes-Hut approximation t-SNE | +//! | [preprocessing](https://docs.rs/linfa-preprocessing/) |Normalization & Vectorization| Tested | Pre-processing | Contains data normalization/whitening and count vectorization/tf-idf| //! //! We believe that only a significant community effort can nurture, build, and sustain a machine learning ecosystem in Rust - there is no other way forward. //!