#include "mp.h" #include <algorithm> #include <cmath> static const double ETA = 500; static const double MIN_GAIN = 1e-2; static const double EPSILON = 1e-12; static const double INITIAL_MOMENTUM = 0.5; static const double FINAL_MOMENTUM = 0.8; static const double EARLY_EXAGGERATION = 4.; static const double GAIN_FRACTION = 0.2; static const int MOMENTUM_THRESHOLD_ITER = 20; static const int EXAGGERATION_THRESHOLD_ITER = 100; static const int MAX_BINSEARCH_TRIES = 50; static void calcP(const arma::mat &X, arma::mat &P, double perplexity, double tol = 1e-5); static double hBeta(const arma::rowvec &Di, double beta, arma::rowvec &Pi); arma::mat mp::tSNE(const arma::mat &X, arma::uword k, double perplexity, arma::uword nIter) { arma::mat Y(X.n_rows, k); mp::tSNE(X, Y, perplexity, nIter); return Y; } void mp::tSNE(const arma::mat &X, arma::mat &Y, double perplexity, arma::uword nIter) { double momentum; arma::uword n = X.n_rows; arma::uword k = Y.n_cols; arma::mat Q(n, n); arma::mat dY(n, k), gains(n, k, arma::fill::ones), iY(n, k, arma::fill::zeros); arma::mat P(n, n, arma::fill::zeros); calcP(X, P, perplexity); P = (P + P.t()); P /= arma::accu(P); P *= EARLY_EXAGGERATION; P.transform([](double v) { return std::max(v, EPSILON); }); for (arma::uword iter = 0; iter < nIter; iter++) { arma::vec sumY = arma::sum(Y % Y, 1); arma::mat num = -2. * (Y * Y.t()); num.each_col() += sumY; arma::inplace_trans(num); num.each_col() += sumY; num = 1. / (1. + num); num.diag() *= 0; Q = num / arma::accu(num); Q.transform([](double v) { return std::max(v, EPSILON); }); for (arma::uword i = 0; i < n; i++) { arma::mat tmp = -Y; tmp.each_row() += Y.row(i); tmp.each_col() %= (P.col(i) - Q.col(i)) % num.col(i); dY.row(i) = arma::sum(tmp, 0); } momentum = (iter < MOMENTUM_THRESHOLD_ITER) ? INITIAL_MOMENTUM : FINAL_MOMENTUM; gains = (gains + GAIN_FRACTION) % ((dY > 0) != (iY > 0)) + (gains * (1 - GAIN_FRACTION)) % ((dY > 0) == (iY > 0)); gains.transform([](double v) { return std::max(v, MIN_GAIN); }); iY = momentum * iY - ETA * (gains % dY); Y += iY; Y.each_row() -= mean(Y, 0); if (iter == EXAGGERATION_THRESHOLD_ITER) { P /= EARLY_EXAGGERATION; // remove early exaggeration } } } static void calcP(const arma::mat &X, arma::mat &P, double perplexity, double tol) { arma::colvec sumX = arma::sum(X % X, 1); arma::mat D = -2 * (X * X.t()); D.each_col() += sumX; arma::inplace_trans(D); D.each_col() += sumX; D.diag() *= 0; double logU = log(perplexity); arma::rowvec beta(X.n_rows, arma::fill::ones); arma::rowvec Pi(X.n_rows); for (arma::uword i = 0; i < X.n_rows; i++) { double betaMin = -arma::datum::inf; double betaMax = arma::datum::inf; arma::rowvec Di = D.row(i); double h = hBeta(Di, beta[i], Pi); double hDiff = h - logU; for (int tries = 0; fabs(hDiff) > tol && tries < MAX_BINSEARCH_TRIES; tries++) { if (hDiff > 0) { betaMin = beta[i]; if (betaMax == arma::datum::inf || betaMax == -arma::datum::inf) { beta[i] *= 2; } else { beta[i] = (beta[i] + betaMax) / 2.; } } else { betaMax = beta[i]; if (betaMin == arma::datum::inf || betaMin == -arma::datum::inf) { beta[i] /= 2; } else { beta[i] = (beta[i] + betaMin) / 2.; } } h = hBeta(Di, beta[i], Pi); hDiff = h - logU; } P.row(i) = Pi; } } static inline double hBeta(const arma::rowvec &Di, double beta, arma::rowvec &Pi) { Pi = arma::exp(-Di * beta); double sumPi = arma::accu(Pi); double h = log(sumPi) + beta * arma::accu(Di % Pi) / sumPi; Pi /= sumPi; return h; }