SciPy, optimización con restricciones

SciPy, optimización con restricciones

SciPy (se pronuncia como sai pai) es un paquete matemático basado en numpy, que también incluye bibliotecas en C y Fortran. Con SciPy, una sesión interactiva de Python se convierte en un entorno completo para el procesamiento de datos, similar a MATLAB, IDL, Octave, R o SciLab.

En este artículo, revisaremos las técnicas básicas de programación matemática: soluciones a problemas de optimización con restricciones para funciones escalares de múltiples variables utilizando el paquete scipy.optimize. Los algoritmos de optimización sin restricciones ya se han revisado en el artículo anterior. Se puede obtener siempre una guía más detallada y actualizada sobre las funciones de scipy utilizando el comando help(), Shift+Tab o en documentación oficial.

Introducción

La interfaz general para resolver problemas tanto de optimización con restricciones como sin restricciones en el paquete scipy.optimize se proporciona mediante la función minimize(). Sin embargo, se sabe que no existe un método universal para resolver todos los problemas, por lo que la elección del método adecuado recae, como siempre, en el investigador.
El algoritmo de optimización adecuado se determina mediante el argumento de la función minimize(..., method="").
Para la optimización con restricciones de funciones de múltiples variables, están disponibles implementaciones de los siguientes métodos:

  • trust-constr — búsqueda de un mínimo local en un área de confianza. Artículo en wiki, artículo en habr;
  • SLSQP — programación cuadrática secuencial con restricciones, método de Newton para resolver el sistema de Lagrange. Artículo en wiki.
  • TNC — Truncated Newton Constrained, número limitado de iteraciones, bueno para funciones no lineales con un gran número de variables independientes. Artículo en wiki.
  • L-BFGS-B — método de la cuádruple Broyden–Fletcher–Goldfarb–Shanno, implementado con un menor consumo de memoria mediante la carga parcial de vectores de la matriz Hessiana. Artículo en wiki, artículo en habr.
  • COBYLA — COBYLA (Constrained Optimization By Linear Approximation), optimización restringida con aproximación lineal (sin cálculo del gradiente). Artículo en wiki.

Dependiendo del método elegido, las condiciones y restricciones para resolver el problema se establecen de diferentes maneras:

  • con un objeto de la clase Bounds para los métodos L-BFGS-B, TNC, SLSQP, trust-constr;
  • con una lista (min, max) para los mismos métodos L-BFGS-B, TNC, SLSQP, trust-constr;
  • con un objeto o lista de objetos LinearConstraint, NonlinearConstraint para los métodos COBYLA, SLSQP, trust-constr;
  • con un diccionario o lista de diccionarios {'type':str, 'fun':callable, 'jac':callable,opt, 'args':sequence,opt} para los métodos COBYLA, SLSQP.

Plan del artículo:
1) Considerar la aplicación del algoritmo de optimización con restricciones en un área de confianza (method="trust-constr") con restricciones establecidas como objetos. Bounds, LinearConstraint, NonlinearConstraint ;
2) Considerar la programación secuencial mediante el método de los mínimos cuadrados (method=»SLSQP») con restricciones definidas como un diccionario {'type', 'fun', 'jac', 'args'};
3) Analizar un ejemplo de optimización de la producción a través de un estudio web.

Optimización condicional method=»trust-constr»

Implementación del método trust-constr se basa en EQSQP para problemas con restricciones en forma de igualdad y en TRIP para problemas con restricciones en forma de desigualdad. Ambos métodos están implementados por algoritmos que buscan el mínimo local en un área de confianza y son adecuados para problemas a gran escala.

Planteamiento matemático del problema de búsqueda de mínimos en forma general:

SciPy, optimización con restricciones

SciPy, optimización con restricciones

SciPy, optimización con restricciones

