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 descenso en gradiente

Algoritmo de descenso en gradiente

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

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

  1. Calcular la dirección de descenso $\Delta x = - \nabla f_o(x)$.
  2. Búsqueda de línea. Elegir un tamaño de paso $t > 0$.
  3. Hacer la actualización: $x = x + t\Delta x$.

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 [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 [8]:
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

Descenso en gradiente


In [9]:
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 [10]:
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 (x_1-2)^2+(2-x_2)^2+x_3^2+x_4^4$$

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

Método de descenso gradiente con $x_0=(5,5,1,0)^T$:


In [11]:
fo = lambda x: (x[0]-2)**2 + (2-x[1])**2 + x[2]**2 + x[3]**4
x_ast = np.array([2,2,0,0],dtype=float)
x_0 = np.array([5,5,1,0],dtype=float)
tol=1e-8
tol_backtracking=1e-14
maxiter=5
p_ast=fo(x_ast)
[x,total_of_iterations,Err_plot,x_plot]=gradient_descent(fo, x_0, tol, tol_backtracking, x_ast, p_ast, maxiter)


I	Normagf		Error x_ast	Error p_ast	line search
0	8.72e+00	1.54e+00	1.90e+01	---
1	3.53e-07	6.10e-08	2.98e-14	5.00e-01
2	2.14e-15	3.06e-09	7.50e-17	5.00e-01
Error of x with respect to x_ast: 3.06e-09
Approximate solution: [ 2.e+00  2.e+00 -5.e-09  0.e+00]

In [12]:
x


Out[12]:
array([ 2.e+00,  2.e+00, -5.e-09,  0.e+00])

In [13]:
total_of_iterations


Out[13]:
3

In [14]:
Err_plot


Out[14]:
array([1.90000000e+01, 2.97654832e-14, 7.49999861e-17])

In [15]:
x_plot


Out[15]:
array([[ 5.00000000e+00,  2.00000011e+00,  2.00000000e+00],
       [ 5.00000000e+00,  2.00000011e+00,  2.00000000e+00],
       [ 1.00000000e+00, -8.27403711e-08, -5.00000000e-09],
       [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00]])

In [16]:
plt.plot(np.arange(Err_plot.size),Err_plot,'o-')
plt.ylabel('Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iterations',size=12)
plt.grid()
plt.show()


Comentario: en esta gráfica se observa una convergencia lineal del método de descenso en gradiente para $f_o$.

Ejemplo: influencia del número de condición

2) El método de descenso en gradiente es altamente sensible 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$.

Descenso en gradiente para caso $C=10$


In [17]:
cte=10

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

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


I	Normagf		Error x_ast	Error p_ast	line search
0	5.02e+00	7.07e-01	1.38e+00	---
1	1.32e+00	4.55e-01	1.74e-01	1.25e-01
2	4.94e-01	3.84e-01	7.82e-02	1.25e-01
3	5.50e-01	2.91e-01	5.22e-02	2.50e-01
4	2.77e-01	2.51e-01	3.22e-02	1.25e-01
5	4.85e-01	1.34e-01	1.89e-02	5.00e-01
6	1.61e-01	1.11e-01	6.73e-03	1.25e-01
7	1.94e-01	8.43e-02	4.94e-03	2.50e-01
8	8.45e-02	7.23e-02	2.70e-03	1.25e-01
9	8.53e-02	5.45e-02	1.68e-03	2.50e-01
10	1.07e-01	4.18e-02	1.31e-03	2.50e-01
11	4.33e-02	3.56e-02	6.61e-04	1.25e-01
12	4.56e-02	2.69e-02	4.23e-04	2.50e-01
13	2.51e-02	2.33e-02	2.76e-04	1.25e-01
14	3.89e-02	1.22e-02	1.37e-04	5.00e-01
15	1.38e-02	1.02e-02	5.62e-05	1.25e-01
16	1.59e-02	7.77e-03	3.89e-05	2.50e-01
17	7.54e-03	6.70e-03	2.30e-05	1.25e-01
18	1.43e-02	3.62e-03	1.53e-05	5.00e-01
19	4.54e-03	2.95e-03	4.89e-06	1.25e-01
20	5.66e-03	2.26e-03	3.77e-06	2.50e-01
21	2.32e-03	1.92e-03	1.93e-06	1.25e-01
22	2.43e-03	1.45e-03	1.23e-06	2.50e-01
23	1.35e-03	1.26e-03	8.06e-07	1.25e-01
24	2.05e-03	6.60e-04	3.90e-07	5.00e-01
25	7.37e-04	5.54e-04	1.64e-07	1.25e-01
26	8.42e-04	4.20e-04	1.12e-07	2.50e-01
27	4.06e-04	3.62e-04	6.71e-08	1.25e-01
28	7.55e-04	1.95e-04	4.32e-08	5.00e-01
29	2.42e-04	1.59e-04	1.42e-08	1.25e-01
30	3.00e-04	1.22e-04	1.08e-08	2.50e-01
31	1.25e-04	1.04e-04	5.63e-09	1.25e-01
32	1.29e-04	7.86e-05	3.57e-09	2.50e-01
33	1.65e-04	6.04e-05	2.90e-09	2.50e-01
34	6.41e-05	5.13e-05	1.38e-09	1.25e-01
35	6.95e-05	3.88e-05	9.04e-10	2.50e-01
36	3.66e-05	3.36e-05	5.73e-10	1.25e-01
37	6.04e-05	1.77e-05	3.09e-10	5.00e-01
38	2.06e-05	1.47e-05	1.18e-10	1.25e-01
39	2.44e-05	1.12e-05	8.43e-11	2.50e-01
40	1.11e-05	9.64e-06	4.78e-11	1.25e-01
41	2.23e-05	5.28e-06	3.53e-11	5.00e-01
42	6.88e-06	4.24e-06	1.03e-11	1.25e-01
43	8.75e-06	3.26e-06	8.35e-12	2.50e-01
44	3.44e-06	2.77e-06	4.01e-12	1.25e-01
45	3.70e-06	2.09e-06	2.62e-12	2.50e-01
46	1.97e-06	1.81e-06	1.66e-12	1.25e-01
47	3.19e-06	9.55e-07	8.91e-13	5.00e-01
48	1.10e-06	7.92e-07	3.37e-13	1.25e-01
49	1.29e-06	6.03e-07	2.46e-13	2.50e-01
Error of x with respect to x_ast: 6.03e-07
Approximate solution: [ 5.90613861e-07 -1.19699510e-07]
Reached maximum of iterations, check approximation

