Skip to content

Transfer Learning

The transfer package provides validated checkpoint serialization plus layer freezing helpers for SNN transfer-learning workflows.

Python
import numpy as np
from sc_neurocore.transfer import (
    SNNCheckpoint,
    TransferConfig,
    apply_transfer_config,
    freeze_layers,
    load_checkpoint,
    save_checkpoint,
)

checkpoint = SNNCheckpoint(
    weights=[
        np.ones((32, 64), dtype=np.float64),
        np.ones((10, 32), dtype=np.float64),
    ],
    layer_names=["hidden", "output"],
    layer_sizes=[(64, 32), (32, 10)],
    neuron_types=["LIF", "LIF"],
    metadata={"task": "mnist"},
)

save_checkpoint(checkpoint, "model_v1")
checkpoint = load_checkpoint("model_v1")
freeze_layers(checkpoint, layer_names=["hidden"])
checkpoint, learning_rates = apply_transfer_config(
    checkpoint,
    TransferConfig(freeze_until=0, lr_backbone=0.0, lr_head=0.01),
)

Checkpoints are stored as a model_v1.npz weight archive plus a model_v1.json metadata file. Loading validates the JSON metadata schema, rejects unexpected archive members, opens .npz weights with pickle disabled, rejects non-finite weights, and checks every matrix against its (input_features, output_features) layer-size contract.

Validation Surface

Surface Contract
Python Constructor and loader reject duplicate layer names, shape mismatches, non-finite weights, unknown frozen layers, invalid learning rates, and non-JSON metadata.
Rust src/sc_neurocore/accel/rust/safety/checkpoint.rs and fine_tune.rs compile as standalone safety mirrors with unit tests.
Julia src/sc_neurocore/accel/julia/transfer/checkpoint.jl and fine_tune.jl validate the same in-memory checkpoint and transfer schedule invariants.
Mojo src/sc_neurocore/accel/mojo/kernels/checkpoint.mojo and fine_tune.mojo run deterministic validation kernels.

Local Evidence

benchmarks/results/bench_transfer.json records local, non-isolated regression evidence. The 2026-06-27 run reports:

Check Result
Python checkpoint roundtrip 100 calls in 0.154779 s, 646.081 calls/s
Python apply_transfer_config 100 calls in 0.009153 s, 10925.32 calls/s
Rust checkpoint compile/tests pass
Rust fine-tune compile/tests pass
Julia validation pass
Mojo checkpoint/fine-tune validation pass

These timings are regression evidence only; the artifact marks production_benchmark_claim as false.

See Tutorial 81: Transfer Learning.

sc_neurocore.transfer

Checkpoint serialization and transfer-learning helpers for SNN models.

The package exports the complete public transfer workflow: build or load a validated checkpoint, freeze or unfreeze named layers, apply a learning-rate schedule, and save the resulting state back to disk.

SNNCheckpoint dataclass

Complete dense-weight SNN checkpoint for transfer workflows.

Parameters

weights: One two-dimensional weight matrix per layer. A matrix shape must be (output_features, input_features) for the matching layer_sizes entry (input_features, output_features). layer_names: Unique layer names in forward order. layer_sizes: (input_features, output_features) pairs for each layer. neuron_types: Optional neuron-model labels, either empty or one label per layer. metadata: JSON-serializable provenance or training metadata. frozen_layers: Layer names currently marked non-trainable.

