Files
proofs/lean/AI/FederatedWildlife.lean
zeekayandHanzo Dev 638fba4c90 Publish under the org, not a person
Bylines, paper authorship, correspondence addresses and the founder blurb under
each post named an individual. They name the organisation that published the
work instead: the brand on posts and pages, the research byline on papers, and
research@ in place of a personal address. Both are what these repositories
already used elsewhere — nothing new was coined.

Co-authors are untouched. Where a paper named someone else alongside, they stay
named; only the one byline changed. Bibliography entries, running heads,
PDF metadata and affiliations move with it, so a citation still resolves to
whoever the cited paper names.

Sample and fixture data uses the placeholder name these tests already use
elsewhere, so no assertion changed meaning.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 08:58:14 -07:00

284 lines
12 KiB
Lean4

/-
Authors: Antje Worring, Zoo Labs Foundation Research
Federated Learning for Wildlife Monitoring — Convergence Properties
Formal verification of ZooFed's federated learning framework for
cross-institutional wildlife monitoring. ZooFed introduces heterogeneous
sensor adaptation (HSA), conservation-calibrated differential privacy (CCDP),
and non-IID robust aggregation (NIRA).
Maps to:
- zoo/papers/zoo-federated-wildlife.tex: full ZooFed specification
Key results:
- Local gradients are bounded (Lipschitz continuity)
- Federated averaging is an unbiased estimator of the global gradient
- Convergence rate O(1/sqrt(T)) under data heterogeneity
- (epsilon, delta)-differential privacy is preserved
Author: Zoo Labs Foundation Research
-/
import Mathlib.Data.Nat.Defs
import Mathlib.Data.List.Basic
import Mathlib.Tactic
namespace AI.FederatedWildlife
/-! ## Core Structures -/
/-- A gradient (abstracted as magnitude + direction hash). -/
structure Gradient where
/-- L2 norm of the gradient (scaled integer) -/
norm : Nat
/-- Hash identifying the direction -/
direction : Nat
deriving DecidableEq, Repr
/-- A site's local model update. -/
structure SiteUpdate where
siteId : Nat
gradient : Gradient
/-- Number of local data samples -/
numSamples : Nat
/-- Species sensitivity level (0=common, 1=threatened, 2=endangered, 3=critical) -/
sensitivityLevel : Nat
deriving DecidableEq, Repr
/-- Global aggregation round state. -/
structure FedRound where
roundNum : Nat
updates : List SiteUpdate
/-- Total samples across all sites -/
totalSamples : Nat
/-- Privacy budget consumed so far (scaled by 1000) -/
epsilonConsumed : Nat
/-- Maximum allowed privacy budget (scaled by 1000) -/
epsilonBudget : Nat
/-- Differential privacy parameters. -/
structure PrivacyParams where
/-- Epsilon for common species (scaled by 1000, e.g., 2000 = eps=2.0) -/
epsilonCommon : Nat
/-- Epsilon for endangered species (scaled by 1000, e.g., 500 = eps=0.5) -/
epsilonEndangered : Nat
/-- Delta (probability of privacy breach, scaled by 1e9) -/
delta : Nat
/-- Noise multiplier -/
noiseMultiplier : Nat
/-! ## Theorem 1: Gradient Bounded -/
/-- Lipschitz bound on local gradients. Each site's gradient norm is bounded
by a constant G (the Lipschitz constant of the loss function).
This is a standard assumption in federated optimization theory
(McMahan et al., 2017). -/
def gradientBounded (updates : List SiteUpdate) (bound : Nat) : Prop :=
∀ u ∈ updates, u.gradient.norm ≤ bound
/-- Local gradients are bounded by the Lipschitz constant G.
This follows from the bounded gradient assumption: the loss
function is G-Lipschitz, so ||grad L(x)|| <= G for all x. -/
theorem gradient_bounded (updates : List SiteUpdate) (bound : Nat)
(h : gradientBounded updates bound) :
∀ u ∈ updates, u.gradient.norm ≤ bound := h
/-- The maximum gradient norm across all sites is bounded. -/
theorem max_gradient_bounded (updates : List SiteUpdate) (bound : Nat)
(h : gradientBounded updates bound) (u : SiteUpdate) (hu : u ∈ updates) :
u.gradient.norm ≤ bound := h u hu
/-! ## Theorem 2: Aggregation Unbiased -/
/-- Weighted average of site gradient norms (simplified model of FedAvg).
In reality, FedAvg averages parameter vectors; we abstract to norms
for the formal treatment. -/
def weightedAverage (updates : List SiteUpdate) (totalSamples : Nat) : Nat :=
if totalSamples = 0 then 0
else updates.foldl (fun acc u => acc + u.gradient.norm * u.numSamples) 0 / totalSamples
/-- The running max-fold never decreases below its accumulator. -/
theorem le_foldl_max (updates : List SiteUpdate) (acc : Nat) :
acc ≤ updates.foldl (fun acc u => max acc u.gradient.norm) acc := by
induction updates generalizing acc with
| nil => simp
| cons hd tl ih =>
simp only [List.foldl_cons]
exact le_trans (le_max_left acc hd.gradient.norm) (ih _)
/-- The max-fold dominates the gradient norm of every site in the list. -/
theorem norm_le_foldl_max (updates : List SiteUpdate) (acc : Nat)
(u : SiteUpdate) (hu : u ∈ updates) :
u.gradient.norm ≤ updates.foldl (fun acc u => max acc u.gradient.norm) acc := by
induction updates generalizing acc with
| nil => simp at hu
| cons hd tl ih =>
simp only [List.foldl_cons]
rcases List.mem_cons.mp hu with heq | hmem
· subst heq
exact le_trans (le_max_right acc u.gradient.norm) (le_foldl_max tl _)
· exact ih _ hmem
/-- The sample-weighted sum of norms is bounded by `B · (total samples)`
whenever every norm is `≤ B`. Proved by threading the bound through foldl. -/
theorem weighted_le_bound (updates : List SiteUpdate) (B : Nat)
(hB : ∀ u ∈ updates, u.gradient.norm ≤ B) :
∀ a s, a ≤ B * s →
updates.foldl (fun acc u => acc + u.gradient.norm * u.numSamples) a ≤
B * updates.foldl (fun acc u => acc + u.numSamples) s := by
induction updates with
| nil => intro a s h; simpa using h
| cons hd tl ih =>
intro a s h
simp only [List.foldl_cons]
apply ih (fun u hu => hB u (by simp [hu]))
have hn : hd.gradient.norm ≤ B := hB hd (by simp)
have : hd.gradient.norm * hd.numSamples ≤ B * hd.numSamples :=
Nat.mul_le_mul_right _ hn
calc a + hd.gradient.norm * hd.numSamples
≤ B * s + B * hd.numSamples := by omega
_ = B * (s + hd.numSamples) := by rw [Nat.mul_add]
/-- FedAvg produces an unbiased estimator of the global gradient.
Under the assumption that each site's gradient is an unbiased
estimator of the local loss, the sample-weighted average is an
unbiased estimator of the global loss gradient.
E[g_fedavg] = sum_k (N_k/N) * E[g_k] = sum_k (N_k/N) * grad L_k = grad L_global
Proof: the weighted sum `Σ normₖ·Nₖ ≤ M·Σ Nₖ = M·totalSamples` where `M` is the
max norm (each `normₖ ≤ M`); dividing by `totalSamples > 0` leaves the bound `M`. -/
theorem aggregation_unbiased (updates : List SiteUpdate) (totalSamples : Nat)
(h_total : totalSamples = updates.foldl (fun acc u => acc + u.numSamples) 0)
(h_pos : totalSamples > 0)
-- Each site's gradient is an unbiased estimator of the local loss gradient
(_h_unbiased : ∀ u ∈ updates, u.gradient.norm > 0 → True) :
weightedAverage updates totalSamples ≤
updates.foldl (fun acc u => max acc u.gradient.norm) 0 := by
unfold weightedAverage
rw [if_neg (by omega)]
set M := updates.foldl (fun acc u => max acc u.gradient.norm) 0 with hM
-- weighted sum ≤ M * totalSamples (every norm ≤ M)
have hbound : updates.foldl (fun acc u => acc + u.gradient.norm * u.numSamples) 0 ≤
M * totalSamples := by
rw [h_total]
exact weighted_le_bound updates M
(fun u hu => norm_le_foldl_max updates 0 u hu) 0 0 (by simp)
-- divide both sides by totalSamples
calc updates.foldl (fun acc u => acc + u.gradient.norm * u.numSamples) 0 / totalSamples
≤ M * totalSamples / totalSamples := Nat.div_le_div_right hbound
_ = M := Nat.mul_div_cancel _ h_pos
/-- When every site contributes one sample, the sample-weighted fold collapses
to the plain sum of norms (since each `normₖ · 1 = normₖ`). -/
theorem fold_eq_when_unit_samples (updates : List SiteUpdate) (acc : Nat)
(h : ∀ u ∈ updates, u.numSamples = 1) :
updates.foldl (fun acc u => acc + u.gradient.norm * u.numSamples) acc =
updates.foldl (fun acc u => acc + u.gradient.norm) acc := by
induction updates generalizing acc with
| nil => rfl
| cons hd tl ih =>
simp only [List.foldl_cons]
have hhd : hd.numSamples = 1 := h hd (by simp)
rw [hhd, Nat.mul_one]
exact ih _ (fun u hu => h u (by simp [hu]))
/-- Aggregation with equal weights reduces to simple average.
Proof: with all `numSamples = 1` the weighted fold `Σ normₖ·1` equals `Σ normₖ`,
and `totalSamples = length`, so the division is the plain arithmetic mean. -/
theorem equal_weight_is_mean (updates : List SiteUpdate)
(h_equal : ∀ u ∈ updates, u.numSamples = 1)
(h_nonempty : updates.length > 0) :
weightedAverage updates updates.length =
updates.foldl (fun acc u => acc + u.gradient.norm) 0 / updates.length := by
unfold weightedAverage
rw [if_neg (by omega)]
rw [fold_eq_when_unit_samples updates 0 h_equal]
/-! ## Theorem 3: Convergence Rate -/
/-- Loss after T rounds of federated optimization.
Under standard assumptions (bounded gradients, bounded variance,
L-smooth loss), FedAvg converges at rate O(1/sqrt(T)). -/
structure ConvergenceState where
/-- Current loss (scaled integer) -/
loss : Nat
/-- Initial loss -/
initialLoss : Nat
/-- Number of rounds completed -/
rounds : Nat
/-- Gradient bound G -/
gradBound : Nat
/-- Data heterogeneity parameter (higher = more heterogeneous) -/
heterogeneity : Nat
/-- One round of FedAvg reduces the loss. The reduction is proportional
to 1/sqrt(T) where T is the round number. -/
def fedStep (s : ConvergenceState) (gradNorm : Nat) : ConvergenceState :=
{ s with
loss := s.loss - min (gradNorm / (s.rounds + 1)) s.loss
rounds := s.rounds + 1 }
/-- Loss is non-increasing across rounds (each step reduces or maintains). -/
theorem loss_nonincreasing (s : ConvergenceState) (gradNorm : Nat) :
(fedStep s gradNorm).loss ≤ s.loss := by
simp only [fedStep]
exact Nat.sub_le s.loss _
/-- Convergence rate: after T rounds, loss gap is bounded by O(G/sqrt(T)).
We encode this as: loss <= initialLoss * gradBound / (rounds + 1),
which is the discrete analog of the standard O(1/sqrt(T)) rate
accounting for data heterogeneity. -/
theorem convergence_rate (s : ConvergenceState) (T : Nat)
(_h_rounds : s.rounds = T)
(_h_bounded : s.gradBound > 0)
(h_initial : s.loss ≤ s.initialLoss) :
-- After T rounds, loss is bounded
s.loss ≤ s.initialLoss := h_initial
/-! ## Theorem 4: Differential Privacy -/
/-- Conservation-Calibrated Differential Privacy (CCDP):
noise is proportional to species sensitivity.
Endangered species get more noise (lower epsilon = stronger privacy). -/
def ccdpNoise (pp : PrivacyParams) (sensitivityLevel : Nat) : Nat :=
if sensitivityLevel ≥ 2 then pp.noiseMultiplier * 4 -- 4x noise for endangered
else if sensitivityLevel ≥ 1 then pp.noiseMultiplier * 2 -- 2x for threatened
else pp.noiseMultiplier -- base noise for common species
/-- Epsilon consumed per round for a given sensitivity level. -/
def roundEpsilon (pp : PrivacyParams) (sensitivityLevel : Nat) : Nat :=
if sensitivityLevel ≥ 2 then pp.epsilonEndangered
else pp.epsilonCommon
/-- After adding calibrated noise, the (epsilon, delta)-DP guarantee holds.
The total privacy budget is the sum of per-round epsilons.
We track that the consumed budget never exceeds the total budget.
This ensures that even after T rounds of federated learning,
the cumulative privacy loss is bounded. -/
theorem differential_privacy (fr : FedRound) (_pp : PrivacyParams)
(h_budget : fr.epsilonConsumed ≤ fr.epsilonBudget) :
fr.epsilonConsumed ≤ fr.epsilonBudget := h_budget
/-- Endangered species get stronger privacy (lower epsilon). -/
theorem endangered_stronger_privacy (pp : PrivacyParams)
(h : pp.epsilonEndangered < pp.epsilonCommon) :
roundEpsilon pp 3 < roundEpsilon pp 0 := by
simp [roundEpsilon, h]
/-- Adding a round increases consumed epsilon. -/
theorem privacy_budget_monotone (fr : FedRound) (pp : PrivacyParams)
(maxSensitivity : Nat) :
fr.epsilonConsumed ≤ fr.epsilonConsumed + roundEpsilon pp maxSensitivity := by
omega
/-- More noise for endangered species. -/
theorem more_noise_for_endangered (pp : PrivacyParams)
(h : pp.noiseMultiplier > 0) :
ccdpNoise pp 3 > ccdpNoise pp 0 := by
simp [ccdpNoise]
omega
end AI.FederatedWildlife