Skip to content

Chapter 7 Exercises and Solutions

These are the exercises from the end of Chapter 7 of Mathematics for Machine Learning (page 247 of the December 2019 draft), restated in this module’s notation. Eleven of them, and they divide neatly: two on gradient descent, two true-or-false batteries on convexity, three on writing down duals, and four on convex conjugates.

Work each one before opening its solution. Every solution is followed by a NumPy block that checks the answer, and the printed output is what that block actually produced.

Consider the univariate function

f(x)=x3+6x23x5f(x) = x^3 + 6x^2 - 3x - 5

Find its stationary points and indicate whether they are maximum, minimum, or saddle points.

Solution

Differentiate twice:

f(x)=3x2+12x3=3(x2+4x1),f(x)=6x+12f'(x) = 3x^2 + 12x - 3 = 3(x^2 + 4x - 1), \qquad f''(x) = 6x + 12

Set f=0f' = 0. The quadratic x2+4x1x^2 + 4x - 1 has roots

x=4±16+42=4±252=2±5x = \frac{-4 \pm \sqrt{16 + 4}}{2} = \frac{-4 \pm 2\sqrt5}{2} = -2 \pm \sqrt5

so the stationary points are x=25=4.2360680x = -2 - \sqrt5 = -4.2360680 and x=2+5=0.2360680x = -2 + \sqrt5 = 0.2360680.

Classify with ff'':

xxf(x)f(x)f(x)f''(x)verdict
25=4.2360680-2-\sqrt5 = -4.2360680+39.3606798+39.360679813.4164079-13.4164079maximum
2+5=+0.2360680-2+\sqrt5 = +0.23606805.3606798-5.3606798+13.4164079+13.4164079minimum

There are no saddle points, and there cannot be. A saddle point needs one direction along which the function increases and another along which it decreases, so it requires at least two dimensions. In one dimension a stationary point is a maximum, a minimum, or degenerate (f=0f'' = 0 as well). The question lists “saddle” as an option, and the correct answer is that the option does not apply here.

Two details worth noticing. The two values of ff'' are 13.4164079\mp 13.4164079 — equal in magnitude, opposite in sign — because ff'' is linear and the two roots are symmetric about x=2x = -2, which is exactly where f=0f'' = 0. And x=2x = -2 is an inflection point, not a stationary point: f(2)=15f'(-2) = -15, comfortably nonzero. Confusing “second derivative vanishes” with “first derivative vanishes” is the trap.

verify_7_1.py
import numpy as np
 
f = np.polynomial.Polynomial([-5.0, -3.0, 6.0, 1.0])   # increasing degree
d1, d2 = f.deriv(1), f.deriv(2)
print("f'(x)  =", d1, "  = 3(x^2 + 4x - 1)")
print("f''(x) =", d2)
 
print(f"\nexact roots: -2 +- sqrt(5) = {-2 - np.sqrt(5):.7f}, "
      f"{-2 + np.sqrt(5):.7f}")
for r in np.sort(d1.roots().real):
    kind = "minimum" if d2(r) > 0 else ("maximum" if d2(r) < 0 else "degenerate")
    print(f"  x = {r:+.7f}   f = {f(r):+.7f}   f'' = {d2(r):+.7f}   {kind}")
 
print(f"\ninflection at f''=0, x = -2.0: f' there = {d1(-2.0):+.4f} "
      f"(nonzero, so NOT a stationary point)")
text
f'(x)  = -3.0 + 12.0 x + 3.0 x**2   = 3(x^2 + 4x - 1)
f''(x) = 12.0 + 6.0 x
 
exact roots: -2 +- sqrt(5) = -4.2360680, 0.2360680
  x = -4.2360680   f = +39.3606798   f'' = -13.4164079   maximum
  x = +0.2360680   f = -5.3606798   f'' = +13.4164079   minimum
 
inflection at f''=0, x = -2.0: f' there = -15.0000 (nonzero, so NOT a stationary point)

Where people get stuck: answering “saddle” for one of them because a cubic “looks like” it has one. The S-shape is real, but the flat-looking middle is the inflection at x=2x = -2, where the gradient is 15-15 and the function is descending steeply. Nothing is stationary there.

Consider the update equation for stochastic gradient descent (Equation 7.15). Write down the update when we use a mini-batch size of one.

Solution

Equation 7.15 is the full-batch update:

θi+1=θiγi(L(θi))=θiγin=1N(Ln(θi))\boldsymbol\theta_{i+1} = \boldsymbol\theta_i - \gamma_i\big(\nabla L(\boldsymbol\theta_i)\big)^\top = \boldsymbol\theta_i - \gamma_i\sum_{n=1}^{N}\big(\nabla L_n(\boldsymbol\theta_i)\big)^\top

With a mini-batch of size one, pick a single index nn uniformly at random from {1,,N}\{1, \dots, N\} and replace the sum by that one term:

 θi+1=θiγi(Ln(θi)),nUniform{1,,N} \boxed{\ \boldsymbol\theta_{i+1} = \boldsymbol\theta_i - \gamma_i\big(\nabla L_n(\boldsymbol\theta_i)\big)^\top,\qquad n \sim \text{Uniform}\{1,\dots,N\}\ }

Why this is legitimate is the whole content of §7.1.3. Convergence requires only that the gradient estimate be unbiased, and

En[NLn(θ)]=N1Nn=1NLn(θ)=L(θ)\mathbb{E}_n\big[N\nabla L_n(\boldsymbol\theta)\big] = N \cdot \frac{1}{N}\sum_{n=1}^{N}\nabla L_n(\boldsymbol\theta) = \nabla L(\boldsymbol\theta)

Note the NN. If you want an unbiased estimate of the sum L=nLn\nabla L = \sum_n \nabla L_n, a single term must be multiplied by NN; the boxed form above therefore has an effective step size of γi/N\gamma_i/N compared with the batch update. Either convention is fine as long as you know which one you are using — that is what page 702 measured as the N/BN/\lvert B\rvert factor, and why changing the batch size can silently change the learning rate.

verify_7_2.py
import numpy as np
 
rng = np.random.default_rng(0)
N, D = 500, 4
X = rng.standard_normal((N, D))
y = X @ rng.standard_normal(D) + 0.4 * rng.standard_normal(N)
theta = rng.standard_normal(D)
 
# L(theta) = sum_n (x_n . theta - y_n)^2, so grad L_n = 2 x_n (x_n.theta - y_n)
grad_full = 2 * X.T @ (X @ theta - y)
grad_one = lambda n: 2 * X[n] * (X[n] @ theta - y[n])
 
trials = 200_000
acc_raw = np.zeros(D)
acc_scaled = np.zeros(D)
for _ in range(trials):
    n = rng.integers(N)
    acc_raw += grad_one(n)
    acc_scaled += N * grad_one(n)
 
