/* Non-Negagive Least Squares Algorithm for Eigen. * * Copyright (C) 2021 Essex Edwards, * Copyright (C) 2013 Hannes Matuschek, hannes.matuschek at uni-potsdam.de * * This Source Code Form is subject to the terms of the Mozilla * Public License v. 2.0. If a copy of the MPL was not distributed * with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** \defgroup nnls Non-Negative Least Squares (NNLS) Module * This module provides a single class @c Eigen::NNLS implementing the NNLS algorithm. * The algorithm is described in "SOLVING LEAST SQUARES PROBLEMS", by Charles L. Lawson and * Richard J. Hanson, Prentice-Hall, 1974 and solves optimization problems of the form * * \f[ \min \left\Vert Ax-b\right\Vert_2^2\quad s.t.\, x\ge 0\,.\f] * * The algorithm solves the constrained least-squares problem above by iteratively improving * an estimate of which constraints are active (elements of \f$x\f$ equal to zero) * and which constraints are inactive (elements of \f$x\f$ greater than zero). * Each iteration, an unconstrained linear least-squares problem solves for the * components of \f$x\f$ in the (estimated) inactive set and the sets are updated. * The unconstrained problem minimizes \f$\left\Vert A^Nx^N-b\right\Vert_2^2\f$, * where \f$A^N\f$ is a matrix formed by selecting all columns of A which are * in the inactive set \f$N\f$. * */ #ifndef EIGEN_NNLS_H #define EIGEN_NNLS_H #include "../../Eigen/Core" #include "../../Eigen/QR" #include namespace Eigen { /** \ingroup nnls * \class NNLS * \brief Implementation of the Non-Negative Least Squares (NNLS) algorithm. * \tparam MatrixType The type of the system matrix \f$A\f$. * * This class implements the NNLS algorithm as described in "SOLVING LEAST SQUARES PROBLEMS", * Charles L. Lawson and Richard J. Hanson, Prentice-Hall, 1974. This algorithm solves a least * squares problem iteratively and ensures that the solution is non-negative. I.e. * * \f[ \min \left\Vert Ax-b\right\Vert_2^2\quad s.t.\, x\ge 0 \f] * * The algorithm solves the constrained least-squares problem above by iteratively improving * an estimate of which constraints are active (elements of \f$x\f$ equal to zero) * and which constraints are inactive (elements of \f$x\f$ greater than zero). * Each iteration, an unconstrained linear least-squares problem solves for the * components of \f$x\f$ in the (estimated) inactive set and the sets are updated. * The unconstrained problem minimizes \f$\left\Vert A^Nx^N-b\right\Vert_2^2\f$, * where \f$A^N\f$ is a matrix formed by selecting all columns of A which are * in the inactive set \f$N\f$. * * See the * wikipedia page on non-negative least squares for more background information. * * \note Please note that it is possible to construct an NNLS problem for which the * algorithm does not converge. In practice these cases are extremely rare. */ template class NNLS { public: typedef MatrixType_ MatrixType; enum { RowsAtCompileTime = MatrixType::RowsAtCompileTime, ColsAtCompileTime = MatrixType::ColsAtCompileTime, Options = MatrixType::Options, MaxRowsAtCompileTime = MatrixType::MaxRowsAtCompileTime, MaxColsAtCompileTime = MatrixType::MaxColsAtCompileTime }; typedef typename MatrixType::Scalar Scalar; typedef typename MatrixType::RealScalar RealScalar; typedef typename MatrixType::Index Index; /** Type of a row vector of the system matrix \f$A\f$. */ typedef Matrix SolutionVectorType; /** Type of a column vector of the system matrix \f$A\f$. */ typedef Matrix RhsVectorType; typedef Matrix IndicesType; /** */ NNLS(); /** \brief Constructs a NNLS sovler and initializes it with the given system matrix @c A. * \param A Specifies the system matrix. * \param max_iter Specifies the maximum number of iterations to solve the system. * \param tol Specifies the precision of the optimum. * This is an absolute tolerance on the gradient of the Lagrangian, \f$A^T(Ax-b)-\lambda\f$ * (with Lagrange multipliers \f$\lambda\f$). */ NNLS(const MatrixType &A, Index max_iter = -1, Scalar tol = NumTraits::dummy_precision()); /** Initializes the solver with the matrix \a A for further solving NNLS problems. * * This function mostly initializes/computes the preconditioner. In the future * we might, for instance, implement column reordering for faster matrix vector products. */ template NNLS &compute(const EigenBase &A); /** \brief Solves the NNLS problem. * * The dimension of @c b must be equal to the number of rows of @c A, given to the constructor. * * \returns The approximate solution vector \f$ x \f$. Use info() to determine if the solve was a success or not. * \sa info() */ const SolutionVectorType &solve(const RhsVectorType &b); /** \brief Returns the solution if a problem was solved. * If not, an uninitialized vector may be returned. */ const SolutionVectorType &x() const { return x_; } /** \returns the tolerance threshold used by the stopping criteria. * \sa setTolerance() */ Scalar tolerance() const { return tolerance_; } /** Sets the tolerance threshold used by the stopping criteria. * * This is an absolute tolerance on the gradient of the Lagrangian, \f$A^T(Ax-b)-\lambda\f$ * (with Lagrange multipliers \f$\lambda\f$). */ NNLS &setTolerance(const Scalar &tolerance) { tolerance_ = tolerance; return *this; } /** \returns the max number of iterations. * It is either the value set by setMaxIterations or, by default, twice the number of columns of the matrix. */ Index maxIterations() const { return max_iter_ < 0 ? 2 * A_.cols() : max_iter_; } /** Sets the max number of iterations. * Default is twice the number of columns of the matrix. * The algorithm requires at least k iterations to produce a solution vector with k non-zero entries. */ NNLS &setMaxIterations(Index maxIters) { max_iter_ = maxIters; return *this; } /** \returns the number of iterations (least-squares solves) performed during the last solve */ Index iterations() const { return iterations_; } /** \returns Success if the iterations converged, and an error values otherwise. */ ComputationInfo info() const { return info_; } private: /** \internal Adds the given index @c idx to the inactive set N and updates the QR decomposition of \f$A^N\f$. */ void moveToInactiveSet_(Index idx); /** \internal Removes the given index idx from the inactive set N and updates the QR decomposition of \f$A^N\f$. */ void moveToActiveSet_(Index idx); /** \internal Solves the least-squares problem \f$\left\Vert y-A^Nx\right\Vert_2^2\f$. */ void solveInactiveSet_(const RhsVectorType &b); private: typedef Matrix MatrixAtAType; /** \internal Holds the maximum number of iterations for the NNLS algorithm. * @c -1 means to use the default value. */ Index max_iter_; /** \internal Holds the number of iterations. */ Index iterations_; /** \internal Holds success/fail of the last solve. */ ComputationInfo info_; /** \internal Size of the inactive set. */ Index numInactive_; /** \internal Accuracy of the algorithm w.r.t the optimality of the solution (gradient). */ Scalar tolerance_; /** \internal The system matrix, a copy of the one given to the constructor. */ MatrixType A_; /** \internal Precomputed product \f$A^TA\f$. */ MatrixAtAType AtA_; /** \internal Will hold the solution. */ SolutionVectorType x_; /** \internal Will hold the current gradient.\f$A^Tb - A^TAx\f$ */ SolutionVectorType gradient_; /** \internal Will hold the partial solution. */ SolutionVectorType y_; /** \internal Precomputed product \f$A^Tb\f$. */ SolutionVectorType Atb_; /** \internal Holds the current permutation partitioning the active and inactive sets. * The first @c numInactive_ elements form the inactive set and the rest the active set. */ IndicesType index_sets_; /** \internal QR decomposition to solve the (inactive) sub system (together with @c qrCoeffs_). */ MatrixType QR_; /** \internal QR decomposition to solve the (inactive) sub system (together with @c QR_). */ SolutionVectorType qrCoeffs_; /** \internal Some workspace for QR decomposition. */ SolutionVectorType tempSolutionVector_; RhsVectorType tempRhsVector_; }; /* ******************************************************************************************** * Implementation * ******************************************************************************************** */ template NNLS::NNLS() : max_iter_(-1), iterations_(0), info_(ComputationInfo::InvalidInput), numInactive_(0), tolerance_(NumTraits::dummy_precision()) {} template NNLS::NNLS(const MatrixType &A, Index max_iter, Scalar tol) : max_iter_(max_iter), tolerance_(tol) { compute(A); } template template NNLS &NNLS::compute(const EigenBase &A) { // Ensure Scalar type is real. The non-negativity constraint doesn't obviously extend to complex numbers. EIGEN_STATIC_ASSERT(!NumTraits::IsComplex, NUMERIC_TYPE_MUST_BE_REAL); // max_iter_: unchanged iterations_ = 0; info_ = ComputationInfo::Success; numInactive_ = 0; // tolerance: unchanged A_ = A.derived(); AtA_.noalias() = A_.transpose() * A_; x_.resize(A_.cols()); gradient_.resize(A_.cols()); y_.resize(A_.cols()); Atb_.resize(A_.cols()); index_sets_.resize(A_.cols()); QR_.resize(A_.rows(), A_.cols()); qrCoeffs_.resize(A_.cols()); tempSolutionVector_.resize(A_.cols()); tempRhsVector_.resize(A_.rows()); return *this; } template const typename NNLS::SolutionVectorType &NNLS::solve(const RhsVectorType &b) { // Initialize solver iterations_ = 0; info_ = ComputationInfo::NumericalIssue; x_.setZero(); index_sets_ = IndicesType::LinSpaced(A_.cols(), 0, A_.cols() - 1); // Identity permutation. numInactive_ = 0; // Precompute A^T*b Atb_.noalias() = A_.transpose() * b; const Index maxIterations = this->maxIterations(); // OUTER LOOP while (true) { // Early exit if all variables are inactive, which breaks 'maxCoeff' below. if (A_.cols() == numInactive_) { info_ = ComputationInfo::Success; return x_; } // Find the maximum element of the gradient in the active set. // If it is small or negative, then we have converged. // Else, we move that variable to the inactive set. gradient_.noalias() = Atb_ - AtA_ * x_; const Index numActive = A_.cols() - numInactive_; Index argmaxGradient = -1; const Scalar maxGradient = gradient_(index_sets_.tail(numActive)).maxCoeff(&argmaxGradient); argmaxGradient += numInactive_; // beacause tail() skipped the first numInactive_ elements if (maxGradient < tolerance_) { info_ = ComputationInfo::Success; return x_; } moveToInactiveSet_(argmaxGradient); // INNER LOOP while (true) { // Check if max. number of iterations is reached if (iterations_ >= maxIterations) { info_ = ComputationInfo::NoConvergence; return x_; } // Solve least-squares problem in inactive set only, // this step is rather trivial as moveToInactiveSet_ & moveToActiveSet_ // updates the QR decomposition of inactive columns A^N. // solveInactiveSet_ puts the solution in y_ solveInactiveSet_(b); ++iterations_; // The solve is expensive, so that is what we count as an iteration. // Check feasability... bool feasible = true; Scalar alpha = NumTraits::highest(); Index infeasibleIdx = -1; // Which variable became infeasible first. for (Index i = 0; i < numInactive_; i++) { Index idx = index_sets_[i]; if (y_(idx) < 0) { // t should always be in [0,1]. Scalar t = -x_(idx) / (y_(idx) - x_(idx)); if (alpha > t) { alpha = t; infeasibleIdx = i; feasible = false; } } } eigen_assert(feasible || 0 <= infeasibleIdx); // If solution is feasible, exit to outer loop if (feasible) { x_ = y_; break; } // Infeasible solution -> interpolate to feasible one for (Index i = 0; i < numInactive_; i++) { Index idx = index_sets_[i]; x_(idx) += alpha * (y_(idx) - x_(idx)); } // Remove these indices from the inactive set and update QR decomposition moveToActiveSet_(infeasibleIdx); } } } template void NNLS::moveToInactiveSet_(Index idx) { // Update permutation matrix: std::swap(index_sets_(idx), index_sets_(numInactive_)); numInactive_++; // Perform rank-1 update of the QR decomposition stored in QR_ & qrCoeff_ internal::householder_qr_inplace_update(QR_, qrCoeffs_, A_.col(index_sets_(numInactive_ - 1)), numInactive_ - 1, tempSolutionVector_.data()); } template void NNLS::moveToActiveSet_(Index idx) { // swap index with last inactive one & reduce number of inactive columns std::swap(index_sets_(idx), index_sets_(numInactive_ - 1)); numInactive_--; // Update QR decomposition starting from the removed index up to the end [idx, ..., numInactive_] for (Index i = idx; i < numInactive_; i++) { Index col = index_sets_(i); internal::householder_qr_inplace_update(QR_, qrCoeffs_, A_.col(col), i, tempSolutionVector_.data()); } } template void NNLS::solveInactiveSet_(const RhsVectorType &b) { eigen_assert(numInactive_ > 0); tempRhsVector_ = b; // tmpRHS(0:numInactive_-1) := Q'*b // tmpRHS(numInactive_:end) := useless stuff we would rather not compute at all. tempRhsVector_.applyOnTheLeft( householderSequence(QR_.leftCols(numInactive_), qrCoeffs_.head(numInactive_)).transpose()); // tempSol(0:numInactive_-1) := inv(R) * Q' * b // = the least-squares solution for the inactive variables. tempSolutionVector_.head(numInactive_) = // QR_.topLeftCorner(numInactive_, numInactive_) // .template triangularView() // .solve(tempRhsVector_.head(numInactive_)); // // tempSol(numInactive_:end) := 0 = the value for the constrained variables. tempSolutionVector_.tail(y_.size() - numInactive_).setZero(); // Back permute into original column order of A y_.noalias() = index_sets_.asPermutation() * tempSolutionVector_.head(y_.size()); } } // namespace Eigen #endif // EIGEN_NNLS_H