Skip to content

Few-Shot Meta-Learning — Hebbian Associative Memory

Learn from 1-5 examples using spike-timing plasticity instead of gradient descent. Two approaches are exposed: class-indexed Hebbian memory and prototypical classification in spike-rate space.

HebbianFewShot — Associative Memory

Support patterns are stored via one-shot Hebbian update: memory[label] += lr * pattern. Queries are classified by cosine similarity to the stored class memories. The few_shot_episode() method handles the full N-way K-shot protocol: reset -> store support set -> classify query set.

Parameter Default Meaning
n_features (required) Input feature dimension
n_classes (required) Number of classes
lr_hebbian 0.1 Hebbian learning rate for storage

Accepts spike-rate vectors (n_features,) or raw spike trains (T, n_features) — automatically averaged over time.

query_scores() returns one bounded cosine score per class. Classes without support examples score 0.0, and querying before storage raises ValueError. export_weights() returns a defensive copy of the class-memory matrix for hardware export or downstream inspection.

SpikePrototypeNet — Prototypical Network

Computes class prototypes as mean spike-rate vectors from the support set. Classifies queries by nearest prototype using cosine similarity, negative Euclidean distance, or negative normalized Hamming disagreement. It stores the most recent prototypes only so they can be inspected or exported.

Parameter Default Meaning
n_features (required) Feature dimension
metric "cosine" Distance metric: "cosine", "euclidean", or "hamming"

Usage

Python
from sc_neurocore.few_shot import HebbianFewShot, SpikePrototypeNet
import numpy as np

# 5-way 1-shot with Hebbian memory
learner = HebbianFewShot(n_features=64, n_classes=5)
support_x = [np.random.rand(64) for _ in range(5)]
support_y = [0, 1, 2, 3, 4]
query_x = [np.random.rand(64) for _ in range(10)]
predictions = learner.few_shot_episode(support_x, support_y, query_x)

# Prototypical network (no training needed)
proto = SpikePrototypeNet(n_features=64, metric="cosine")
predictions = proto.classify(support_x, support_y, query_x)
prototypes = proto.export_prototypes()

Reference: HAAM (BICS 2024).

Validation and Benchmark Evidence

The public Python API is covered by the module-specific tests/test_few_shot.py surface. The maintained polyglot mirrors are:

Surface Scope Local check
Python Public API, vector and temporal inputs, exports, validation guards pytest tests/test_few_shot.py
Rust Safety mirror for vector HAAM and prototype episodes rustc --edition=2021 --test src/sc_neurocore/accel/rust/safety/haam.rs
Julia Vector HAAM and prototype validation mirror julia --startup-file=no --history-file=no ... validate_haam()
Mojo Standalone vector/temporal HAAM and prototype validation kernel mojo src/sc_neurocore/accel/mojo/kernels/haam.mojo

benchmarks/results/bench_few_shot_haam.json records the latest local, non-isolated evidence. The Python public API measured 1000 deterministic calls at 3581.011 Hebbian episodes/s and 4770.742 prototype classifications/s on the current workstation. Rust, Julia, and Mojo validation checks all passed in that same run. The artifact is a local regression record, not an isolated production benchmark claim.

See Tutorial 84: Few-Shot Meta-Learning.

sc_neurocore.few_shot.haam

Spike-domain few-shot learners for associative-memory episodes.

The module provides two small deterministic learners for N-way K-shot spike classification. HebbianFewShot stores support examples in class-indexed associative memory and scores queries by cosine similarity. SpikePrototypeNet keeps no training state between calls; it computes support-set prototypes and classifies query vectors by cosine similarity, Euclidean distance, or binary Hamming distance.

HebbianFewShot

Class-indexed Hebbian memory for few-shot spike episodes.

Parameters

n_features : int Number of spike-rate features per pattern after temporal averaging. n_classes : int Number of class slots stored by the associative memory. lr_hebbian : float, default=0.1 Non-negative multiplier applied when support patterns are accumulated into their class memory rows.

Source code in src/sc_neurocore/few_shot/haam.py
Python
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
class HebbianFewShot:
    """Class-indexed Hebbian memory for few-shot spike episodes.

    Parameters
    ----------
    n_features : int
        Number of spike-rate features per pattern after temporal averaging.
    n_classes : int
        Number of class slots stored by the associative memory.
    lr_hebbian : float, default=0.1
        Non-negative multiplier applied when support patterns are accumulated
        into their class memory rows.
    """

    def __init__(self, n_features: int, n_classes: int, lr_hebbian: float = 0.1) -> None:
        self.n_features = _validate_positive_int(n_features, "n_features")
        self.n_classes = _validate_positive_int(n_classes, "n_classes")
        self.lr_hebbian = float(lr_hebbian)
        if not np.isfinite(self.lr_hebbian) or self.lr_hebbian < 0.0:
            raise ValueError("lr_hebbian must be finite and non-negative")

        self.memory: FloatArray = np.zeros((self.n_classes, self.n_features), dtype=np.float64)
        self._counts: NDArray[np.int64] = np.zeros(self.n_classes, dtype=np.int64)

    def store(self, spike_pattern: ArrayLike, label: int) -> None:
        """Store one support pattern in the class memory.

        Parameters
        ----------
        spike_pattern : array_like
            Spike-rate vector with shape ``(n_features,)`` or spike train with
            shape ``(T, n_features)``. Temporal spike trains are averaged over
            the first axis before storage.
        label : int
            Class slot to update.

        Raises
        ------
        ValueError
            If the label is out of range or the pattern cannot be resolved to a
            finite feature vector.
        """
        class_index = _validate_label(label, self.n_classes)
        pattern = _as_feature_vector(spike_pattern, self.n_features, name="spike_pattern")
        self.memory[class_index] += self.lr_hebbian * pattern
        self._counts[class_index] += 1

    def query_scores(self, spike_pattern: ArrayLike) -> FloatArray:
        """Return cosine scores for a query against every stored class.

        Parameters
        ----------
        spike_pattern : array_like
            Query spike-rate vector or temporal spike train.

        Returns
        -------
        numpy.ndarray
            One score per class. Classes with no support examples receive a
            score of ``0``.
        """
        pattern = _as_feature_vector(spike_pattern, self.n_features, name="spike_pattern")
        scores = np.zeros(self.n_classes, dtype=np.float64)
        for class_index in range(self.n_classes):
            if self._counts[class_index] > 0:
                scores[class_index] = _cosine_score(self.memory[class_index], pattern)
        return scores

    def query(self, spike_pattern: ArrayLike) -> int:
        """Classify one query pattern by nearest stored memory.

        Parameters
        ----------
        spike_pattern : array_like
            Query spike-rate vector or temporal spike train.

        Returns
        -------
        int
            Predicted class label.

        Raises
        ------
        ValueError
            If no support examples have been stored.
        """
        if not np.any(self._counts):
            raise ValueError("at least one support example must be stored before query")
        return int(np.argmax(self.query_scores(spike_pattern)))

    def few_shot_episode(
        self,
        support_x: Sequence[ArrayLike],
        support_y: Sequence[int],
        query_x: Sequence[ArrayLike],
    ) -> list[int]:
        """Run one reset-store-query few-shot episode.

        Parameters
        ----------
        support_x : list of array_like
            Support spike patterns.
        support_y : list of int
            Class label for each support pattern.
        query_x : list of array_like
            Query spike patterns to classify after support storage.

        Returns
        -------
        list of int
            Predicted labels for the query set.

        Raises
        ------
        ValueError
            If the support pattern and label lists have different lengths.
        """
        if len(support_x) != len(support_y):
            raise ValueError("support_x and support_y must have the same length")

        self.reset()
        for pattern, label in zip(support_x, support_y, strict=True):
            self.store(pattern, label)
        return [self.query(query) for query in query_x]

    def export_weights(self) -> FloatArray:
        """Return a defensive copy of the class memory matrix.

        Returns
        -------
        numpy.ndarray
            Matrix with shape ``(n_classes, n_features)`` containing the
            accumulated Hebbian support memory.
        """
        return self.memory.copy()

    def reset(self) -> None:
        """Clear the memory matrix and support counts."""
        self.memory.fill(0.0)
        self._counts.fill(0)

