summaryrefslogtreecommitdiff
path: root/experiments/conv_local_smoke.py
blob: 35073a65fddd270ea47471941e509bb770028aed (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
#!/usr/bin/env python3
"""Prove the convolutional local eligibility matches exact BP when instructed."""
import os
import sys

import torch
import torch.nn.functional as F

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sdil.conv import CIFARLocalResNet


def architecture_checks():
    expected = {
        8: (7, 74810),
        20: (19, 268346),
        32: (31, 461882),
        56: (55, 848954),
    }
    for depth, (hidden, parameters) in expected.items():
        net = CIFARLocalResNet(depth=depth)
        assert net.n_hidden == hidden
        assert net.n_forward_parameters == parameters
        assert len(net.blocks) == 3 * ((depth - 2) // 6)
        assert len(net.W) + 1 == depth
    for depth in (7, 9, 21):
        try:
            CIFARLocalResNet(depth=depth)
        except ValueError:
            pass
        else:
            raise AssertionError(f"invalid depth {depth} was accepted")

    x = torch.arange(2 * 3 * 8 * 8, dtype=torch.float32).reshape(2, 3, 8, 8)
    shortcut = CIFARLocalResNet._option_a_shortcut(x, 6, 2)
    assert tuple(shortcut.shape) == (2, 6, 4, 4)
    assert torch.count_nonzero(shortcut[:, 0]) == 0
    assert torch.count_nonzero(shortcut[:, -2:]) == 0
    assert torch.equal(shortcut[:, 1:4], x[:, :, ::2, ::2])


def exact_local_gradient_check():
    torch.manual_seed(123)
    batch = 3
    x = torch.randn(batch, 3, 32, 32)
    y = torch.tensor([0, 3, 8])
    local = CIFARLocalResNet(depth=8, base_width=4, seed=19)
    bp = CIFARLocalResNet(depth=8, base_width=4, seed=19)

    parameters = local.W + [local.W_out, local.b_out]
    for parameter in parameters:
        parameter.requires_grad_(True)
    forward = local.forward(x, return_cache=True)
    for hidden in forward["hiddens"]:
        hidden.retain_grad()
    loss = F.cross_entropy(forward["logits"], y)
    loss.backward()

    # .backward() differentiated a batch-mean loss.  Multiplying hidden grads
    # by B recovers the per-example convention consumed by the local rule.
    teaching = [-batch * hidden.grad for hidden in forward["hiddens"]]
    output_error = (torch.softmax(forward["logits"].detach(), dim=1)
                    - F.one_hot(y, local.n_classes))
    directions, out_direction, bias_direction = local.local_ascent_directions(
        teaching, output_error, forward)

    relative_errors = []
    for direction, parameter in zip(directions, local.W):
        absolute = (direction + parameter.grad).abs().max()
        scale = parameter.grad.abs().max().clamp_min(1e-12)
        relative_errors.append(float(absolute / scale))
    output_abs = float((out_direction + local.W_out.grad).abs().max())
    bias_abs = float((bias_direction + local.b_out.grad).abs().max())
    assert max(relative_errors) < 3e-5
    assert output_abs < 2e-6 and bias_abs < 2e-6

    for parameter in parameters:
        parameter.requires_grad_(False)
    eta = 0.017
    local.apply_ascent(directions, out_direction, bias_direction, eta)
    bp_loss = bp.bp_step(x, y, eta)
    assert abs(float(loss.detach()) - bp_loss) < 1e-7
    parameter_differences = [
        float((left - right).abs().max())
        for left, right in zip(
            local.W + [local.W_out, local.b_out],
            bp.W + [bp.W_out, bp.b_out])]
    assert max(parameter_differences) < 2e-7
    return {
        "max_relative_local_gradient_error": max(relative_errors),
        "output_absolute_error": output_abs,
        "post_update_parameter_max_error": max(parameter_differences),
    }


def perturbation_checks():
    net = CIFARLocalResNet(depth=8, base_width=4, seed=3)
    x = torch.randn(2, 3, 32, 32)
    clean = net.forward(x)
    perturbations = [torch.zeros_like(hidden) for hidden in clean["hiddens"]]
    perturbed = net.forward(x, perturbations=perturbations)
    assert torch.equal(clean["logits"], perturbed["logits"])
    perturbations[0] = torch.ones_like(perturbations[0]) * 0.01
    changed = net.forward(x, perturbations=perturbations)
    assert not torch.equal(clean["logits"], changed["logits"])
    try:
        net.forward(x, perturbations=perturbations[:-1])
    except ValueError:
        pass
    else:
        raise AssertionError("short perturbation list was accepted")


def main():
    architecture_checks()
    perturbation_checks()
    report = exact_local_gradient_check()
    print(report)
    print("ALL CONVOLUTIONAL LOCAL-ELIGIBILITY CHECKS PASSED")


if __name__ == "__main__":
    main()