Notas para contenedor de docker:

Comando de docker para ejecución de la nota de forma local:

nota: cambiar <ruta a mi directorio> por la ruta de directorio que se desea mapear a /datos dentro del contenedor de docker.

docker run --rm -v <ruta a mi directorio>:/datos --name jupyterlab_numerical -p 8888:8888 -d palmoreck/jupyterlab_numerical:1.1.0

password para jupyterlab: qwerty

Detener el contenedor de docker:

docker stop jupyterlab_numerical

Documentación de la imagen de docker palmoreck/jupyterlab_numerical:1.1.0 en liga.


Nota basada en liga1, liga2

En esta nota se consideran resolver problemas de la forma:

$$\min f_o(x)$$

con $f_o:\mathbb{R}^n \rightarrow \mathbb{R}$ fuertemente convexa y $f_o \in \mathcal{C}^2(\text{dom}f_o)$ para buscar óptimos locales. Además se asume que los puntos iniciales $x^{(0)}$ de los métodos iterativos están en $\text{dom}f_o$ y los conjuntos $f_o(x^{(0)})$-subnivel son conjuntos cerrados. Ver 1.4.Polinomios_de_Taylor_y_diferenciacion_numerica y 4.1.Optimizacion_numerica_y_machine_learning para definiciones utilizadas en esta nota.

También se asume que existe un punto óptimo $x^*$ por lo que el problema tiene solución y el valor óptimo se denota por $p^* = f_o(x^*) = \inf f_o(x)$

Las suposición que una función $f$ sea convexa asegura que una condición necesaria y suficiente para que $x^*$ sea óptimo es: $\nabla f(x^*) = 0$ la cual es en general es un conjunto de $n$ ecuaciones no lineales en $n$ variables y que resuelve el problema de optimización planteado al inicio.

Método de Newton

Método de Newton como un caso particular del método steepest descent bajo la norma cuadrática

Consideramos con la norma cuadrática: $P=\nabla ^2 f_o(x^*)$ el método steepest descent:

$$\Delta x_{\text{nsd}} = \text{argmin} \{ \nabla f_o(x)^Tv : ||v||_P \leq 1, \nabla f_o(x)^Tv < 0 \} $$

se prueba que $\Delta x_{\text{sd}} = - P^{-1} \nabla f_o(x)$.

Ver 4.2.Algoritmos_para_optimizacion_sin_restricciones. Entonces:

Algoritmo de Newton (visto como un caso particular del método steepest descent para matrices Hessianas simétricas definidas positivas)

Dado un punto inicial $x$ en $\text{dom}f_o$

Repetir el siguiente bloque para $k=0,1,2,...$

  1. Calcular la Hessiana de $f_o$ y definir $P$ como $P=\nabla ^2 f_o(x^*)$ con $x^*$ óptimo de $f_o$.
  2. Transformar $\hat{x}$ con: $x = P^{-1/2} \hat{x}$ con $\hat{x}$ variable del problema transformado*.
  3. Calcular la dirección de descenso en gradiente $\Delta \hat{x} = - \nabla \hat{f}_o(\hat{x}) = - \nabla f_o(P^{-1/2}\hat{x})$.
  4. Búsqueda de línea. Elegir un tamaño de paso $t > 0$.
  5. Hacer la actualización: $\hat{x} = \hat{x} + t\Delta \hat{x}$.

hasta convergencia (satisfacer criterio de paro).

*El problema transformado bajo $P$ es:

$$min \hat{f}_o(\hat{x})$$

con $\hat{f}_o (\hat{x}) = f_o(P^{-1/2}\hat{x}) = f_o(x)$ y $x$ variable "original": $\hat{x} = P^{1/2}x$.

Entonces aplicamos descenso en gradiente a $f_o$ pero transformando los datos de entrada de $f_o$ por la matriz raíz cuadrada simétrica* $P^{-1/2}$.

*$P^{1/2}$ se nombra raíz cuadrada simétrica o symmetric squareroot y está definida para matrices $P$ simétricas semidefinidas positivas como $P^{1/2}=Qdiag(\lambda_1^{1/2},\dots,\lambda_n^{1/2})Q^T$ con $Q$ y $diag(\lambda_1^{1/2},\dots,\lambda_n^{1/2})$ obtenidas con la descomposición espectral de $P$, ver 3.3.d.SVD.

Obs: en el cambio de coordenadas dado por $\hat{x} = P^{1/2}x$ y por tanto $\hat{f}_o({\hat{x}}) = f_o(P^{-1/2}\hat{x})$ se tiene $\nabla ^2 \hat{f}_o(\hat{x}) = P^{-1/2} \nabla f_o^2(x) P^{-1/2}$. Por lo que si $P$ se toma igual a $\nabla^2 f_o (x^*)$ entonces $P^{-1/2} \nabla f^2(x) P^{-1/2} = I$ (recuérdese que $P$ se asume simétrica definida positiva).

