Why Constraints Appear#
In many optimisation problems, parameters must satisfy hard constraints:
- Budget limits: — total allocation cannot exceed a budget
- Physics: — a robot's control input is bounded
- Probability: — weights must form a valid probability distribution (simplex)
- Box constraints: — each parameter must lie in an interval
- Robustness: — the model's parameter norm must be bounded
The constrained optimisation problem is:
where is the feasible set (assumed convex for this week). GD alone ignores and may step outside it. We need mechanisms that respect the constraint.
Projected Gradient Descent#
The simplest approach: after each GD step, project the result back onto the feasible set:
where is the Euclidean projection onto .
For convex and convex , projected GD with converges at the same rate as unconstrained GD. The projection step adds no asymptotic cost — its per-iteration cost depends on how fast we can project onto .
When projection is cheap: A projection is fast if has a simple closed form. We need to be computable in or time.
Simple Projections#
ball of radius :
If is already inside the ball (), nothing changes. Otherwise, rescale to lie exactly on the sphere of radius . Cost: .
Box constraints :
Each coordinate is independently clamped to its interval. Cost: .
Probability simplex:
The projection onto the simplex can be done in via sorting (details in the browser lab code). This is more expensive than ball/box projections but still practical.
Worked example — ball projection:
Project onto the ball of radius :
, so rescale: . The projected point lies on the boundary of the ball.
Soft Penalties#
An alternative to hard projection is to add a penalty term to the objective:
where measures how much violates the constraints. For example:
- — penalises being outside the ball
- — penalises box violations
As , the penalty solution approaches the hard-constrained solution. Finite allows some violation — a "soft" constraint.
Tradeoff:
- Hard projection: exactly feasible at every step, but the projection itself can be expensive
- Soft penalty: simple GD on the augmented objective (no projection), but constraints are only approximately satisfied
- In practice: use hard projection for simple constraints (ball, box); soft penalty or augmented Lagrangian for complex constraints
Lagrange Multiplier Intuition#
For the problem subject to , the Lagrangian is:
The Lagrange multiplier acts as a "price" for violating the constraint. If is large, the penalty for is severe and the solver stays well inside the feasible region. If is small, violation is cheap.
At the optimum , complementary slackness holds: . Either the constraint is active () or the multiplier is zero (, the constraint costs nothing).
The optimal also has a sensitivity interpretation: if we tighten the constraint by a small amount (change to ), the optimal objective value changes by approximately . The multiplier tells you how much you'd gain by relaxing the constraint.
This intuition is the gateway to duality (Week 12), where we solve the dual problem instead.
Project $v = (5, 0)$ onto the $\\ell_2$ ball of radius 3. What is the x-coordinate of the projection?
Projected GD replaces $x_{k+1} = x_k - \eta\nabla f(x_k)$ with $x_{k+1} =$ ___ (use P_C notation).
Browser lab#
Run projected GD to minimise subject to being on the probability simplex. Plot the trajectory and observe the projection at each step.
import numpy as np
import matplotlib.pyplot as plt
def project_simplex(v):
u = np.sort(v)[::-1]
css = np.cumsum(u)
rho = np.searchsorted((css - 1.0) / (np.arange(len(v)) + 1.0) < u, True) - 1
r = rho + 1 if rho >= 0 else 0
theta = (css[r-1] - 1.0) / r if r > 0 else 0
return np.maximum(v - theta, 0)
x0 = np.array([2.0, -1.0, 0.5])
def f(x): return 0.5 * np.sum((x - x0)**2)
def grad(x): return x - x0
w = np.array([0.4, 0.3, 0.3])
eta = 0.5
K = 30
path = [w.copy()]; losses = [f(w)]
for _ in range(K):
w = project_simplex(w - eta * grad(w))
path.append(w.copy()); losses.append(f(w))
path = np.array(path)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.5))
ax1.plot(path[:, 0], path[:, 1], "o-", markersize=3, label="Projected GD")
ax1.set_xlabel("x1"); ax1.set_ylabel("x2")
w_opt = project_simplex(x0)
ax1.plot(w_opt[0], w_opt[1], "kx", markersize=10, label="optimum on simplex")
ax1.plot(x0[0], x0[1], "rx", markersize=8, label="unconstrained target x0")
ax1.set_title("Iterates on the Simplex (x1–x2 plane)")
ax1.legend()
ax2.plot(losses)
ax2.set_xlabel("Iteration"); ax2.set_ylabel("f(x)")
ax2.set_title("Loss vs Iteration")
ax2.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()
print(f"Final point on simplex: {np.round(path[-1], 4)}, sum={path[-1].sum():.6f}")