Source code in src/sc_neurocore/transfer/checkpoint.py
Python
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 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
@dataclass
class SNNCheckpoint:
    """Complete dense-weight SNN checkpoint for transfer workflows.

    Parameters
    ----------
    weights:
        One two-dimensional weight matrix per layer. A matrix shape must be
        ``(output_features, input_features)`` for the matching ``layer_sizes``
        entry ``(input_features, output_features)``.
    layer_names:
        Unique layer names in forward order.
    layer_sizes:
        ``(input_features, output_features)`` pairs for each layer.
    neuron_types:
        Optional neuron-model labels, either empty or one label per layer.
    metadata:
        JSON-serializable provenance or training metadata.
    frozen_layers:
        Layer names currently marked non-trainable.
    """

    weights: list[FloatArray]
    layer_names: list[str]
    layer_sizes: list[tuple[int, int]]
    neuron_types: list[str] = field(default_factory=list)
    metadata: dict[str, object] = field(default_factory=dict)
    frozen_layers: list[str] = field(default_factory=list)

    def __post_init__(self) -> None:
        """Normalize arrays and reject inconsistent checkpoint state."""
        _validate_string_vector(self.layer_names, "layer_names")
        if len(set(self.layer_names)) != len(self.layer_names):
            raise ValueError("Checkpoint layer_names must be unique")
        if len(self.weights) != len(self.layer_names):
            raise ValueError("Checkpoint weights length must match layer_names")
        if len(self.layer_sizes) != len(self.layer_names):
            raise ValueError("Checkpoint layer_sizes length must match layer_names")
        self.layer_sizes = [_validate_layer_size_tuple(size) for size in self.layer_sizes]
        if self.neuron_types:
            _validate_string_vector(self.neuron_types, "neuron_types")
            if len(self.neuron_types) != len(self.layer_names):
                raise ValueError("Checkpoint neuron_types length must match layer_names")
        _validate_string_vector(self.frozen_layers, "frozen_layers")
        unknown_frozen = sorted(set(self.frozen_layers) - set(self.layer_names))
        if unknown_frozen:
            raise ValueError("Checkpoint frozen_layers must reference known layers")
        self.frozen_layers = sorted(set(self.frozen_layers))
        _validate_json_serializable(self.metadata)
        self.weights = [
            _validate_weight_array(weight, f"layer_{index}", self.layer_sizes[index])
            for index, weight in enumerate(self.weights)
        ]

    @property
    def n_layers(self) -> int:
        """Return the number of serialized layers."""
        return len(self.weights)

    @property
    def total_params(self) -> int:
        """Return the total number of scalar weight parameters."""
        return int(sum(weight.size for weight in self.weights))

n_layers property

Return the number of serialized layers.

total_params property

Return the total number of scalar weight parameters.

__post_init__()

Normalize arrays and reject inconsistent checkpoint state.

Source code in src/sc_neurocore/transfer/checkpoint.py
Python
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __post_init__(self) -> None:
    """Normalize arrays and reject inconsistent checkpoint state."""
    _validate_string_vector(self.layer_names, "layer_names")
    if len(set(self.layer_names)) != len(self.layer_names):
        raise ValueError("Checkpoint layer_names must be unique")
    if len(self.weights) != len(self.layer_names):
        raise ValueError("Checkpoint weights length must match layer_names")
    if len(self.layer_sizes) != len(self.layer_names):
        raise ValueError("Checkpoint layer_sizes length must match layer_names")
    self.layer_sizes = [_validate_layer_size_tuple(size) for size in self.layer_sizes]
    if self.neuron_types:
        _validate_string_vector(self.neuron_types, "neuron_types")
        if len(self.neuron_types) != len(self.layer_names):
            raise ValueError("Checkpoint neuron_types length must match layer_names")
    _validate_string_vector(self.frozen_layers, "frozen_layers")
    unknown_frozen = sorted(set(self.frozen_layers) - set(self.layer_names))
    if unknown_frozen:
        raise ValueError("Checkpoint frozen_layers must reference known layers")
    self.frozen_layers = sorted(set(self.frozen_layers))
    _validate_json_serializable(self.metadata)
    self.weights = [
        _validate_weight_array(weight, f"layer_{index}", self.layer_sizes[index])
        for index, weight in enumerate(self.weights)
    ]

TransferConfig dataclass

Configuration for checkpoint-based SNN transfer learning.

Parameters

freeze_until: Freeze all layers up to and including this layer name or index. -1 means do not add frozen layers. lr_backbone: Learning rate for frozen backbone layers, usually zero or a small value. lr_head: Learning rate for unfrozen task-head layers.

Source code in src/sc_neurocore/transfer/fine_tune.py
Python
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@dataclass
class TransferConfig:
    """Configuration for checkpoint-based SNN transfer learning.

    Parameters
    ----------
    freeze_until:
        Freeze all layers up to and including this layer name or index. ``-1``
        means do not add frozen layers.
    lr_backbone:
        Learning rate for frozen backbone layers, usually zero or a small value.
    lr_head:
        Learning rate for unfrozen task-head layers.
    """

    freeze_until: str | int = -1
    lr_backbone: float = 0.0
    lr_head: float = 0.01

    def __post_init__(self) -> None:
        """Reject invalid freeze targets and learning rates."""
        if isinstance(self.freeze_until, bool) or not isinstance(self.freeze_until, (str, int)):
            raise ValueError("TransferConfig freeze_until must be a layer name or integer index")
        if isinstance(self.freeze_until, int) and self.freeze_until < -1:
            raise ValueError("TransferConfig freeze_until index must be -1 or non-negative")
        if (
            not math.isfinite(self.lr_backbone)
            or not math.isfinite(self.lr_head)
            or self.lr_backbone < 0.0
            or self.lr_head < 0.0
        ):
            raise ValueError("TransferConfig learning rates must be finite and non-negative")