store(spike_pattern, label)

Store one support pattern in the class memory.

Parameters

spike_pattern : array_like Spike-rate vector with shape (n_features,) or spike train with shape (T, n_features). Temporal spike trains are averaged over the first axis before storage. label : int Class slot to update.

Raises

ValueError If the label is out of range or the pattern cannot be resolved to a finite feature vector.

Source code in src/sc_neurocore/few_shot/haam.py
Python
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def store(self, spike_pattern: ArrayLike, label: int) -> None:
    """Store one support pattern in the class memory.

    Parameters
    ----------
    spike_pattern : array_like
        Spike-rate vector with shape ``(n_features,)`` or spike train with
        shape ``(T, n_features)``. Temporal spike trains are averaged over
        the first axis before storage.
    label : int
        Class slot to update.

    Raises
    ------
    ValueError
        If the label is out of range or the pattern cannot be resolved to a
        finite feature vector.
    """
    class_index = _validate_label(label, self.n_classes)
    pattern = _as_feature_vector(spike_pattern, self.n_features, name="spike_pattern")
    self.memory[class_index] += self.lr_hebbian * pattern
    self._counts[class_index] += 1

query_scores(spike_pattern)

Return cosine scores for a query against every stored class.

Parameters

spike_pattern : array_like Query spike-rate vector or temporal spike train.

Returns

numpy.ndarray One score per class. Classes with no support examples receive a score of 0.

Source code in src/sc_neurocore/few_shot/haam.py
Python
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def query_scores(self, spike_pattern: ArrayLike) -> FloatArray:
    """Return cosine scores for a query against every stored class.

    Parameters
    ----------
    spike_pattern : array_like
        Query spike-rate vector or temporal spike train.

    Returns
    -------
    numpy.ndarray
        One score per class. Classes with no support examples receive a
        score of ``0``.
    """
    pattern = _as_feature_vector(spike_pattern, self.n_features, name="spike_pattern")
    scores = np.zeros(self.n_classes, dtype=np.float64)
    for class_index in range(self.n_classes):
        if self._counts[class_index] > 0:
            scores[class_index] = _cosine_score(self.memory[class_index], pattern)
    return scores

query(spike_pattern)

Classify one query pattern by nearest stored memory.

Parameters

spike_pattern : array_like Query spike-rate vector or temporal spike train.

Returns

int Predicted class label.

Raises

ValueError If no support examples have been stored.

Source code in src/sc_neurocore/few_shot/haam.py
Python
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def query(self, spike_pattern: ArrayLike) -> int:
    """Classify one query pattern by nearest stored memory.

    Parameters
    ----------
    spike_pattern : array_like
        Query spike-rate vector or temporal spike train.

    Returns
    -------
    int
        Predicted class label.

    Raises
    ------
    ValueError
        If no support examples have been stored.
    """
    if not np.any(self._counts):
        raise ValueError("at least one support example must be stored before query")
    return int(np.argmax(self.query_scores(spike_pattern)))

few_shot_episode(support_x, support_y, query_x)

Run one reset-store-query few-shot episode.

Parameters

support_x : list of array_like Support spike patterns. support_y : list of int Class label for each support pattern. query_x : list of array_like Query spike patterns to classify after support storage.

Returns

list of int Predicted labels for the query set.

Raises

ValueError If the support pattern and label lists have different lengths.