Para las restricciones de igualdad estricta, el límite inferior se establece igual al límite superior SciPy, optimización con restricciones.
Para la restricción unilateral, se establece el límite superior o inferior np.inf con el signo correspondiente.
Supongamos que es necesario encontrar el mínimo de la conocida función de Rosenbrock de dos variables:

SciPy, optimización con restricciones

Dichas restricciones son las siguientes sobre su dominio:

SciPy, optimización con restricciones

SciPy, optimización con restricciones

SciPy, optimización con restricciones

SciPy, optimización con restricciones

SciPy, optimización con restricciones

SciPy, optimización con restricciones

En nuestro caso hay una única solución en el punto SciPy, optimización con restricciones, para el cual solo se cumplen la primera y la cuarta restricciones.
Pasemos por las restricciones de abajo hacia arriba y veamos cómo se pueden expresar en scipy.
Limitaciones SciPy, optimización con restricciones y SciPy, optimización con restricciones definir con el objeto Bounds.

from scipy.optimize import Bounds
bounds = Bounds ([0, -0.5], [1.0, 2.0])

Limitaciones SciPy, optimización con restricciones y SciPy, optimización con restricciones escribamos en forma lineal:

SciPy, optimización con restricciones

Definamos estas restricciones como un objeto LinearConstraint:

import numpy as np
from scipy.optimize import LinearConstraint
linear_constraint = LinearConstraint ([[1, 2], [2, 1]], [-np.inf, 1], [1, 1])

Y finalmente, la restricción no lineal en forma matricial:

SciPy, optimización con restricciones

Definimos la matriz Jacobiana para esta restricción y la combinación lineal de la matriz Hessiana con un vector arbitrario SciPy, optimización con restricciones:

SciPy, optimización con restricciones

SciPy, optimización con restricciones

Ahora podemos definir la restricción no lineal como un objeto NonlinearConstraint:

from scipy.optimize import NonlinearConstraint

def cons_f(x):
     return [x[0]**2 + x[1], x[0]**2 - x[1]]

def cons_J(x):
     return [[2*x[0], 1], [2*x[0], -1]]

def cons_H(x, v):
     return v[0]*np.array([[2, 0], [0, 0]]) + v[1]*np.array([[2, 0], [0, 0]])

nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1, jac=cons_J, hess=cons_H)

Si el tamaño es grande, las matrices se pueden definir también en forma dispersa:

from scipy.sparse import csc_matrix

def cons_H_sparse(x, v):
     return v[0]*csc_matrix([[2, 0], [0, 0]]) + v[1]*csc_matrix([[2, 0], [0, 0]])

nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1,
                                            jac=cons_J, hess=cons_H_sparse)

o como un objeto LinearOperator:

from scipy.sparse.linalg import LinearOperator

def cons_H_linear_operator(x, v):
    def matvec(p):
        return np.array([p[0]*2*(v[0]+v[1]), 0])
    return LinearOperator((2, 2), matvec=matvec)

nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1,
                                jac=cons_J, hess=cons_H_linear_operator)

Cuando el cálculo de la matriz Hessiana SciPy, optimización con restricciones requiere altos costos, se puede utilizar la clase HessianUpdateStrategy.Las siguientes estrategias están disponibles: BFGS y SR1.

from scipy.optimize import BFGS

nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1, jac=cons_J, hess=BFGS())

La Hessiana también se puede calcular usando diferencias finitas:

nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1, jac='2-point', hess='2-point')

La matriz Jacobiana para las restricciones también se puede calcular usando diferencias finitas. Sin embargo, en este caso, la matriz Hessiana ya no se puede calcular con diferencias finitas. La Hessiana debe ser definida como una función o mediante la clase HessianUpdateStrategy.

nonlinear_constraint = NonlinearConstraint(cons_f, -np.inf, 1, jac='2-point', hess=BFGS())

La solución del problema de optimización es la siguiente:

from scipy.optimize import minimize
from scipy.optimize import rosen, rosen_der, rosen_hess, rosen_hess_prod

