Paper · 2018-12-02 · cs.LG, stat.ML
Efficient Lifelong Learning with A-GEM
A-GEM replaces GEM's per-task gradient constraints with one constraint on the average episodic-memory gradient: if the current gradient g has negative dot product with a reference gradient g_ref from a random memory batch, project g onto g_ref's orthogonal plane. Matches GEM accuracy (62.3% vs 61.2% on Split CIFAR) at roughly 10x less training time.
Continual learningGradient projectionZero-shot learningcontinual-learninglifelong-learningcatastrophic-forgettingepisodic-memorygradient-projectionzero-shot
arXivPDFCodeMarkdown13 min read
AI-compiled by claude-opus-5-5 on 2026-10-18, not yet reviewed by a human. Every claim cites its source; check it when a detail matters.
In plain words
Train a neural network on task A, then on task B, and it usually forgets much of task A. This is called catastrophic forgetting, and it is the central problem of lifelong (or continual) learning, where a model sees a sequence of tasks one after another and each training example only once.
An earlier method, GEM, keeps a small notebook of examples from every past task. Before each update it checks, task by task, whether the update would make any past task worse, and if so it solves a small optimization problem to bend the update until no past task is hurt. That works, but it gets slow and memory-hungry as the number of tasks grows.
A-GEM simplifies the check. Instead of asking every past task separately, it grabs a random handful of notebook examples from all past tasks, computes one "don't make this worse" direction from them, and only intervenes if the new update points against it. The fix is a one-line projection: remove the part of the update that conflicts with that direction. The authors report the same or better average accuracy than GEM at a fraction of its time and memory, on par with simple regularization methods such as EWC.
The paper also argues for a stricter way to evaluate such methods (one pass over the data, hyperparameters tuned only on a few held-out tasks), adds a metric for how fast a model picks up a new task, and shows that describing each task by the attributes of its classes (beak shape, fur color and so on) helps every method learn new tasks faster.
TL;DR
A-GEM is a cheaper variant of Gradient Episodic Memory for single-pass continual learning. At each step it computes the gradient g on the current mini-batch and a reference gradient g_ref on a random batch drawn from the episodic memory of all past tasks; if gᵀg_ref < 0 it replaces g by g − (gᵀg_ref / g_refᵀg_ref) g_ref, otherwise it uses g unchanged §4 eq. 11. No quadratic program and no per-task gradient matrix are needed. On Permuted MNIST, Split CIFAR, Split CUB and Split AWA it matches or beats GEM in average accuracy and beats EWC, PI, MAS and RWalk, with training time close to EWC tab. 4 tab. 7. The paper also proposes a protocol (tune on 3 tasks, train and report on 17 in one pass), a Learning Curve Area (LCA) metric and an attribute-based joint-embedding model §2 §3 §5.
Problem
Most lifelong-learning work trains with several epochs per task and tunes hyperparameters by sweeping the whole task sequence repeatedly, which leaks the evaluation stream into model selection §2. Under a single pass, the regularization methods (EWC and relatives) barely beat plain fine-tuning §6.1 app. F. GEM works in the single-pass setting but needs, at every step, the gradients of all t−1 past tasks (a (t−1)×P matrix) and a QP solve; this becomes prohibitive as memory and task count grow §4 tab. 7.
Method
Setting §2. Tasks k = 1..T, each a dataset of triplets (x, t, y) with input x, task descriptor t and label y from a task-specific label set. The predictor is f_θ(x, t). Training examples of the evaluation stream are seen exactly once.
GEM recap §4. GEM stores an episodic memory M_k per task and solves, at task t:
\text{minimize}_\theta\ \ell(f_\theta, \mathcal{D}_t)\quad \text{s.t.}\quad \ell(f_\theta, \mathcal{M}_k) \le \ell(f_\theta^{t-1}, \mathcal{M}_k)\ \ \forall k<t eq. 6
In gradient form this becomes a projection with t−1 constraints:
\text{minimize}_{\tilde g}\ \tfrac12\lVert g-\tilde g\rVert_2^2\quad \text{s.t.}\quad \langle \tilde g, g_k\rangle \ge 0\ \ \forall k<t eq. 7
solved via its dual, a QP in t−1 variables:
\text{minimize}_v\ \tfrac12 v^\top G G^\top v + g^\top G^\top v \quad \text{s.t.}\quad v \ge 0 eq. 8
with G = −(g_1, …, g_{t−1}) ∈ ℝ^{(t−1)×P} and g̃ = Gᵀv* + g §4.
A-GEM §4. Constrain only the average memory loss over all past tasks:
\text{minimize}_\theta\ \ell(f_\theta, \mathcal{D}_t)\quad \text{s.t.}\quad \ell(f_\theta, \mathcal{M}) \le \ell(f_\theta^{t-1}, \mathcal{M}),\ \ \mathcal{M} = \cup_{k<t}\mathcal{M}_k eq. 9
\text{minimize}_{\tilde g}\ \tfrac12\lVert g-\tilde g\rVert_2^2\quad \text{s.t.}\quad \tilde g^\top g_{ref} \ge 0 eq. 10
where g_ref is the gradient on a batch (x_ref, y_ref) sampled at random from M. The closed-form solution when the constraint is violated:
\tilde g = g - \frac{g^\top g_{ref}}{g_{ref}^\top g_{ref}}\, g_{ref} eq. 11
The derivation (Lagrangian with multiplier α, z* = g + α g_ref, α* = −gᵀg_ref / g_refᵀg_ref) is in app. C.
Algorithm: A-GEM training [alg. 2]
Input: model f_θ, task streams D_1..D_T (train/test), learning rate α
1. M ← {} ; A ← T×T zero matrix
2. for t = 1..T:
3. for each mini-batch (x, y) in D_t^train:
4. sample (x_ref, y_ref) from M
5. g_ref ← ∇_θ ℓ(f_θ(x_ref, t), y_ref)
6. g ← ∇_θ ℓ(f_θ(x, t), y)
7. if gᵀ g_ref ≥ 0: g̃ ← g
8. else: g̃ ← g − (gᵀ g_ref / g_refᵀ g_ref) · g_ref
9. θ ← θ − α · g̃
10. M ← UpdateEpsMem(M, D_t^train, T) # add samples of task t to memory
11. A[t, :] ← accuracy on every task's test set
Output: f_θ, AMemory is populated per task with samples chosen uniformly at random §6. The code's actual procedure (see Paper vs code) differs in details.
Evaluation protocol §2 alg. 1. Split the task stream into D^CV (first T^CV tasks) and D^EV (remaining tasks); T^CV = 3, T = 20. Grid search hyperparameters with as many passes over D^CV as needed, pick h* by average accuracy (eq. 1) on D^CV's test sets, reset the model, then train once, in a single pass, over D^EV (17 tasks) and report metrics on D^EV.
Metrics §3. Let a_{k,i,j} be test accuracy on task j after training on the i-th mini-batch of task k, B_k the number of mini-batches of task k.
A_k = \frac1k \sum_{j=1}^{k} a_{k,B_k,j} eq. 1
F_k = \frac{1}{k-1}\sum_{j=1}^{k-1} f_j^k,\qquad f_j^k = \max_{l\in\{1,\dots,k-1\}} a_{l,B_l,j} - a_{k,B_k,j} eq. 2 eq. 3
Z_b = \frac1T \sum_{k=1}^{T} a_{k,b,k},\qquad \text{LCA}_\beta = \frac{1}{\beta+1}\sum_{b=0}^{\beta} Z_b eq. 4 eq. 5
LCA_0 is average 0-shot accuracy on a new task; LCA_β is the area under the early learning curve, high when 0-shot accuracy is good and learning is fast §3. Results use β = 10 fig. 1.
Joint embedding with task descriptors (the "-JE" models) §5. The task descriptor t^k is a C_k × A matrix of class attribute vectors. A feature extractor φ_θ(x) ∈ ℝ^D and an attribute embedding ψ_ω, an A × D parameter matrix, so ψ_ω(t^k) ∈ ℝ^{C_k×D} (each class embedding is a linear combination of attribute vectors) §5 fig. 9. Train with cross-entropy:
\ell_k(\theta,\omega) = \frac1N \sum_{i=1}^{N} -\log p(y_i^k \mid x_i^k, t^k;\theta,\omega) eq. 12
p(c \mid x_i^k, t^k;\theta,\omega) = \frac{\exp([\phi_\theta(x_i^k)\,\psi_\omega(t^k)^\top]_c)}{\sum_j \exp([\phi_\theta(x_i^k)\,\psi_\omega(t^k)^\top]_j)} eq. 13
This is orthogonal to A-GEM and is applied to every baseline §5. Integer task descriptors are used for Permuted MNIST and Split CIFAR; attribute descriptors for Split CUB and Split AWA §6.
Hyperparameters
| Name | Value | Where | Notes |
|---|---|---|---|
| optimizer | SGD | §6 replicate_results_iclr19.sh | no momentum in replication script |
| mini-batch size | 10 | §6 replicate_results_iclr19.sh | all methods, all datasets |
| epochs per task | 1 (single pass) | §2 §6 | |
| T, T^CV | 20, 3 | §2 app. G | metrics on the remaining 17 tasks |
| episodic memory per task | 250 / 65 / 50 / 100 (MNIST / CIFAR / CUB / AWA) | §6 | same for iCaRL, GEM, A-GEM; code: 25/class × 10, 13/class × 5, 5/class × 10, 20/class × 5 fc_permute_mnist.py conv_split_cifar.py conv_split_cub.py conv_split_awa.py |
| g_ref batch size | 256 / 1300 / 128 / 128 (MNIST / CIFAR / CUB / AWA) | §6 | "when the episodic memory is sufficiently filled"; code CIFAR default is 256, see Paper vs code |
| memory sampling | uniformly at random per task | §6 | code uses a per-class ring buffer |
| A-GEM learning rate | 0.1 MNIST, 0.03 CIFAR, 0.03 CUB, 0.01 AWA | app. G replicate_results_iclr19.sh replicate_results_iclr19.sh conv_split_cub.py conv_split_awa.py | grid [0.3, 0.1, 0.03, 0.01, 0.003, 0.001, 0.0003, 0.0001] |
| A-GEM-JE learning rate | 0.03 CUB, 0.003 AWA | app. G | same grid |
| Permuted MNIST network | MLP, 2 hidden layers × 256 ReLU | §6 | |
| Split CIFAR network | reduced ResNet18 | §6 | "3 times less feature maps" app. F; code filters [20, 20, 40, 80, 160] model.py |
| Split CUB / AWA network | standard ResNet18 | §6 | CUB starts from an ImageNet-pretrained model; MNIST, CIFAR, AWA from random init §6 |
| runs | 5 (MNIST, CIFAR), 10 (CUB, AWA) | fig. 1 fig. 2 tab. 4 tab. 5 tab. 6 | different seed each run |
| weight decay, momentum, LR schedule | not stated | replication script uses plain SGD with a constant LR |
Baseline settings chosen by the same grid search app. G:
| Method | MNIST lr / λ | CIFAR lr / λ | CUB lr / λ | AWA lr / λ |
|---|---|---|---|---|
| VAN | 0.03 / – | 0.01 / – | 0.03 / – | 0.001 / – |
| EWC | 0.03 / 10 | 0.03 / 10 | 0.03 / 1 | 0.003 / 100 |
| PI | 0.1 / 0.1 | 0.01 / 0.1 | 0.03 / 0.1 | 0.003 / 1 |
| MAS | 0.1 / 0.1 | 0.03 / 0.1 | 0.03 / 0.1 | 0.003 / 1 |
| RWALK | 0.1 / 1 | 0.03 / 1 | 0.03 / 1 | 0.003 / 10 |
| PROG-NN | 0.1 / – | 0.03 / – | 0.01 / – | 0.03 / – |
| MULTI-TASK | 0.03 / – | 0.03 / – | 0.03 / – | 0.03 / – |
| A-GEM | 0.1 / – | 0.03 / – | 0.03 / – | 0.01 / – |
Grids: EWC λ ∈ [1, 10, 100, 1000, 10000]; PI λ ∈ [0.001, 0.01, 0.1, 1, 10]; MAS λ ∈ [0.01, 0.1, 1, 10]; RWALK λ ∈ [0.1, 1, 10, 100, 1000] app. G. The -JE variants have their own optima, listed in app. G.
Dataset statistics tab. 1:
| Perm. MNIST | Split CIFAR | Split CUB | Split AWA | |
|---|---|---|---|---|
| num. of tasks | 20 | 20 | 20 | 20 |
| input size | 1×28×28 | 3×32×32 | 3×224×224 | 3×224×224 |
| num. of classes per task | 10 | 5 | 10 | 5 |
| num. of training images per task | 60000 | 2500 | 300 | - |
| num. of test images per task | 10000 | 500 | 290 | 560 |
Results
Author-reported. Single pass over 17 evaluation tasks, mini-batch 10, means (± confidence interval) over 5 runs for MNIST/CIFAR and 10 runs for CUB/AWA. A_T in %, F_T and LCA_10 in [0, 1] §6.1.
Permuted MNIST and Split CIFAR tab. 4:
| Method | MNIST A_T | MNIST F_T | MNIST LCA_10 | CIFAR A_T | CIFAR F_T | CIFAR LCA_10 |
|---|---|---|---|---|---|---|
| VAN | 47.9 (±1.32) | 0.51 (±0.01) | 0.26 (±0.006) | 42.9 (±2.07) | 0.25 (±0.03) | 0.30 (±0.008) |
| iCaRL | - | - | - | 50.1 | 0.11 | - |
| EWC | 68.3 (±0.69) | 0.29 (±0.01) | 0.27 (±0.003) | 42.4 (±3.02) | 0.26 (±0.02) | 0.33 (±0.01) |
| PI | ∞ | ∞ | ∞ | 47.1 (±4.41) | 0.17 (±0.04) | 0.31 (±0.008) |
| MAS | 69.6 (±0.93) | 0.27 (±0.01) | 0.29 (±0.003) | 44.2 (±2.39) | 0.25 (±0.02) | 0.33 (±0.009) |
| RWALK | 85.7 (±0.56) | 0.08 (±0.01) | 0.31 (±0.005) | 40.9 (±3.97) | 0.29 (±0.04) | 0.32 (±0.005) |
| PROG-NN | 93.5 (±0.07) | 0 | 0.19 (±0.006) | 59.2 (±0.85) | 0 | 0.21 (±0.001) |
| GEM | 89.5 (±0.48) | 0.06 (±0.004) | 0.23 (±0.005) | 61.2 (±0.78) | 0.06 (±0.007) | 0.36 (±0.007) |
| A-GEM | 89.1 (±0.14) | 0.06 (±0.001) | 0.29 (±0.004) | 62.3 (±1.24) | 0.07 (±0.01) | 0.35 (±0.01) |
| MULTI-TASK | 95.3 | - | - | 68.3 | - | - |
∞ means the model failed to train with the hyperparameters cross-validated on the first 3 tasks tab. 4.
Split CUB tab. 5 and Split AWA tab. 6, standard / joint-embedding (-JE):
| Method | CUB A_T | CUB F_T | CUB LCA_10 | AWA A_T | AWA F_T | AWA LCA_10 |
|---|---|---|---|---|---|---|
| VAN | 54.3 (±2.03) / 67.1 (±4.77) | 0.13 (±0.02) / 0.10 (±0.04) | 0.29 (±0.009) / 0.52 (±0.01) | 30.3 (±2.84) / 42.8 (±2.86) | 0.04 (±0.01) / 0.07 (±0.02) | 0.21 (±0.008) / 0.37 (±0.02) |
| EWC | 54 (±1.08) / 68.4 (±4.08) | 0.13 (±0.02) / 0.09 (±0.03) | 0.29 (±0.007) / 0.52 (±0.01) | 33.9 (±2.87) / 43.3 (±3.71) | 0.08 (±0.02) / 0.07 (±0.03) | 0.26 (±0.01) / 0.37 (±0.02) |
| PI | 55.3 (±2.28) / 66.6 (±5.18) | 0.12 (±0.02) / 0.10 (±0.04) | 0.29 (±0.008) / 0.52 (±0.01) | 33.9 (±3.25) / 43.4 (±3.49) | 0.08 (±0.02) / 0.06 (±0.02) | 0.26 (±0.01) / 0.37 (±0.02) |
| RWALK | 54.4 (±1.82) / 67.4 (±3.50) | 0.13 (±0.01) / 0.10 (±0.03) | 0.29 (±0.008) / 0.52 (±0.01) | 33.9 (±2.91) / 42.9 (±3.10) | 0.08 (±0.02) / 0.07 (±0.02) | 0.26 (±0.01) / 0.37 (±0.02) |
| PROG-NN | OoM / OoM | OoM / OoM | OoM / OoM | OoM / OoM | OoM / OoM | OoM / OoM |
| A-GEM | 62 (±3.5) / 71 (±2.83) | 0.07 (±0.02) / 0.07 (±0.01) | 0.30 (±0.008) / 0.54 (±0.02) | 44 (±4.10) / 50 (±3.25) | 0.05 (±0.02) / 0.03 (±0.02) | 0.29 (±0.01) / 0.39 (±0.02) |
| MULTI-TASK | 65.6 / 73.8 | - / - | - / - | 64.8 / 66.8 | - / - | - / - |
Training time and memory tab. 7 (P parameters, B mini-batch size, H hidden state size, M episodic memory per task, T tasks; time is GPU training time):
| Method | MNIST [s] | CIFAR [s] | CUB [s] | AWA [s] | Train memory | Test memory |
|---|---|---|---|---|---|---|
| VAN | 186 | 105 | 54 | 4123 | P + B·H | P + B·H |
| EWC | 403 | 250 | 72 | 4136 | 4·P + B·H | P + B·H |
| PROG-NN | 510 | 409 | ∞ | ∞ | 2·P·T + B·H·T | 2·P·T + B·H·T |
| GEM | 3442 | 5238 | - | - | P·T + (B+M)·H | P + B·H |
| A-GEM | 477 | 449 | 420 | 5221 | 2·P + (B+M)·H | P + B·H |
GEM vs its variants tab. 3 (S-GEM projects onto one randomly chosen past task constraint per step app. D.3):
| Method | MNIST A_T | MNIST F_T | CIFAR A_T | CIFAR F_T |
|---|---|---|---|---|
| GEM | 89.5 | 0.06 | 61.2 | 0.06 |
| S-GEM | 88.2 | 0.08 | 56.2 | 0.12 |
| A-GEM | 89.1 | 0.06 | 62.3 | 0.07 |
Average accuracy vs worst-case forgetting F_wst on the memory and on the test set tab. 2:
| Method | MNIST M A_T | MNIST M F_wst | MNIST test A_T | MNIST test F_wst | CIFAR M A_T | CIFAR M F_wst | CIFAR test A_T | CIFAR test F_wst |
|---|---|---|---|---|---|---|---|---|
| GEM | 99.5 | 0 | 89.5 | 0.10 | 97.1 | 0.05 | 61.2 | 0.14 |
| A-GEM | 99.3 | 0.008 | 89.1 | 0.13 | 72.1 | 0.15 | 62.3 | 0.15 |
Other findings:
- A-GEM has the best A_T on all datasets except Permuted MNIST, where PROG-NN wins thanks to the large number of examples per task; PROG-NN runs out of memory on CUB and AWA §6.1.
- GEM violates its constraints at almost every step as tasks accumulate, while A-GEM's violation count plateaus much lower app. D.1 fig. 6.
- EWC only pulls ahead of VAN with a bigger network and multiple epochs (up to 10 on MNIST, 30 on CIFAR) app. F fig. 7 fig. 8.
- Attribute task descriptors raise A_T and LCA for every method; zero-shot accuracy of the -JE models improves as more tasks are learned, most for A-GEM-JE §6.1 fig. 4 fig. 5.
Paper vs code
Checked against facebookresearch/agem at 4542149.
- g_ref batch size on CIFAR. Paper: 1300 §6. Code default is 256
conv_split_cifar.py and the replication script does not
override
--eps-mem-batchreplicate_results_iclr19.sh. MNIST (256), CUB (128) and AWA (128) match fc_permute_mnist.py conv_split_cub.py conv_split_awa.py. - Memory population. Paper: samples chosen uniformly at random per task §6; Alg. 2 adds |M|/T samples after each task alg. 2. Code: while training on a task, each incoming example is written into a fixed-size ring buffer slot for its class (mem_size slots per class), so the memory holds the last mem_size examples per class of the shuffled stream; the slots of task t become visible to g_ref sampling only after task t ends conv_split_cifar.py conv_split_cifar.py fc_permute_mnist.py. Because data is shuffled first conv_split_cifar.py, this is a random subset in effect.
- Small memory. When the filled memory has at most
eps_mem_batchexamples, the code uses all of it for g_ref; otherwise it samples without replacement conv_split_cifar.py. The paper only says the stated batch sizes apply "when the episodic memory is sufficiently filled" §6. - First task. Alg. 2 samples from an empty memory on task 1 alg. 2; the code uses an ordinary SGD step on the first task conv_split_cifar.py model.py.
- Multi-head loss on memory. In split (multi-head) setups, each memory example's cross-entropy is computed only over the logits of its own task's classes; per-task losses are summed and divided by the batch size model.py model.py conv_split_cifar.py. The paper writes f_θ(x_ref, t) with the current task id t alg. 2, which would be wrong for multi-head outputs.
- Joint-embedding bias. The code adds a learned per-class bias to the logits φ(x)ψ(t)ᵀ model.py; eq. 13 has no bias eq. 13.
- Projection. Matches eq. 11; the projection triggers only when the dot product is strictly negative, with no epsilon in the denominator model.py.
- Learning rates for A-GEM match app. G: MNIST 0.1 and CIFAR 0.03 come from the replication script, CUB 0.03 and AWA 0.01 are hard-coded replicate_results_iclr19.sh conv_split_cub.py conv_split_awa.py.
Pitfalls
- Use the memory gradient with the right heads. For multi-head tasks, g_ref must be computed with each memory example routed to its own task's output classes, not to the current task's head (see Paper vs code).
- Speed-up claim vs table. The text says A-GEM is "about 100 times faster" than GEM §6.1, but tab. 7 gives 3442 s vs 477 s on MNIST and 5238 s vs 449 s on CIFAR, i.e. roughly 7× and 12×. Expect the table's numbers, not the text's.
- Not truly free. Each step costs two backward passes (one on the current batch, one on a memory batch of 128 to 1300 examples), versus one for plain SGD; A-GEM takes about 2.6× (MNIST) and 4.3× (CIFAR) the training time of VAN tab. 7. Training memory holds an extra P-sized gradient vector (2·P vs P) tab. 7.
- Projection only removes conflict on average. A-GEM bounds the average memory loss, not each task's loss. GEM has lower worst-case forgetting on the memory itself (e.g. CIFAR memory accuracy 97.1% vs 72.1%) tab. 2 §4. If per-task guarantees matter, A-GEM is weaker.
- g_refᵀg_ref near zero. Eq. 11 divides by ‖g_ref‖²; neither paper nor code guards against a near-zero reference gradient model.py.
- Alg. 2's UpdateEpsMem is underspecified. It sets s = |M|/T and adds s samples alg. 2, which is zero for an empty memory; follow the per-task budgets in §6 (or the per-class slots in the code) instead.
- Protocol matters. Hyperparameters are tuned on the first 3 tasks only, and results are on the other 17 §2. PI fails to train on MNIST under this protocol tab. 4. Results tuned on the full stream are not comparable.
- Split AWA reuses classes. Tasks sample classes with replacement, each class's training data is split into disjoint parts per occurrence, and the class weights are re-initialized in each head §6. Split AWA's training images per task are not given tab. 1.
- Permuted MNIST step count. app. D.1 states 5500 gradient steps per task at batch 10 (55000 examples), while tab. 1 lists 60000 training images per task; how the remainder is used is not stated.
- LCA indexing. Z_b starts at b = 0, the accuracy before the first mini-batch of the task (0-shot) eq. 4 eq. 5; the code evaluates after every mini-batch for the first 20 iterations, then every 50 conv_split_cifar.py.
- Weight decay, momentum and LR schedule are not stated; the replication script uses plain SGD at a constant rate.
Lineage
- Builds on: GEM (Lopez-Paz & Ranzato, 2017, arXiv:1706.08840): same episodic memory and gradient-projection idea, but t−1 per-task constraints and a QP are replaced by one averaged constraint with a closed-form projection §4.
- Compared against: EWC (arXiv:1612.00796), PI, MAS, RWalk, iCaRL, Progressive Networks, GEM, and a multi-task upper bound §6.
- Metrics A and F come from RWalk (Chaudhry et al., 2018); LCA is new here §3.
Source and license
Adapted from “Efficient Lifelong Learning with A-GEM” by Arslan Chaudhry, Marc'Aurelio Ranzato, Marcus Rohrbach, Mohamed Elhoseiny (arXiv:1812.00420), used under CC BY 4.0. Changes: condensed and restructured for implementation, with tables, equations and algorithms taken from the paper and its code, each cited to its source. Not affiliated with or endorsed by the authors. Code citations refer to https://github.com/facebookresearch/agem at commit 45421499483b (MIT). Any quoted code is shown under that license.