From 0a5bc6c7fdc1057a4136ae63bf61929d0ca3bd7d Mon Sep 17 00:00:00 2001 From: Jan-Williams Date: Tue, 10 Feb 2026 11:56:26 -0800 Subject: [PATCH] Added base class for and ESNClassifier --- examples/classification.ipynb | 419 +++++++++++++++++++++ src/orc/classifier/__init__.py | 13 +- src/orc/classifier/base.py | 237 +++++++++++- src/orc/classifier/models.py | 141 ++++++- src/orc/classifier/train.py | 85 ++++- tests/classifier/test_classifier_models.py | 187 +++++++++ 6 files changed, 1066 insertions(+), 16 deletions(-) create mode 100644 examples/classification.ipynb create mode 100644 tests/classifier/test_classifier_models.py diff --git a/examples/classification.ipynb b/examples/classification.ipynb new file mode 100644 index 0000000..d2f4bd1 --- /dev/null +++ b/examples/classification.ipynb @@ -0,0 +1,419 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a1b2c3d4", + "metadata": {}, + "source": [ + "# ORC\n", + "### Example: Time-Series Classification with ESN\n", + "This notebook demonstrates how to use ORC's classifier module to classify time-series data using an Echo State Network (ESN). We generate synthetic time-series belonging to distinct classes, train an `ESNClassifier`, and evaluate its performance. This is a common task in areas like activity recognition, medical signal analysis, and sensor data classification." + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5", + "metadata": {}, + "source": [ + "As with other ORC modules, only `jax`, `orc`, and `equinox` are needed. We also import `matplotlib` for visualization." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c3d4e5f6", + "metadata": {}, + "outputs": [], + "source": [ + "import jax\n", + "import jax.numpy as jnp\n", + "import equinox as eqx\n", + "import matplotlib.pyplot as plt\n", + "import orc" + ] + }, + { + "cell_type": "markdown", + "id": "d4e5f6a7", + "metadata": {}, + "source": [ + "## Generate Synthetic Classification Data\n", + "\n", + "We create a simple 3-class classification problem where each class is a sinusoidal time-series with a distinct frequency. A small amount of noise is added to each sample. This mimics the kind of structure found in real-world time-series classification tasks, where different classes exhibit different temporal patterns." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e5f6a7b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training set: (60, 200, 3) (n_samples, seq_len, data_dim)\n", + "Test set: (30, 200, 3)\n" + ] + } + ], + "source": [ + "# Dataset parameters\n", + "data_dim = 3\n", + "n_classes = 3\n", + "seq_len = 200\n", + "n_train_per_class = 20\n", + "n_test_per_class = 10\n", + "\n", + "key = jax.random.PRNGKey(0)\n", + "\n", + "def generate_samples(key, n_per_class):\n", + " \"\"\"Generate sinusoidal sequences with class-specific frequencies.\"\"\"\n", + " seqs, labels = [], []\n", + " t = jnp.linspace(0, 4 * jnp.pi, seq_len).reshape(-1, 1)\n", + " for class_idx in range(n_classes):\n", + " freq = (class_idx + 1) * 0.5\n", + " for _ in range(n_per_class):\n", + " key, subkey = jax.random.split(key)\n", + " noise = 0.05 * jax.random.normal(subkey, (seq_len, data_dim))\n", + " seq = jnp.sin(freq * t * jnp.arange(1, data_dim + 1)) + noise\n", + " seqs.append(seq)\n", + " labels.append(class_idx)\n", + " return jnp.stack(seqs), jnp.array(labels, dtype=jnp.int32), key\n", + "\n", + "train_seqs, train_labels, key = generate_samples(key, n_train_per_class)\n", + "test_seqs, test_labels, key = generate_samples(key, n_test_per_class)\n", + "\n", + "print(f\"Training set: {train_seqs.shape} (n_samples, seq_len, data_dim)\")\n", + "print(f\"Test set: {test_seqs.shape}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f6a7b8c9", + "metadata": {}, + "source": [ + "Let's visualize one example from each class to see how the temporal patterns differ." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a7b8c9d0", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, n_classes, figsize=(14, 3), sharey=True)\n", + "class_names = [\"Low freq\", \"Medium freq\", \"High freq\"]\n", + "for i in range(n_classes):\n", + " idx = i * n_train_per_class\n", + " axes[i].plot(train_seqs[idx, :, 0])\n", + " axes[i].set_title(f\"Class {i}: {class_names[i]}\")\n", + " axes[i].set_xlabel(\"Time step\")\n", + "axes[0].set_ylabel(\"Amplitude (channel 0)\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b8c9d0e1", + "metadata": {}, + "source": [ + "## Initialize the ESN Classifier\n", + "\n", + "The `ESNClassifier` follows the same Embedding $\\to$ Driver $\\to$ Readout architecture as other ORC models. To instantiate a classifier, we specify:\n", + "- `data_dim`: the number of input channels per time-step\n", + "- `n_classes`: the number of classification classes\n", + "- `res_dim`: the dimension of the reservoir (latent state)\n", + "\n", + "The `state_repr` parameter controls how reservoir states are summarized into a feature vector for classification:\n", + "- `\"final\"` (default): uses only the last reservoir state\n", + "- `\"mean\"`: averages all reservoir states (after an optional spinup period)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c9d0e1f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Reservoir dimension: 500\n", + "Input dimension: 3\n", + "Output dimension: 3\n", + "State representation: final\n" + ] + } + ], + "source": [ + "res_dim = 500\n", + "\n", + "classifier = orc.classifier.ESNClassifier(\n", + " data_dim=data_dim,\n", + " n_classes=n_classes,\n", + " res_dim=res_dim,\n", + " seed=42,\n", + ")\n", + "\n", + "print(f\"Reservoir dimension: {classifier.res_dim}\")\n", + "print(f\"Input dimension: {classifier.in_dim}\")\n", + "print(f\"Output dimension: {classifier.out_dim}\")\n", + "print(f\"State representation: {classifier.state_repr}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d0e1f2a3", + "metadata": {}, + "source": [ + "## Train the Classifier\n", + "\n", + "Training is performed by `orc.classifier.train_ESNClassifier`. Under the hood, this:\n", + "1. Forces each training sequence through the reservoir (in parallel via `jax.vmap`)\n", + "2. Extracts a feature vector per sequence (final state or mean state)\n", + "3. Solves ridge regression from features to one-hot encoded class labels\n", + "4. Updates the readout weights\n", + "\n", + "The `beta` parameter controls Tikhonov regularization." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e1f2a3b4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training complete.\n" + ] + } + ], + "source": [ + "trained_classifier = orc.classifier.train_ESNClassifier(\n", + " classifier,\n", + " train_seqs=train_seqs,\n", + " labels=train_labels,\n", + " beta=1e-6,\n", + ")\n", + "\n", + "print(\"Training complete.\")" + ] + }, + { + "cell_type": "markdown", + "id": "f2a3b4c5", + "metadata": {}, + "source": [ + "## Evaluate on Training Data\n", + "\n", + "We can classify individual sequences using `classify`, which by default zero-initializes the reservoir state and returns a vector of class probabilities (via softmax). The predicted class is the one with the highest probability." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a3b4c5d6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training accuracy: 100.0%\n" + ] + } + ], + "source": [ + "probs = jax.vmap(trained_classifier.classify)(train_seqs)\n", + "\n", + "\n", + "train_preds = jnp.array(jnp.argmax(probs, axis=1))\n", + "train_acc = jnp.mean(train_preds == train_labels)\n", + "print(f\"Training accuracy: {train_acc:.1%}\")" + ] + }, + { + "cell_type": "markdown", + "id": "b4c5d6e7", + "metadata": {}, + "source": [ + "## Evaluate on Test Data\n", + "\n", + "The real test of the classifier is on unseen data. We classify each test sequence and compute the accuracy." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c5d6e7f8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test accuracy: 100.0%\n" + ] + } + ], + "source": [ + "probs = jax.vmap(trained_classifier.classify)(test_seqs)\n", + "\n", + "test_preds = jnp.array(jnp.argmax(probs, axis=1))\n", + "test_acc = jnp.mean(test_preds == test_labels)\n", + "print(f\"Test accuracy: {test_acc:.1%}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d6e7f8a9", + "metadata": {}, + "source": [ + "Let's look at the predicted probabilities for a few test samples to see how confident the classifier is." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e7f8a9b0", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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cueOnuuN+/fvt2XLlrnkXDRejAEQP+IhlmTGwIEDrV+/fr58tpJ7R48etaeeesrOOussX9qA+ERiCjHnlltuCXv8ySefuOCTcnlK+/bts0KFCuVw62LXtm3brGrVqsddTydYOvmKphMgALEpHo7nV1xxhb355pv2zjvvWOvWrYPLly5dauvXr7e2bdu6xFVOK1CggEWT7du3u78zMsxj7969Vrhw4Qi0CkA8iodYkhl58+Z1N7/OFzJybA8EAu6coWDBghFqGWIdZ5aISxdffLFVq1bNVqxYYRdddJELOgMGDHDPqTusxlenlFZ9jj///NPVWipfvrz7T7+uDIwcOdJdKQi1ZcsW17X10KFD2dJ+/Sf9vvvuC37uueeea6NHj3YHec91111nderUCXvd1Vdf7b6fTpI8n376qVumk6b0rnzoeZ1AzZ07N9jdecOGDcHnZsyY4a7OlCtXzm3L3bt3B99bJ2WqH6LlzZo1s48//jjVZ3z00Ud2wQUXuJ4D6vr77LPPntD4eACJI9qP5zouql3Tp08PWz5t2jSrXr26a3taTuT4mZaU3zm9Y6w3LETH+NDXXnXVVe6Yr+F2OpFQ271hL6+//rp7rDbUrVvXvvzyy2NuE312hQoV3P0HHnjAfZ4+I7Rd3333nd188812yimn2IUXXhh87csvv+w+Q2049dRT7aabbrJNmzal+gwNH9H20Hr169e3Dz/80P1WdAOAWIslmZHW8V29VHv16mUlSpSwk08+2a655hrbvHlzut9N30PfTQkmxaHOnTu7RN2xaHt4IytOO+20sPf24siCBQuCccSLVxndZl6b1B61S8PBV65c6T5HsQvxjR5TiFu///67/eMf/3D/qdUVk1KlSmXq9To460RBB/W77rrLzjjjDHcFvH///i7YjB07Nriulr344osuueP95zurlHxSMFm8eLF16dLFatWq5Q7y+s+92vLkk0+69Zo2bWpz5sxxSaIiRYq41+mkRj2Z9B90vYfovpY1adIkzc9Tt2Z1de7Tp4+dfvrpLiHmBRzvxGXYsGGul9T9999vBw8edPc19E/bVycQClL6jClTptill17qPlMnCqL6Iip+q/dT8Dp8+LBbP7P7A0DiivbjuRIsvXv3dpNInHTSSe449+qrr1rfvn3THMYXjcfPdevWue+h7aNtrIshutgxceJEd/LWrVs3t96IESPsxhtvPObQQV040UmF4kr79u2tVatWbruEuuGGG+zss8929Q29iy6qRfXwww+791fdLvW6evrpp91JpJJh3hX6SZMmuXY2btzYnez89NNPLuYpkaUTHwCItViiWJFWLULFlYxQQueVV15xNWJVmuP999+3K6+8Mt31dZytVKmSO6Z/8cUX9sILL1jJkiVdwig9+n4aJvnGG2/YhAkT3HG9Ro0awecVF3TM17bp2rWru7Ce0W2mOKBex7oYc/fdd7vzE32OklNIEAEgxnXv3l3/ow1b1qxZM7ds4sSJqdbX8sGDB6daXqFChUCnTp2Cj4cNGxYoXLhwYO3atWHr9evXL5AnT57Axo0bg8v0Or3v+vXrj9terac2p2f27NlunUceeSRs+fXXXx/IlStXYN26de7xZ5995tabN2+ee7xq1Sr3+IYbbgg0aNAg+LprrrkmULt27eO2S9//yiuvDFu2ePFi956VK1cO7Nu3L7j86NGjgbPPPjvQsmVLd9+jdSpVqhS4/PLLg8vatGkTSEpKCvz888/BZd99953bhhyCAMTD8Xznzp2B/PnzB1566SW3fO7cue54vWHDBtc+rbd9+/YcPX6m/M7e56Y0ZcqUVN9Pr9WypUuXBpctWLDALStYsGDY5z/77LNuueLDsej9td7jjz8ettxrV/v27cOWa1vpew0fPjxs+ddffx3ImzdvcHlycnKgZMmSgVq1agUOHjwYXO+5555z76vfC4DEFoux5Hg3/b8/veP7ihUr3ON777037H1vu+22VN/Ne+3tt98etu61114bKF68+HHbmjKmpYwj8+fPD1ue0W3mnf+MGjUquM7hw4cDTZs2dcsVuxDfGMqHuKWuouqWmlW62q1eSRpmoCsY3q158+Z25MgR++CDD4Lrqnup4sqJ9paSefPmWZ48eVx33FDqyaTP8Ibk1a5d212p8Nqhq+zq8dSxY0d35UNXKLS+rjzoe5wIXa0IHSOubrWaBlxX13X1yds2GoJ42WWXuTape662k3p7tWnTxl0h8egqSMuWLU+oTQASR7Qfz/W+GpanYueiYX3qzeMNZwsVrcdP1Rhs1KhR8HGDBg3c3+rFFfr53nL1UjoRuiIeSsMF9b11FT90H5UuXdr1rFIvYtHMVKpxoteHFpv3hn8AQCzGEvUWUl2slDeNmDie+fPnu7+9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3Xf29NNP23XXXWcFChRItw7V4sWLs/L2AAAAAAAASABZGso3ePBgN0wvZVLq8OHD9sEHH7j7efPmtWbNmmVPKwEAAAAAABB3spSYuuSSS2znzp2plu/atcs9BwAAAAAAAORIYkq1pXLlypVq+e+//+7qS2XGuHHjrGLFipaUlGQNGjSw5cuXH3P9P//807p3725lypRxPbbOOeccmzdvXqa/AwAg+hATAADEAwBILJmqMaWaUqKk1G233RY2lO/IkSO2atUqa9y4cYbfb+bMmda3b1+bOHGiS0qNHTvWWrZsaWvWrLGSJUumWj85Odkuv/xy95xmBCxXrpz9/PPPVqxYscx8DQBAFCImAACIBwCQeDKVmCpatGiwx9TJJ59sBQsWDD6XP39+a9iwoXXt2jXD7/fEE0+49Tt37uweK0E1d+5cmzx5svXr1y/V+lquIYRLly61fPnyuWXqbQUAiH3EBAAA8QAAEk+mElNTpkwJJoPuv//+TA/bS9n7acWKFda/f//gsty5c1vz5s1t2bJlab7mzTfftEaNGrmhfHPmzLHTTjvNbr75ZnvooYcsT548WW4LAMBfxAQAAPEAQGZ93LotGy0HNZnzmkVdYip0Vr4TtWPHDjf8r1SpUmHL9Xj16tVpvuann36y9957zzp06ODqSq1bt866detmhw4dSrdNBw8edDfP7t27T7jtAIDsRUwAABAPACAxZTgxVadOHVu0aJGdcsopVrt27TSLn3u++OILywlHjx519aWee+4510Oqbt26tnnzZnv88cfTTUyNGDHChg4dmiPtAQD4h5gAACAeAEACJaZat24dLHbepk2bE/7gEiVKuOTS1q1bw5brcenSpdN8jWbiU22p0GF75513nv32229uGIjqXKWkoYIqsB7aY6p8+fIn3H4AQPYhJgAAiAcAkJgynJgK7ZGUHUP5lERSjyf1wvISXbr6rcc9evRI8zVNmjSx6dOnu/VUj0rWrl3rElZpJaVEybTQ2QMBANGHmAAAIB4AQGL6X3bHJ+rJ9Pzzz9uLL75o33//vd1zzz22d+/e4Cx9HTt2DCuOruc1K1/v3r1dQkoz+D366KOuGDoAILYREwAAxAMASDwZ7jGl2lLHqisVSsmjjGjXrp1t377dBg0a5Ibj1apVy+bPnx8siL5x48ZgzyjRELwFCxZYnz59rEaNGlauXDmXpNKsfACA2EZMAAAQDwAg8eQKBAKBjKyoXk0Z1alTJ4tWqjFVtGhR27VrlxUpUsTv5gBAzIvl42ostx0AolGsHldjtd1Aovu4dVu/mxDXmsx5LSLH1bzxkGwCAAAAAABA7MmbmWyXl+XS/WPhKgMAAAAAAACytcbUli1brGTJklasWLE0601pVKCWHzlyxOLZ1ffN8bsJce+tMa39bgIAAAAAAIiWxNR7771np556qru/ePHinGwTAAAAAAAAEkCGE1PNmjVL8z4AAAAAAACQo4mplP744w+bNGmSff/99+5x1apVrXPnzsFeVQAAAAAAAMCx5LYs+OCDD6xixYr2f//3fy5BpZvuV6pUyT0HAAAAAAAA5EiPqe7du1u7du1swoQJlidPHrdMBc+7devmnvv666+z8rYAAAAAAABIIFlKTK1bt85mzZoVTEqJ7vft29emTp2ane0DAADIdjfOvIetmsNeaTeBbQwAAHJmKF+dOnWCtaVCaVnNmjWz8pYAAAAAAABIMBnuMbVq1arg/V69elnv3r1dz6mGDRu6ZZ988omNGzfOHnvssZxpKQAAAAAAABIzMVWrVi3LlSuXBQKB4LIHH3ww1Xo333yzqz8FAAAAAAAAZEtiav369RldFQAAAAAAAMi+xFSFChUyuioAAAAAAACQM7Pyeb777jvbuHGjJScnhy2/5pprTuRtAQAAAAAAkACylJj66aef7Nprr7Wvv/46rO6U7suRI0eyt5UAAAAAAACIO7mz8iLNyFepUiXbtm2bFSpUyL799lv74IMPrF69erZkyZLsbyUAAAAAAADiTpZ6TC1btszee+89K1GihOXOndvdLrzwQhsxYoT16tXLvvzyy+xvKQAAAAAAAOJKlnpMaajeySef7O4rOfXrr78GC6SvWbMme1sIAAAAAACAuJSlHlPVqlWzr776yg3na9CggY0aNcry589vzz33nFWuXDn7WwkAAAAAAIC4k6XE1MCBA23v3r3u/r/+9S+76qqrrGnTpla8eHGbOXNmdrcRAAAAAAAAcShLiamWLVsG75911lm2evVq27lzp51yyinBmfkAAAAAAACAbE9Mhdq0aZP7u3z58if6VgAAAAAAAEggWSp+fvjwYXv44YetaNGiVrFiRXfTfQ3xO3ToUPa3EgAAAAAAAHEnSz2mevbsaa+//roret6oUSO3bNmyZTZkyBD7/fffbcKECdndTgAAAAAAAMSZLCWmpk+fbjNmzLB//OMfwWU1atRww/nat29PYgoAAAAAAAA5M5SvQIECbvheSpUqVbL8+fNn5S0BAAAAAACQYLKUmOrRo4cNGzbMDh48GFym+8OHD3fPZda4ceNcoispKckaNGhgy5cvz9Dr1GtLswC2adMm058JAIg+xAMAADEBABJLhofyXXfddWGPFy5caKeffrrVrFnTPf7qq68sOTnZLrvsskw1YObMmda3b1+bOHGiS0qNHTvWWrZsaWvWrLGSJUum+7oNGzbY/fffb02bNs3U5wEAohPxAABATACAxJPhHlOadS/01rZtW7vqqqtcXSnddF/JKz2XGU888YR17drVOnfubFWrVnUJqkKFCtnkyZPTfc2RI0esQ4cONnToUKtcuXKmPg8AEJ2IBwAAYgIAJJ4M95iaMmVKtn+4elitWLHC+vfvH1yWO3dua968uZvlLz3/+te/XG+qLl262Icffpjt7QIARBbxAABATACAxJSlWfk827dvd0Pu5Nxzz7XTTjstU6/fsWOH6/1UqlSpsOV6vHr16jRf89FHH9mkSZNs5cqVGfoM1b4KrYW1e/fuTLURAJDzIhEPhJgAANGPcwQASCxZKn6+d+9eu/32261MmTJ20UUXuVvZsmVdD6Z9+/ZZTtmzZ4/deuut9vzzz1uJEiUy9JoRI0aEDUHUsEMAQGzLSjwQYgIAxB/OEQAgAXtMqVj5+++/b2+99ZY1adIkeOW6V69edt9999mECRMy9D46mciTJ49t3bo1bLkely5dOtX6P/74oyt6fvXVVweXHT169H9fJG9e13vrzDPPDHuNhgmqvaE9pkhOAUB0iUQ8EGICAEQ/zhEAILFkKTH12muv2axZs+ziiy8OLmvVqpUVLFjQbrzxxgwnpvLnz29169a1RYsWWZs2bYInFnrco0ePVOtXqVLFvv7667BlAwcOdFdJnnrqqTQTTgUKFHA3AED0ikQ8EGICAEQ/zhEAILFkKTGl4Xop64CICpJndiifejN16tTJ6tWrZ/Xr17exY8e6oYKapU86duxo5cqVc8MvkpKSrFq1amGvL1asmPs75XIAQGwhHgAAiAkAkHiylJhq1KiRDR482KZOneqSRbJ//34bOnSoey4z2rVr54qoDxo0yH777TerVauW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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axes = plt.subplots(1, 3, figsize=(12, 3))\n", + "for i, class_idx in enumerate(range(n_classes)):\n", + " sample_idx = class_idx * n_test_per_class # first test sample of each class\n", + " probs = trained_classifier.classify(test_seqs[sample_idx])\n", + " axes[i].bar(range(n_classes), probs, color=[\"#4C72B0\", \"#55A868\", \"#C44E52\"])\n", + " axes[i].set_xticks(range(n_classes))\n", + " axes[i].set_xticklabels(class_names)\n", + " axes[i].set_ylim(0, 1)\n", + " axes[i].set_title(f\"True: {class_names[class_idx]}\")\n", + " axes[i].set_ylabel(\"Probability\" if i == 0 else \"\")\n", + "plt.suptitle(\"Predicted Class Probabilities\", y=1.02)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f8a9b0c1", + "metadata": {}, + "source": [ + "## Using Mean State Representation\n", + "\n", + "Instead of using only the final reservoir state for classification, we can average the reservoir states over the sequence. This can be more robust when the discriminative information is spread throughout the time-series rather than concentrated at the end. The `spinup` parameter discards early transient states before averaging." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a9b0c1d2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test accuracy (mean state): 100.0%\n" + ] + } + ], + "source": [ + "classifier_mean = orc.classifier.ESNClassifier(\n", + " data_dim=data_dim,\n", + " n_classes=n_classes,\n", + " res_dim=res_dim,\n", + " seed=42,\n", + " state_repr=\"mean\",\n", + ")\n", + "\n", + "trained_mean = orc.classifier.train_ESNClassifier(\n", + " classifier_mean,\n", + " train_seqs=train_seqs,\n", + " labels=train_labels,\n", + " spinup=20,\n", + " beta=1e-6,\n", + ")\n", + "\n", + "in_seq = test_seqs\n", + "res_state = None\n", + "spinup = 20\n", + "probs = eqx.filter_vmap(trained_mean.classify)(in_seq, res_state, spinup)\n", + "\n", + "mean_preds = jnp.argmax(probs, axis=1)\n", + "mean_acc = jnp.mean(mean_preds == test_labels)\n", + "print(f\"Test accuracy (mean state): {mean_acc:.1%}\")" + ] + }, + { + "cell_type": "markdown", + "id": "b0c1d2e3", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "This notebook demonstrated the core classification workflow in ORC:\n", + "\n", + "1. **Initialize** an `ESNClassifier` with `orc.classifier.ESNClassifier`\n", + "2. **Train** with `orc.classifier.train_ESNClassifier`, which uses ridge regression for a closed-form solution\n", + "3. **Classify** new sequences with `classify` (reservoir state defaults to zero if not provided)\n", + "\n", + "Key parameters to tune:\n", + "- `res_dim`: larger reservoirs can capture more complex dynamics\n", + "- `beta`: regularization strength (increase if overfitting)\n", + "- `state_repr`: `\"final\"` vs `\"mean\"` depending on where class-discriminative information lies in the sequence\n", + "- `spinup`: number of transient states to discard when using `state_repr=\"mean\"`" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "orc2", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/orc/classifier/__init__.py b/src/orc/classifier/__init__.py index 1de6bbf..c5c96bd 100644 --- a/src/orc/classifier/__init__.py +++ b/src/orc/classifier/__init__.py @@ -1,6 +1,11 @@ -"""Classification with Reservoir Computers. +"""Classification with Reservoir Computers.""" -This module is currently a placeholder for future classifier implementations. -""" +from orc.classifier.base import RCClassifierBase +from orc.classifier.models import ESNClassifier +from orc.classifier.train import train_ESNClassifier -__all__ = [] +__all__ = [ + "RCClassifierBase", + "ESNClassifier", + "train_ESNClassifier", +] diff --git a/src/orc/classifier/base.py b/src/orc/classifier/base.py index 0b8f538..03a0f23 100644 --- a/src/orc/classifier/base.py +++ b/src/orc/classifier/base.py @@ -1,6 +1,235 @@ -"""Base classes for Reservoir Computer Classifiers. +"""Defines base classes for Reservoir Computer Classifiers.""" -This module is currently a placeholder for future classifier implementations. -""" +from abc import ABC -# TODO: Implement RCClassifierBase and related classes +import equinox as eqx +import jax +import jax.numpy as jnp +from jaxtyping import Array, Float + +from orc.drivers import DriverBase +from orc.embeddings import EmbedBase +from orc.readouts import ReadoutBase + + +class RCClassifierBase(eqx.Module, ABC): + """Base class for reservoir computer classifiers. + + Defines the interface for the reservoir computer classifier which includes + the driver, readout and embedding layers. The classifier forces input sequences + through the reservoir, extracts a feature vector from the reservoir states, + and applies a trained readout to produce class probabilities. + + Attributes + ---------- + driver : DriverBase + Driver layer of the reservoir computer. + readout : ReadoutBase + Readout layer of the reservoir computer. Output dimension equals n_classes. + embedding : EmbedBase + Embedding layer of the reservoir computer. + in_dim : int + Dimension of the input data. + out_dim : int + Dimension of the output data (equals n_classes). + res_dim : int + Dimension of the reservoir. + n_classes : int + Number of classification classes. + state_repr : str + Reservoir state representation for classification. + "final" uses the last reservoir state, "mean" averages states after spinup. + dtype : type + Data type of the reservoir computer (jnp.float64 is highly recommended). + seed : int + Random seed for generating the PRNG key for the reservoir computer. + + Methods + ------- + force(in_seq, res_state) + Teacher forces the reservoir with the input sequence. + classify(in_seq, res_state) + Classify an input sequence, returning class probabilities. + set_readout(readout) + Replaces the readout layer of the reservoir computer. + set_embedding(embedding) + Replaces the embedding layer of the reservoir computer. + """ + + driver: DriverBase + readout: ReadoutBase + embedding: EmbedBase + in_dim: int + out_dim: int + res_dim: int + n_classes: int + state_repr: str = "final" + dtype: Float = jnp.float64 + seed: int = 0 + + def __init__( + self, + driver: DriverBase, + readout: ReadoutBase, + embedding: EmbedBase, + n_classes: int, + state_repr: str = "final", + dtype: Float = jnp.float64, + seed: int = 0, + ) -> None: + """Initialize RCClassifier Base. + + Parameters + ---------- + driver : DriverBase + Driver layer of the reservoir computer. + readout : ReadoutBase + Readout layer of the reservoir computer. + embedding : EmbedBase + Embedding layer of the reservoir computer. + n_classes : int + Number of classification classes. + state_repr : str + Reservoir state representation for classification. + "final" uses the last reservoir state, "mean" averages states after spinup. + dtype : type + Data type of the reservoir computer (jnp.float64 is highly recommended). + seed : int + Random seed for generating the PRNG key for the reservoir computer. + """ + if state_repr not in ("final", "mean"): + raise ValueError( + f"state_repr must be 'final' or 'mean', got '{state_repr}'." + ) + self.driver = driver + self.readout = readout + self.embedding = embedding + self.in_dim = self.embedding.in_dim + self.out_dim = self.readout.out_dim + self.res_dim = self.driver.res_dim + self.n_classes = n_classes + self.state_repr = state_repr + self.dtype = dtype + self.seed = seed + + @eqx.filter_jit + def force(self, in_seq: Array, res_state: Array) -> Array: + """Teacher forces the reservoir with the input sequence. + + Parameters + ---------- + in_seq : Array + Input sequence to force the reservoir, (shape=(seq_len, in_dim)). + res_state : Array + Initial reservoir state, (shape=(res_dim,)). + + Returns + ------- + Array + Forced reservoir sequence, (shape=(seq_len, res_dim)). + """ + + def scan_fn(state, in_vars): + proj_vars = self.embedding.embed(in_vars) + res_state = self.driver.advance(proj_vars, state) + return (res_state, res_state) + + _, res_seq = jax.lax.scan(scan_fn, res_state, in_seq) + return res_seq + + @eqx.filter_jit + def classify( + self, in_seq: Array, res_state: Array | None = None, spinup: int = 0 + ) -> Array: + """Classify an input sequence. + + Forces the reservoir with the input sequence, extracts a feature vector + from the reservoir states, and returns softmax class probabilities. + + Parameters + ---------- + in_seq : Array + Input sequence to classify, (shape=(seq_len, in_dim)). + res_state : Array + Initial reservoir state, (shape=(res_dim,)). + spinup : int + Number of initial reservoir states to discard before extracting + features. Only used when state_repr="mean". + + Returns + ------- + Array + Class probabilities, (shape=(n_classes,)). + """ + if res_state is None: + res_state = jnp.zeros(self.res_dim) + + res_seq = self.force(in_seq, res_state) + + if self.state_repr == "final": + feature = res_seq[-1] + else: # "mean" + feature = jnp.mean(res_seq[spinup:], axis=0) + + logits = self.readout.readout(feature) + return jax.nn.softmax(logits) + + def __call__(self, in_seq: Array, res_state: Array, spinup: int = 0) -> Array: + """Classify an input sequence, wrapper for `classify` method. + + Parameters + ---------- + in_seq : Array + Input sequence to classify, (shape=(seq_len, in_dim)). + res_state : Array + Initial reservoir state, (shape=(res_dim,)). + spinup : int + Number of initial reservoir states to discard before extracting + features. Only used when state_repr="mean". + + Returns + ------- + Array + Class probabilities, (shape=(n_classes,)). + """ + return self.classify(in_seq, res_state, spinup) + + def set_readout(self, readout: ReadoutBase) -> "RCClassifierBase": + """Replace readout layer. + + Parameters + ---------- + readout : ReadoutBase + New readout layer. + + Returns + ------- + RCClassifierBase + Updated model with new readout layer. + """ + + def where(m: "RCClassifierBase"): + return m.readout + + new_model = eqx.tree_at(where, self, readout) + return new_model + + def set_embedding(self, embedding: EmbedBase) -> "RCClassifierBase": + """Replace embedding layer. + + Parameters + ---------- + embedding : EmbedBase + New embedding layer. + + Returns + ------- + RCClassifierBase + Updated model with new embedding layer. + """ + + def where(m: "RCClassifierBase"): + return m.embedding + + new_model = eqx.tree_at(where, self, embedding) + return new_model diff --git a/src/orc/classifier/models.py b/src/orc/classifier/models.py index 25df4a2..1dc1178 100644 --- a/src/orc/classifier/models.py +++ b/src/orc/classifier/models.py @@ -1,6 +1,139 @@ -"""Classifier models based on Reservoir Computing. +"""Discrete ESN classifier implementation.""" -This module is currently a placeholder for future classifier implementations. -""" +import jax +import jax.numpy as jnp -# TODO: Implement classifier models (e.g., ESNClassifier) +from orc.classifier.base import RCClassifierBase +from orc.drivers import ESNDriver +from orc.embeddings import LinearEmbedding +from orc.readouts import LinearReadout, QuadraticReadout + +jax.config.update("jax_enable_x64", True) + + +class ESNClassifier(RCClassifierBase): + """ + Basic implementation of ESN for classification tasks. + + Attributes + ---------- + res_dim : int + Reservoir dimension. + data_dim : int + Input data dimension. + n_classes : int + Number of classification classes. + driver : ESNDriver + Driver implementing the Echo State Network dynamics. + readout : ReadoutBase + Trainable linear readout layer with out_dim=n_classes. + embedding : LinearEmbedding + Untrainable linear embedding layer. + + Methods + ------- + force(in_seq, res_state) + Teacher forces the reservoir with the input sequence. + classify(in_seq, res_state) + Classify an input sequence, returning class probabilities. + set_readout(readout) + Replace readout layer. + set_embedding(embedding) + Replace embedding layer. + """ + + res_dim: int + data_dim: int + + def __init__( + self, + data_dim: int, + n_classes: int, + res_dim: int, + leak_rate: float = 0.6, + bias: float = 1.6, + embedding_scaling: float = 0.08, + Wr_density: float = 0.02, + Wr_spectral_radius: float = 0.8, + dtype: type = jnp.float64, + seed: int = 0, + quadratic: bool = False, + use_sparse_eigs: bool = True, + state_repr: str = "final", + ) -> None: + """ + Initialize the ESN classifier. + + Parameters + ---------- + data_dim : int + Dimension of the input data. + n_classes : int + Number of classification classes. + res_dim : int + Dimension of the reservoir adjacency matrix Wr. + leak_rate : float + Integration leak rate of the reservoir dynamics. + bias : float + Bias term for the reservoir dynamics. + embedding_scaling : float + Scaling factor for the embedding layer. + Wr_density : float + Density of the reservoir adjacency matrix Wr. + Wr_spectral_radius : float + Largest eigenvalue of the reservoir adjacency matrix Wr. + dtype : type + Data type of the model (jnp.float64 is highly recommended). + seed : int + Random seed for generating the PRNG key for the reservoir computer. + quadratic : bool + Use quadratic nonlinearity in output, default False. + use_sparse_eigs : bool + Whether to use sparse eigensolver for setting the spectral radius of wr. + Default is True, which is recommended to save memory and compute time. If + False, will use dense eigensolver which may be more accurate. + state_repr : str + Reservoir state representation for classification. + "final" uses the last reservoir state, "mean" averages states after spinup. + """ + # Initialize the random key and reservoir dimension + self.res_dim = res_dim + self.seed = seed + self.data_dim = data_dim + key = jax.random.PRNGKey(seed) + key_driver, key_readout, key_embedding = jax.random.split(key, 3) + + embedding = LinearEmbedding( + in_dim=data_dim, + res_dim=res_dim, + seed=key_embedding[0], + scaling=embedding_scaling, + ) + driver = ESNDriver( + res_dim=res_dim, + seed=key_driver[0], + leak=leak_rate, + bias=bias, + density=Wr_density, + spectral_radius=Wr_spectral_radius, + dtype=dtype, + use_sparse_eigs=use_sparse_eigs, + ) + if quadratic: + readout = QuadraticReadout( + out_dim=n_classes, res_dim=res_dim, seed=key_readout[0] + ) + else: + readout = LinearReadout( + out_dim=n_classes, res_dim=res_dim, seed=key_readout[0] + ) + + super().__init__( + driver=driver, + readout=readout, + embedding=embedding, + n_classes=n_classes, + state_repr=state_repr, + dtype=dtype, + seed=seed, + ) diff --git a/src/orc/classifier/train.py b/src/orc/classifier/train.py index 97b304c..00b94ed 100644 --- a/src/orc/classifier/train.py +++ b/src/orc/classifier/train.py @@ -1,6 +1,83 @@ -"""Training functions for Reservoir Computer Classifiers. +"""Training functions for reservoir computer classifiers.""" -This module is currently a placeholder for future classifier implementations. -""" +import equinox as eqx +import jax +import jax.numpy as jnp +from jaxtyping import Array -# TODO: Implement training functions for classifiers +from orc.classifier.models import ESNClassifier +from orc.readouts import NonlinearReadout +from orc.utils.regressions import ridge_regression + + +def train_ESNClassifier( + model: ESNClassifier, + train_seqs: Array, + labels: Array, + spinup: int = 0, + beta: float = 8e-8, +) -> ESNClassifier: + """Training function for ESNClassifier. + + Trains the classifier by forcing each input sequence through the reservoir, + extracting a feature vector from the reservoir states, and solving ridge + regression against one-hot encoded class labels. + + Parameters + ---------- + model : ESNClassifier + ESNClassifier model to train. + train_seqs : Array + Batch of training input sequences, + (shape=(n_samples, seq_len, data_dim)). + labels : Array + Integer class labels for each sequence, (shape=(n_samples,)). + Values should be in [0, n_classes). + spinup : int + Number of initial reservoir states to discard before extracting + features. Only used when model.state_repr="mean". + beta : float + Tikhonov regularization parameter. + + Returns + ------- + model : ESNClassifier + Trained ESN classifier model. + """ + if not isinstance(model, ESNClassifier): + raise TypeError("Model must be an ESNClassifier.") + + if train_seqs.shape[0] != labels.shape[0]: + raise ValueError("Number of training sequences must match number of labels.") + + n_samples = train_seqs.shape[0] + initial_res_states = jnp.zeros((n_samples, model.res_dim), dtype=model.dtype) + + # Force all sequences through the reservoir in parallel via vmap + all_res_seqs = jax.vmap(model.force)(train_seqs, initial_res_states) + # all_res_seqs shape: (n_samples, seq_len, res_dim) + + # Extract feature vectors + if model.state_repr == "final": + feature_matrix = all_res_seqs[:, -1, :] # (n_samples, res_dim) + else: # "mean" + feature_matrix = jnp.mean(all_res_seqs[:, spinup:, :], axis=1) + + # Build one-hot target matrix + one_hot_targets = jnp.zeros((n_samples, model.n_classes), dtype=model.dtype) + one_hot_targets = one_hot_targets.at[jnp.arange(n_samples), labels].set(1.0) + + # Apply optional nonlinear transform + if isinstance(model.readout, NonlinearReadout): + feature_matrix = model.readout.nonlinear_transform(feature_matrix) + + # Solve ridge regression + cmat = ridge_regression(feature_matrix, one_hot_targets, beta) + cmat = cmat.reshape(1, cmat.shape[0], cmat.shape[1]) + + def where(m): + return m.readout.wout + + model = eqx.tree_at(where, model, cmat) + + return model diff --git a/tests/classifier/test_classifier_models.py b/tests/classifier/test_classifier_models.py new file mode 100644 index 0000000..7b10a0b --- /dev/null +++ b/tests/classifier/test_classifier_models.py @@ -0,0 +1,187 @@ +import jax +import jax.numpy as jnp +import pytest + +import orc +import orc.classifier + + +@pytest.fixture +def dummy_classification_data(): + """Generate dummy classification data with 3 classes of sinusoidal sequences.""" + data_dim = 4 + n_classes = 3 + seq_len = 100 + n_samples_per_class = 10 + + key = jax.random.PRNGKey(42) + + seqs = [] + labels = [] + for class_idx in range(n_classes): + freq = (class_idx + 1) * 0.5 # Different frequency per class + for _ in range(n_samples_per_class): + key, subkey = jax.random.split(key) + t = jnp.linspace(0, 2 * jnp.pi, seq_len).reshape(-1, 1) + noise = 0.01 * jax.random.normal(subkey, (seq_len, data_dim)) + seq = jnp.sin(freq * t * jnp.arange(1, data_dim + 1)) + noise + seqs.append(seq) + labels.append(class_idx) + + train_seqs = jnp.stack(seqs) # (n_samples, seq_len, data_dim) + labels = jnp.array(labels, dtype=jnp.int32) + return data_dim, n_classes, train_seqs, labels + + +####################### ESN CLASSIFIER DIMENSION TESTS ##################### + + +def test_esn_classifier_initialization(): + """Test that ESNClassifier initializes with correct dimensions.""" + data_dim = 4 + n_classes = 3 + res_dim = 100 + + classifier = orc.classifier.ESNClassifier( + data_dim=data_dim, n_classes=n_classes, res_dim=res_dim, seed=0 + ) + + assert classifier.data_dim == data_dim + assert classifier.n_classes == n_classes + assert classifier.res_dim == res_dim + assert classifier.in_dim == data_dim + assert classifier.out_dim == n_classes + assert classifier.state_repr == "final" + assert classifier.embedding.in_dim == data_dim + + +def test_esn_classifier_force_shapes(): + """Test that force method produces correct output shapes.""" + data_dim = 4 + n_classes = 3 + res_dim = 100 + seq_len = 50 + + classifier = orc.classifier.ESNClassifier( + data_dim=data_dim, n_classes=n_classes, res_dim=res_dim, seed=0 + ) + + in_seq = jax.random.normal(jax.random.PRNGKey(0), (seq_len, data_dim)) + res_state = jnp.zeros(res_dim, dtype=jnp.float64) + + res_seq = classifier.force(in_seq, res_state) + + assert res_seq.shape == (seq_len, res_dim) + + +def test_esn_classifier_classify_shapes(): + """Test that classify method produces correct output shapes.""" + data_dim = 4 + n_classes = 3 + res_dim = 100 + seq_len = 50 + + classifier = orc.classifier.ESNClassifier( + data_dim=data_dim, n_classes=n_classes, res_dim=res_dim, seed=0 + ) + + in_seq = jax.random.normal(jax.random.PRNGKey(0), (seq_len, data_dim)) + res_state = jnp.zeros(res_dim, dtype=jnp.float64) + + probs = classifier.classify(in_seq, res_state) + + assert probs.shape == (n_classes,) + # Probabilities should sum to 1 + assert jnp.allclose(jnp.sum(probs), 1.0, atol=1e-6) + # All probabilities should be non-negative + assert jnp.all(probs >= 0) + + +def test_esn_classifier_classify_default_state(): + """Test that classify works with default (None) reservoir state.""" + data_dim = 4 + n_classes = 3 + res_dim = 100 + seq_len = 50 + + classifier = orc.classifier.ESNClassifier( + data_dim=data_dim, n_classes=n_classes, res_dim=res_dim, seed=0 + ) + + in_seq = jax.random.normal(jax.random.PRNGKey(0), (seq_len, data_dim)) + + probs = classifier.classify(in_seq) + + assert probs.shape == (n_classes,) + assert jnp.allclose(jnp.sum(probs), 1.0, atol=1e-6) + + +####################### ESN CLASSIFIER TRAINING TESTS ##################### + + +def test_esn_classifier_train(dummy_classification_data): + """Test training of ESNClassifier on dummy data.""" + data_dim, n_classes, train_seqs, labels = dummy_classification_data + + res_dim = 200 + + classifier = orc.classifier.ESNClassifier( + data_dim=data_dim, + n_classes=n_classes, + res_dim=res_dim, + seed=0, + ) + + classifier_trained = orc.classifier.train_ESNClassifier( + classifier, + train_seqs=train_seqs, + labels=labels, + beta=1e-6, + ) + + assert classifier_trained is not None + + # Verify trained model can classify and produces valid probabilities + probs = classifier_trained.classify(train_seqs[0]) + assert probs.shape == (n_classes,) + assert jnp.allclose(jnp.sum(probs), 1.0, atol=1e-6) + assert jnp.all(jnp.isfinite(probs)) + + +def test_esn_classifier_mean_state(dummy_classification_data): + """Test classifier with mean state representation.""" + data_dim, n_classes, train_seqs, labels = dummy_classification_data + + res_dim = 200 + + classifier = orc.classifier.ESNClassifier( + data_dim=data_dim, + n_classes=n_classes, + res_dim=res_dim, + seed=0, + state_repr="mean", + ) + + classifier_trained = orc.classifier.train_ESNClassifier( + classifier, + train_seqs=train_seqs, + labels=labels, + spinup=10, + beta=1e-6, + ) + + assert classifier_trained is not None + + # Verify trained model can classify + probs = classifier_trained.classify(train_seqs[0], spinup=10) + assert probs.shape == (n_classes,) + assert jnp.allclose(jnp.sum(probs), 1.0, atol=1e-6) + assert jnp.all(jnp.isfinite(probs)) + + +def test_esn_classifier_invalid_state_repr(): + """Test that invalid state_repr raises an error.""" + with pytest.raises(ValueError, match="state_repr must be"): + orc.classifier.ESNClassifier( + data_dim=4, n_classes=3, res_dim=100, state_repr="invalid" + )