System Identification › Neural System Identification from Trajectories

Michael BrenndoerferJune 9, 202614 min read
World Models HandbookAll chapters
Part 2 of 12Inference, Decisions, and Control
Chapter 9System Identification › Neural System Identification from Trajectories
Reading section 7 of 11 · Neural System Identification from Trajectories · 14 minSection 7 of 11 · 14 min

Book contents

Section 7 of 11 · 14 min read

System Identification › Neural System Identification from TrajectoriesOne section · Sections in this chapter7 / 11 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Everything so far leaned on structure we rarely enjoy: global linearity, low order, scalar outputs. The interesting worlds are nonlinear, high-dimensional, and partially observed. Neural system identification replaces the hand-designed model class with a learned function, typically producing what modern readers would simply call a world model's dynamics module. This equivalence is worth stating bluntly: if you take the classical identification pipeline, swap the linear predictor for a network, the lagged-history feature map for a learned encoder, and the squared one-step loss for a probabilistic likelihood, you have reinvented the core architecture of contemporary world modeling. The main changes are scale, generality of the observation spaces, and engineering discipline around training. The conceptual issues (excitation, compounding, and noise-model specification) carry over, but their classical remedies require adaptation rather than transferring unchanged.

The Standard Recipe

The dominant architecture keeps a recurrent latent state ztz_t instead of the true state xtx_t, and learns three maps jointly from raw trajectories. The division of labor mirrors the classical pipeline exactly, which is why classical vocabulary remains useful for diagnosing failures:

  • Encoder q(zt∣o≤t,u<t)q(z_t \mid o_{\le t}, u_{<t}): compresses the observation-and-action history into a latent summary, playing the role of the belief state from our filtering chapter, but parameterized by a recurrent network or transformer rather than a Bayesian recursion. Where the Bayesian belief tracked an exact posterior distribution, the encoder learns whatever compression suffices to support accurate prediction, which may discard details irrelevant to future outcomes.
  • Transition model z^t+1=f^θ(zt,ut)\hat z_{t+1} = \hat f_\theta(z_t, u_t): the actual dynamics hypothesis, trained to predict its own next latent state (often as a distribution, following the probabilistic styling of our filtering discussion). This component plays precisely the role f⋆f_\star played in our setup section; everything the classical theory said about the transition function's responsibilities applies here.
  • Decoder o^t=g^ϕ(zt)\hat o_t = \hat g_\phi(z_t): ties latents back to measurable observations so the whole thing is supervised by prediction of real data. When the predictive likelihood is expressed through this decoder, latents that do not support accurate observational predictions cannot lower the predictive loss, however tidy their internal dynamics. An explicit reconstruction loss strengthens that grounding but is an optional auxiliary term in the objective below.

At the conceptual level, training targets a one-step or few-step predictive log-likelihood over transitions drawn from the data-collecting policy, such as min⁡θ,ϕ∑t−log⁡p(ot+1∣o≤t,u≤t)\min_{\theta,\phi} \sum_t -\log p(o_{t+1} \mid o_{\le t}, u_{\le t}), optionally with auxiliary terms, reconstruction losses, and regularization. Latent-variable implementations usually optimize a variational bound or sampled approximation rather than this exact marginal likelihood.

where:

  • oto_t: the raw observation received at time tt, whatever the sensors actually stream
  • o≤to_{\le t}: the entire observation history up to and including time tt
  • p(ot+1∣o≤t,u≤t)p(o_{t+1} \mid o_{\le t}, u_{\le t}): the model's predictive distribution over the next observation given past observations and the action history through the latest command
  • θ,ϕ\theta, \phi: trainable parameters of the encoder and transition model (θ\theta) and of the decoder (ϕ\phi). Architectural choices (recurrent networks, gated units, attention, state-space layers like S4/Mamba-style selective SSMs) supply inductive biases about memory and timescale; we survey these in Part III, Chapters 3 and 4, and the full lineage (emergent latent dynamics models such as E2C, PlaNet, Dreamer, and successors) belongs to Part V and Part VIII.

Reading the objective against the classical checklist makes several things visible at once. The loss is one-step (at best few-step), so it grades the model exactly where Figure @fig-rollout-compounding warned us grading is cheapest and decisions demand the most. The noise model is implicitly "outputs are contaminated, dynamics may be treated as flexible," inheriting ARX-style assumptions without anyone declaring them. And nothing in the objective penalizes confidently wrong predictions in regions the data-collecting policy never visited, because no data lives there to score. None of these observations amounts to a flaw in the recipe; each flags a place where additional machinery must be bolted on deliberately rather than inherited for free.

Neural identification also admits hybrid forms closer to classical spirit. Physics-informed and grey-box models hard-code trusted structure, estimate unknown physical parameters, and may learn a residual for whatever the trusted equations omit. Classical operator inference fits reduced linear or polynomial operators by least squares on prescribed lifted features; related Koopman-autoencoder variants learn the lifting itself. Implicit training on multistep rollout errors explicitly buys closed-loop quality at gradient cost. The grey-box strategy is especially attractive in engineering contexts where some physics is trustworthy and some is not: encoding conservation laws removes entire families of physically impossible predictions from consideration, shrinking what the data must teach and protecting the parts of the model that matter most for safety.

Why Neural Identification Is Harder Than It Looks

Four recurring failure surfaces deserve explicit treatment. Three have direct classical analogues, while optimization through long learned recurrences is a modern training issue that interacts with the older questions of excitation and conditioning:

  • Compounding rollout error. Multistep autoregressive prediction feeds each guess back in, so small per-step bias can compound rapidly, especially through unstable modes. Mitigations include multistep objectives, scheduled teacher forcing, and per-step spectral or Lipschitz constraints. These constraints bound local amplification, but the realized rollout rate depends on products of Jacobians—the associated Lyapunov exponents or joint spectral behavior—not on the eigenvalues of any single Jacobian alone.
  • Distribution shift and model exploitation. Data collected under one policy trains a model accurate near that policy's trajectory tube. A planner then queries the model off-tube, where it is silently wrong, acts foolishly, and steers the next data collection somewhere even stranger. A decision procedure can actively seek out the region where model validity lapses, because optimization concentrates wherever errors happen to favor unrealistically optimistic outcomes. Model-based reinforcement learning (Part II, Chapter 6) turns this cycle into an algorithm; the epistemic lesson belongs in every practitioner's head regardless.
  • Similarity-transform ambiguity. Latent coordinates are identified only up to transformations preserving behavior, so latents need not align with interpretable physical quantities. If you followed the linear identifiability discussion, you will recognize this instantly as the structural ambiguity from decades ago, generalized to arbitrary learned encodings, and with the same fundamental status: it reflects what input-output information can determine, not a defect of any particular training run. For planning this is harmless; for scientific modeling it is a reproducibility hazard, since two laboratories fitting the same data can obtain latents differing by an arbitrary reparameterization yet making identical predictions, defeating naive comparisons of learned variables across studies.
  • Optimization pathologies through time. Backpropagating through hundreds of recurrence steps meets vanishing or exploding gradients, while rare events contribute little loss mass when the data under-excites them. Classical excitation discipline helps diagnose the coverage problem, but it does not by itself solve long-horizon optimization. A nominally excellent model can therefore remain blind to exactly the transitions a cautious planner most wants predicted accurately.

Code: When Linearity Runs Out

To make the stakes concrete, we study a forced damped pendulum, smooth and innocent-looking, but governed by sin⁡\sin of position. The pendulum is a useful adversary for a linear predictor on the native variables (θ,ω,u)(\theta,\omega,u): no affine map on those variables can reproduce the sine term exactly over a nondegenerate interval. A lifted linear model containing sin⁡(θ)\sin(\theta) could represent the Euler step, so the limitation belongs to the chosen features rather than to every conceivable linear representation. We fit a linear model and a small feedforward network on identical lagged feature definitions and rollout protocols, using separate independently generated training and validation trajectories.

First the environment and data. The state is (θ,ω)(\theta, \omega): angular position and velocity; the action is applied torque.

In[18]:
Code
import torch
import torch.nn as nn

DT = 0.05


def simulate_pendulum(n_steps, seed, x0=None):
    """Euler-integrated forced pendulum: theta'' = -10 sin(theta) - 0.5 theta' + u."""
    rng_p = np.random.default_rng(seed)
    x = (
        np.array([rng_p.uniform(-np.pi, np.pi), 0.0])
        if x0 is None
        else np.asarray(x0, float)
    )
    actions = 1.5 * np.tanh(rng_p.normal(size=n_steps))
    traj = np.zeros((n_steps + 1, 2))
    traj[0] = x
    for t in range(n_steps):
        th, om = x
        acc = -10.0 * np.sin(th) - 0.5 * om + actions[t]
        x = x + DT * np.array([om, acc])
        traj[t + 1] = x
    return traj, actions


## Training and held-out validation trajectories, generated independently
X_train, U_train = simulate_pendulum(n_steps=1200, seed=42)
X_val, U_val = simulate_pendulum(n_steps=800, seed=7)
Out[19]:
Console
Train transitions: 1200
Validation transitions: 800

The 1,200-transition training record and independently seeded 800-transition validation record give both learners ample supervised signal, and keeping validation in a fresh trajectory ensures the recursive scores below measure generalization rather than memorization. NumPy draws one initial angle for each trajectory from [−π,π)[-\pi, \pi), while tanh smoothly bounds every sampled action inside (−1.5,1.5)(-1.5, 1.5); the lagged-feature construction below therefore produces 1,199 training rows and 799 validation rows. The action transform is a convenient bounded command generator, not a physical actuator model, and one initial draw per trajectory does not by itself establish broad state coverage. The independently generated records provide a modest generalization check within the regimes those two rollouts actually visit.

Now the two models. Both receive the two most recent states plus the two most recent actions, six features, and predict the next state. The linear baseline fits a least-squares map on those features; the network receives the identical rows and targets. Using the same lag window keeps the comparison focused on whether nonlinear function approximation helps rather than quietly giving one model more history.

In[20]:
Code
def make_features(X, U):
    """Rows: [x_t, x_{t-1}, u_t, u_{t-1}], six features -> target x_{t+1}."""
    feats = np.column_stack([X[1:-1], X[:-2], U[1:][:, None], U[:-1][:, None]])
    targets = X[2:]
    return feats, targets


F_train, Y_train_target = make_features(X_train, U_train)
F_val, Y_val_target = make_features(X_val, U_val)

## --- Linear baseline on exactly the same lagged features and targets ---
Theta_lin, *_ = np.linalg.lstsq(F_train, Y_train_target, rcond=None)


## --- Tiny feedforward dynamics network ---

torch.manual_seed(0)
net = nn.Sequential(
    nn.Linear(6, 64), nn.Tanh(), nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, 2)
)
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
loss_fn = nn.MSELoss()
F_tr_t = torch.tensor(F_train, dtype=torch.float32)
Y_tr_t = torch.tensor(Y_train_target, dtype=torch.float32)
F_va_t = torch.tensor(F_val, dtype=torch.float32)
Y_va_t = torch.tensor(Y_val_target, dtype=torch.float32)

train_loss_hist, val_loss_hist = [], []
for epoch in range(400):
    opt.zero_grad()
    loss = loss_fn(net(F_tr_t), Y_tr_t)
    loss.backward()
    opt.step()
    if epoch % 20 == 0 or epoch == 399:
        with torch.no_grad():
            val_loss = loss_fn(net(F_va_t), Y_va_t).item()
        train_loss_hist.append(loss.item())
        val_loss_hist.append(val_loss)

## Precompute final metrics so the output cell only displays results
with torch.no_grad():
    tr_mse = loss_fn(net(F_tr_t), Y_tr_t).item()
    va_mse = loss_fn(net(F_va_t), Y_va_t).item()

lin_train_mse = float(np.mean((F_train @ Theta_lin - Y_train_target) ** 2))
lin_val_mse = float(np.mean((F_val @ Theta_lin - Y_val_target) ** 2))
Out[21]:
Console
One-step MSE, linear on TRAIN:   0.0000
One-step MSE, neural on TRAIN:   0.0028
One-step MSE, linear on VAL:     0.0000
One-step MSE, neural on VAL:     0.0036

Surprisingly, the one-step story here favors the linear baseline: although a linear map on these fixed lagged features cannot express the sine nonlinearity exactly, its best-average approximation has lower squared error than the small network over this training distribution. Both models receive the same six inputs and are evaluated on the same held-out targets, so feature access and targets are controlled. The remaining differences include function class, optimization, and parameterization—the network layers have biases while the least-squares map is linear through the origin—so this is not a pure test of nonlinearity alone. The interesting number is what happens under recursion. We roll both models out from identical validation warm starts using the recorded action sequence; after the two initial states and previous action, each model consumes its own predicted states and no further ground-truth state.

In[22]:
Code
H_max = 40
## Starts index the first of two warm-up states. The exclusive upper bound keeps
## X_val[s + 2 : s + 2 + H_max] and U_val[s + 1 : s + 1 + H_max] in bounds.
starts = np.arange(1, len(X_val) - H_max - 1, 25)


@torch.no_grad()
def rollout_neural(x_tm1, x_t, u_tm1, horizon, actions):
    preds = []
    for k in range(horizon):
        feats = torch.tensor(
            np.concatenate([x_t, x_tm1, [actions[k]], [u_tm1]]),
            dtype=torch.float32,
        )
        x_next = net(feats).numpy()
        preds.append(x_next)
        x_tm1, x_t, u_tm1 = x_t, x_next, actions[k]
    return np.array(preds)


def rollout_linear(x_tm1, x_t, u_tm1, horizon, actions):
    preds = []
    for k in range(horizon):
        feats = np.concatenate([x_t, x_tm1, [actions[k]], [u_tm1]])
        x_next = feats @ Theta_lin
        preds.append(x_next)
        x_tm1, x_t, u_tm1 = x_t, x_next, actions[k]
    return np.array(preds)


neural_curve = np.zeros(H_max)
linear_curve = np.zeros(H_max)
for s in starts:
    u_seg = U_val[s + 1 : s + 1 + H_max]
    # Neural rollout: needs x_{t-1} and u_{t-1} from ground truth to initialize
    preds_n = rollout_neural(X_val[s], X_val[s + 1], U_val[s], H_max, u_seg)
    truths = X_val[s + 2 : s + 2 + H_max]
    neural_curve += np.mean((preds_n - truths) ** 2, axis=1)
    preds_l = rollout_linear(X_val[s], X_val[s + 1], U_val[s], H_max, u_seg)
    linear_curve += np.mean((preds_l - truths) ** 2, axis=1)

neural_curve /= len(starts)
linear_curve /= len(starts)
h_axis = np.arange(1, H_max + 1)
Out[23]:
Console
Rollout starts: 31 overlapping starts, stride 25, seed 7
Horizon  1: linear MSE=7.27234e-06, neural MSE=0.00433636
Horizon 10: linear MSE=0.00685831, neural MSE=0.140683
Horizon 20: linear MSE=0.0170298, neural MSE=0.452295
Horizon 40: linear MSE=0.0881307, neural MSE=0.904876

Now inspect the rollout figure, rendered purely from the curves just computed.

Out[24]:
Visualization
Log-scale line chart of squared rollout error versus horizon; in this seeded run both curves rise and the linear curve remains below the neural curve.
Recursive rollout error on a forced pendulum: a least-squares linear model versus a small feedforward network trained on the same controls-rich trajectory. Both curves rise with horizon, and in this run the fitted network does not dominate the linear baseline within the tested window.

The one-step bar chart below isolates the cheapest grading protocol—held-out validation transitions restarted from truth—and complements the recursive rollout comparison above.

In[25]:
Code
@torch.no_grad()
def neural_onestep_mse():
    errs = []
    for t in range(1, len(X_val) - 1):
        feats = torch.tensor(
            np.concatenate(
                [X_val[t], X_val[t - 1], [U_val[t]], [U_val[t - 1]]]
            ),
            dtype=torch.float32,
        )
        pred = net(feats).numpy()
        errs.append(np.mean((pred - X_val[t + 1]) ** 2))
    return float(np.mean(errs))


nn_val_mse = neural_onestep_mse()
model_names = ["Linear", "Neural"]
onestep_bars = [lin_val_mse, nn_val_mse]
Out[26]:
Visualization
Bar chart comparing one-step validation error of linear and neural models.
One-step squared prediction error on held-out validation transitions for the linear and neural pendulum models. Even without recursion the linear baseline edges out the small network here; neither number certifies closed-loop quality, and this averaged on-distribution score does not expose how either model behaves outside the tested trajectory regime.

In this seeded run both curves increase overall with horizon, and the structure-blind linear regression stays ahead across all tested horizons. That run-specific outcome illustrates how a misspecified but well-conditioned estimator can beat a more expressive network trained only on a modest one-step dataset. The logarithmic axis makes multiplicative differences readable without implying that it magnifies them; the printed and plotted values determine the size of the gap. The oscillatory mismatch likely includes phase error, but this experiment does not decompose the discrepancy by cause. Notice too what the exercise did not require: reward signals, interaction, or a planner. Neural identification here is pure supervised sequence learning. The gap between "good sequence model" and "good basis for deciding" is precisely the space of the coming chapters: Classical Planning and Optimal Control will consume models like these, and Model-Based Reinforcement Learning will interrogate them adversarially.