mod tsne;
#[cfg(test)]
mod test;
use std::{
iter::Sum,
ops::{AddAssign, DivAssign, MulAssign, SubAssign},
};
#[cfg(feature = "csv")]
use std::{error::Error, fs::File};
use num_traits::{Float, cast::AsPrimitive};
use crossbeam::utils::CachePadded;
use rayon::{
iter::{
IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator,
ParallelIterator,
},
slice::{ParallelSlice, ParallelSliceMut},
};
pub type EpochCallback<'data, T> = Box<dyn FnMut(usize, &[T]) + Send + Sync + 'data>;
enum Fit<T> {
Exact,
BarnesHut { theta: T },
}
#[allow(non_camel_case_types)]
pub struct tSNE<'data, T, U>
where
T: Send + Sync + Float + Sum + DivAssign + MulAssign + AddAssign + SubAssign,
U: Send + Sync,
{
data: &'data [U],
learning_rate: T,
epochs: usize,
momentum: T,
final_momentum: T,
momentum_switch_epoch: usize,
stop_lying_epoch: usize,
embedding_dim: u8,
perplexity: T,
p_values: Vec<CachePadded<T>>,
p_rows: Vec<usize>,
p_columns: Vec<usize>,
q_values: Vec<CachePadded<T>>,
y: Vec<CachePadded<T>>,
dy: Vec<CachePadded<T>>,
uy: Vec<CachePadded<T>>,
gains: Vec<CachePadded<T>>,
epoch_callback: Option<EpochCallback<'data, T>>,
initial_embedding: Option<Vec<T>>,
fit: Option<Fit<T>>,
}
impl<'data, T, U> tSNE<'data, T, U>
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: T::from(200.0).unwrap(),
epochs: 1000,
momentum: T::from(0.5).unwrap(),
final_momentum: T::from(0.8).unwrap(),
momentum_switch_epoch: 250,
stop_lying_epoch: 250,
embedding_dim: 2,
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,
fit: None,
}
}
pub fn learning_rate(&mut self, learning_rate: T) -> &mut Self {
self.learning_rate = 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 embedding_dim(&mut self, embedding_dim: u8) -> &mut Self {
self.embedding_dim = embedding_dim;
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]) + Send + Sync + '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.iter().map(|x| **x).collect()
}
pub fn kl_divergence(&self) -> Option<T> {
let n_samples = self.data.len();
let embedding_dim = self.embedding_dim as usize;
match self.fit.as_ref()? {
Fit::Exact => Some(tsne::evaluate_error(
&self.p_values,
&self.y,
n_samples,
embedding_dim,
)),
Fit::BarnesHut { theta } => Some(tsne::evaluate_error_approximately(
&self.p_rows,
&self.p_columns,
&self.p_values,
&self.y,
n_samples,
embedding_dim,
*theta,
)),
}
}
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 embedding_dim = self.embedding_dim as usize;
let grad_entries = n_samples * embedding_dim;
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().into()); self.q_values.resize(pairwise_entries, T::zero().into());
let mut distances: Vec<CachePadded<T>> = vec![T::zero().into(); pairwise_entries];
for i in 0..n_samples {
distances[i * n_samples + i] = T::zero().into();
}
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];
}
}
tsne::normalize_p_values(&mut self.p_values);
if self.stop_lying_epoch == 0 {
tsne::stop_lying(&mut self.p_values);
}
match self.initial_embedding.take() {
Some(init) => {
assert_eq!(
init.len(),
grad_entries,
"error: initial embedding has {} values, expected n_samples * embedding_dim = {}",
init.len(),
grad_entries
);
self.y.iter_mut().zip(&init).for_each(|(y, &v)| **y = v);
}
None => tsne::random_init(&mut self.y),
}
let mut means: Vec<T> = vec![T::zero(); embedding_dim];
let mut epoch_callback = self.epoch_callback.take();
let mut snapshot: Vec<T> = match epoch_callback {
Some(_) => vec![T::zero(); grad_entries],
None => Vec::new(),
};
for epoch in 0..self.epochs {
tsne::compute_pairwise_distance_matrix(
&mut distances,
|ith: &[CachePadded<T>], jth: &[CachePadded<T>]| {
ith.iter()
.zip(jth.iter())
.map(|(&i, &j)| (*i - *j).powi(2))
.sum()
},
|index| &self.y[index * embedding_dim..index * embedding_dim + embedding_dim],
n_samples,
);
self.q_values
.par_iter_mut()
.zip(distances.par_iter())
.for_each(|(q, d)| **q = T::one() / (T::one() + **d));
let q_values_sum: T = self.q_values.par_iter().map(|&q| *q).sum();
let y = &self.y;
self.dy
.par_chunks_mut(embedding_dim)
.zip(self.y.par_chunks(embedding_dim))
.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(embedding_dim))
.for_each(|((&p, &q), other_sample)| {
let m: T = (*p - *q / q_values_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,
&self.learning_rate,
&self.momentum,
);
self.dy.iter_mut().for_each(|el| **el = T::zero());
tsne::zero_mean(&mut means, &mut self.y, n_samples, embedding_dim);
if epoch == self.stop_lying_epoch && epoch != 0 {
tsne::stop_lying(&mut self.p_values);
}
if epoch == self.momentum_switch_epoch {
self.momentum = self.final_momentum;
}
if let Some(callback) = epoch_callback.as_mut() {
snapshot
.iter_mut()
.zip(self.y.iter())
.for_each(|(dst, src)| *dst = **src);
callback(epoch, &snapshot);
}
}
self.epoch_callback = epoch_callback;
tsne::clear_buffers(&mut self.dy, &mut self.uy, &mut self.gains);
self.fit = Some(Fit::Exact);
self
}
pub fn barnes_hut<F>(&mut self, theta: T, metric_f: F) -> &mut Self
where
F: Fn(&U, &U) -> T + Send + Sync,
{
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."
);
let data = self.data;
let n_samples = self.data.len();
tsne::check_perplexity(&self.perplexity, &n_samples);
let embedding_dim = self.embedding_dim as usize;
let n_neighbors: usize = (T::from(3.0).unwrap() * self.perplexity).as_();
let grad_entries = n_samples * embedding_dim;
let pairwise_entries = n_samples * n_neighbors;
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().into());
let mut p_columns: Vec<CachePadded<usize>> = vec![0.into(); pairwise_entries];
{
let mut distances: Vec<CachePadded<T>> = vec![T::zero().into(); pairwise_entries];
let tree = tsne::vptree::VPTree::new(data, &metric_f);
{
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))
.zip(data.par_iter())
.enumerate()
.for_each(
|(index, (((p_values_row, distances_row), p_columns_row), sample))| {
tree.search(
sample,
index,
n_neighbors + 1, p_columns_row,
distances_row,
&metric_f,
);
debug_assert!(!p_columns_row.iter().any(|&i| *i == index));
tsne::search_beta(p_values_row, distances_row, perplexity);
},
);
}
}
tsne::symmetrize_sparse_matrix(
&mut self.p_rows,
&mut self.p_columns,
p_columns,
&mut self.p_values,
n_samples,
&n_neighbors,
);
tsne::normalize_p_values(&mut self.p_values);
if self.stop_lying_epoch == 0 {
tsne::stop_lying(&mut self.p_values);
}
match self.initial_embedding.take() {
Some(init) => {
assert_eq!(
init.len(),
grad_entries,
"error: initial embedding has {} values, expected n_samples * embedding_dim = {}",
init.len(),
grad_entries
);
self.y.iter_mut().zip(&init).for_each(|(y, &v)| **y = v);
}
None => tsne::random_init(&mut self.y),
}
let mut positive_forces: Vec<CachePadded<T>> = vec![T::zero().into(); grad_entries];
let mut negative_forces: Vec<CachePadded<T>> = vec![T::zero().into(); grad_entries];
let mut forces_buffer: Vec<CachePadded<T>> = vec![T::zero().into(); grad_entries];
let mut q_sums: Vec<CachePadded<T>> = vec![T::zero().into(); n_samples];
let mut means: Vec<T> = vec![T::zero(); embedding_dim];
let mut epoch_callback = self.epoch_callback.take();
let mut snapshot: Vec<T> = match epoch_callback {
Some(_) => vec![T::zero(); grad_entries],
None => Vec::new(),
};
for epoch in 0..self.epochs {
{
let tree = tsne::sptree::SPTree::new(embedding_dim, &self.y, n_samples);
debug_assert!(tree.is_correct(), "error: SPTree is not correct.");
positive_forces
.par_chunks_mut(embedding_dim)
.zip(negative_forces.par_chunks_mut(embedding_dim))
.zip(forces_buffer.par_chunks_mut(embedding_dim))
.zip(q_sums.par_iter_mut())
.zip(self.y.par_chunks(embedding_dim))
.enumerate()
.for_each(
|(
index,
(
(
((positive_forces_row, negative_forces_row), forces_buffer_row),
q_sum,
),
sample,
),
)| {
tree.compute_edge_forces(
index,
sample,
&self.p_rows,
&self.p_columns,
&self.p_values,
forces_buffer_row,
positive_forces_row,
);
tree.compute_non_edge_forces(
index,
theta,
negative_forces_row,
forces_buffer_row,
q_sum,
);
},
);
}
let q_sum: T = q_sums.par_iter_mut().map(|sum| **sum).sum();
self.dy
.par_iter_mut()
.zip(positive_forces.par_iter_mut())
.zip(negative_forces.par_iter_mut())
.for_each(|((grad, pf), nf)| {
**grad = **pf - (**nf / q_sum);
**pf = T::zero();
**nf = T::zero();
});
q_sums.iter_mut().for_each(|sum| **sum = T::zero());
tsne::update_solution(
&mut self.y,
&self.dy,
&mut self.uy,
&mut self.gains,
&self.learning_rate,
&self.momentum,
);
tsne::zero_mean(&mut means, &mut self.y, n_samples, embedding_dim);
if epoch == self.stop_lying_epoch && epoch != 0 {
tsne::stop_lying(&mut self.p_values);
}
if epoch == self.momentum_switch_epoch {
self.momentum = self.final_momentum;
}
if let Some(callback) = epoch_callback.as_mut() {
snapshot
.iter_mut()
.zip(self.y.iter())
.for_each(|(dst, src)| *dst = **src);
callback(epoch, &snapshot);
}
}
self.epoch_callback = epoch_callback;
tsne::clear_buffers(&mut self.dy, &mut self.uy, &mut self.gains);
self.fit = Some(Fit::BarnesHut { theta });
self
}
#[cfg(feature = "csv")]
pub fn write_csv(&mut self, path: &str) -> Result<&mut Self, Box<dyn Error>>
where
T: Float + ToString,
{
let mut writer = csv::Writer::from_path(path)?;
let to_write = self
.y
.iter()
.map(|&el| (*el).to_string())
.collect::<Vec<String>>();
match self.embedding_dim {
2 => writer.write_record(["x", "y"])?,
3 => writer.write_record(["x", "y", "z"])?,
_ => (), }
for record in to_write.chunks(self.embedding_dim as usize) {
writer.write_record(record)?
}
writer.flush()?;
Ok(self)
}
}
#[cfg(feature = "csv")]
pub fn load_csv<T, F>(
path: &str,
has_headers: bool,
skip: Option<&[usize]>,
f: F,
) -> Result<Vec<T>, Box<dyn Error>>
where
F: Fn(String) -> T,
{
let mut data: Vec<T> = Vec::new();
let file = File::open(path)?;
let mut reader = csv::ReaderBuilder::new()
.has_headers(has_headers)
.from_reader(file);
match skip {
Some(range) => {
for result in reader.records() {
let record = result?;
(0..record.len())
.filter(|column| !range.contains(column))
.for_each(|field| data.push(f(record.get(field).unwrap().to_string())));
}
}
None => {
for result in reader.records() {
let record = result?;
(0..record.len())
.for_each(|field| data.push(f(record.get(field).unwrap().to_string())));
}
}
}
Ok(data)
}