summaryrefslogtreecommitdiff
path: root/experiments/rain_ep_adapter_smoke.py
blob: 8ad73b74df48d81ab42d95265cfe87ef6be21836 (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
"""Integration smoke test against the pinned Rain EP implementation."""

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
    RainGradientCorrector,
    attach_to_rain_estimator,
)


RAIN_REVISION = "6b253fd8a5d267535f58ab79992256ef10031ceb"


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)
    output = energy.layers()[-1]
    cost = SquaredError(output)
    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 free_state(network, cost, minimizer, augmented, x, labels):
    network.set_input(x, reset=True)
    cost.set_target(labels)
    augmented.nudging = 0.0
    minimizer.compute_equilibrium()
    return [layer.state.clone() for layer in minimizer._layers]


def restore(minimizer, states):
    for layer, state in zip(minimizer._layers, states):
        layer.state = state.clone()


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


def main() -> None:
    args = parse_args()
    torch.manual_seed(20260806)
    energy, network, cost, augmented, minimizer, estimator = build_estimator(
        args.author_root)
    x = torch.randn(8, 4)
    labels = torch.arange(8) % 3
    states = free_state(network, cost, minimizer, augmented, x, labels)
    clean = [value.clone() for value in estimator.compute_gradient()]
    assert all(not value.requires_grad for value in clean)

    restore(minimizer, states)
    oracle_corrector = RainGradientCorrector(
        mode="oracle", bias_ratio=0.5, seed=19)
    attach_to_rain_estimator(estimator, oracle_corrector)
    oracle = estimator.compute_gradient()
    assert all(torch.equal(a, b) for a, b in zip(clean, oracle))

    # Exercise the shared corrector on a sequence of local states.  The
    # structured field is exactly affine in its fixed local basis; innovation
    # should learn it while a constant filter retains state-dependent error.
    template_states = [torch.randn_like(value) for value in clean]
    innovation = RainGradientCorrector(
        mode="innovation", bias_ratio=0.5, predictor_rate=0.2, seed=31)
    constant = RainGradientCorrector(
        mode="constant", bias_ratio=0.5, predictor_rate=0.2, seed=31)
    zero = [torch.zeros_like(value) for value in clean]
    local_sequence = [
        [scale * value for value in template_states]
        for scale in torch.linspace(-1.2, 1.2, 50)
    ]
    generator = torch.Generator().manual_seed(1988)
    for _ in range(20):
        for index in torch.randperm(len(local_sequence), generator=generator):
            local = local_sequence[int(index)]
            innovation.apply(zero, local)
            constant.apply(zero, local)
    held = [1.45 * value for value in template_states]
    innovation.apply(zero, held)
    constant.apply(zero, held)
    innovation_error = innovation.last_diagnostics["residual_bias_rms"]
    constant_error = constant.last_diagnostics["residual_bias_rms"]
    assert innovation_error < 0.25 * constant_error, (
        innovation_error, constant_error)
    assert innovation.debiaser.neutral_observations == constant.debiaser.neutral_observations
    print({
        "rain_revision_expected": RAIN_REVISION,
        "parameter_tensors": len(clean),
        "oracle_matches_clean_bitwise": True,
        "innovation_residual_bias_rms": innovation_error,
        "constant_residual_bias_rms": constant_error,
        "matched_neutral_observations": innovation.debiaser.neutral_observations,
        "requires_grad": False,
    })


if __name__ == "__main__":
    main()