Skip to content

Commit

Permalink
feat: upload WIP implementation of SLOPE
Browse files Browse the repository at this point in the history
  • Loading branch information
jolars committed Nov 21, 2023
0 parents commit cbe8ab3
Show file tree
Hide file tree
Showing 29 changed files with 1,578 additions and 0 deletions.
47 changes: 47 additions & 0 deletions src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Prerequisites
*.d

# Compiled Object files
*.slo
*.lo
*.o
*.obj

# Precompiled Headers
*.gch
*.pch

# Compiled Dynamic libraries
*.so
*.dylib
*.dll

# Fortran module files
*.mod
*.smod

# Compiled Static libraries
*.lai
*.la
*.a
*.lib

# Executables
*.exe
*.out
*.app

CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
build/

.cache/
Empty file added src/CMakeLists.txt
Empty file.
140 changes: 140 additions & 0 deletions src/slope/cd.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#pragma once

#include "clusters.h"
#include "slope_threshold.h"
#include "sorted_l1_norm.h"
#include <RcppEigen.h>
#include <vector>

namespace slope {

template<typename T>
void
coordinateDescent(double& beta0,
Eigen::VectorXd& beta,
Eigen::VectorXd& residual,
Clusters& clusters,
const T& x,
const Eigen::VectorXd& w,
const Eigen::VectorXd& z,
const SortedL1Norm& sl1_norm,
const Eigen::VectorXd& x_centers,
const Eigen::VectorXd& x_scales,
bool intercept,
bool standardize,
bool update_clusters,
int print_level)
{
using namespace Eigen;

const int n = x.rows();

for (int j = 0; j < clusters.n_clusters(); ++j) {
double c_old = clusters.coeff(j);

if (c_old == 0) {
// We do not update the zero cluster because it can be very large, but
// often does not change.
continue;
}

std::vector<int> s;
int cluster_size = clusters.cluster_size(j);
s.reserve(cluster_size);

double hessian_j = 1;
double gradient_j = 0;
VectorXd x_s(n);

if (cluster_size == 1) {
int k = *clusters.cbegin(j);
double s_k = sign(beta(k));
s.emplace_back(s_k);

if (standardize) {
gradient_j = -s_k *
(x.col(k).cwiseProduct(w).dot(residual) -
w.dot(residual) * x_centers(k)) /
(n * x_scales(k));
// TODO: Consider caching the hessian values. We need to invalidate the
// cache every time the cluster is updated or the signs flip relatively
// inside the cluster.
hessian_j =
(x.col(k).cwiseAbs2().dot(w) - 2 * x_centers(k) * x.col(k).dot(w) +
std::pow(x_centers(k), 2) * w.sum()) /
(std::pow(x_scales(k), 2) * n);
} else {
gradient_j = -s_k * x.col(k).cwiseProduct(w).dot(residual) / n;
hessian_j = x.col(k).cwiseAbs2().dot(w) / n;
}
} else {
// There's no reasonable just-in-time standardization approach for sparse
// design matrices when there are clusters in the data, so we need to
// reduce to a dense column vector.
x_s.setZero();

for (auto c_it = clusters.cbegin(j); c_it != clusters.cend(j); ++c_it) {
int k = *c_it;
double s_k = sign(beta(k));
s.emplace_back(s_k);

if (standardize) {
x_s += x.col(k) * (s_k / x_scales(k));
x_s.array() -= x_centers(k) * s_k / x_scales(k);
} else {
x_s += x.col(k) * s_k;
}
}

hessian_j = x_s.cwiseAbs2().dot(w) / n;
gradient_j = -x_s.cwiseProduct(w).dot(residual) / n;
}

auto thresholding_results =
slopeThreshold(c_old - gradient_j / hessian_j,
j,
sl1_norm.lambda * sl1_norm.getAlpha() / (hessian_j),
clusters);

double c_tilde = thresholding_results.value;
int new_index = thresholding_results.new_index;

auto s_it = s.cbegin();
auto c_it = clusters.cbegin(j);
for (; c_it != clusters.cend(j); ++c_it, ++s_it) {
beta(*c_it) = c_tilde * (*s_it);
}

double c_diff = c_old - c_tilde;

if (c_diff != 0) {
if (cluster_size == 1) {
int k = *clusters.cbegin(j);

if (standardize) {
residual += x.col(k) * (s[0] * c_diff / x_scales(k));
residual.array() -= x_centers(k) * s[0] * c_diff / x_scales(k);
} else {
residual += x.col(k) * (s[0] * c_diff);
}
} else {
residual += x_s * c_diff;
}
}

if (update_clusters) {
clusters.update(j, new_index, std::abs(c_tilde));
} else {
clusters.setCoeff(j, std::abs(c_tilde));
}

if (intercept) {
double beta0_update = residual.dot(w) / w.sum();
residual.array() -= beta0_update;
beta0 += beta0_update;
}

Rcpp::checkUserInterrupt();
}
}
}
206 changes: 206 additions & 0 deletions src/slope/clusters.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#include "clusters.h"

