Clase 19: Scipy y computación numérica

En esta clase revisaremos el trabajo, la librería scipy.

Scipy

La librería scipy es una librería de computación científica que utiliza numpy. Nos va a permitir realizar optimización, procesos estadísticos, interpolación, procesamiento de imágenes, integración, entre otros.

Álgebra lineal

Para operaciones de álgebra lineal podemos ocupar el módulo linag.

#1. Determinante de una matriz
import numpy as np
from scipy import linalg
arr = np.array([[1, 2],
               [3, 4]])
linalg.det(arr)
-2.0
arr = np.array([[3, 2],
                [6, 4]])

linalg.det(arr)
0.0
#2. Inversa de una matriz
linalg.inv(arr)
# calcular la inversa de una matriz singular (su determinante es cero) generará un error
---------------------------------------------------------------------------
LinAlgError                               Traceback (most recent call last)
<ipython-input-3-317de499420c> in <module>
      1 #2. Inversa de una matriz
----> 2 linalg.inv(arr)
      3 # calcular la inversa de una matriz singular (su determinante es cero) generará un error

~/miniconda3/lib/python3.9/site-packages/scipy/linalg/basic.py in inv(a, overwrite_a, check_finite)
    966         inv_a, info = getri(lu, piv, lwork=lwork, overwrite_lu=1)
    967     if info > 0:
--> 968         raise LinAlgError("singular matrix")
    969     if info < 0:
    970         raise ValueError('illegal value in %d-th argument of internal '

LinAlgError: singular matrix
arr = np.array([[1, 2],
               [3, 4]])
linalg.inv(arr)
array([[-2. ,  1. ],
       [ 1.5, -0.5]])

Optimización

Podemos ocupar el módulo optimize para encontrar el mínimo (máximo) de una función (escalares o multidimensional).

from scipy import optimize
import matplotlib.pyplot as plt
def f(x):
    return x**2 + 10*np.sin(x)
x = np.arange(-10, 10, 0.1)
plt.plot(x, f(x)) 
plt.show() 
_images/Clase19_11_0.png

Mínimo global: corresponde al valor mínimo en el dominio de la función. Mínimo local: corresponde al valor mínimo en una región particular de la curva.

En el ejemplo tenemos un mínimo local y un mínimo global. Mediante un algoritmo de scipy podemos encontrar estos valores, por ejemplo podemos usar la función fmin_bfgs (método BFGS) para encontrar el mínimo.

#Usamos el módigo optmize, la función fmin_bfgs
#La sintaxis es fmin_bfgs(función a optimizar, valor inicial)
opt = optimize.fmin_bfgs(f, 0)
print(opt)
#El resultado nos entrega: 
#1) Valor de la función evaluada en el punto mínimo
#2) Número de iteraciones que se demoró en encontrar el mínimo
#3) Número de evaluaciones de la función
#4) Número de evaluaciones del gradiente
#5) Parámetro que minimiza la función
Optimization terminated successfully.
         Current function value: -7.945823
         Iterations: 5
         Function evaluations: 12
         Gradient evaluations: 6
[-1.30644012]

Por la naturaleza del método podemos quedar en el mínimo local si no partimos del “lado correcto”

optimize.fmin_bfgs(f, 3)
Optimization terminated successfully.
         Current function value: 8.315586
         Iterations: 6
         Function evaluations: 14
         Gradient evaluations: 7
array([3.83746709])

Cuando no sabemos el intervalo del mínimo global podemos ocupar un método costoso que utiliza la fuerza bruta, en que se evalúa la función en cada punto

grid = (-10, 10, 0.1)
xmin_global = optimize.brute(f, (grid,))
xmin_global
array([-1.30641113])

Podemos encontrar un mínimo local (dentro de un intervalo), usando fminbound

xmin_local = optimize.fminbound(f, 0, 10)    
print(xmin_local)
3.8374671194983834

Para encontrar las raíces de una función, podemos utilizar fsolve. La raíz corresponde al punto donde \(f(x)=0\).

root = optimize.fsolve(f, 1)  # our initial guess is 1
root
array([0.])

Si la función tiene más de una raíz, tenemos que cambiar el valor inicial para partir de un punto que nos lleve a la segunda (tercera…) solución

root2 = optimize.fsolve(f, -2.5)
root2
array([-2.47948183])
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, f(x), 'b-', label="f(x)")
xmins = np.array([xmin_global[0], xmin_local])
ax.plot(xmins, f(xmins), 'go', label="Minima")
roots = np.array([root, root2])
ax.plot(roots, f(roots), 'kv', label="Roots")
ax.legend()
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
Text(0, 0.5, 'f(x)')
_images/Clase19_24_1.png

