🧠 5. Learn with PC-ALM¶
The module mujou.pc learns a PyTorch model split into nodes with PC-ALM (Augmented Lagrangian Predictive Coding, https://pub.sakana.ai/pc-alm/). Each node holds a part of the model and learns from its neighbors' messages only; at equilibrium, its gradients are those of backpropagation. This page learns a regression with it. What you choose are the time constants, the optimizer of the weights, and how long each batch is shown.
The program¶
batch 1: loss 0.4157 (the mean of the last 1)
batch 50: loss 0.1686 (the mean of the last 50)
...
batch 300: loss 0.0043 (the mean of the last 50)
The chain¶
SHOWN = 0.032 # seconds of the run's timer each batch is shown
# tau.h sets how fast the credit settles (and the period of the nodes, h / 2),
# and the weights learn once per batch, by SGD at the rate
# SHOWN / optimizer.tau = 0.3.
tau = pc.TimeConstants(h=4e-3, lam=2e-3, r=4e-3)
optimizer = pc.optim.SGD(tau=SHOWN / 0.3)
torch.manual_seed(0) # the initial weights, before the run
net = pc.Chain(
nn.Sequential(nn.Linear(2, 64), nn.Tanh()),
nn.Sequential(nn.Linear(64, 64), nn.Tanh()),
nn.Linear(64, 1),
x=TensorSpec((B, 2)),
y=TensorSpec((B, 1)),
batch=B,
loss=pc.loss.MSELoss(B), # the loss of each sample, (B,)
tau=tau,
optimizer=optimizer,
learn_interval=0.75 * SHOWN, # learn once, 3/4 of the way through each batch
)
pc.Chain makes one node for each module and connects them in order, as nn.Sequential computes them: a pc.Input that takes the samples, a pc.Layer for each module but the last (layer0, layer1), and a pc.Output for the last, with the loss. The children of the chain are input, layer0, layer1 and output, which are also their paths in the state; net.layers gives the layers in order, net.layers[0] being layer0.
xandyare the specs of the inputs and targets, andbatchis the shape they start with:Bsamples here. Withoutbatch, the chain takes one sample at a time, andxandywould beTensorSpec((2,))andTensorSpec((1,))lossreturns the loss of each sample, of the shape of the batch:pc.loss.MSELoss(B)(the mean squared error of each sample, averaged over its dimensions asnn.MSELossdoes) orpc.loss.CrossEntropyLoss(B), or a function of your own that returns that shape. Give the loss the same batch as the chain (the batch)tauholds the time constants of the activities and the duals, in seconds of the run's timer:hfor the activities (every node of the chain updates once everytau.period, which istau.h / 2withtau.r = tau.h),lamfor the duals andrfor the decay of the deviations. With these, the credit moves one node back in each period. The time constant of the weights is in the optimizeroptimizermoves the weights when a node learns.pc.optim.SGD(tau=...)is gradient descent with the time constanttauof the weights: a learning step moves the weights by (the time since the node last learned) /tautimes the mean gradient of the batch. Here the nodes learn once per batch, so the learning rate per batch isSHOWN / optimizer.tau= 0.3, whereSHOWNis the time each batch is shown.pc.optim.AdamWis the other choice (Choosing the optimizer). Every node binds the optimizer to its own weights, so onepc.optim.SGDserves the whole chain. It has no defaultlearn_intervalis when the nodes learn, counted from each write: here once per batch, three quarters of the way through it. It has no default:Nonelearns in every update, for a stream whose input changes at every frame, and good values for that mode have not been measured yet
Inside a node, the module is differentiated by autograd, as in BP. Initialize the weights when you make the modules (here from torch.manual_seed in the host, before the run); the nodes do not.
Show and learn¶
torch.set_num_threads(1)
g = torch.Generator().manual_seed(1)
every = torch.ones(B, dtype=torch.bool) # every sample has a target, and is new
losses: list[float] = []
with mujou.run(net) as run:
for i in range(300):
x = 2 * torch.rand(B, 2, generator=g) - 1
y = torch.sin(2 * x[:, :1]) * torch.cos(x[:, 1:])
sample = pc.msg.Sample(x=x, y=y, mask=every, reset=every)
p = pc.show(run, net, sample, seconds=SHOWN)
losses.append(nn.functional.mse_loss(p["y_hat"], y).item()) # before learning
if i == 0 or (i + 1) % 50 == 0:
recent = losses[-50:]
mean = sum(recent) / len(recent)
print(
f"batch {i + 1:3d}: loss {mean:.4f} (the mean of the last {len(recent)})"
)
# The state of a node, by its path: input, layer0, layer1, output.
lam = run.state_dict()["nodes"]["layer0"]["state"]["lam"]
print(f"the mean |lam| of layer0 (its credit) at the end: {lam.abs().mean():.2e}")
pc.show(run, net, sample, seconds=SHOWN) writes the batch, waits for the first prediction the chain makes for it, and returns that prediction once the batch has been shown for SHOWN seconds of the run's timer. Meanwhile the nodes relax towards the target and learn from it. The prediction comes before any node has learned from the batch, so its loss measures how well the model predicts data it has not seen yet (the prequential loss).
masksays which samples have a target. A sample whosemaskis False adds nothing to the loss: with everymaskFalse, no weight moves (the activities still do), which is how to evaluate the same running chain. There is no inference mode to switch toresetsays which samples start anew. Each batch here is drawn afresh, so every sample is new: the nodes drop what they hold of the batch before (Starting anew). For a stream whose frames continue each other, it is False- A
pc.msg.Predictionholdsy_hatwith they,maskandtimeof the sample it was made from, so a prediction always pairs with its own target run.state_dict()["nodes"]["layer0"]["state"]is the state of the first layer: the deviationrof its activity from its prediction, its duallam(the credit of backpropagation at equilibrium), its module, its clock, its optimizer and its relaxer. What it sends, its predictionmu, is in the state of its portnext(How mujou.pc Works)
Next¶
- How the parts connect, and what a node does in one update: How mujou.pc Works
- A 784-256-256-10 network that learns MNIST with
pc.Chainand compares itself with BP:examples/mnist/(uv run python examples/mnist/main.py), explained in PC-ALM - A ResNet-20 that learns CIFAR-10 on the GPU with
pc.Chainand compares itself with BP:examples/cifar10/(uv run python examples/cifar10/main.py), explained in PC-ALM - How to choose the time constants, the optimizer and
learn_interval, the pitfalls, and nodes of your own: PC-ALM