From df4a9b2ac2e21f10082d588fa89fa829b73ec02a Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Wed, 3 Jun 2026 10:49:46 +0200 Subject: [PATCH] [add] Rnn.py: unrolled character-level RNN-RBM matching docs/Rnn.drawio.png Co-Authored-By: Claude Sonnet 4.6 --- Rnn.py | 122 ++++++++++++++++++++++++++++++++++++++++++++ docs/Rnn.drawio | 112 ++++++++++++++++++++++++++++++++++++++++ docs/Rnn.drawio.png | Bin 0 -> 32560 bytes 3 files changed, 234 insertions(+) create mode 100644 Rnn.py create mode 100644 docs/Rnn.drawio create mode 100644 docs/Rnn.drawio.png diff --git a/Rnn.py b/Rnn.py new file mode 100644 index 0000000..b6513f9 --- /dev/null +++ b/Rnn.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Character-level RNN-RBM (unrolled) — see docs/Rnn.drawio.png. + +Diagram recap + t=0: v[0]={0} v[1:N]=[' ',' ','J'] W[0] → h + t=1: v[0]=h₀ v[1:N]=[' ','J','A'] W[1] → h + ... + t=M-1: v[0]=h_{M-2} v[1:N]=['J','A','Y'] W[M-1] → h + +v[0] = recurrent context (previous hidden state, or zeros at t=0) +v[1:N] = N_WIN one-hot-encoded characters concatenated (the sliding window) +W[t] = RBM weight matrix for time step t (one per step → unrolled mode) +""" +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +import numpy as np +from stack.rnn import StackRnn +from rbm.entity import EntityParams, TrainingParams +from rbm.matrix import convert + +# ── Hyper-parameters ────────────────────────────────────────────────────────── +TEXT = " JAY" # the character sequence to learn (matches diagram example) +N_WIN = 3 # sliding-window width (= N in the diagram) +H_SIZE = 8 # hidden units per RBM cell + +TRAIN_PARAMS = TrainingParams( + learning_rate = 0.05, + momentum = 0.9, + num_epochs = 1000, + do_rao_blackwell = True, +) +N_REPS = 300 # training repetitions of the sequence + +# ── Vocabulary ──────────────────────────────────────────────────────────────── +chars = sorted(set(TEXT)) +VOCAB_SIZE = len(chars) +c2i = {c: i for i, c in enumerate(chars)} +i2c = {i: c for c, i in c2i.items()} +SENS_SIZE = N_WIN * VOCAB_SIZE # width of v[1:N] + +print(f"Vocab ({VOCAB_SIZE}): {chars}") +print(f"N_WIN={N_WIN} H_SIZE={H_SIZE} SENS_SIZE={SENS_SIZE}") + +# ── Encoding helpers ────────────────────────────────────────────────────────── +def encode_window(window: str) -> np.ndarray: + """Encode N_WIN characters as a flat one-hot vector (v[1:N]).""" + vec = np.zeros(SENS_SIZE, dtype=np.float64) + for j, c in enumerate(window): + vec[j * VOCAB_SIZE + c2i[c]] = 1.0 + return vec + + +def decode_char(x: np.ndarray, position: int) -> str: + """Decode the one-hot slot at `position` inside a flat window vector.""" + seg = x[position * VOCAB_SIZE : (position + 1) * VOCAB_SIZE] + return i2c[int(np.argmax(seg))] + + +# ── Build training sequences (num_seq, T, SENS_SIZE) ───────────────────────── +# M = number of sliding windows per period of TEXT. +M = len(TEXT) - N_WIN + 1 # time steps per sequence +seqs = np.zeros((N_REPS, M, SENS_SIZE), dtype=np.float64) +for s in range(N_REPS): + for t in range(M): + seqs[s, t] = encode_window(TEXT[t : t + N_WIN]) + +print(f"\nSequences: {N_REPS} × {M} steps (T={M})") + +# ── Build unrolled StackRnn: M layers, one W[t] per time step ───────────────── +rnn = StackRnn("rnn_char", "results/rnn_char") +for layer in StackRnn.make_unrolled(M, SENS_SIZE, H_SIZE, EntityParams(), TRAIN_PARAMS): + rnn.append(layer) +rnn.state_init(0.01) + +print(f"Training…") +rnn.train(seqs) +rnn.state_save() +print("Weights saved to results/rnn_char/") + +# ── Generate text ───────────────────────────────────────────────────────────── +def generate(seed: str, num_chars: int = 20) -> str: + """Generate text by repeatedly reconstructing the next character. + + The model reconstructs v[1:N] from the hidden state h. We take the last + one-hot slot of the reconstructed window as the next predicted character, + then slide the window by one. + """ + assert len(seed) >= N_WIN, f"seed must be ≥ {N_WIN} chars" + rnn.reset(batch_size=1) + + # Prime: step through all but the last window of the seed + for t in range(len(seed) - N_WIN): + rnn.step(encode_window(seed[t : t + N_WIN])[None, :]) + + window = seed[-N_WIN:] + result = list(seed) + + for _ in range(num_chars): + h = rnn.step(encode_window(window)[None, :]) + x_recon = convert(rnn.reconstruct(h))[0] + next_char = decode_char(x_recon, N_WIN - 1) + result.append(next_char) + window = window[1:] + next_char + + return ''.join(result) + + +seed = TEXT[:N_WIN] +print(f"\nGenerated (seed={seed!r}):") +print(repr(generate(seed, num_chars=20))) + + +def main(): + _seed = TEXT[:N_WIN] + print(f"\nGenerated (seed={_seed!r}):") + print(repr(generate(_seed, num_chars=20))) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docs/Rnn.drawio b/docs/Rnn.drawio new file mode 100644 index 0000000..cfe0630 --- /dev/null +++ b/docs/Rnn.drawio @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Rnn.drawio.png b/docs/Rnn.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca9d0b2446f470036c53644fac4e91569ae4029 GIT binary patch literal 32560 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 literal 0 HcmV?d00001