Nota: los pasos en el algoritmo anterior representan una guía para la implementación. Al describirse los pasos de un algoritmo no implica que se tengan que implementar uno a continuación del otro como se describe. Si una implementación respeta la lógica y al mismo método, entonces pueden seguirse los pasos de una forma distinta.


In [1]:
import math
import matplotlib.pyplot as plt
import numpy as np

Funciones auxiliares


In [2]:
def inc_index(vec,index,h):
    '''
    Auxiliary function for gradient and Hessian computation.
    Args:
        vec (array): numpy array.
        index (int): index.
        h (float):   quantity that vec[index] will be increased.
    Returns:
        vec (array): numpy array vec with vec[index] increased by h.
    '''
    vec[index] +=h
    return vec

In [3]:
def dec_index(vec,index,h=1):
    '''
    Auxiliary function for gradient and Hessian computation.
    Args:
        vec (array): numpy array.
        index (int): index.
        h (float):   quantity that vec[index] will be decreased.
    Returns:
        vec (array): numpy array vec with vec[index] decreased by h.
    '''
    vec[index] -=h
    return vec

In [4]:
def gradient_approximation(f,x,h=1e-8):
    '''
    Numerical approximation of gradient for function f using forward differences.
    Args:
        f (lambda expression): definition of function f.
        x (array): numpy array that holds values where gradient will be computed.
        h (float): step size for forward differences, tipically h=1e-8
    Returns:
        gf (array): numerical approximation to gradient of f.
    '''
    n = x.size
    gf = np.zeros(n)
    f_x = f(x)
    for i in np.arange(n):
        inc_index(x,i,h)
        gf[i] = f(x) - f_x
        dec_index(x,i,h)
    return gf/h

In [5]:
def Hessian_approximation(f,x,h=1e-6):
    '''
    Numerical approximation of Hessian for function f using forward differences.
    Args:
        f (lambda expression): definition of function f.
        x (array): numpy array that holds values where Hessian will be computed.
        h (float): step size for forward differences, tipically h=1e-6
    Returns:
        Hf (array): numerical approximation to Hessian of f.
    '''
    n = x.size
    Hf = np.zeros((n,n))
    f_x = f(x)
    for i in np.arange(n):
        inc_index(x,i,h)
        f_x_inc_in_i = f(x)
        for j in np.arange(i,n):
            inc_index(x,j,h)
            f_x_inc_in_i_j = f(x)
            dec_index(x,i,h)
            f_x_inc_in_j = f(x)
            dif = f_x_inc_in_i_j-f_x_inc_in_i-f_x_inc_in_j+f_x
            Hf[i,j] = dif
            if j != i:
                Hf[j,i] = dif
            dec_index(x,j,h)
            inc_index(x,i,h)
        dec_index(x,i,h)
    return Hf/h**2

In [6]:
def line_search_by_backtracking(f,dir_desc,x,
                                der_direct, alpha=.15, beta=.5):
    """
    Line search that sufficiently decreases f restricted to a ray in the direction dir_desc.
    Args:
        alpha (float): parameter in line search with backtracking, tipically .15
        beta (float): parameter in line search with backtracking, tipically .5
        f (lambda expression): definition of function f.
        dir_desc (array): descent direction.
        x (array): numpy array that holds values where line search will be performed.
        der_direct (float): directional derivative of f.
    Returns:
        t (float): positive number for stepsize along dir_desc that sufficiently decreases f.
    """
    t=1
    if alpha > 1/2:
        print('alpha must be less than or equal to 1/2')
        t=-1
    if beta>1:
        print('beta must be less than 1')
        t=-1;   
    if t!=-1:
        eval1 = f(x+t*dir_desc)
        eval2 = f(x) + alpha*t*der_direct
        while eval1 > eval2:
            t=beta*t
            eval1=f(x+t*dir_desc)
            eval2=f(x)+alpha*t*der_direct
    return t

In [7]:
def compute_error(x_obj,x_approx):
    '''
    Relative or absolute error between x_obj and x_approx.
    '''
    if np.linalg.norm(x_obj) > np.nextafter(0,1):
        Err=np.linalg.norm(x_obj-x_approx)/np.linalg.norm(x_obj)
    else:
        Err=np.linalg.norm(x_obj-x_approx)
    return Err

