Contents
Deep Breakdown
What it tells me (Situation Awareness): The Fenchel conjugate (also called the Legendre-Fenchel transform or convex conjugate) of a function is a new function that encodes the "slopes" of : asks "how much can I make large by choosing optimally?" For a convex , this is a supremum over all . The transform is fundamental in convex duality theory — it connects primal and dual optimization problems. In DA/DS: the conjugate appears in the derivation of SVM dual, Lasso duality, variational inference, and entropic regularization (Sinkhorn algorithms). Understanding it lets you interpret dual variables as prices/multipliers and derive efficient dual algorithms.
Flowchart
Real-world use
Where it is used
Deriving dual optimization problems (SVM, Lasso, logistic regression dual), computing the convex hull of non-convex functions, variational representations of KL divergence and other f-divergences, and Sinkhorn optimal transport.
Problem it solves
Primal optimization problems (minimize over ) are sometimes hard to solve directly but have tractable duals. The Fenchel conjugate provides the bridge: the dual of uses and . When both and are convex and their conjugates have closed-form expressions (as they do for many standard functions), the dual can be solved efficiently. The Fenchel-Young inequality provides a variational lower bound used throughout ML theory.
How it solves the problem
flips the optimization problem: instead of minimizing , it maximizes the "gap" between the linear term and . The result is always convex in (supremum of linear functions is convex). The Fenchel-Young inequality follows directly: for all .
Example in a business
A DS derives the SVM dual. The SVM primal: subject to . Via Lagrangian duality (using conjugates), the dual becomes subject to . The dual are support vector weights. The conjugate of is (self-conjugate for the squared norm), which drives the kernel extension.
The formula, piece by piece
- : the conjugate function; a new function of the dual variable
- : the dual variable; in optimization, is a "direction" or "slope" in the input space
- : linear function of with slope ; grows unboundedly if doesn't grow fast enough
- : the original primal function being conjugated
- : the "gap" between the linear term and ; maximized over
- : supremum over all in the domain of ; could be infinity if grows too slowly
- Fenchel-Young inequality: for all ; equality iff
- Double conjugate: ; equals the convex hull of if is closed proper convex,
The formula in words
"For each direction , find the maximum vertical gap between the line and over all . That maximum gap is . It tells you the tightest lower bound on when all you know is the slope ." Tiny example: (1D). . Maximizing over : . . So . Check Fenchel-Young: ? Equals .
Critical Concepts
- is always convex: it's a pointwise supremum of linear (hence convex) functions of . Even if is non-convex, is convex.
- Self-conjugate functions: . The squared norm is self-conjugate. This is why ridge regression and SVM dual have the same form.
- Conjugate of norm: (0 if , otherwise). This connects Lasso ( penalty) to its dual involving an ball constraint.
- Conjugate of entropy: (for , 0 at ) . Appears in entropic regularization and Sinkhorn optimal transport.
- Conjugate of exponential: for . Connects to log-sum-exp and softmax.
- Fenchel duality: (under regularity conditions).
This is the foundation of dual decomposition algorithms.
Step-by-step calculation (tiny example)
Compute for .
Step 1: Set up the supremum .
Step 2: Find the critical point (requires ).
Step 3: Check second-order condition . This is a maximum.
Step 4: Evaluate at for .
Step 5: Boundary cases For : as , so for . For : , so . Result: for , for .
Code examples
Beginner example (Python)
import numpy as np
from scipy.optimize import minimize_scalar
def fenchel_conjugate_numerical(
func,
u_val: float,
x_bounds: tuple = (-10, 10),
) -> float:
"""Numerically compute f*(u) = sup_x [xu - f(x)]."""
result = minimize_scalar(
lambda x: -(x * u_val - func(x)),
bounds=x_bounds,
method="bounded",
)
return -result.fun
# f(x) = x^2: conjugate should be u^2/4
def f_squared(x: float) -> float:
return x**2
def f_exp(x: float) -> float:
return np.exp(x)
u_grid = np.linspace(-3, 3, 13)
print("f(x) = x^2: f*(u) vs analytical u^2/4")
for u in u_grid:
numerical = fenchel_conjugate_numerical(f_squared, u)
analytical = u**2 / 4
print(f" u={u:5.1f}: numerical={numerical:7.4f}, analytical={analytical:7.4f}")
print("\nf(x) = e^x (u > 0): f*(u) vs analytical u*log(u) - u")
for u in [0.5, 1.0, 2.0, 3.0]:
numerical = fenchel_conjugate_numerical(f_exp, u, x_bounds=(-20, 20))
analytical = u * np.log(u) - u
print(f" u={u}: numerical={numerical:.4f}, analytical={analytical:.4f}")Beginner example (R)
# Fenchel conjugate: f*(u) = sup_x [xu - f(x)]
fenchel_conjugate <- function(f, u_val, lower = -10, upper = 10) {
result <- optimize(function(x) -(x * u_val - f(x)), lower = lower, upper = upper)
-result$objective
}
f_squared <- function(x) x^2
f_entropy <- function(x) {
if (x > 0) x * log(x)
else if (x == 0) 0
else Inf
}
u_vals <- seq(-2, 2, by = 0.5)
cat("f(x)=x^2: f*(u) vs u^2/4\n")
for (u in u_vals) {
num <- fenchel_conjugate(f_squared, u)
anal <- u^2 / 4
cat(sprintf(" u=%4.1f: numerical=%.4f, analytical=%.4f\n", u, num, anal))
}
# Fenchel-Young inequality check
cat("\nFenchel-Young: f(x) + f*(u) >= xu\n")
pairs <- list(c(1, 2), c(-1, 0.5), c(3, 1))
for (p in pairs) {
x <- p[1]; u <- p[2]
f_val <- f_squared(x)
fstar <- fenchel_conjugate(f_squared, u)
cat(sprintf(" x=%g, u=%g: f(x)=%g, f*(u)=%.4f, sum=%.4f >= xu=%g? %s\n", x, u, f_val, fstar, f_val+fstar, x*u, f_val+fstar >= x*u - 1e-8))
}Professional example (Python)
import numpy as np
from scipy.optimize import minimize
np.random.seed(42)
# -- SVM dual via Fenchel duality: demonstrate the connection -------------------
# The SVM primal uses f(w) = 0.5*||w||^2, whose conjugate is f*(u) = 0.5*||u||^2
# This self-conjugacy is WHY the SVM dual has the same quadratic form.
# Generate linearly separable 2D data
N_POS = 30
N_NEG = 30
X_pos = np.random.randn(N_POS, 2) + np.array([2, 2])
X_neg = np.random.randn(N_NEG, 2) + np.array([-2, -2])
X = np.vstack([X_pos, X_neg])
y = np.hstack([np.ones(N_POS), -np.ones(N_NEG)])
C_PARAM = 1.0
# SVM dual: max sum(alpha) - 0.5 * alpha @ K @ alpha
# s.t. 0 <= alpha <= C, sum(alpha * y) = 0
# K_ij = y_i y_j x_i^T x_j (linear kernel with label encoding)
K = (y[:, None] * X) @ (y[:, None] * X).T
def svm_dual_negative(alpha: np.ndarray) -> float:
"""Negative SVM dual objective (for minimization)."""
return 0.5 * alpha @ K @ alpha - alpha.sum()
def svm_dual_gradient(alpha: np.ndarray) -> np.ndarray:
return K @ alpha - np.ones(len(alpha))
n_samples = len(y)
alpha_init = np.zeros(n_samples)
bounds = [(0, C_PARAM)] * n_samples
constraints = {"type": "eq", "fun": lambda a: a @ y, "jac": lambda a: y}
result = minimize(
svm_dual_negative,
alpha_init,
jac=svm_dual_gradient,
method="SLSQP",
bounds=bounds,
constraints=constraints,
options={"ftol": 1e-9, "maxiter": 500},
)
alpha_opt = result.x
dual_value = -result.fun
# Recover primal from dual: w = sum(alpha_i * y_i * x_i)
w_dual = (alpha_opt * y) @ X
support_mask = alpha_opt > 1e-4
b_dual = np.mean(y[support_mask] - X[support_mask] @ w_dual)
print("SVM via Fenchel-dual optimization")
print(f"Support vectors: {support_mask.sum()} / {n_samples}")
print(f"Dual objective value: {dual_value:.6f}")
print(f"||w|| (margin = 2/||w||): {np.linalg.norm(w_dual):.4f}")
print(f"Margin: {2/np.linalg.norm(w_dual):.4f}")
# Verify primal objective matches dual (strong duality for convex QP)
xi = np.maximum(0, 1 - y * (X @ w_dual + b_dual))
primal_value = 0.5 * np.linalg.norm(w_dual)**2 + C_PARAM * xi.sum()
print(f"\nPrimal objective: {primal_value:.6f}")
print(f"Dual objective: {dual_value:.6f}")
print(f"Duality gap: {abs(primal_value - dual_value):.2e} (should be ~0)")
print("\nNote: self-conjugacy of 0.5*||w||^2 <-> 0.5*||u||^2 is why")
print("the SVM dual has the same quadratic form as the primal.")Source: domain knowledge