print("||full gradient||         =", f"{np.linalg.norm(grad_full):.4f}")
print("mean of one term, x1      : relative error",
      f"{np.linalg.norm(acc_raw / trials - grad_full) / np.linalg.norm(grad_full):.4f}")
print("mean of one term, xN      : relative error",
      f"{np.linalg.norm(acc_scaled / trials - grad_full) / np.linalg.norm(grad_full):.2e}")
print("\nso a single-example gradient is unbiased for the MEAN gradient,")
print("and needs the factor N to be unbiased for the SUM.")
text
||full gradient||         = 2276.9037
mean of one term, x1      : relative error 0.9980
mean of one term, xN      : relative error 3.15e-03
 
so a single-example gradient is unbiased for the MEAN gradient,
and needs the factor N to be unbiased for the SUM.

Read the two error figures against each other. The unscaled average is 99.8%99.8\% wrong — it is converging, but to L/N\nabla L / N rather than to L\nabla L, so it is off by the factor N=500N = 500. The scaled version is within 3×1033\times10^{-3}, and that residual is Monte Carlo noise from 200,000200{,}000 samples rather than bias.

Where people get stuck: writing the update with the sum still present but over a one-element set, which is correct but hides the point; or dropping the NN and then being surprised that the effective learning rate depends on NN.

Consider whether the following statements are true or false:

(a) The intersection of any two convex sets is convex. (b) The union of any two convex sets is convex. (c) The difference of a convex set AA from another convex set BB is convex.

Solution

(a) TRUE. Take x,yAB\mathbf{x}, \mathbf{y} \in A \cap B and θ[0,1]\theta \in [0,1]. Since both lie in AA and AA is convex, θx+(1θ)yA\theta\mathbf{x} + (1-\theta)\mathbf{y} \in A. The same argument gives membership in BB. So the point lies in ABA \cap B.

Note this argument uses nothing about there being only two sets — the intersection of any family of convex sets is convex. That is why a feasible region defined by many convex constraints (Equation 7.38) stays convex however many constraints you add, and it is the single most useful closure fact in the chapter.

(b) FALSE in general. Counterexample: A=[1,1]A = [-1, 1] and B=[3,4]B = [3, 4]. Then x=2x = 2 lies on the segment from 1A1 \in A to 3B3 \in B but is in neither set.

Be careful choosing the counterexample. [1,1][0.5,2][-1,1] \cup [0.5, 2] is convex, because the intervals overlap and the union is the single interval [1,2][-1, 2]. The claim is false in general, not always — so the witness must be disjoint.

(c) FALSE in general. Counterexample: A=[2,2]A = [-2, 2] and B=[1,1]B = [-1, 1] gives AB=[2,1)(1,2]A \setminus B = [-2, -1) \cup (1, 2], two pieces. Again, an overlapping-but-not-nested pair can come out convex: [1,1][0.5,2]=[1,0.5)[-1, 1] \setminus [0.5, 2] = [-1, 0.5), a single interval.

A cleaner way to see (c): removing a set is intersecting with its complement, and the complement of a convex set is essentially never convex, so nothing in (a) applies.

verify_7_3.py
import numpy as np
 
# A subset of the real line is convex exactly when it is one contiguous run.
ts = np.linspace(-6, 8, 1_400_001)
 
def contiguous(mask):
    idx = np.flatnonzero(mask)
    return bool(idx.size == 0 or np.all(np.diff(idx) == 1))
 
iv = lambda lo, hi: (ts >= lo) & (ts <= hi)
 
cases = [
    ("(a) intersection [-1,1] & [0.5,2]", iv(-1, 1) & iv(0.5, 2), True),
    ("(a) intersection [-1,1] & [3,4] (empty)", iv(-1, 1) & iv(3, 4), True),
    ("(b) union [-1,1] | [0.5,2] (overlapping)", iv(-1, 1) | iv(0.5, 2), True),
    ("(b) union [-1,1] | [3,4] (disjoint)", iv(-1, 1) | iv(3, 4), False),
    ("(c) difference [-1,1] \\ [0.5,2]", iv(-1, 1) & ~iv(0.5, 2), True),
    ("(c) difference [-2,2] \\ [-1,1]", iv(-2, 2) & ~iv(-1, 1), False),
]
for name, mask, expect in cases:
    print(f"  {name:44} convex: {str(contiguous(mask)):5} "
          f"(expected {expect})")
print("\n(a) TRUE always. (b) FALSE in general. (c) FALSE in general.")
print("note: the empty set is convex, so an empty intersection is fine.")
text
  (a) intersection [-1,1] & [0.5,2]            convex: True  (expected True)
  (a) intersection [-1,1] & [3,4] (empty)      convex: True  (expected True)
  (b) union [-1,1] | [0.5,2] (overlapping)     convex: True  (expected True)
  (b) union [-1,1] | [3,4] (disjoint)          convex: False (expected False)
  (c) difference [-1,1] \ [0.5,2]              convex: True  (expected True)
  (c) difference [-2,2] \ [-1,1]               convex: False (expected False)
 
(a) TRUE always. (b) FALSE in general. (c) FALSE in general.
note: the empty set is convex, so an empty intersection is fine.

Where people get stuck: offering an overlapping pair as a counterexample for (b) or (c). Getting the verdict right and the witness wrong is worth no credit, and it is easy to do — as the table shows, four of the six cases tested come out convex.

Consider whether the following statements are true or false:

(a) The sum of any two convex functions is convex. (b) The difference of any two convex functions is convex. (c) The product of any two convex functions is convex. (d) The maximum of any two convex functions is convex.

Solution

(a) TRUE. This is Example 7.4. Add the two instances of Definition 7.3 and regroup:

(f1+f2)(θx+(1θ)y)θ(f1(x)+f2(x))+(1θ)(f1(y)+f2(y))(f_1+f_2)(\theta\mathbf{x} + (1-\theta)\mathbf{y}) \leq \theta\big(f_1(\mathbf{x})+f_2(\mathbf{x})\big) + (1-\theta)\big(f_1(\mathbf{y})+f_2(\mathbf{y})\big)

Combined with αf\alpha f convex for α0\alpha \geq 0, this gives closure under all non-negative weighted sums.

(b) FALSE. With f1=x2f_1 = x^2 and f2=exf_2 = e^{-x}, both convex, the difference has minimum second derivative 5.3872-5.3872 on [2,3][-2,3] and a measured worst chord violation of 0.575350.57535. The word “non-negative” in (a) is doing real work: subtracting is adding with α=1\alpha = -1.

(c) FALSE. The same pair: x2exx^2 e^{-x} has minimum second derivative 0.4120-0.4120 and a chord violation of 0.280930.28093. Products are simply not a closure operation. (Special cases work — the product of two non-negative, non-decreasing convex functions is convex — but the general claim fails.)

(d) TRUE. For any θ[0,1]\theta \in [0,1], apply Definition 7.3 inside each branch and then use “a max of sums is at most the sum of maxes”:

maxifi(θx+(1θ)y)maxi(θfi(x)+(1θ)fi(y))θmaxifi(x)+(1θ)maxifi(y)\max_i f_i(\theta\mathbf{x} + (1-\theta)\mathbf{y}) \leq \max_i\big(\theta f_i(\mathbf{x}) + (1-\theta)f_i(\mathbf{y})\big) \leq \theta\max_i f_i(\mathbf{x}) + (1-\theta)\max_i f_i(\mathbf{y})

Note that the maximum can destroy differentiabilitymax(x2,ex)\max(x^2, e^{-x}) has a kink where the two cross — without destroying convexity. That is why Definition 7.3, and not the Hessian test, is the real definition. The hinge loss of Exercise 7.11 is exactly this shape.

verify_7_4.py
import numpy as np
 
def min_d2(fn, lo, hi, n=20001):
    xs = np.linspace(lo, hi, n)
    h = (hi - lo) / (n - 1)
    return float(((fn(xs[1:-1] + h) - 2 * fn(xs[1:-1])
                   + fn(xs[1:-1] - h)) / h ** 2).min())
 
rng = np.random.default_rng(7)
def worst_chord(fn, lo, hi, n=300_000):
    a = rng.uniform(lo, hi, n); b = rng.uniform(lo, hi, n)
    t = rng.uniform(0, 1, n)
    return float((fn(t * a + (1 - t) * b)
                  - (t * fn(a) + (1 - t) * fn(b))).max())
 
f1 = lambda x: x ** 2
f2 = lambda x: np.exp(-x)
for tag, name, fn, expect in (
        ("(a)", "f1 + f2", lambda x: f1(x) + f2(x), True),
        ("(b)", "f1 - f2", lambda x: f1(x) - f2(x), False),
        ("(c)", "f1 * f2", lambda x: f1(x) * f2(x), False),
        ("(d)", "max(f1,f2)", lambda x: np.maximum(f1(x), f2(x)), True)):
    d2v, cv = min_d2(fn, -2, 3), worst_chord(fn, -2, 3)
    print(f"  {tag} {name:11} min f'' = {d2v:>10.4f}  worst chord = "
          f"{cv:>10.5f}  convex: {str(cv <= 1e-9):5} (expected {expect})")
print("\n(a) TRUE. (b) FALSE. (c) FALSE. (d) TRUE.")
text
  (a) f1 + f2     min f'' =     2.0498  worst chord =   -0.00000  convex: True  (expected True)
  (b) f1 - f2     min f'' =    -5.3872  worst chord =    0.57535  convex: False (expected False)
  (c) f1 * f2     min f'' =    -0.4120  worst chord =    0.28093  convex: False (expected False)
  (d) max(f1,f2)  min f'' =     0.4951  worst chord =   -0.00000  convex: True  (expected True)
 
(a) TRUE. (b) FALSE. (c) FALSE. (d) TRUE.

Where people get stuck: guessing that (d) is false because the maximum introduces a kink. Kinks break differentiability, not convexity — and the max is one of the most useful convexity-preserving operations there is, since it is how piecewise-linear convex losses get built.

Express the following optimization problem as a standard linear program in matrix notation:

maxxR2, ξRpx+ξ\max_{\mathbf{x}\in\mathbb{R}^2,\ \xi\in\mathbb{R}} \mathbf{p}^\top\mathbf{x} + \xi

subject to ξ0\xi \geq 0, x00x_0 \leq 0 and x13x_1 \leq 3.

Solution

Standard form (Equation 7.39) is minzcz\min_{\mathbf{z}} \mathbf{c}^\top\mathbf{z} subject to Azb\mathbf{A}\mathbf{z} \leq \mathbf{b}. Three changes are needed: stack the variables, flip the maximisation, and flip the one lower bound.

Stack. z=(x0,x1,ξ)R3\mathbf{z} = (x_0, x_1, \xi)^\top \in \mathbb{R}^3.

Flip the objective. maxpx+ξ=min((px+ξ))\max \mathbf{p}^\top\mathbf{x} + \xi = -\min\big(-(\mathbf{p}^\top\mathbf{x} + \xi)\big), so

c=[p0p11]\mathbf{c} = \begin{bmatrix}-p_0\\-p_1\\-1\end{bmatrix}

Flip the lower bound. ξ0\xi \geq 0 is ξ0-\xi \leq 0. The other two are already the right way round.

A=[100010001],b=[030]\mathbf{A} = \begin{bmatrix}1&0&0\\0&1&0\\0&0&-1\end{bmatrix}, \qquad \mathbf{b} = \begin{bmatrix}0\\3\\0\end{bmatrix}
verify_7_5.py
import numpy as np
 
p = np.array([2.0, -1.0])            # an arbitrary p, to make it concrete
# z = (x0, x1, xi);  maximise p.x + xi  ==  minimise -(p.x + xi)
c = np.array([-p[0], -p[1], -1.0])
A = np.array([[1.0, 0.0, 0.0],       # x0 <= 0
              [0.0, 1.0, 0.0],       # x1 <= 3
              [0.0, 0.0, -1.0]])     # -xi <= 0, i.e. xi >= 0
b = np.array([0.0, 3.0, 0.0])
 
print("z = (x0, x1, xi)")
print("c =", c.tolist(), "   (minimising -(p.x + xi))")
print("A =", A.tolist())
print("b =", b.tolist())
for z in ([0.0, 3.0, 0.0], [0.0, 3.0, 10.0], [0.0, 3.0, 1000.0]):
    z = np.array(z)
    print(f"  z = {str(z.tolist()):24} feasible: "
          f"{bool(np.all(A @ z <= b + 1e-12))}   objective c.z = {c @ z:10.1f}")
text
z = (x0, x1, xi)
c = [-2.0, 1.0, -1.0]    (minimising -(p.x + xi))
A = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0]]
b = [0.0, 3.0, 0.0]
  z = [0.0, 3.0, 0.0]          feasible: True   objective c.z =        3.0
  z = [0.0, 3.0, 10.0]         feasible: True   objective c.z =       -7.0
  z = [0.0, 3.0, 1000.0]       feasible: True   objective c.z =     -997.0

Where people get stuck: writing ξ0\xi \geq 0 as a row of A\mathbf{A} without negating it, which silently imposes ξ0\xi \leq 0 instead — the opposite constraint. Every \geq must become a \leq by negating both the row and the right-hand side.

Consider the linear program illustrated in Figure 7.9,

minxR2[53][x1x2]subject to[2224210101][x1x2][338518]\min_{\mathbf{x}\in\mathbb{R}^2} -\begin{bmatrix}5\\3\end{bmatrix}^\top\begin{bmatrix}x_1\\x_2\end{bmatrix} \quad\text{subject to}\quad \begin{bmatrix}2&2\\2&-4\\-2&1\\0&-1\\0&1\end{bmatrix}\begin{bmatrix}x_1\\x_2\end{bmatrix} \leq \begin{bmatrix}33\\8\\5\\-1\\8\end{bmatrix}

