import itertools, warnings
import numpy as np
from spdal import TRACED
warnings.filterwarnings("ignore")
class CountingTRACED(TRACED):
"""TRACED with counters around its own learning steps. Behaviour is unchanged."""
def reset_events(self):
self.events = {"created": 0, "updated": 0, "merged": 0}
def create_new_neuron(self, X, y):
self.events["created"] += 1
return super().create_new_neuron(X, y)
def update_parameter(self, neuron, alpha, X, Y, Y_index):
self.events["updated"] += 1
return super().update_parameter(neuron, alpha, X, Y, Y_index)
def merge_neuron(self, alpha, y):
merged, alpha = super().merge_neuron(alpha, y)
self.events["merged"] += int(merged)
return merged, alpha
N_CHUNKS, CHUNK, START, ARRIVE, APPEAR, OUTLIERS = 20, 50, 4, 16, 8, 0.05
SD = np.array([0.5, 0.35])
CENTRES = np.array([[0.0, 0.0], [3.0, 0.0], [1.5, 2.2]]) # class A, class B, class C (new)
TARGETS = {"towards": np.array([1.0, 0.0]), "past": np.array([-1.5, 1.2])}
LO, HI, NX, NY = np.array([-3.5, -2.0]), np.array([4.5, 3.5]), 56, 39
SCENARIOS = ["stationary", "towards", "past", "new", "outliers"]
def centre_b(k, scenario):
"""Class B stays at its first distribution, then moves gradually to its target (ease in and out)."""
if scenario not in TARGETS:
return CENTRES[1]
t = np.clip((k - START) / (ARRIVE - START), 0, 1)
t = t * t * (3 - 2 * t)
return CENTRES[1] + t * (TARGETS[scenario] - CENTRES[1])
def make_stream(scenario, seed):
rng = np.random.default_rng(seed)
chunks = []
for k in range(N_CHUNKS):
n_classes = 3 if scenario == "new" and k >= APPEAR else 2
y = rng.integers(0, n_classes, CHUNK)
centres = CENTRES.copy()
centres[1] = centre_b(k, scenario)
X = centres[y] + rng.normal(0, 1, (CHUNK, 2)) * SD
if scenario == "outliers":
noisy = rng.random(CHUNK) < OUTLIERS
X[noisy] = rng.uniform(LO, HI, (noisy.sum(), 2))
chunks.append((X, y))
return chunks
gx, gy = np.linspace(LO[0], HI[0], NX), np.linspace(LO[1], HI[1], NY)
GRID = np.array([[a, b] for b in gy for a in gx])
def run(scenario, seed, record=False, alpha=0.5):
chunks = make_stream(scenario, seed)
classes = [0, 1, 2] if scenario == "new" else [0, 1]
model = CountingTRACED(r=1, delta=4, alpha=alpha, beta=0.5, reduce_dims=0, method="none")
steps, acc = [], {"none": [], "outside": []}
for k in range(N_CHUNKS - 1):
model.reset_events()
model.partial_fit(*chunks[k], classes=classes)
X_next, y_next = chunks[k + 1]
pred = {}
for method in acc:
model.method = method
pred[method] = model.predict(X_next).astype(int)
acc[method].append(float(np.mean(pred[method] == y_next)))
if not record:
continue
maps = {}
for method in acc:
model.method = method
maps[method] = model.predict(GRID).astype(int)
status = np.where((pred["none"] != y_next) & (pred["outside"] == y_next), 1, # fixed by the rule
np.where((pred["none"] == y_next) & (pred["outside"] != y_next), -1, 0)) # broken by the rule
steps.append({
"neurons": [[*np.round(n["center"], 3), *np.round(n["eig_component"].ravel(), 3),
*np.round(n["width"], 3), int(n["y"]), int(n["n"]), *np.round(n["displacement"], 3)]
for n in model.neuron_list],
"decision": "".join(map(str, maps["none"])),
"changed": np.flatnonzero(maps["none"] != maps["outside"]).tolist(),
"test": [[round(float(a), 3), round(float(b), 3), int(c), int(s)]
for (a, b), c, s in zip(X_next, y_next, status)],
"learned": [[round(float(a), 3), round(float(b), 3), int(c)] for (a, b), c in zip(*chunks[k])],
"target": centre_b(k + 1, scenario).round(3).tolist() if scenario in TARGETS else None,
"events": dict(model.events),
"acc": {m: acc[m][-1] for m in acc},
})
return steps, {m: float(np.mean(v)) for m, v in acc.items()}, acc
runs, summary = {}, {}
for scenario in SCENARIOS:
key = scenario
runs[key] = run(scenario, seed=0, record=True)[0]
per_seed = [run(scenario, seed=s) for s in range(5)]
gains = [p[1]["outside"] - p[1]["none"] for p in per_seed]
summary[key] = {"none": float(np.mean([p[1]["none"] for p in per_seed])),
"outside": float(np.mean([p[1]["outside"] for p in per_seed])),
"gain_min": float(min(gains)), "gain_max": float(max(gains)),
# accuracy on the first chunk that contains the new class, and on the one after it
"appear": [float(np.mean([p[2]["none"][APPEAR - 1] for p in per_seed])),
float(np.mean([p[2]["none"][APPEAR] for p in per_seed]))]}
ojs_define(tr_runs=runs, tr_summary=summary,
tr_meta={"nx": NX, "ny": NY, "lo": LO.tolist(), "hi": HI.tolist(), "n_steps": N_CHUNKS - 1,
"N0": TRACED().N0, "appear": APPEAR})