Grow a Target From One Seed

Page content

Cellular Automata From First Principles 39: Grow a Target From One Seed

Now we can pose the central morphogenesis problem:

Start from one cell and learn local rules that construct a target pattern.

Prepare a target

Assume an RGBA image has been loaded into a tensor:

# shape: [1, 4, H, W], values in [0, 1]
target = load_target("target.png").to(DEVICE)

The NCA state contains more channels than the image, so only the visible channels are compared:

def target_loss(x, target):
    return F.mse_loss(x[:, :4], target)

Randomize rollout length

Training at one exact step count encourages brittle timing tricks.

Instead sample a horizon:

steps = torch.randint(64, 97, ()).item()
x = make_seed(size=target.shape[-1], channels=16)

for _ in range(steps):
    x = model(x)

Now the model must approach a useful region of state space across a range of times.

Train the local rule

optimizer = torch.optim.Adam(model.parameters(), lr=2e-3)

for iteration in range(8000):
    x = make_seed(size=target.shape[-1], channels=16)
    steps = torch.randint(64, 97, ()).item()

    for _ in range(steps):
        x = model(x)

    loss = target_loss(x, target)

    optimizer.zero_grad()
    loss.backward()
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    optimizer.step()

Why growth from one seed is difficult

Every cell executes the same local rule, yet different regions must eventually play different roles.

The system has to create its own positional information through local interactions.

That is the real problem:

identical rule
+ local communication
+ recurrent hidden state
spatial differentiation

Do not judge only the final frame

Record the entire trajectory:

frames = []
x = make_seed()

for step in range(128):
    x = model(x)
    if step % 4 == 0:
        frames.append(x[:, :4].detach().cpu())

Later, the visual pass for this book should turn this trajectory into an animation. The process of becoming is more informative than the final image.

A low final loss can still hide a bad dynamical system

Check what happens after the training horizon:

for _ in range(500):
    x = model(x)

Does the organism:

persist?
explode?
decay?
drift?
keep growing?

A model that reaches the target and then destroys it has learned growth, not homeostasis.

Separate growth from persistence

This distinction gives us three different capabilities:

growing      = reach the target
persistent   = remain near the target
regenerating = return after damage

They should be tested separately.

Further reading: Growing Neural Cellular Automata.

Before training persistence and regeneration, however, we need to remove another unrealistic assumption: that every cell updates at exactly the same instant.

In the next chapter we randomize the update schedule.