In [20]:
x


Out[20]:
array([ 5.90613861e-07, -1.19699510e-07])

In [21]:
total_of_iterations


Out[21]:
50

In [22]:
Err_plot.shape


Out[22]:
(50,)

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[:,0]


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

In [25]:
x_plot.shape


Out[25]:
(2, 50)

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


Out[26]:
array([ 5.90613861e-07, -1.19699510e-07])

In [27]:
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 descenso en gradiente para $f_o$ con $C=10$')
plt.grid()
plt.show()



In [28]:
plt.plot(np.arange(Err_plot.size-40),Err_plot[40:],'.-')
plt.ylabel('Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Últimas 10 iteraciones del método descenso en gradiente para $f_o$ con $C=10$')
plt.grid()
plt.show()


Comentario: en esta gráfica se observa una convergencia lineal del método de descenso en gradiente para $f_o$.

Curvas de nivel de $f_o$


In [29]:
z=lambda x_mesh,y_mesh,cte: 1/2*(x_mesh**2+cte*y_mesh**2)

In [30]:
density=1e-1
xl=-1
yl=-1
xr=1
yr=1
x_p=np.arange(xl,xr,density)
y_p=np.arange(yl,yr,density)
x_mesh,y_mesh = np.meshgrid(x_p,y_p)
plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.contour(x_p,y_p,z(x_mesh,y_mesh,cte))
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 $f_o$, $C=10$")
plt.show()


Curvas de nivel $C=1$


In [31]:
cte=1
plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.contour(x_p,y_p,z(x_mesh,y_mesh,cte))
plt.grid()
plt.title("Curvas de nivel de $f_o$, $C=1$")
plt.show()


Curvas de nivel $C=0.1$


In [32]:
cte=.1
plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.contour(x_p,y_p,z(x_mesh,y_mesh,cte))
plt.grid()
plt.title("Curvas de nivel de $f_o$, $C=0.1$")
plt.show()


Se puede probar que las curvas de nivel para $f_o$ bajo las suposiciones que están al inicio de esta nota tienen forma de elipsoides cerca del óptimo $x^*$ y son aproximadamente iguales al elipsoide definido como:

$$\mathcal{E}_{x^*} = \{z \in \mathbb{R}^2 : ||z-x^*||_{\nabla^2 f_o(x^*)}^2 \leq 1\}$$

*La norma $||z||_P$ se nombra norma cuadrática y está definida para matrices simétricas positivas definidas $P$. La definición es: $$||z||_P = \sqrt{z^TPz}.$$

Obsérvese que $f_o(x)=\frac{1}{2}\left(x_1^2+Cx_2^2 \right)$ tiene una Hessiana igual a: $\nabla^2f(x)=\left [\begin{array}{cc} 1 & 0\\ 0 & C\\ \end{array} \right] $ $\forall x \in \mathbb{R}^2$.

A continuación se presenta el elipsoide $\mathcal{E}_{x^{(k)}}=\{z \in \mathbb{R}^2 : ||z-x^{(k)}||_{\nabla^2 f_o(x^{(k)})}^2 \leq 1\}$ para la última iteración $x^{(k)}$ de los resultados del método de descenso en gradiente anteriores usando $C=10$:


In [33]:
cte=10

In [34]:
plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.contour(x_p,y_p,z(x_mesh,y_mesh,cte))
plt.grid()
plt.title("Curvas de nivel de $f_o$, $C=10$")
plt.show()



In [35]:
x_k = x_plot[:,-1].reshape(2,1)
Hf_x_k = Hessian_approximation(fo,x_k)
norm_cuad = lambda z : np.sum((z-x_k)*(Hf_x_k@(z-x_k)),axis=0)
density_p=int(2.5*10**5)
z_p=np.random.uniform(-2,2,(2,density_p))
lim_sup=2
lim_inf=1.95
ind=(norm_cuad(z_p)<=lim_sup) & (lim_inf<=norm_cuad(z_p))
z_p_subset = z_p[:,ind]
plt.xlim(-1, 1)
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 $f_o$ evaluada en la última iteración')
plt.grid()
plt.show()


Número de condición de la matriz Hessiana


In [36]:
Hf_x_k


Out[36]:
array([[ 1.00000000e+00, -2.01948392e-16],
       [-2.01948392e-16,  1.00000000e+01]])

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


Out[37]:
10.0

La Hessiana es medianamente bien condicionada

Descenso en gradiente para caso $C=1$


In [38]:
cte=1

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


I	Normagf		Error x_ast	Error p_ast	line search
0	7.07e-01	7.07e-01	2.50e-01	---
1	4.33e-09	1.14e-08	6.50e-17	1.00e+00
Error of x with respect to x_ast: 1.14e-08
Approximate solution: [-8.06349476e-09 -8.06349476e-09]

In [40]:
x


Out[40]:
array([-8.06349476e-09, -8.06349476e-09])

In [41]:
total_of_iterations


Out[41]:
2

In [42]:
plt.plot(np.arange(Err_plot.size),Err_plot,'.-')
plt.ylabel('Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iterations',size=12)
plt.grid()
plt.show()


Comentario: en esta gráfica se observa una convergencia lineal del método de descenso en gradiente para $f_o$ con $C=1$.


In [43]:
plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.contour(x_p,y_p,z(x_mesh,y_mesh,cte))
plt.plot(x_plot[0,:],x_plot[1,:],'-*')
plt.annotate('$x^{(0)}$',(x_plot[0,0],x_plot[1,0]),fontsize=12)
plt.title("Curvas de nivel de $f_o$, $C=1$")
plt.grid()
plt.show()



