From 05dc6266fe4b2ab5f4694c99791f76886e8550a9 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 26 Jul 2026 23:09:35 +0200 Subject: [PATCH] Add CLAUDE.md with architecture overview and dev commands Documents the RBM/Stack/Model composition patterns, the numpy/cupy backend switch gotcha, and test/run commands for future Claude Code sessions. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VSd9diGXwX5E9mAsw4Wp7R --- CLAUDE.md | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8c6f2fa --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,148 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +pyRBM is a from-scratch, hand-derived implementation of Restricted Boltzmann Machines (RBMs) and +Recurrent Temporal RBMs (RTRBMs) — no autodiff, no PyTorch training loop. Every gradient (Contrastive +Divergence) is coded by hand in `src/rbm/train.py`. It's a research/educational codebase, not a +competitive ML framework — see `README.rbm.md` for a full Q&A on the design rationale, why a +Transformer can't be bolted on without adding autodiff, and how the `src/compat/torch.py` bridge +relates to that. + +## Environment + +- Python >=3.12, project uses a `.venv` at the repo root (`.venv/bin/python`, `.venv/bin/pytest`). +- Package is installed editable (`pip install -e .`), src-layout: `src/{rbm,stack,model,label,image,compat}` + import as top-level packages (`from rbm.entity import Entity`, `from stack.rnn import StackRnn`, etc.) + — there is no `pyrbm.` prefix. +- GPU (CUDA) is used by default — see "CPU vs GPU" below. +- No linter/formatter config exists (no ruff/black/flake8/mypy) and no pytest config file — match + existing style (tabs for indentation in most `src/rbm` and `src/stack` files, spaces elsewhere; + check the file you're editing). + +## Running tests + +```bash +.venv/bin/pytest tests/ # whole suite +.venv/bin/pytest tests/test_rnn.py -q # single file +.venv/bin/pytest tests/test_rnn.py::test_rnn_shared -q # single test +``` + +`tests/README.md` documents each test's purpose and is worth reading before adding new tests — it +also has a fix-log at the bottom explaining several non-obvious bugs already found and fixed in +`train.py`/`entity.py` (e.g. why L1/L2 regularization must NOT be scaled by `1/mini_batch_size`, why +CD-k>1 Gibbs loops must sample rather than use means). Some `tests/*.py` files are pytest suites +(`def test_...`), others (`test_faces_sub_image.py`, `test_norbs.py`, `test_rbm.py`, etc.) are +standalone scripts invoked directly (`python tests/test_rbm.py `) that need external +data/images not in the repo (see the table in `tests/README.md`). + +## CPU vs GPU (important gotcha) + +`src/rbm/matrix.py` is the single backend switch for the entire codebase: + +```python +USE_CUDA = 1 +if USE_CUDA: + import cupy as np +else: + import numpy as np +``` + +Every module imports the array module as `np` from here (`from .matrix import np`), so flipping +`USE_CUDA` to `0` switches the whole codebase to CPU-only numpy with no other code changes. This +machine has cupy-cuda12x + an RTX 4070 Ti, so GPU is the default/expected path. If you see +`cupy`-related errors on a machine without a GPU, this flag is the first place to check. Note that +`cupy-cuda12x` is a de-facto hard runtime dependency (nothing works with `USE_CUDA=1` without it) but +is *not* listed in `pyproject.toml`'s `dependencies` — it's only present because it was installed +manually into this `.venv`. `torch` is also installed but only backs `src/compat/torch.py` / +notebook use, not core training. + +## Architecture + +### Core primitives (`src/rbm/`) + +- **`matrix.py`** — numpy/cupy backend switch (see above) + shared math helpers (`sample`, `prob` + (sigmoid), `gaussian`, `rms_error`, `read_armadillo` for loading legacy Armadillo `.dat` files). +- **`state.py`** (`RbmState`) — holds `w_hv`, `b_v`, `b_h`; pure linear algebra (`v_to_h`, `h_to_v`) + plus `.npz` save/load. +- **`entity.py`** (`Entity`) — one RBM unit. `Entity.Type` is one of `BB_RBM`/`BG_RBM`/`GB_RBM`/`GG_RBM` + (binary/Gaussian visible × binary/Gaussian hidden), derived from `EntityParams`. Owns a `RbmState` + (weights) and a `grad` (momentum accumulator). `forward`/`reconstruct` do (optionally multi-step) + Gibbs sampling; `h_given_v`/`v_given_h` are the raw (unsampled) linear step. `grad_compute` / + `state_adjust` apply momentum + L1/L2/weight-decay (regularization is applied at full strength, + NOT scaled by mini-batch size — see the fix-log note above). +- **`train.py`** — the four CD variants (`cd_binary_binary`, `cd_gaussian_binary`, + `cd_gaussian_gaussian`, `cd_binary_gaussian`), each a positive-phase/negative-phase Gibbs pass + mirroring the classic Hinton Matlab RBM code (comments in the file reference the original Matlab + pseudocode). `train()` is the standard epoch/mini-batch loop for a single `Entity`, dispatching to + the right CD function via `entity.type` and reporting through a `Status`. +- **`status.py`** — pluggable progress reporting/checkpointing (`Status`, `CheckpointStatus`) driven + by a callback (`on_report`) that can also halt training early. +- **`layer.py`** (`Layer`) — thin wrapper: one `Entity` + name + save/load, sized from + `(numVisibleX, numVisibleY, numContext, numHidden)`. + +### Composition patterns — two different ways to build multi-entity models + +1. **`Stack` family (`src/stack/`)** — an ordered `list[Layer]` with save/load-by-index into + `{name}-{i}-state.npz` files under `work_dir`. + - `StackDeep` (`deep.py`) — classic layer-wise-pretrained deep belief net: `train()` trains layer 0, + forwards its output as layer 1's input, etc. `pass_up`/`pass_down`/`pass_down_up` for + encode/decode/round-trip through the stack. + - `StackRnn` (`rnn.py`) — the RTRBM. Visible = `[context h_{t-1} | sensory x_t]`. Two modes based on + how many layers you `append()`: + - **Shared weights** (1 layer): the same `Entity` is reused every time step; sequences can be any + length T. + - **Unrolled** (N layers): `layers[t % N]` handles time step t, each with its own weights; + training requires sequences of length exactly N (uses "temporal-shift padding" — see the + docstring in `_train_unrolled`). + Use `StackRnn.make_layer(...)` / `StackRnn.make_unrolled(...)` to construct layers, `.step(x)` / + `.reconstruct(h)` for inference, `.train(sequences)` for training. + - `factory.py` (`StackFactory`) — builds a `Stack` from a JSON project dict/file (legacy `.prj` + format from a prior C++ implementation this was ported from — field names like `numVisibleX`, + `doSampleBatch` reflect that origin). + - `rnn_helper.py` — one-hot vocab encode/decode (`ch2idx`/`idx2ch`/`vec2idx`/`idx2vec`) and + visible-vector `concat`/`split`/`shift_*` helpers used by the RNN scripts/notebooks. `VOCAB` here + is a small ad-hoc set (currently space + digits) — the top-level scripts/notebooks each define + their own, larger vocab inline rather than importing this one. + +2. **`Model` (`src/model/model.py`)** — an alternate, reflection-based composition: subclass `Model`, + assign `Entity` instances as plain attributes in `__init__`, implement `forward()`. Base class + discovers entities via `self.objects(Entity)` (walks `__dict__`) for `train`/`save`/`load`/`init`, + training each entity in sequence and feeding its output forward — used by `test_model.py`'s + `DeepModel` and by `src/label/label.py`'s `Label.Fitter`. + +### Other modules + +- **`src/label/label.py`** (`Label`) — encodes discrete labels as binary or one-hot vectors, then fits + a `Model` (an RBM) on the vectors so labels can be embedded/decoded through learned features. +- **`src/image/sub_image.py`** — patch-based image tiling: `SubImageExtract` cuts overlapping patches + (with stride) for training on image patches instead of whole images; `SubImageCombine` blends + overlapping reconstructed patches back into a full image (pixel-averaged); also a small `conv2d`. +- **`src/compat/torch.py`** — a thin PyTorch-`optim`-shaped wrapper (`LossFunction`, `Optimizer` with + `zero_grad()`/`step()`) around the *same* hand-coded CD functions from `train.py` — it mimics the + PyTorch API surface but is not autodiff; see `README.rbm.md` for why real backprop would require + replacing the numpy/cupy backend with torch tensors throughout. + +### Top-level scripts and notebooks + +These are the char-level RTRBM demos/experiments, each self-contained and runnable directly +(`python Rnn.py`, no install needed — they `sys.path.insert(0, "src")` themselves): + +- **`Rnn.py`** — minimal unrolled-mode demo matching `docs/Rnn.drawio.png` (sliding window + context, + small hard-coded text). +- **`JayRnn.py`** / `README_JayRnn.md` — larger char-level language model, single shared-weight RBM, + fixed vocabulary, checkpoint/resume via `results/JayRnn-*-state.npz`. +- **`moby_rnn.ipynb`** / `README_moby_rnn.md` — the most fully worked example: trains on + `data/moby.txt`, unrolled `StackRnn` with `TEMPORAL_DEPTH` positions, includes evaluation + (teacher-forced reconstruction vs. honest next-step prediction), clamped-Gibbs text generation at + multiple temperatures, and hidden-state visualization. +- **`RNN-RBM.ipynb`** — the notebook the top-level `README.md` documents (context/sensory concat, + `UNROLL_DEPTH` modes). +- **`mnist_test*.ipynb`, `cifar_test*.ipynb`, `layer_test.ipynb`** — image-domain (non-recurrent) + RBM/DBN experiments using `data/MNIST` and `data/cifar-10-batches-py`. + +Checkpoints from all of the above land in `results/{name}-{index}-state.npz`, loaded automatically on +the next run if present (`RbmState.load` silently no-ops on missing file/shape mismatch, so deleting a +checkpoint is the way to force retraining from scratch).