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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
|
"""Textbook BP-free learning rules on the physical/digital partition.
Every rule is expressed in the same terms as ZBP: it decides which error signal reaches each physical block's
OUTPUT and what (if anything) is propagated to its INPUT; block parameters always update locally from the
incoming error (in-block autograd = the physical-equivalent local rule) and digital layers use autograd.
drtp Direct Random Target Projection (Frenkel, Lefebvre, Bol 2021): DFA whose feedback signal is the
(negated) one-hot target instead of the output error. Handled inside ZBPFunction (mode 'drtp').
pepita PEPITA (Dellaferrera & Kreiman 2022): a second forward pass with the network input modulated by a
fixed random projection of the output error; every parameterised unit's error is its own
(clean - modulated) output, gradients are taken on the modulated pass, nothing propagates.
wm weight mirror (Akrout et al. 2019): textbook FA whose feedback matrices are learned in a mirror
phase from input noise / output readout of each linear layer (B -> W, so FA -> BP as B converges).
dtp difference target propagation (Lee et al. 2015), MLP / residual MLP: learned inverses g_l propagate
targets, each block minimises |f_l(h_{l-1}) - t_l|^2 locally.
ff forward-forward (Hinton 2022), MLP / residual MLP: per-block goodness on positive / negative data
(label embedded in the input), inputs length-normalised between blocks, no propagation.
"""
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from .autograd import ZBPBlock, zbp_blocks, DFA
from .fa import FALinear, FAConv2d
# ----------------------------------------------------------------------------------------------- units
def _inside(module, containers):
return any(m is module for m in containers)
def learning_units(model):
"""PEPITA units: every ZBPBlock (a physical block is one unit: only its output is observable) plus every
parameterised leaf module that is not inside a ZBPBlock."""
blocks = zbp_blocks(model)
inside = set()
for b in blocks:
for m in b.modules():
if m is not b:
inside.add(id(m))
units = []
for m in model.modules():
if isinstance(m, ZBPBlock):
units.append(m)
elif id(m) not in inside and not isinstance(m, ZBPBlock) and any(True for _ in m.parameters(recurse=False)):
units.append(m)
return units
# ----------------------------------------------------------------------------------------------- PEPITA
class Pepita:
"""Holds the fixed random projection F of the output error onto the network input (or the embedding)."""
def __init__(self, n_out, in_shape, device, scale=0.05, seed=0, embed=None):
g = torch.Generator(device="cpu"); g.manual_seed(seed)
n_in = math.prod(in_shape)
# Dellaferrera & Kreiman: F ~ U(-a, a), a = sqrt(6 / (n_in + n_out)) * scale-ish; we use their 0.05 default
a = math.sqrt(6.0 / (n_in + n_out))
self.F = ((torch.rand(n_out, n_in, generator=g) * 2 - 1) * a * scale).to(device)
self.in_shape = tuple(in_shape)
self.embed = embed # nn.Embedding to modulate instead of the raw input (language models)
self._mod = None
if embed is not None:
embed.register_forward_hook(self._embed_hook)
def _embed_hook(self, m, inp, out):
return out if self._mod is None else out - self._mod
def step(self, model, x, y, n_out, blocks_mode="zero"):
"""One PEPITA step: returns the clean loss. Leaves .grad on every parameter (gradient-like quantities)."""
units = learning_units(model)
blocks = zbp_blocks(model)
modes = [b.cfg for b in blocks]
for b in blocks:
b.cfg = b.cfg.replace(mode=blocks_mode) # no propagation through physical blocks
# clean pass
outs_clean = {}
hs = [u.register_forward_hook(lambda m, i, o, u=u: outs_clean.__setitem__(id(u), o.detach())) for u in units]
with torch.no_grad():
logits = model(x)
for h in hs:
h.remove()
if logits.dim() == 3: # LM: per-token error
e = logits.softmax(-1) - F.one_hot(y, n_out).to(logits.dtype)
e = e / (logits.shape[0] * logits.shape[1])
else:
e = (logits.softmax(-1) - F.one_hot(y, n_out).to(logits.dtype)) / logits.shape[0]
loss = F.cross_entropy(logits.reshape(-1, n_out), y.reshape(-1))
# modulated pass: input (or embedding) minus F e, each unit's input detached so nothing chains
if self.embed is None:
x_mod = x - (e.reshape(-1, n_out) @ self.F).reshape(x.shape) * x.shape[0] # undo the 1/B in e
else:
self._mod = (e @ self.F).reshape(*e.shape[:-1], -1) * (e.shape[0] * e.shape[1])
x_mod = x
outs_mod = {}
def pre(m, inp):
return tuple(t.detach().requires_grad_(t.is_floating_point()) if torch.is_tensor(t) else t for t in inp)
hs = [u.register_forward_pre_hook(pre) for u in units]
hs += [u.register_forward_hook(lambda m, i, o, u=u: outs_mod.__setitem__(id(u), o)) for u in units]
logits_mod = model(x_mod)
for h in hs:
h.remove()
self._mod = None
# inject: last unit (logits) gets the true error, every other unit its activation difference
tensors, grads = [], []
last = next((u for u in units if outs_mod.get(id(u)) is logits_mod), units[-1])
n_units = logits.shape[0] * (logits.shape[1] if logits.dim() == 3 else 1) # batch (x tokens) average, as for e
for u in units:
o = outs_mod.get(id(u))
if o is None or not torch.is_tensor(o) or not o.requires_grad:
continue
g = e if u is last else (outs_clean[id(u)] - o.detach()) / n_units
tensors.append(o); grads.append(g)
torch.autograd.backward(tensors, grads)
for b, c in zip(blocks, modes):
b.cfg = c
return loss.detach()
# ----------------------------------------------------------------------------------------------- weight mirror
@torch.no_grad()
def mirror_update(model, rate=0.01, n_noise=256, gen=None):
"""Akrout et al. 2019 mirror phase: for every FA layer, drive the linear part with zero-mean noise xi, read
y = W xi, and update B <- B + rate * (y^T xi / n) - rate * B, whose fixed point is B = W."""
for m in model.modules():
if isinstance(m, FALinear):
xi = torch.randn(n_noise, m.in_features, generator=gen, device=m.weight.device, dtype=m.weight.dtype)
y = xi @ m.weight.t()
m.B.mul_(1 - rate).add_(rate * (y.t() @ xi) / n_noise)
elif isinstance(m, FAConv2d):
k = m.in_channels * m.kernel_size[0] * m.kernel_size[1]
xi = torch.randn(n_noise, k, generator=gen, device=m.weight.device, dtype=m.weight.dtype)
y = xi @ m.weight.flatten(1).t()
m.B.mul_(1 - rate).add_(rate * (y.t() @ xi).reshape(m.B.shape) / n_noise)
# ----------------------------------------------------------------------------------------------- DTP
class DTP:
"""Difference target propagation on a chain of ZBP blocks (MLP / residual MLP) with a digital readout.
Inverses g_l (one per block, residual form h + V2 tanh(V1 h)) are trained on noisy forward passes of the block."""
def __init__(self, model, device, sigma=0.1, eta_hat=0.5, lr_inv=1e-3):
self.blocks = [b for b in zbp_blocks(model) if b.estimate_input_grad] # blocks that need a target below them
self.first = [b for b in zbp_blocks(model) if not b.estimate_input_grad]
self.model, self.sigma, self.eta_hat = model, sigma, eta_hat
self.inv = nn.ModuleDict()
self.dims = {}
self.opt_inv = None
self.device, self.lr_inv = device, lr_inv
def _inverse(self, b, d_out, d_in):
k = b.name
if k not in self.inv:
g = nn.Sequential(nn.Linear(d_out, d_in), nn.Tanh(), nn.Linear(d_in, d_in)).to(self.device)
nn.init.zeros_(g[2].weight); nn.init.zeros_(g[2].bias)
self.inv[k] = g
self.opt_inv = torch.optim.Adam(self.inv.parameters(), lr=self.lr_inv)
g = self.inv[k]
return lambda h: h[..., :d_in] + g(h) if d_out >= d_in else g(h)
def step(self, x, y):
model = self.model
caps = {}
hs = [b.register_forward_hook(lambda m, i, o, b=b: caps.__setitem__(b.name, (i[0].detach(), o.detach()))) for b in zbp_blocks(model)]
with torch.no_grad():
hs_all = model(x)
for h in hs:
h.remove()
# digital readout: exact gradient w.r.t. its parameters and w.r.t. the top block's output
top = zbp_blocks(model)[-1]
h_top = caps[top.name][1].clone().requires_grad_(True)
logits = model.readout(h_top)
loss = F.cross_entropy(logits, y)
loss.backward() # readout params get .grad; h_top.grad = dL/dh_top
t = h_top.detach() - self.eta_hat * h_top.grad
# targets downward, local losses, inverse training
order = zbp_blocks(model)
inv_loss_total = 0.0
for b in reversed(order):
x_in, h_out = caps[b.name]
# local loss for the block: |f(x_in) - t|^2 (gradient w.r.t. block parameters only)
with torch.enable_grad():
out = b(x_in.detach())
l_loc = 0.5 * ((out - t) ** 2).sum() / x_in.shape[0]
grads = torch.autograd.grad(l_loc, [p for p in b.parameters() if p.requires_grad], allow_unused=True)
for p, g in zip([p for p in b.parameters() if p.requires_grad], grads):
if g is not None:
p.grad = g if p.grad is None else p.grad + g
if not b.estimate_input_grad:
break # first block: nothing below
g_inv = self._inverse(b, h_out.shape[-1], x_in.shape[-1])
# inverse training on a noisy forward pass of the block (one extra physical query)
with torch.no_grad():
xn = x_in + self.sigma * x_in.std() * torch.randn_like(x_in)
hn = b(xn)
with torch.enable_grad():
l_inv = ((g_inv(hn) - xn) ** 2).mean()
self.opt_inv.zero_grad(set_to_none=True)
l_inv.backward()
self.opt_inv.step()
inv_loss_total += l_inv.item()
with torch.no_grad(): # difference target for the block below
t = x_in + g_inv(t) - g_inv(h_out)
self.last_inv_loss = inv_loss_total
return loss.detach()
# ----------------------------------------------------------------------------------------------- Forward-Forward
class FF:
"""Hinton 2022 on a chain of ZBP blocks: positive = input with the true label embedded in its first n_out
coordinates, negative = a wrong label; per block loss softplus(-(G_pos - theta)) + softplus(G_neg - theta) with
goodness G = mean(h^2); block inputs are length-normalised; prediction = label with the largest summed goodness
of all blocks after the first."""
def __init__(self, model, n_out=10, theta=2.0, label_scale=1.0):
self.model, self.n_out, self.theta, self.label_scale = model, n_out, theta, label_scale
def embed(self, x, labels):
x = x.flatten(1).clone()
x[:, :self.n_out] = F.one_hot(labels, self.n_out).to(x.dtype) * self.label_scale
return x
@staticmethod
def norm(h):
return h / (h.norm(dim=-1, keepdim=True) + 1e-4) * math.sqrt(h.shape[-1])
def goodness_chain(self, x, train=False):
"""Returns the list of per-block goodness values; with train=True also leaves gradients (FF losses)."""
h = x
goods = []
for b in zbp_blocks(self.model):
h_in = self.norm(h).detach()
out = b(h_in) if train else b(h_in).detach()
goods.append(out.pow(2).mean(-1))
h = out.detach()
return goods
def step(self, x, y):
blocks = zbp_blocks(self.model)
x_pos = self.embed(x, y)
wrong = (y + torch.randint(1, self.n_out, y.shape, device=y.device)) % self.n_out
x_neg = self.embed(x, wrong)
h_pos, h_neg = x_pos, x_neg
total = 0.0
for b in blocks:
hp, hn = self.norm(h_pos).detach(), self.norm(h_neg).detach()
with torch.enable_grad():
op, on = b(hp), b(hn)
gp, gn = op.pow(2).mean(-1), on.pow(2).mean(-1)
loss = (F.softplus(-(gp - self.theta)) + F.softplus(gn - self.theta)).mean()
loss.backward()
total += loss.item()
h_pos, h_neg = op.detach(), on.detach()
return torch.tensor(total / len(blocks))
@torch.no_grad()
def predict(self, x):
scores = []
for c in range(self.n_out):
xc = self.embed(x, torch.full((x.shape[0],), c, device=x.device, dtype=torch.long))
goods = self.goodness_chain(xc)
scores.append(torch.stack(goods[1:] if len(goods) > 1 else goods).sum(0))
return torch.stack(scores, 1).argmax(1)
|