mod repulsion;
mod tsne;
#[cfg(feature = "csv")]
mod csv;
use std::{
iter::Sum,
ops::{AddAssign, DivAssign, MulAssign, SubAssign},
};
use num_traits::{Float, cast::AsPrimitive};
use repulsion::{BarnesHutRepulsion, InterpolatedRepulsion, Repulsion};
#[cfg(feature = "csv")]
pub use csv::load_csv;
pub use {
rustfft::FftNum,
tsne::interpolation::FftDim,
tsne::morton::{Dim, Morton},
};
use rayon::{
iter::{
IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator,
ParallelIterator,
},
slice::{ParallelSlice, ParallelSliceMut},
};
const PARALLEL_CODE_THRESHOLD: usize = 4096;
pub type EpochCallback<'data, T> = Box<dyn FnMut(usize, &[T]) + 'data>;
enum Fit<T> {
Exact,
BarnesHut { theta: T },
Interpolated,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Neighbor<T> {
pub index: usize,
pub distance: T,
}
#[derive(Clone)]
pub struct SparseAffinities<T> {
rows: Vec<usize>,
columns: Vec<u32>,
values: Vec<T>,
perplexity: T,
}
#[allow(non_camel_case_types)]
pub struct tSNE<'data, T, U, const D: usize = 2>
where
T: Send + Sync + Float + Sum + DivAssign + MulAssign + AddAssign + SubAssign,
U: Send + Sync,
{
data: &'data [U],
learning_rate: Option<T>,
epochs: usize,
momentum: T,
final_momentum: T,
momentum_switch_epoch: usize,
stop_lying_epoch: usize,
early_exaggeration: T,
perplexity: T,
p_values: Vec<T>,
p_rows: Vec<usize>,
p_columns: Vec<u32>,
q_values: Vec<T>,
y: Vec<T>,
dy: Vec<T>,
uy: Vec<T>,
gains: Vec<T>,
epoch_callback: Option<EpochCallback<'data, T>>,
initial_embedding: Option<Vec<T>>,
stop_lying_fired: bool,
cached_perplexity: Option<T>,
fit: Option<Fit<T>>,
}
impl<'data, T, U, const D: usize> tSNE<'data, T, U, D>
where
T: Float
+ Send
+ Sync
+ AsPrimitive<usize>
+ Sum
+ DivAssign
+ AddAssign
+ MulAssign
+ SubAssign,
U: Send + Sync,
{
pub fn new(data: &'data [U]) -> Self {
Self {
data,
learning_rate: None,
epochs: 1000,
momentum: T::from(0.5).unwrap(),
final_momentum: T::from(0.8).unwrap(),
momentum_switch_epoch: 250,
stop_lying_epoch: 250,
early_exaggeration: T::from(12.0).unwrap(),
perplexity: T::from(20.0).unwrap(),
p_values: Vec::new(),
p_rows: Vec::new(),
p_columns: Vec::new(),
q_values: Vec::new(),
y: Vec::new(),
dy: Vec::new(),
uy: Vec::new(),
gains: Vec::new(),
epoch_callback: None,
initial_embedding: None,
stop_lying_fired: false,
cached_perplexity: None,
fit: None,
}
}
pub fn learning_rate(&mut self, learning_rate: T) -> &mut Self {
self.learning_rate = Some(learning_rate);
self
}
pub fn epochs(&mut self, epochs: usize) -> &mut Self {
self.epochs = epochs;
self
}
pub fn momentum(&mut self, momentum: T) -> &mut Self {
self.momentum = momentum;
self
}
pub fn final_momentum(&mut self, final_momentum: T) -> &mut Self {
self.final_momentum = final_momentum;
self
}
pub fn momentum_switch_epoch(&mut self, momentum_switch_epoch: usize) -> &mut Self {
self.momentum_switch_epoch = momentum_switch_epoch;
self
}
pub fn stop_lying_epoch(&mut self, stop_lying_epoch: usize) -> &mut Self {
self.stop_lying_epoch = stop_lying_epoch;
self
}
pub fn early_exaggeration(&mut self, early_exaggeration: T) -> &mut Self {
self.early_exaggeration = early_exaggeration;
self
}
pub fn perplexity(&mut self, perplexity: T) -> &mut Self {
self.perplexity = perplexity;
self
}
pub fn epoch_callback<C>(&mut self, callback: C) -> &mut Self
where
C: FnMut(usize, &[T]) + 'data,
{
self.epoch_callback = Some(Box::new(callback));
self
}
pub fn initial_embedding(&mut self, embedding: impl Into<Vec<T>>) -> &mut Self {
self.initial_embedding = Some(embedding.into());
self
}
pub fn embedding(&self) -> Vec<T> {
self.y.clone()
}
pub fn kl_divergence(&self) -> Option<T>
where
T: FftNum,
Dim<D>: Morton<D>,
{
let n_samples = self.data.len();
match self.fit.as_ref()? {
Fit::Exact => Some(tsne::evaluate_error::<T, D>(
&self.p_values,
&self.y,
n_samples,
)),
Fit::BarnesHut { theta } => Some(BarnesHutRepulsion::<T, D>::new(*theta).error(
&self.p_rows,
&self.p_columns,
&self.p_values,
&self.y,
n_samples,
)),
Fit::Interpolated => Some(InterpolatedRepulsion::<T, D>::new().error(
&self.p_rows,
&self.p_columns,
&self.p_values,
&self.y,
n_samples,
)),
}
}
pub fn exact<F: Fn(&U, &U) -> T + Send + Sync>(&mut self, distance_f: F) -> &mut Self {
let data = self.data;
let n_samples = self.data.len();
tsne::check_perplexity(&self.perplexity, &n_samples);
let grad_entries = n_samples * D;
let pairwise_entries = n_samples * n_samples;
tsne::prepare_buffers(
&mut self.y,
&mut self.dy,
&mut self.uy,
&mut self.gains,
grad_entries,
);
self.p_values.resize(pairwise_entries, T::zero()); self.q_values.resize(pairwise_entries, T::zero());
let mut distances: Vec<T> = vec![T::zero(); pairwise_entries];
distances
.iter_mut()
.step_by(n_samples + 1)
.for_each(|d| *d = T::zero());
tsne::compute_pairwise_distance_matrix(
&mut distances,
distance_f,
|index| &data[*index],
n_samples,
);
{
let perplexity = &self.perplexity;
self.p_values
.par_chunks_mut(n_samples)
.zip(distances.par_chunks(n_samples))
.for_each(|(p_values_row, distances_row)| {
tsne::search_beta(p_values_row, distances_row, perplexity);
});
}
for i in 0..n_samples {
for j in (i + 1)..n_samples {
let symmetric = self.p_values[j * n_samples + i];
self.p_values[i * n_samples + j] += symmetric;
self.p_values[j * n_samples + i] = self.p_values[i * n_samples + j];
}
}
self.finalize_p_and_seed(grad_entries);
let learning_rate = self.resolve_learning_rate(n_samples);
let (mut epoch_callback, mut snapshot) = self.take_callback_and_snapshot(grad_entries);
for epoch in 0..self.epochs {
let (y_chunks, _) = self.y.as_chunks::<D>();
tsne::compute_pairwise_distance_matrix(
&mut distances,
|ith: &[T; D], jth: &[T; D]| {
ith.iter()
.zip(jth.iter())
.map(|(&i, &j)| (i - j).powi(2))
.sum()
},
|index| &y_chunks[*index],
n_samples,
);
self.q_values
.par_iter_mut()
.zip(distances.par_iter())
.for_each(|(q, d)| *q = (T::one() + *d).recip());
let q_values_sum: T = self.q_values.par_iter().copied().sum::<T>();
let inverse_q_sum = q_values_sum.recip();
let (dy_chunks, _) = self.dy.as_chunks_mut::<D>();
dy_chunks
.par_iter_mut()
.zip(y_chunks.par_iter())
.zip(self.p_values.par_chunks(n_samples))
.zip(self.q_values.par_chunks(n_samples))
.for_each(
|(((dy_sample, y_sample), p_values_sample), q_values_sample)| {
p_values_sample
.iter()
.zip(q_values_sample.iter())
.zip(y_chunks.iter())
.for_each(|((&p, &q), other_sample)| {
let m = (p - q * inverse_q_sum) * q;
dy_sample
.iter_mut()
.zip(y_sample.iter())
.zip(other_sample.iter())
.for_each(|((dy_el, &y_el), &other_el)| {
*dy_el += (y_el - other_el) * m
});
});
},
);
tsne::update_solution(
&mut self.y,
&self.dy,
&mut self.uy,
&mut self.gains,
&learning_rate,
&self.momentum,
);
self.dy.fill(T::zero());
tsne::zero_mean::<T, D>(&mut self.y, n_samples);
self.epoch_tail(epoch, &mut epoch_callback, &mut snapshot);
}
self.epoch_callback = epoch_callback;
tsne::clear_buffers(&mut self.dy, &mut self.uy, &mut self.gains);
self.fit = Some(Fit::Exact);
self
}
#[inline]
fn resolve_learning_rate(&self, n_samples: usize) -> T {
self.learning_rate.unwrap_or_else(|| {
let auto =
T::from(n_samples).unwrap() / self.early_exaggeration / T::from(4.0).unwrap();
auto.max(T::from(50.0).unwrap())
})
}
fn finalize_p_and_seed(&mut self, grad_entries: usize) {
tsne::normalize_p_values(&mut self.p_values, self.early_exaggeration);
if self.stop_lying_epoch == 0 {
tsne::stop_lying(&mut self.p_values, self.early_exaggeration);
}
match self.initial_embedding.take() {
Some(init) => {
assert_eq!(
init.len(),
grad_entries,
"error: initial embedding has {} values, expected n_samples * D = {}",
init.len(),
grad_entries
);
self.y.iter_mut().zip(&init).for_each(|(y, &v)| *y = v);
}
None => tsne::random_init(&mut self.y),
}
}
pub fn barnes_hut<F>(&mut self, theta: T, metric_f: F) -> &mut Self
where
F: Fn(&U, &U) -> T + Send + Sync,
Dim<D>: Morton<D>,
{
self.validate_fit_params(theta);
let n_samples = self.data.len();
if self.has_cached_affinities(n_samples) {
return self.run_cached(
n_samples,
BarnesHutRepulsion::new(theta),
Fit::BarnesHut { theta },
);
}
let data = self.data;
let n_neighbors: usize = (T::from(3.0).unwrap() * self.perplexity).as_();
let tree = tsne::vptree::VPTree::new(data, &metric_f);
self.approximate_fit(
n_neighbors,
move |scratch, index, p_columns_row, distances_row| {
tree.search(
&data[index],
index,
n_neighbors + 1,
(p_columns_row, distances_row),
scratch,
&metric_f,
);
},
BarnesHutRepulsion::new(theta),
Fit::BarnesHut { theta },
)
}
pub fn barnes_hut_with_neighbors(
&mut self,
theta: T,
neighbors: &[Vec<Neighbor<T>>],
) -> &mut Self
where
Dim<D>: Morton<D>,
{
self.validate_fit_params(theta);
self.approximate_fit_with_neighbors(
neighbors,
BarnesHutRepulsion::new(theta),
Fit::BarnesHut { theta },
)
}
fn check_neighbors(&self, neighbors: &[Vec<Neighbor<T>>]) -> usize {
let n_samples = self.data.len();
assert_eq!(
neighbors.len(),
n_samples,
"error: neighbors has {} rows, expected one per sample = {}",
neighbors.len(),
n_samples
);
let n_neighbors = neighbors.first().map_or(0, Vec::len);
assert!(
n_neighbors > 0 && neighbors.iter().all(|row| row.len() == n_neighbors),
"error: every neighbors row must have the same length, greater than zero."
);
n_neighbors
}
fn assert_neighbor_indices_in_range(&self, neighbors: &[Vec<Neighbor<T>>]) {
let n_samples = self.data.len();
assert!(
neighbors
.iter()
.flatten()
.all(|neighbor| neighbor.index < n_samples),
"error: a neighbor index is out of range, every index must be < n_samples = {n_samples}."
);
}
pub fn fit_sne<F>(&mut self, metric_f: F) -> &mut Self
where
F: Fn(&U, &U) -> T + Send + Sync,
T: FftNum,
Dim<D>: FftDim,
{
tsne::check_perplexity(&self.perplexity, &self.data.len());
let n_samples = self.data.len();
if self.has_cached_affinities(n_samples) {
return self.run_cached(n_samples, InterpolatedRepulsion::new(), Fit::Interpolated);
}
let data = self.data;
let n_neighbors: usize = (T::from(3.0).unwrap() * self.perplexity).as_();
let tree = tsne::vptree::VPTree::new(data, &metric_f);
self.approximate_fit(
n_neighbors,
move |scratch, index, p_columns_row, distances_row| {
tree.search(
&data[index],
index,
n_neighbors + 1,
(p_columns_row, distances_row),
scratch,
&metric_f,
);
},
InterpolatedRepulsion::new(),
Fit::Interpolated,
)
}
pub fn fit_sne_with_neighbors(&mut self, neighbors: &[Vec<Neighbor<T>>]) -> &mut Self
where
T: FftNum,
Dim<D>: FftDim,
{
self.approximate_fit_with_neighbors(
neighbors,
InterpolatedRepulsion::new(),
Fit::Interpolated,
)
}
fn approximate_fit<R, F>(
&mut self,
n_neighbors: usize,
fill_neighbors: F,
strategy: R,
fit: Fit<T>,
) -> &mut Self
where
R: Repulsion<T, D>,
F: Fn(&mut tsne::vptree::SearchScratch<T>, usize, &mut [u32], &mut [T]) + Send + Sync,
{
let grad_entries = self.build_affinities(n_neighbors, fill_neighbors);
self.finalize_p_and_seed(grad_entries);
self.run_loop(grad_entries, strategy, fit)
}
fn approximate_fit_with_neighbors<R>(
&mut self,
neighbors: &[Vec<Neighbor<T>>],
strategy: R,
fit: Fit<T>,
) -> &mut Self
where
R: Repulsion<T, D>,
{
let n_samples = self.data.len();
let n_neighbors = self.check_neighbors(neighbors);
if self.cached_affinities_match_neighbors(neighbors) {
return self.run_cached(n_samples, strategy, fit);
}
self.assert_neighbor_indices_in_range(neighbors);
self.approximate_fit(
n_neighbors,
|_, index, p_columns_row, distances_row| {
copy_neighbor_row(neighbors, index, p_columns_row, distances_row);
},
strategy,
fit,
)
}
fn run_loop<R>(&mut self, grad_entries: usize, mut strategy: R, fit: Fit<T>) -> &mut Self
where
R: Repulsion<T, D>,
{
let n_samples = self.data.len();
let mut positive_forces: Vec<T> = vec![T::zero(); grad_entries];
let mut negative_forces: Vec<T> = vec![T::zero(); grad_entries];
let (mut epoch_callback, mut snapshot) = self.take_callback_and_snapshot(grad_entries);
let learning_rate = self.resolve_learning_rate(n_samples);
for epoch in 0..self.epochs {
let inverse_norm = strategy.step(
&self.y,
&self.p_rows,
&self.p_columns,
&self.p_values,
&mut positive_forces,
&mut negative_forces,
);
tsne::gradient_descent_step::<T, D>(
&mut self.y,
&positive_forces,
&negative_forces,
&mut self.uy,
&mut self.gains,
tsne::GradientStep {
learning_rate,
momentum: self.momentum,
inverse_norm,
},
);
tsne::zero_mean::<T, D>(&mut self.y, n_samples);
self.epoch_tail(epoch, &mut epoch_callback, &mut snapshot);
}
self.epoch_callback = epoch_callback;
tsne::clear_buffers(&mut self.dy, &mut self.uy, &mut self.gains);
self.fit = Some(fit);
self
}
pub fn affinities(&self) -> Option<SparseAffinities<T>> {
if self.p_rows.is_empty() {
return None;
}
let values = if self.stop_lying_fired {
self.p_values.clone()
} else {
let scale = self.early_exaggeration.recip();
self.p_values.iter().map(|v| *v * scale).collect()
};
Some(SparseAffinities {
rows: self.p_rows.clone(),
columns: self.p_columns.clone(),
values,
perplexity: self.perplexity,
})
}
pub fn with_affinities(&mut self, affinities: SparseAffinities<T>) -> &mut Self {
self.perplexity = affinities.perplexity;
self.p_rows = affinities.rows;
self.p_columns = affinities.columns;
self.p_values = affinities.values;
self.cached_perplexity = Some(self.perplexity);
self
}
fn build_affinities<F>(&mut self, n_neighbors: usize, fill_neighbors: F) -> usize
where
F: Fn(&mut tsne::vptree::SearchScratch<T>, usize, &mut [u32], &mut [T]) + Send + Sync,
{
let n_samples = self.data.len(); let grad_entries = n_samples * D;
let pairwise_entries = n_samples * n_neighbors;
self.y.resize(grad_entries, T::zero());
self.uy.resize(grad_entries, T::zero());
self.gains.resize(grad_entries, T::one());
self.p_values.resize(pairwise_entries, T::zero());
let mut p_columns: Vec<u32> = vec![0u32; pairwise_entries];
{
let mut distances: Vec<T> = vec![T::zero(); pairwise_entries];
let perplexity = &self.perplexity; self.p_values
.par_chunks_mut(n_neighbors)
.zip(distances.par_chunks_mut(n_neighbors))
.zip(p_columns.par_chunks_mut(n_neighbors))
.enumerate()
.for_each_init(
tsne::vptree::SearchScratch::default,
|scratch, (index, ((p_values_row, distances_row), p_columns_row))| {
fill_neighbors(scratch, index, p_columns_row, distances_row);
debug_assert!(!p_columns_row.contains(&(index as u32)));
tsne::search_beta(p_values_row, distances_row, perplexity);
},
);
}
drop(fill_neighbors);
tsne::symmetrize_sparse_matrix(
&mut self.p_rows,
&mut self.p_columns,
p_columns,
&mut self.p_values,
n_samples,
&n_neighbors,
);
self.cached_perplexity = Some(self.perplexity);
grad_entries
}
fn take_callback_and_snapshot(
&mut self,
grad_entries: usize,
) -> (Option<EpochCallback<'data, T>>, Vec<T>) {
let epoch_callback = self.epoch_callback.take();
let snapshot = match epoch_callback {
Some(_) => vec![T::zero(); grad_entries],
None => Vec::new(),
};
(epoch_callback, snapshot)
}
fn epoch_tail(
&mut self,
epoch: usize,
epoch_callback: &mut Option<EpochCallback<'data, T>>,
snapshot: &mut [T],
) {
if epoch == self.stop_lying_epoch && epoch != 0 {
tsne::stop_lying(&mut self.p_values, self.early_exaggeration);
self.stop_lying_fired = true;
}
if epoch == self.momentum_switch_epoch {
self.momentum = self.final_momentum;
}
if let Some(callback) = epoch_callback.as_mut() {
snapshot.copy_from_slice(&self.y);
callback(epoch, snapshot);
}
}
fn validate_fit_params(&self, theta: T) {
assert!(
theta > T::zero(),
"error: theta value must be greater than 0.0.
A value of 0.0 corresponds to using the exact version of the algorithm."
);
tsne::check_perplexity(&self.perplexity, &self.data.len());
}
fn has_cached_affinities(&self, n_samples: usize) -> bool {
!self.p_rows.is_empty()
&& self.p_rows.len() == n_samples + 1
&& self.cached_perplexity == Some(self.perplexity)
}
fn cached_affinities_match_neighbors(&self, neighbors: &[Vec<Neighbor<T>>]) -> bool {
if self.p_rows.is_empty()
|| self.p_rows.len() != neighbors.len() + 1
|| self.cached_perplexity != Some(self.perplexity)
{
return false;
}
let p_rows = &self.p_rows;
let p_columns = &self.p_columns;
neighbors.iter().enumerate().all(|(i, row)| {
let start = p_rows[i];
let end = p_rows[i + 1];
(end - start) == row.len()
&& row
.iter()
.enumerate()
.all(|(j, neighbor)| p_columns[start + j] == neighbor.index as u32)
})
}
fn prepare_cached(&mut self, n_samples: usize) -> usize {
self.stop_lying_fired = false;
let grad_entries = n_samples * D;
self.y.resize(grad_entries, T::zero());
self.uy.resize(grad_entries, T::zero());
self.gains.resize(grad_entries, T::one());
self.finalize_p_and_seed(grad_entries);
grad_entries
}
fn run_cached<R>(&mut self, n_samples: usize, strategy: R, fit: Fit<T>) -> &mut Self
where
R: Repulsion<T, D>,
{
let grad_entries = self.prepare_cached(n_samples);
self.run_loop(grad_entries, strategy, fit)
}
}
#[inline]
fn copy_neighbor_row<T: Copy>(
neighbors: &[Vec<Neighbor<T>>],
index: usize,
p_columns_row: &mut [u32],
distances_row: &mut [T],
) {
p_columns_row
.iter_mut()
.zip(distances_row.iter_mut())
.zip(neighbors[index].iter())
.for_each(|((column, distance), neighbor)| {
*column = neighbor.index as u32;
*distance = neighbor.distance;
});
}
#[cfg(test)]
mod test;