SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

SciPy (pronunciato come sai pai) è un pacchetto matematico basato su numpy, che include anche librerie in C e Fortran. Con SciPy, una sessione interattiva di Python si trasforma in un ambiente di elaborazione dati completo, come MATLAB, IDL, Octave, R o SciLab.

In questo articolo esploreremo le principali tecniche di programmazione matematica — soluzioni per problemi di ottimizzazione vincolata per una funzione scalare di più variabili utilizzando il pacchetto scipy.optimize. Gli algoritmi di ottimizzazione non vincolata sono già stati trattati in un articolo precedente. Una documentazione più dettagliata e aggiornata sulle funzioni di scipy è sempre disponibile tramite il comando help(), Shift+Tab o in documentazione ufficiale.

Introduzione

L'interfaccia generale per risolvere sia problemi di ottimizzazione vincolata che non vincolata nel pacchetto scipy.optimize è fornita dalla funzione minimize(). Tuttavia, è noto che non esiste un metodo universale per risolvere tutti i problemi, quindi la scelta di un metodo adeguato ricade sempre sulle spalle del ricercatore.
L'algoritmo di ottimizzazione appropriato è definito tramite l'argomento della funzione minimize(..., method="").
Per l'ottimizzazione vincolata di funzioni di più variabili sono disponibili implementazioni dei seguenti metodi:

  • trust-constr — ricerca del minimo locale in un'area di fiducia. Articolo su wiki, articolo su habra;
  • SLSQP — programmazione quadratica sequenziale con vincoli, metodo di Newton per risolvere il sistema di Lagrange. Articolo su wiki.
  • TNC — Truncated Newton Constrained, numero limitato di iterazioni, buono per funzioni non lineari con un alto numero di variabili indipendenti. Articolo su wiki.
  • L-BFGS-B — metodo della quadrupla Broyden–Fletcher–Goldfarb–Shanno, implementato con un ridotto consumo di memoria grazie al caricamento parziale dei vettori dalla matrice Hessiana. Articolo su wiki, articolo su habra.
  • COBYLA — KOBYLA Constrained Optimization By Linear Approximation, ottimizzazione vincolata con approssimazione lineare (senza calcolo del gradiente). Articolo su wiki.

A seconda del metodo scelto, le condizioni e i vincoli per risolvere il problema sono definiti in modi diversi:

  • oggetto della classe Bounds per i metodi L-BFGS-B, TNC, SLSQP, trust-constr;
  • lista (min, max) per questi stessi metodi L-BFGS-B, TNC, SLSQP, trust-constr;
  • oggetto o lista di oggetti LinearConstraint, NonlinearConstraint per i metodi COBYLA, SLSQP, trust-constr;
  • dizionario o lista di dizionari {'type':str, 'fun':callable, 'jac':callable,opt, 'args':sequence,opt} per i metodi COBYLA, SLSQP.

Piano dell'articolo:
1) Considerare l'applicazione dell'algoritmo di ottimizzazione vincolata nella regione di fiducia (method=»trust-constr») con vincoli definiti sotto forma di oggetti Bounds, LinearConstraint, NonlinearConstraint ;
2) Considerare la programmazione sequenziale mediante metodo dei minimi quadrati (method=»SLSQP») con vincoli definiti in forma di dizionario {'type', 'fun', 'jac', 'args'};
3) Analizzare un esempio di ottimizzazione della produzione in un'agenzia web.

Ottimizzazione vincolata method=»trust-constr»

L'implementazione del metodo trust-constr si basa su EQSQP per problemi con vincoli di uguaglianza e su TRIP per problemi con vincoli di disuguaglianza. Entrambi i metodi sono implementati mediante algoritmi di ricerca del minimo locale nella regione di fiducia e sono adatti per problemi su larga scala.

La formulazione matematica del problema di ricerca del minimo in forma generale:

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

Per vincoli di uguaglianza rigorosa, il limite inferiore è impostato uguale al limite superiore SciPy, ottimizzazione con vincoli.
Per un vincolo unilaterale, il limite superiore o inferiore è impostato np.inf con il segno corrispondente.
Supponiamo di dover trovare il minimo della nota funzione di Rosenbrock di due variabili:

SciPy, ottimizzazione con vincoli

In questo caso, sono imposti i seguenti vincoli sulla sua area di definizione:

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

Nel nostro caso, c'è una sola soluzione nel punto SciPy, ottimizzazione con vincoli, che soddisfa solo il primo e il quarto vincolo.
Esaminiamo i vincoli dall'alto verso il basso e vediamo come possono essere registrati in scipy.
Limitazioni SciPy, ottimizzazione con vincoli e SciPy, ottimizzazione con vincoli definiamoli utilizzando l'oggetto Bounds.

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

Limitazioni SciPy, ottimizzazione con vincoli e SciPy, ottimizzazione con vincoli scriviamo in forma lineare:

SciPy, ottimizzazione con vincoli

Definiamo questi vincoli come oggetto LinearConstraint:

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

E infine un vincolo non lineare in forma matriciale:

SciPy, ottimizzazione con vincoli

Definiamo la matrice Jacobiana per questo vincolo e una combinazione lineare della matrice Hessiana con un vettore arbitrario SciPy, ottimizzazione con vincoli:

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

Ora possiamo definire il vincolo non lineare come oggetto 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)

Se la dimensione è grande, le matrici possono essere definite anche in forma sparsa:

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 come oggetto LinearOperator:

da 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)

Quando il calcolo della matrice Hessiana SciPy, ottimizzazione con vincoli richiede elevati costi, è possibile utilizzare la classe HessianUpdateStrategy. Sono disponibili le seguenti strategie: BFGS e SR1.

da scipy.optimize import BFGS

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

L'Hessiano può anche essere calcolato usando le differenze finite:

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

La matrice Jacobiana per i vincoli può essere calcolata anche usando le differenze finite. Tuttavia, in questo caso l'Hessiano non può più essere calcolato con le differenze finite. L'Hessiano deve essere definito come una funzione o utilizzando la classe HessianUpdateStrategy.

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

La soluzione del problema di ottimizzazione appare come segue:

da scipy.optimize import minimize
da 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` condizione di terminazione soddisfatta.
Numero di iterazioni: 12, valutazioni della funzione: 8, iterazioni CG: 7, optimalità: 2.99e-09, violazione del vincolo: 1.11e-16, tempo di esecuzione: 0.033 s.
[0.41494531 0.17010937]

Se necessario, la funzione per calcolare l'Hessiano può essere definita usando la classe 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 il prodotto dell'Hessiano e di un vettore arbitrario tramite il parametro 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)

In alternativa, le prime e seconde derivate della funzione ottimizzata possono essere calcolate approssimativamente. Ad esempio, l'Hessiano può essere approssimato usando la funzione SR1 (approssimazione quasi-newtoniana). Il gradiente può essere approssimato con le differenze finite.

da 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)

Ottimizzazione condizionata method='SLSQP'

Il metodo SLSQP è progettato per risolvere problemi di minimizzazione della funzione nella forma:

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

SciPy, ottimizzazione con vincoli

Dove SciPy, ottimizzazione con vincoli e SciPy, ottimizzazione con vincoli — insiemi di indici di espressioni che descrivono vincoli sotto forma di uguaglianze o disuguaglianze. SciPy, ottimizzazione con vincoli — insiemi di limiti inferiori e superiori per il dominio di definizione della funzione.

I vincoli lineari e non lineari sono descritti sotto forma di dizionari con chiavi type, fun e 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 ricerca del minimo avviene nel seguente modo:

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)

Ottimizzazione terminata con successo.    (Modalità di uscita 0)
            Valore attuale della funzione: 0.34271757499419825
            Iterazioni: 4
            Valutazioni della funzione: 5
            Valutazioni del gradiente: 4
[0.41494475 0.1701105 ]

Esempio di ottimizzazione

In relazione al passaggio al quinto paradigma tecnologico, consideriamo l'ottimizzazione della produzione prendendo come esempio uno studio web che ci fornisce un reddito modesto ma stabile. Immaginiamo di essere il direttore di una galleria, dove vengono prodotti tre tipi di prodotti:

  • x0 — landing page di vendita, a partire da 10.000.
  • x1 — siti web aziendali, a partire da 20.000.
  • x2 — negozi online, a partire da 30.000.

Il nostro affiatato team di lavoro è composto da quattro junior, due mid e un senior. Il loro tempo di lavoro disponibile per mese è:

  • junior: 4 * 150 = 600 ore * uomo,
  • mid: 2 * 150 = 300 ore * uomo,
  • senior: 150 ore * uomo.

Supponiamo che per sviluppare e rilasciare un sito di tipo (x0, x1, x2) il primo junior a disposizione debba impiegare (10, 20, 30) ore, il mid — (7, 15, 20), il senior — (5, 10, 15) ore del miglior tempo della sua vita.

Come a qualsiasi normale direttore, desideriamo massimizzare il profitto mensile. Il primo passo verso il successo è scrivere la funzione obiettivo value come la somma dei ricavi dei prodotti realizzati durante il mese:

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

Non è un errore, nella ricerca del massimo la funzione obiettivo viene minimizzata con segno opposto.

Il passo successivo è vietare il lavoro straordinario ai dipendenti e introdurre vincoli sul tempo di lavoro disponibile:

SciPy, ottimizzazione con vincoli

Che equivale a:

SciPy, ottimizzazione con vincoli

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 limitazione formale è che la produzione deve essere solo positiva:

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

E infine, l'assunzione più ottimista è che, grazie al prezzo basso e all'alta qualità, si crea continuamente una fila di clienti soddisfatti. Possiamo scegliere autonomamente i volumi di produzione mensili, basandoci sulla soluzione del problema di ottimizzazione condizionata 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]

Arrotondiamo in modo impreciso ai numeri interi e calcoliamo il carico mensile degli rematori nella situazione ottimale di produzione x = (8, 6, 3) :

  • junior: 8 * 10 + 6 * 20 + 3 * 30 = 290 persone * ora;
  • mid: 8 * 7 + 6 * 15 + 3 * 20 = 206 persone * ora;
  • senior: 8 * 5 + 6 * 10 + 3 * 15 = 145 persone * ora.

Conclusione: per far sì che il direttore riceva il suo meritato massimo, è ottimale realizzare 8 landing page, 6 siti medi e 3 negozi al mese. Il senior, nel frattempo, deve lavorare incessantemente, il carico dei mid-level sarà di circa 2/3, mentre per i junior sarà meno della metà.

Conclusione

L'articolo espone le principali tecniche di lavoro con il pacchetto scipy.optimize, utilizzate per risolvere problemi di minimizzazione condizionata. Personalmente utilizzo scipy esclusivamente a fini accademici, quindi l'esempio fornito ha un carattere piuttosto giocoso.

Molta teoria e esempi in WinRAR possono essere trovati, ad esempio, nel libro di I.L. Akulich "Programmazione matematica in esempi e compiti". Un'applicazione più hardcore scipy.optimize per costruire strutture 3D da un insieme di immagini (articolo su habra) può essere vista in scipy-cookbook.

La principale fonte di informazioni è docs.scipy.org, chi desidera contribuire alla traduzione di questa e di altre sezioni scipy è benvenuto su GitHub.

Grazie mephistopheies per la partecipazione nella preparazione della pubblicazione.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster