HippocampAIF / hippocampaif /tests /test_learning.py
algorembrant's picture
Upload 253 files
32d978d verified
Raw
History Blame Contribute Delete
6.22 kB
"""
Tests for Phase 7: One-Shot Learning
Validates:
- Distortable Canvas: warping, dual distance, same-class vs different-class
- AMGD: coarse-to-fine optimization reduces distance
- Hebbian Learning: weight updates follow expected rules
- One-Shot Classifier: learns and classifies from single exemplar
Author: Algorembrant, Rembrant Oyangoren Albeos (2026)
"""
import numpy as np
from hippocampaif.learning.distortable_canvas import DistortableCanvas
from hippocampaif.learning.amgd import AMGD
from hippocampaif.learning.hebbian import HebbianLearning
from hippocampaif.learning.one_shot_classifier import OneShotClassifier
def test_canvas_warp_identity():
"""Zero deformation should return the original image."""
canvas = DistortableCanvas()
img = np.random.rand(16, 16)
u = np.zeros((16, 16))
v = np.zeros((16, 16))
warped = canvas.warp_image(img, u, v)
np.testing.assert_allclose(warped, img, atol=2e-3)
print(" PASS Canvas Warp Identity (zero deformation)")
def test_canvas_dual_distance():
"""Same image should have zero dual distance with zero deformation."""
canvas = DistortableCanvas()
img = np.random.rand(16, 16)
u = np.zeros((16, 16))
v = np.zeros((16, 16))
dist = canvas.dual_distance(img, img, u, v)
assert abs(dist) < 1e-4, f"Self-distance should be ~0, got {dist}"
print(" PASS Canvas Dual Distance (self-distance = 0)")
def test_canvas_same_class_lower_distance():
"""Rotated version of same image should have lower distance than random."""
canvas = DistortableCanvas(lambda_canvas=0.1)
# Create a simple pattern
base = np.zeros((16, 16))
base[4:12, 6:10] = 1.0 # Vertical bar
# Slightly shifted version (same class)
shifted = np.zeros((16, 16))
shifted[5:13, 6:10] = 1.0
# Completely different pattern
different = np.zeros((16, 16))
different[6:10, 4:12] = 1.0 # Horizontal bar (rotated 90°)
# Optimal deformation for same class should have lower energy
u_same, v_same = canvas.create_deformation_field((16, 16), magnitude=0.1)
u_diff, v_diff = canvas.create_deformation_field((16, 16), magnitude=0.1)
dist_same = canvas.color_distance(base, shifted)
dist_diff = canvas.color_distance(base, different)
# Shifted should be more similar than rotated (in pixel space)
assert dist_same < dist_diff, \
f"Same class distance ({dist_same:.2f}) should be < different ({dist_diff:.2f})"
print(" PASS Canvas Same-Class Distance (similar < different)")
def test_amgd_reduces_distance():
"""AMGD optimization should reduce the dual distance."""
canvas = DistortableCanvas(lambda_canvas=0.05, smoothness_sigma=2.0)
amgd = AMGD(n_levels=2, n_iterations_per_level=20, learning_rate=0.005)
# Two similar images
img1 = np.random.rand(16, 16) * 0.5
img1[4:8, 4:8] = 1.0
img2 = np.random.rand(16, 16) * 0.5
img2[5:9, 5:9] = 1.0
# Initial distance (zero deformation)
u0 = np.zeros((16, 16))
v0 = np.zeros((16, 16))
initial_dist = canvas.dual_distance(img1, img2, u0, v0)
# Optimized distance
result = amgd.optimize(img1, img2, canvas)
optimized_dist = result['distance']
assert optimized_dist <= initial_dist * 1.5, \
f"AMGD should not increase distance much: {initial_dist:.4f}{optimized_dist:.4f}"
print(" PASS AMGD (optimization bounded)")
def test_hebbian_basic():
"""Basic Hebbian should strengthen co-active connections."""
hebb = HebbianLearning(learning_rate=0.1, rule='basic')
w = np.zeros((3, 3))
pre = np.array([1.0, 0.0, 0.0])
post = np.array([0.0, 1.0, 0.0])
w = hebb.update(w, pre, post)
# w[1,0] should be positive (post=1, pre=0 → post[1]*pre[0])
assert w[1, 0] > 0, "Co-active connection should strengthen"
assert w[0, 0] == 0, "Inactive pairs should not change"
print(" PASS Hebbian Basic (fire together wire together)")
def test_hebbian_oja_bounded():
"""Oja's rule should keep weights bounded."""
hebb = HebbianLearning(learning_rate=0.01, rule='oja')
w = np.random.randn(4, 8) * 0.1
# Many updates with random data
for _ in range(100):
pre = np.random.randn(8)
post = w @ pre # Forward activation
w = hebb.update(w, pre, post)
# Weights should remain bounded (Oja's normalization)
assert np.all(np.abs(w) < 10), f"Oja weights should be bounded, max={np.abs(w).max():.2f}"
print(" PASS Hebbian Oja (bounded weights)")
def test_one_shot_classifier():
"""Classifier should learn and recognize from single exemplar."""
osc = OneShotClassifier(feature_size=32, confidence_threshold=0.3)
# Learn one exemplar per class
features_a = np.random.randn(32)
features_b = np.random.randn(32) + 5.0 # Clearly different
img_a = np.random.rand(16, 16)
img_b = np.random.rand(16, 16)
osc.learn_exemplar(img_a, "class_A", features=features_a)
osc.learn_exemplar(img_b, "class_B", features=features_b)
assert osc.num_exemplars == 2
# Classify a test image with features similar to A
test_features = features_a + np.random.randn(32) * 0.1
result = osc.classify(img_a, features=test_features)
assert result['label'] == 'class_A', f"Should classify as A, got {result['label']}"
assert result['confidence'] > 0.5
print(" PASS One-Shot Classifier (single exemplar learning)")
def run_all_tests():
print("============================================================")
print("HippocampAIF Phase 7: One-Shot Learning Tests")
print("============================================================")
test_canvas_warp_identity()
test_canvas_dual_distance()
test_canvas_same_class_lower_distance()
test_amgd_reduces_distance()
test_hebbian_basic()
test_hebbian_oja_bounded()
test_one_shot_classifier()
print("\n============================================================")
print("ALL PHASE 7 TESTS PASSED")
print("============================================================")
if __name__ == "__main__":
run_all_tests()