SigmaField
Mathematical Foundations for Quant Finance
0 of 256 lessons
      1. 4.4.1 Optimality conditions in several variablesConcept · 30 min
      2. 4.4.2 Gradient descentConcept · 30 min
      3. 4.4.3 Newton's method in several variablesConcept · 30 min
      4. 4.4.4 Practice set: unconstrained optimisationPractice set · 25 min
      5. 4.4.5 Case study: fitting a curve by least squaresCase study · 35 min

Level 4, Multivariable calculus and optimisation 4.4 Unconstrained optimisation Lesson 4 of 5

Practice setLesson 4.4.4 in Level 4, Multivariable calculus and optimisation. About 25 minutes.

Practice set: unconstrained optimisation

Optimality conditions

Optimality conditions in several variables (lesson 4.4.1)

0 of 7 correct

  1. 1.
  2. 2.
  3. 3.
  4. 4.
  5. 5.
  6. 6.
  7. 7.
How to type answers
  • Powers with ^: x^7, 2^(1/3), a^-2.
  • Multiply with * or by writing side by side: 3x^2, 2(x+1).
  • ln(x) is the natural log, log(x, 2) is log base 2, e is Euler's number, sqrt(x) is a square root.
  • sin(x), cos(x) and tan(x) take radians; write pi for π and sin(x)^2 for sin²x.
  • Your first answer to each question is saved: it counts towards your score and brings the question back for review.
  • Exact answers like ln(20)/ln(3) are best. Decimals need about 4 significant figures.

Gradient descent

Gradient descent (lesson 4.4.2)

0 of 7 correct

  1. 1.
  2. 2.
  3. 3.
  4. 4.
  5. 5.
  6. 6.
  7. 7.
How to type answers
  • Powers with ^: x^7, 2^(1/3), a^-2.
  • Multiply with * or by writing side by side: 3x^2, 2(x+1).
  • ln(x) is the natural log, log(x, 2) is log base 2, e is Euler's number, sqrt(x) is a square root.
  • sin(x), cos(x) and tan(x) take radians; write pi for π and sin(x)^2 for sin²x.
  • Your first answer to each question is saved: it counts towards your score and brings the question back for review.
  • Exact answers like ln(20)/ln(3) are best. Decimals need about 4 significant figures.

Newton's method

Newton's method in several variables (lesson 4.4.3)

0 of 7 correct

  1. 1.
  2. 2.
  3. 3.
  4. 4.
  5. 5.
  6. 6.
  7. 7.
How to type answers
  • Powers with ^: x^7, 2^(1/3), a^-2.
  • Multiply with * or by writing side by side: 3x^2, 2(x+1).
  • ln(x) is the natural log, log(x, 2) is log base 2, e is Euler's number, sqrt(x) is a square root.
  • sin(x), cos(x) and tan(x) take radians; write pi for π and sin(x)^2 for sin²x.
  • Your first answer to each question is saved: it counts towards your score and brings the question back for review.
  • Exact answers like ln(20)/ln(3) are best. Decimals need about 4 significant figures.

Coding exercise

Exercise

Gradient descent with backtracking

Write descend(f, grad, x0, tol=1e-6, max_steps=10_000) that minimises f from x0, with the backtracking rule of lesson 4.4.2. It returns (x, steps): a NumPy array and the number of steps taken.

  • Start from a new decimal array, np.array(x0, dtype=float), so the caller's array is left unchanged.
  • Before each step, if ∥∇f(x)∥\lVert\nabla f(\mathbf{x})\rVert is below tol, return. A start at the minimum takes 0 steps.
  • For each step, set η=1\eta = 1 and halve it until f(x−η∇f)≤f(x)−12η∥∇f∥2f(\mathbf{x} - \eta\nabla f) \le f(\mathbf{x}) - \frac{1}{2}\eta\lVert\nabla f\rVert^2. Then step to x−η∇f\mathbf{x} - \eta\nabla f.
  • Raise ValueError if tol is not positive, or if η\eta falls below 10−1210^{-12}: then no step lowers ff, and grad is probably wrong.
  • Raise RuntimeError if the gradient is still too long after max_steps steps.

The tests use the examples of lessons 4.4.1 to 4.4.3: the two quadratics, the curved valley and the quartic from two starts. They add 10x2+y210x^2 + y^2, where a fixed step of 1 diverges. Others pass a gradient with the wrong sign, tol=0 and too few steps.

See the tests
import numpy as np


def _tilted(p):
    return p[0] ** 2 + p[0] * p[1] + p[1] ** 2 - 3 * p[0]


def _tilted_grad(p):
    return np.array([2 * p[0] + p[1] - 3, p[0] + 2 * p[1]])


def _valley(p):
    return (1 - p[0]) ** 2 + 5 * (p[1] - p[0] ** 2) ** 2