x0 = np.array([0.5, 0])
res = minimize(rosen, x0, method='trust-constr', jac=rosen_der, hess=rosen_hess,
                constraints=[linear_constraint, nonlinear_constraint],
                options={'verbose': 1}, bounds=bounds)
print(res.x)

`gtol` condición de terminación satisfecha.
Número de iteraciones: 12, evaluaciones de función: 8, iteraciones CG: 7, optimalidad: 2.99e-09, violación de restricciones: 1.11e-16, tiempo de ejecución: 0.033 s.
[0.41494531 0.17010937]

Si es necesario, la función que calcula la Hessiana se puede definir utilizando la clase LinearOperator.

def rosen_hess_linop(x):
    def matvec(p):
        return rosen_hess_prod(x, p)
    return LinearOperator((2, 2), matvec=matvec)

res = minimize(rosen, x0, method='trust-constr', jac=rosen_der, hess=rosen_hess_linop,
                 constraints=[linear_constraint, nonlinear_constraint],
                 options={'verbose': 1}, bounds=bounds)

print(res.x)

o el producto de la Hessiana y un vector arbitrario a través del parámetro hessp:

res = minimize(rosen, x0, method='trust-constr', jac=rosen_der, hessp=rosen_hess_prod,
                constraints=[linear_constraint, nonlinear_constraint],
                options={'verbose': 1}, bounds=bounds)
print(res.x)

Alternativamente, las primeras y segundas derivadas de la función optimizada pueden ser calculadas aproximadamente. Por ejemplo, la Hessiana puede ser aproximada mediante una función SR1 (aproximación cuasi-Newton). El gradiente puede ser aproximado utilizando diferencias finitas.

from scipy.optimize import SR1
res = minimize(rosen, x0, method='trust-constr',  jac='2-point', hess=SR1(),
               constraints=[linear_constraint, nonlinear_constraint],
               options={'verbose': 1}, bounds=bounds)
print(res.x)

Optimización condicional método='SLSQP'

El método SLSQP está diseñado para resolver problemas de minimización de funciones en forma de:

SciPy, optimización con restricciones

SciPy, optimización con restricciones

SciPy, optimización con restricciones

SciPy, optimización con restricciones

Donde SciPy, optimización con restricciones y SciPy, optimización con restricciones — conjuntos de índices de expresiones que describen restricciones en forma de igualdades o desigualdades. SciPy, optimización con restricciones — conjuntos de límites inferiores y superiores para el dominio de definición de la función.

Las restricciones lineales y no lineales se describen en forma de diccionarios con claves tipo, fun y jac.

ineq_cons = {'type': 'ineq',
             'fun': lambda x: np.array ([1 - x [0] - 2 * x [1],
                                          1 - x [0] ** 2 - x [1],
                                          1 - x [0] ** 2 + x [1]]),
             'jac': lambda x: np.array ([[- 1.0, -2.0],
                                          [-2 * x [0], -1.0],
                                          [-2 * x [0], 1.0]])
            }

eq_cons = {'type': 'eq',
           'fun': lambda x: np.array ([2 * x [0] + x [1] - 1]),
           'jac': lambda x: np.array ([2.0, 1.0])
          }

La búsqueda del mínimo se realiza de la siguiente manera:

x0 = np.array([0.5, 0])
res = minimize(rosen, x0, method='SLSQP', jac=rosen_der,
               constraints=[eq_cons, ineq_cons], options={'ftol': 1e-9, 'disp': True},
               bounds=bounds)

print(res.x)

La optimización se ha terminado con éxito.    (Modo de salida 0)
            Valor actual de la función: 0.34271757499419825
            Iteraciones: 4
            Evaluaciones de función: 5
            Evaluaciones de gradiente: 4
[0.41494475 0.1701105 ]

Ejemplo de optimización

