When you train a coordinate GAN under DP-SGD, per-sample gradient clipping and calibrated noise routinely flatten the very spatial structure the model is supposed to reproduce, so the challenge is spending a (ε,δ) budget that provides a real privacy guarantee while leaving the point pattern’s second-order structure intact.
Part of GAN-Based Spatial Data Generation: that page covers the generator and critic architecture in general; this page drills into training that architecture under a formal privacy guarantee, where the clip norm and noise multiplier are not hyperparameters you tune for accuracy alone but the knobs that trade geographic fidelity against a provable bound on what any adversary can learn about a single input coordinate.
DP-SGD makes each training step differentially private by bounding and then perturbing the contribution of any single training example. Two operations do this. First, every per-sample gradient is clipped to a maximum L2 norm C:
gˉi=gi⋅min(1,∥gi∥2C)
Second, Gaussian noise scaled to that clip norm is added to the summed gradient before the optimizer step:
g~=B1(i∑gˉi+N(0,σ2C2I))
where B is the batch (lot) size and σ the noise multiplier. Composed over many steps with subsampling, this yields an (ε,δ) bound: informally, the presence or absence of any one coordinate changes the output distribution by at most a factor eε, except with probability δ.
The problem for spatial data is that both operations attack structure the model needs. Clipping caps the influence of the rare, high-gradient samples — which in a heavy-tailed geographic distribution are exactly the peripheral and boundary locations that define the pattern’s extent and its heavy-tailed periphery. Clip too aggressively and the generator learns only the dense cores. The added noise, meanwhile, is isotropic in gradient space but the loss surface for coordinate data is highly anisotropic: a small parameter perturbation that is negligible for one coordinate axis can shift the generated distribution sharply along a spatial gradient, smearing tight clusters into blur. The net effect is a privacy mechanism whose two levers both push toward the same failure — loss of spatial detail — which is why naive DP-SGD on coordinates so often produces a privacy-compliant but geographically useless generator, and frequently one that also mode-collapses (see diagnosing mode collapse in spatial GANs).
The stabilization strategy is to normalize coordinates so per-sample gradient norms are comparable across the extent, set the clip norm from the observed gradient-norm distribution rather than a guessed constant, and pick the smallest σ that meets the target ε under a tight accountant.
Every step clips per-sample gradients into the norm ball C, adds noise scaled by σC, and applies the result; the accountant converts (σ, q, t) into a cumulative ε that must stop the run before it crosses the budget ceiling.
Before tuning, confirm the optimizer actually computes per-sample gradients (not batch-averaged ones, which give no privacy) and that an accountant is attached. Pin the toolchain.
import torch
from opacus import PrivacyEngine
# A private optimizer must expose per-sample grad support and an accountant.defassert_dp_wired(privacy_engine: PrivacyEngine, optimizer)->None:asserthasattr(optimizer,"max_grad_norm"),"clip norm not set: no per-sample clipping"assert privacy_engine.accountant isnotNone,"no accountant: epsilon is not being tracked"# A zero or absent noise multiplier means epsilon is unbounded (infinite).assertgetattr(optimizer,"noise_multiplier",0.0)>0.0,"sigma=0 -> no privacy guarantee"# If get_epsilon() raises or returns inf, the run has no valid (epsilon, delta) bound.
If noise_multiplier is zero or the accountant is missing, the run trains normally but carries no privacy guarantee — the most dangerous silent failure, because the artifact looks private and is not.
The stabilization sequence is: normalize coordinates so gradient norms are comparable, choose the clip norm from the observed per-sample gradient-norm distribution, and select the smallest noise multiplier that reaches the target ε.
Project to an equal-area CRS and standardize, so no axis dominates the gradient norm and clipping does not preferentially discard one direction of spatial spread. Latitude/longitude in EPSG:4326 distorts distances with latitude; EPSG:6933 keeps area comparable.
python
import numpy as np
from pyproj import Transformer
defnormalize_coords(lon: np.ndarray, lat: np.ndarray)->tuple[np.ndarray,dict]:"""Reproject to equal-area metres, then zero-mean/unit-std per axis."""
tf = Transformer.from_crs("EPSG:4326","EPSG:6933", always_xy=True)
x, y = tf.transform(lon, lat)
xy = np.column_stack([x, y]).astype("float64")
mu, sd = xy.mean(0), xy.std(0)
stats ={"mu": mu,"sd": sd}# persist to invert after generationreturn(xy - mu)/ sd, stats
Guessing C wastes budget: too large and you inject more noise than needed, too small and you discard the peripheral gradients. Measure the per-sample gradient-norm distribution on a non-private warmup batch and set C near its median.
python
import torch
defcalibrate_clip_norm(model, loss_fn, batch, quantile:float=0.5)->float:"""Median per-sample gradient L2 norm — a data-driven clip that keeps the tail."""
norms =[]for sample in batch:# one example at a time -> true per-sample grad
model.zero_grad(set_to_none=True)
loss_fn(model, sample.unsqueeze(0)).backward()
g = torch.cat([p.grad.flatten()for p in model.parameters()if p.grad isnotNone])
norms.append(g.norm().item())returnfloat(torch.tensor(norms).quantile(quantile))
Solve for the smallest noise multiplier that reaches the target ε at a fixed δ over the planned number of steps, using the accountant rather than a hand rule. Bind δ to less than the inverse dataset size so the guarantee is meaningful; this is the same budgeting discipline the privacy-preserving generation frameworks apply across releases.
python
import torch
from opacus import PrivacyEngine
defmake_private(model, optimizer, data_loader,*,
target_epsilon:float, target_delta:float, epochs:int, max_grad_norm:float):
engine = PrivacyEngine(accountant="rdp")# Renyi DP -> tight composition
model, optimizer, data_loader = engine.make_private_with_epsilon(
module=model,
optimizer=optimizer,
data_loader=data_loader,
target_epsilon=target_epsilon,# e.g. 8.0
target_delta=target_delta,# e.g. 1e-5 < 1/N
epochs=epochs,
max_grad_norm=max_grad_norm,# C from Step 2)return engine, model, optimizer, data_loader
# After training, report the SPENT budget, not the target:# eps = engine.get_epsilon(delta=target_delta) # must be <= target_epsilon
Apply DP-SGD to the critic only in the common setup: by the post-processing property of differential privacy, if the critic is trained privately then the generator — which only ever sees the critic’s private gradients, never the raw data — inherits the guarantee without needing its own noise. This halves the fidelity cost. For how DP-SGD compares to output-perturbation and PATE-style mechanisms on spatial data, see differential privacy mechanisms compared.
A DP run must be gated on two things at once: the spent ε is within budget, and the spatial structure survived. Gating on privacy alone ships a useless generator; gating on fidelity alone ships a non-private one.
python
import numpy as np
deftest_dp_run_is_private_and_faithful(engine, generator, ref_points,*,
target_epsilon=8.0, target_delta=1e-5, seed=42):# 1. Privacy: spent epsilon must not exceed the budget at the bound delta.
eps = engine.get_epsilon(delta=target_delta)assert np.isfinite(eps)and eps <= target_epsilon +1e-6,f"budget exceeded: eps={eps:.2f}"# 2. Structure: nearest-neighbour distance distribution must track the reference.
z = np.random.default_rng(seed).standard_normal((4096, generator.latent_dim))
fake = generator.sample(z)defnnd(p):from scipy.spatial import cKDTree
d, _ = cKDTree(p).query(p, k=2)return np.sort(d[:,1])
a, b = nnd(fake), nnd(ref_points)# Kolmogorov-Smirnov style gap on the NND CDFs, resampled to equal length.
grid = np.linspace(0,max(a.max(), b.max()),256)
gap = np.abs(np.searchsorted(a, grid)/len(a)- np.searchsorted(b, grid)/len(b)).max()assert gap <0.15,f"DP noise destroyed clustering structure: NND gap {gap:.3f}"
The nearest-neighbour-distance gap is the tell-tale for over-noising: as σ rises past the usable range, tight clusters diffuse and the generated NND distribution shifts right, tripping this gate well before a human notices the map looks wrong.
Clipping silently deletes the antimeridian tail. Coordinates straddling ±180° longitude produce two far-apart clusters in raw EPSG:4326, so per-sample gradients for the seam-crossing points are large and are exactly what clipping truncates first — the model then learns only one side. Reproject into the equal-area metric CRS (Step 1) before any gradient is computed, so the seam becomes an ordinary interior region and its gradients are unexceptional.
Null Island as a normalization sink. Records defaulted to (0, 0) from a failed geocode form a spike at the origin that dominates the mean and inflates per-axis std, so normalization shrinks the real extent toward zero and clipping then treats genuine peripheral points as outliers. Filter (0, 0) (and other sentinel coordinates) before computing normalization statistics, and assert the filtered fraction is below a threshold.
δ set larger than 1/N voids the guarantee. Because δ is the probability the ε bound simply fails, choosing δ≥1/N for a dataset of N records permits a mechanism that reproduces whole records with non-negligible probability while still “passing.” Always set δ<1/N (commonly 10−5 or smaller), and record both ε and δ with the artifact — reporting ε without its δ is meaningless.
Should I apply DP-SGD to the generator, the critic, or both?
Apply it to the critic only in the standard setup. The generator never touches the raw training data — it learns only through the critic's gradients — so by the post-processing property of differential privacy, a privately trained critic confers the same guarantee on the generator for free. Adding noise to the generator as well spends budget twice and degrades fidelity for no additional protection.
How do I pick the clip norm C without wasting budget?
Measure the per-sample gradient-norm distribution on a short non-private warmup and set C near its median. A clip much larger than typical norms forces you to add proportionally more noise for the same privacy, while a clip much smaller truncates the heavy-tailed peripheral gradients and drops the pattern's extent. The median is a robust starting point; tune within the interquartile range against the fidelity gate.
Why report both epsilon and delta instead of just epsilon?
Delta is the probability that the epsilon bound fails to hold at all, so an epsilon value is only interpretable alongside its delta. A small epsilon paired with a large delta offers weak protection because the guarantee can break with non-negligible probability. Set delta below the inverse of the dataset size and always record the pair with the artifact.
My DP generator mode-collapsed — is that the noise or a separate bug?
DP noise on the critic can trigger or worsen mode collapse, because it can drown out the batch-diversity signal the critic uses to punish repetition. Diagnose it with spatial-entropy and coverage metrics rather than the loss, and tune the noise multiplier alongside any anti-collapse remedy such as minibatch discrimination rather than treating the two problems in isolation.