def _valley_grad(p):
    x, y = p
    return np.array([-2 * (1 - x) - 20 * x * (y - x**2), 10 * (y - x**2)])


def _quartic(p):
    return p[0] ** 4 - 3 * p[0] ** 2 + p[0] + p[1] ** 2


def _quartic_grad(p):
    return np.array([4 * p[0] ** 3 - 6 * p[0] + 1, 2 * p[1]])


def _expect_error(kind, call, why):
    try:
        call()
    except kind:
        return
    raise AssertionError(f"expected {kind.__name__}: {why}")


def test_quadratic():
    """x² + xy + y² − 3x from (0, 0) ends at its minimum (2, −1)"""
    x, steps = descend(_tilted, _tilted_grad, [0.0, 0.0])
    expect_close(x, np.array([2.0, -1.0]), what="the point returned", abs_tol=1e-5)


def test_returns_a_pair():
    """The result is a pair: a NumPy array and a whole number of steps"""
    result = descend(_tilted, _tilted_grad, [0.0, 0.0])
    assert isinstance(result, tuple) and len(result) == 2, f"expected (x, steps), got {result!r}"
    x, steps = result
    assert isinstance(x, np.ndarray), f"x should be a NumPy array, got {type(x).__name__}"
    assert int(steps) == steps and steps > 0, f"steps should be a positive whole number, got {steps!r}"


def test_start_at_minimum():
    """Started at the minimum, it takes 0 steps"""
    x, steps = descend(_tilted, _tilted_grad, [2.0, -1.0])
    expect_equal(steps, 0, what="steps from the minimum itself")
    expect_close(x, np.array([2.0, -1.0]), what="the point returned")


def test_three_variables():
    """½xᵀKx − bᵀx with lesson 4.4.1's K and b = (3, 0, −3) ends at (1, 1, −1)"""
    K = np.array([[4.0, -1.0, 0.0], [-1.0, 3.0, 2.0], [0.0, 2.0, 5.0]])
    b = np.array([3.0, 0.0, -3.0])
    x, _ = descend(lambda v: 0.5 * v @ K @ v - b @ v, lambda v: K @ v - b, np.zeros(3))
    expect_close(x, np.array([1.0, 1.0, -1.0]), what="the minimiser", abs_tol=1e-5)


def test_step_too_large_is_halved():
    """On 10x² + y² a fixed step of 1 diverges; backtracking still reaches (0, 0)"""
    x, _ = descend(lambda p: 10 * p[0] ** 2 + p[1] ** 2, lambda p: np.array([20 * p[0], 2 * p[1]]), [1.0, 1.0])
    expect_close(x, np.array([0.0, 0.0]), what="the point returned", abs_tol=1e-5)


def test_curved_valley():
    """The curved valley from (−1.5, 2.5) reaches (1, 1) within 10,000 steps"""
    x, steps = descend(_valley, _valley_grad, [-1.5, 2.5])
    expect_close(x, np.array([1.0, 1.0]), what="the point returned", abs_tol=1e-4)
    assert steps < 10_000, f"expected fewer than 10,000 steps, took {steps}"


def test_start_decides_the_minimum():
    """x⁴ − 3x² + x + y² ends at the local minimum from (1, 0.5) and the global one from (−1, 0.5)"""
    right, _ = descend(_quartic, _quartic_grad, [1.0, 0.5])
    left, _ = descend(_quartic, _quartic_grad, [-1.0, 0.5])
    expect_close(right, np.array([1.130901, 0.0]), what="the minimum reached from (1, 0.5)", abs_tol=1e-5)
    expect_close(left, np.array([-1.300840, 0.0]), what="the minimum reached from (−1, 0.5)", abs_tol=1e-5)


def test_too_few_steps():
    """With max_steps=20 the curved valley is not done: RuntimeError"""
    _expect_error(RuntimeError, lambda: descend(_valley, _valley_grad, [-1.5, 2.5], max_steps=20),
                  "the gradient is still large after 20 steps")


def test_wrong_gradient():
    """A gradient with the wrong sign points uphill: ValueError"""
    _expect_error(ValueError, lambda: descend(_tilted, lambda p: -_tilted_grad(p), [0.0, 0.0]),
                  "no step size makes f fall, because -grad points uphill")


def test_bad_tolerance():
    """tol must be positive: ValueError for tol=0"""
    _expect_error(ValueError, lambda: descend(_tilted, _tilted_grad, [0.0, 0.0], tol=0), "tol=0 can never be met")


def test_input_unchanged():
    """The caller's starting array is left unchanged"""
    start = np.array([0.0, 0.0])
    descend(_tilted, _tilted_grad, start)
    expect_equal(start.tolist(), [0.0, 0.0], what="the starting array after the call")

Free further reading

Optional. This lesson covers what you need; these add more examples and practice. All are free to read.