Notebooks
04 — Continual Learning
Incremental updates with new classes using sufficient statistics.
Vyntri's defining feature: update() adds new data — new examples of known classes and brand-new classes — analytically, from sufficient statistics.
Setup
from pathlib import Path
import numpy as np
from PIL import Image
from vyntri import Vyntri
from vyntri.data import split
def make_solid(root, classes, per_class, seed=0):
colors = {'cats': (200, 90, 40), 'dogs': (40, 180, 70), 'birds': (50, 80, 200)}
root = Path(root)
rng = np.random.default_rng(seed)
for name in classes:
folder = root / name
folder.mkdir(parents=True, exist_ok=True)
for j in range(per_class):
noise = rng.normal(0, 6, (32, 32, 3))
base = np.asarray(colors[name], dtype=np.float64).reshape(1, 1, 3)
img = (base + noise).clip(0, 255).astype(np.uint8)
Image.fromarray(img).save(folder / f'{name}_{j:02d}.png')
return root
DATA = Path('continual_data')
initial = make_solid(DATA / 'initial', ['cats', 'dogs'], per_class=12, seed=1)
new_data = make_solid(DATA / 'new_data', ['cats', 'birds'], per_class=10, seed=2)
test = make_solid(DATA / 'test', ['cats', 'dogs', 'birds'], per_class=8, seed=99)
print('initial classes : cats, dogs')
print('update data : more cats + NEW class birds')1. Fit on initial data
s = split(str(initial), train=0.7, val=0.1, test=0.2, seed=42)
model = Vyntri(seed=42)
model.fit(train=s.train, val=s.val, progress=False)
print('classes after fit :', list(model.classes_))
print('validation accuracy :', round(model.validation_accuracy_, 3))2. Update with old class + new class
update() accepts mixed batches — new examples of cats and a brand-new birds class in one call.
result = model.update(str(new_data))
print('classes before :', result.classes_before)
print('classes after :', result.classes_after)
print('new classes :', result.new_classes)
print('samples added :', result.n_samples)3. Class registry is stable
Existing class ids are never renumbered; new classes get new ids.
print('class -> id mapping:', model.class_to_idx_)
img = str(test / 'birds' / 'birds_00.png')
print(model.predict(img))4. Sequential == joint (measured)
An updated model matches a model refit on all the data. The sufficient-statistics update re-solves the same joint solution.
import shutil
from vyntri.data import FolderDataset
# Create joint dataset (all data combined)
joint_dir = DATA / 'joint'
if joint_dir.exists():
shutil.rmtree(joint_dir)
for name in ['cats', 'dogs']:
shutil.copytree(initial / name, joint_dir / name)
shutil.copytree(new_data / 'cats', joint_dir / 'cats', dirs_exist_ok=True)
shutil.copytree(new_data / 'birds', joint_dir / 'birds')
# Fit joint model on the same split
joint = Vyntri(seed=42)
joint.fit(train=s.train, val=s.val, progress=False)
# Compare
test_ds = FolderDataset(str(test))
seq_acc = model.evaluate(test_ds).accuracy
joint_acc = joint.evaluate(test_ds).accuracy
print(f'updated model (sequential): test acc = {seq_acc:.3f}')
print(f'joint refit model : test acc = {joint_acc:.3f}')
agree = sum(
model.predict(p).label == joint.predict(p).label
for p in test.rglob('*.png')
)
print(f'predictions agree on {agree}/{len(list(test.rglob("*.png")))} test images')5. Save, load, and update again
The sufficient statistics travel with the saved model, so a loaded model can keep learning without any historical raw data.
model.save('continual.vyntri')
loaded = Vyntri.load('continual.vyntri')
more = make_solid(DATA / 'even_more', ['birds'], per_class=6, seed=3)
res = loaded.update(str(more))
print('after load + update:', res.classes_after, '| new:', res.new_classes)