Gradient Descent¶
import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
Example¶
x, y = sp.symbols("x y", real = True)
f = x**4 + y**4 + sp.Rational(1, 2) * (x**2 + y**2) + 2 * x * y
f_num = sp.lambdify((x, y), f)
f_grad = sp.Matrix([sp.diff(f, x), sp.diff(f, y)])
f_grad_num = sp.lambdify((x, y), list(f_grad)) # use list here to get a nicer output as flat array
H = sp.hessian(f, (x, y))
H_num = sp.lambdify((x, y), H)
x_num = np.linspace(-1, 1, 400)
y_num = np.linspace(-2, 1, 400)
X_num, Y_num = np.meshgrid(x_num, y_num)
Z_num = f_num(X_num, Y_num)
plt.contour(X_num, Y_num, Z_num, 30)
<matplotlib.contour.QuadContourSet at 0x783a7d77b5f0>
Exact Solutions¶
stationary_points = sp.solve([sp.diff(f, x), sp.diff(f, y)], [x, y], dict = True)
stationary_points
[{x: -1/2, y: 1/2}, {x: 0, y: 0}, {x: 1/2, y: -1/2}]
eigenvalues_values = [H.subs({x: sol[x], y: sol[y]}).eigenvals() for sol in stationary_points]
eigenvalues_values
[{6: 1, 2: 1}, {3: 1, -1: 1}, {6: 1, 2: 1}]
eigenvalues_values[0].keys() # eigenvalues are the keys of the list
dict_keys([6, 2])
So we get the numerical value by ... and therefore saddle point for (-1/2, 1/2):
[ev.evalf() for ev in eigenvalues_values[0].keys()]
[6.00000000000000, 2.00000000000000]
Saddlepoint for point for (0, 0):
[ev.evalf() for ev in eigenvalues_values[1].keys()]
[3.00000000000000, -1.00000000000000]
Minimum for point for (1/2, -1/2):
[ev.evalf() for ev in eigenvalues_values[2].keys()]
[6.00000000000000, 2.00000000000000]
Newton's Method¶
1 dimensional approach to solve g(x) = 0 (scalar equation)
sol_new = sol_old - g(sol_old) / g'(sol_old)
Approach to solve g(x, y) = [0, 0] (2 equations!)
sol_new, sol_old and g(sol_old) are vectors now
g'(sol_old) gets a matrix of all derivatives H(sol_old) = [[g1x, g1y], [g2x, g2y]](sol_old)
sol_new = sol_old - H^{-1}(sol_old) * g(sol_old)
Approach to solve grad f(x, y) = [0, 0] (2 equations!)
sol_new, sol_old and g = grad f(sol_old) are again vectors
g'(sol_old) gets a matrix of all derivatives H(sol_old) = [[g1x, g1y], [g2x, g2y]] = [[fxx, fxy], [fyx, fyy]](sol_old)
sol_new = sol_old - H^{-1}(sol_old) * g(sol_old)
sol_0 = np.array([0.99, 0.50])
g_0 = np.array(f_grad_num(sol_0[0], sol_0[1]))
H_0 = np.array(H_num(sol_0[0], sol_0[1]))
H_0_inv = np.linalg.inv(H_0)
sol_1 = sol_0 - H_0_inv @ g_0
sol_1
array([ 0.61748733, -0.05874367])
g_1 = np.array(f_grad_num(sol_1[0], sol_1[1]))
H_1 = np.array(H_num(sol_1[0], sol_1[1]))
H_1_inv = np.linalg.inv(H_1)
sol_2 = sol_1 - H_1_inv @ g_1
sol_2
array([ 1.08769541, -2.09044748])
g_2 = np.array(f_grad_num(sol_2[0], sol_2[1]))
H_2 = np.array(H_num(sol_2[0], sol_2[1]))
H_2_inv = np.linalg.inv(H_2)
sol_3 = sol_2 - H_2_inv @ g_2
sol_3
array([ 0.86163572, -1.39980011])
sol_old = np.array([0.99, 0.50])
sol_new = np.array([0.99, 0.50])
solutions = [sol_old]
for i in range(50):
g_old = np.array(f_grad_num(sol_old[0], sol_old[1]))
H_old = np.array(H_num(sol_old[0], sol_old[1]))
# H_old_inv = np.linalg.inv(H_old)
# sol_new = sol_old - H_old_inv @ g_old
sol_new = sol_old - np.linalg.solve(H_old, g_old) # better to avoid inverse and ask numpy to solve a system of linear equations directly
solutions.append(sol_new)
sol_old = sol_new
sol_new
array([ 0.5, -0.5])
path = np.array(solutions)
x_path = path[:, 0]
y_path = path[:, 1]
plt.contour(X_num, Y_num, Z_num, 30)
plt.plot(x_path, y_path, 'ro-', markersize=3)
[<matplotlib.lines.Line2D at 0x783a7d689c70>]
We could improve the method, we see that the second solution is very bad, but then it is converging!
Gradient Descent¶
step_size = 0.1
sol_0 = np.array([0.99, 0.50])
sol_1 = sol_0 - step_size * np.array(f_grad_num(sol_0[0], sol_0[1]))
sol_1
array([0.4028804, 0.202 ])
sol_2 = sol_1 - step_size * np.array(f_grad_num(sol_1[0], sol_1[1]))
sol_2
array([0.29603533, 0.09792696])
sol_3 = sol_2 - step_size * np.array(f_grad_num(sol_2[0], sol_2[1]))
sol_3
array([0.23646896, 0.02855156])
sol_old = np.array([0.99, 0.50])
sol_new = np.array([0.99, 0.50])
solutions = [sol_old]
for i in range(50):
sol_new = sol_old - step_size * np.array(f_grad_num(sol_old[0], sol_old[1]))
solutions.append(sol_new)
sol_old = sol_new
sol_new
array([ 0.49970797, -0.49970797])
path = np.array(solutions)
x_path = path[:, 0]
y_path = path[:, 1]
plt.contour(X_num, Y_num, Z_num, 20)
plt.plot(x_path, y_path, 'ro-', markersize=3)
[<matplotlib.lines.Line2D at 0x783a7d6c6960>]