Con la transición hacia la quinta revolución tecnológica, consideremos la optimización de la producción a través del ejemplo de una agencia web que nos genera ingresos pequeños pero estables. Pongámonos en el lugar del director de una galería, donde se producen tres tipos de productos:

  • x0 — landing pages que venden, desde 10 mil R.
  • x1 — sitios web corporativos, desde 20 mil R.
  • x2 — tiendas en línea, desde 30 mil R.

Nuestro amistoso equipo de trabajo consiste en cuatro juniors, dos mediadores y un senior. El fondo de su tiempo laboral al mes:

  • juniors: 4 * 150 = 600 horas-persona,
  • mediadores: 2 * 150 = 300 horas-persona,
  • senior: 150 horas-persona.

Supongamos que para desarrollar y desplegar un sitio de tipo (x0, x1, x2), el primer junior que se encuentra necesita dedicar (10, 20, 30) horas, el mediador — (7, 15, 20), y el senior — (5, 10, 15) horas del mejor tiempo de su vida.

Como a cualquier director normal, nos gustaría maximizar las ganancias mensuales. El primer paso hacia el éxito es escribir la función objetivo value como la suma de los ingresos generados por la producción mensual:

def value(x):
    return - 10*x[0] - 20*x[1] - 30*x[2]

Esto no es un error, al buscar el máximo la función objetivo se minimiza con un signo inverso.

El siguiente paso es prohibir a los empleados hacer horas extras e introducir restricciones sobre el fondo de tiempo laboral:

SciPy, optimización con restricciones

Lo cual es equivalente a:

SciPy, optimización con restricciones

ineq_cons = {'type': 'ineq',
             'fun': lambda x: np.array ([600 - 10 * x [0] - 20 * x [1] - 30 * x[2],
                                         300 - 7  * x [0] - 15 * x [1] - 20 * x[2],
                                         150 - 5  * x [0] - 10 * x [1] - 15 * x[2]])
            }

La restricción formal es que la producción debe ser únicamente positiva:

bnds = Bounds ([0, 0, 0], [np.inf, np.inf, np.inf])

Y finalmente, la suposición más optimista: debido a nuestros precios bajos y alta calidad, siempre hay una cola de clientes satisfechos. Podemos elegir nosotros mismos los volúmenes de producción mensual, basándonos en la solución de un problema de optimización condicional con scipy.optimize:

x0 = np.array([10, 10, 10])
res = minimize(value, x0, method='SLSQP', constraints=ineq_cons, bounds=bnds)
print(res.x)

[7.85714286 5.71428571 3.57142857]

Redondeamos a números enteros y calculamos la carga mensual de los remeros en el escenario óptimo de producción x = (8, 6, 3) :

  • juniors: 8 * 10 + 6 * 20 + 3 * 30 = 290 personas * hora;
  • mediadores: 8 * 7 + 6 * 15 + 3 * 20 = 206 personas * hora;
  • senior: 8 * 5 + 6 * 10 + 3 * 15 = 145 personas * hora.

Conclusión: para que el director reciba su máximo merecido, lo óptimo es hacer al mes 8 landing pages, 6 sitios web medios y 3 tiendas. El senior debe trabajar sin descanso, la carga de los mid-levels será aproximadamente de 2/3, y la de los juniors menos de la mitad.

Conclusión

En el artículo se exponen las principales técnicas para trabajar con el paquete scipy.optimize, utilizadas para resolver problemas de minimización condicional. Personalmente, utilizo scipy solo con fines académicos, por lo que el ejemplo dado tiene un carácter humorístico.

Se puede encontrar mucha teoría y ejemplos en raros en el libro de I.L. Akulich "Programación matemática en ejemplos y tareas". Un uso más hardcore scipy.optimize para construir una estructura 3D a partir de un conjunto de imágenes (artículo en habr) se puede ver en scipy-cookbook.

La fuente principal de información es docs.scipy.org, los interesados en contribuir a la traducción de esta y otras secciones scipy son bienvenidos a GitHub.

Gracias mephistopheies por su participación en la preparación de la publicación.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster