Foundation Models#
A foundation model is one network trained on a family of Hamiltonians at once, instead of one network per Hamiltonian. The network takes the couplings of the Hamiltonian as an input, next to the configuration, so a single training run covers the whole family: the Hubbard model at several values of \(U\), for instance.
Formulation#
The method is that of Rende, Viteritti, Becca, Scardicchio, Laio & Carleo, “Foundation neural-network quantum states as a unified Ansatz for multiple Hamiltonians”, Nature Communications (2025).
The wavefunction \(\psi_\theta(\sigma|\gamma)\) depends on the configuration \(\sigma\) and on the couplings \(\gamma\) of the Hamiltonian \(\hat H_\gamma\). The parameters \(\theta\) minimize the energy averaged over a distribution \(P(\gamma)\) of couplings,
In practice, \(P(\gamma)\) is replaced by \(\mathcal{R}\) values \(\gamma_1, \dots, \gamma_\mathcal{R}\), and each step samples \(M_k = M/\mathcal{R}\) configurations from \(|\psi_\theta(\sigma|\gamma_k)|^2\) for each system \(k\). Each energy has its own normalization \(\langle\psi_\theta(\gamma_k)|\psi_\theta(\gamma_k)\rangle\), so the gradient is a sum of covariances, one per system. With \(O(\sigma,\gamma) = \nabla_\theta \log\psi_\theta(\sigma|\gamma)\) and real parameters,
where \(j\) runs over the samples of system \(k\), and \(\bar E_k\) and \(\bar O_k\) are averages over these samples. Compared with a single Hamiltonian, the only change is that local energies and log-derivatives are centred on the mean of their own system, not of the whole batch. The optimizers do this automatically, for the local energies and for the kernel of stochastic reconfiguration.
What changes in a script#
sample, compute_expectation, the optimizers and the training loop work
unchanged. Three objects change:
The state records the system of each sample.
SpinFoundationStateandFermionFoundationStateare aSpinStateand aFermionStatewith three more fields:system_ids, of shape(N_mc,), the index \(k\) of the system of each sample;system_couplings, of shape(N_mc, n_couplings), its couplings \(\gamma_k\); andn_systems, the number of systems \(\mathcal{R}\).The Hamiltonian is a single operator whose couplings change from sample to sample.
combine_systems(Hs, n_mc_per_system)builds it from a listHsof Hamiltonians made by the same factory, one per system: the firstn_mc_per_systemsamples seeHs[0], the nextn_mc_per_systemseeHs[1], and so on.extract_system_couplings(H)returns the couplings that differ between the systems, as the arraysystem_couplings.The ansatz reads
state.system_couplingsalong with the configuration.SpinFoundationRBMandFermionFoundationRBMareSpinRBMandFermionRBMwith the couplings appended to the input of their first dense layer.
Example: the Hubbard model at four values of \(U\)#
One network for the Hubbard model at \(U = 0, 2, 4, 8\), on the \(4 \times 4\) lattice with 10 electrons, and 256 samples per system:
import jax
import jax.numpy as jnp
from tachys.lattice.lattice_database import square
from tachys.lattice.fermions.fermion_state import init_config_spinful
from tachys.lattice.fermions.hamiltonians.hubbard import hubbard_hamiltonian
from tachys.lattice.foundation.foundation_state import FermionFoundationState
from tachys.lattice.foundation.operators import combine_systems, extract_system_couplings
from tachys.lattice.ansatz.rbm_foundation import FermionFoundationRBM
from tachys.lattice.bond_exchange import BondExchange
from tachys.wavefunction import WaveFunction
from tachys.optimizer import SR
from tachys.montecarlo import sample
from tachys.lattice.operator.local_estimator import compute_expectation
L = 4
lattice = square(shape=(L, L))
N = lattice.Ns
Ne = 10 # the same for every system
Us = [0.0, 2.0, 4.0, 8.0]
n_systems = len(Us)
n_mc_per_system = 256
N_mc = n_systems * n_mc_per_system
# one Hamiltonian per system, from the same factory
Hs = [hubbard_hamiltonian(lattice, nn=[((1, 0), 1.0), ((0, 1), 1.0)], U=U) for U in Us]
H = combine_systems(Hs, n_mc_per_system)
system_couplings = extract_system_couplings(H) # (N_mc, 1): U
system_ids = jnp.repeat(jnp.arange(n_systems), n_mc_per_system) # (N_mc,): 0, ..., 0, 1, ...
key = jax.random.key(0)
key, subkey = jax.random.split(key)
occupations, N_up, N_down = init_config_spinful(subkey, Ns=N, Ne=Ne, N_mc=N_mc)
state = FermionFoundationState(
occupations=occupations, Ne=Ne, lattice=lattice,
system_couplings=system_couplings, system_ids=system_ids, n_systems=n_systems,
)
model = FermionFoundationRBM(hidden_units=64)
key, subkey = jax.random.split(key)
params = model.init(subkey, state)
wf = WaveFunction(params=params, apply_fn=model.apply)
action = BondExchange.create(lattice, Nbands=2)
optimizer = SR(diag_shift=1e-3, mode="real")
opt_state = optimizer.init(wf.params)
N_steps, lr = 100, 0.05
for step in range(N_steps):
key, subkey = jax.random.split(key)
mc_keys = jax.random.split(subkey, N_mc)
state, log_amps, acceptance = sample(1, state, action, mc_keys, wf)
E_L, e_mean, e2_mean = compute_expectation(H, wf, state, log_amps)
updates, opt_state = optimizer(E_L, opt_state, state, wf)
wf = wf.apply_gradients(updates, lr)
if step % 20 == 0:
E_per_system = jnp.real(E_L).reshape(n_systems, n_mc_per_system).mean(axis=1)
print(f"step {step:3d} E/N = {(E_per_system / N).round(4)}")
step 0 E/N = [0.0207 0.2234 0.4364 0.8336]
step 20 E/N = [-1.1259 -1.1267 -1.0346 -0.8497]
step 40 E/N = [-1.4763 -1.319 -1.1918 -0.9727]
step 60 E/N = [-1.495 -1.3339 -1.2153 -1.0136]
step 80 E/N = [-1.4933 -1.3289 -1.2057 -1.0356]
Each line gives the energy per site of the four systems, in the order of Us.
The samples of each system are consecutive, so reshaping E_L separates them;
e_mean, the mean over the whole batch, mixes the four energies. At \(U = 0\)
the energy approaches the exact value for free fermions, \(E/N = -1.5\). The
100 steps take about 20 seconds on the CPU of a laptop (Apple M4 Max).
system_couplings has a single column, holding \(U\):
extract_system_couplings keeps only the couplings that differ between the
systems, and the hopping is the same in all four. A parameter that appears with
two different prefactors gives two columns: for a family of
heisenberg_hamiltonian models at different \(J\), they hold \(J\), the
coefficient of \(S^zS^z\), and \(J/2\), that of the exchange term.