In this writing we will review how damped newton method works and provide an implementation of it. Throughout, we will assume that the given function is strictly convex and twice differentiable. Newton’s method can be thought of finding the minimizer of Taylor’s second order approximation,
The exact minimizer assuming strict convexity is unique and has the following form,
Let us define the decrement direction at iteration as , then the update step can be rewritten as,
With step size , this update rule is referred as the pure newton step, while this seems fine but it only converges (in fact quadratically) if the initial starting point is close enough to minimizer and with the assumption that the function is strongly convex and Lipschtiz continuous.
Lemma 1 (Newton’s method is affine invariant). Given an invertible matrix and a convex function . Let be newton iterates of with starting point respectively. Then .
Proof. Notice that and and by induction we have
□Note at optimality we have , so one way to measure convergence is by bounding upper bounding , but notice that is not affine invariant i.e. so instead we have the following measure,
which becomes affine invariant and can be interpreted as an induced norm by the PD matrix . So now instead of bounding on we can try to bound .
Under the assumption that is self concordant with constant 1 one can show that (in order for it to decrease we need ), but this still assumes a good starting point (which is the condition ). It turns out if we change the step size then we are able to achieve global convergence but before we do so, we shall define self-concordant functions more formally.
Definition 2 (Self-concordant function). A function is called self-concordant if there exists a constant such that
holds for all and .
We now give three different stepping sizes and discuss their convergence properties, their advantage and disadvantages.
From above it is not hard to see that the following inequality of the steps sizes holds,
Method uses the most aggressive and uses the least aggressive stepping size, is somewhere in the middle hence the name intermediate. So from here can almost expect that if doesn’t converge then so does and . NOTE: Method always generates a monotonically decreasing sequence i.e. , and indeed this also holds for method as given in the following theorem.
Theorem 3 (Global Convergence). [1] The following holds for any and let denote the newton iterates,
where .
From the above global convergence theorem we can see that if is relatively large then Damped Newton’s method would be better. We have the following theorem for their convergence behavior.
Theorem 4 (Convergence Rate). [1] Let and , let be the point generated by one step of newton’s method (either ). Then and the following holds,
Notice that even though the stepping size of is more aggressive the convergence analysis doesn’t actually yield a better bound compared to or (why? consider when then it yields a very bad bound), this suggests that we should completely abandon . Now method is almost a linear convergence since when is very big then the term dominates Method does not require any condition on how close our current iterate is to the optimal solution so the idea is then always run method then switch to method when we satisfy the condition which is yields a quadratic convergence at the end.
We wish to adapt Newton’s method to any function we wish including log barrier functions and potentially many other barrier functions. Essentially Newton’s algorithm can be abstracted as follows,
Both may depend on the gradient and hessian. The condition to check convergence may depend on the function value, gradient, current solution, and current iterates. There are lots of ways to do this but one easy way is to: Implement a main newton routine and let the user provide lambda functions to compute the required information. But there are several caveats to this approach that I can think of,
1#ifndef NEWTON 2#define NEWTON 3 4#include <Eigen/Dense> 5 6using namespace Eigen; 7 8template <typename T> 9concept ObjectiveImpl = requires(const T &obj, const VectorXd &x, int it) { 10 { obj.value_impl(x) } -> std::convertible_to<double>; 11 { obj.gradient_impl(x) } -> std::convertible_to<VectorXd>; 12 { obj.hessian_impl(x) } -> std::convertible_to<MatrixXd>; 13 { obj.newtonDirection_impl(x) } -> std::same_as<VectorXd>; 14 { obj.converged_impl(x, it) } -> std::same_as<bool>; 15 { obj.stepsize_impl(x, it) } -> std::same_as<double>; 16}; 17 18template <class Derived> class ObjectiveBase { 19public: 20 double value(const VectorXd& x) const { 21 return static_cast<const Derived*>(this)->value_impl(x); 22 } 23 24 VectorXd gradient(const VectorXd& x) const { 25 return static_cast<const Derived*>(this)->gradient_impl(x); 26 } 27 28 MatrixXd hessian(const VectorXd& x) const { 29 return static_cast<const Derived*>(this)->hessian_impl(x); 30 } 31 32 MatrixXd newtonDirection(const VectorXd& x) const { 33 return static_cast<const Derived*>(this)->newtonDirection_impl(x); 34 } 35 36 bool converged(const VectorXd& x, int it) const { 37 return static_cast<const Derived*>(this)->converged_impl(x, it); 38 } 39 40 double stepsize(const VectorXd& x, int it) const { 41 return static_cast<const Derived*>(this)->stepsize_impl(x, it); 42 } 43}; 44 45template <ObjectiveImpl Obj> 46class NewtonSolver { 47public: 48 NewtonSolver(Obj& objective) : obj_(objective) {} 49 50 bool solve() { 51 int i = 0; 52 while(!obj_.converged(cur_sol_, i)) { 53 cur_sol_ = cur_sol_ + obj_.stepsize(cur_sol_, i) * obj_.newtonDirection(cur_sol_); 54 ++i; 55 } 56 return true; 57 } 58 59 // TODO: Currently max_iter_ and tol_ are not used... 60 NewtonSolver& set_max_iter(int n) { max_iter_ = n; return *this; } 61 NewtonSolver& set_tolerance_max_iter(double tol) { tol_ = tol; return *this; } 62 NewtonSolver& set_initial_sol(const VectorXd& x) { cur_sol_ = x; return *this; } 63 VectorXd get_sol() { return cur_sol_; } 64private: 65 Obj& obj_; 66 int max_iter_ = 100; 67 double tol_ = 1e-8; 68 VectorXd cur_sol_; 69}; 70 71#endif // NEWTON
As on how to use this library, we need to provide an derived class that inherits from ObjectiveBase that provides all the information NewtonSolver needs. We provide an example of the log barrier function,
1#ifndef LOGBARRIER 2#define LOGBARRIER 3 4#include "newton.h" 5 6struct LogBarrier : ObjectiveBase<LogBarrier> { 7 double value_impl(const VectorXd& x) const; 8 VectorXd gradient_impl(const VectorXd& x) const; 9 MatrixXd hessian_impl(const VectorXd& x) const; 10 VectorXd newtonDirection_impl(const VectorXd& x) const; 11 bool converged_impl(const VectorXd& x, int it) const; 12 double stepsize_impl(const VectorXd& x, int it) const; 13 14 // Assume of the program min c^T x s.t. Ax <= b. 15 LogBarrier(const MatrixXd& A, const VectorXd& b, const VectorXd& c, double t = 1) : A_(A), b_(b), c_(c), t_(t), m_(A.rows()), n_(A.cols()) {} 16 17private: 18 MatrixXd A_; 19 VectorXd b_; 20 VectorXd c_; 21 double t_; 22 int m_, n_; 23}; 24 25#endif // LOGBARRIER
1#include "logbarrier.h" 2 3double LogBarrier::value_impl(const VectorXd& x) const { 4 return t_ * c_.dot(x) - (b_ - A_ * x).array().log().sum(); 5} 6 7VectorXd LogBarrier::gradient_impl(const VectorXd& x) const { 8 VectorXd s = b_ - A_ * x; 9 return t_ * c_ + A_.transpose() * s.cwiseInverse(); 10} 11 12MatrixXd LogBarrier::hessian_impl(const VectorXd& x) const { 13 VectorXd s = b_ - A_ * x; 14 VectorXd s_inv_sq = s.array().square().inverse(); 15 return A_.transpose() * s_inv_sq.asDiagonal() * A_; 16} 17 18VectorXd LogBarrier::newtonDirection_impl(const VectorXd& x) const { 19 return -(hessian_impl(x).lu().solve(gradient_impl(x))); 20} 21 22bool LogBarrier::converged_impl(const VectorXd& x, int it) const { 23 return it >= 27; 24} 25 26double LogBarrier::stepsize_impl(const VectorXd& x, int it) const { 27 // TODO: Should optimize this to resuse previously computed values if possible. 28 double lambda = sqrt(gradient_impl(x).dot(hessian_impl(x).lu().solve(gradient_impl(x)))); 29 double alpha; 30 31 double m_lambda = m_*lambda; 32 33 if (m_lambda <= 0.5) { 34 alpha = (1+m_lambda)/(1+m_lambda+m_lambda*m_lambda); 35 } else { 36 alpha = 1/(1+m_lambda); 37 } 38 39 return alpha; 40}
Further detailed code can be found on my github page.