Source code in src/sc_neurocore/few_shot/haam.py
Python
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def few_shot_episode(
    self,
    support_x: Sequence[ArrayLike],
    support_y: Sequence[int],
    query_x: Sequence[ArrayLike],
) -> list[int]:
    """Run one reset-store-query few-shot episode.

    Parameters
    ----------
    support_x : list of array_like
        Support spike patterns.
    support_y : list of int
        Class label for each support pattern.
    query_x : list of array_like
        Query spike patterns to classify after support storage.

    Returns
    -------
    list of int
        Predicted labels for the query set.

    Raises
    ------
    ValueError
        If the support pattern and label lists have different lengths.
    """
    if len(support_x) != len(support_y):
        raise ValueError("support_x and support_y must have the same length")

    self.reset()
    for pattern, label in zip(support_x, support_y, strict=True):
        self.store(pattern, label)
    return [self.query(query) for query in query_x]

export_weights()

Return a defensive copy of the class memory matrix.

Returns

numpy.ndarray Matrix with shape (n_classes, n_features) containing the accumulated Hebbian support memory.

Source code in src/sc_neurocore/few_shot/haam.py
Python
204
205
206
207
208
209
210
211
212
213
def export_weights(self) -> FloatArray:
    """Return a defensive copy of the class memory matrix.

    Returns
    -------
    numpy.ndarray
        Matrix with shape ``(n_classes, n_features)`` containing the
        accumulated Hebbian support memory.
    """
    return self.memory.copy()

reset()

Clear the memory matrix and support counts.

Source code in src/sc_neurocore/few_shot/haam.py
Python
215
216
217
218
def reset(self) -> None:
    """Clear the memory matrix and support counts."""
    self.memory.fill(0.0)
    self._counts.fill(0)

SpikePrototypeNet dataclass

Nearest-prototype classifier for spike-rate few-shot episodes.

Parameters

n_features : int Number of features per vector after temporal averaging. metric : {"cosine", "euclidean", "hamming"}, default="cosine" Distance or similarity metric used to score queries against support-set prototypes. hamming thresholds vectors at zero and scores by negative normalised bit disagreement.

Source code in src/sc_neurocore/few_shot/haam.py
Python
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
@dataclass
class SpikePrototypeNet:
    """Nearest-prototype classifier for spike-rate few-shot episodes.

    Parameters
    ----------
    n_features : int
        Number of features per vector after temporal averaging.
    metric : {"cosine", "euclidean", "hamming"}, default="cosine"
        Distance or similarity metric used to score queries against support-set
        prototypes. ``hamming`` thresholds vectors at zero and scores by negative
        normalised bit disagreement.
    """

    n_features: int
    metric: Metric = "cosine"
    prototypes: dict[int, FloatArray] = field(default_factory=dict, init=False)

    def __post_init__(self) -> None:
        """Validate the prototype classifier configuration after dataclass init."""
        self.n_features = _validate_positive_int(self.n_features, "n_features")
        if self.metric not in {"cosine", "euclidean", "hamming"}:
            raise ValueError("metric must be one of: cosine, euclidean, hamming")

    def classify(
        self,
        support_x: Sequence[ArrayLike],
        support_y: Sequence[int],
        query_x: Sequence[ArrayLike],
    ) -> list[int]:
        """Classify query patterns from support-set prototypes.

        Parameters
        ----------
        support_x : list of array_like
            Support spike patterns with one-dimensional or temporal shapes.
        support_y : list of int
            Label for each support pattern.
        query_x : list of array_like
            Query spike patterns to classify.

        Returns
        -------
        list of int
            Predicted class labels.

        Raises
        ------
        ValueError
            If support inputs are empty, labels are mismatched, or any pattern
            cannot be resolved to a finite feature vector.
        """
        self.prototypes = self._build_prototypes(support_x, support_y)
        classes = sorted(self.prototypes)
        predictions: list[int] = []

        for query in query_x:
            query_vector = _as_feature_vector(query, self.n_features, name="query")
            best_class = classes[0]
            best_score = -float("inf")
            for class_index in classes:
                score = _metric_score(self.metric, query_vector, self.prototypes[class_index])
                if score > best_score:
                    best_score = score
                    best_class = class_index
            predictions.append(best_class)

        return predictions

    def export_prototypes(self) -> dict[int, FloatArray]:
        """Return defensive copies of the most recently computed prototypes.

        Returns
        -------
        dict of int to numpy.ndarray
            Mapping from class label to prototype vector.
        """
        return {label: prototype.copy() for label, prototype in self.prototypes.items()}

    def _build_prototypes(
        self,
        support_x: Sequence[ArrayLike],
        support_y: Sequence[int],
    ) -> dict[int, FloatArray]:
        if not support_x:
            raise ValueError("support_x must contain at least one support pattern")
        if len(support_x) != len(support_y):
            raise ValueError("support_x and support_y must have the same length")

        grouped: dict[int, list[FloatArray]] = {}
        for pattern, label in zip(support_x, support_y, strict=True):
            if not isinstance(label, int):
                raise ValueError("support_y labels must be integers")
            grouped.setdefault(label, []).append(
                _as_feature_vector(pattern, self.n_features, name="support pattern")
            )

        prototypes: dict[int, FloatArray] = {}
        for label, patterns in sorted(grouped.items()):
            prototype = np.mean(np.stack(patterns, axis=0), axis=0, dtype=np.float64)
            prototypes[label] = np.asarray(prototype, dtype=np.float64)
        return prototypes