In [8]:
def gradient_descent(f, x_0, tol, 
                     tol_backtracking, x_ast=None, p_ast=None, maxiter=30):
    '''
    Method of gradient descent to numerically approximate solution of min f.
    Args:
        f (lambda expression): definition of function f.
        x_0 (numpy ndarray): initial point for gradient descent method.
        tol (float): tolerance that will halt method. Controls norm of gradient of f.
        tol_backtracking (float): tolerance that will halt method. Controls value of line search by backtracking.
        x_ast (numpy ndarray): solution of min f, now it's required that user knows the solution...
        p_ast (float): value of f(x_ast), now it's required that user knows the solution...
        maxiter (int): maximum number of iterations
    Returns:
        x (numpy ndarray): numpy array, approximation of x_ast.
        iteration (int): number of iterations.
        Err_plot (numpy ndarray): numpy array of absolute error between p_ast and f(x) with x approximation
                                  of x_ast. Useful for plotting.
        x_plot (numpy ndarray): numpy array that containts in columns vector of approximations. Last column
                                contains x, approximation of solution. Useful for plotting.
    '''
    iteration = 0
    x = x_0
    
    feval = f(x)
    gfeval = gradient_approximation(f,x)

    normgf = np.linalg.norm(gfeval)
    
    Err_plot_aux = np.zeros(maxiter)
    Err_plot_aux[iteration]=compute_error(p_ast,feval)
    
    Err = compute_error(x_ast,x)
    n = x.size
    x_plot = np.zeros((n,maxiter))
    x_plot[:,iteration] = x
    
    print('I\tNormagf\t\tError x_ast\tError p_ast\tline search')
    print('{}\t{:0.2e}\t{:0.2e}\t{:0.2e}\t{}'.format(iteration,normgf,Err,Err_plot_aux[iteration],"---"))
    iteration+=1
    while(normgf>tol and iteration < maxiter):
        dir_desc = -gfeval
        der_direct = gfeval.dot(dir_desc)
        t = line_search_by_backtracking(f,dir_desc,x,der_direct)
        x = x + t*dir_desc
        feval = f(x)
        gfeval = gradient_approximation(f,x)
        normgf = np.linalg.norm(gfeval)
        Err_plot_aux[iteration]=compute_error(p_ast,feval)
        x_plot[:,iteration] = x
        Err = compute_error(x_ast,x)
        print('{}\t{:0.2e}\t{:0.2e}\t{:0.2e}\t{:0.2e}'.format(iteration,normgf,Err,
                                                                      Err_plot_aux[iteration],t))
        if t<tol_backtracking: #if t is less than tol_backtracking then we need to check the reason
            iter_salida=iteration
            iteration = maxiter - 1
        iteration+=1
    print('{} {:0.2e}'.format("Error of x with respect to x_ast:",Err))
    print('{} {}'.format("Approximate solution:", x))
    cond = Err_plot_aux > np.finfo(float).eps*10**(-2)
    Err_plot = Err_plot_aux[cond]
    if iteration == maxiter and t < tol_backtracking:
        print("Backtracking value less than tol_backtracking, check approximation")
        iteration=iter_salida
    else:
        if iteration == maxiter:
            print("Reached maximum of iterations, check approximation")    
    x_plot = x_plot[:,:iteration]
    return [x,iteration,Err_plot,x_plot]

Ejemplo

1) $$\min \quad e^{(x_1+3x_2-0.1)}+e^{x_1-3x_2-0.1}+e^{-x_1-0.1}$$


In [9]:
fo = lambda x: math.exp(x[0]+3*x[1]-0.1)+math.exp(x[0]-3*x[1]-0.1)+math.exp(-x[0]-0.1)

In [10]:
x_ast=np.array([-0.3465735941251887,0], dtype=float)

In [11]:
P=Hessian_approximation(fo,x_ast)

In [12]:
P


Out[12]:
array([[ 2.55884203e+00, -4.44089210e-04],
       [-4.44089210e-04,  1.15165655e+01]])

In [13]:
def coord_transform_original_variable(x):
    eigenvalues, eigenvectors = np.linalg.eig(P)       
    return eigenvectors@((eigenvalues**(-1/2))*(np.transpose(eigenvectors)@x))

In [14]:
def coord_transform(x):
    eigenvalues, eigenvectors = np.linalg.eig(P)       
    return eigenvectors@((eigenvalues**(1/2))*(np.transpose(eigenvectors)@x))

In [15]:
fo_transf = lambda x: fo(coord_transform_original_variable(x))

In [16]:
p_ast=fo(x_ast)

In [17]:
x_ast = coord_transform(x_ast)

In [18]:
x_ast


Out[18]:
array([-5.54392322e-01,  3.08235635e-05])

