Damped Newton’s Method with Self-Concordant Function

Ziwen Wang

February 16, 2026

1 Theoretical discussion

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 f : m is strictly convex and twice differentiable. Newton’s method can be thought of finding the minimizer of Taylor’s second order approximation,

f(x) f(xk) + x xk,f(xk) + 1 2(x xk)2f(x k)(x xk).

The exact minimizer assuming strict convexity is unique and has the following form,

argxm{f(xk) + x xk,f(xk) + 1 2(x xk)2f(x k)(x xk)} = xk (2f(x k))1f(x).

Let us define the decrement direction at iteration k as dk = (2f(xk))1f(x), then the update step can be rewritten as,

xk+1 = xk + αkdk,αk = 1.

With step size αk = 1, 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 x0 is close enough to minimizer and with the assumption that the function f is strongly convex and Lipschtiz continuous.

Lemma 1 (Newton’s method is affine invariant). Given an invertible matrix A m×m and a convex function f : m . Let xi,x~i be newton iterates of f(x),f~(x) := f(Ax) with starting point x0,x~0 := A1x0 respectively. Then x~i = A1xi.

Proof. Notice that x~f~(x~) = AT xf(Ax) and x~2f~(Ax) = AT x2f(Ax)A and by induction we have

x~i+1 = x~i (2f(x~ i))1f(x~ i) = x~i A12f(x i)AT AT f(x i) = x~i A12f(x i)f(xi) = A1x i A12f(x i)f(xi) = A1x i+1.

Note at optimality we have f(x) = 0, so one way to measure convergence is by bounding upper bounding f(x), but notice that f(x) is not affine invariant i.e. f(x)f(Ax) so instead we have the following measure,

λf(x) := (f(x))T (2 f(x))1 f(x) =: f(x)(2f(x))1

which becomes affine invariant and can be interpreted as an induced norm by the PD matrix (2f(x))1. So now instead of bounding on f(x) we can try to bound λf(x).

Under the assumption that f is self concordant with constant 1 one can show that λf(xk+1) λf(xk) 1λf(xk)2 (in order for it to decrease we need λf(xk) < 1 ), but this still assumes a good starting point (which is the condition λf(xk) < 1). 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 f is called self-concordant if there exists a constant Mf > 0 such that

|D3f(x)[u,u,u]| 2M fu2f(x)3

holds for all x domf m and u m.

We now give three different stepping sizes and discuss their convergence properties, their advantage and disadvantages.

1.
(Standard Newton’s method) αk = 1,
2.
(Damped Newton’s method) αk = 1 1+Mfλk,
3.
(Intermediate Newton’s method) αk = 1 1+(Mfλk)2 1+Mfλk, where λk := λf(xk).

From above it is not hard to see that the following inequality of the steps sizes holds,

(1.) (3.) (2.)

Method (1.) uses the most aggressive and (2.) uses the least aggressive stepping size, (3.) is somewhere in the middle hence the name intermediate. So from here can almost expect that if (2.) doesn’t converge then so does (3.) and (1.). NOTE: Method (3.) always generates a monotonically decreasing sequence i.e. f(xk+1) f(xk), and indeed this also holds for method (2.) as given in the following theorem.

Theorem 3 (Global Convergence). [1] The following holds for any k > 0 and let xk denote the newton iterates,

1.
(Standard Newton’s method) No guarantee on monotonically decreasing.
2.
(Damped Newton’s method) f(xk+1) f(xk) 1 Mf2ω(Mfλf(xk)),

where ω(x) = x ln(1 + x).

3.
(Intermediate Newton’s method) f(xk+1) f(xk) [ λf(xk)2 2 i=02(Mfλf(xk))2 + λf(xk)2 2(1 + Mfλf(xk))(3 + 2Mfλf(xk)))]

From the above global convergence theorem we can see that if λf(xk) 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 x dom(f) and λ = λf(x), let x be the point generated by one step of newton’s method (either (1.),(2.),or (3.)). Then x domf and the following holds,

1.
(Standard Newton’s method) If λMf < 1 then λf(x) ( Mfλ 1 Mfλ)2.

2.
(Damped Newton’s method) λf(x) M fλ2(1 + 1 1 + Mfλ).

3.
(Intermediate Newton’s method) If i=13(Mfλ)i 1 then λf(x) M fλ2(1 + M fλ + 1 + Mfλ 1 + Mfλ + (Mfλ)2) Mfλ2(1 + 2M fλ).

Notice that even though the stepping size of (1.) is more aggressive the convergence analysis doesn’t actually yield a better bound compared to (3.) or (2.) (why? consider when λMf 1 then it yields a very bad bound), this suggests that we should completely abandon (1.). Now method (2.) is almost a linear convergence since when λMf is very big then the term Mfλ2 1+Mfλ dominates Method (2.) does not require any condition on how close our current iterate is to the optimal solution so the idea is then always run method (2.) then switch to method (3.) when we satisfy the condition i=13(Mfλ)i 1 which is yields a quadratic convergence at the end.

2 Implementation

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,

Input: Initial guess x0, function f, derivative f k 0; while Not converged do xk+1 xk + αkdk; k k + 1; end Algorithm 1: Newton’s Method

Both αk,dk 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.
First this method does not allow compiler optimiztion in particular function inlining. To my knowledge, nor runtime polymorphism can achieve this (though I think with more aggressive compiler optimization you can still achieve this). So instead we will need compile time polymorphism (speficially CRTP Curiously Recurring Template Pattern) that uses template. Just an aside: One may ask why would function inlining be important here? Since the only difference between enabling compile time polymorphism and runtime polymorphism here is to inline the actual call to gradient/hessian and no matter what we do the calls inside of gradient/hessian would already be optimized so the only difference is that we do not need to pay the overhead of virtual table in runtime polymorphism? Not really, usually computing the gradient and hessian could involve the same intermediate computation, if we then inlined both gradient/hessian then compiler’s can avoid the same computation.
2.
If we used compile time polymorphism then usually this creates a bunch of weird error messages that are harder to resolve. To address this problem and to increase the readability of our code we use Concept which acts as a compile time contract.
3.
Recently I got pretty interested into template programming and how it can be used for compile time optimization and so I decided to implement some toy example.
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.

References

[1]   Yurii Nesterov. Lectures on Convex Optimization, volume 137 of Springer Optimization and Its Applications. Springer, 2 edition, 2018.