Derive the dual linear program using Lagrange duality.

Solution

Three steps, exactly as in §7.3.1.

Step 1. The Lagrangian, with λR5\boldsymbol\lambda \in \mathbb{R}^5, λ0\boldsymbol\lambda \geq 0:

L(x,λ)=cx+λ(Axb)L(\mathbf{x}, \boldsymbol\lambda) = \mathbf{c}^\top\mathbf{x} + \boldsymbol\lambda^\top(\mathbf{A}\mathbf{x} - \mathbf{b})

Step 2. Collect the x\mathbf{x} terms:

L(x,λ)=(c+Aλ)xλbL(\mathbf{x}, \boldsymbol\lambda) = (\mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda)^\top\mathbf{x} - \boldsymbol\lambda^\top\mathbf{b}

Step 3. L/x=c+Aλ\partial L/\partial\mathbf{x} = \mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda. There is no x\mathbf{x} in it, so it cannot be solved for x\mathbf{x} — instead it is a condition: unless c+Aλ=0\mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda = \mathbf{0}, the Lagrangian is a nonconstant linear function of x\mathbf{x} whose infimum is -\infty. Hence

maxλR5bλsubject toc+Aλ=0,λ0\max_{\boldsymbol\lambda\in\mathbb{R}^5} -\mathbf{b}^\top\boldsymbol\lambda \qquad\text{subject to}\qquad \mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda = \mathbf{0},\quad \boldsymbol\lambda \geq 0

The answer, concretely. The primal optimum is at x=(37/3,25/6)\mathbf{x}^\star = (37/3, 25/6) with value 445/6=74.166667-445/6 = -74.166667, where constraints 00 and 11 are active. By complementary slackness only those two multipliers can be nonzero, so solve Aactλact=c\mathbf{A}_{\text{act}}^\top\boldsymbol\lambda_{\text{act}} = -\mathbf{c}:

[2224][λ0λ1]=[53]6λ1=2,  λ1=13,  λ0=52/32=136\begin{bmatrix}2&2\\2&-4\end{bmatrix}\begin{bmatrix}\lambda_0\\\lambda_1\end{bmatrix} = \begin{bmatrix}5\\3\end{bmatrix} \quad\Longrightarrow\quad 6\lambda_1 = 2,\ \ \lambda_1 = \tfrac13,\ \ \lambda_0 = \tfrac{5 - 2/3}{2} = \tfrac{13}{6}

Both non-negative, as the dual requires. And

bλ=(33136+813)=(1432+83)=4456-\mathbf{b}^\top\boldsymbol\lambda^\star = -\Big(33\cdot\tfrac{13}{6} + 8\cdot\tfrac13\Big) = -\Big(\tfrac{143}{2} + \tfrac83\Big) = -\tfrac{445}{6}

the same value. Gap zero, as strong duality requires for a convex problem.

verify_7_6.py
import itertools
 
import numpy as np
 
c = np.array([-5.0, -3.0])
A = np.array([[2.0, 2.0], [2.0, -4.0], [-2.0, 1.0], [0.0, -1.0], [0.0, 1.0]])
b = np.array([33.0, 8.0, 5.0, -1.0, 8.0])
 
verts = []
for i, j in itertools.combinations(range(5), 2):
    M = A[[i, j]]
    if abs(np.linalg.det(M)) < 1e-12:
        continue
    v = np.linalg.solve(M, b[[i, j]])
    if np.all(A @ v <= b + 1e-9) and not any(
            np.allclose(v, w, atol=1e-9) for w in verts):
        verts.append(v)
verts = np.array(verts)
xs = verts[int(np.argmin(verts @ c))]
 
act = [i for i, q in enumerate(A @ xs - b) if abs(q) < 1e-9]
lam = np.zeros(5)
lam[act] = np.linalg.solve(A[act].T, -c)
 
print("dual: max -b^T lam  s.t.  c + A^T lam = 0, lam >= 0")
print(f"  x*      = ({xs[0]:.6f}, {xs[1]:.6f}) = (37/3, 25/6)")
print(f"  lambda* = {np.round(lam, 6).tolist()} = (13/6, 1/3, 0, 0, 0)")
print(f"  primal {xs @ c:.6f}   dual {-(b @ lam):.6f}   "
      f"gap {abs(xs @ c + b @ lam):.2e}")
text
dual: max -b^T lam  s.t.  c + A^T lam = 0, lam >= 0
  x*      = (12.333333, 4.166667) = (37/3, 25/6)
  lambda* = [2.166667, 0.333333, 0.0, 0.0, 0.0] = (13/6, 1/3, 0, 0, 0)
  primal -74.166667   dual -74.166667   gap 1.42e-14

Where people get stuck: trying to solve c+Aλ=0\mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda = \mathbf{0} for x\mathbf{x}. There is no x\mathbf{x} in it. That absence is the defining feature of the LP case, and it is what makes the dual another LP rather than a quadratic.

Consider the quadratic program illustrated in Figure 7.4,

minxR212[x1x2][2114][x1x2]+[53][x1x2]\min_{\mathbf{x}\in\mathbb{R}^2} \frac12\begin{bmatrix}x_1\\x_2\end{bmatrix}^\top\begin{bmatrix}2&1\\1&4\end{bmatrix}\begin{bmatrix}x_1\\x_2\end{bmatrix} + \begin{bmatrix}5\\3\end{bmatrix}^\top\begin{bmatrix}x_1\\x_2\end{bmatrix}

subject to the box 1xi1-1 \leq x_i \leq 1. Derive the dual quadratic program using Lagrange duality.

Solution

Same three steps, but step 3 goes differently.

Steps 1 and 2:

L(x,λ)=12xQx+(c+Aλ)xλbL(\mathbf{x}, \boldsymbol\lambda) = \frac12\mathbf{x}^\top\mathbf{Q}\mathbf{x} + (\mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda)^\top\mathbf{x} - \boldsymbol\lambda^\top\mathbf{b}

Step 3. L/x=Qx+(c+Aλ)=0\partial L/\partial\mathbf{x} = \mathbf{Q}\mathbf{x} + (\mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda) = \mathbf{0}. This one does contain x\mathbf{x}, and Q\mathbf{Q} is positive definite — its eigenvalues are 1.5857861.585786 and 4.4142144.414214 — so it is invertible and

x=Q1(c+Aλ)\mathbf{x} = -\mathbf{Q}^{-1}(\mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda)

Substituting back, and writing v=c+Aλ\mathbf{v} = \mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda, the quadratic and linear terms combine as 12vQ1vvQ1v=12vQ1v\tfrac12\mathbf{v}^\top\mathbf{Q}^{-1}\mathbf{v} - \mathbf{v}^\top\mathbf{Q}^{-1}\mathbf{v} = -\tfrac12\mathbf{v}^\top\mathbf{Q}^{-1}\mathbf{v}:

