Autor Tema: Newton Raphson con line search backtracking

0 Usuarios y 1 Visitante están viendo este tema.

25 Abril, 2024, 03:14 pm
Leído 33 veces

speed_methal

  • $$\Large \color{#6a84c0}\pi$$
  • Mensajes: 20
  • País: es
  • Karma: +0/-0
  • Sexo: Masculino
Buenas a tod@s,

llevo unas semanas dándome de cabezazos y no consigo dar con la tecla. Les comento mi problema a ver si alguien me pudiera arrojar algo de luz.

La cuestión es que tengo un sistema de ecuaciones no lineales (3 para ser exactos), con tres incognitas. He implementado el método de Newton Raphson multivariable en python y funciona, pero para mi sorpresa dependiendo del valor de las funciones me he encontrado que la convergencia puede ser realmente lenta. Buscando encontré que con el algoritmo de line search se puede optimizar el tamaño del paso para mejorar la convergencia. El problema viene aquí no sé si lo tengo bien implementado porque me salen cosas muy raras.

la función que tengo es la siguiente
$$\bar{\sigma}={\left(\frac{1}{2}\left({\left |{S_1 - S_2}\right |}^m+{\left |{S_2 - S_3}\right |}^m+{\left |{S_3 - S_1}\right |}^m\right)\right)}^\frac{1}{m}$$

Por no complicar  el asunto ya que $$S_i$$ son valores complejos, aunque si lo necesitaseis añadiría todas la fórmulas, las 3 funciones no lineales las obtengo de tres casos particulares ya que de otro modo las derivadas serían monstruosas. Así pues $$f_1 = f(b, c)$$, $$f_2 = f(a, c)$$ y $$f_3=f(a, b)$$. Donde $$a, b, c$$ son las incógnitas a calcular por medio de Newton Raphson. Con estas tres ecuaciones construyo el vector $$B=\left[f_1, f_2, f_3\right]$$

El Jacobiano quedaría de la siguiente manera:

J=\begin{bmatrix}{\nabla{f_1(x)}}\\{\nabla{f_2(x)}}\\{\nabla{f_3(x)}}\end{bmatrix}=\begin{bmatrix}{\frac{{\partial f_1}}{{\partial a}}}&{\frac{{\partial f_1}}{{\partial b}}}&{\frac{{\partial f_1}}{{\partial c}}}\\{\frac{{\partial f_2}}{{\partial a}}}&{\frac{{\partial f_2}}{{\partial b}}}&{\frac{{\partial f_2}}{{\partial c}}}\\{\frac{{\partial f_3}}{{\partial a}}}&{\frac{{\partial f_3}}{{\partial b}}}&{\frac{{\partial f_3}}{{\partial c}}}\end{bmatrix}

Debido a los tres casos especiales se obtiene que $$\frac{{\partial f_1}}{{\partial a}}=\frac{{\partial f_2}}{{\partial b}}=\frac{{\partial f_3}}{{\partial c}}=0$$.

Para obtener el vector incremento $$P$$ para la iteración actual en el método de Newton-Raphson:

$$P=J^{-1}·(-B)$$

siendo los nuevos valores tal que:

$$\begin{pmatrix}{a1}\\{b1}\\{c1}\end{pmatrix}=P+\begin{pmatrix}{a0}\\{b0}\\{c0}\end{pmatrix}$$

Hasta aquí todo bien. Ahora empieza la tortura con el line search. Si lo he entendido bien, consiste en estimar un tamaño inicial lo suficientemente grande en una dirección de búsque determinada para optimizar el paso de $$a_0 \rightarrow{a_1}$$.
La dirección de búsqueda deber ser tal que asegure el decaimiento de la función de búsqueda. Dicha función es del tipo:

$$\phi(\alpha) = f(X_k+\alpha{p_k})$$

donde $$X_k$$ es el vector de coeficientes actuales $$a0, b0, c0$$, $$\alpha$$ es el tamaño del paso y $$p_k$$ es la dirección de búsqueda que para asegurar el decaimiento $$p_k=-\nabla{f(X_k)}$$.

A la hora de implementar mi propia función de line search en python tengo algunas dudas como: ¿Cómo defino el tamaño de paso inicial? La bibliografía establece un valor de paso de 1. Podría usarlo como valor por defecto, pero ¿hay alguna forma de calcular un primer valor?
El otro problema viene a la hora del criterio de convergencia de $$\alpha$$. Si me he enterado bien, se puede usar la condición de Armijo (1966) la cual dice que $$\alpha$$ se actualizará multiplicando por un valor $$\rho$$; tal que $$\alpha=\alpha \rho$$; hasta que se cumpla la condición:

$$f(X_k + \alpha{p_k})\geq{f(X_k) + c\alpha{\nabla{f(X_k)p_k}}}$$

donde $$c$$ y $$\rho$$ son parámetros del control de búsqueda y están el en rango $$(0, 1)$$.

El código que tengo en python es el siguiente:

def line_search(s11, s22, s33, J, B, P, a0, b0, c0, m, alpha_max=1.0):
    # S11, S22, S33, m son parámetros para lad funciones f1, f2, f3
    # J: Jacobiano
    # B: Vector [f1, f2, f3] para a0, b0, c0
    # P: Vector incremento [a1-a0, b1-b0, c1-c0]
    alpha = 1.0  # initial step size
    c = 0.5      # parameter for sufficient decrease condition
    rho = 0.9    # parameter for backtracking

    f0 = getPhiVector(s11, s22, s33, a0, b0, c0, m)

    while(alpha > 1e-8):
        a1 = a0 + alpha * P[0]
        b1 = b0 + alpha * P[1]
        c1 = c0 + alpha * P[2]
        fnext = getPhiVector(s11, s22, s33, a1, b1, c1, m)
        if(fnext[0] <= f0[0] - c * alpha * np.dot(J[0], J[0]) and
           fnext[1] <= f0[1] - c * alpha * np.dot(J[1], J[1]) and
           fnext[2] <= f0[2] - c * alpha * np.dot(J[2], J[2])):
            return alpha
        else:
            alpha *= rho
    return alpha_max


P.D. no sé si hay en LateX algún comando para insertar código.

El $$\alpha$$ devuelto es aplicado a la actualización de los coeficientes en el método de Newton Raphson tal que:

$$\begin{pmatrix}{a1}\\{b1}\\{c1}\end{pmatrix}=\alpha{P}+\begin{pmatrix}{a0}\\{b0}\\{c0}\end{pmatrix}$$

El caso es que no sé si estoy aplicando bien el line search, la concición de convergencia de $$\alpha$$ o qué está pasando pues para nada mejora la convergencia.

Muchas gracias de antemano.

26 Abril, 2024, 11:55 am
Respuesta #1

speed_methal

  • $$\Large \color{#6a84c0}\pi$$
  • Mensajes: 20
  • País: es
  • Karma: +0/-0
  • Sexo: Masculino
Hola de nuevo,

ayer conseguí hacer algunos avances pero me surgen algunas dudas. Resulta que la definición de la dirección de $$p_k=-\nabla{f(X_k)}$$ es para el método de descenso del gradiente. Para el método de Newton, la dirección es $$p_k=-H^{-1}\nabla{f(X_k)}$$. Donde $$H$$ es el Hessiano y ha de ser simétrico y definido positivo. Que sea simétrico lo tengo claro, pero que sea definido positivo no tengo muy claro qué es.

Al tener tres funciones, me salen tres Hessianos:

$$H_1=\begin{bmatrix}{\frac{{\partial^2 f_1}}{{\partial a^2}}}&{\frac{{\partial^2 f_1}}{{\partial a\partial b}}}&{\frac{{\partial^2 f_1}}{{\partial a\partial c}}}\\{\frac{{\partial^2 f_2}}{{\partial b\partial a}}}&{\frac{{\partial^2 f_1}}{{\partial b^2}}}&{\frac{{\partial^2 f_1}}{{\partial b\partial c}}}\\{\frac{{\partial^2 f_1}}{{\partial c\partial a}}}&{\frac{{\partial^2 f_1}}{{\partial c\partial b}}}&{\frac{{\partial^2 f_1}}{{\partial c^2}}}\end{bmatrix}; H_2=\begin{bmatrix}{\frac{{\partial^2 f_2}}{{\partial a^2}}}&{\frac{{\partial^2 f_2}}{{\partial a\partial b}}}&{\frac{{\partial^2 f_2}}{{\partial a\partial c}}}\\{\frac{{\partial^2 f_2}}{{\partial b\partial a}}}&{\frac{{\partial^2 f_2}}{{\partial b^2}}}&{\frac{{\partial^2 f_2}}{{\partial b\partial c}}}\\{\frac{{\partial^2 f_2}}{{\partial c\partial a}}}&{\frac{{\partial^2 f_2}}{{\partial c\partial b}}}&{\frac{{\partial^2 f_2}}{{\partial c^2}}}\end{bmatrix}; H_3=\begin{bmatrix}{\frac{{\partial^2 f_3}}{{\partial a^2}}}&{\frac{{\partial^2 f_3}}{{\partial a\partial b}}}&{\frac{{\partial^2 f_3}}{{\partial a\partial c}}}\\{\frac{{\partial^2 f_3}}{{\partial b\partial a}}}&{\frac{{\partial^2 f_3}}{{\partial b^2}}}&{\frac{{\partial^2 f_3}}{{\partial b\partial c}}}\\{\frac{{\partial^2 f_3}}{{\partial c\partial a}}}&{\frac{{\partial^2 f_3}}{{\partial c\partial b}}}&{\frac{{\partial^2 f_3}}{{\partial c^2}}}\end{bmatrix}$$

que por los casos particulares para construir el sistema de tres ecuaciones no lineales, quedan de la siguiente manera:

$$H_1=\begin{bmatrix}0&0&0\\0&{\frac{{\partial^2 f_1}}{{\partial b^2}}}&{\frac{{\partial^2 f_1}}{{\partial b\partial c}}}\\0&{\frac{{\partial^2 f_1}}{{\partial c\partial b}}}&{\frac{{\partial^2 f_1}}{{\partial c^2}}}\end{bmatrix}; H_2=\begin{bmatrix}{\frac{{\partial^2 f_2}}{{\partial a^2}}}&0&{\frac{{\partial^2 f_2}}{{\partial a\partial c}}}\\0&0&0\\{\frac{{\partial^2 f_2}}{{\partial c\partial a}}}&0&{\frac{{\partial^2 f_2}}{{\partial c^2}}}\end{bmatrix}; H_3=\begin{bmatrix}{\frac{{\partial^2 f_3}}{{\partial a^2}}}&{\frac{{\partial^2 f_3}}{{\partial a\partial b}}}&0\\{\frac{{\partial^2 f_3}}{{\partial b\partial a}}}&{\frac{{\partial^2 f_3}}{{\partial b^2}}}&0\\0&0&0\end{bmatrix}$$

Obviamente, estas matrices no son simétricas, pero si para este campo vectorial entiendo estos Hessianos como "vectores", el Hesiano $$H$$ sería un vector de Hessianos $$H=[H_1, H_2, H_3]$$. Combinando linealmente estos "vectores" (como si de una suma de vectores se tratase para obtener el vector resultante), obtengo una matriz 3x3 simétrica.

El código de line search actualizado es el siguiente. Además de la condición de Armijo e incluido la condición de Wolfe.

def line_search(s11, s22, s33, J, B, P, a0, b0, c0, m, alpha_max=1.0, _print=False):
    alpha = 1.0  # initial step size
    c = 0.5        # parameter for sufficient decrease condition
    rho = 0.9     # parameter for backtracking
    nu = 0.9      # Curvature factor. Wolfe condition

    f0 = getPhiVector(s11, s22, s33, a0, b0, c0, m)
    grad_f = np.dot(J, f0)

    # ¿Debería usar los gradientes de J. J[0], J[1], J[2] en vez de grad_f?

    # Hessian
    #
    H1 = getHessianRD(s11, b, c, m)
    H2 = getHessianTD(s22, a, c, m)
    H3 = getHessianND(s33, a, b, m)

    # ¿Se podría crear direcciones pk_1, pk_2, pk_3 individuales con
    # las matrices Hi 2x2 y usarlas para chequear las condiciones
    # de Armijo y Wolfe?

    H = H1 + H2 + H3
    pk = np.dot(-np.linalg.inv(H), f0)

    while(alpha > 1e-8):
        a1 = a0 + alpha * pk[0]
        b1 = b0 + alpha * pk[1]
        c1 = c0 + alpha * pk[2]
        fnext = getPhiVector(s11, s22, s33, a1, b1, c1, m)
        Jk = getJacobian(s11, s22, s33, a1, b1, c1, m)
        grad_fk = np.dot(Jk, fnext)
       
        armijo_condition = (
            fnext[0] <= f0[0] + c * alpha * np.dot(grad_f, pk) and
            fnext[1] <= f0[1] + c * alpha * np.dot(grad_f, pk) and
            fnext[2] <= f0[2] + c * alpha * np.dot(grad_f, pk))
        wolfe_condition = (
            abs(np.dot(grad_fk, pk)) <= nu*abs(np.dot(grad_f, pk)) and
            abs(np.dot(grad_fk, pk)) <= nu*abs(np.dot(grad_f, pk)) and
            abs(np.dot(grad_fk, pk)) <= nu*abs(np.dot(grad_f, pk)))
        if(armijo_condition and wolfe_condition):
            return alpha
        else:
            alpha *= rho
    return alpha_max



Mis dudas son:
1- ¿Qué significa que una matriz sea definida positiva?
2- ¿Es correcto sumar los Hessianos para obtener un Hessiano resultante? En caso afirmativo, ¿Qué significado físico tiene?
3- Para obtener la dirección $$p_k$$, ¿es correcto usar este Hessiano resultante $$H$$ y obtener así una única dirección para las tres componentes?
4- Para realizar el line search es correcto utilizar una única dirección calculada a partir de $$H$$ o necesito un vector para cada Hessiano, de tal modo que:

$$p_{k1}=\begin{pmatrix}0\\n_b\\n_c\end{pmatrix}; p_{k2}=\begin{pmatrix}n_a\\0\\n_c\end{pmatrix}; p_{k3}=\begin{pmatrix}n_a\\n_b\\0\end{pmatrix}$$

He probado con la duda 3, y ahora si actualizo el valor de $$\alpha$$ en según que iteración. Pero al actualizar $$\alpha$$ se disminuye el valor del incremento de paso por lo que se produce un efecto de amortiguación en la convergencia. Usando FindRoot de Wolfram Mathematica obtengo los mismos valores si $$\bar{\sigma}$$ tiene un valor de $$1.0$$ o de $$350$$ para unos valores iniciales $$a=1, b=1, c=1$$. FindRoot usa por defecto Newton Raphson con line search. De aquí la motivación de todo esto. Con mi implementación de Newton Raphson con line search obtengo, aunque dentro de la tolerancia, distintos valores de $$a, b$$ y $$c$$ en función de si $$\bar{\sigma}=1.0$$ o $$\bar{\sigma}=350$$

5- Por último, en las definicion de $$p_k$$ y en las condiciones se usa el gradiente de f, $$\nabla{f(X_k)}$$, en algún lado vi que se puede calcular el gradiente como $$\nabla{f(X_k)}=J·B$$, siendo $$J$$ el Jacobiano y $$B$$ el vector con los valores de las funciones para $$X_k$$. Esta definición es la que he implementado en la función de line search, ¿Es correcto? ¿Qué significado físico tiene? ¿debería usar los gradientes del Jacobiano $$\nabla{f_1(X_k)}; \nabla{f_2(X_k)}; \nabla{f_3(X_k)}$$ directamente en vez de $$\nabla{f(X_k)}=J·B$$ }?

Les estaría muy agradecido si me pudieran aclarar estas cosas.

Que tengan un buen fin de semana. :)