Supongamos que \(f\) tiene un poco de ruido

xdata = np.linspace(-10, 10, num=20)
ydata = f(xdata) + np.random.randn(xdata.size)


plt.plot(x, f(x), 'b-', label="f(x)")
plt.plot(xdata, ydata, color="red")
[<matplotlib.lines.Line2D at 0x7f6a1004aa00>]
_images/Clase19_26_1.png

La forma funcional original es $\(f(x) = x^2 + 10*sin(x)\)$

Entonces podemos generalizar mediante

\[f(x,a,b) = a*x^2 + b*sin(x)\]

Mediante curve_fit() podemos encontrar los valores de a y b.

def f2(x,a ,b):
    return a*x**2 + b**np.sin(x)
#Valor inicial
guess = [2,2]

#curve_fit(f, xdata, ydata, valor inicial)
params, params_covariance = optimize.curve_fit(f2, xdata, ydata, guess)

#retorna: 
#1) Valores óptimos que minimizan f(xdata) - ydata
#2) Covarianza estimada de valores óptimos
params
array([ 0.95241285, 11.53081666])
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, f(x), 'b-', label="f(x)")
ax.plot(x, f2(x, *params), 'r--', label="Curve fit result")

xmins = np.array([xmin_global[0], xmin_local])
ax.plot(xmins, f(xmins), 'go', label="Minima")
roots = np.array([root, root2])
ax.plot(roots, f(roots), 'kv', label="Roots")
ax.legend()
ax.set_xlabel('x')
ax.set_ylabel('f(x)')
Text(0, 0.5, 'f(x)')
_images/Clase19_30_1.png

Estadísticas y números aleatorios

Podemos ocupar el módulo scipy.stats tiene potentes herramientas estadísticas y probabilísticas.

Para la generación de procesos aleatorios podemos ocupar numpy.random

a = np.random.normal(size=10_000)
plt.plot(a)
[<matplotlib.lines.Line2D at 0x7f6a0ffa56a0>]
_images/Clase19_32_1.png
plt.hist(x=a, bins=50, alpha=0.7, rwidth=0.85);
_images/Clase19_33_0.png
mu, sigma = 2, 0.5
s = np.random.normal(mu, sigma, 10000)
plt.hist(x=s, bins=50, alpha=0.7, rwidth=0.85);
_images/Clase19_34_0.png
n, p = 10, .5 
s = np.random.binomial(n, p, 10_000)
plt.plot(s)
#Resultado de tirar una moneda 10 veces, testeado 10_000 veces
[<matplotlib.lines.Line2D at 0x7f6a0f8fa4c0>]
_images/Clase19_35_1.png
plt.hist(x=s, bins=10, alpha=0.7, rwidth=0.85);
_images/Clase19_36_0.png
sum(np.random.binomial(2, 0.1, 20000) == 0)/20000
#COrremos el modelo 20_000 veces, y vemos el resultado de la probabilidad que se generen valores igual a cero
0.81165

Probability density function for norm: $\(f(x) = \frac{exp(-x^2/2)}{\sqrt{2\pi}}\)$

#Probability density function
from scipy.stats import norm

#Obtener momentos
x = np.linspace(-3, 3, 1000)
pdf = norm.pdf(x)
plt.plot(x, pdf)
[<matplotlib.lines.Line2D at 0x7f6a0e9992b0>]
_images/Clase19_39_1.png
cdf = norm.cdf(x)
plt.plot(x, cdf)
[<matplotlib.lines.Line2D at 0x7f6a0e971d30>]
_images/Clase19_40_1.png
cdf = np.linspace(0, 1, 1000)
ppf = norm.ppf(cdf)
plt.plot(ppf, cdf)
[<matplotlib.lines.Line2D at 0x7f6a0e92e910>]
_images/Clase19_41_1.png

Interpolación

Cuando tenemos datos y no conocemos su forma funcional, podemos realizar una aproximación mediante una interpolación. Esto consiste en evaluar los puntos y obtener una aproximación de la función. En simple, nos permite mediante un set de valores discretos tener una función que nos permite tener un espacio continuo

Para esto podemos ocupar el módulo interpolate

#Creamos datos experimentales: ejm similar a una función seno
x = np.linspace(0, 1, 10)
noise = (np.random.random(10)*2 - 1) * 1e-1
valores = np.sin(2 * np.pi * x) + noise
plt.plot(x, valores);
_images/Clase19_44_0.png
from scipy.interpolate import interp1d

#Creamos valores nuevos
val_nuevos = np.linspace(0, 1, 50)
#Interpolación lineal
linear_interp = interp1d(x, valores)
linear_results = linear_interp(val_nuevos)
#Interpolación cúbica
cubic_interp = interp1d(x, valores, kind='cubic')
cubic_results = cubic_interp(val_nuevos)
plt.plot(x, valores, 'o', ms=6, label='Valores')
plt.plot(val_nuevos, linear_results, label='linear interp')
plt.plot(val_nuevos, cubic_results, label='cubic interp')
plt.legend()
plt.show()
_images/Clase19_46_0.png

Actividad

  1. Obtenga la inversa de una matriz a del tipo $\(a = \left[ \begin{array}{ccc} 3 & 2 & 0 \\ 1 & -1 & 0 \\ 0 & 5 & 1 \\ \end{array}\right]\)$

inv_a = linalg.inv(a)
  1. Resuelva el sistema \(a*x = b\), donde $\(b = \left[ \begin{array}{c} 2 \\ 4 \\ -1 \\ \end{array}\right]\)$

import numpy as np
a = np.array([[3, 2, 0], [1, -1, 0], [0, 5, 1]])

b = np.array([2, 4, -1])

from scipy import linalg

x = linalg.solve(a, b)
x
array([ 2., -2.,  9.])
  1. Si compara \(x = a^{-1}*b\) usando 1) con el resultado de 2), son iguales?

inv_a @ b
array([ 2., -2.,  9.])
4. 
4.0
b = 10;

def f(X): 
    return (X[0]-1)**2 + b*(X[1]-X[0]**2)**2
# Initialize figure 
import matplotlib.pyplot as plt
figRos = plt.figure(figsize=(12, 7))
axRos = figRos.gca(projection='3d')

# Evaluate function
X = np.arange(-2, 2, 0.15)
Y = np.arange(-1, 3, 0.15)
X, Y = np.meshgrid(X, Y)
Z = f([X,Y])

# Plot the surface
surf = axRos.plot_surface(X, Y, Z,
                       linewidth=0, antialiased=False)
axRos.set_zlim(0, 200)
figRos.colorbar(surf, shrink=0.5, aspect=10)
plt.show()
_images/Clase19_55_0.png
from scipy.optimize import least_squares
input = np.array([2, 2])
res = least_squares(f, input)
res
 active_mask: array([0., 0.])
        cost: 0.0021748096395843544
         fun: array([0.06595164])
        grad: array([-0.00102082,  0.01379367])
         jac: array([[-0.01547836,  0.20914825]])
     message: 'The maximum number of function evaluations is exceeded.'
        nfev: 200
        njev: 189
  optimality: 0.013793670721659863
      status: 0
     success: False
           x: array([1.25467248, 1.58466043])