In [19]:
x_0 = np.array([0,0],dtype=float)
tol=1e-8
tol_backtracking=1e-14
maxiter=50

[x,total_of_iterations,Err_plot,x_plot]=gradient_descent(fo_transf, x_0,tol, 
                                                         tol_backtracking, x_ast, p_ast, maxiter)


I	Normagf		Error x_ast	Error p_ast	line search
0	5.66e-01	1.00e+00	6.07e-02	---
1	1.13e-02	2.03e-02	2.48e-05	1.00e+00
2	2.03e-06	3.60e-06	7.72e-13	1.00e+00
3	8.88e-08	5.82e-08	3.47e-16	1.00e+00
4	0.00e+00	2.33e-08	0.00e+00	5.00e-01
Error of x with respect to x_ast: 2.33e-08
Approximate solution: [-5.54392310e-01  3.08197912e-05]

In [20]:
x


Out[20]:
array([-5.54392310e-01,  3.08197912e-05])

In [21]:
total_of_iterations


Out[21]:
5

In [22]:
Err_plot.shape


Out[22]:
(4,)

In [23]:
plt.yscale('log') #escala logarítmica en el eje vertical
plt.plot(np.arange(Err_plot.size),Err_plot,'.-')
plt.ylabel('Log Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iterations',size=12)
plt.grid()
plt.show()



In [24]:
x_plot.shape


Out[24]:
(2, 5)

In [25]:
x_plot[:,0]


Out[25]:
array([0., 0.])

In [26]:
x_plot[:,-1]


Out[26]:
array([-5.54392310e-01,  3.08197912e-05])

In [27]:
plt.plot(x_plot[0,:],x_plot[1,:],'-*')
plt.ylabel('y')
plt.xlabel('x')
plt.annotate('$x^{(0)}$',(x_plot[0,0],x_plot[1,0]),fontsize=12)
plt.title('Iteraciones del método de descenso en gradiente para $\hat{f}_o$')
plt.grid()
plt.show()



In [28]:
plt.plot(np.arange(Err_plot.size-2),Err_plot[2:],'.-')
plt.ylabel('Error relativo entre: $\hat{f}_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iteraciones del método descenso en gradiente para $\hat{f}_o$')
plt.grid()
plt.show()



In [29]:
def coord_transform_original_variable_2(x):
    eigenvalues, eigenvectors = np.linalg.eig(P)      
    return eigenvectors@((eigenvalues**(-1/2)).reshape(x.shape[0],1)*(eigenvectors@x))

In [30]:
z=lambda x_mesh,y_mesh: np.exp(x_mesh+3*y_mesh-0.1)+np.exp(x_mesh-3*y_mesh-0.1)+np.exp(-x_mesh-0.1)

Curvas de nivel de $f_o$ transformada


In [31]:
density=1e-1
xl=-1.5
yl=-1
xr=.5
yr=1
x_p=np.arange(xl,xr,density)
y_p=np.arange(yl,yr,density)
x_y_p_t = coord_transform_original_variable_2(np.row_stack((x_p,y_p)))
x_p_t = x_y_p_t[0,:]
y_p_t = x_y_p_t[1,:]
x_mesh,y_mesh = np.meshgrid(x_p_t,y_p_t)
plt.xlim(-1.5, .5)
plt.ylim(-1, 1)
plt.contour(x_p,y_p,z(x_mesh,y_mesh))
plt.plot(x_plot[0,:],x_plot[1,:],'-*')
plt.annotate('$x^{(0)}$',(x_plot[0,0],x_plot[1,0]),fontsize=12)
plt.grid()
plt.title("Curvas de nivel de $\hat{f}_o$")
plt.show()



In [32]:
x_k = x_plot[:,-1]
Hf_x_k = Hessian_approximation(fo_transf,x_k)
x_k = x_k.reshape(2,1)
norm_cuad = lambda z : np.sum((z-x_k)*(Hf_x_k@(z-x_k)),axis=0)
density_p=int(2.5*10**4)
z_p=np.random.uniform(-2,1,(2,density_p))
lim_sup=.9
lim_inf=.85
ind=(norm_cuad(z_p)<=lim_sup) & (lim_inf<=norm_cuad(z_p))
z_p_subset = z_p[:,ind]
plt.xlim(-1.6, .5)
plt.ylim(-1, 1)
plt.scatter(z_p_subset[0,:],z_p_subset[1,:],marker='.')
plt.title('Puntos en el plano que cumplen $||x|| \leq 1$ para norma cuadrática usando la segunda derivada de $\hat{f}_o$ evaluada en la última iteración')
plt.grid()
plt.show()



In [33]:
np.linalg.cond(Hf_x_k)


Out[33]:
1.0008884940026654

Obsérvese que la Hessiana con la transformación de $f_o$ está muy bien condicionada.

Método de Newton

La dirección de descenso $\Delta x_{\text{nt}} = -\nabla ^2 f_o(x) \nabla f(x)$ para $x \in \text{dom}f_o$ se le nombra paso de Newton para $f_o$ en $x$.

Obs: si $\nabla ^2 f_o(x)$ es simétrica definida positiva en $\text{dom}f_o$ y $x$ no es óptimo entonces $\Delta x_{\text{nt}}$ es dirección de descenso pues: $\nabla f_o(x)^T \Delta x_\text{nt} = -\nabla f_o(x)^T \nabla^2f_o(x)^{-1} \nabla f(x) < 0 $ con $x \in \text{dom}f_o$ y $\nabla f(x) \neq 0$.

El paso de Newton puede obtenerse de diferentes formas:

1) Utilizando la aproximación de segundo orden por Taylor $\hat{f}_o$ de $f_o$ en $x$:

$$\hat{f}_o(x+v) \approx f_o(x) + \nabla f_o(x)^Tv + \frac{1}{2} v^T \nabla ^2 f_o(x) v$$

la cual si $\nabla ^2 f_o(x)$ es definida positiva en $\text{dom}f_o$ entonces es una función cuadrática convexa (estricta) en $v$ cuyo mínimo es $\hat{v} = \Delta x_\text{nt}$:

En el dibujo anterior $\hat{f}$ es un modelo cuadrático, $\Delta x_{nt}$ es dirección de descenso de Newton y $x^*$ es el óptimo de $f$. Del punto $(x,f(x))$ nos debemos mover al punto $(x+\Delta x_{nt}, f(x + \Delta x_{nt}))$ para llegar al óptimo y el valor de $f$ decrece: $f(x+\Delta x_{nt}) < f(x)$.

Comentario: encontrar el mínimo de $f_o(x) + \nabla f_o(x)^Tv + \frac{1}{2} v^T \nabla ^2 f_o(x) v$ es un problema de mínimos cuadrados, ver 4.1.Optimizacion_numerica_y_machine_learning.

2) Como un caso particular del método steepest descent (visto al inicio de la nota).

3) Utilizando la aproximación lineal de la condición de optimalidad $\nabla f(x^*) = 0$. Para valores cercanos a $x$ se tiene:

$$\nabla \hat{f}(x+v) \approx \nabla f(x) + \nabla ^2 f_o(x) v = 0$$

entonces $\Delta x_{\text{nt}}$ es la solución de esta ecuación y es lo que debe añadirse a $x$ para que la condición de linealidad se satisfaga. Si $x$ es cercano a $x^*$ entonces $x + \Delta x_{\text{nt}}$ es una buena aproximación a $x^*$:

Decremento de Newton

Una cantidad que se utiliza como criterio de paro del algoritmo y resultados de convergencia del método, es el decremento de Newton para $f_o$ en $x$ definido como:

$$\lambda (x) = (\nabla f(x) ^T \nabla ^2 f(x)^{-1} \nabla f(x))^{1/2}.$$

Esta cantidad tiene propiedades como son:

  • $\frac{1}{2} \lambda ^2 (x)$ estima $f(x)-p^*$.

  • $|| \nabla^2 f(x)^{-1} \nabla f(x)||_{\nabla ^2f(x)} = \left ( \nabla f(x)^T \nabla^2 f(x)^{-1} \nabla ^2 f(x) \nabla ^2 f(x)^{-1} \nabla f(x) \right )^{1/2} = \lambda(x) $ que indica que $\lambda(x)$ es la norma del paso de Newton en la norma cuadrática definida por la Hessiana.

  • En el método de búsqueda de línea por backtracking $-\lambda (x) ^2$ es la derivada direccional de $f$ en $x$ en la direccióin de $\Delta x_{\text{nt}}$:

$$\frac{df(x+t \Delta x_{\text{nt}})}{dt} \Bigr|_{t=0} = \nabla f(x)^T \Delta x_{\text{nt}} = \nabla f(x)^T (-\nabla^2 f(x)^{-1} \nabla f(x)) = -\lambda(x)^2.$$

Algoritmo de Newton para problemas de optimización sin restricciones

Dado un punto inicial $x$ en $\text{dom}f_o$ y una tolerancia $\epsilon >0$.

Repetir el siguiente bloque para $k=0,1,2,...$

  1. Calcular la dirección de descenso de Newton $\Delta x_{\text{nt}} = - \nabla ^2 f_o(x)^{-1} \nabla f_o(x)$ y el decremento de Newton al cuadrado: $\lambda^2(x)=\nabla f(x)^T \nabla ^2 f(x)^{-1} \nabla f(x)$.
  2. Criterio de paro: finalizar el método si $\frac{\lambda^2(x)}{2} \leq \epsilon$.
  3. Búsqueda de línea. Elegir un tamaño de paso $t > 0$ (usar el cálculo de $\lambda (x)$ del paso anterior).
  4. Hacer la actualización: $x = x + t\Delta x_{\text{nt}}$.

hasta convergencia (satisfacer criterio de paro).

Nota: los pasos en el algoritmo anterior representan una guía para la implementación. Al describirse los pasos de un algoritmo no implica que se tengan que implementar uno a continuación del otro como se describe. Si una implementación respeta la lógica y al mismo método, entonces pueden seguirse los pasos de una forma distinta.


In [35]:
def Newtons_method(f, x_0, tol, 
                   tol_backtracking, x_ast=None, p_ast=None, maxiter=30):
    '''
    Newton's method to numerically approximate solution of min f.
    Args:
        f (lambda expression): definition of function f.
        x_0 (numpy ndarray): initial point for Newton's method.
        tol (float): tolerance that will halt method. Controls stopping criteria.
        tol_backtracking (float): tolerance that will halt method. Controls value of line search by backtracking.
        x_ast (numpy ndarray): solution of min f, now it's required that user knows the solution...
        p_ast (float): value of f(x_ast), now it's required that user knows the solution...
        maxiter (int): maximum number of iterations
    Returns:
        x (numpy ndarray): numpy array, approximation of x_ast.
        iteration (int): number of iterations.
        Err_plot (numpy ndarray): numpy array of absolute error between p_ast and f(x) with x approximation
                          of x_ast. Useful for plotting.
        x_plot (numpy ndarray): numpy array that containts in columns vector of approximations. Last column
                        contains x, approximation of solution. Useful for plotting.
    '''
    iteration = 0
        
    x = x_0
    
    feval = f(x)
    gfeval = gradient_approximation(f,x)
    Hfeval = Hessian_approximation(f,x)
    
    normgf = np.linalg.norm(gfeval)
    condHf= np.linalg.cond(Hfeval)
    
    Err_plot_aux = np.zeros(maxiter)
    Err_plot_aux[iteration]=compute_error(p_ast,feval)
    
    Err = compute_error(x_ast,x)
    n = x.size
    x_plot = np.zeros((n,maxiter))
    x_plot[:,iteration] = x
    
    #Newton's direction and Newton's decrement
    
    dir_Newton = np.linalg.solve(Hfeval, -gfeval)
    dec_Newton = -gfeval.dot(dir_Newton)
    
    print('I\tNormgf \tNewton Decrement\tError x_ast\tError p_ast\tline search\tCondHf')
    print('{}\t{:0.2e}\t{:0.2e}\t{:0.2e}\t{:0.2e}\t{}\t\t{:0.2e}'.format(iteration,normgf,
                                                                         dec_Newton,Err,
                                                                         Err_plot_aux[iteration],"---",
                                                                         condHf))
    stopping_criteria = dec_Newton/2
    iteration+=1
    while(stopping_criteria>tol and iteration < maxiter):
        der_direct = -dec_Newton
        t = line_search_by_backtracking(f,dir_Newton,x,der_direct)
        x = x + t*dir_Newton
        feval = f(x)
        gfeval = gradient_approximation(f,x)
        Hfeval = Hessian_approximation(f,x)
        normgf = np.linalg.norm(gfeval)
        condHf= np.linalg.cond(Hfeval)
        #Newton's direction and Newton's decrement

        dir_Newton = np.linalg.solve(Hfeval, -gfeval)
        dec_Newton = -gfeval.dot(dir_Newton)
        Err_plot_aux[iteration]=compute_error(p_ast,feval)
        x_plot[:,iteration] = x
        Err = compute_error(x_ast,x)
        print('{}\t{:0.2e}\t{:0.2e}\t{:0.2e}\t{:0.2e}\t{:0.2e}\t{:0.2e}'.format(iteration,normgf,
                                                                                dec_Newton,Err,
                                                                                Err_plot_aux[iteration],t,
                                                                                condHf))
        stopping_criteria = dec_Newton/2
        if t<tol_backtracking: #if t is less than tol_backtracking then we need to check the reason
            iter_salida=iteration
            iteration = maxiter - 1
        iteration+=1
    print('{} {:0.2e}'.format("Error of x with respect to x_ast:",Err))
    print('{} {}'.format("Approximate solution:", x))
    cond = Err_plot_aux > np.finfo(float).eps*10**(-2)
    Err_plot = Err_plot_aux[cond]
    if iteration == maxiter and t < tol_backtracking:
        print("Backtracking value less than tol_backtracking, check approximation")
        iteration=iter_salida
    else:
        if iteration == maxiter:
            print("Reached maximum of iterations, check approximation")
    x_plot = x_plot[:,:iteration]
    return [x,iteration,Err_plot,x_plot]

Ejemplo:

1) $$\min \quad e^{(x_1+3x_2-0.1)}+e^{x_1-3x_2-0.1}+e^{-x_1-0.1}$$


In [36]:
fo = lambda x: math.exp(x[0]+3*x[1]-0.1)+math.exp(x[0]-3*x[1]-0.1)+math.exp(-x[0]-0.1)

In [37]:
x_ast=np.array([-0.3465735941251887,0], dtype=float)

In [38]:
x_ast


Out[38]:
array([-0.34657359,  0.        ])

In [39]:
p_ast=fo(x_ast)

In [40]:
x_0 = np.array([0,0],dtype=float)
tol=1e-8
tol_backtracking=1e-14
maxiter=50

[x,total_of_iterations,Err_plot,x_plot]=Newtons_method(fo, x_0,tol, 
                                                       tol_backtracking, x_ast, p_ast, maxiter)


I	Normgf 	Newton Decrement	Error x_ast	Error p_ast	line search	CondHf
0	9.05e-01	3.02e-01	1.00e+00	6.07e-02	---		6.00e+00
1	3.38e-02	4.47e-04	3.81e-02	8.73e-05	1.00e+00	4.56e+00
2	7.11e-07	1.97e-13	8.03e-07	3.99e-14	1.00e+00	4.50e+00
Error of x with respect to x_ast: 8.03e-07
Approximate solution: [-3.46573873e-01 -4.43632609e-09]

In [41]:
x


Out[41]:
array([-3.46573873e-01, -4.43632609e-09])

In [42]:
total_of_iterations


Out[42]:
3

In [43]:
Err_plot.shape


Out[43]:
(3,)

In [44]:
plt.yscale('log') #escala logarítmica en el eje vertical
plt.plot(np.arange(Err_plot.size),Err_plot,'.-')
plt.ylabel('Log Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iterations',size=12)
plt.grid()
plt.show()



In [45]:
x_plot.shape


Out[45]:
(2, 3)

In [46]:
x_plot[:,0]


Out[46]:
array([0., 0.])

In [47]:
x_plot[:,-1]


Out[47]:
array([-3.46573873e-01, -4.43632609e-09])

In [48]:
plt.plot(x_plot[0,:],x_plot[1,:],'-*')
plt.ylabel('y')
plt.xlabel('x')
plt.annotate('$x^{(0)}$',(x_plot[0,0],x_plot[1,0]),fontsize=12)
plt.title('Iteraciones del método de Newton para $f_o$')
plt.grid()
plt.show()



In [51]:
plt.plot(np.arange(Err_plot.size-1),Err_plot[1:],'.-')
plt.ylabel('$f_o(x^k)-p^*$',size=12)
plt.xlabel('Iteraciones del método de Newton para $f_o$')
plt.grid()
plt.show()


Ejemplo: no influencia del número de condición

2) El método de Newton es insensible a la forma de las curvas de nivel de la función objetivo $f_o$. Para observar esto considérese el problema: $$\min \frac{1}{2}\left(x_1^2+Cx_2^2 \right)$$

La solución del problema anterior es $x^*=(0,0)^T$.

Método de descenso en gradiente con $x_0=(0.5,0.5)^T$.

Método de Newton para caso $C=10$


In [52]:
cte=10

In [53]:
fo = lambda x: 1/2*(x[0]**2+cte*x[1]**2)

In [54]:
x_ast=np.array([0,0],dtype=float)

In [55]:
p_ast=fo(x_ast)

In [56]:
x_0 = np.array([0.5,0.5],dtype=float)
tol=1e-8
tol_backtracking=1e-14
maxiter=50
[x,total_of_iterations,Err_plot,x_plot]=Newtons_method(fo, x_0, 
                                                       tol, tol_backtracking, x_ast, p_ast, maxiter)


I	Normgf 	Newton Decrement	Error x_ast	Error p_ast	line search	CondHf
0	5.02e+00	2.75e+00	7.07e-01	1.38e+00	---		1.00e+01
1	2.10e-04	3.28e-08	1.78e-04	1.64e-08	1.00e+00	1.00e+01
2	5.47e-16	4.33e-32	7.07e-09	1.37e-16	1.00e+00	1.00e+01
Error of x with respect to x_ast: 7.07e-09
Approximate solution: [-4.99999988e-09 -4.99999995e-09]

