Repository navigation
Description
Codebase summary
This is Facebook’s Stack-RNN C++ project (Joulin & Mikolov, 2015). It is mostly header-only and trains stack-augmented RNNs on algorithmic sequence tasks.
| Component | Role |
|---|---|
common.h | my_int / my_real types |
Vec.h | Dense vectors / matrices (Vec, Vec2D) |
Linear.h | Affine layers with forward/backward/gradients |
Nonlinearity.h | Sigmoid, Softmax |
utils.h | matrixXvector, clipping, RNG |
StackRNN.h | Full model: stacks, BPTT, save/load |
task.h | Toy tasks + binary addition generators |
train_toy.cpp / train_add.cpp | Training loops |
makefile | Builds train_toy and train_add |
StackRNN dataflow (today):
input → hidden (+ optional stack tops / prev hidden) → sigmoid → actions (push/pop/noop) → continuous stack update → hidden → softmax output. Training uses full BPTT on all weights.
Reservoir Computing / ESN gap: No fixed random reservoir, no spectral-radius scaling, no readout-only training (ridge / closed-form). Those are what a new ESN.h should add, reusing the stack idea where useful.
<plan> # Implementation Plan: Reservoir Computing (ESN) on Stack-RNN
1. Goals and design principles
Add Echo-State Networks (ESN) as a first-class model next to StackRNN, in the same C++ style and task harness.
Primary deliverable: new header ESN.h implementing:
- A large, sparsely connected reservoir with fixed random weights
- Fixed random input → reservoir weights
- Optional stack-augmented reservoir (Stack-ESN), reusing Stack-RNN stack mechanics
- A trainable readout only (ridge regression and/or SGD), not full BPTT through the reservoir
Non-goals (v1): CUDA, multi-layer deep ESNs, online recursive least squares as the only solver, or rewriting StackRNN internals.
Style constraints: match existing patterns — namespace rnn, header-only structs, Vec/Vec2D/Linear, my_int/my_real, same task generators and CLI conventions.
2. Conceptual mapping: StackRNN → ESN
| StackRNN concept | ESN analogue |
|---|---|
_hid (trained recurrent state) | Reservoir state x_t (dynamics fixed after init) |
_hid2hid (trained if mod==2) | Reservoir matrix W_res (fixed, scaled by spectral radius) |
_in2hidTranspose (trained) | Input matrix W_in (fixed random) |
_hid2out (trained) | Readout W_out (only trained parameters) |
| Stacks + actions (trained) | Optional Stack-ESN: fixed or lightly trained stack controller; stack tops feed reservoir/readout |
BPTT backward() + update(lr) | Collect states → solve readout (ridge); optional SGD on readout only |
| Softmax next-symbol prediction | Same output head for toy/add tasks; optional linear regression mode later |
Echo-state property: spectral radius of W_res (and leaky integration) keeps fading memory without training the reservoir.
3. New file layout
| File | Purpose |
|---|---|
ESN.h | Core ESN / Stack-ESN model (main deliverable) |
RidgeSolver.h (optional split) | Collect design matrix + ridge closed form; can live inside ESN.h initially |
train_esn.cpp | Training binary for toy tasks with ESN (mirror train_toy.cpp) |
train_esn_add.cpp (optional later) | Addition task with ESN |
makefile | Targets esn, optionally esn_add |
script_esn.sh | Repro experiments vs Stack-RNN baselines |
README.md | Document ESN options and how they relate to Stack-RNN |
Do not fork Vec.h / Linear.h unless a small API extension is required (e.g. spectral norm helpers in utils.h).
4. ESN.h public API (mirror StackRNN where practical)
4.1 Constructor parameters
si,sh,so— input / reservoir / output sizes (shtypically much larger than StackRNNnhid, e.g. 100–500+)nstack,stack_capacity,depth,isnoop— stack options (0 stacks ⇒ classic ESN)spectral_radius(default ~0.9)input_scalingsparsity(fraction of nonzeros inW_res)leak_rateα ∈ (0,1] for leaky integrator:
x_t = (1−α) x_{t−1} + α f(W_in u_t + W_res x_{t−1} + …)washoutlength (discard initial transient when fitting readout)ridge_lambdaseedfor reservoir initmod-like switch for Stack-ESN recurrence paths (see §5)- Optional bias unit in reservoir/readout
4.2 Core methods
initialize()— randomW_in, sparseW_res, scale to target spectral radius; init stacks if any; zero/random readoutreset()/emptyStacks()— clear reservoir (and stacks) between sequencesforward(cur, target, ishard=false)— one step; store reservoir (and stack) state; compute softmax (or linear) output via current readoutcollectState()/ access to last extended state vector for ridgefitReadoutRidge(states, targets)— closed-form ridge on collected batchbackwardReadoutOnly()+updateReadout(lr)— optional SGD path for online trainingeval(target),pred()— same semantics asStackRNNsave/load— include ESN hyperparameters + fixed reservoir + readoutcopy— for validation backup models if needed
4.3 State layout (suggested members)
- Fixed:
_W_in(LinearorVec2D),_W_res(Vec2Dsparse or dense), optional_bias - Trainable:
_hid2out(reuseLinear) - Runtime:
_xreservoir state (Vec), history buffer if needed for multi-step washout logging - Stack path (if enabled): reuse patterns from
StackRNN—_act,_stack,_hid2act,_hid2stack,_stack2hid— with a clear policy for which of these are frozen vs trained (see §5) - Extended readout features:
[x_t ; stack_tops ; 1]optional concatenation
5. Architecture variants (implement in phases)
Phase A — Classic ESN (no stack)
u_t= one-hot / index embedding viaW_in(same index-lookup style as_in2hidTranspose.forward_transpose)x_t = (1−α) x_{t−1} + α · tanh(W_in u_t + W_res x_{t−1})- Prefer tanh for reservoir (standard ESN); keep sigmoid available as option for consistency with StackRNN
y_t = softmax(W_out x_t)for discrete next-symbol tasks- Train only
W_out
Phase B — Stack-ESN (recommended research extension)
Two sub-modes:
B1. Fixed stack controller (pure RC spirit)
- Random fixed
hid2act,hid2stack,stack2hid(or deterministic push/pop heuristics) - Reservoir + stack dynamics frozen
- Readout from
[reservoir ; top-k stack]
B2. Hybrid (closer to Stack-RNN)
- Reservoir +
W_infixed - Train stack controllers and/or readout with short BPTT or SGD
- Useful ablation: “how much of Stack-RNN gain is stack vs trained hidden recurrence?”
Default v1 recommendation: Phase A + B1, with B2 as a follow-up flag (-stack_train 0|1).
Phase C — Deep / multi-reservoir (optional later)
- Cascade or parallel reservoirs; out of scope for first PR
6. Critical algorithms to implement
6.1 Sparse reservoir initialization (utils.h or private in ESN.h)
- Draw each entry of
W_resas 0 with prob1−sparsity, else uniform/Gaussian - Estimate spectral radius ρ (power iteration on dense matrix is fine for
nhid≲ 1000) - Scale:
W_res ← (spectral_radius / ρ) · W_res W_inentries ~ uniform[−input_scaling, input_scaling]or Gaussian
Add helpers in utils.h if reusable:
power_iteration_spectral_radius(const Vec2D&)scale_matrix(Vec2D&, my_real)- Optional sparse multiply if memory becomes an issue
6.2 Ridge regression readout
For classification-style next-char (match existing loss):
- Collect design matrix
Xrows = extended states after washout - Targets as one-hot rows
T - Solve
W_out^T = (X^T X + λ I)^{-1} X^T T(or solve linear system without explicit inverse) - Implement with a small dense solver (Gaussian elimination / Cholesky) for
N_features × N_featureswhereN_features ≈ nhid (+ stack depth × nstack)
Alternatively for pure MSE on logits, same form with continuous targets.
Training loop pattern (offline ESN):
- For each epoch or once: run all training sequences with washout; collect
(state, target) fitReadoutRidge- Validate with frozen readout; optionally re-collect and refit if curriculum (
nmax) grows
Online pattern (optional): SGD only on _hid2out after each forward, no reservoir backprop — reuses Linear::computeGradient / update on readout only.
6.3 Washout and sequence reset
- On each new sequence:
reset()reservoir; optionallyemptyStacks() - Skip first
washoutsteps when filling ridge buffers - Align with existing
-nresetsemantics intrain_toy
6.4 Hard actions
- Keep
ishardfor stack action discretization at valid/test if Stack-ESN is enabled (same UX as StackRNN)
7. Integration with training binaries
7.1 New train_esn.cpp (preferred over heavily branching train_toy.cpp)
Reuse:
- CLI parsing pattern from
train_toy.cpp task.hgenerators- Entropy logging, curriculum on
nmax, save paths underdata/
New / adapted flags:
-nhid→ reservoir size-spectral_radius,-sparsity,-input_scaling,-leak-washout,-ridge-nstack,-depth,-stack_size,-noop(Stack-ESN)-train_mode ridge|sgd-seed,-ntask,-nchar, … same task flags
Training differences vs train_toy:
- No
rnn.backward()through full network whentrain_mode=ridge - Periodic or end-of-epoch
fitReadoutRidge - For
sgd: only readout gradient
7.2 Makefile
esn : train_esn.cpp
$(CC) $(CFLAGS) train_esn.cpp -o train_esn
Add esn to all or keep separate to avoid breaking existing builds.
7.3 Experiments (script_esn.sh)
Mirror script_tasks.sh for tasks 1–4 with:
- Pure ESN (nstack=0)
- Stack-ESN (nstack=1–10, depth=1–2)
- Compare entropy / accuracy to Stack-RNN baselines with similar readout capacity
Suggested starting points:
- Reservoir 200–400, ρ=0.9, sparsity=0.1, leak=0.3–1.0, ridge=1e-6…1e-2
- Washout ~ few symbols or short prefix of each sequence
8. Minimal extensions to existing code
Prefer additive changes:
utils.h— spectral radius + optional matrix scale; maybetanhforward/backward inNonlinearity.hif not presentLinear.h— only if ridge needs a “set weights from Vec2D” helper; otherwise assign_hid2out._datadirectly- Do not change
StackRNNtraining semantics - Optional later: thin shared interface / comments documenting parallel APIs (
forward/eval/pred/emptyStacks)
9. Implementation order (checklist)
-
Scaffolding
- Add
ESN.hskeleton with constructors, dimensions,reset, classicforward(no stack) - Add
tanhnonlinearity if needed
- Add
-
Reservoir math
- Sparse init, power iteration, spectral scaling, leaky update
- Unit-level sanity: spectral radius after scale ≈ target; states remain bounded
-
Readout training
- State collection + ridge solver
eval/pred/ softmax output path- Save/load of ESN
-
train_esn.cpp+ makefile- End-to-end on task1
a^n b^nwith ridge - Logging parity with
train_toy
- End-to-end on task1
-
Stack-ESN (B1)
- Port stack update equations from
StackRNN::forward(push/pop/noop mixture) - Feed stack tops into reservoir and/or readout feature vector
- Freeze stack-related weights after random init
- Port stack update equations from
-
Ablations & scripts
script_esn.sh, README section- Compare nstack=0 vs >0; ridge vs SGD readout
-
Optional hardening
- Hybrid B2 (train stack controller)
train_esn_add.cppfor binary addition- Sparse
W_resmultiply for large reservoirs
10. Testing and validation strategy
No formal test framework exists today; use:
- Compile
make esnwith existingCFLAGS - Smoke run: short
nseq, small reservoir, task1 — finishes without NaNs - Echo-state check: identical inputs after reset → similar trajectories; perturbation in
x_0fades over time when ρ < 1 - Readout overfit check: on tiny fixed dataset, ridge drives train entropy down
- Task sanity: on
a^n b^nwith enough reservoir capacity, validation entropy competitive with small StackRNN or at least clearly better than chance - Regression: ensure
make toy/make addstill build unchanged
11. Risks and design decisions to lock early
| Decision | Recommendation |
|---|---|
| Reservoir activation | tanh default; CLI override to sigmoid |
Density of W_res | Dense Vec2D first; sparsity via zeroing entries (simple, matches codebase) |
| Stack training | Frozen (B1) in v1 |
| Output loss | Softmax + NLL to match existing metrics (log10 entropy in trainers) |
| Ridge vs SGD | Ridge as primary; SGD for online/curriculum convenience |
| Coupling stack → reservoir | Add weighted stack tops into pre-activation (like mod!=0 path in StackRNN) |
| Memory of history | Single current state enough for ESN; no BPTT buffer unless B2 |
12. Success criteria
- New
ESN.hcompiles and exposes a StackRNN-like inference API - Classic ESN trains readout via ridge on existing toy tasks
- Optional stacks yield a true Stack-ESN path without training the reservoir
train_esn+ makefile + docs/scripts allow reproduction and comparison withtrain_toy- No breakage of existing Stack-RNN binaries or headers beyond small shared utils/nonlinearity additions
13. Suggested first PR scope (minimal vertical slice)
Nonlinearity.h: addTanhutils.h: spectral radius helperESN.h: classic ESN + ridge readouttrain_esn.cpp+ makefile target- README blurb + one example command for task1
Follow-up PR: Stack-ESN (B1), scripts, addition task. </plan>
This plan stays aligned with the repo’s header-only C++ style and task setup, while introducing ESN-specific pieces (fixed reservoir, spectral scaling, readout-only training) and a clear path to a stack-augmented ESN that reuses Stack-RNN’s memory mechanism without full BPTT.
Activity