In [44]:
x_k = x_plot[:,-1].reshape(2,1)
Hf_x_k = Hessian_approximation(fo,x_k)
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(-1,1,(2,density_p))
lim_sup=1
lim_inf=.95
ind=(norm_cuad(z_p)<=lim_sup) & (lim_inf<=norm_cuad(z_p))
z_p_subset = z_p[:,ind]
plt.xlim(-1.1, 1.1)
plt.ylim(-1.1, 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 $f_o$ evaluada en la última iteración')
plt.grid()
plt.show()



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


Out[45]:
1.0

La matriz Hessiana es bien condicionada

Descenso en gradiente para caso $C=0.1$


In [46]:
cte=.1

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


I	Normagf		Error x_ast	Error p_ast	line search
0	5.02e-01	7.07e-01	1.38e-01	---
1	4.50e-02	4.50e-01	1.01e-02	1.00e+00
2	4.05e-02	4.05e-01	8.20e-03	1.00e+00
3	3.65e-02	3.65e-01	6.64e-03	1.00e+00
4	3.28e-02	3.28e-01	5.38e-03	1.00e+00
5	2.95e-02	2.95e-01	4.36e-03	1.00e+00
6	2.66e-02	2.66e-01	3.53e-03	1.00e+00
7	2.39e-02	2.39e-01	2.86e-03	1.00e+00
8	2.15e-02	2.15e-01	2.32e-03	1.00e+00
9	1.94e-02	1.94e-01	1.88e-03	1.00e+00
10	1.74e-02	1.74e-01	1.52e-03	1.00e+00
11	1.57e-02	1.57e-01	1.23e-03	1.00e+00
12	1.41e-02	1.41e-01	9.97e-04	1.00e+00
13	1.27e-02	1.27e-01	8.08e-04	1.00e+00
14	1.14e-02	1.14e-01	6.54e-04	1.00e+00
15	1.03e-02	1.03e-01	5.30e-04	1.00e+00
16	9.27e-03	9.27e-02	4.29e-04	1.00e+00
17	8.34e-03	8.34e-02	3.48e-04	1.00e+00
18	7.50e-03	7.50e-02	2.82e-04	1.00e+00
19	6.75e-03	6.75e-02	2.28e-04	1.00e+00
20	6.08e-03	6.08e-02	1.85e-04	1.00e+00
21	5.47e-03	5.47e-02	1.50e-04	1.00e+00
22	4.92e-03	4.92e-02	1.21e-04	1.00e+00
23	4.43e-03	4.43e-02	9.82e-05	1.00e+00
24	3.99e-03	3.99e-02	7.95e-05	1.00e+00
25	3.59e-03	3.59e-02	6.44e-05	1.00e+00
26	3.23e-03	3.23e-02	5.22e-05	1.00e+00
27	2.91e-03	2.91e-02	4.23e-05	1.00e+00
28	2.62e-03	2.62e-02	3.42e-05	1.00e+00
29	2.36e-03	2.36e-02	2.77e-05	1.00e+00
30	2.12e-03	2.12e-02	2.25e-05	1.00e+00
31	1.91e-03	1.91e-02	1.82e-05	1.00e+00
32	1.72e-03	1.72e-02	1.47e-05	1.00e+00
33	1.55e-03	1.55e-02	1.19e-05	1.00e+00
34	1.39e-03	1.39e-02	9.67e-06	1.00e+00
35	1.25e-03	1.25e-02	7.83e-06	1.00e+00
36	1.13e-03	1.13e-02	6.34e-06	1.00e+00
37	1.01e-03	1.01e-02	5.14e-06	1.00e+00
38	9.12e-04	9.12e-03	4.16e-06	1.00e+00
39	8.21e-04	8.21e-03	3.37e-06	1.00e+00
40	7.39e-04	7.39e-03	2.73e-06	1.00e+00
41	6.65e-04	6.65e-03	2.21e-06	1.00e+00
42	5.99e-04	5.99e-03	1.79e-06	1.00e+00
43	5.39e-04	5.39e-03	1.45e-06	1.00e+00
44	4.85e-04	4.85e-03	1.18e-06	1.00e+00
45	4.36e-04	4.36e-03	9.52e-07	1.00e+00
46	3.93e-04	3.93e-03	7.71e-07	1.00e+00
47	3.53e-04	3.53e-03	6.25e-07	1.00e+00
48	3.18e-04	3.18e-03	5.06e-07	1.00e+00
49	2.86e-04	2.86e-03	4.10e-07	1.00e+00
50	2.58e-04	2.58e-03	3.32e-07	1.00e+00
51	2.32e-04	2.32e-03	2.69e-07	1.00e+00
52	2.09e-04	2.09e-03	2.18e-07	1.00e+00
53	1.88e-04	1.88e-03	1.76e-07	1.00e+00
54	1.69e-04	1.69e-03	1.43e-07	1.00e+00
55	1.52e-04	1.52e-03	1.16e-07	1.00e+00
56	1.37e-04	1.37e-03	9.38e-08	1.00e+00
57	1.23e-04	1.23e-03	7.60e-08	1.00e+00
58	1.11e-04	1.11e-03	6.15e-08	1.00e+00
59	9.98e-05	9.98e-04	4.98e-08	1.00e+00
60	8.99e-05	8.99e-04	4.04e-08	1.00e+00
61	8.09e-05	8.09e-04	3.27e-08	1.00e+00
62	7.28e-05	7.28e-04	2.65e-08	1.00e+00
63	6.55e-05	6.55e-04	2.15e-08	1.00e+00
64	5.90e-05	5.90e-04	1.74e-08	1.00e+00
65	5.31e-05	5.31e-04	1.41e-08	1.00e+00
66	4.78e-05	4.77e-04	1.14e-08	1.00e+00
67	4.30e-05	4.30e-04	9.23e-09	1.00e+00
68	3.87e-05	3.87e-04	7.48e-09	1.00e+00
69	3.48e-05	3.48e-04	6.06e-09	1.00e+00
Error of x with respect to x_ast: 3.48e-04
Approximate solution: [-4.99999993e-09  3.48094310e-04]
Reached maximum of iterations, check approximation

In [48]:
x


Out[48]:
array([-4.99999993e-09,  3.48094310e-04])

In [49]:
total_of_iterations


Out[49]:
70

In [50]:
Err_plot.shape


Out[50]:
(70,)

In [51]:
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 [52]:
x_plot[:,-1]


Out[52]:
array([-4.99999993e-09,  3.48094310e-04])

In [53]:
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 descenso en gradiente para $f_o$ con $C=10$')
plt.grid()
plt.show()



In [54]:
plt.plot(np.arange(Err_plot.size-65),Err_plot[65:],'.-')
plt.ylabel('Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Últimas 5 iteraciones del método descenso en gradiente para $f_o$ con $C=0.1$')
plt.grid()
plt.show()


Comentario: en esta gráfica se observa una convergencia lineal del método de descenso en gradiente para $f_o$.


In [55]:
plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.contour(x_p,y_p,z(x_mesh,y_mesh,cte))
plt.plot(x_plot[0,:],x_plot[1,:],'-*')
plt.annotate('$x^{(0)}$',(x_plot[0,0],x_plot[1,0]),fontsize=12)
plt.title("Curvas de nivel de $f_o$, $C=0.1$")
plt.grid()
plt.show()



In [56]:
x_k = x_plot[:,-1].reshape(2,1)
Hf_x_k = Hessian_approximation(fo,x_k)
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(-1,1,(2,density_p))
lim_sup=.5
lim_inf=.45
ind=(norm_cuad(z_p)<=lim_sup) & (lim_inf<=norm_cuad(z_p))
z_p_subset = z_p[:,ind]
plt.xlim(-1, 1)
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 $f_o$ evaluada en la última iteración')
plt.grid()
plt.show()



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


Out[57]:
10.000000000033086

La matriz Hessiana es medianamente bien condicionada

Comentarios:

  • El número de condición de la matriz Hessiana de la función $f_o(x) = \frac{1}{2}\left ( x_1^2 + Cx_2^2 \right)$ es igual a $\max\left\{C, \frac{1}{C}\right\}$ lo cual se obtiene del cociente del eigenvalor máximo entre el eigenvalor mínimo de $\nabla ^2f_o(x)$: $\frac{\max\{1,C\}}{\min\{1,C\}}$ ya que $\nabla ^2 f_o(x)$ es una matriz diagonal y positiva definida pues $C>0$: $\nabla^2f_o(x)=\left [\begin{array}{cc} 1 & 0\\ 0 & C\\ \end{array} \right] $ $\forall x \in \mathbb{R}^2$.

  • Por el punto anterior el número de condición de la matriz Hessiana es grande si $C >> 1$ o $C <<1$.

  • El número de condición de la matriz Hessiana se observa que influye mucho en la convergencia del método de descenso en gradiente. Aún siendo la Hessiana medianamente bien condicionadas (número de condición igual a 10 con $C=10$ o $C=0.1$) el número de iteraciones cambió drásticamente (un número de iteraciones mayor a $50$). Esto se cumple para este método y el método más general: el método de descenso más pronunciado: steepest descent method bajo la norma cuadrática con matrices $P$ que no aproximen bien a la Hessiana de $f_o$. Para matrices mal condicionadas el método de gradiente es muy lento por lo que no se recomienda en la práctica.

  • La simplicidad del método de descenso en gradiente es su mayor ventaja pero su enorme dependencia en el número de condición en la Hessiana o los conjuntos subnivel es su desventaja.

  • La elección de valores en los parámetros de búsqueda de línea por backtracking $\alpha$ y $\beta$ tienen un efecto visible pero no dramático en la convergencia. Típicamente se utilizan $\alpha=0.01, 0.15$ y $\beta = 0.5$.

Ejercicio: realizar mismas gráficas y análisis de convergencia y curvas de nivel para la función objetivo $f_o(x) = \frac{1}{2}\left ( Cx_1^2 + x_2^2 \right)$ para $C=10^{-2}, 10^2$.

Método de descenso en gradiente visto como caso particular del método steepest descent bajo la norma cuadrática

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

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

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

  1. Definir $P$ matriz simétrica definida positiva.
  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 x = - \nabla f_o(x)$.
  4. Búsqueda de línea. Elegir un tamaño de paso $t > 0$.
  5. Hacer la actualización: $x = x + t\Delta 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.

Ejemplo

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

a)Usando $P_1=\left [\begin{array}{cc} 1 & 0\\ 0 & 1\\ \end{array} \right] $ por lo que no hay transformación de coordenadas.