In [57]:
x


Out[57]:
array([-4.99999988e-09, -4.99999995e-09])

In [58]:
total_of_iterations


Out[58]:
3

In [59]:
Err_plot.shape


Out[59]:
(3,)

In [60]:
plt.yscale('log') #escala logarítmica en el eje vertical
plt.plot(np.arange(Err_plot.size),Err_plot,'.-')
plt.ylabel('Log Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iterations',size=12)
plt.grid()
plt.show()



In [61]:
x_plot[:,0]


Out[61]:
array([0.5, 0.5])

In [62]:
x_plot.shape


Out[62]:
(2, 3)

In [63]:
x_plot[:,-1]


Out[63]:
array([-4.99999988e-09, -4.99999995e-09])

In [64]:
plt.figure(figsize=(8,4))
plt.plot(x_plot[0,:],x_plot[1,:],'-*')
plt.ylabel('y')
plt.xlabel('x')
plt.annotate('$x^{(0)}$',(x_plot[0,0],x_plot[1,0]),fontsize=12,xytext=(0.5, 0.47))
plt.title('Iteraciones del método de Newton para $f_o$ con $C=10$')
plt.grid()
plt.show()



In [65]:
plt.plot(np.arange(Err_plot.size-1),Err_plot[1:],'.-')
plt.ylabel('Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iteraciones del método de Newton para $f_o$ con $C=10$')
plt.grid()
plt.show()


Comentarios:

  • Tanto el paso de Newton como el decremento de Newton son invariantes ante transformaciones afines. Por ejemplo para el paso de Newton: si $T \in \mathbb{R}^{n \times n}$ es no singular y $\hat{f}_o: \mathbb{R}^n \rightarrow \mathbb{R}$ está definida como $\hat{f}_o(\hat{x}) = f_o(T^{-1}x)$ entonces se puede probar que $\Delta \hat{x}_{\text{nt}} = T^{-1} \Delta x_{\text{nt}}$ con $\Delta x_{\text{nt}}$ el paso de Newton para $f_o$ en $x$. Esto indica que los pasos de Newton están relacionados por la misma transformación lineal $T$, de hecho: $x + \Delta x_{\text{nt}} = T(\hat{x} + \Delta \hat{x}_{\text{nt}})$.

  • Contrástese el punto anterior con el método de descenso en gradiente que es afectado fuertemente por el cambio de coordenadas, ver 4.2.Descenso_en_gradiente.

  • Al igual que en el método de descenso en gradiente los valores de $\alpha$ y $\beta$ en el método de búsqueda de línea por backtracking tienen un efecto pequeño en el performance del método de Newton. Típicamente se utilizan $\alpha=0.01, .15$ y $\beta = 0.5$.

  • El método de Newton puede dividirse en dos etapas:

    • La etapa damped o guarded que se caracteriza por valores de búsqueda de línea por backtracking menores a $1$: $t < 1$.

    • La etapa pure que se caracteriza por valores de búsqueda de línea por backtracking iguales a $1$: $t =1$ . En esta etapa el método de Newton tiene una convergencia cuadrática. Una vez en esta etapa le toma no más de $6$ iteraciones al método para obtener una aproximación a $p^*$ muy buena (precisiones mayores o iguales a $8$ dígitos).

  • El número de condición de la Hessiana de $f_o$ (o el de los conjuntos subnivel) afectan al método de Newton únicamente en los problemas numéricos al calcular el paso de Newton $\Delta x_{\text{nt}}$ (al resolver un sistema de ecuaciones lineales) y no en el número de iteraciones.
  • En la práctica se ha observado que el método de Newton escala bien con el tamaño del problema: ejemplos de su performance para problemas con variables en $\mathbb{R}^{10^4}$ muestran un ligero incremento en el número de iteraciones que en problemas en $\mathbb{R}^{10}$.

  • La gran desventaja del método de Newton radica en construir, almacenar a la Hessiana y resolver el sistema de ecuaciones lineales asociado que típicamente es costoso. Para estos casos se sugieren:

    • Utilizar una familia de algoritmos para optimización sin restricciones conocida con el nombre Quasi Newton los cuales requieren un menor costo computacional para calcular la dirección de búsqueda y tienen propiedades en común con el método de Newton como convergencia cuadrática para iteraciones cercanas a $x^*$.

Referencias:

  • S. P. Boyd, L. Vandenberghe, Convex Optimization, Cambridge University Press, 2009.