__post_init__()

Reject invalid freeze targets and learning rates.

Source code in src/sc_neurocore/transfer/fine_tune.py
Python
44
45
46
47
48
49
50
51
52
53
54
55
56
def __post_init__(self) -> None:
    """Reject invalid freeze targets and learning rates."""
    if isinstance(self.freeze_until, bool) or not isinstance(self.freeze_until, (str, int)):
        raise ValueError("TransferConfig freeze_until must be a layer name or integer index")
    if isinstance(self.freeze_until, int) and self.freeze_until < -1:
        raise ValueError("TransferConfig freeze_until index must be -1 or non-negative")
    if (
        not math.isfinite(self.lr_backbone)
        or not math.isfinite(self.lr_head)
        or self.lr_backbone < 0.0
        or self.lr_head < 0.0
    ):
        raise ValueError("TransferConfig learning rates must be finite and non-negative")

load_checkpoint(path)

Load and validate an SNN checkpoint from path.npz and path.json.

Parameters

path: Base path without extension.

Returns

SNNCheckpoint: Reconstructed checkpoint with finite float64 weight arrays.

Source code in src/sc_neurocore/transfer/checkpoint.py
Python
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
def load_checkpoint(path: str | Path) -> SNNCheckpoint:
    """Load and validate an SNN checkpoint from ``path.npz`` and ``path.json``.

    Parameters
    ----------
    path:
        Base path without extension.

    Returns
    -------
    SNNCheckpoint:
        Reconstructed checkpoint with finite ``float64`` weight arrays.
    """
    path = Path(path)

    with _json_path(path).open(encoding="utf-8") as handle:
        raw_meta: object = json.load(handle)
    meta = _validate_metadata(raw_meta)
    n_layers = meta["n_layers"]

    with np.load(_npz_path(path), allow_pickle=False) as data:
        expected_keys = [f"layer_{index}" for index in range(n_layers)]
        if set(data.files) != set(expected_keys):
            raise ValueError("Checkpoint weight archive does not match metadata layers")
        weights = [
            _validate_weight_array(data[key], key, meta["layer_sizes"][index])
            for index, key in enumerate(expected_keys)
        ]

    checkpoint = SNNCheckpoint(
        weights=weights,
        layer_names=meta["layer_names"],
        layer_sizes=meta["layer_sizes"],
        neuron_types=meta["neuron_types"],
        metadata=meta["metadata"],
        frozen_layers=meta["frozen_layers"],
    )
    if checkpoint.total_params != meta["total_params"]:
        raise ValueError("Checkpoint metadata total_params does not match weights")
    return checkpoint

save_checkpoint(checkpoint, path)

Save an SNN checkpoint to path.npz plus path.json.

Parameters

checkpoint: Validated checkpoint to serialize. path: Base path without extension. Parent directories are created.

Source code in src/sc_neurocore/transfer/checkpoint.py
Python
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
def save_checkpoint(checkpoint: SNNCheckpoint, path: str | Path) -> None:
    """Save an SNN checkpoint to ``path.npz`` plus ``path.json``.

    Parameters
    ----------
    checkpoint:
        Validated checkpoint to serialize.
    path:
        Base path without extension. Parent directories are created.
    """
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)

    weight_dict = {f"layer_{index}": weight for index, weight in enumerate(checkpoint.weights)}
    np.savez_compressed(_npz_path(path), **weight_dict)  # type: ignore[arg-type]

    meta = {
        "layer_names": checkpoint.layer_names,
        "layer_sizes": checkpoint.layer_sizes,
        "neuron_types": checkpoint.neuron_types,
        "frozen_layers": checkpoint.frozen_layers,
        "n_layers": checkpoint.n_layers,
        "total_params": checkpoint.total_params,
        "metadata": checkpoint.metadata,
    }
    _json_path(path).write_text(
        json.dumps(meta, allow_nan=False, indent=2) + "\n",
        encoding="utf-8",
    )

