| """ |
| 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) |
| |
| |
| base = np.zeros((16, 16)) |
| base[4:12, 6:10] = 1.0 |
| |
| |
| shifted = np.zeros((16, 16)) |
| shifted[5:13, 6:10] = 1.0 |
| |
| |
| different = np.zeros((16, 16)) |
| different[6:10, 4:12] = 1.0 |
| |
| |
| 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) |
| |
| |
| 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) |
| |
| |
| 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 |
| |
| |
| u0 = np.zeros((16, 16)) |
| v0 = np.zeros((16, 16)) |
| initial_dist = canvas.dual_distance(img1, img2, u0, v0) |
| |
| |
| 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) |
| |
| |
| 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 |
| |
| |
| for _ in range(100): |
| pre = np.random.randn(8) |
| post = w @ pre |
| w = hebb.update(w, pre, post) |
| |
| |
| 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) |
| |
| |
| features_a = np.random.randn(32) |
| features_b = np.random.randn(32) + 5.0 |
| |
| 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 |
| |
| |
| 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() |
|
|