maxλR412(c+Aλ)Q1(c+Aλ)λbsubject toλ0\max_{\boldsymbol\lambda\in\mathbb{R}^4} -\frac12(\mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda)^\top\mathbf{Q}^{-1}(\mathbf{c} + \mathbf{A}^\top\boldsymbol\lambda) - \boldsymbol\lambda^\top\mathbf{b} \qquad\text{subject to}\qquad \boldsymbol\lambda \geq 0

A concave quadratic with no equality constraints — the structural difference from Exercise 7.6.

The answer, concretely. x=(1,12)\mathbf{x}^\star = (-1, -\tfrac12) with f=4.5f = -4.5, only the constraint x11-x_1 \leq 1 active, and λ=(0,2.5,0,0)\boldsymbol\lambda^\star = (0, 2.5, 0, 0). Duality gap exactly zero.

verify_7_7.py
import numpy as np
 
Q = np.array([[2.0, 1.0], [1.0, 4.0]])
c = np.array([5.0, 3.0])
A = np.array([[1.0, 0.0], [-1.0, 0.0], [0.0, 1.0], [0.0, -1.0]])
b = np.ones(4)
Qi = np.linalg.inv(Q)
 
dual = lambda l: -0.5 * (c + A.T @ l) @ Qi @ (c + A.T @ l) - l @ b
xq = np.array([-1.0, -0.5])
 
print(f"Q eigenvalues {np.linalg.eigvalsh(Q).round(6).tolist()} -> invertible")
ts = np.linspace(0, 6, 60001)
dv = np.array([dual(np.array([0.0, t, 0.0, 0.0])) for t in ts])
lq = np.array([0.0, ts[int(np.argmax(dv))], 0.0, 0.0])
print("dual: max -1/2 (c + A^T lam)^T Q^-1 (c + A^T lam) - lam^T b, lam >= 0")
print(f"  lambda* = {np.round(lq, 6).tolist()}")
print(f"  primal {0.5 * xq @ Q @ xq + c @ xq:.9f}   dual {dual(lq):.9f}   "
      f"gap {abs(0.5 * xq @ Q @ xq + c @ xq - dual(lq)):.2e}")
print(f"  x from Eq 7.50 = {np.round(-Qi @ (c + A.T @ lq), 6).tolist()}")
text
Q eigenvalues [1.585786, 4.414214] -> invertible
dual: max -1/2 (c + A^T lam)^T Q^-1 (c + A^T lam) - lam^T b, lam >= 0
  lambda* = [0.0, 2.5, 0.0, 0.0]
  primal -4.500000000   dual -4.500000000   gap 0.00e+00
  x from Eq 7.50 = [-1.0, -0.5]

Where people get stuck: using Q1\mathbf{Q}^{-1} without checking that it exists. Convexity only needs Q\mathbf{Q} positive semidefinite; this substitution needs it strictly positive definite. State the assumption, as the book does at Equation 7.50.

Consider the convex optimization problem

minwRD12wwsubject towx1\min_{\mathbf{w}\in\mathbb{R}^D} \frac12\mathbf{w}^\top\mathbf{w} \qquad\text{subject to}\qquad \mathbf{w}^\top\mathbf{x} \geq 1

Derive the Lagrangian dual by introducing the Lagrange multiplier λ\lambda.

Solution

Standard form first. The constraint wx1\mathbf{w}^\top\mathbf{x} \geq 1 becomes g(w)=1wx0g(\mathbf{w}) = 1 - \mathbf{w}^\top\mathbf{x} \leq 0. Then

L(w,λ)=12ww+λ(1wx),λ0L(\mathbf{w}, \lambda) = \frac12\mathbf{w}^\top\mathbf{w} + \lambda\big(1 - \mathbf{w}^\top\mathbf{x}\big), \qquad \lambda \geq 0

Minimise over w\mathbf{w}. L/w=wλx=0\partial L/\partial\mathbf{w} = \mathbf{w} - \lambda\mathbf{x} = \mathbf{0}, so

w=λx\mathbf{w} = \lambda\mathbf{x}

The optimal w\mathbf{w} is a multiple of the data point — a one-point version of the representer property that Chapter 12 leans on. Substituting:

D(λ)=12λ2x2+λ(1λx2)=λ12λ2x2D(\lambda) = \frac12\lambda^2\lVert\mathbf{x}\rVert^2 + \lambda\big(1 - \lambda\lVert\mathbf{x}\rVert^2\big) = \lambda - \frac12\lambda^2\lVert\mathbf{x}\rVert^2

so the dual problem is

 maxλ0 λ12λ2x2 \boxed{\ \max_{\lambda \geq 0}\ \lambda - \frac12\lambda^2\lVert\mathbf{x}\rVert^2\ }

A concave quadratic in one variable. Its unconstrained maximum is at

λ=1x2\lambda^\star = \frac{1}{\lVert\mathbf{x}\rVert^2}

which is positive, so the constraint λ0\lambda \geq 0 is inactive and this is the answer. The optimal value is

d=1x2121x4x2=12x2d^\star = \frac{1}{\lVert\mathbf{x}\rVert^2} - \frac12\cdot\frac{1}{\lVert\mathbf{x}\rVert^4}\lVert\mathbf{x}\rVert^2 = \frac{1}{2\lVert\mathbf{x}\rVert^2}

Check against the primal. w=λx=x/x2\mathbf{w}^\star = \lambda^\star\mathbf{x} = \mathbf{x}/\lVert\mathbf{x}\rVert^2, so 12w2=12x2/x4=1/(2x2)\tfrac12\lVert\mathbf{w}^\star\rVert^2 = \tfrac12\lVert\mathbf{x}\rVert^2/\lVert\mathbf{x}\rVert^4 = 1/(2\lVert\mathbf{x}\rVert^2). Same value. And the constraint is tight: wx=1\mathbf{w}^{\star\top}\mathbf{x} = 1 exactly.

What this problem is. Minimising w\lVert\mathbf{w}\rVert subject to a unit margin is the maximum-margin problem. The margin is 1/w=x1/\lVert\mathbf{w}\rVert = \lVert\mathbf{x}\rVert, so shrinking w\lVert\mathbf{w}\rVert widens the margin — and the answer says the widest margin achievable with a single point at distance x\lVert\mathbf{x}\rVert is x\lVert\mathbf{x}\rVert. That is the whole SVM, with one data point.

verify_7_8.py
import numpy as np
 
