Skip to content

Continual Learning — EWC + On-Chip Plasticity

Train with backprop, deploy with local plasticity. Combines Elastic Weight Consolidation (EWC) for catastrophic forgetting protection with STDP-based local learning rules that can run on-chip.

Pipeline

  1. Train task A with standard backprop
  2. Compute Fisher diagonal from per-sample gradients → identifies important parameters
  3. Train task B with EWC penalty: L_ewc = (λ/2) * Σ F_i * (θ_i - θ*_i)²
  4. Extract plasticity configs for on-chip deployment (STDP parameters derived from weight statistics)
  5. Deploy with active on-chip plasticity rules

No framework provides the integrated pipeline from "trained model" to "deployed model with active on-chip plasticity."

Components

  • ContinualLearner — Main engine managing EWC and plasticity extraction.
Parameter Default Meaning
weights (required) List of weight matrices per layer
layer_names auto Names for each layer
ewc_lambda 1000.0 EWC regularization strength
plasticity_rule "stdp" Default on-chip plasticity rule

Key methods:

  • compute_fisher(gradients_per_sample) — Compute Fisher Information diagonal from per-sample gradients
  • ewc_penalty() → float — Current EWC regularization penalty
  • register_task(accuracy) — Register task completion
  • update_weights(new_weights) — Update weights after training
  • extract_plasticity_configs() → list of PlasticityConfig — Derive on-chip deployment parameters
  • report()ContinualReport — Full report with accuracy history

  • PlasticityConfig — Per-layer on-chip plasticity configuration (rule, tau_pre/post, LR, weight bounds, homeostatic target).

  • ContinualReport — Report dataclass with summary() method.

Usage

Python
from sc_neurocore.continual import ContinualLearner
import numpy as np

weights = [np.random.randn(64, 32) * 0.3, np.random.randn(10, 64) * 0.3]
learner = ContinualLearner(weights, layer_names=["hidden", "output"])

# After training task A: compute Fisher
gradients = [[np.random.randn(64, 32), np.random.randn(10, 64)] for _ in range(100)]
learner.compute_fisher(gradients)
learner.register_task(accuracy=0.95)

# Training task B: EWC penalty prevents forgetting
print(f"EWC penalty: {learner.ewc_penalty():.4f}")

# Deploy with on-chip plasticity
configs = learner.extract_plasticity_configs()
for c in configs:
    print(f"{c.layer_name}: rule={c.rule}, lr+={c.lr_potentiation:.4f}")

Reference: Kirkpatrick et al. 2017 — "Overcoming catastrophic forgetting in neural networks" (EWC).

See Tutorial 58: Continual Learning.

sc_neurocore.continual

Continual learning: train → deploy → adapt without catastrophic forgetting.

ContinualLearner

Continual learning engine with EWC and on-chip plasticity extraction.

Parameters

weights : list of ndarray Initial trained weight matrices per layer. layer_names : list of str Names for each layer. ewc_lambda : float Regularization strength for EWC (0 = no protection). plasticity_rule : str Default on-chip plasticity rule for all layers.

Source code in src/sc_neurocore/continual/engine.py
Python
 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
class ContinualLearner:
    """Continual learning engine with EWC and on-chip plasticity extraction.

    Parameters
    ----------
    weights : list of ndarray
        Initial trained weight matrices per layer.
    layer_names : list of str
        Names for each layer.
    ewc_lambda : float
        Regularization strength for EWC (0 = no protection).
    plasticity_rule : str
        Default on-chip plasticity rule for all layers.
    """

    def __init__(
        self,
        weights: list[np.ndarray],
        layer_names: list[str] | None = None,
        ewc_lambda: float = 1000.0,
        plasticity_rule: str = "stdp",
    ):
        self.weights = [w.copy() for w in weights]
        self.layer_names = layer_names or [f"layer_{i}" for i in range(len(weights))]
        self.ewc_lambda = ewc_lambda
        self.plasticity_rule = plasticity_rule

        self._fisher_diag: list[np.ndarray] | None = None
        self._star_weights: list[np.ndarray] | None = None
        self._task_count = 0
        self._accuracy_history: list[float] = []

    def compute_fisher(self, gradients_per_sample: list[list[np.ndarray]]) -> None:
        """Compute Fisher Information diagonal from per-sample gradients.

        Parameters
        ----------
        gradients_per_sample : list of (list of ndarray)
            Outer list: samples. Inner list: gradient per layer.
            Each ndarray has same shape as the corresponding weight matrix.
        """
        n_layers = len(self.weights)
        fisher = [np.zeros_like(w) for w in self.weights]

        for sample_grads in gradients_per_sample:
            for i in range(min(len(sample_grads), n_layers)):
                fisher[i] += sample_grads[i] ** 2

        n_samples = max(len(gradients_per_sample), 1)
        self._fisher_diag = [f / n_samples for f in fisher]
        self._star_weights = [w.copy() for w in self.weights]

    def ewc_penalty(self) -> float:
        """Compute EWC regularization penalty."""
        if self._fisher_diag is None or self._star_weights is None:
            return 0.0
        penalty = 0.0
        for w, w_star, fisher in zip(self.weights, self._star_weights, self._fisher_diag):
            penalty += float(np.sum(fisher * (w - w_star) ** 2))
        return 0.5 * self.ewc_lambda * penalty

    def register_task(self, accuracy: float) -> None:
        """Register completion of a task."""
        self._task_count += 1
        self._accuracy_history.append(accuracy)

    def update_weights(self, new_weights: list[np.ndarray]) -> None:
        """Update weights (e.g., after training on a new task)."""
        self.weights = [w.copy() for w in new_weights]

    def extract_plasticity_configs(self) -> list[PlasticityConfig]:
        """Extract per-layer plasticity parameters for on-chip deployment.

        Derives STDP parameters from weight statistics:
        - LR proportional to weight variance (active synapses learn faster)
        - Bounds from weight range
        - Homeostatic target from mean firing rate proxy
        """
        configs = []
        for i, (w, name) in enumerate(zip(self.weights, self.layer_names)):
            w_std = float(np.std(w))
            w_range = float(w.max() - w.min())
            lr_scale = min(w_std * 0.1, 0.05)

            configs.append(
                PlasticityConfig(
                    layer_name=name,
                    rule=self.plasticity_rule,
                    tau_pre=20.0,
                    tau_post=20.0,
                    lr_potentiation=lr_scale,
                    lr_depression=lr_scale * 1.2,
                    w_min=float(w.min()),
                    w_max=float(w.max()),
                    homeostatic_target=0.1,
                )
            )
        return configs

    def report(self) -> ContinualReport:
        """Generate a continual learning report."""
        configs = self.extract_plasticity_configs()
        return ContinualReport(
            tasks_trained=self._task_count,
            ewc_lambda=self.ewc_lambda,
            fisher_computed=self._fisher_diag is not None,
            plasticity_configs=configs,
            accuracy_per_task=list(self._accuracy_history),
        )

compute_fisher(gradients_per_sample)

Compute Fisher Information diagonal from per-sample gradients.

Parameters

gradients_per_sample : list of (list of ndarray) Outer list: samples. Inner list: gradient per layer. Each ndarray has same shape as the corresponding weight matrix.

Source code in src/sc_neurocore/continual/engine.py
Python
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def compute_fisher(self, gradients_per_sample: list[list[np.ndarray]]) -> None:
    """Compute Fisher Information diagonal from per-sample gradients.

    Parameters
    ----------
    gradients_per_sample : list of (list of ndarray)
        Outer list: samples. Inner list: gradient per layer.
        Each ndarray has same shape as the corresponding weight matrix.
    """
    n_layers = len(self.weights)
    fisher = [np.zeros_like(w) for w in self.weights]

    for sample_grads in gradients_per_sample:
        for i in range(min(len(sample_grads), n_layers)):
            fisher[i] += sample_grads[i] ** 2

    n_samples = max(len(gradients_per_sample), 1)
    self._fisher_diag = [f / n_samples for f in fisher]
    self._star_weights = [w.copy() for w in self.weights]

ewc_penalty()

Compute EWC regularization penalty.

Source code in src/sc_neurocore/continual/engine.py
Python
142
143
144
145
146
147
148
149
def ewc_penalty(self) -> float:
    """Compute EWC regularization penalty."""
    if self._fisher_diag is None or self._star_weights is None:
        return 0.0
    penalty = 0.0
    for w, w_star, fisher in zip(self.weights, self._star_weights, self._fisher_diag):
        penalty += float(np.sum(fisher * (w - w_star) ** 2))
    return 0.5 * self.ewc_lambda * penalty

register_task(accuracy)

Register completion of a task.

Source code in src/sc_neurocore/continual/engine.py
Python
151
152
153
154
def register_task(self, accuracy: float) -> None:
    """Register completion of a task."""
    self._task_count += 1
    self._accuracy_history.append(accuracy)

update_weights(new_weights)

Update weights (e.g., after training on a new task).

Source code in src/sc_neurocore/continual/engine.py
Python
156
157
158
def update_weights(self, new_weights: list[np.ndarray]) -> None:
    """Update weights (e.g., after training on a new task)."""
    self.weights = [w.copy() for w in new_weights]

extract_plasticity_configs()

Extract per-layer plasticity parameters for on-chip deployment.

Derives STDP parameters from weight statistics: - LR proportional to weight variance (active synapses learn faster) - Bounds from weight range - Homeostatic target from mean firing rate proxy

Source code in src/sc_neurocore/continual/engine.py
Python
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
def extract_plasticity_configs(self) -> list[PlasticityConfig]:
    """Extract per-layer plasticity parameters for on-chip deployment.

    Derives STDP parameters from weight statistics:
    - LR proportional to weight variance (active synapses learn faster)
    - Bounds from weight range
    - Homeostatic target from mean firing rate proxy
    """
    configs = []
    for i, (w, name) in enumerate(zip(self.weights, self.layer_names)):
        w_std = float(np.std(w))
        w_range = float(w.max() - w.min())
        lr_scale = min(w_std * 0.1, 0.05)

        configs.append(
            PlasticityConfig(
                layer_name=name,
                rule=self.plasticity_rule,
                tau_pre=20.0,
                tau_post=20.0,
                lr_potentiation=lr_scale,
                lr_depression=lr_scale * 1.2,
                w_min=float(w.min()),
                w_max=float(w.max()),
                homeostatic_target=0.1,
            )
        )
    return configs

report()

Generate a continual learning report.

Source code in src/sc_neurocore/continual/engine.py
Python
189
190
191
192
193
194
195
196
197
198
def report(self) -> ContinualReport:
    """Generate a continual learning report."""
    configs = self.extract_plasticity_configs()
    return ContinualReport(
        tasks_trained=self._task_count,
        ewc_lambda=self.ewc_lambda,
        fisher_computed=self._fisher_diag is not None,
        plasticity_configs=configs,
        accuracy_per_task=list(self._accuracy_history),
    )

PlasticityConfig dataclass

Per-layer on-chip plasticity configuration.

Extracted from training for hardware deployment.

Parameters

layer_name : str rule : str Plasticity rule: 'stdp', 'r_stdp', 'homeostatic', 'none'. tau_pre : float Pre-synaptic trace time constant (ms). tau_post : float Post-synaptic trace time constant (ms). lr_potentiation : float Potentiation learning rate (A+). lr_depression : float Depression learning rate (A-). w_min : float Minimum weight. w_max : float Maximum weight. homeostatic_target : float Target firing rate for homeostatic regulation.

Source code in src/sc_neurocore/continual/engine.py
Python
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
57
58
59
60
61
62
63
64
65
@dataclass
class PlasticityConfig:
    """Per-layer on-chip plasticity configuration.

    Extracted from training for hardware deployment.

    Parameters
    ----------
    layer_name : str
    rule : str
        Plasticity rule: 'stdp', 'r_stdp', 'homeostatic', 'none'.
    tau_pre : float
        Pre-synaptic trace time constant (ms).
    tau_post : float
        Post-synaptic trace time constant (ms).
    lr_potentiation : float
        Potentiation learning rate (A+).
    lr_depression : float
        Depression learning rate (A-).
    w_min : float
        Minimum weight.
    w_max : float
        Maximum weight.
    homeostatic_target : float
        Target firing rate for homeostatic regulation.
    """

    layer_name: str
    rule: str = "stdp"
    tau_pre: float = 20.0
    tau_post: float = 20.0
    lr_potentiation: float = 0.01
    lr_depression: float = 0.012
    w_min: float = 0.0
    w_max: float = 1.0
    homeostatic_target: float = 0.1

ContinualReport dataclass

Report from a continual learning session.

Source code in src/sc_neurocore/continual/engine.py
Python
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@dataclass
class ContinualReport:
    """Report from a continual learning session."""

    tasks_trained: int = 0
    ewc_lambda: float = 0.0
    fisher_computed: bool = False
    plasticity_configs: list[PlasticityConfig] = field(default_factory=list)
    accuracy_per_task: list[float] = field(default_factory=list)

    def summary(self) -> str:
        lines = [
            f"Continual Learning Report: {self.tasks_trained} tasks",
            f"  EWC lambda: {self.ewc_lambda}",
            f"  Fisher diagonal: {'computed' if self.fisher_computed else 'not computed'}",
            f"  Plasticity configs: {len(self.plasticity_configs)} layers",
        ]
        for i, acc in enumerate(self.accuracy_per_task):
            lines.append(f"  Task {i}: accuracy = {acc:.4f}")
        return "\n".join(lines)