__post_init__()

Validate the prototype classifier configuration after dataclass init.

Source code in src/sc_neurocore/few_shot/haam.py
Python
239
240
241
242
243
def __post_init__(self) -> None:
    """Validate the prototype classifier configuration after dataclass init."""
    self.n_features = _validate_positive_int(self.n_features, "n_features")
    if self.metric not in {"cosine", "euclidean", "hamming"}:
        raise ValueError("metric must be one of: cosine, euclidean, hamming")

classify(support_x, support_y, query_x)

Classify query patterns from support-set prototypes.

Parameters

support_x : list of array_like Support spike patterns with one-dimensional or temporal shapes. support_y : list of int Label for each support pattern. query_x : list of array_like Query spike patterns to classify.

Returns

list of int Predicted class labels.

Raises

ValueError If support inputs are empty, labels are mismatched, or any pattern cannot be resolved to a finite feature vector.

Source code in src/sc_neurocore/few_shot/haam.py
Python
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
def classify(
    self,
    support_x: Sequence[ArrayLike],
    support_y: Sequence[int],
    query_x: Sequence[ArrayLike],
) -> list[int]:
    """Classify query patterns from support-set prototypes.

    Parameters
    ----------
    support_x : list of array_like
        Support spike patterns with one-dimensional or temporal shapes.
    support_y : list of int
        Label for each support pattern.
    query_x : list of array_like
        Query spike patterns to classify.

    Returns
    -------
    list of int
        Predicted class labels.

    Raises
    ------
    ValueError
        If support inputs are empty, labels are mismatched, or any pattern
        cannot be resolved to a finite feature vector.
    """
    self.prototypes = self._build_prototypes(support_x, support_y)
    classes = sorted(self.prototypes)
    predictions: list[int] = []

    for query in query_x:
        query_vector = _as_feature_vector(query, self.n_features, name="query")
        best_class = classes[0]
        best_score = -float("inf")
        for class_index in classes:
            score = _metric_score(self.metric, query_vector, self.prototypes[class_index])
            if score > best_score:
                best_score = score
                best_class = class_index
        predictions.append(best_class)

    return predictions

export_prototypes()

Return defensive copies of the most recently computed prototypes.

Returns

dict of int to numpy.ndarray Mapping from class label to prototype vector.

Source code in src/sc_neurocore/few_shot/haam.py
Python
290
291
292
293
294
295
296
297
298
def export_prototypes(self) -> dict[int, FloatArray]:
    """Return defensive copies of the most recently computed prototypes.

    Returns
    -------
    dict of int to numpy.ndarray
        Mapping from class label to prototype vector.
    """
    return {label: prototype.copy() for label, prototype in self.prototypes.items()}