rng = np.random.default_rng(11)
print("L = 1/2 w^T w + lam(1 - w^T x); dL/dw = w - lam x = 0")
print("so w = lam x, and D(lam) = lam - 1/2 lam^2 ||x||^2")
print("maximised at lam* = 1/||x||^2, giving d* = 1/(2||x||^2)")
print(f"\n{'||x||':>10} {'lam*':>12} {'d*':>12} {'primal':>12} {'gap':>10}")
for _ in range(4):
    x = rng.standard_normal(5) * rng.uniform(0.5, 3)
    n2 = x @ x
    lam = 1.0 / n2
    w = lam * x
    primal = 0.5 * w @ w
    dual = lam - 0.5 * lam ** 2 * n2
    assert abs(w @ x - 1.0) < 1e-12            # the constraint is tight
    print(f"{np.sqrt(n2):>10.6f} {lam:>12.6f} {dual:>12.6f} "
          f"{primal:>12.6f} {abs(primal - dual):>10.1e}")
print("the constraint is active at the optimum in every case, and")
print("d* = 1/(2||x||^2) exactly.")
text
L = 1/2 w^T w + lam(1 - w^T x); dL/dw = w - lam x = 0
so w = lam x, and D(lam) = lam - 1/2 lam^2 ||x||^2
maximised at lam* = 1/||x||^2, giving d* = 1/(2||x||^2)
 
     ||x||         lam*           d*       primal        gap
  5.424824     0.033980     0.016990     0.016990    6.9e-18
  4.621385     0.046823     0.023411     0.023411    3.5e-18
  2.195402     0.207478     0.103739     0.103739    1.4e-17
  3.287618     0.092520     0.046260     0.046260    0.0e+00
the constraint is active at the optimum in every case, and
d* = 1/(2||x||^2) exactly.

Where people get stuck: forgetting to rewrite wx1\mathbf{w}^\top\mathbf{x} \geq 1 as 1wx01 - \mathbf{w}^\top\mathbf{x} \leq 0 before forming the Lagrangian. Keep the \geq and you get +λ(wx1)+\lambda(\mathbf{w}^\top\mathbf{x} - 1), whose stationary point is w=λx\mathbf{w} = -\lambda\mathbf{x} and whose dual comes out with the wrong sign.

Consider the negative entropy of xRD\mathbf{x} \in \mathbb{R}^D,

f(x)=d=1Dxdlogxdf(\mathbf{x}) = \sum_{d=1}^{D} x_d\log x_d

Derive the convex conjugate f(s)f^*(\mathbf{s}), assuming the standard dot product.

Solution

Use separability first. By Example 7.8, since ff is a sum of functions of individual coordinates, the conjugate is the sum of the coordinate conjugates:

f(s)=d=1D(sd),(t)=tlogtf^*(\mathbf{s}) = \sum_{d=1}^{D} \ell^*(s_d), \qquad \ell(t) = t\log t

So there is a single one-dimensional calculation to do.

One coordinate.

(s)=supt>0 (sttlogt)\ell^*(s) = \sup_{t>0}\ \big(st - t\log t\big)

Take the derivative in tt and set it to zero — the hint’s instruction:

ddt(sttlogt)=slogt1=0logt=s1t=es1\frac{\mathrm{d}}{\mathrm{d}t}\big(st - t\log t\big) = s - \log t - 1 = 0 \quad\Longrightarrow\quad \log t = s - 1 \quad\Longrightarrow\quad t = e^{\,s-1}

Substituting, and this is the pleasing part:

(s)=ses1es1(s1)=es1(ss+1)=es1\ell^*(s) = s\,e^{\,s-1} - e^{\,s-1}(s - 1) = e^{\,s-1}\big(s - s + 1\big) = e^{\,s-1}

Therefore

 f(s)=d=1Desd1 \boxed{\ f^*(\mathbf{s}) = \sum_{d=1}^{D} e^{\,s_d - 1}\ }

Two sanity checks. The second derivative of sttlogtst - t\log t is 1/t<0-1/t < 0 for t>0t > 0, so the stationary point really is a maximum. And the answer is convex in s\mathbf{s} (a sum of exponentials), as every conjugate must be.

Why it is worth knowing. The negative entropy is the objective behind the softmax, and its conjugate being an exponential sum is where logdesd\log\sum_d e^{s_d} comes from in classification. The pairing “entropy on one side, log-sum-exp on the other” is this exercise.

verify_7_9.py
import numpy as np
 
tg = np.linspace(1e-9, 60, 600001)
print(f"{'s':>7} {'grid sup':>14} {'exp(s-1)':>14} {'argmax t':>12} "
      f"{'exp(s-1)':>12}")
for s in (-2.0, 0.0, 1.0, 2.5):
    vals = s * tg - tg * np.log(tg)
    k = int(np.argmax(vals))
    print(f"{s:>7.2f} {vals[k]:>14.8f} {np.exp(s - 1):>14.8f} "
          f"{tg[k]:>12.6f} {np.exp(s - 1):>12.6f}")
 
rng = np.random.default_rng(11)
_ = [rng.standard_normal(5) * rng.uniform(0.5, 3) for _ in range(4)]  # same stream
D = 4
sv = rng.standard_normal(D)
grid = np.linspace(1e-9, 40, 200001)
per = np.array([(sv[d] * grid - grid * np.log(grid)).max() for d in range(D)])
print(f"\nvector check, D = {D}:  sum of per-coordinate sups = {per.sum():.9f}")
print(f"                        sum of exp(s_d - 1)        = "
      f"{np.exp(sv - 1).sum():.9f}")
text
      s       grid sup       exp(s-1)     argmax t     exp(s-1)
  -2.00     0.04978707     0.04978707     0.049800     0.049787
   0.00     0.36787944     0.36787944     0.367900     0.367879
   1.00     1.00000000     1.00000000     1.000000     1.000000
   2.50     4.48168907     4.48168907     4.481700     4.481689
 
vector check, D = 4:  sum of per-coordinate sups = 0.558944757
                        sum of exp(s_d - 1)        = 0.558944845

Where people get stuck: attacking the DD-dimensional supremum directly instead of invoking Example 7.8 first. The vector problem decouples completely, so all the work is one scalar calculus problem — and noticing that is most of the exercise.

Consider the function

f(x)=12xAx+bx+cf(\mathbf{x}) = \frac12\mathbf{x}^\top\mathbf{A}\mathbf{x} + \mathbf{b}^\top\mathbf{x} + c

where A\mathbf{A} is strictly positive definite, hence invertible. Derive the convex conjugate of ff.

Solutionf(s)=supx(sx12xAxbxc)f^*(\mathbf{s}) = \sup_{\mathbf{x}}\Big(\mathbf{s}^\top\mathbf{x} - \frac12\mathbf{x}^\top\mathbf{A}\mathbf{x} - \mathbf{b}^\top\mathbf{x} - c\Big)

Take the gradient in x\mathbf{x} and set it to zero:

sAxb=0x=A1(sb)\mathbf{s} - \mathbf{A}\mathbf{x} - \mathbf{b} = \mathbf{0} \quad\Longrightarrow\quad \mathbf{x}^\star = \mathbf{A}^{-1}(\mathbf{s} - \mathbf{b})

This is a genuine maximum because the Hessian of the objective in x\mathbf{x} is A-\mathbf{A}, which is negative definite. Write d=sb\mathbf{d} = \mathbf{s} - \mathbf{b} and substitute:

f(s)=sA1d12dA1dbA1dcf^*(\mathbf{s}) = \mathbf{s}^\top\mathbf{A}^{-1}\mathbf{d} - \frac12\mathbf{d}^\top\mathbf{A}^{-1}\mathbf{d} - \mathbf{b}^\top\mathbf{A}^{-1}\mathbf{d} - c

The first and third terms combine, since sA1dbA1d=(sb)A1d=dA1d\mathbf{s}^\top\mathbf{A}^{-1}\mathbf{d} - \mathbf{b}^\top\mathbf{A}^{-1}\mathbf{d} = (\mathbf{s}-\mathbf{b})^\top\mathbf{A}^{-1}\mathbf{d} = \mathbf{d}^\top\mathbf{A}^{-1}\mathbf{d}, leaving

 f(s)=12(sb)A1(sb)c \boxed{\ f^*(\mathbf{s}) = \frac12(\mathbf{s} - \mathbf{b})^\top\mathbf{A}^{-1}(\mathbf{s} - \mathbf{b}) - c\ }

Read off the pattern, because it generalises: A\mathbf{A} inverts, b\mathbf{b} becomes a shift of the argument, and cc flips sign. Setting b=0\mathbf{b} = \mathbf{0}, c=0c = 0 and A=λK1\mathbf{A} = \lambda\mathbf{K}^{-1} recovers Example 7.7, and setting A=2\mathbf{A} = 2, b=0\mathbf{b} = 0, c=0c = 0 in one dimension recovers f(s)=s2/4f^*(s) = s^2/4 for f(x)=x2f(x) = x^2.

verify_7_10.py
import numpy as np
 
A = np.array([[3.0, 1.0], [1.0, 2.0]])
b = np.array([1.0, -2.0])
c = 0.7
Ai = np.linalg.inv(A)
print(f"A eigenvalues {np.linalg.eigvalsh(A).round(6).tolist()} -> "
      f"strictly positive definite")
 
f_star = lambda s: 0.5 * (s - b) @ Ai @ (s - b) - c
g = np.linspace(-16, 16, 2401)
X1, X2 = np.meshgrid(g, g)
FX = (0.5 * (3 * X1 ** 2 + 2 * X1 * X2 + 2 * X2 ** 2)
      + b[0] * X1 + b[1] * X2 + c)
for s in ([0.0, 0.0], [2.0, 2.0], [-3.0, 4.0]):
    s = np.array(s)
    print(f"  s = {str(s.tolist()):12} grid sup = "
          f"{(s[0] * X1 + s[1] * X2 - FX).max():10.6f}   closed form = "
          f"{f_star(s):10.6f}")
text
A eigenvalues [1.381966, 3.618034] -> strictly positive definite
  s = [0.0, 0.0]   grid sup =   1.100000   closed form =   1.100000
  s = [2.0, 2.0]   grid sup =   3.500000   closed form =   3.500000
  s = [-3.0, 4.0]  grid sup =  18.100000   closed form =  18.100000

Where people get stuck: dropping the cross terms when substituting. It is worth doing the sA1dbA1d\mathbf{s}^\top\mathbf{A}^{-1}\mathbf{d} - \mathbf{b}^\top\mathbf{A}^{-1}\mathbf{d} combination explicitly rather than by eye — the symmetry of A1\mathbf{A}^{-1} is what makes it collapse so cleanly.

The hinge loss, used by the support vector machine, is

L(α)=max{0, 1α}L(\alpha) = \max\{0,\ 1 - \alpha\}

If we want to use gradient methods such as L-BFGS and do not want to resort to subgradient methods, we need to smooth the kink. Compute the convex conjugate L(β)L^*(\beta). Then add an 2\ell_2 proximal term and compute the conjugate of

L(β)+γ2β2L^*(\beta) + \frac{\gamma}{2}\beta^2
Solution

Part 1: the conjugate.

L(β)=supα (βαmax{0,1α})L^*(\beta) = \sup_{\alpha}\ \big(\beta\alpha - \max\{0, 1-\alpha\}\big)

Split at α=1\alpha = 1, where the max switches branch.

For α1\alpha \geq 1: the loss is 00, so the expression is βα\beta\alpha. Its supremum over [1,)[1,\infty) is ++\infty if β>0\beta > 0, and β\beta (attained at α=1\alpha = 1) if β0\beta \leq 0.

For α<1\alpha < 1: the loss is 1α1-\alpha, so the expression is βα1+α=(β+1)α1\beta\alpha - 1 + \alpha = (\beta+1)\alpha - 1. Its supremum over (,1)(-\infty, 1) is ++\infty if β+1<0\beta + 1 < 0, and β\beta (approached as α1\alpha \to 1^-) if β+1>0\beta + 1 > 0.

Both branches are finite exactly when 1β0-1 \leq \beta \leq 0, and both give β\beta:

L(β)={β1β0+otherwiseL^*(\beta) = \begin{cases}\beta & -1 \leq \beta \leq 0\\ +\infty & \text{otherwise}\end{cases}

A piecewise-linear function with one kink became a linear function on a box. Kinks in the primal become domain boundaries in the dual.

Part 2: add the proximal term and conjugate back. Let h(β)=L(β)+γ2β2h(\beta) = L^*(\beta) + \tfrac\gamma2\beta^2. The infinite values confine the supremum to the box:

h(α)=sup1β0(αββγ2β2)=sup1β0(uβγ2β2),u=α1h^*(\alpha) = \sup_{-1\leq\beta\leq 0}\Big(\alpha\beta - \beta - \frac\gamma2\beta^2\Big) = \sup_{-1\leq\beta\leq 0}\Big(u\beta - \frac\gamma2\beta^2\Big), \qquad u = \alpha - 1

The unconstrained maximiser is β=u/γ\beta = u/\gamma. Three cases, depending on whether it lands in the box:

α\alphamaximiserh(α)h^*(\alpha)
α>1\alpha > 1, so u>0u > 0β=0\beta = 0 (clipped above)00
1γα11-\gamma \leq \alpha \leq 1, so γu0-\gamma \leq u \leq 0β=α1γ\beta = \dfrac{\alpha-1}{\gamma}(1α)22γ\dfrac{(1-\alpha)^2}{2\gamma}
α<1γ\alpha < 1-\gamma, so u<γu < -\gammaβ=1\beta = -1 (clipped below)1αγ21 - \alpha - \dfrac\gamma2
 h(α)={0α1(1α)22γ1γα11αγ2α1γ \boxed{\ h^*(\alpha) = \begin{cases} 0 & \alpha \geq 1\\[4pt] \dfrac{(1-\alpha)^2}{2\gamma} & 1-\gamma \leq \alpha \leq 1\\[6pt] 1 - \alpha - \dfrac\gamma2 & \alpha \leq 1-\gamma\end{cases}\ }

This is the Moreau envelope of the hinge: flat, then a parabola, then linear.

Two properties, both measured at γ=0.6\gamma = 0.6.

It is differentiable everywhere. At α=1γ\alpha = 1-\gamma the left branch has slope 1-1 and the middle branch has slope (1α)/γ=γ/γ=1-(1-\alpha)/\gamma = -\gamma/\gamma = -1. At α=1\alpha = 1 the middle branch has slope 00, matching the flat branch. Measured slope jump at both joins: 8.3×1088.3\times10^{-8}, which is finite-difference error — a real kink gives a jump of order one. This is what the exercise was for.

It is biased low by exactly γ/2\gamma/2. On the linear branch, (1αγ2)(1α)=γ2(1 - \alpha - \tfrac\gamma2) - (1-\alpha) = -\tfrac\gamma2. Measured maximum deviation from the hinge over [3,3][-3,3]: 0.3000000.300000, and γ/2=0.300000\gamma/2 = 0.300000. So γ\gamma trades smoothness against fidelity at a fixed rate, and there is no setting that gives both.

verify_7_11.py
import numpy as np
 
gam = 0.6
hinge = lambda a: np.maximum(0.0, 1.0 - a)
smooth = lambda a: np.where(a >= 1, 0.0,
                            np.where(a >= 1 - gam, (1 - a) ** 2 / (2 * gam),
                                     1 - a - gam / 2))
 
# the numeric conjugate of L*(beta) + (gamma/2) beta^2, over the box [-1, 0]
bb = np.linspace(-1.0, 0.0, 40001)
num = lambda a: (a * bb - (bb + 0.5 * gam * bb ** 2)).max()
 
al = np.linspace(-3, 3, 601)
print(f"gamma = {gam}")
print(f"max |numeric - closed form| over 601 alphas: "
      f"{np.abs(np.array([num(a) for a in al]) - smooth(al)).max():.3e}")
fine = np.linspace(-3, 3, 60001)
print(f"max |smoothed - hinge| = {np.abs(smooth(fine) - hinge(fine)).max():.6f}"
      f"   gamma/2 = {gam / 2:.6f}")
h = 1e-7
for a in (1.0 - gam, 1.0):
    dl = (smooth(a) - smooth(a - h)) / h
    dr = (smooth(a + h) - smooth(a)) / h
    print(f"  at alpha = {a:.2f}: slope jump {float(abs(dr - dl)):.1e}")
text
gamma = 0.6
max |numeric - closed form| over 601 alphas: 2.083e-11
max |smoothed - hinge| = 0.300000   gamma/2 = 0.300000
  at alpha = 0.40: slope jump 8.3e-08
  at alpha = 1.00: slope jump 8.3e-08

Where people get stuck: forgetting that LL^* is ++\infty outside [1,0][-1,0] and taking the supremum over all of R\mathbb{R} in part 2. The infinite values are what restrict the supremum to the box, and the box is what produces the two clipped branches — without them you would get only the parabola, which is not the answer.

These five run the exercises above rather than restating them, and each one is the step where the solution actually turns.

Exercise 1 – Classify 7.1’s stationary points

Section titled “Exercise 1 – Classify 7.1’s stationary points”

Exercise 2 – 7.3’s counterexamples must be disjoint

Section titled “Exercise 2 – 7.3’s counterexamples must be disjoint”

Exercise 3 – 7.5 in matrix form, and its unboundedness

Section titled “Exercise 3 – 7.5 in matrix form, and its unboundedness”

Exercise 4 – 7.8’s dual, and its tight constraint

Section titled “Exercise 4 – 7.8’s dual, and its tight constraint”

Exercise 5 – 7.9’s separable conjugate

Section titled “Exercise 5 – 7.9’s separable conjugate”
  • 7.1: f’ = 3(x^2 + 4x - 1) gives stationary points at -2 plus or minus root 5, i.e. -4.2360680 (maximum, f” = -13.4164079) and 0.2360680 (minimum, f” = +13.4164079). No saddle: a saddle needs two dimensions. The inflection at x = -2 has f’ = -15, so it is not stationary.
  • 7.2: with a mini-batch of one, theta next equals theta minus gamma times the gradient of a single randomly chosen L_n. Unbiasedness is all convergence needs — and the factor N is what makes one term unbiased for the SUM rather than the mean.
  • 7.3: intersection TRUE always and for any family; union and difference FALSE in general. The counterexamples must be DISJOINT — an overlapping union or difference of intervals is convex.
  • 7.4: sum TRUE, difference FALSE, product FALSE, maximum TRUE. Measured with x squared and exp(-x): the difference reaches f” = -5.3872 and the product -0.4120. The maximum destroys differentiability, not convexity.
  • 7.5: with z = (x0, x1, xi), c = (-p0, -p1, -1) and A rows (1,0,0), (0,1,0), (0,0,-1) against b = (0, 3, 0). Every “greater than or equal” constraint needs BOTH the row and the right-hand side negated. As printed the program is unbounded, since xi has no upper bound.
  • 7.6: three steps, and step 3’s condition c + A-transpose lambda = 0 contains no x, so it becomes a constraint and the dual is another LP. lambda-star = (13/6, 1/3, 0, 0, 0), value -445/6 both sides.
  • 7.7: the same three steps, but step 3 contains x, so Eq 7.50 substitutes back and the dual is a concave quadratic with only lambda at least zero. lambda-star = (0, 2.5, 0, 0), gap exactly zero. Requires Q strictly positive definite, not merely semidefinite.
  • 7.8: rewrite the constraint as 1 - w-transpose x at most 0 FIRST. Then w = lambda x, D(lambda) = lambda - half lambda squared times the squared norm of x, lambda-star = 1 over that norm squared, and d-star = 1/(2||x||^2). This is the maximum-margin problem with one data point.
  • 7.9: separable by Example 7.8, so one scalar problem: t = exp(s-1) and the value is exp(s-1) too. Hence f-star(s) = sum of exp(s_d - 1). This pairing of entropy with an exponential sum is where log-sum-exp comes from.
  • 7.10: f-star(s) = half (s - b)-transpose A-inverse (s - b) minus c. The pattern generalises: A inverts, b shifts the argument, c flips sign.
  • 7.11: L-star(beta) = beta on [-1, 0] and infinite elsewhere — a kink in the primal became a domain boundary in the dual. Adding a proximal term and conjugating back gives three branches (0, then (1-alpha)^2/(2 gamma), then 1 - alpha - gamma/2), differentiable at both joins to 8.3e-8, and biased exactly gamma/2 below the hinge.
  • The recurring lesson across 7.5 to 7.8: get the constraint into the form g at most 0 before writing any Lagrangian. Three of the four dual exercises are lost at that step and nowhere else.

Next: everything in one place — Chapter 7 Formula Sheet

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading