Tutorial: certifying a ReLU network¶
We train a small classifier, then ask: is the prediction at a point \(x_0\) unchanged for every input within \(\ell_\infty\)-distance \(\varepsilon\)? The answer is a polynomial optimization problem; its relaxations give certificates.
Train a network¶
import numpy as np
import torch
from moment_sos import nn
def train(d, widths, seed, n_train=2000, steps=1500):
"""Small ReLU classifier on synthetic data labelled by a random smooth teacher; returns numpy (Ws, bs)."""
rng = np.random.default_rng(seed)
torch.manual_seed(seed)
teacher, phase = rng.standard_normal((d, 4)), rng.uniform(0, 6.28, 4)
X = torch.as_tensor(rng.uniform(-1, 1, (n_train, d)))
t = torch.sin(X @ torch.as_tensor(teacher) * 2.0 + torch.as_tensor(phase)).sum(1)
y = (t > t.median()).long()
dims, layers = [d] + list(widths) + [2], []
for i in range(len(dims) - 1):
layers.append(torch.nn.Linear(dims[i], dims[i + 1]).double())
if i < len(dims) - 2:
layers.append(torch.nn.ReLU())
net = torch.nn.Sequential(*layers)
opt = torch.optim.Adam(net.parameters(), lr=3e-3)
for _ in range(steps):
opt.zero_grad()
torch.nn.functional.cross_entropy(net(X), y).backward()
opt.step()
lin = [l for l in layers if isinstance(l, torch.nn.Linear)]
return [l.weight.detach().numpy() for l in lin], [l.bias.detach().numpy() for l in lin]
Ws, bs = train(4, [8, 8], seed=5)
rng = np.random.default_rng(105)
while True: # a test point with a clear margin
x0 = rng.uniform(-0.8, 0.8, 4)
logits = nn.forward(Ws, bs, x0)
if abs(logits[0] - logits[1]) > 0.4:
break
cls = int(logits.argmax())
c = np.zeros(2)
c[1 - cls], c[cls] = 1.0, -1.0 # maximise (wrong logit) - (true logit)
The verification problem¶
For each \(\varepsilon\) we maximize the margin violation \(c^\top\text{logits}\) over the ball. A negative maximum proves
robustness. nn.relu_verification builds the POP (stable neurons are substituted away; each unstable neuron becomes a
variable with the exact ReLU constraints plus the triangle inequality):
import moment_sos as po
rows = []
for eps in (0.2,):
problem, info = nn.relu_verification(Ws, bs, x0, eps, c)
row = dict(eps=eps, unstable=info["n_unstable"], ibp=info["ibp_upper"],
exact=nn.verification_exact_milp(Ws, bs, x0, eps, c)[0],
order1=po.solve_alm(problem, order=1).bound,
order2=po.solve_alm(problem, order=2).bound)
rows.append(row)
print({k: round(v, 3) for k, v in row.items()})
The columns are: the interval-bound-propagation bound (ibp, the cheapest), the exact maximum from a MILP (exact), and
the order-1 (Shor SDP) and order-2 relaxations. At \(\varepsilon=0.2\):
r = rows[0]
assert r["ibp"] > 0 and r["order1"] > 0 > r["order2"] # only the second-order relaxation proves robustness
assert abs(r["order2"] - r["exact"]) < 1e-3 # and it is tight
(Other values of \(\varepsilon\): at \(0.22\) order 1 again fails and order 2 again certifies; at \(0.15\) order 1 already suffices. The order-2 solve takes about a minute; since every iterate is a valid bound, time_limit= stops it early with a looser but still certified value.)
At \(\varepsilon=0.2\) the interval bound (\(\approx86\)) and the order-1 SDP (\(\approx4.2\)) both fail to prove robustness; the order-2 relaxation gives \(-2.80\), equal to the exact maximum: robustness is certified, with a first-order solver and no interior-point SDP.
Practical advice¶
Use
po.solve_almfor these problems: they have many inequality and equality constraints, andsolve_almhas no smoothing bias.The cost is governed by the number of unstable neurons \(n\): the order-2 block has size \(1+n+\binom n2\)-ish. Up to about 14 unstable neurons the order-2 value is computed to \(10^{-5}\). At about 40 it is not solved to convergence in 25 minutes; use correlative sparsity (
cliques="auto") when layers decouple. See Benchmarks and limitations.On a small MNIST classifier (3 vs 8, PCA to 4 inputs) order 1 already certified every robust point, so check order 1 first: order 2 helps only when order 1 leaves a gap.