apply_transfer_config(checkpoint, config)

Apply a transfer config and return per-layer learning rates.

Parameters

checkpoint: Checkpoint to mutate according to config. config: Validated transfer schedule.

Returns

tuple[SNNCheckpoint, list[float]]: The mutated checkpoint and one learning rate per layer.

Source code in src/sc_neurocore/transfer/fine_tune.py
Python
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
def apply_transfer_config(
    checkpoint: SNNCheckpoint,
    config: TransferConfig,
) -> tuple[SNNCheckpoint, list[float]]:
    """Apply a transfer config and return per-layer learning rates.

    Parameters
    ----------
    checkpoint:
        Checkpoint to mutate according to ``config``.
    config:
        Validated transfer schedule.

    Returns
    -------
    tuple[SNNCheckpoint, list[float]]:
        The mutated checkpoint and one learning rate per layer.
    """
    if isinstance(config.freeze_until, int) and config.freeze_until >= 0:
        freeze_layers(checkpoint, until_index=config.freeze_until)
    elif isinstance(config.freeze_until, str):
        if config.freeze_until not in checkpoint.layer_names:
            raise ValueError("TransferConfig freeze_until layer is not present in checkpoint")
        freeze_layers(checkpoint, until_index=checkpoint.layer_names.index(config.freeze_until))

    per_layer_lr = [
        config.lr_backbone if name in checkpoint.frozen_layers else config.lr_head
        for name in checkpoint.layer_names
    ]
    return checkpoint, per_layer_lr

freeze_layers(checkpoint, layer_names=None, until_index=None)

Mark checkpoint layers as frozen.

Parameters

checkpoint: Checkpoint to mutate. layer_names: Specific layer names to freeze. until_index: Freeze every layer with index less than or equal to this value.

Returns

SNNCheckpoint: The same checkpoint object with frozen_layers updated.

Source code in src/sc_neurocore/transfer/fine_tune.py
Python
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def freeze_layers(
    checkpoint: SNNCheckpoint,
    layer_names: Sequence[str] | None = None,
    until_index: int | None = None,
) -> SNNCheckpoint:
    """Mark checkpoint layers as frozen.

    Parameters
    ----------
    checkpoint:
        Checkpoint to mutate.
    layer_names:
        Specific layer names to freeze.
    until_index:
        Freeze every layer with index less than or equal to this value.

    Returns
    -------
    SNNCheckpoint:
        The same checkpoint object with ``frozen_layers`` updated.
    """
    frozen = set(checkpoint.frozen_layers)

    if layer_names is not None:
        _validate_layer_names(checkpoint, layer_names)
        frozen.update(layer_names)

    if until_index is not None:
        _validate_until_index(checkpoint, until_index)
        for index, name in enumerate(checkpoint.layer_names):
            if index <= until_index:
                frozen.add(name)

    checkpoint.frozen_layers = sorted(frozen)
    return checkpoint

unfreeze_layers(checkpoint, layer_names=None, all_layers=False)

Mark checkpoint layers as trainable.

Parameters

checkpoint: Checkpoint to mutate. layer_names: Specific layer names to unfreeze. all_layers: When true, clear every frozen-layer marker.

Returns

SNNCheckpoint: The same checkpoint object with frozen_layers updated.

Source code in src/sc_neurocore/transfer/fine_tune.py
Python
 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
def unfreeze_layers(
    checkpoint: SNNCheckpoint,
    layer_names: Sequence[str] | None = None,
    all_layers: bool = False,
) -> SNNCheckpoint:
    """Mark checkpoint layers as trainable.

    Parameters
    ----------
    checkpoint:
        Checkpoint to mutate.
    layer_names:
        Specific layer names to unfreeze.
    all_layers:
        When true, clear every frozen-layer marker.

    Returns
    -------
    SNNCheckpoint:
        The same checkpoint object with ``frozen_layers`` updated.
    """
    if all_layers:
        checkpoint.frozen_layers = []
        return checkpoint

    if layer_names is not None:
        _validate_layer_names(checkpoint, layer_names)
        removals = set(layer_names)
        checkpoint.frozen_layers = [
            name for name in checkpoint.frozen_layers if name not in removals
        ]

    return checkpoint