Problemas de Optimización Con Python
Problemas de Optimización Con Python
Introducción
La optimización es fundamental para cualquier problema relacionado con la toma de
decisiones, ya sea en ingeniería o en ciencias económicas. La tarea de tomar decisiones
implica elegir entre varias alternativas. Esta opción va a estar gobernada por nuestro deseo
de tomar la "mejor" decisión posible. Que tan buena va a ser cada una de las alternativas va a
estar descripta por una función objetivo o índice de rendimiento. La teoría y los métodos
de optimización nos van a ayudar a seleccionar la mejor alternativa de acuerdo a esta función
objetivo dada. El área de optimización ha recibido gran atención en los últimos años,
principalmente por el rápido desarrollo de las ciencias de computación, incluido el desarrollo
y la disponibilidad de herramientas de software sumamente amigables, procesadores
paralelos de alta velocidad, y redes neuronales artificiales. El poder de los métodos
de optimización reside en la posibilidad de determinar la solución óptima sin realmente tener
que probar todos los casos posibles. Para logar esto, se utiliza un nivel modesto
de Matemáticas y se realizan cálculos numéricos iterativos utilizando procedimientos lógicos
claramente definidos o algoritmos implementados en computadoras.
Programación lineal
Matemáticamente, un problema de optimización con restricciones asume la siguiente forma:
minf0(x)sujeto a fi(x)≤bi,i=1,…,m.minf0(x)sujeto a fi(x)≤bi,i=1,…,m.
En donde el vector x=(x1,…,xn)x=(x1,…,xn) es la variable de optimización del problema,
la función f0:Rn→Rf0:Rn→R es la función objetivo, las
funciones fi:Rn→R,i=1,…,mfi:Rn→R,i=1,…,m; son las funciones
de restricciones de desigualdad; y las constantes b1,…,mb1,…,m son los límites de
las restricciones. Dentro de este marco, un caso importante es el de la programación lineal,
en el cual la función objetivo y las restricciones son lineales. El objetivo de la programación
lineal es determinar los valores de las variables de decisión que maximizan o
minimizan una función objetivo lineal, y en donde las variables de decisión están sujetas
a restricciones lineales. En general, el objetivo es encontrar un punto que minimice
la función objetivo al mismo tiempo que satisface las restricciones.
Para resolver un problema de programación lineal, debemos seguir los siguientes pasos:
1. Elegir las incógnitas o variables de decisión.
5. Calcular las coordenadas de los vértices del recinto de soluciones factibles (si son
pocos).
6. Calcular el valor de la función objetivo en cada uno de los vértices para ver en cuál
de ellos presenta el valor máximo o mínimo según nos pida el problema (hay que
tener en cuenta aquí la posible no existencia de solución).
Uno de los algoritmos más eficientes para resolver problemas de programación lineal, es
el método simplex.
Optimización convexa
Otro caso importante de optimización que debemos destacar es el de la optimización
convexa, en el cual la función objetivo y las restricciones son convexas. En realidad,
la programación lineal que vimos anteriormente, no es más que un caso especial
de optimización convexa.
Los conjuntos y funciones convexas tienen algunas propiedades que los hacen especiales
para problemas de optimización, como ser:
Una función convexa no tiene mínimos locales que no sean globales.
Un conjunto convexo tiene un interior relativo no vacío.
# funcion modelo
def f(x, b0, b1, b2):
return b0 + b1 * np.exp(-b2 * x**2)
# datos aleatorios para simular las observaciones
xdata = np.linspace(0, 5, 50)
y = f(xdata, *beta)
ydata = y + 0.05 * np.random.randn(len(xdata))
# función residual
def g(beta):
return ydata - f(xdata, *beta)
# comenzamos la optimización
beta_start = (1, 1, 1)
beta_opt, beta_cov = optimize.leastsq(g, beta_start)
beta_opt
Out[2]:
array([ 0.24022514, 0.76030423, 0.48425909])
In [3]:
# graficamos
fig, ax = plt.subplots(figsize=(10,8))
ax.scatter(xdata, ydata)
ax.plot(xdata, y, 'r', lw=2)
ax.plot(xdata, f(xdata, *beta_opt), 'b', lw=2)
ax.set_xlim(0, 5)
ax.set_xlabel(r"$x$", fontsize=18)
ax.set_ylabel(r"$f(x, \beta)$", fontsize=18)
ax.set_title('Mínimos cuadrados no lineales')
plt.show()
# función a minimizar
def f(X):
x, y = X
return (x - 1)**2 + (y - 1)**2
# graficando la solución
fig, ax = plt.subplots(figsize=(10, 8))
x_ = y_ = np.linspace(-1, 3, 100)
X, Y = np.meshgrid(x_, y_)
c = ax.contour(X, Y, func_X_Y_to_XY(f, X, Y), 50)
ax.plot(x_opt[0], x_opt[1], 'b*', markersize=15)
ax.plot(x_cons_opt[0], x_cons_opt[1], 'r*', markersize=15)
bound_rect = plt.Rectangle((bnd_x1[0], bnd_x2[0]),
bnd_x1[1] - bnd_x1[0], bnd_x2[1] - bnd_x2[0],
facecolor="grey")
ax.add_patch(bound_rect)
ax.set_xlabel(r"$x_1$", fontsize=18)
ax.set_ylabel(r"$x_2$", fontsize=18)
plt.colorbar(c, ax=ax)
ax.set_title('Optimización con restricciones')
plt.show()
Las restricciones que se definen por igualdades o desigualdades que incluyen más de una
variable son más complicadas de tratar. Sin embargo, existen técnicas generales que también
podemos utilizar para este tipo de problemas. Volviendo al ejemplo anterior, cambiemos la
restricción por una más compleja, como
ser: g(x)=x1−1.75−(x0−0.75)4≥0g(x)=x1−1.75−(x0−0.75)4≥0. Para resolver este
problema scipy.optimize nos ofrece un método llamado programación secuencial por
mínimos cuadrados, o SLSQP por su abreviatura en inglés.
In [5]:
# Ejemplo scipy SLSQP
# funcion de restriccion
def g(X):
return X[1] - 1.75 - (X[0] - 0.75)**4
# definimos el diccionario con la restricción
restriccion = dict(type='ineq', fun=g)
# resolvemos
x_opt = optimize.minimize(f, (0, 0), method='BFGS').x
x_cons_opt = optimize.minimize(f, (0, 0), method='SLSQP',
constraints=restriccion).x
# graficamos
ig, ax = plt.subplots(figsize=(10, 8))
x_ = y_ = np.linspace(-1, 3, 100)
X, Y = np.meshgrid(x_, y_)
c = ax.contour(X, Y, func_X_Y_to_XY(f, X, Y), 50)
ax.plot(x_opt[0], x_opt[1], 'b*', markersize=15)
ax.plot(x_, 1.75 + (x_-0.75)**4, 'k-', markersize=15)
ax.fill_between(x_, 1.75 + (x_-0.75)**4, 3, color='grey')
ax.plot(x_cons_opt[0], x_cons_opt[1], 'r*', markersize=15)
ax.set_ylim(-1, 3)
ax.set_xlabel(r"$x_0$", fontsize=18)
ax.set_ylabel(r"$x_1$", fontsize=18)
plt.colorbar(c, ax=ax)
plt.show()
x1+1.5x2≤7502x1+x2≤1000x1≥0x2≥0x1+1.5x2≤7502x1+x2≤1000x1≥0x2≥0
In [6]:
# Ejemplo programación lineal con CVXopt
# resolviendo el problema
sol=cvxopt.solvers.lp(c,A,b)
pcost dcost gap pres dres k/t
0: -2.5472e+04 -3.6797e+04 5e+03 0e+00 3e-01 1e+00
1: -2.8720e+04 -2.9111e+04 1e+02 2e-16 9e-03 2e+01
2: -2.8750e+04 -2.8754e+04 1e+00 8e-17 9e-05 2e-01
3: -2.8750e+04 -2.8750e+04 1e-02 4e-16 9e-07 2e-03
4: -2.8750e+04 -2.8750e+04 1e-04 9e-17 9e-09 2e-05
Optimal solution found.
In [7]:
# imprimiendo la solucion.
print('{0:.2f}, {1:.2f}'.format(sol['x'][0]*-1, sol['x'][1]*-1))
375.00, 250.00
In [8]:
# Resolviendo la optimizacion graficamente.
x_vals = np.linspace(0, 800, 10) # 10 valores entre 0 y 800
y1 = ((750 - x_vals)/1.5) # x1 + 1.5x2 = 750
y2 = (1000 - 2*x_vals) # 2x1 + x2 = 1000
plt.figure(figsize=(10,8))
plt.plot(x_vals, y1, label=r'$x_1 + 1.5x_2 \leq 750$')
plt.plot(x_vals, y2, label=r'$2x_1 + x_2 \leq 1000$') #
plt.plot(375, 250, 'b*', markersize=15)
# Región factible
y3 = np.minimum(y1, y2)
plt.fill_between(x_vals, 0, y3, alpha=0.15, color='b')
plt.axis(ymin = 0)
plt.title('Optimización lineal')
plt.legend()
plt.show()
Como podemos ver, tanto la solución utilizando CVXopt, como la solución gráfica; nos
devuelven el mismo resultado x1=375x1=375 y x2=250x2=250.
El problema de transporte
El problema de transporte es un problema clásico de programación lineal en el cual se
debe minimizar el costo del abastecimiento a una serie de puntos de demanda a partir de un
grupo de puntos de oferta, teniendo en cuenta los distintos precios de envío de cada punto de
oferta a cada punto de demanda. Por ejemplo, supongamos que tenemos que enviar cajas de
cervezas de 2 cervecerías a 5 bares de acuerdo al siguiente gráfico:
Asimismo, supongamos que nuestro gerente financiero nos informa que el costo de
transporte por caja de cada ruta se conforma de acuerdo a la siguiente tabla:
Y por último, las restricciones del problema, van a estar dadas por las capacidades de oferta
y demanda de cada cervecería y cada bar, las cuales se detallan en el gráfico de más arriba.
Veamos como podemos modelar este ejemplo con la ayuda de PuLP y de Pyomo.
In [9]:
# Ejemplo del problema de transporte de las cervezas utilizando PuLP
# Creamos la variable prob que contiene los datos del problema
prob = pulp.LpProblem("Problema de distribución de cerveza", pulp.LpMinimize)
In [10]:
# Creamos lista de cervecerías o nodos de oferta
cervecerias = ["Cervecería A", "Cervercería B"]
# Creamos una lista de tuplas que contiene todas las posibles rutas de tranporte.
rutas = [(c,b) for c in cervecerias for b in bares]
In [11]:
# creamos diccionario x que contendrá la candidad enviada en las rutas
x = pulp.LpVariable.dicts("ruta", (cervecerias, bares),
lowBound = 0,
cat = pulp.LpInteger)
# Resolviendo el problema.
prob.solve()
Como vemos, la solución óptima que encontramos con la ayuda de PuLP, nos dice que
deberíamos enviar desde la Cervecería A, 300 cajas al Bar 1 y 700 cajas al Bar 5; y que
desde la Cervecería B deberíamos enviar 200 cajas al Bar 1, 900 cajas al Bar 2, 1800 cajas al
Bar 3 y 200 cajas al Bar 4. De esta forma podemos minimizar el costo de transporte a un
total de 8600.
Veamos ahora si podemos formular este mismo problema con Pyomo, así podemos darnos
una idea de las diferencias entre las herramientas.
In [12]:
# Ejemplo del problema de transporte de las cervezas utilizando Pyomo
# Creamos el modelo
modelo = ConcreteModel()
# Límite de demanda
def f_demanda(modelo, j):
return sum(modelo.x[i,j] for i in modelo.i) >= modelo.b[j]
modelo.demanda = Constraint(modelo.j, rule=f_demanda,
doc='Límites demanda de cada bar')
In [14]:
## Definimos la función objetivo y resolvemos el problema ##
# Función objetivo
def f_objetivo(modelo):
return sum(modelo.c[i,j]*modelo.x[i,j] for i in modelo.i for j in modelo.j)
modelo.objetivo = Objective(rule=f_objetivo, sense=minimize,
doc='Función Objetivo')
# imprimimos resultados
print("\nSolución óptima encontrada\n" + '-'*80)
pyomo_postprocess(None, None, resultados)
Solución óptima encontrada
--------------------------------------------------------------------------------
x : Cantidad de cajas
Size=10, Index=x_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
('Cervecería A', 'Bar 1') : 0.0 : 300.0 : None : False : False : Reals
('Cervecería A', 'Bar 2') : 0.0 : 0.0 : None : False : False : Reals
('Cervecería A', 'Bar 3') : 0.0 : 0.0 : None : False : False : Reals
('Cervecería A', 'Bar 4') : 0.0 : 0.0 : None : False : False : Reals
('Cervecería A', 'Bar 5') : 0.0 : 700.0 : None : False : False : Reals
('Cervecería B', 'Bar 1') : 0.0 : 200.0 : None : False : False : Reals
('Cervecería B', 'Bar 2') : 0.0 : 900.0 : None : False : False : Reals
('Cervecería B', 'Bar 3') : 0.0 : 1800.0 : None : False : False : Reals
('Cervecería B', 'Bar 4') : 0.0 : 200.0 : None : False : False : Reals
('Cervecería B', 'Bar 5') : 0.0 : 0.0 : None : False : False : Reals
Como podemos ver, arribamos a la mismo solución que utilizando PuLP. Ambas
herramientas siguen un lenguaje de modelado totalmente distinto, particularmente me gusta
más la sintaxis que ofrece PuLP sobre la de Pyomo.