namespace slope {

Clusters::Clusters(const Eigen::VectorXd& beta)
{
update(beta);
}

std::vector<int>::iterator
Clusters::begin(const int i)
{
return c_ind.begin() + c_ptr[i];
}

std::vector<int>::iterator
Clusters::end(const int i)
{
return c_ind.begin() + c_ptr[i + 1];
}

std::vector<int>::const_iterator
Clusters::cbegin(const int i) const
{
return c_ind.cbegin() + c_ptr[i];
}

std::vector<int>::const_iterator
Clusters::cend(const int i) const
{
return c_ind.cbegin() + c_ptr[i + 1];
}

int
Clusters::cluster_size(const int i) const
{
return c_ptr[i + 1] - c_ptr[i];
}

int
Clusters::pointer(const int i) const
{
return c_ptr[i];
}

int
Clusters::n_clusters() const
{
return c.size();
}

double
Clusters::coeff(const int i) const
{
return c[i];
}

void
Clusters::setCoeff(const int i, const double x)
{
c[i] = x;
}

std::vector<double>
Clusters::coeffs() const
{
return c;
}

std::vector<int>
Clusters::indices() const
{
return c_ind;
}

std::vector<int>
Clusters::pointers() const
{
return c_ptr;
}

void
Clusters::update(const int old_index, const int new_index, const double c_new)
{
auto c_old = coeff(old_index);

if (c_new != c_old) {
if (c_new == coeff(new_index)) {
merge(old_index, new_index);
} else {
setCoeff(old_index, c_new);
if (old_index != new_index) {
reorder(old_index, new_index);
}
}
}
}

void
Clusters::update(const Eigen::VectorXd& beta)
{
using sort_pair = std::pair<double, int>;

c.clear();
c_ind.clear();
c_ptr.clear();

std::vector<sort_pair> sorted;
sorted.reserve(beta.size());

for (int i = 0; i < beta.size(); ++i) {
sorted.emplace_back(std::abs(beta(i)), i);
}

std::sort(sorted.begin(), sorted.end(), std::greater<sort_pair>());

c_ind.reserve(sorted.size());
for (const auto& sorted_i : sorted) {
c_ind.emplace_back(sorted_i.second);
}

std::vector<sort_pair> sorted_unique;
std::unique_copy(sorted.begin(),
sorted.end(),
std::back_inserter(sorted_unique),
[](const sort_pair& a, const sort_pair& b) -> bool {
return a.first == b.first;
});

c.reserve(sorted_unique.size());
for (const auto& sorted_unique_i : sorted_unique) {
c.emplace_back(std::move(sorted_unique_i.first));
}

c_ptr.reserve(c.size() + 1);
c_ptr.emplace_back(0);

auto range_start = sorted.begin();
for (const auto& c_i : c) {
auto range_end = std::find_if(
std::next(range_start), sorted.end(), [&c_i](const sort_pair& x) -> bool {
return x.first != c_i;
});
c_ptr.emplace_back(std::distance(range_start, range_end));
range_start = range_end;
}

std::partial_sum(c_ptr.cbegin(), c_ptr.cend(), c_ptr.begin());
}

void
Clusters::reorder(const int old_index, const int new_index)
{
auto c_size = cluster_size(old_index);

// update coefficients
move_elements(c, old_index, new_index, 1);

// update indices
move_elements(c_ind, pointer(old_index), pointer(new_index), c_size);

// update pointers
if (new_index < old_index) {
move_elements(c_ptr, old_index + 1, new_index + 1, 1);

std::for_each(c_ptr.begin() + new_index + 1,
c_ptr.begin() + old_index + 2,
[c_size](int& x) { x += c_size; });

c_ptr[new_index + 1] = c_ptr[new_index] + c_size;
} else {
move_elements(c_ptr, old_index, new_index, 1);

std::for_each(c_ptr.begin() + old_index,
c_ptr.begin() + new_index,
[c_size](int& x) { x -= c_size; });
c_ptr[new_index] = c_ptr[new_index + 1] - c_size;
}
}

void
Clusters::merge(const int old_index, const int new_index)
{
auto c_size = cluster_size(old_index);

// update coefficients
c.erase(c.cbegin() + old_index);

// update indices
move_elements(c_ind, pointer(old_index), pointer(new_index), c_size);

// update pointers
if (new_index < old_index) {
std::for_each(c_ptr.begin() + new_index + 1,
c_ptr.begin() + old_index + 1,
[c_size](int& x) { x += c_size; });
} else {
std::for_each(c_ptr.begin() + old_index + 1,
c_ptr.begin() + new_index + 1,
[c_size](int& x) { x -= c_size; });
}

c_ptr.erase(c_ptr.begin() + old_index + 1);
}

} // namespace slope
Loading

0 comments on commit cbe8ab3

Please sign in to comment.