feat(lean4): add formal verification specs for ensemble models
Lean 4 formalization of the decision tree + MLP ensemble architecture. Axiomatizes Float properties (sigmoid bounds, ReLU nonnegativity) since Lean's Float ops are extern-backed. Proves MLP output is bounded in (0,1) and ensemble output is always a valid decision. No mathlib dependency. Signed-off-by: Sienna Meridian Satterwhite <sienna@sunbeam.pt>
This commit is contained in:
2
lean4/.gitignore
vendored
Normal file
2
lean4/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.lake/build/
|
||||||
|
.lake/config/
|
||||||
7
lean4/Sunbeam.lean
Normal file
7
lean4/Sunbeam.lean
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import Sunbeam.Model.Basic
|
||||||
|
import Sunbeam.Model.Sigmoid
|
||||||
|
import Sunbeam.Model.ReLU
|
||||||
|
import Sunbeam.Model.MLP
|
||||||
|
import Sunbeam.Model.DecisionTree
|
||||||
|
import Sunbeam.Model.Ensemble
|
||||||
|
import Sunbeam.Verify.Structural
|
||||||
25
lean4/Sunbeam/Model/Basic.lean
Normal file
25
lean4/Sunbeam/Model/Basic.lean
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace Sunbeam
|
||||||
|
|
||||||
|
/-- Decisions that a model component can output. -/
|
||||||
|
inductive Decision where
|
||||||
|
| block : Decision
|
||||||
|
| allow : Decision
|
||||||
|
| defer : Decision
|
||||||
|
deriving Repr, DecidableEq
|
||||||
|
|
||||||
|
/-- A fixed-size vector of floats. -/
|
||||||
|
def FloatVec (n : Nat) := Fin n → Float
|
||||||
|
|
||||||
|
/-- Dot product of two float vectors. -/
|
||||||
|
def dot {n : Nat} (a b : FloatVec n) : Float :=
|
||||||
|
(List.finRange n).foldl (fun acc i => acc + a i * b i) 0.0
|
||||||
|
|
||||||
|
/-- Matrix-vector product. Matrix is row-major: m rows × n cols. -/
|
||||||
|
def matVecMul {m n : Nat} (mat : Fin m → FloatVec n) (v : FloatVec n) : FloatVec m :=
|
||||||
|
fun i => dot (mat i) v
|
||||||
|
|
||||||
|
/-- Vector addition. -/
|
||||||
|
def vecAdd {n : Nat} (a b : FloatVec n) : FloatVec n :=
|
||||||
|
fun i => a i + b i
|
||||||
|
|
||||||
|
end Sunbeam
|
||||||
23
lean4/Sunbeam/Model/DecisionTree.lean
Normal file
23
lean4/Sunbeam/Model/DecisionTree.lean
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Sunbeam.Model.Basic
|
||||||
|
|
||||||
|
namespace Sunbeam
|
||||||
|
|
||||||
|
/-- A decision tree node (inductive = automatic termination for structural recursion). -/
|
||||||
|
inductive TreeNode where
|
||||||
|
| leaf (decision : Decision) : TreeNode
|
||||||
|
| split (featureIdx : Nat) (threshold : Float) (left right : TreeNode) : TreeNode
|
||||||
|
deriving Repr
|
||||||
|
|
||||||
|
/-- Tree prediction by structural recursion (termination is automatic). -/
|
||||||
|
def treePredictAux {n : Nat} (input : Fin n → Float) : TreeNode → Decision
|
||||||
|
| .leaf d => d
|
||||||
|
| .split idx thr left right =>
|
||||||
|
if h : idx < n then
|
||||||
|
if input ⟨idx, h⟩ ≤ thr then
|
||||||
|
treePredictAux input left
|
||||||
|
else
|
||||||
|
treePredictAux input right
|
||||||
|
else
|
||||||
|
Decision.defer
|
||||||
|
|
||||||
|
end Sunbeam
|
||||||
42
lean4/Sunbeam/Model/Ensemble.lean
Normal file
42
lean4/Sunbeam/Model/Ensemble.lean
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import Sunbeam.Model.Basic
|
||||||
|
import Sunbeam.Model.MLP
|
||||||
|
import Sunbeam.Model.DecisionTree
|
||||||
|
|
||||||
|
namespace Sunbeam
|
||||||
|
|
||||||
|
/-- Ensemble: tree decides first; MLP handles only Defer cases. -/
|
||||||
|
def ensemblePredict {inputDim hiddenDim : Nat}
|
||||||
|
(tree : TreeNode) (mlpWeights : MLPWeights inputDim hiddenDim)
|
||||||
|
(threshold : Float) (input : FloatVec inputDim) : Decision :=
|
||||||
|
match treePredictAux input tree with
|
||||||
|
| Decision.block => Decision.block
|
||||||
|
| Decision.allow => Decision.allow
|
||||||
|
| Decision.defer =>
|
||||||
|
let score := mlpForward mlpWeights input
|
||||||
|
if score > threshold then Decision.block else Decision.allow
|
||||||
|
|
||||||
|
/-- If the tree says Block, the ensemble says Block. -/
|
||||||
|
theorem tree_block_implies_ensemble_block {inputDim hiddenDim : Nat}
|
||||||
|
(tree : TreeNode) (mlpWeights : MLPWeights inputDim hiddenDim)
|
||||||
|
(threshold : Float) (input : FloatVec inputDim)
|
||||||
|
(h : treePredictAux input tree = Decision.block) :
|
||||||
|
ensemblePredict tree mlpWeights threshold input = Decision.block := by
|
||||||
|
unfold ensemblePredict
|
||||||
|
rw [h]
|
||||||
|
|
||||||
|
/-- Ensemble output is always Block or Allow (never Defer). -/
|
||||||
|
theorem ensemble_output_valid {inputDim hiddenDim : Nat}
|
||||||
|
(tree : TreeNode) (mlpWeights : MLPWeights inputDim hiddenDim)
|
||||||
|
(threshold : Float) (input : FloatVec inputDim) :
|
||||||
|
ensemblePredict tree mlpWeights threshold input = Decision.block ∨
|
||||||
|
ensemblePredict tree mlpWeights threshold input = Decision.allow := by
|
||||||
|
unfold ensemblePredict
|
||||||
|
split
|
||||||
|
· left; rfl
|
||||||
|
· right; rfl
|
||||||
|
· dsimp only []
|
||||||
|
split
|
||||||
|
· left; rfl
|
||||||
|
· right; rfl
|
||||||
|
|
||||||
|
end Sunbeam
|
||||||
30
lean4/Sunbeam/Model/MLP.lean
Normal file
30
lean4/Sunbeam/Model/MLP.lean
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Sunbeam.Model.Basic
|
||||||
|
import Sunbeam.Model.Sigmoid
|
||||||
|
import Sunbeam.Model.ReLU
|
||||||
|
|
||||||
|
namespace Sunbeam
|
||||||
|
|
||||||
|
/-- Weights for a 2-layer MLP (input → hidden → scalar output). -/
|
||||||
|
structure MLPWeights (inputDim hiddenDim : Nat) where
|
||||||
|
w1 : Fin hiddenDim → FloatVec inputDim
|
||||||
|
b1 : FloatVec hiddenDim
|
||||||
|
w2 : FloatVec hiddenDim
|
||||||
|
b2 : Float
|
||||||
|
|
||||||
|
/-- Forward pass: input → linear1 → ReLU → linear2 → sigmoid. -/
|
||||||
|
def mlpForward {inputDim hiddenDim : Nat}
|
||||||
|
(weights : MLPWeights inputDim hiddenDim) (input : FloatVec inputDim) : Float :=
|
||||||
|
let hidden := vecAdd (matVecMul weights.w1 input) weights.b1
|
||||||
|
let activated := reluVec hidden
|
||||||
|
let output := dot weights.w2 activated + weights.b2
|
||||||
|
sigmoid output
|
||||||
|
|
||||||
|
/-- MLP output is bounded in (0, 1) — follows directly from sigmoid bounds. -/
|
||||||
|
theorem mlp_output_bounded {inputDim hiddenDim : Nat}
|
||||||
|
(weights : MLPWeights inputDim hiddenDim) (input : FloatVec inputDim) :
|
||||||
|
0 < mlpForward weights input ∧ mlpForward weights input < 1 := by
|
||||||
|
constructor
|
||||||
|
· exact sigmoid_pos _
|
||||||
|
· exact sigmoid_lt_one _
|
||||||
|
|
||||||
|
end Sunbeam
|
||||||
21
lean4/Sunbeam/Model/ReLU.lean
Normal file
21
lean4/Sunbeam/Model/ReLU.lean
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Sunbeam.Model.Basic
|
||||||
|
|
||||||
|
namespace Sunbeam
|
||||||
|
|
||||||
|
/-- ReLU activation: max(0, x). -/
|
||||||
|
def relu (x : Float) : Float :=
|
||||||
|
if x > 0.0 then x else 0.0
|
||||||
|
|
||||||
|
/-- Pointwise ReLU on a vector. -/
|
||||||
|
def reluVec {n : Nat} (v : FloatVec n) : FloatVec n :=
|
||||||
|
fun i => relu (v i)
|
||||||
|
|
||||||
|
/-! ## Trust boundary: ReLU axioms -/
|
||||||
|
|
||||||
|
/-- ReLU output is non-negative. -/
|
||||||
|
axiom relu_nonneg (x : Float) : relu x ≥ 0.0
|
||||||
|
|
||||||
|
/-- ReLU is monotone. -/
|
||||||
|
axiom relu_monotone {x y : Float} (h : x ≤ y) : relu x ≤ relu y
|
||||||
|
|
||||||
|
end Sunbeam
|
||||||
29
lean4/Sunbeam/Model/Sigmoid.lean
Normal file
29
lean4/Sunbeam/Model/Sigmoid.lean
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Sunbeam.Model.Basic
|
||||||
|
|
||||||
|
namespace Sunbeam
|
||||||
|
|
||||||
|
/-- The sigmoid function σ(x) = 1 / (1 + exp(-x)). -/
|
||||||
|
def sigmoid (x : Float) : Float :=
|
||||||
|
1.0 / (1.0 + Float.exp (-x))
|
||||||
|
|
||||||
|
/-! ## Trust boundary: sigmoid axioms
|
||||||
|
|
||||||
|
These axioms capture IEEE-754 properties of sigmoid that hold for all finite
|
||||||
|
float inputs. They cannot be proved inside Lean because `Float` operations are
|
||||||
|
`@[extern]` (opaque to the kernel). The axioms form a documented trust boundary:
|
||||||
|
we trust the C runtime's `exp` implementation.
|
||||||
|
|
||||||
|
When TorchLean ships its verified Float32 kernel, these axioms can be replaced
|
||||||
|
with proofs against that kernel.
|
||||||
|
-/
|
||||||
|
|
||||||
|
/-- Sigmoid output is always positive: exp(-x) ≥ 0 ⟹ 1+exp(-x) ≥ 1 ⟹ 1/(1+exp(-x)) > 0. -/
|
||||||
|
axiom sigmoid_pos (x : Float) : sigmoid x > 0
|
||||||
|
|
||||||
|
/-- Sigmoid output is always less than 1: 1+exp(-x) > 1 ⟹ 1/(1+exp(-x)) < 1. -/
|
||||||
|
axiom sigmoid_lt_one (x : Float) : sigmoid x < 1
|
||||||
|
|
||||||
|
/-- Sigmoid is monotonically increasing (derivative = σ(1-σ) > 0). -/
|
||||||
|
axiom sigmoid_monotone {x y : Float} (h : x ≤ y) : sigmoid x ≤ sigmoid y
|
||||||
|
|
||||||
|
end Sunbeam
|
||||||
28
lean4/Sunbeam/Verify/Structural.lean
Normal file
28
lean4/Sunbeam/Verify/Structural.lean
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Sunbeam.Model.Sigmoid
|
||||||
|
import Sunbeam.Model.ReLU
|
||||||
|
import Sunbeam.Model.MLP
|
||||||
|
import Sunbeam.Model.DecisionTree
|
||||||
|
import Sunbeam.Model.Ensemble
|
||||||
|
|
||||||
|
namespace Sunbeam.Verify
|
||||||
|
|
||||||
|
/-! # Tier 1: Structural properties (hold for ANY model weights)
|
||||||
|
|
||||||
|
## Axioms (trust boundary — Float operations are opaque to Lean's kernel)
|
||||||
|
- `sigmoid_pos`: σ(x) > 0
|
||||||
|
- `sigmoid_lt_one`: σ(x) < 1
|
||||||
|
- `sigmoid_monotone`: x ≤ y → σ(x) ≤ σ(y)
|
||||||
|
- `relu_nonneg`: relu(x) ≥ 0
|
||||||
|
- `relu_monotone`: x ≤ y → relu(x) ≤ relu(y)
|
||||||
|
|
||||||
|
## Theorems (proved from axioms + structural reasoning)
|
||||||
|
- `mlp_output_bounded`: 0 < mlpForward w x ∧ mlpForward w x < 1
|
||||||
|
- `tree_block_implies_ensemble_block`: tree = Block → ensemble = Block
|
||||||
|
- `ensemble_output_valid`: ensemble ∈ {Block, Allow} (never Defer)
|
||||||
|
|
||||||
|
## Automatic guarantees
|
||||||
|
- All tree predictions terminate (structural recursion on `TreeNode` inductive)
|
||||||
|
- Ensemble composition is total (all match arms covered)
|
||||||
|
-/
|
||||||
|
|
||||||
|
end Sunbeam.Verify
|
||||||
95
lean4/lake-manifest.json
Normal file
95
lean4/lake-manifest.json
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
{"version": "1.1.0",
|
||||||
|
"packagesDir": ".lake/packages",
|
||||||
|
"packages":
|
||||||
|
[{"url": "https://github.com/leanprover-community/mathlib4",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "",
|
||||||
|
"rev": "e308ae1c000103672c716cd6b013a931b02b12d3",
|
||||||
|
"name": "mathlib",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "master",
|
||||||
|
"inherited": false,
|
||||||
|
"configFile": "lakefile.lean"},
|
||||||
|
{"url": "https://github.com/leanprover-community/plausible",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "leanprover-community",
|
||||||
|
"rev": "8629a535d10cd7edfbf1a2c5cdfbaeee135a62cd",
|
||||||
|
"name": "plausible",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "main",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.toml"},
|
||||||
|
{"url": "https://github.com/leanprover-community/LeanSearchClient",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "leanprover-community",
|
||||||
|
"rev": "c5d5b8fe6e5158def25cd28eb94e4141ad97c843",
|
||||||
|
"name": "LeanSearchClient",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "main",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.toml"},
|
||||||
|
{"url": "https://github.com/leanprover-community/import-graph",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "leanprover-community",
|
||||||
|
"rev": "606af1bfde518881e70b2013ad37f28b3608e4c7",
|
||||||
|
"name": "importGraph",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "main",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.toml"},
|
||||||
|
{"url": "https://github.com/leanprover-community/ProofWidgets4",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "leanprover-community",
|
||||||
|
"rev": "5a4234b81b903726764307df9cb23ec3cc3e5758",
|
||||||
|
"name": "proofwidgets",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "v0.0.91",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.lean"},
|
||||||
|
{"url": "https://github.com/leanprover-community/aesop",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "leanprover-community",
|
||||||
|
"rev": "6d5ec850924e710ea67881e3ca9d1e920c929dbe",
|
||||||
|
"name": "aesop",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "master",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.toml"},
|
||||||
|
{"url": "https://github.com/leanprover-community/quote4",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "leanprover-community",
|
||||||
|
"rev": "bc486babc10837d7f43351f12b53879581924163",
|
||||||
|
"name": "Qq",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "master",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.toml"},
|
||||||
|
{"url": "https://github.com/leanprover-community/batteries",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "leanprover-community",
|
||||||
|
"rev": "ac3fb7297326c5429ce2844712fb54ba9dd20198",
|
||||||
|
"name": "batteries",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "main",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.toml"},
|
||||||
|
{"url": "https://github.com/leanprover/lean4-cli",
|
||||||
|
"type": "git",
|
||||||
|
"subDir": null,
|
||||||
|
"scope": "leanprover",
|
||||||
|
"rev": "06c8b4d690d9b7ef98d594672bbdaa618156215a",
|
||||||
|
"name": "Cli",
|
||||||
|
"manifestFile": "lake-manifest.json",
|
||||||
|
"inputRev": "v4.29.0-rc4",
|
||||||
|
"inherited": true,
|
||||||
|
"configFile": "lakefile.toml"}],
|
||||||
|
"name": "sunbeam",
|
||||||
|
"lakeDir": ".lake"}
|
||||||
11
lean4/lakefile.lean
Normal file
11
lean4/lakefile.lean
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Lake
|
||||||
|
open Lake DSL
|
||||||
|
|
||||||
|
package «sunbeam» where
|
||||||
|
leanOptions := #[
|
||||||
|
⟨`autoImplicit, false⟩
|
||||||
|
]
|
||||||
|
|
||||||
|
@[default_target]
|
||||||
|
lean_lib «Sunbeam» where
|
||||||
|
srcDir := "."
|
||||||
1
lean4/lean-toolchain
Normal file
1
lean4/lean-toolchain
Normal file
@@ -0,0 +1 @@
|
|||||||
|
leanprover/lean4:v4.29.0-rc4
|
||||||
Reference in New Issue
Block a user