Skip to content

When initialization decides the outcome

Fits sin(3x)cos(3y) with a SiLU MLP at two depths, with and without AnyInit, and prints the activation levels next to the losses so the mechanism is visible rather than asserted. Initialization is decisive when signal propagation is the binding constraint, and the margin shrinks as that stops being true.

python examples/depth_matters.py
"""Where correcting the initialization changes the outcome, and where it does not.

Fits sin(3x)cos(3y) with a SiLU MLP at two depths and reports both, with the activation
levels alongside so the mechanism is visible rather than asserted.  Initialization is
decisive when signal propagation is the binding constraint, and the margin shrinks as that
stops being true.

Run: python examples/depth_matters.py
"""

from __future__ import annotations

import torch
import torch.nn as nn

import anyinit

WIDTH, STEPS, LEARNING_RATE = 128, 400, 1e-3


def build(depth: int) -> nn.Module:
    layers: list[nn.Module] = [nn.Linear(2, WIDTH), nn.SiLU()]
    for _ in range(depth - 1):
        layers += [nn.Linear(WIDTH, WIDTH), nn.SiLU()]
    layers.append(nn.Linear(WIDTH, 1))
    return nn.Sequential(*layers)


def target(points: torch.Tensor) -> torch.Tensor:
    return torch.sin(3 * points[:, :1]) * torch.cos(3 * points[:, 1:])


def sample_points(count: int = 2048) -> torch.Tensor:
    return torch.rand(count, 2) * 4 - 2


def train(depth: int, use_anyinit: bool) -> float:
    torch.manual_seed(0)
    model = build(depth)
    points = sample_points()
    if use_anyinit:
        # The real batch, not just its shape: these points are uniform on [-2, 2], so
        # their second moment is 4/3 and assuming a standard normal would misscale the
        # whole network.
        anyinit.initialize(model, input_spec=points, seed=0)

    optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
    values = target(points)
    for _ in range(STEPS):
        optimizer.zero_grad()
        loss = (model(points) - values).pow(2).mean()
        loss.backward()
        optimizer.step()
    return float(loss.detach())


def activation_levels(depth: int, use_anyinit: bool) -> tuple[float, float]:
    """Second moment after the first and last activation."""
    torch.manual_seed(0)
    model = build(depth)
    points = sample_points()
    if use_anyinit:
        anyinit.initialize(model, input_spec=points, seed=0)

    hidden = points
    levels = []
    with torch.no_grad():
        for layer in model:
            hidden = layer(hidden)
            if isinstance(layer, nn.SiLU):
                levels.append(float(hidden.pow(2).mean()))
    return levels[0], levels[-1]


HEADER = " depth       init  E[a^2] first  E[a^2] last  final loss"


if __name__ == "__main__":
    print(HEADER)
    for depth in (10, 30):
        for use_anyinit in (False, True):
            first, last = activation_levels(depth, use_anyinit)
            loss = train(depth, use_anyinit)
            label = "AnyInit" if use_anyinit else "PyTorch"
            print(f"{depth:6d} {label:>10s} {first:13.4f} {last:12.3e} {loss:11.5f}")

    print(
        "\nAt depth 30 the stock initialization lets the signal decay to 8e-04 by the last\n"
        "layer and the network barely moves -- a loss of 0.247 is the variance of the\n"
        "target, so it has learned nothing.  At depth 10 the signal survives either way\n"
        "and the margin is smaller.  The gain is in making the network trainable, and it\n"
        "grows with depth because that is when signal propagation starts to bind."
    )