summaryrefslogtreecommitdiff
path: root/experiments/rain_ep_dillavou_smoke.py
blob: d4a634a667919d6f4fee7aeea1163fb9786eb7e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/usr/bin/env python3
"""Mechanics checks for the post-estimator Dillavou update bias."""

from __future__ import annotations

import argparse
from pathlib import Path
import sys

import torch

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from sdil.rain_ep_adapter import (  # noqa: E402
    DillavouUpdateCorrector,
    attach_dillavou_to_rain_estimator,
)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--author-root", type=Path, required=True)
    return parser.parse_args()


def build_estimator(author_root: Path):
    sys.path.insert(0, str(author_root))
    from model.function.cost import SquaredError
    from model.function.network import Network
    from model.hopfield.minimizer import FixedPointMinimizer
    from model.hopfield.network import DeepHopfieldEnergy
    from training.sgd import AugmentedFunction, EquilibriumProp

    energy = DeepHopfieldEnergy([(4,), (7,), (3,)], [0.5, 0.5])
    energy.set_device("cpu")
    network = Network(energy)
    cost = SquaredError(energy.layers()[-1])
    augmented = AugmentedFunction(energy, cost)
    minimizer = FixedPointMinimizer(augmented, network.free_layers())
    minimizer.mode = "asynchronous"
    minimizer.num_iterations = 12
    estimator = EquilibriumProp(
        energy.params(), energy.layers(), augmented, cost, minimizer)
    estimator.variant = "positive"
    estimator.nudging = 0.25
    return energy, network, cost, augmented, minimizer, estimator


def main() -> None:
    args = parse_args()
    torch.manual_seed(20260807)

    # The exact paper model has a fixed B_i after the estimator.  Changing the
    # clean signal or parameter state must not change that field.
    clean_a = [torch.randn(11, 7), torch.randn(7)]
    clean_b = [torch.randn_like(value) for value in clean_a]
    parameters_a = [torch.randn_like(value) for value in clean_a]
    parameters_b = [value + 0.3 for value in parameters_a]
    raw = DillavouUpdateCorrector(
        mode="raw", bias_ratio=0.2, seed=41)
    measured_a = raw.apply(clean_a, parameters_a)
    measured_b = raw.apply(clean_b, parameters_b)
    bias_a = [value - clean for value, clean in zip(measured_a, clean_a)]
    bias_b = [value - clean for value, clean in zip(measured_b, clean_b)]
    fixed_relative_error = max(
        float((first - second).norm() / first.norm().clamp_min(1e-30))
        for first, second in zip(bias_a, bias_b)
    )
    assert fixed_relative_error < 2e-6, fixed_relative_error

    constant = DillavouUpdateCorrector(
        mode="constant", bias_ratio=0.2, predictor_rate=1.0, seed=41)
    innovation = DillavouUpdateCorrector(
        mode="innovation", bias_ratio=0.2, predictor_rate=1.0, seed=41)
    corrected_constant = constant.apply(clean_a, parameters_a)
    corrected_innovation = innovation.apply(clean_a, parameters_a)
    constant_error = max(
        float((actual - target).norm() / target.norm().clamp_min(1e-30))
        for actual, target in zip(corrected_constant, clean_a)
    )
    innovation_error = max(
        float((actual - target).norm() / target.norm().clamp_min(1e-30))
        for actual, target in zip(corrected_innovation, clean_a)
    )
    assert constant_error < 2e-7, constant_error
    assert innovation_error < 2e-7, innovation_error

    # Integration check: the corruption is attached after Rain's hand-written
    # local EP estimator and introduces no autograd graph.
    energy, network, cost, augmented, minimizer, estimator = build_estimator(
        args.author_root)
    x = torch.randn(8, 4)
    labels = torch.arange(8) % 3
    network.set_input(x, reset=True)
    cost.set_target(labels)
    augmented.nudging = 0.0
    minimizer.compute_equilibrium()
    free = [layer.state.clone() for layer in minimizer._layers]
    clean = [value.clone() for value in estimator.compute_gradient()]
    for layer, state in zip(minimizer._layers, free):
        layer.state = state.clone()
    integrated = DillavouUpdateCorrector(
        mode="raw", bias_ratio=0.2, seed=53)
    attach_dillavou_to_rain_estimator(estimator, integrated)
    measured = estimator.compute_gradient()
    assert all(not value.requires_grad for value in measured)
    assert integrated.last_diagnostics["bias_model"] == (
        "dillavou_constant_update")
    observed_ratio = integrated.last_diagnostics["bias_to_clean_update_rms"]
    assert abs(observed_ratio - 0.2) < 2e-6, observed_ratio

    print({
        "fixed_bias_relative_error_after_state_change": fixed_relative_error,
        "constant_calibration_relative_error": constant_error,
        "innovation_relative_error": innovation_error,
        "integrated_bias_to_clean_update_rms": observed_ratio,
        "neutral_observations": constant.debiaser.neutral_observations,
        "autodiff_used_for_learning": False,
    })


if __name__ == "__main__":
    main()