In [58]:
P=np.eye(2)

In [59]:
P


Out[59]:
array([[1., 0.],
       [0., 1.]])

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

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

In [62]:
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 [63]:
fo_transf = lambda x: fo(coord_transform_original_variable(x))

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

In [65]:
p_ast=fo(x_ast)

In [66]:
x_ast = coord_transform(x_ast)

In [67]:
x_ast


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

In [68]:
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	9.05e-01	1.00e+00	6.07e-02	---
1	2.71e-01	3.05e-01	5.61e-03	5.00e-01
2	7.64e-02	8.61e-02	4.46e-04	5.00e-01
3	2.14e-02	2.41e-02	3.49e-05	5.00e-01
4	5.98e-03	6.74e-03	2.73e-06	5.00e-01
5	1.68e-03	1.89e-03	2.14e-07	5.00e-01
6	1.07e-03	5.79e-04	3.23e-08	5.00e-01
7	5.28e-04	3.74e-04	1.07e-08	1.25e-01
8	2.85e-04	2.48e-04	4.15e-09	1.25e-01
9	3.57e-04	1.24e-04	2.52e-09	2.50e-01
10	1.62e-04	7.10e-05	6.12e-10	1.25e-01
11	7.64e-05	4.40e-05	1.76e-10	1.25e-01
12	3.85e-05	2.86e-05	6.06e-11	1.25e-01
13	2.12e-05	1.91e-05	2.41e-11	1.25e-01
14	2.54e-05	9.18e-06	1.30e-11	2.50e-01
15	1.16e-05	5.37e-06	3.30e-12	1.25e-01
16	5.53e-06	3.36e-06	9.70e-13	1.25e-01
17	2.86e-06	2.22e-06	3.55e-13	1.25e-01
18	1.56e-06	1.46e-06	1.36e-13	1.25e-01
19	1.74e-06	6.75e-07	6.59e-14	2.50e-01
20	8.34e-07	3.99e-07	1.56e-14	1.25e-01
21	3.82e-07	2.49e-07	5.38e-15	1.25e-01
22	1.88e-07	1.51e-07	1.21e-15	1.25e-01
23	9.93e-08	1.07e-07	8.68e-16	1.25e-01
24	1.99e-07	7.70e-08	3.47e-16	2.50e-01
25	1.26e-07	5.40e-08	0.00e+00	6.25e-02
26	9.93e-08	4.67e-08	0.00e+00	3.12e-02
27	4.44e-08	3.40e-08	0.00e+00	6.25e-02
28	0.00e+00	1.61e-08	0.00e+00	2.50e-01
Error of x with respect to x_ast: 1.61e-08
Approximate solution: [-3.46573595e-01 -5.55111512e-09]

In [69]:
x


Out[69]:
array([-3.46573595e-01, -5.55111512e-09])

In [70]:
total_of_iterations


Out[70]:
29

In [71]:
Err_plot.shape


Out[71]:
(25,)

In [72]:
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 [73]:
x_plot.shape


Out[73]:
(2, 29)

In [74]:
x_plot[:,0]


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

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


Out[75]:
array([-3.46573595e-01, -5.55111512e-09])

In [76]:
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 $f_o$')
plt.grid()
plt.show()



In [77]:
plt.plot(np.arange(Err_plot.size-22),Err_plot[22:],'.-')
plt.ylabel('Error relativo entre: $f_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Últimas 7 iteraciones del método descenso en gradiente para $f_o$')
plt.grid()
plt.show()


Comentario: en esta gráfica se observa una convergencia lineal del método de descenso en gradiente para $f_o$.

Curvas de nivel de $f_o$


In [78]:
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)

In [79]:
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 [80]:
density=1e-1
xl=-.5
yl=-.5
xr=.5
yr=.5
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(-.5, .5)
plt.ylim(-.5, .5)
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 $f_o$")
plt.show()



In [81]:
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(-1,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(-.5, .5)
plt.ylim(-.5, .5)
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 $f_o$ evaluada en la última iteración')
plt.grid()
plt.show()



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


Out[82]:
4.500694250729451

La matriz Hessiana está bien condicionada.

En los siguientes ejemplos continuamos usando el método de descenso en gradiente pero con diferentes matrices de transformación para el método de steepest descent bajo la norma cuadrática y ver su influencia en las curvas de nivel y en el número de condición de la Hessiana.

3) Usando $P_1=\left [\begin{array}{cc} 2 & 0\\ 0 & 8\\ \end{array} \right] $ para transformar a $f_0$.


In [83]:
P=np.array([[2,0],
            [0,8]])

In [84]:
P


Out[84]:
array([[2, 0],
       [0, 8]])

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

In [86]:
p_ast=fo(x_ast)

In [87]:
x_ast = coord_transform(x_ast)

In [88]:
x_ast


Out[88]:
array([-0.49012908,  0.        ])

In [89]:
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	6.40e-01	1.00e+00	6.07e-02	---
1	1.92e-01	3.05e-01	5.61e-03	1.00e+00
2	5.40e-02	8.61e-02	4.46e-04	1.00e+00
3	1.51e-02	2.41e-02	3.49e-05	1.00e+00
4	4.23e-03	6.74e-03	2.73e-06	1.00e+00
5	1.18e-03	1.88e-03	2.13e-07	1.00e+00
6	3.31e-04	5.27e-04	1.67e-08	1.00e+00
7	9.25e-05	1.47e-04	1.31e-09	1.00e+00
8	2.59e-05	4.12e-05	1.02e-10	1.00e+00
9	7.28e-06	1.16e-05	8.14e-12	1.00e+00
10	2.00e-06	3.23e-06	6.22e-13	1.00e+00
11	4.91e-07	8.47e-07	4.42e-14	1.00e+00
12	1.26e-07	1.75e-07	1.74e-15	1.00e+00
13	8.88e-08	9.61e-08	6.94e-16	1.00e+00
14	4.44e-08	3.20e-08	0.00e+00	5.00e-01
15	4.44e-08	3.20e-08	0.00e+00	3.91e-03
16	0.00e+00	3.22e-08	0.00e+00	3.12e-02
Error of x with respect to x_ast: 3.22e-08
Approximate solution: [-4.90129093e-01 -1.56125113e-09]

In [90]:
x


Out[90]:
array([-4.90129093e-01, -1.56125113e-09])

In [91]:
total_of_iterations


Out[91]:
17

In [92]:
Err_plot.shape


Out[92]:
(14,)

In [93]:
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: $\hat{f}_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iterations',size=12)
plt.grid()
plt.show()



In [94]:
x_plot.shape


Out[94]:
(2, 17)

In [95]:
x_plot[:,0]


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

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


Out[96]:
array([-4.90129093e-01, -1.56125113e-09])

In [97]:
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 [98]:
plt.plot(np.arange(Err_plot.size-10),Err_plot[10:],'.-')
plt.ylabel('Error relativo entre: $\hat{f}_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Últimas 7 iteraciones del método descenso en gradiente para $\hat{f}_o$')
plt.grid()
plt.show()


Comentario: en esta gráfica se observa una convergencia lineal del método de descenso en gradiente para $f_o$ transformada.

Curvas de nivel de $f_o$ transformada


In [99]:
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 [100]:
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.5, .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 [101]:
np.linalg.cond(Hf_x_k)


Out[101]:
1.124913254684247

Obsérvese que es menor el número de condición con la transformación de $f_o$.

c) Usando $P_2=\left [\begin{array}{cc} 8 & 0\\ 0 & 2\\ \end{array} \right] $ para transformar a $f_o$.


In [102]:
P=np.array([[8,0],
            [0,2]])

In [103]:
P


Out[103]:
array([[8, 0],
       [0, 2]])

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

In [105]:
p_ast=fo(x_ast)

In [106]:
x_ast = coord_transform(x_ast)

In [107]:
x_ast


Out[107]:
array([-0.98025815,  0.        ])

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

[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	3.20e-01	1.00e+00	6.07e-02	---
1	2.13e-01	6.74e-01	2.74e-02	1.00e+00
2	1.44e-01	4.56e-01	1.25e-02	1.00e+00
3	9.73e-02	3.10e-01	5.76e-03	1.00e+00
4	6.60e-02	2.10e-01	2.66e-03	1.00e+00
5	4.49e-02	1.43e-01	1.23e-03	1.00e+00
6	3.05e-02	9.73e-02	5.68e-04	1.00e+00
7	2.08e-02	6.61e-02	2.63e-04	1.00e+00
8	1.48e-02	4.50e-02	1.22e-04	1.00e+00
9	2.41e-02	3.08e-02	7.26e-05	1.00e+00
10	1.34e-02	2.82e-02	5.09e-05	2.50e-01
11	9.31e-03	2.59e-02	4.09e-05	2.50e-01
12	2.25e-02	1.80e-02	3.47e-05	1.00e+00
13	1.10e-02	1.63e-02	1.90e-05	2.50e-01
14	6.41e-03	1.49e-02	1.40e-05	2.50e-01
15	9.18e-03	1.26e-02	1.17e-05	5.00e-01
16	5.17e-03	1.15e-02	8.42e-06	2.50e-01
17	7.62e-03	9.75e-03	7.27e-06	5.00e-01
18	4.17e-03	8.91e-03	5.08e-06	2.50e-01
19	6.31e-03	7.54e-03	4.52e-06	5.00e-01
20	3.38e-03	6.89e-03	3.07e-06	2.50e-01
21	2.29e-03	6.33e-03	2.45e-06	2.50e-01
22	5.65e-03	4.41e-03	2.13e-06	1.00e+00
23	2.72e-03	3.98e-03	1.14e-06	2.50e-01
24	1.56e-03	3.65e-03	8.35e-07	2.50e-01
25	2.23e-03	3.08e-03	6.99e-07	5.00e-01
26	1.25e-03	2.82e-03	5.02e-07	2.50e-01
27	1.83e-03	2.38e-03	4.30e-07	5.00e-01
28	1.00e-03	2.18e-03	3.02e-07	2.50e-01
29	1.50e-03	1.84e-03	2.65e-07	5.00e-01
30	8.06e-04	1.68e-03	1.82e-07	2.50e-01
31	5.54e-04	1.55e-03	1.46e-07	2.50e-01
32	1.32e-03	1.08e-03	1.22e-07	1.00e+00
33	6.39e-04	9.73e-04	6.70e-08	2.50e-01
34	3.73e-04	8.91e-04	4.97e-08	2.50e-01
35	5.21e-04	7.52e-04	4.09e-08	5.00e-01
36	2.97e-04	6.89e-04	2.98e-08	2.50e-01
37	4.26e-04	5.82e-04	2.51e-08	5.00e-01
38	2.38e-04	5.33e-04	1.80e-08	2.50e-01
39	3.48e-04	4.50e-04	1.54e-08	5.00e-01
40	1.90e-04	4.12e-04	1.08e-08	2.50e-01
41	2.84e-04	3.48e-04	9.50e-09	5.00e-01
42	1.53e-04	3.18e-04	6.51e-09	2.50e-01
43	1.05e-04	2.92e-04	5.22e-09	2.50e-01
44	2.50e-04	2.03e-04	4.37e-09	1.00e+00
45	1.21e-04	1.84e-04	2.39e-09	2.50e-01
46	7.05e-05	1.68e-04	1.77e-09	2.50e-01
47	9.87e-05	1.42e-04	1.46e-09	5.00e-01
48	5.63e-05	1.30e-04	1.07e-09	2.50e-01
49	8.05e-05	1.10e-04	8.96e-10	5.00e-01
50	4.49e-05	1.01e-04	6.41e-10	2.50e-01
51	6.57e-05	8.51e-05	5.50e-10	5.00e-01
52	3.60e-05	7.78e-05	3.86e-10	2.50e-01
53	5.40e-05	6.58e-05	3.40e-10	5.00e-01
54	2.89e-05	6.02e-05	2.33e-10	2.50e-01
55	1.98e-05	5.53e-05	1.86e-10	2.50e-01
56	4.76e-05	3.84e-05	1.57e-10	1.00e+00
57	2.30e-05	3.48e-05	8.57e-11	2.50e-01
58	1.34e-05	3.18e-05	6.34e-11	2.50e-01
59	1.87e-05	2.69e-05	5.23e-11	5.00e-01
Error of x with respect to x_ast: 2.69e-05
Approximate solution: [-9.80231973e-01  2.89768209e-06]
Reached maximum of iterations, check approximation

In [109]:
x


Out[109]:
array([-9.80231973e-01,  2.89768209e-06])

In [110]:
total_of_iterations


Out[110]:
60

In [111]:
Err_plot.shape


Out[111]:
(60,)

In [112]:
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: $\hat{f}_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Iterations',size=12)
plt.grid()
plt.show()



In [113]:
x_plot.shape


Out[113]:
(2, 60)

In [114]:
x_plot[:,0]


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

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


Out[115]:
array([-9.80231973e-01,  2.89768209e-06])

In [116]:
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 [117]:
plt.plot(np.arange(Err_plot.size-53),Err_plot[53:],'.-')
plt.ylabel('Error relativo entre: $\hat{f}_o(x^k)$ y $p^*$',size=12)
plt.xlabel('Últimas 7 iteraciones del método descenso en gradiente para $\hat{f}_o$')
plt.grid()
plt.show()


Comentario: en esta gráfica se observa una convergencia lineal del método de descenso en gradiente para $f_o$ transformada.

Curvas de nivel de $f_o$ transformada


In [118]:
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 [119]:
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.5, .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 [120]:
np.linalg.cond(Hf_x_k)


Out[120]:
18.011111111111113

Obsérvese que es mayor el número de condición con la transformación de $f_o$.

Comentarios:

  • Como se observa en los ejemplos anteriores la elección de la norma en el método de descenso más pronunciado tiene un efecto fuerte en la tasa de convergencia.

  • Siempre existe una matriz $P$ para la cual el método de descenso más pronunciado tiene una convergencia buena. El reto está en encontrar tal matriz. La idea es identificar una matriz $P$ para la cual el problema transformado tenga un número de condición moderado.

Ejemplo de uso de cvxpy

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

óptimo:

$x^* = \left [\begin{array}{c} -0.3465735941251887 \\ 0 \end{array} \right] $


In [122]:
!pip3 install --user -q cvxpy


  WARNING: The scripts futurize and pasteurize are installed in '/home/miuser/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
WARNING: You are using pip version 19.3.1; however, version 20.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

In [123]:
import cvxpy as cp

In [124]:
x1 = cp.Variable()
x2 = cp.Variable()

In [125]:
obj = cp.Minimize(cp.exp(x1+3*x2-0.1)+cp.exp(x1-3*x2-0.1)+cp.exp(-x1-0.1))

In [126]:
prob = cp.Problem(obj)

In [127]:
prob.solve()


Out[127]:
2.5592666797159542

In [128]:
print("status:", prob.status)
print("optimal value", prob.value)
print("optimal var", x1.value, x2.value)


status: optimal
optimal value 2.5592666797159542
optimal var -0.3465735941251887 -2.1868540781648926e-17

Referencias:

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