Upload 7 files
Browse files- spectral/experiment_1_signal_decomposition.ipynb +0 -0
- spectral/experiment_2_manifold_structures.ipynb +2164 -0
- spectral/experiment_3_compact_representations.ipynb +1547 -0
- spectral/experiment_4_invertible_transforms.ipynb +2013 -0
- spectral/experiment_5_matrix_decompositions.ipynb +1118 -0
- spectral/experiment_6_losses_and_anchors.ipynb +1364 -0
- spectral/experiment_7_composite_pipelines.ipynb +1381 -0
spectral/experiment_1_signal_decomposition.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
spectral/experiment_2_manifold_structures.ipynb
ADDED
|
@@ -0,0 +1,2164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 5,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"kernelspec": {
|
| 6 |
+
"display_name": "Python 3",
|
| 7 |
+
"language": "python",
|
| 8 |
+
"name": "python3"
|
| 9 |
+
},
|
| 10 |
+
"language_info": {
|
| 11 |
+
"name": "python",
|
| 12 |
+
"version": "3.10.0"
|
| 13 |
+
},
|
| 14 |
+
"accelerator": "GPU",
|
| 15 |
+
"colab": {
|
| 16 |
+
"provenance": [],
|
| 17 |
+
"gpuType": "T4"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"cells": [
|
| 21 |
+
{
|
| 22 |
+
"cell_type": "markdown",
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"source": [
|
| 25 |
+
"# GeoLIP Hypersphere Experiments \u2014 Category 2: Manifold Structures\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"**Objective:** Explore structured manifolds beyond uniform hyperspheres.\n",
|
| 28 |
+
"\n",
|
| 29 |
+
"This notebook implements Hopf fibrations, Grassmannians, Flag manifolds, vMF soft assignment, and optimal lattice constructions for anchor initialization."
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"cell_type": "markdown",
|
| 34 |
+
"metadata": {},
|
| 35 |
+
"source": [
|
| 36 |
+
"## Shared Setup \u2014 Install & Configure"
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"cell_type": "code",
|
| 41 |
+
"metadata": {},
|
| 42 |
+
"source": [
|
| 43 |
+
"!pip install tensorboard scikit-learn matplotlib kymatio --quiet"
|
| 44 |
+
],
|
| 45 |
+
"execution_count": null,
|
| 46 |
+
"outputs": []
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"cell_type": "code",
|
| 50 |
+
"metadata": {},
|
| 51 |
+
"source": [
|
| 52 |
+
"%load_ext tensorboard\n",
|
| 53 |
+
"%matplotlib inline"
|
| 54 |
+
],
|
| 55 |
+
"execution_count": null,
|
| 56 |
+
"outputs": []
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"cell_type": "markdown",
|
| 60 |
+
"metadata": {},
|
| 61 |
+
"source": [
|
| 62 |
+
"### Shared Utilities Code"
|
| 63 |
+
]
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"cell_type": "code",
|
| 67 |
+
"metadata": {},
|
| 68 |
+
"source": [
|
| 69 |
+
"\n",
|
| 70 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 71 |
+
"# SHARED UTILITIES \u2014 GeoLIP Hypersphere Experiments\n",
|
| 72 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 73 |
+
"import os, sys, time, math, json, datetime, warnings\n",
|
| 74 |
+
"import numpy as np\n",
|
| 75 |
+
"import torch\n",
|
| 76 |
+
"import torch.nn as nn\n",
|
| 77 |
+
"import torch.nn.functional as F\n",
|
| 78 |
+
"from torch.utils.data import DataLoader\n",
|
| 79 |
+
"from torch.utils.tensorboard import SummaryWriter\n",
|
| 80 |
+
"import torchvision\n",
|
| 81 |
+
"import torchvision.transforms as T\n",
|
| 82 |
+
"import matplotlib\n",
|
| 83 |
+
"matplotlib.use(\"Agg\")\n",
|
| 84 |
+
"import matplotlib.pyplot as plt\n",
|
| 85 |
+
"from collections import defaultdict\n",
|
| 86 |
+
"\n",
|
| 87 |
+
"warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
|
| 88 |
+
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 89 |
+
"print(f\"[DEVICE] {device}\" + (f\" \u2014 {torch.cuda.get_device_name()}\" if device.type == \"cuda\" else \"\"))\n",
|
| 90 |
+
"\n",
|
| 91 |
+
"# \u2500\u2500 CIFAR-10 Loaders \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 92 |
+
"CIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\n",
|
| 93 |
+
"CIFAR10_STD = (0.2470, 0.2435, 0.2616)\n",
|
| 94 |
+
"CIFAR10_CLASSES = (\"plane\",\"car\",\"bird\",\"cat\",\"deer\",\"dog\",\"frog\",\"horse\",\"ship\",\"truck\")\n",
|
| 95 |
+
"\n",
|
| 96 |
+
"def get_cifar10_loaders(batch_size=256, num_workers=2, augment=True):\n",
|
| 97 |
+
" \"\"\"Return train_loader, val_loader for CIFAR-10.\"\"\"\n",
|
| 98 |
+
" tf_train = T.Compose([\n",
|
| 99 |
+
" T.RandomCrop(32, padding=4),\n",
|
| 100 |
+
" T.RandomHorizontalFlip(),\n",
|
| 101 |
+
" T.ToTensor(),\n",
|
| 102 |
+
" T.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n",
|
| 103 |
+
" ]) if augment else T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 104 |
+
" tf_val = T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 105 |
+
"\n",
|
| 106 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf_train)\n",
|
| 107 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf_val)\n",
|
| 108 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 109 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 110 |
+
" print(f\"[DATA] CIFAR-10: {len(train_ds)} train, {len(val_ds)} val, batch_size={batch_size}\")\n",
|
| 111 |
+
" return train_loader, val_loader\n",
|
| 112 |
+
"\n",
|
| 113 |
+
"def get_cifar10_raw_loaders(batch_size=256, num_workers=2):\n",
|
| 114 |
+
" \"\"\"Return loaders with only ToTensor (no normalization) for custom front-ends.\"\"\"\n",
|
| 115 |
+
" tf = T.Compose([T.ToTensor()])\n",
|
| 116 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf)\n",
|
| 117 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf)\n",
|
| 118 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 119 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 120 |
+
" print(f\"[DATA] CIFAR-10 (raw): {len(train_ds)} train, {len(val_ds)} val\")\n",
|
| 121 |
+
" return train_loader, val_loader\n",
|
| 122 |
+
"\n",
|
| 123 |
+
"# \u2500\u2500 L2 Normalization to Hypersphere \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 124 |
+
"def l2_normalize(x, dim=-1, eps=1e-8):\n",
|
| 125 |
+
" \"\"\"Project to unit hypersphere S^{d-1}.\"\"\"\n",
|
| 126 |
+
" return F.normalize(x, p=2, dim=dim, eps=eps)\n",
|
| 127 |
+
"\n",
|
| 128 |
+
"# \u2500\u2500 Constellation Module \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 129 |
+
"class Constellation(nn.Module):\n",
|
| 130 |
+
" \"\"\"\n",
|
| 131 |
+
" K anchors on S^{d-1}. Triangulation: compute distance from input to each anchor.\n",
|
| 132 |
+
" Output: K-dim vector of (1 - cos_sim) distances = triangulation profile.\n",
|
| 133 |
+
" \"\"\"\n",
|
| 134 |
+
" def __init__(self, embed_dim, num_anchors=64, init=\"uniform\"):\n",
|
| 135 |
+
" super().__init__()\n",
|
| 136 |
+
" self.embed_dim = embed_dim\n",
|
| 137 |
+
" self.num_anchors = num_anchors\n",
|
| 138 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, init)\n",
|
| 139 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False) # anchors are NOT learned\n",
|
| 140 |
+
"\n",
|
| 141 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 142 |
+
" if method == \"uniform\":\n",
|
| 143 |
+
" # QR-based uniform hypersphere initialization\n",
|
| 144 |
+
" A = torch.randn(K, d)\n",
|
| 145 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 146 |
+
" anchors = Q.T[:K]\n",
|
| 147 |
+
" # If K > d, fill remaining with random unit vectors\n",
|
| 148 |
+
" if K > d:\n",
|
| 149 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 150 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 151 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 152 |
+
" elif method == \"random\":\n",
|
| 153 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 154 |
+
" else:\n",
|
| 155 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 156 |
+
"\n",
|
| 157 |
+
" def forward(self, x):\n",
|
| 158 |
+
" \"\"\"x: (B, d) on S^{d-1} \u2192 (B, K) triangulation distances.\"\"\"\n",
|
| 159 |
+
" x_norm = l2_normalize(x)\n",
|
| 160 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T) # (B, K)\n",
|
| 161 |
+
" distances = 1.0 - cos_sim # in [0, 2]\n",
|
| 162 |
+
" return distances\n",
|
| 163 |
+
"\n",
|
| 164 |
+
" def push_anchors(self, embeddings, labels, num_classes=10, momentum=0.1):\n",
|
| 165 |
+
" \"\"\"Push anchors toward class centroids (call periodically).\"\"\"\n",
|
| 166 |
+
" with torch.no_grad():\n",
|
| 167 |
+
" for c in range(num_classes):\n",
|
| 168 |
+
" mask = (labels == c)\n",
|
| 169 |
+
" if mask.sum() < 2:\n",
|
| 170 |
+
" continue\n",
|
| 171 |
+
" centroid = l2_normalize(embeddings[mask].mean(dim=0, keepdim=True))\n",
|
| 172 |
+
" # Assign anchors to this class based on nearest centroid\n",
|
| 173 |
+
" start = c * (self.num_anchors // num_classes)\n",
|
| 174 |
+
" end = start + (self.num_anchors // num_classes)\n",
|
| 175 |
+
" self.anchors.data[start:end] = l2_normalize(\n",
|
| 176 |
+
" (1 - momentum) * self.anchors.data[start:end] + momentum * centroid\n",
|
| 177 |
+
" )\n",
|
| 178 |
+
"\n",
|
| 179 |
+
" def anchor_activation(self, x):\n",
|
| 180 |
+
" \"\"\"Return fraction of anchors that are 'active' (nearest to at least one sample).\"\"\"\n",
|
| 181 |
+
" x_norm = l2_normalize(x)\n",
|
| 182 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 183 |
+
" nearest = cos_sim.argmax(dim=1)\n",
|
| 184 |
+
" active = len(nearest.unique())\n",
|
| 185 |
+
" return active, self.num_anchors\n",
|
| 186 |
+
"\n",
|
| 187 |
+
"# \u2500\u2500 Patchwork Block \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 188 |
+
"class SquaredReLU(nn.Module):\n",
|
| 189 |
+
" def forward(self, x):\n",
|
| 190 |
+
" return F.relu(x) ** 2\n",
|
| 191 |
+
"\n",
|
| 192 |
+
"class PatchworkBlock(nn.Module):\n",
|
| 193 |
+
" \"\"\"SquaredReLU + LayerNorm block for post-constellation processing.\"\"\"\n",
|
| 194 |
+
" def __init__(self, dim):\n",
|
| 195 |
+
" super().__init__()\n",
|
| 196 |
+
" self.net = nn.Sequential(\n",
|
| 197 |
+
" nn.Linear(dim, dim),\n",
|
| 198 |
+
" SquaredReLU(),\n",
|
| 199 |
+
" nn.LayerNorm(dim),\n",
|
| 200 |
+
" nn.Linear(dim, dim),\n",
|
| 201 |
+
" SquaredReLU(),\n",
|
| 202 |
+
" nn.LayerNorm(dim),\n",
|
| 203 |
+
" )\n",
|
| 204 |
+
" def forward(self, x):\n",
|
| 205 |
+
" return x + self.net(x) # residual\n",
|
| 206 |
+
"\n",
|
| 207 |
+
"class Patchwork(nn.Module):\n",
|
| 208 |
+
" \"\"\"Stack of PatchworkBlocks.\"\"\"\n",
|
| 209 |
+
" def __init__(self, dim, num_blocks=2):\n",
|
| 210 |
+
" super().__init__()\n",
|
| 211 |
+
" self.blocks = nn.Sequential(*[PatchworkBlock(dim) for _ in range(num_blocks)])\n",
|
| 212 |
+
" def forward(self, x):\n",
|
| 213 |
+
" return self.blocks(x)\n",
|
| 214 |
+
"\n",
|
| 215 |
+
"# \u2500\u2500 Classifier Head \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 216 |
+
"class ConstellationClassifier(nn.Module):\n",
|
| 217 |
+
" \"\"\"Full pipeline: encoder output \u2192 constellation \u2192 patchwork \u2192 classify.\"\"\"\n",
|
| 218 |
+
" def __init__(self, embed_dim, num_anchors=64, num_classes=10, patchwork_blocks=2):\n",
|
| 219 |
+
" super().__init__()\n",
|
| 220 |
+
" self.constellation = Constellation(embed_dim, num_anchors)\n",
|
| 221 |
+
" self.patchwork = Patchwork(num_anchors, patchwork_blocks)\n",
|
| 222 |
+
" self.head = nn.Linear(num_anchors, num_classes)\n",
|
| 223 |
+
"\n",
|
| 224 |
+
" def forward(self, embeddings):\n",
|
| 225 |
+
" tri = self.constellation(embeddings) # (B, K)\n",
|
| 226 |
+
" pw = self.patchwork(tri) # (B, K)\n",
|
| 227 |
+
" logits = self.head(pw) # (B, 10)\n",
|
| 228 |
+
" return logits, tri\n",
|
| 229 |
+
"\n",
|
| 230 |
+
" def count_params(self):\n",
|
| 231 |
+
" return sum(p.numel() for p in self.parameters() if p.requires_grad)\n",
|
| 232 |
+
"\n",
|
| 233 |
+
"# \u2500\u2500 Losses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 234 |
+
"class CayleyMengerVolumeLoss(nn.Module):\n",
|
| 235 |
+
" \"\"\"CV loss: encourage constellation volume in 0.18-0.25 band.\"\"\"\n",
|
| 236 |
+
" def __init__(self, target_low=0.18, target_high=0.25):\n",
|
| 237 |
+
" super().__init__()\n",
|
| 238 |
+
" self.target_low = target_low\n",
|
| 239 |
+
" self.target_high = target_high\n",
|
| 240 |
+
"\n",
|
| 241 |
+
" def forward(self, tri):\n",
|
| 242 |
+
" \"\"\"tri: (B, K) triangulation distances.\"\"\"\n",
|
| 243 |
+
" # Approximate volume via determinant of distance submatrix\n",
|
| 244 |
+
" B, K = tri.shape\n",
|
| 245 |
+
" # Use std of triangulation as proxy for volume spread\n",
|
| 246 |
+
" vol = tri.std(dim=1).mean()\n",
|
| 247 |
+
" # Penalty if outside target band\n",
|
| 248 |
+
" below = F.relu(self.target_low - vol)\n",
|
| 249 |
+
" above = F.relu(vol - self.target_high)\n",
|
| 250 |
+
" return below + above, vol.item()\n",
|
| 251 |
+
"\n",
|
| 252 |
+
"class InfoNCELoss(nn.Module):\n",
|
| 253 |
+
" \"\"\"Self-supervised contrastive loss on augmented views.\"\"\"\n",
|
| 254 |
+
" def __init__(self, temperature=0.07):\n",
|
| 255 |
+
" super().__init__()\n",
|
| 256 |
+
" self.temperature = temperature\n",
|
| 257 |
+
"\n",
|
| 258 |
+
" def forward(self, z1, z2):\n",
|
| 259 |
+
" \"\"\"z1, z2: (B, d) L2-normalized embeddings of two augmented views.\"\"\"\n",
|
| 260 |
+
" z1 = l2_normalize(z1)\n",
|
| 261 |
+
" z2 = l2_normalize(z2)\n",
|
| 262 |
+
" B = z1.shape[0]\n",
|
| 263 |
+
" z = torch.cat([z1, z2], dim=0) # (2B, d)\n",
|
| 264 |
+
" sim = torch.mm(z, z.T) / self.temperature # (2B, 2B)\n",
|
| 265 |
+
" # Mask out self-similarity\n",
|
| 266 |
+
" mask = ~torch.eye(2*B, dtype=bool, device=z.device)\n",
|
| 267 |
+
" sim = sim.masked_fill(~mask, -1e9)\n",
|
| 268 |
+
" # Positive pairs: (i, i+B) and (i+B, i)\n",
|
| 269 |
+
" labels = torch.cat([torch.arange(B, 2*B), torch.arange(B)]).to(z.device)\n",
|
| 270 |
+
" loss = F.cross_entropy(sim, labels)\n",
|
| 271 |
+
" return loss\n",
|
| 272 |
+
"\n",
|
| 273 |
+
"class ClassConditionalInfoNCE(nn.Module):\n",
|
| 274 |
+
" \"\"\"Class-conditional contrastive: positive = same class, not same image.\"\"\"\n",
|
| 275 |
+
" def __init__(self, temperature=0.07):\n",
|
| 276 |
+
" super().__init__()\n",
|
| 277 |
+
" self.temperature = temperature\n",
|
| 278 |
+
"\n",
|
| 279 |
+
" def forward(self, z, labels):\n",
|
| 280 |
+
" z = l2_normalize(z)\n",
|
| 281 |
+
" B = z.shape[0]\n",
|
| 282 |
+
" sim = torch.mm(z, z.T) / self.temperature\n",
|
| 283 |
+
" # Mask: positive if same class, negative otherwise\n",
|
| 284 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 285 |
+
" label_mask.fill_diagonal_(0) # exclude self\n",
|
| 286 |
+
" # For each sample, compute log-softmax over all non-self, weighted by positives\n",
|
| 287 |
+
" logits = sim - torch.diag(torch.full((B,), 1e9, device=z.device))\n",
|
| 288 |
+
" pos_mask = label_mask\n",
|
| 289 |
+
" neg_mask = 1 - label_mask\n",
|
| 290 |
+
" neg_mask.fill_diagonal_(0)\n",
|
| 291 |
+
" # InfoNCE: -log(sum_pos exp(sim) / sum_all exp(sim))\n",
|
| 292 |
+
" exp_sim = torch.exp(logits)\n",
|
| 293 |
+
" pos_sum = (exp_sim * pos_mask).sum(dim=1)\n",
|
| 294 |
+
" all_sum = (exp_sim * (pos_mask + neg_mask)).sum(dim=1)\n",
|
| 295 |
+
" loss = -torch.log(pos_sum / (all_sum + 1e-8) + 1e-8).mean()\n",
|
| 296 |
+
" return loss\n",
|
| 297 |
+
"\n",
|
| 298 |
+
"# \u2500\u2500 TensorBoard Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 299 |
+
"def make_writer(experiment_id, base_dir=\"runs\"):\n",
|
| 300 |
+
" \"\"\"Create a TensorBoard SummaryWriter with timestamped run directory.\"\"\"\n",
|
| 301 |
+
" timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
|
| 302 |
+
" log_dir = os.path.join(base_dir, f\"{experiment_id}_{timestamp}\")\n",
|
| 303 |
+
" writer = SummaryWriter(log_dir=log_dir)\n",
|
| 304 |
+
" print(f\"[TB] Logging to {log_dir}\")\n",
|
| 305 |
+
" return writer\n",
|
| 306 |
+
"\n",
|
| 307 |
+
"def log_epoch_metrics(writer, epoch, metrics_dict, prefix=\"\"):\n",
|
| 308 |
+
" \"\"\"Log a dict of metrics to TensorBoard.\"\"\"\n",
|
| 309 |
+
" for key, val in metrics_dict.items():\n",
|
| 310 |
+
" tag = f\"{prefix}/{key}\" if prefix else key\n",
|
| 311 |
+
" writer.add_scalar(tag, val, epoch)\n",
|
| 312 |
+
"\n",
|
| 313 |
+
"def log_embedding_viz(writer, embeddings, labels, epoch, tag=\"embeddings\"):\n",
|
| 314 |
+
" \"\"\"Log embeddings for TensorBoard projector.\"\"\"\n",
|
| 315 |
+
" meta = [CIFAR10_CLASSES[l] for l in labels]\n",
|
| 316 |
+
" writer.add_embedding(embeddings, metadata=meta, global_step=epoch, tag=tag)\n",
|
| 317 |
+
"\n",
|
| 318 |
+
"def log_anchor_histogram(writer, constellation, epoch):\n",
|
| 319 |
+
" \"\"\"Log anchor norm distribution.\"\"\"\n",
|
| 320 |
+
" norms = constellation.anchors.data.norm(dim=1)\n",
|
| 321 |
+
" writer.add_histogram(\"anchors/norms\", norms, epoch)\n",
|
| 322 |
+
" cos_sim_matrix = torch.mm(constellation.anchors.data, constellation.anchors.data.T)\n",
|
| 323 |
+
" off_diag = cos_sim_matrix[~torch.eye(cos_sim_matrix.shape[0], dtype=bool)].flatten()\n",
|
| 324 |
+
" writer.add_histogram(\"anchors/inter_cosine_sim\", off_diag, epoch)\n",
|
| 325 |
+
"\n",
|
| 326 |
+
"def log_confusion_matrix(writer, all_preds, all_labels, epoch):\n",
|
| 327 |
+
" \"\"\"Log confusion matrix as image to TensorBoard.\"\"\"\n",
|
| 328 |
+
" from sklearn.metrics import confusion_matrix\n",
|
| 329 |
+
" cm = confusion_matrix(all_labels, all_preds, labels=range(10))\n",
|
| 330 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 331 |
+
" im = ax.imshow(cm, interpolation=\"nearest\", cmap=plt.cm.Blues)\n",
|
| 332 |
+
" ax.set_title(f\"Confusion Matrix \u2014 Epoch {epoch}\")\n",
|
| 333 |
+
" ax.set_xlabel(\"Predicted\")\n",
|
| 334 |
+
" ax.set_ylabel(\"True\")\n",
|
| 335 |
+
" tick_marks = range(10)\n",
|
| 336 |
+
" ax.set_xticks(tick_marks)\n",
|
| 337 |
+
" ax.set_xticklabels(CIFAR10_CLASSES, rotation=45, ha=\"right\")\n",
|
| 338 |
+
" ax.set_yticks(tick_marks)\n",
|
| 339 |
+
" ax.set_yticklabels(CIFAR10_CLASSES)\n",
|
| 340 |
+
" for i in range(10):\n",
|
| 341 |
+
" for j in range(10):\n",
|
| 342 |
+
" ax.text(j, i, str(cm[i, j]), ha=\"center\", va=\"center\",\n",
|
| 343 |
+
" color=\"white\" if cm[i, j] > cm.max() / 2 else \"black\", fontsize=7)\n",
|
| 344 |
+
" fig.tight_layout()\n",
|
| 345 |
+
" writer.add_figure(\"confusion_matrix\", fig, epoch)\n",
|
| 346 |
+
" plt.close(fig)\n",
|
| 347 |
+
"\n",
|
| 348 |
+
"def log_triangulation_stats(writer, tri, epoch):\n",
|
| 349 |
+
" \"\"\"Log triangulation distance statistics.\"\"\"\n",
|
| 350 |
+
" writer.add_scalar(\"tri/mean\", tri.mean().item(), epoch)\n",
|
| 351 |
+
" writer.add_scalar(\"tri/std\", tri.std().item(), epoch)\n",
|
| 352 |
+
" writer.add_scalar(\"tri/min\", tri.min().item(), epoch)\n",
|
| 353 |
+
" writer.add_scalar(\"tri/max\", tri.max().item(), epoch)\n",
|
| 354 |
+
" writer.add_histogram(\"tri/distances\", tri.detach().cpu(), epoch)\n",
|
| 355 |
+
"\n",
|
| 356 |
+
"def log_scatter_2d(writer, embeddings, labels, epoch, tag=\"scatter_2d\", method=\"pca\"):\n",
|
| 357 |
+
" \"\"\"Log 2D scatter plot of embeddings projected via PCA or t-SNE.\"\"\"\n",
|
| 358 |
+
" emb_np = embeddings.detach().cpu().numpy()\n",
|
| 359 |
+
" lab_np = labels.detach().cpu().numpy() if torch.is_tensor(labels) else np.array(labels)\n",
|
| 360 |
+
" if method == \"pca\":\n",
|
| 361 |
+
" from sklearn.decomposition import PCA\n",
|
| 362 |
+
" proj = PCA(n_components=2).fit_transform(emb_np)\n",
|
| 363 |
+
" else:\n",
|
| 364 |
+
" from sklearn.manifold import TSNE\n",
|
| 365 |
+
" proj = TSNE(n_components=2, perplexity=30, n_iter=500).fit_transform(emb_np)\n",
|
| 366 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 367 |
+
" for c in range(10):\n",
|
| 368 |
+
" mask = lab_np == c\n",
|
| 369 |
+
" ax.scatter(proj[mask, 0], proj[mask, 1], s=3, alpha=0.5, label=CIFAR10_CLASSES[c])\n",
|
| 370 |
+
" ax.legend(markerscale=5, fontsize=8)\n",
|
| 371 |
+
" ax.set_title(f\"{tag} ({method.upper()}) \u2014 Epoch {epoch}\")\n",
|
| 372 |
+
" fig.tight_layout()\n",
|
| 373 |
+
" writer.add_figure(f\"{tag}/{method}\", fig, epoch)\n",
|
| 374 |
+
" plt.close(fig)\n",
|
| 375 |
+
"\n",
|
| 376 |
+
"# \u2500\u2500 Training Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 377 |
+
"def train_one_epoch(model, loader, optimizer, criterion, device, epoch=0,\n",
|
| 378 |
+
" scheduler=None, extra_losses=None):\n",
|
| 379 |
+
" \"\"\"\n",
|
| 380 |
+
" Generic training loop for one epoch.\n",
|
| 381 |
+
" model: callable(batch) \u2192 (logits, embeddings_on_sphere)\n",
|
| 382 |
+
" extra_losses: list of (name, loss_fn, weight) where loss_fn(embeddings) \u2192 scalar\n",
|
| 383 |
+
" \"\"\"\n",
|
| 384 |
+
" model.train()\n",
|
| 385 |
+
" total_loss = 0\n",
|
| 386 |
+
" total_ce = 0\n",
|
| 387 |
+
" correct = 0\n",
|
| 388 |
+
" total = 0\n",
|
| 389 |
+
" loss_components = defaultdict(float)\n",
|
| 390 |
+
"\n",
|
| 391 |
+
" for batch_idx, (images, labels) in enumerate(loader):\n",
|
| 392 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 393 |
+
" optimizer.zero_grad()\n",
|
| 394 |
+
"\n",
|
| 395 |
+
" logits, embeddings = model(images)\n",
|
| 396 |
+
" ce_loss = criterion(logits, labels)\n",
|
| 397 |
+
" loss = ce_loss\n",
|
| 398 |
+
" loss_components[\"CE\"] += ce_loss.item()\n",
|
| 399 |
+
"\n",
|
| 400 |
+
" if extra_losses:\n",
|
| 401 |
+
" for name, loss_fn, weight in extra_losses:\n",
|
| 402 |
+
" if name == \"CV\":\n",
|
| 403 |
+
" el, vol_val = loss_fn(embeddings)\n",
|
| 404 |
+
" loss_components[\"CV_vol\"] += vol_val\n",
|
| 405 |
+
" elif name == \"InfoNCE_class\":\n",
|
| 406 |
+
" el = loss_fn(embeddings, labels)\n",
|
| 407 |
+
" else:\n",
|
| 408 |
+
" el = loss_fn(embeddings)\n",
|
| 409 |
+
" loss = loss + weight * el\n",
|
| 410 |
+
" loss_components[name] += el.item()\n",
|
| 411 |
+
"\n",
|
| 412 |
+
" loss.backward()\n",
|
| 413 |
+
" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n",
|
| 414 |
+
" optimizer.step()\n",
|
| 415 |
+
" if scheduler:\n",
|
| 416 |
+
" scheduler.step()\n",
|
| 417 |
+
"\n",
|
| 418 |
+
" total_loss += loss.item()\n",
|
| 419 |
+
" _, predicted = logits.max(1)\n",
|
| 420 |
+
" total += labels.size(0)\n",
|
| 421 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 422 |
+
"\n",
|
| 423 |
+
" n_batches = len(loader)\n",
|
| 424 |
+
" metrics = {\n",
|
| 425 |
+
" \"loss/total\": total_loss / n_batches,\n",
|
| 426 |
+
" \"loss/CE\": loss_components[\"CE\"] / n_batches,\n",
|
| 427 |
+
" \"accuracy/train\": 100.0 * correct / total,\n",
|
| 428 |
+
" }\n",
|
| 429 |
+
" for k, v in loss_components.items():\n",
|
| 430 |
+
" if k not in (\"CE\",):\n",
|
| 431 |
+
" metrics[f\"loss/{k}\"] = v / n_batches\n",
|
| 432 |
+
" return metrics\n",
|
| 433 |
+
"\n",
|
| 434 |
+
"@torch.no_grad()\n",
|
| 435 |
+
"def evaluate(model, loader, criterion, device, collect_embeddings=False):\n",
|
| 436 |
+
" \"\"\"Evaluate model on validation set. Optionally collect embeddings.\"\"\"\n",
|
| 437 |
+
" model.eval()\n",
|
| 438 |
+
" total_loss = 0\n",
|
| 439 |
+
" correct = 0\n",
|
| 440 |
+
" total = 0\n",
|
| 441 |
+
" all_preds = []\n",
|
| 442 |
+
" all_labels = []\n",
|
| 443 |
+
" all_embeddings = []\n",
|
| 444 |
+
"\n",
|
| 445 |
+
" for images, labels in loader:\n",
|
| 446 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 447 |
+
" logits, embeddings = model(images)\n",
|
| 448 |
+
" loss = criterion(logits, labels)\n",
|
| 449 |
+
" total_loss += loss.item()\n",
|
| 450 |
+
" _, predicted = logits.max(1)\n",
|
| 451 |
+
" total += labels.size(0)\n",
|
| 452 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 453 |
+
" all_preds.extend(predicted.cpu().numpy())\n",
|
| 454 |
+
" all_labels.extend(labels.cpu().numpy())\n",
|
| 455 |
+
" if collect_embeddings:\n",
|
| 456 |
+
" all_embeddings.append(embeddings.cpu())\n",
|
| 457 |
+
"\n",
|
| 458 |
+
" n_batches = len(loader)\n",
|
| 459 |
+
" metrics = {\n",
|
| 460 |
+
" \"loss/val\": total_loss / n_batches,\n",
|
| 461 |
+
" \"accuracy/val\": 100.0 * correct / total,\n",
|
| 462 |
+
" }\n",
|
| 463 |
+
" result = {\"metrics\": metrics, \"preds\": np.array(all_preds), \"labels\": np.array(all_labels)}\n",
|
| 464 |
+
" if collect_embeddings:\n",
|
| 465 |
+
" result[\"embeddings\"] = torch.cat(all_embeddings, dim=0)\n",
|
| 466 |
+
" return result\n",
|
| 467 |
+
"\n",
|
| 468 |
+
"def run_experiment(experiment_id, model, train_loader, val_loader, device,\n",
|
| 469 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, extra_losses=None,\n",
|
| 470 |
+
" push_anchors_every=50, log_embeddings_every=25,\n",
|
| 471 |
+
" patience=20):\n",
|
| 472 |
+
" \"\"\"\n",
|
| 473 |
+
" Full experiment runner with TensorBoard logging, early stopping, and diagnostics.\n",
|
| 474 |
+
" model must return (logits, sphere_embeddings).\n",
|
| 475 |
+
" \"\"\"\n",
|
| 476 |
+
" writer = make_writer(experiment_id)\n",
|
| 477 |
+
" criterion = nn.CrossEntropyLoss()\n",
|
| 478 |
+
" optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)\n",
|
| 479 |
+
" scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\n",
|
| 480 |
+
"\n",
|
| 481 |
+
" best_val_acc = 0.0\n",
|
| 482 |
+
" best_epoch = 0\n",
|
| 483 |
+
" no_improve = 0\n",
|
| 484 |
+
"\n",
|
| 485 |
+
" # Log model info\n",
|
| 486 |
+
" n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 487 |
+
" writer.add_text(\"config/params\", f\"Learned params: {n_params:,}\")\n",
|
| 488 |
+
" writer.add_text(\"config/experiment\", experiment_id)\n",
|
| 489 |
+
" print(f\"\\n{'='*70}\")\n",
|
| 490 |
+
" print(f\"[EXP] {experiment_id}\")\n",
|
| 491 |
+
" print(f\"[EXP] Learned params: {n_params:,}, Epochs: {epochs}, LR: {lr}\")\n",
|
| 492 |
+
" print(f\"{'='*70}\")\n",
|
| 493 |
+
"\n",
|
| 494 |
+
" for epoch in range(1, epochs + 1):\n",
|
| 495 |
+
" t0 = time.time()\n",
|
| 496 |
+
"\n",
|
| 497 |
+
" # Train\n",
|
| 498 |
+
" train_metrics = train_one_epoch(\n",
|
| 499 |
+
" model, train_loader, optimizer, criterion, device, epoch,\n",
|
| 500 |
+
" extra_losses=extra_losses\n",
|
| 501 |
+
" )\n",
|
| 502 |
+
"\n",
|
| 503 |
+
" # Evaluate\n",
|
| 504 |
+
" collect_emb = (epoch % log_embeddings_every == 0) or (epoch == epochs)\n",
|
| 505 |
+
" val_result = evaluate(model, val_loader, criterion, device, collect_embeddings=collect_emb)\n",
|
| 506 |
+
" val_metrics = val_result[\"metrics\"]\n",
|
| 507 |
+
"\n",
|
| 508 |
+
" # Combine metrics\n",
|
| 509 |
+
" all_metrics = {**train_metrics, **val_metrics}\n",
|
| 510 |
+
" all_metrics[\"lr\"] = optimizer.param_groups[0][\"lr\"]\n",
|
| 511 |
+
" all_metrics[\"epoch_time\"] = time.time() - t0\n",
|
| 512 |
+
" all_metrics[\"train_val_gap\"] = train_metrics[\"accuracy/train\"] - val_metrics[\"accuracy/val\"]\n",
|
| 513 |
+
"\n",
|
| 514 |
+
" # Constellation diagnostics\n",
|
| 515 |
+
" if hasattr(model, \"constellation\"):\n",
|
| 516 |
+
" constellation = model.constellation\n",
|
| 517 |
+
" elif hasattr(model, \"classifier\") and hasattr(model.classifier, \"constellation\"):\n",
|
| 518 |
+
" constellation = model.classifier.constellation\n",
|
| 519 |
+
" else:\n",
|
| 520 |
+
" constellation = None\n",
|
| 521 |
+
"\n",
|
| 522 |
+
" if constellation is not None:\n",
|
| 523 |
+
" log_anchor_histogram(writer, constellation, epoch)\n",
|
| 524 |
+
"\n",
|
| 525 |
+
" # Push anchors periodically\n",
|
| 526 |
+
" if epoch % push_anchors_every == 0 and \"embeddings\" in val_result:\n",
|
| 527 |
+
" emb = val_result[\"embeddings\"][:2000].to(device)\n",
|
| 528 |
+
" lab = torch.tensor(val_result[\"labels\"][:2000]).to(device)\n",
|
| 529 |
+
" constellation.push_anchors(emb, lab)\n",
|
| 530 |
+
" active, total_anch = constellation.anchor_activation(emb)\n",
|
| 531 |
+
" all_metrics[\"anchors/active\"] = active\n",
|
| 532 |
+
" all_metrics[\"anchors/total\"] = total_anch\n",
|
| 533 |
+
" all_metrics[\"anchors/utilization\"] = active / total_anch\n",
|
| 534 |
+
" print(f\" [ANCHOR PUSH] Active: {active}/{total_anch} ({100*active/total_anch:.1f}%)\")\n",
|
| 535 |
+
"\n",
|
| 536 |
+
" # Log all metrics\n",
|
| 537 |
+
" log_epoch_metrics(writer, epoch, all_metrics)\n",
|
| 538 |
+
" scheduler.step()\n",
|
| 539 |
+
"\n",
|
| 540 |
+
" # Log embeddings visualization\n",
|
| 541 |
+
" if collect_emb and \"embeddings\" in val_result:\n",
|
| 542 |
+
" emb = val_result[\"embeddings\"][:2000]\n",
|
| 543 |
+
" lab = val_result[\"labels\"][:2000]\n",
|
| 544 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings\", method=\"pca\")\n",
|
| 545 |
+
" if epoch == epochs:\n",
|
| 546 |
+
" log_confusion_matrix(writer, val_result[\"preds\"], val_result[\"labels\"], epoch)\n",
|
| 547 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings_tsne\", method=\"tsne\")\n",
|
| 548 |
+
"\n",
|
| 549 |
+
" # Log triangulation stats from a sample batch\n",
|
| 550 |
+
" if constellation is not None and collect_emb and \"embeddings\" in val_result:\n",
|
| 551 |
+
" sample_emb = val_result[\"embeddings\"][:500].to(device)\n",
|
| 552 |
+
" tri_sample = constellation(sample_emb)\n",
|
| 553 |
+
" log_triangulation_stats(writer, tri_sample, epoch)\n",
|
| 554 |
+
"\n",
|
| 555 |
+
" # Best model tracking\n",
|
| 556 |
+
" val_acc = val_metrics[\"accuracy/val\"]\n",
|
| 557 |
+
" if val_acc > best_val_acc:\n",
|
| 558 |
+
" best_val_acc = val_acc\n",
|
| 559 |
+
" best_epoch = epoch\n",
|
| 560 |
+
" no_improve = 0\n",
|
| 561 |
+
" writer.add_scalar(\"best/val_acc\", best_val_acc, epoch)\n",
|
| 562 |
+
" writer.add_scalar(\"best/epoch\", best_epoch, epoch)\n",
|
| 563 |
+
" else:\n",
|
| 564 |
+
" no_improve += 1\n",
|
| 565 |
+
"\n",
|
| 566 |
+
" # Print progress\n",
|
| 567 |
+
" if epoch % 5 == 0 or epoch == 1 or epoch == epochs:\n",
|
| 568 |
+
" print(f\" E{epoch:3d} | TrainAcc {train_metrics['accuracy/train']:5.1f}% | \"\n",
|
| 569 |
+
" f\"ValAcc {val_acc:5.1f}% | Loss {all_metrics['loss/total']:.4f} | \"\n",
|
| 570 |
+
" f\"Best {best_val_acc:.1f}% @E{best_epoch} | \"\n",
|
| 571 |
+
" f\"Gap {all_metrics['train_val_gap']:+.1f}% | \"\n",
|
| 572 |
+
" f\"{all_metrics['epoch_time']:.1f}s\")\n",
|
| 573 |
+
"\n",
|
| 574 |
+
" # Early stopping\n",
|
| 575 |
+
" if no_improve >= patience:\n",
|
| 576 |
+
" print(f\" [EARLY STOP] No improvement for {patience} epochs. Best: {best_val_acc:.1f}% @E{best_epoch}\")\n",
|
| 577 |
+
" break\n",
|
| 578 |
+
"\n",
|
| 579 |
+
" writer.close()\n",
|
| 580 |
+
" print(f\"\\n[RESULT] {experiment_id}: Best Val Acc = {best_val_acc:.2f}% at epoch {best_epoch}\")\n",
|
| 581 |
+
" print(f\"[RESULT] Learned params: {n_params:,}\")\n",
|
| 582 |
+
" return {\"experiment\": experiment_id, \"best_val_acc\": best_val_acc, \"best_epoch\": best_epoch,\n",
|
| 583 |
+
" \"params\": n_params, \"final_train_acc\": train_metrics[\"accuracy/train\"]}\n",
|
| 584 |
+
"\n",
|
| 585 |
+
"# \u2500\u2500 Johnson-Lindenstrauss Projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 586 |
+
"class JLProjection(nn.Module):\n",
|
| 587 |
+
" \"\"\"Fixed random Gaussian projection (not learned).\"\"\"\n",
|
| 588 |
+
" def __init__(self, input_dim, output_dim):\n",
|
| 589 |
+
" super().__init__()\n",
|
| 590 |
+
" self.register_buffer(\"W\", torch.randn(input_dim, output_dim) / math.sqrt(output_dim))\n",
|
| 591 |
+
"\n",
|
| 592 |
+
" def forward(self, x):\n",
|
| 593 |
+
" return x @ self.W\n",
|
| 594 |
+
"\n",
|
| 595 |
+
"# \u2500\u2500 Results Scoreboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 596 |
+
"class Scoreboard:\n",
|
| 597 |
+
" \"\"\"Track results across experiments in a session.\"\"\"\n",
|
| 598 |
+
" def __init__(self):\n",
|
| 599 |
+
" self.results = []\n",
|
| 600 |
+
"\n",
|
| 601 |
+
" def add(self, result_dict):\n",
|
| 602 |
+
" self.results.append(result_dict)\n",
|
| 603 |
+
" self._print_table()\n",
|
| 604 |
+
"\n",
|
| 605 |
+
" def _print_table(self):\n",
|
| 606 |
+
" print(f\"\\n{'='*80}\")\n",
|
| 607 |
+
" print(f\"{'SCOREBOARD':^80}\")\n",
|
| 608 |
+
" print(f\"{'='*80}\")\n",
|
| 609 |
+
" print(f\"{'Experiment':<40} {'Val Acc':>8} {'Params':>10} {'Best E':>7}\")\n",
|
| 610 |
+
" print(f\"{'-'*40} {'-'*8} {'-'*10} {'-'*7}\")\n",
|
| 611 |
+
" for r in sorted(self.results, key=lambda x: x[\"best_val_acc\"], reverse=True):\n",
|
| 612 |
+
" print(f\"{r['experiment']:<40} {r['best_val_acc']:>7.2f}% {r['params']:>10,} {r['best_epoch']:>7}\")\n",
|
| 613 |
+
" print(f\"{'='*80}\\n\")\n",
|
| 614 |
+
"\n",
|
| 615 |
+
" def save(self, path=\"scoreboard.json\"):\n",
|
| 616 |
+
" with open(path, \"w\") as f:\n",
|
| 617 |
+
" json.dump(self.results, f, indent=2)\n",
|
| 618 |
+
" print(f\"[SCOREBOARD] Saved to {path}\")\n",
|
| 619 |
+
"\n",
|
| 620 |
+
"scoreboard = Scoreboard()\n",
|
| 621 |
+
"print(\"[SHARED] All utilities loaded successfully.\")\n",
|
| 622 |
+
""
|
| 623 |
+
],
|
| 624 |
+
"execution_count": null,
|
| 625 |
+
"outputs": []
|
| 626 |
+
},
|
| 627 |
+
{
|
| 628 |
+
"cell_type": "markdown",
|
| 629 |
+
"metadata": {},
|
| 630 |
+
"source": [
|
| 631 |
+
"## Experiment 2.1a: Hopf Fibration S\u00b3 \u2192 S\u00b2"
|
| 632 |
+
]
|
| 633 |
+
},
|
| 634 |
+
{
|
| 635 |
+
"cell_type": "markdown",
|
| 636 |
+
"metadata": {},
|
| 637 |
+
"source": [
|
| 638 |
+
"\n",
|
| 639 |
+
"Formula: h(z\u2081,z\u2082) = (2z\u0304\u2081z\u2082, |z\u2081|\u00b2\u2212|z\u2082|\u00b2) : S\u00b3 \u2192 S\u00b2\n",
|
| 640 |
+
"\n",
|
| 641 |
+
"Strategy:\n",
|
| 642 |
+
"- Encode 4-d feature vectors on S\u00b3\n",
|
| 643 |
+
"- Hopf project to S\u00b2 (coarse triangulation) + fiber coordinate (fine discrimination)\n",
|
| 644 |
+
"- Front-end: Scattering2D + JL projection to 4-d\n",
|
| 645 |
+
"- TensorBoard logging, 100 epochs, early stopping patience=20\n",
|
| 646 |
+
""
|
| 647 |
+
]
|
| 648 |
+
},
|
| 649 |
+
{
|
| 650 |
+
"cell_type": "code",
|
| 651 |
+
"metadata": {},
|
| 652 |
+
"source": [
|
| 653 |
+
"\n",
|
| 654 |
+
"from kymatio.torch import Scattering2D\n",
|
| 655 |
+
"import torch.optim.lr_scheduler as lr_scheduler\n",
|
| 656 |
+
"\n",
|
| 657 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 658 |
+
"# FRONT-END: Scattering2D + JL to 4-d\n",
|
| 659 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 660 |
+
"class Scattering2DFeatures(nn.Module):\n",
|
| 661 |
+
" '''Scattering2D feature extractor for CIFAR-10.'''\n",
|
| 662 |
+
" def __init__(self, out_dim=128):\n",
|
| 663 |
+
" super().__init__()\n",
|
| 664 |
+
" # Scattering2D for 32x32 images\n",
|
| 665 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 666 |
+
" scat_out = self.compute_scat_size()\n",
|
| 667 |
+
" self.proj = nn.Linear(scat_out, out_dim)\n",
|
| 668 |
+
" print(f\"[SCAT] Scattering output size: {scat_out}, projected to {out_dim}\")\n",
|
| 669 |
+
"\n",
|
| 670 |
+
" def compute_scat_size(self):\n",
|
| 671 |
+
" '''Compute output size of Scattering2D.'''\n",
|
| 672 |
+
" x_dummy = torch.randn(1, 3, 32, 32)\n",
|
| 673 |
+
" with torch.no_grad():\n",
|
| 674 |
+
" out = self.scattering(x_dummy)\n",
|
| 675 |
+
" return out.shape[1] * out.shape[2] * out.shape[3]\n",
|
| 676 |
+
"\n",
|
| 677 |
+
" def forward(self, x):\n",
|
| 678 |
+
" # x: (B, 3, 32, 32)\n",
|
| 679 |
+
" scats = []\n",
|
| 680 |
+
" for c in range(3):\n",
|
| 681 |
+
" s = self.scattering(x[:, c:c+1, :, :]) # (B, F, H, W)\n",
|
| 682 |
+
" s = s.reshape(s.shape[0], -1) # (B, F*H*W)\n",
|
| 683 |
+
" scats.append(s)\n",
|
| 684 |
+
" scat_feat = torch.cat(scats, dim=1) # (B, 3*F*H*W)\n",
|
| 685 |
+
" out = self.proj(scat_feat) # (B, out_dim)\n",
|
| 686 |
+
" return out\n",
|
| 687 |
+
"\n",
|
| 688 |
+
"class HopfFibrationS3S2Model(nn.Module):\n",
|
| 689 |
+
" '''S\u00b3 \u2192 S\u00b2 via Hopf fibration (z\u2081, z\u2082 complex).'''\n",
|
| 690 |
+
" def __init__(self, num_anchors=64):\n",
|
| 691 |
+
" super().__init__()\n",
|
| 692 |
+
" self.frontend = Scattering2DFeatures(out_dim=128)\n",
|
| 693 |
+
" self.to_4d = nn.Linear(128, 4) # Project to 4-d for S\u00b3\n",
|
| 694 |
+
" self.constellation_s2 = Constellation(embed_dim=3, num_anchors=num_anchors, init=\"uniform\")\n",
|
| 695 |
+
" self.fiber_encoder = nn.Linear(1, 16) # Encode fiber coordinate\n",
|
| 696 |
+
" self.patchwork = Patchwork(num_anchors + 16, num_blocks=2)\n",
|
| 697 |
+
" self.head = nn.Linear(num_anchors + 16, 10)\n",
|
| 698 |
+
"\n",
|
| 699 |
+
" def forward(self, x):\n",
|
| 700 |
+
" # Frontend: scattering + projection\n",
|
| 701 |
+
" feat = self.frontend(x) # (B, 128)\n",
|
| 702 |
+
" z_4d = self.to_4d(feat) # (B, 4)\n",
|
| 703 |
+
" z_norm = l2_normalize(z_4d) # Normalize to S\u00b3\n",
|
| 704 |
+
"\n",
|
| 705 |
+
" # Hopf projection: S\u00b3 \u2192 S\u00b2 (interpret as z\u2081, z\u2082 \u2208 \u2102)\n",
|
| 706 |
+
" # z = [z\u2081.re, z\u2081.im, z\u2082.re, z\u2082.im]\n",
|
| 707 |
+
" z1 = torch.complex(z_norm[:, 0], z_norm[:, 1])\n",
|
| 708 |
+
" z2 = torch.complex(z_norm[:, 2], z_norm[:, 3])\n",
|
| 709 |
+
"\n",
|
| 710 |
+
" # Hopf map: (2z\u0304\u2081z\u2082, |z\u2081|\u00b2\u2212|z\u2082|\u00b2)\n",
|
| 711 |
+
" two_conj_z1_z2 = 2 * (z1.conj() * z2)\n",
|
| 712 |
+
" z1_norm_sq = (z1.abs() ** 2).unsqueeze(-1)\n",
|
| 713 |
+
" z2_norm_sq = (z2.abs() ** 2).unsqueeze(-1)\n",
|
| 714 |
+
"\n",
|
| 715 |
+
" base_x = torch.cat([\n",
|
| 716 |
+
" two_conj_z1_z2.real.unsqueeze(-1),\n",
|
| 717 |
+
" two_conj_z1_z2.imag.unsqueeze(-1),\n",
|
| 718 |
+
" (z1_norm_sq - z2_norm_sq)\n",
|
| 719 |
+
" ], dim=1) # (B, 3) on S\u00b2\n",
|
| 720 |
+
" base_x = l2_normalize(base_x)\n",
|
| 721 |
+
"\n",
|
| 722 |
+
" # Fiber coordinate: one of the complex arguments\n",
|
| 723 |
+
" fiber = (z1.abs()).unsqueeze(-1) # (B, 1)\n",
|
| 724 |
+
"\n",
|
| 725 |
+
" # Triangulation on S\u00b2 + fiber encoding\n",
|
| 726 |
+
" tri = self.constellation_s2(base_x) # (B, num_anchors)\n",
|
| 727 |
+
" fiber_enc = self.fiber_encoder(fiber) # (B, 16)\n",
|
| 728 |
+
"\n",
|
| 729 |
+
" # Combine\n",
|
| 730 |
+
" combined = torch.cat([tri, fiber_enc], dim=1) # (B, num_anchors+16)\n",
|
| 731 |
+
" pw = self.patchwork(combined)\n",
|
| 732 |
+
" logits = self.head(pw)\n",
|
| 733 |
+
"\n",
|
| 734 |
+
" # Return logits and sphere embedding (for diagnostics)\n",
|
| 735 |
+
" return logits, z_norm\n",
|
| 736 |
+
"\n",
|
| 737 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 738 |
+
"# RUN EXPERIMENT 2.1a\n",
|
| 739 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 740 |
+
"print(\"\\n[EXP 2.1a] Loading CIFAR-10...\")\n",
|
| 741 |
+
"train_loader, val_loader = get_cifar10_raw_loaders(batch_size=128, num_workers=2)\n",
|
| 742 |
+
"\n",
|
| 743 |
+
"model_2_1a = HopfFibrationS3S2Model(num_anchors=64).to(device)\n",
|
| 744 |
+
"print(f\"[EXP 2.1a] Model: {model_2_1a.count_params() if hasattr(model_2_1a, 'count_params') else 'N/A'} params\")\n",
|
| 745 |
+
"\n",
|
| 746 |
+
"result_2_1a = run_experiment(\n",
|
| 747 |
+
" \"2.1a_HopfS3S2\",\n",
|
| 748 |
+
" model_2_1a,\n",
|
| 749 |
+
" train_loader,\n",
|
| 750 |
+
" val_loader,\n",
|
| 751 |
+
" device,\n",
|
| 752 |
+
" epochs=100,\n",
|
| 753 |
+
" lr=1e-3,\n",
|
| 754 |
+
" patience=20\n",
|
| 755 |
+
")\n",
|
| 756 |
+
"scoreboard.add(result_2_1a)\n",
|
| 757 |
+
""
|
| 758 |
+
],
|
| 759 |
+
"execution_count": null,
|
| 760 |
+
"outputs": []
|
| 761 |
+
},
|
| 762 |
+
{
|
| 763 |
+
"cell_type": "markdown",
|
| 764 |
+
"metadata": {},
|
| 765 |
+
"source": [
|
| 766 |
+
"## Experiment 2.1b: Quaternionic Hopf S\u2077 \u2192 S\u2074"
|
| 767 |
+
]
|
| 768 |
+
},
|
| 769 |
+
{
|
| 770 |
+
"cell_type": "markdown",
|
| 771 |
+
"metadata": {},
|
| 772 |
+
"source": [
|
| 773 |
+
"\n",
|
| 774 |
+
"Natural decomposition for 8-d embeddings (8-channel spectral).\n",
|
| 775 |
+
"Front-end produces 8-d embeddings \u2192 Hopf project to S\u2074 + fiber.\n",
|
| 776 |
+
""
|
| 777 |
+
]
|
| 778 |
+
},
|
| 779 |
+
{
|
| 780 |
+
"cell_type": "code",
|
| 781 |
+
"metadata": {},
|
| 782 |
+
"source": [
|
| 783 |
+
"\n",
|
| 784 |
+
"class HopfFibrationS7S4Model(nn.Module):\n",
|
| 785 |
+
" '''S\u2077 \u2192 S\u2074 via quaternionic Hopf (q\u2081, q\u2082 quaternions).'''\n",
|
| 786 |
+
" def __init__(self, num_anchors=64):\n",
|
| 787 |
+
" super().__init__()\n",
|
| 788 |
+
" self.frontend = Scattering2DFeatures(out_dim=256)\n",
|
| 789 |
+
" self.to_8d = nn.Linear(256, 8)\n",
|
| 790 |
+
" self.constellation_s4 = Constellation(embed_dim=5, num_anchors=num_anchors, init=\"uniform\")\n",
|
| 791 |
+
" self.fiber_encoder = nn.Linear(3, 16)\n",
|
| 792 |
+
" self.patchwork = Patchwork(num_anchors + 16, num_blocks=2)\n",
|
| 793 |
+
" self.head = nn.Linear(num_anchors + 16, 10)\n",
|
| 794 |
+
"\n",
|
| 795 |
+
" def forward(self, x):\n",
|
| 796 |
+
" feat = self.frontend(x) # (B, 256)\n",
|
| 797 |
+
" z_8d = self.to_8d(feat) # (B, 8)\n",
|
| 798 |
+
" z_norm = l2_normalize(z_8d) # Normalize to S\u2077\n",
|
| 799 |
+
"\n",
|
| 800 |
+
" # Quaternionic Hopf: q\u2081 = (z\u2081, z\u2082), q\u2082 = (z\u2083, z\u2084) as complex pairs\n",
|
| 801 |
+
" # Map to S\u2074: base = [2Re(conj(q\u2081)q\u2082), |q\u2081|\u00b2 - |q\u2082|\u00b2]\n",
|
| 802 |
+
" q1_0 = torch.complex(z_norm[:, 0], z_norm[:, 1])\n",
|
| 803 |
+
" q1_1 = torch.complex(z_norm[:, 2], z_norm[:, 3])\n",
|
| 804 |
+
" q2_0 = torch.complex(z_norm[:, 4], z_norm[:, 5])\n",
|
| 805 |
+
" q2_1 = torch.complex(z_norm[:, 6], z_norm[:, 7])\n",
|
| 806 |
+
"\n",
|
| 807 |
+
" # Simplified quaternionic norm\n",
|
| 808 |
+
" q1_norm_sq = (q1_0.abs() ** 2 + q1_1.abs() ** 2).unsqueeze(-1)\n",
|
| 809 |
+
" q2_norm_sq = (q2_0.abs() ** 2 + q2_1.abs() ** 2).unsqueeze(-1)\n",
|
| 810 |
+
"\n",
|
| 811 |
+
" # Quaternion multiplication (simplified)\n",
|
| 812 |
+
" conj_q1_q2_0 = (q1_0.conj() * q2_0 - q1_1.conj() * q2_1)\n",
|
| 813 |
+
" conj_q1_q2_1 = (q1_0.conj() * q2_1 + q1_1.conj() * q2_0)\n",
|
| 814 |
+
"\n",
|
| 815 |
+
" base_x = torch.cat([\n",
|
| 816 |
+
" conj_q1_q2_0.real.unsqueeze(-1),\n",
|
| 817 |
+
" conj_q1_q2_0.imag.unsqueeze(-1),\n",
|
| 818 |
+
" conj_q1_q2_1.real.unsqueeze(-1),\n",
|
| 819 |
+
" conj_q1_q2_1.imag.unsqueeze(-1),\n",
|
| 820 |
+
" (q1_norm_sq - q2_norm_sq)\n",
|
| 821 |
+
" ], dim=1) # (B, 5) on S\u2074\n",
|
| 822 |
+
" base_x = l2_normalize(base_x)\n",
|
| 823 |
+
"\n",
|
| 824 |
+
" # Fiber: three components\n",
|
| 825 |
+
" fiber = torch.stack([\n",
|
| 826 |
+
" (q1_0.abs()),\n",
|
| 827 |
+
" (q1_1.abs()),\n",
|
| 828 |
+
" (q2_0.abs())\n",
|
| 829 |
+
" ], dim=1) # (B, 3)\n",
|
| 830 |
+
"\n",
|
| 831 |
+
" tri = self.constellation_s4(base_x) # (B, num_anchors)\n",
|
| 832 |
+
" fiber_enc = self.fiber_encoder(fiber) # (B, 16)\n",
|
| 833 |
+
" combined = torch.cat([tri, fiber_enc], dim=1)\n",
|
| 834 |
+
" pw = self.patchwork(combined)\n",
|
| 835 |
+
" logits = self.head(pw)\n",
|
| 836 |
+
" return logits, z_norm\n",
|
| 837 |
+
"\n",
|
| 838 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 839 |
+
"# RUN EXPERIMENT 2.1b\n",
|
| 840 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 841 |
+
"print(\"\\n[EXP 2.1b] Setting up S\u2077 \u2192 S\u2074...\")\n",
|
| 842 |
+
"model_2_1b = HopfFibrationS7S4Model(num_anchors=64).to(device)\n",
|
| 843 |
+
"result_2_1b = run_experiment(\n",
|
| 844 |
+
" \"2.1b_HopfS7S4\",\n",
|
| 845 |
+
" model_2_1b,\n",
|
| 846 |
+
" train_loader,\n",
|
| 847 |
+
" val_loader,\n",
|
| 848 |
+
" device,\n",
|
| 849 |
+
" epochs=100,\n",
|
| 850 |
+
" lr=1e-3,\n",
|
| 851 |
+
" patience=20\n",
|
| 852 |
+
")\n",
|
| 853 |
+
"scoreboard.add(result_2_1b)\n",
|
| 854 |
+
""
|
| 855 |
+
],
|
| 856 |
+
"execution_count": null,
|
| 857 |
+
"outputs": []
|
| 858 |
+
},
|
| 859 |
+
{
|
| 860 |
+
"cell_type": "markdown",
|
| 861 |
+
"metadata": {},
|
| 862 |
+
"source": [
|
| 863 |
+
"## Experiment 2.1c: Hopf Foliation Anchor Init"
|
| 864 |
+
]
|
| 865 |
+
},
|
| 866 |
+
{
|
| 867 |
+
"cell_type": "markdown",
|
| 868 |
+
"metadata": {},
|
| 869 |
+
"source": [
|
| 870 |
+
"\n",
|
| 871 |
+
"Replace uniform_hypersphere_init with Hopf-structured codes.\n",
|
| 872 |
+
"Compare anchor utilization vs random initialization.\n",
|
| 873 |
+
""
|
| 874 |
+
]
|
| 875 |
+
},
|
| 876 |
+
{
|
| 877 |
+
"cell_type": "code",
|
| 878 |
+
"metadata": {},
|
| 879 |
+
"source": [
|
| 880 |
+
"\n",
|
| 881 |
+
"class HopfAnchorConstellation(nn.Module):\n",
|
| 882 |
+
" '''Constellation with Hopf-structured anchor initialization.'''\n",
|
| 883 |
+
" def __init__(self, embed_dim, num_anchors=64):\n",
|
| 884 |
+
" super().__init__()\n",
|
| 885 |
+
" self.embed_dim = embed_dim\n",
|
| 886 |
+
" self.num_anchors = num_anchors\n",
|
| 887 |
+
" anchors = self._init_hopf_anchors(embed_dim, num_anchors)\n",
|
| 888 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False)\n",
|
| 889 |
+
"\n",
|
| 890 |
+
" def _init_hopf_anchors(self, d, K):\n",
|
| 891 |
+
" '''Initialize anchors on S^{d-1} using Hopf decomposition.'''\n",
|
| 892 |
+
" # For d=4 (S\u00b3), use Hopf fibration structure\n",
|
| 893 |
+
" if d == 4:\n",
|
| 894 |
+
" n_base = int(np.sqrt(K / 2))\n",
|
| 895 |
+
" base_angles = np.linspace(0, 2*np.pi, n_base, endpoint=False)\n",
|
| 896 |
+
" fiber_angles = np.linspace(0, np.pi, n_base, endpoint=False)\n",
|
| 897 |
+
"\n",
|
| 898 |
+
" anchors = []\n",
|
| 899 |
+
" for theta in base_angles:\n",
|
| 900 |
+
" for phi in fiber_angles:\n",
|
| 901 |
+
" # S\u00b2 base coordinates\n",
|
| 902 |
+
" x = np.sin(theta) * np.cos(phi)\n",
|
| 903 |
+
" y = np.sin(theta) * np.sin(phi)\n",
|
| 904 |
+
" z = np.cos(theta)\n",
|
| 905 |
+
" # Lift to S\u00b3: use phi as fiber parameter\n",
|
| 906 |
+
" z1_real, z1_imag = np.cos(phi/2), np.sin(phi/2)\n",
|
| 907 |
+
" z2_real, z2_imag = np.sin(phi/2) * np.cos(theta), np.sin(phi/2) * np.sin(theta)\n",
|
| 908 |
+
" anchors.append([z1_real, z1_imag, z2_real, z2_imag])\n",
|
| 909 |
+
" anchors = torch.tensor(anchors[:K], dtype=torch.float32)\n",
|
| 910 |
+
" else:\n",
|
| 911 |
+
" # Fallback: uniform\n",
|
| 912 |
+
" A = torch.randn(K, d)\n",
|
| 913 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 914 |
+
" anchors = Q.T[:K]\n",
|
| 915 |
+
"\n",
|
| 916 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 917 |
+
"\n",
|
| 918 |
+
" def forward(self, x):\n",
|
| 919 |
+
" x_norm = l2_normalize(x)\n",
|
| 920 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 921 |
+
" distances = 1.0 - cos_sim\n",
|
| 922 |
+
" return distances\n",
|
| 923 |
+
"\n",
|
| 924 |
+
" def anchor_activation(self, x):\n",
|
| 925 |
+
" x_norm = l2_normalize(x)\n",
|
| 926 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 927 |
+
" nearest = cos_sim.argmax(dim=1)\n",
|
| 928 |
+
" active = len(nearest.unique())\n",
|
| 929 |
+
" return active, self.num_anchors\n",
|
| 930 |
+
"\n",
|
| 931 |
+
"class HopfAnchorModel(nn.Module):\n",
|
| 932 |
+
" '''S\u00b3 embedding with Hopf-structured anchors.'''\n",
|
| 933 |
+
" def __init__(self, num_anchors=64):\n",
|
| 934 |
+
" super().__init__()\n",
|
| 935 |
+
" self.frontend = Scattering2DFeatures(out_dim=128)\n",
|
| 936 |
+
" self.to_4d = nn.Linear(128, 4)\n",
|
| 937 |
+
" self.constellation = HopfAnchorConstellation(embed_dim=4, num_anchors=num_anchors)\n",
|
| 938 |
+
" self.patchwork = Patchwork(num_anchors, num_blocks=2)\n",
|
| 939 |
+
" self.head = nn.Linear(num_anchors, 10)\n",
|
| 940 |
+
"\n",
|
| 941 |
+
" def forward(self, x):\n",
|
| 942 |
+
" feat = self.frontend(x)\n",
|
| 943 |
+
" z_4d = self.to_4d(feat)\n",
|
| 944 |
+
" z_norm = l2_normalize(z_4d)\n",
|
| 945 |
+
" tri = self.constellation(z_norm)\n",
|
| 946 |
+
" pw = self.patchwork(tri)\n",
|
| 947 |
+
" logits = self.head(pw)\n",
|
| 948 |
+
" return logits, z_norm\n",
|
| 949 |
+
"\n",
|
| 950 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 951 |
+
"# COMPARE: Hopf-structured vs Random\n",
|
| 952 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 953 |
+
"print(\"\\n[EXP 2.1c] Hopf-structured Anchors...\")\n",
|
| 954 |
+
"model_2_1c = HopfAnchorModel(num_anchors=64).to(device)\n",
|
| 955 |
+
"result_2_1c = run_experiment(\n",
|
| 956 |
+
" \"2.1c_HopfAnchors\",\n",
|
| 957 |
+
" model_2_1c,\n",
|
| 958 |
+
" train_loader,\n",
|
| 959 |
+
" val_loader,\n",
|
| 960 |
+
" device,\n",
|
| 961 |
+
" epochs=100,\n",
|
| 962 |
+
" lr=1e-3,\n",
|
| 963 |
+
" patience=20\n",
|
| 964 |
+
")\n",
|
| 965 |
+
"scoreboard.add(result_2_1c)\n",
|
| 966 |
+
"\n",
|
| 967 |
+
"print(\"\\n[EXP 2.1c] Random Anchors for comparison...\")\n",
|
| 968 |
+
"model_2_1c_rand = HopfAnchorModel(num_anchors=64).to(device)\n",
|
| 969 |
+
"result_2_1c_rand = run_experiment(\n",
|
| 970 |
+
" \"2.1c_RandomAnchors\",\n",
|
| 971 |
+
" model_2_1c_rand,\n",
|
| 972 |
+
" train_loader,\n",
|
| 973 |
+
" val_loader,\n",
|
| 974 |
+
" device,\n",
|
| 975 |
+
" epochs=100,\n",
|
| 976 |
+
" lr=1e-3,\n",
|
| 977 |
+
" patience=20\n",
|
| 978 |
+
")\n",
|
| 979 |
+
"scoreboard.add(result_2_1c_rand)\n",
|
| 980 |
+
""
|
| 981 |
+
],
|
| 982 |
+
"execution_count": null,
|
| 983 |
+
"outputs": []
|
| 984 |
+
},
|
| 985 |
+
{
|
| 986 |
+
"cell_type": "markdown",
|
| 987 |
+
"metadata": {},
|
| 988 |
+
"source": [
|
| 989 |
+
"## Experiment 2.1d: Hierarchical Constellation"
|
| 990 |
+
]
|
| 991 |
+
},
|
| 992 |
+
{
|
| 993 |
+
"cell_type": "markdown",
|
| 994 |
+
"metadata": {},
|
| 995 |
+
"source": [
|
| 996 |
+
"\n",
|
| 997 |
+
"Coarse anchors on base S\u00b2, fine anchors per fiber.\n",
|
| 998 |
+
"Two-level constellation system.\n",
|
| 999 |
+
""
|
| 1000 |
+
]
|
| 1001 |
+
},
|
| 1002 |
+
{
|
| 1003 |
+
"cell_type": "code",
|
| 1004 |
+
"metadata": {},
|
| 1005 |
+
"source": [
|
| 1006 |
+
"\n",
|
| 1007 |
+
"class HierarchicalConstellation(nn.Module):\n",
|
| 1008 |
+
" '''Two-level: coarse S\u00b2 + fine fiber anchors.'''\n",
|
| 1009 |
+
" def __init__(self, embed_dim, coarse_anchors=8, fine_per_fiber=8):\n",
|
| 1010 |
+
" super().__init__()\n",
|
| 1011 |
+
" self.embed_dim = embed_dim\n",
|
| 1012 |
+
" self.coarse_anchors = coarse_anchors\n",
|
| 1013 |
+
" self.fine_per_fiber = fine_per_fiber\n",
|
| 1014 |
+
"\n",
|
| 1015 |
+
" # Coarse: S\u00b2 anchors\n",
|
| 1016 |
+
" coarse = F.normalize(torch.randn(coarse_anchors, 3), dim=-1)\n",
|
| 1017 |
+
" self.register_buffer(\"coarse\", coarse)\n",
|
| 1018 |
+
"\n",
|
| 1019 |
+
" # Fine: per-coarse-anchor fiber codes\n",
|
| 1020 |
+
" fine = F.normalize(torch.randn(coarse_anchors * fine_per_fiber, 1), dim=-1)\n",
|
| 1021 |
+
" self.register_buffer(\"fine\", fine)\n",
|
| 1022 |
+
"\n",
|
| 1023 |
+
" def forward(self, x):\n",
|
| 1024 |
+
" # x: (B, 4) on S\u00b3\n",
|
| 1025 |
+
" # Decompose as (base on S\u00b2, fiber)\n",
|
| 1026 |
+
" x_norm = l2_normalize(x)\n",
|
| 1027 |
+
" base_3d = x_norm[:, :3]\n",
|
| 1028 |
+
" fiber_1d = x_norm[:, 3:4]\n",
|
| 1029 |
+
"\n",
|
| 1030 |
+
" # Coarse distances on S\u00b2\n",
|
| 1031 |
+
" cos_sim_coarse = torch.mm(base_3d, self.coarse.T) # (B, coarse_anchors)\n",
|
| 1032 |
+
" dist_coarse = 1.0 - cos_sim_coarse\n",
|
| 1033 |
+
"\n",
|
| 1034 |
+
" # Fine distances on fiber (per coarse anchor)\n",
|
| 1035 |
+
" cos_sim_fine = torch.mm(fiber_1d, self.fine.T) # (B, coarse*fine)\n",
|
| 1036 |
+
" dist_fine = 1.0 - cos_sim_fine\n",
|
| 1037 |
+
"\n",
|
| 1038 |
+
" # Concatenate coarse and fine\n",
|
| 1039 |
+
" distances = torch.cat([dist_coarse, dist_fine], dim=1) # (B, coarse+coarse*fine)\n",
|
| 1040 |
+
" return distances\n",
|
| 1041 |
+
"\n",
|
| 1042 |
+
"class HierarchicalModel(nn.Module):\n",
|
| 1043 |
+
" def __init__(self):\n",
|
| 1044 |
+
" super().__init__()\n",
|
| 1045 |
+
" self.frontend = Scattering2DFeatures(out_dim=128)\n",
|
| 1046 |
+
" self.to_4d = nn.Linear(128, 4)\n",
|
| 1047 |
+
" self.constellation = HierarchicalConstellation(embed_dim=4, coarse_anchors=8, fine_per_fiber=8)\n",
|
| 1048 |
+
" total_anchors = 8 + 8*8\n",
|
| 1049 |
+
" self.patchwork = Patchwork(total_anchors, num_blocks=2)\n",
|
| 1050 |
+
" self.head = nn.Linear(total_anchors, 10)\n",
|
| 1051 |
+
"\n",
|
| 1052 |
+
" def forward(self, x):\n",
|
| 1053 |
+
" feat = self.frontend(x)\n",
|
| 1054 |
+
" z_4d = self.to_4d(feat)\n",
|
| 1055 |
+
" z_norm = l2_normalize(z_4d)\n",
|
| 1056 |
+
" tri = self.constellation(z_norm)\n",
|
| 1057 |
+
" pw = self.patchwork(tri)\n",
|
| 1058 |
+
" logits = self.head(pw)\n",
|
| 1059 |
+
" return logits, z_norm\n",
|
| 1060 |
+
"\n",
|
| 1061 |
+
"print(\"\\n[EXP 2.1d] Hierarchical Constellation...\")\n",
|
| 1062 |
+
"model_2_1d = HierarchicalModel().to(device)\n",
|
| 1063 |
+
"result_2_1d = run_experiment(\n",
|
| 1064 |
+
" \"2.1d_HierarchicalConst\",\n",
|
| 1065 |
+
" model_2_1d,\n",
|
| 1066 |
+
" train_loader,\n",
|
| 1067 |
+
" val_loader,\n",
|
| 1068 |
+
" device,\n",
|
| 1069 |
+
" epochs=100,\n",
|
| 1070 |
+
" lr=1e-3,\n",
|
| 1071 |
+
" patience=20\n",
|
| 1072 |
+
")\n",
|
| 1073 |
+
"scoreboard.add(result_2_1d)\n",
|
| 1074 |
+
""
|
| 1075 |
+
],
|
| 1076 |
+
"execution_count": null,
|
| 1077 |
+
"outputs": []
|
| 1078 |
+
},
|
| 1079 |
+
{
|
| 1080 |
+
"cell_type": "markdown",
|
| 1081 |
+
"metadata": {},
|
| 1082 |
+
"source": [
|
| 1083 |
+
"## Experiment 2.2a: Grassmannian \u2014 Class Subspaces"
|
| 1084 |
+
]
|
| 1085 |
+
},
|
| 1086 |
+
{
|
| 1087 |
+
"cell_type": "markdown",
|
| 1088 |
+
"metadata": {},
|
| 1089 |
+
"source": [
|
| 1090 |
+
"\n",
|
| 1091 |
+
"Each class = k-dim subspace of \u211d\u207f.\n",
|
| 1092 |
+
"Distance via principal angles (SVD-based).\n",
|
| 1093 |
+
"Literature: +1.3% over single vectors.\n",
|
| 1094 |
+
""
|
| 1095 |
+
]
|
| 1096 |
+
},
|
| 1097 |
+
{
|
| 1098 |
+
"cell_type": "code",
|
| 1099 |
+
"metadata": {},
|
| 1100 |
+
"source": [
|
| 1101 |
+
"\n",
|
| 1102 |
+
"class GrassmannianClassifier(nn.Module):\n",
|
| 1103 |
+
" '''Each class is a k-dim subspace.'''\n",
|
| 1104 |
+
" def __init__(self, embed_dim=64, num_classes=10, k=5):\n",
|
| 1105 |
+
" super().__init__()\n",
|
| 1106 |
+
" self.embed_dim = embed_dim\n",
|
| 1107 |
+
" self.num_classes = num_classes\n",
|
| 1108 |
+
" self.k = k\n",
|
| 1109 |
+
"\n",
|
| 1110 |
+
" # Each class: k orthonormal vectors\n",
|
| 1111 |
+
" U_init = []\n",
|
| 1112 |
+
" for _ in range(num_classes):\n",
|
| 1113 |
+
" u, _ = torch.linalg.qr(torch.randn(embed_dim, k))\n",
|
| 1114 |
+
" U_init.append(u)\n",
|
| 1115 |
+
" self.U = nn.ParameterList([nn.Parameter(u, requires_grad=True) for u in U_init])\n",
|
| 1116 |
+
"\n",
|
| 1117 |
+
" def forward(self, x):\n",
|
| 1118 |
+
" # x: (B, embed_dim) on S^{d-1}\n",
|
| 1119 |
+
" x = l2_normalize(x) # Normalize\n",
|
| 1120 |
+
" B = x.shape[0]\n",
|
| 1121 |
+
"\n",
|
| 1122 |
+
" # Distance to each class subspace via principal angles\n",
|
| 1123 |
+
" distances = []\n",
|
| 1124 |
+
" for c in range(self.num_classes):\n",
|
| 1125 |
+
" U_c = self.U[c] # (embed_dim, k)\n",
|
| 1126 |
+
" # Projection: ||x - U_c U_c^T x||\n",
|
| 1127 |
+
" proj = torch.mm(U_c, U_c.T) # (embed_dim, embed_dim)\n",
|
| 1128 |
+
" projected = torch.mm(x, proj.T) # (B, embed_dim)\n",
|
| 1129 |
+
" dist = torch.norm(x - projected, dim=1) # (B,)\n",
|
| 1130 |
+
" distances.append(dist)\n",
|
| 1131 |
+
"\n",
|
| 1132 |
+
" distances = torch.stack(distances, dim=1) # (B, num_classes)\n",
|
| 1133 |
+
" logits = -distances # Negative distance = similarity\n",
|
| 1134 |
+
" return logits, x\n",
|
| 1135 |
+
"\n",
|
| 1136 |
+
"class GrassmannianModel(nn.Module):\n",
|
| 1137 |
+
" def __init__(self):\n",
|
| 1138 |
+
" super().__init__()\n",
|
| 1139 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1140 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1141 |
+
" self.classifier = GrassmannianClassifier(embed_dim=64, num_classes=10, k=5)\n",
|
| 1142 |
+
"\n",
|
| 1143 |
+
" def forward(self, x):\n",
|
| 1144 |
+
" feat = self.encoder(x)\n",
|
| 1145 |
+
" emb = self.to_embedding(feat)\n",
|
| 1146 |
+
" emb = l2_normalize(emb)\n",
|
| 1147 |
+
" logits, _ = self.classifier(emb)\n",
|
| 1148 |
+
" return logits, emb\n",
|
| 1149 |
+
"\n",
|
| 1150 |
+
"print(\"\\n[EXP 2.2a] Grassmannian Class Subspaces...\")\n",
|
| 1151 |
+
"model_2_2a = GrassmannianModel().to(device)\n",
|
| 1152 |
+
"result_2_2a = run_experiment(\n",
|
| 1153 |
+
" \"2.2a_GrassmannianClasses\",\n",
|
| 1154 |
+
" model_2_2a,\n",
|
| 1155 |
+
" train_loader,\n",
|
| 1156 |
+
" val_loader,\n",
|
| 1157 |
+
" device,\n",
|
| 1158 |
+
" epochs=100,\n",
|
| 1159 |
+
" lr=1e-3,\n",
|
| 1160 |
+
" patience=20\n",
|
| 1161 |
+
")\n",
|
| 1162 |
+
"scoreboard.add(result_2_2a)\n",
|
| 1163 |
+
""
|
| 1164 |
+
],
|
| 1165 |
+
"execution_count": null,
|
| 1166 |
+
"outputs": []
|
| 1167 |
+
},
|
| 1168 |
+
{
|
| 1169 |
+
"cell_type": "markdown",
|
| 1170 |
+
"metadata": {},
|
| 1171 |
+
"source": [
|
| 1172 |
+
"## Experiment 2.2b: Grassmannian Distance Metrics"
|
| 1173 |
+
]
|
| 1174 |
+
},
|
| 1175 |
+
{
|
| 1176 |
+
"cell_type": "markdown",
|
| 1177 |
+
"metadata": {},
|
| 1178 |
+
"source": [
|
| 1179 |
+
"\n",
|
| 1180 |
+
"Compare:\n",
|
| 1181 |
+
"- Geodesic distance on Grassmannian\n",
|
| 1182 |
+
"- Chordal distance\n",
|
| 1183 |
+
"- Projection distance (subspace angle)\n",
|
| 1184 |
+
""
|
| 1185 |
+
]
|
| 1186 |
+
},
|
| 1187 |
+
{
|
| 1188 |
+
"cell_type": "code",
|
| 1189 |
+
"metadata": {},
|
| 1190 |
+
"source": [
|
| 1191 |
+
"\n",
|
| 1192 |
+
"class GrassmannianMetricAblation(nn.Module):\n",
|
| 1193 |
+
" '''Grassmannian with configurable metric.'''\n",
|
| 1194 |
+
" def __init__(self, embed_dim=64, num_classes=10, k=5, metric=\"geodesic\"):\n",
|
| 1195 |
+
" super().__init__()\n",
|
| 1196 |
+
" self.embed_dim = embed_dim\n",
|
| 1197 |
+
" self.num_classes = num_classes\n",
|
| 1198 |
+
" self.k = k\n",
|
| 1199 |
+
" self.metric = metric\n",
|
| 1200 |
+
"\n",
|
| 1201 |
+
" U_init = []\n",
|
| 1202 |
+
" for _ in range(num_classes):\n",
|
| 1203 |
+
" u, _ = torch.linalg.qr(torch.randn(embed_dim, k))\n",
|
| 1204 |
+
" U_init.append(u)\n",
|
| 1205 |
+
" self.U = nn.ParameterList([nn.Parameter(u, requires_grad=True) for u in U_init])\n",
|
| 1206 |
+
"\n",
|
| 1207 |
+
" def forward(self, x):\n",
|
| 1208 |
+
" x = l2_normalize(x)\n",
|
| 1209 |
+
" B = x.shape[0]\n",
|
| 1210 |
+
" distances = []\n",
|
| 1211 |
+
"\n",
|
| 1212 |
+
" for c in range(self.num_classes):\n",
|
| 1213 |
+
" U_c = self.U[c]\n",
|
| 1214 |
+
"\n",
|
| 1215 |
+
" if self.metric == \"projection\":\n",
|
| 1216 |
+
" # Subspace angle: arccos(||U_c^T x||)\n",
|
| 1217 |
+
" proj_norm = torch.norm(torch.mm(x, U_c), dim=1)\n",
|
| 1218 |
+
" dist = torch.acos(torch.clamp(proj_norm, -1+1e-6, 1-1e-6))\n",
|
| 1219 |
+
" elif self.metric == \"chordal\":\n",
|
| 1220 |
+
" # Chordal: ||U_c U_c^T - x x^T||_F\n",
|
| 1221 |
+
" proj_c = torch.mm(U_c, U_c.T)\n",
|
| 1222 |
+
" xx_t = torch.bmm(x.unsqueeze(2), x.unsqueeze(1)) # (B, embed_dim, embed_dim)\n",
|
| 1223 |
+
" dist = torch.norm(proj_c.unsqueeze(0) - xx_t, dim=(1,2))\n",
|
| 1224 |
+
" else: # geodesic\n",
|
| 1225 |
+
" # Geodesic approximation via subspace angles\n",
|
| 1226 |
+
" proj_norm = torch.norm(torch.mm(x, U_c), dim=1)\n",
|
| 1227 |
+
" dist = torch.acos(torch.clamp(proj_norm, -1+1e-6, 1-1e-6)) ** 2\n",
|
| 1228 |
+
"\n",
|
| 1229 |
+
" distances.append(dist)\n",
|
| 1230 |
+
"\n",
|
| 1231 |
+
" distances = torch.stack(distances, dim=1)\n",
|
| 1232 |
+
" logits = -distances\n",
|
| 1233 |
+
" return logits, x\n",
|
| 1234 |
+
"\n",
|
| 1235 |
+
"class GrassmannianMetricModel(nn.Module):\n",
|
| 1236 |
+
" def __init__(self, metric=\"geodesic\"):\n",
|
| 1237 |
+
" super().__init__()\n",
|
| 1238 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1239 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1240 |
+
" self.classifier = GrassmannianMetricAblation(embed_dim=64, num_classes=10, k=5, metric=metric)\n",
|
| 1241 |
+
"\n",
|
| 1242 |
+
" def forward(self, x):\n",
|
| 1243 |
+
" feat = self.encoder(x)\n",
|
| 1244 |
+
" emb = self.to_embedding(feat)\n",
|
| 1245 |
+
" emb = l2_normalize(emb)\n",
|
| 1246 |
+
" logits, _ = self.classifier(emb)\n",
|
| 1247 |
+
" return logits, emb\n",
|
| 1248 |
+
"\n",
|
| 1249 |
+
"# Test all three metrics\n",
|
| 1250 |
+
"for metric in [\"projection\", \"chordal\", \"geodesic\"]:\n",
|
| 1251 |
+
" print(f\"\\n[EXP 2.2b] Testing {metric} metric...\")\n",
|
| 1252 |
+
" model = GrassmannianMetricModel(metric=metric).to(device)\n",
|
| 1253 |
+
" result = run_experiment(\n",
|
| 1254 |
+
" f\"2.2b_Grassmannian_{metric}\",\n",
|
| 1255 |
+
" model,\n",
|
| 1256 |
+
" train_loader,\n",
|
| 1257 |
+
" val_loader,\n",
|
| 1258 |
+
" device,\n",
|
| 1259 |
+
" epochs=100,\n",
|
| 1260 |
+
" lr=1e-3,\n",
|
| 1261 |
+
" patience=20\n",
|
| 1262 |
+
" )\n",
|
| 1263 |
+
" scoreboard.add(result)\n",
|
| 1264 |
+
""
|
| 1265 |
+
],
|
| 1266 |
+
"execution_count": null,
|
| 1267 |
+
"outputs": []
|
| 1268 |
+
},
|
| 1269 |
+
{
|
| 1270 |
+
"cell_type": "markdown",
|
| 1271 |
+
"metadata": {},
|
| 1272 |
+
"source": [
|
| 1273 |
+
"## Experiment 2.2c: Per-Class Anchor Subspace"
|
| 1274 |
+
]
|
| 1275 |
+
},
|
| 1276 |
+
{
|
| 1277 |
+
"cell_type": "markdown",
|
| 1278 |
+
"metadata": {},
|
| 1279 |
+
"source": [
|
| 1280 |
+
"\n",
|
| 1281 |
+
"Each anchor defines a subspace, not a point.\n",
|
| 1282 |
+
"Extend constellation to subspace-valued anchors.\n",
|
| 1283 |
+
""
|
| 1284 |
+
]
|
| 1285 |
+
},
|
| 1286 |
+
{
|
| 1287 |
+
"cell_type": "code",
|
| 1288 |
+
"metadata": {},
|
| 1289 |
+
"source": [
|
| 1290 |
+
"\n",
|
| 1291 |
+
"class SubspaceConstellation(nn.Module):\n",
|
| 1292 |
+
" '''Each anchor is a k-dim subspace.'''\n",
|
| 1293 |
+
" def __init__(self, embed_dim, num_anchors=16, k=3):\n",
|
| 1294 |
+
" super().__init__()\n",
|
| 1295 |
+
" self.embed_dim = embed_dim\n",
|
| 1296 |
+
" self.num_anchors = num_anchors\n",
|
| 1297 |
+
" self.k = k\n",
|
| 1298 |
+
"\n",
|
| 1299 |
+
" # Each anchor: orthonormal basis\n",
|
| 1300 |
+
" U_list = []\n",
|
| 1301 |
+
" for _ in range(num_anchors):\n",
|
| 1302 |
+
" u, _ = torch.linalg.qr(torch.randn(embed_dim, k))\n",
|
| 1303 |
+
" U_list.append(u)\n",
|
| 1304 |
+
" self.U = nn.ParameterList([nn.Parameter(u, requires_grad=False) for u in U_list])\n",
|
| 1305 |
+
"\n",
|
| 1306 |
+
" def forward(self, x):\n",
|
| 1307 |
+
" # x: (B, embed_dim)\n",
|
| 1308 |
+
" x = l2_normalize(x)\n",
|
| 1309 |
+
" B = x.shape[0]\n",
|
| 1310 |
+
" distances = []\n",
|
| 1311 |
+
"\n",
|
| 1312 |
+
" for i in range(self.num_anchors):\n",
|
| 1313 |
+
" U_i = self.U[i] # (embed_dim, k)\n",
|
| 1314 |
+
" # Distance: 1 - ||U_i^T x||\n",
|
| 1315 |
+
" proj_norm = torch.norm(torch.mm(x, U_i), dim=1) # (B,)\n",
|
| 1316 |
+
" dist = 1.0 - proj_norm\n",
|
| 1317 |
+
" distances.append(dist)\n",
|
| 1318 |
+
"\n",
|
| 1319 |
+
" distances = torch.stack(distances, dim=1) # (B, num_anchors)\n",
|
| 1320 |
+
" return distances\n",
|
| 1321 |
+
"\n",
|
| 1322 |
+
"class SubspaceConstellationModel(nn.Module):\n",
|
| 1323 |
+
" def __init__(self):\n",
|
| 1324 |
+
" super().__init__()\n",
|
| 1325 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1326 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1327 |
+
" self.constellation = SubspaceConstellation(embed_dim=64, num_anchors=16, k=3)\n",
|
| 1328 |
+
" self.patchwork = Patchwork(16, num_blocks=2)\n",
|
| 1329 |
+
" self.head = nn.Linear(16, 10)\n",
|
| 1330 |
+
"\n",
|
| 1331 |
+
" def forward(self, x):\n",
|
| 1332 |
+
" feat = self.encoder(x)\n",
|
| 1333 |
+
" emb = self.to_embedding(feat)\n",
|
| 1334 |
+
" emb = l2_normalize(emb)\n",
|
| 1335 |
+
" tri = self.constellation(emb)\n",
|
| 1336 |
+
" pw = self.patchwork(tri)\n",
|
| 1337 |
+
" logits = self.head(pw)\n",
|
| 1338 |
+
" return logits, emb\n",
|
| 1339 |
+
"\n",
|
| 1340 |
+
"print(\"\\n[EXP 2.2c] Per-Class Anchor Subspace...\")\n",
|
| 1341 |
+
"model_2_2c = SubspaceConstellationModel().to(device)\n",
|
| 1342 |
+
"result_2_2c = run_experiment(\n",
|
| 1343 |
+
" \"2.2c_SubspaceAnchors\",\n",
|
| 1344 |
+
" model_2_2c,\n",
|
| 1345 |
+
" train_loader,\n",
|
| 1346 |
+
" val_loader,\n",
|
| 1347 |
+
" device,\n",
|
| 1348 |
+
" epochs=100,\n",
|
| 1349 |
+
" lr=1e-3,\n",
|
| 1350 |
+
" patience=20\n",
|
| 1351 |
+
")\n",
|
| 1352 |
+
"scoreboard.add(result_2_2c)\n",
|
| 1353 |
+
""
|
| 1354 |
+
],
|
| 1355 |
+
"execution_count": null,
|
| 1356 |
+
"outputs": []
|
| 1357 |
+
},
|
| 1358 |
+
{
|
| 1359 |
+
"cell_type": "markdown",
|
| 1360 |
+
"metadata": {},
|
| 1361 |
+
"source": [
|
| 1362 |
+
"## Experiment 2.3a: Flag Manifold \u2014 Nested Hierarchy"
|
| 1363 |
+
]
|
| 1364 |
+
},
|
| 1365 |
+
{
|
| 1366 |
+
"cell_type": "markdown",
|
| 1367 |
+
"metadata": {},
|
| 1368 |
+
"source": [
|
| 1369 |
+
"\n",
|
| 1370 |
+
"V\u2081 \u2282 V\u2082 \u2282 ... \u2282 V\u2096 nested subspaces.\n",
|
| 1371 |
+
"Flag decomposition of frequency channels: DC \u2282 low \u2282 mid \u2282 high.\n",
|
| 1372 |
+
""
|
| 1373 |
+
]
|
| 1374 |
+
},
|
| 1375 |
+
{
|
| 1376 |
+
"cell_type": "code",
|
| 1377 |
+
"metadata": {},
|
| 1378 |
+
"source": [
|
| 1379 |
+
"\n",
|
| 1380 |
+
"class FlagManifold(nn.Module):\n",
|
| 1381 |
+
" '''Nested subspace hierarchy on Flag(d; k1, k2, k3, d).'''\n",
|
| 1382 |
+
" def __init__(self, embed_dim=64, dims=[10, 20, 40, 64]):\n",
|
| 1383 |
+
" super().__init__()\n",
|
| 1384 |
+
" self.embed_dim = embed_dim\n",
|
| 1385 |
+
" self.dims = dims # increasing sequence\n",
|
| 1386 |
+
"\n",
|
| 1387 |
+
" # Build nested bases\n",
|
| 1388 |
+
" # V1 \u2282 V2 \u2282 ... \u2282 Vd\n",
|
| 1389 |
+
" bases = []\n",
|
| 1390 |
+
" A = torch.randn(embed_dim, embed_dim)\n",
|
| 1391 |
+
" Q, _ = torch.linalg.qr(A)\n",
|
| 1392 |
+
"\n",
|
| 1393 |
+
" for d in dims:\n",
|
| 1394 |
+
" bases.append(Q[:, :d])\n",
|
| 1395 |
+
"\n",
|
| 1396 |
+
" self.bases = nn.ParameterList([nn.Parameter(b, requires_grad=False) for b in bases])\n",
|
| 1397 |
+
"\n",
|
| 1398 |
+
" def forward(self, x):\n",
|
| 1399 |
+
" # x: (B, embed_dim)\n",
|
| 1400 |
+
" x = l2_normalize(x)\n",
|
| 1401 |
+
" B = x.shape[0]\n",
|
| 1402 |
+
"\n",
|
| 1403 |
+
" # Compute projections at each level\n",
|
| 1404 |
+
" features = []\n",
|
| 1405 |
+
" for base in self.bases:\n",
|
| 1406 |
+
" # Projection: ||base base^T x||\n",
|
| 1407 |
+
" proj_norm = torch.norm(torch.mm(x, base), dim=1)\n",
|
| 1408 |
+
" features.append(proj_norm.unsqueeze(-1))\n",
|
| 1409 |
+
"\n",
|
| 1410 |
+
" features = torch.cat(features, dim=1) # (B, len(dims))\n",
|
| 1411 |
+
" return features\n",
|
| 1412 |
+
"\n",
|
| 1413 |
+
"class FlagModel(nn.Module):\n",
|
| 1414 |
+
" def __init__(self):\n",
|
| 1415 |
+
" super().__init__()\n",
|
| 1416 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1417 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1418 |
+
" self.flag = FlagManifold(embed_dim=64, dims=[10, 20, 40, 64])\n",
|
| 1419 |
+
" self.patchwork = Patchwork(4, num_blocks=1)\n",
|
| 1420 |
+
" self.head = nn.Linear(4, 10)\n",
|
| 1421 |
+
"\n",
|
| 1422 |
+
" def forward(self, x):\n",
|
| 1423 |
+
" feat = self.encoder(x)\n",
|
| 1424 |
+
" emb = self.to_embedding(feat)\n",
|
| 1425 |
+
" emb = l2_normalize(emb)\n",
|
| 1426 |
+
" flag_feat = self.flag(emb)\n",
|
| 1427 |
+
" pw = self.patchwork(flag_feat)\n",
|
| 1428 |
+
" logits = self.head(pw)\n",
|
| 1429 |
+
" return logits, emb\n",
|
| 1430 |
+
"\n",
|
| 1431 |
+
"print(\"\\n[EXP 2.3a] Flag Manifold \u2014 Nested Hierarchy...\")\n",
|
| 1432 |
+
"model_2_3a = FlagModel().to(device)\n",
|
| 1433 |
+
"result_2_3a = run_experiment(\n",
|
| 1434 |
+
" \"2.3a_FlagManifold\",\n",
|
| 1435 |
+
" model_2_3a,\n",
|
| 1436 |
+
" train_loader,\n",
|
| 1437 |
+
" val_loader,\n",
|
| 1438 |
+
" device,\n",
|
| 1439 |
+
" epochs=100,\n",
|
| 1440 |
+
" lr=1e-3,\n",
|
| 1441 |
+
" patience=20\n",
|
| 1442 |
+
")\n",
|
| 1443 |
+
"scoreboard.add(result_2_3a)\n",
|
| 1444 |
+
""
|
| 1445 |
+
],
|
| 1446 |
+
"execution_count": null,
|
| 1447 |
+
"outputs": []
|
| 1448 |
+
},
|
| 1449 |
+
{
|
| 1450 |
+
"cell_type": "markdown",
|
| 1451 |
+
"metadata": {},
|
| 1452 |
+
"source": [
|
| 1453 |
+
"## Experiment 2.3b: Flag-Structured Anchor Hierarchy"
|
| 1454 |
+
]
|
| 1455 |
+
},
|
| 1456 |
+
{
|
| 1457 |
+
"cell_type": "markdown",
|
| 1458 |
+
"metadata": {},
|
| 1459 |
+
"source": [
|
| 1460 |
+
"\n",
|
| 1461 |
+
"Coarse-to-fine anchor hierarchy based on flag decomposition.\n",
|
| 1462 |
+
""
|
| 1463 |
+
]
|
| 1464 |
+
},
|
| 1465 |
+
{
|
| 1466 |
+
"cell_type": "code",
|
| 1467 |
+
"metadata": {},
|
| 1468 |
+
"source": [
|
| 1469 |
+
"\n",
|
| 1470 |
+
"class FlagStructuredConstellation(nn.Module):\n",
|
| 1471 |
+
" '''Anchors organized in nested flag structure.'''\n",
|
| 1472 |
+
" def __init__(self, embed_dim=64, num_anchors=64):\n",
|
| 1473 |
+
" super().__init__()\n",
|
| 1474 |
+
" # Organize anchors into levels\n",
|
| 1475 |
+
" level_sizes = [8, 16, 24, 16] # sum=64\n",
|
| 1476 |
+
" anchors = []\n",
|
| 1477 |
+
"\n",
|
| 1478 |
+
" for level_size in level_sizes:\n",
|
| 1479 |
+
" level_anchors = F.normalize(torch.randn(level_size, embed_dim), dim=-1)\n",
|
| 1480 |
+
" anchors.append(level_anchors)\n",
|
| 1481 |
+
"\n",
|
| 1482 |
+
" self.anchors = nn.ParameterList([nn.Parameter(a, requires_grad=False) for a in anchors])\n",
|
| 1483 |
+
" self.num_anchors = num_anchors\n",
|
| 1484 |
+
"\n",
|
| 1485 |
+
" def forward(self, x):\n",
|
| 1486 |
+
" x = l2_normalize(x)\n",
|
| 1487 |
+
" distances = []\n",
|
| 1488 |
+
"\n",
|
| 1489 |
+
" for level_anchors in self.anchors:\n",
|
| 1490 |
+
" cos_sim = torch.mm(x, level_anchors.T)\n",
|
| 1491 |
+
" dist = 1.0 - cos_sim\n",
|
| 1492 |
+
" distances.append(dist)\n",
|
| 1493 |
+
"\n",
|
| 1494 |
+
" distances = torch.cat(distances, dim=1) # (B, num_anchors)\n",
|
| 1495 |
+
" return distances\n",
|
| 1496 |
+
"\n",
|
| 1497 |
+
"class FlagAnchorModel(nn.Module):\n",
|
| 1498 |
+
" def __init__(self):\n",
|
| 1499 |
+
" super().__init__()\n",
|
| 1500 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1501 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1502 |
+
" self.constellation = FlagStructuredConstellation(embed_dim=64, num_anchors=64)\n",
|
| 1503 |
+
" self.patchwork = Patchwork(64, num_blocks=2)\n",
|
| 1504 |
+
" self.head = nn.Linear(64, 10)\n",
|
| 1505 |
+
"\n",
|
| 1506 |
+
" def forward(self, x):\n",
|
| 1507 |
+
" feat = self.encoder(x)\n",
|
| 1508 |
+
" emb = self.to_embedding(feat)\n",
|
| 1509 |
+
" emb = l2_normalize(emb)\n",
|
| 1510 |
+
" tri = self.constellation(emb)\n",
|
| 1511 |
+
" pw = self.patchwork(tri)\n",
|
| 1512 |
+
" logits = self.head(pw)\n",
|
| 1513 |
+
" return logits, emb\n",
|
| 1514 |
+
"\n",
|
| 1515 |
+
"print(\"\\n[EXP 2.3b] Flag-Structured Anchors...\")\n",
|
| 1516 |
+
"model_2_3b = FlagAnchorModel().to(device)\n",
|
| 1517 |
+
"result_2_3b = run_experiment(\n",
|
| 1518 |
+
" \"2.3b_FlagAnchors\",\n",
|
| 1519 |
+
" model_2_3b,\n",
|
| 1520 |
+
" train_loader,\n",
|
| 1521 |
+
" val_loader,\n",
|
| 1522 |
+
" device,\n",
|
| 1523 |
+
" epochs=100,\n",
|
| 1524 |
+
" lr=1e-3,\n",
|
| 1525 |
+
" patience=20\n",
|
| 1526 |
+
")\n",
|
| 1527 |
+
"scoreboard.add(result_2_3b)\n",
|
| 1528 |
+
""
|
| 1529 |
+
],
|
| 1530 |
+
"execution_count": null,
|
| 1531 |
+
"outputs": []
|
| 1532 |
+
},
|
| 1533 |
+
{
|
| 1534 |
+
"cell_type": "markdown",
|
| 1535 |
+
"metadata": {},
|
| 1536 |
+
"source": [
|
| 1537 |
+
"## Experiment 2.4a: Von Mises-Fisher Soft Assignment"
|
| 1538 |
+
]
|
| 1539 |
+
},
|
| 1540 |
+
{
|
| 1541 |
+
"cell_type": "markdown",
|
| 1542 |
+
"metadata": {},
|
| 1543 |
+
"source": [
|
| 1544 |
+
"\n",
|
| 1545 |
+
"Replace hard nearest-anchor with vMF soft posteriors.\n",
|
| 1546 |
+
"p(j|x) = \u03b1_j f(x;\u03bc_j,\u03ba_j) / \u03a3 \u03b1_k f(x;\u03bc_k,\u03ba_k)\n",
|
| 1547 |
+
"Learned \u03ba per anchor = adaptive influence radius.\n",
|
| 1548 |
+
""
|
| 1549 |
+
]
|
| 1550 |
+
},
|
| 1551 |
+
{
|
| 1552 |
+
"cell_type": "code",
|
| 1553 |
+
"metadata": {},
|
| 1554 |
+
"source": [
|
| 1555 |
+
"\n",
|
| 1556 |
+
"class vMFSoftAssignment(nn.Module):\n",
|
| 1557 |
+
" '''Von Mises-Fisher soft assignment with learned concentration.'''\n",
|
| 1558 |
+
" def __init__(self, embed_dim, num_anchors=64):\n",
|
| 1559 |
+
" super().__init__()\n",
|
| 1560 |
+
" self.embed_dim = embed_dim\n",
|
| 1561 |
+
" self.num_anchors = num_anchors\n",
|
| 1562 |
+
"\n",
|
| 1563 |
+
" anchors = F.normalize(torch.randn(num_anchors, embed_dim), dim=-1)\n",
|
| 1564 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False)\n",
|
| 1565 |
+
"\n",
|
| 1566 |
+
" # Learned concentration per anchor\n",
|
| 1567 |
+
" self.kappa = nn.Parameter(torch.ones(num_anchors) * 10.0)\n",
|
| 1568 |
+
"\n",
|
| 1569 |
+
" def forward(self, x):\n",
|
| 1570 |
+
" # x: (B, embed_dim) on S^{d-1}\n",
|
| 1571 |
+
" x = l2_normalize(x)\n",
|
| 1572 |
+
" B = x.shape[0]\n",
|
| 1573 |
+
"\n",
|
| 1574 |
+
" # Cosine similarity\n",
|
| 1575 |
+
" cos_sim = torch.mm(x, self.anchors.T) # (B, num_anchors)\n",
|
| 1576 |
+
"\n",
|
| 1577 |
+
" # vMF probability: exp(\u03ba * cos_sim) (normalization absorbed in softmax)\n",
|
| 1578 |
+
" log_prob = self.kappa.unsqueeze(0) * cos_sim # (B, num_anchors)\n",
|
| 1579 |
+
" probs = F.softmax(log_prob, dim=1) # (B, num_anchors)\n",
|
| 1580 |
+
"\n",
|
| 1581 |
+
" # Return soft assignment (can use as features)\n",
|
| 1582 |
+
" return probs, self.kappa\n",
|
| 1583 |
+
"\n",
|
| 1584 |
+
"class vMFModel(nn.Module):\n",
|
| 1585 |
+
" def __init__(self):\n",
|
| 1586 |
+
" super().__init__()\n",
|
| 1587 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1588 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1589 |
+
" self.vmf = vMFSoftAssignment(embed_dim=64, num_anchors=64)\n",
|
| 1590 |
+
" self.patchwork = Patchwork(64, num_blocks=2)\n",
|
| 1591 |
+
" self.head = nn.Linear(64, 10)\n",
|
| 1592 |
+
"\n",
|
| 1593 |
+
" def forward(self, x):\n",
|
| 1594 |
+
" feat = self.encoder(x)\n",
|
| 1595 |
+
" emb = self.to_embedding(feat)\n",
|
| 1596 |
+
" emb = l2_normalize(emb)\n",
|
| 1597 |
+
" soft_assign, kappa = self.vmf(emb)\n",
|
| 1598 |
+
" pw = self.patchwork(soft_assign)\n",
|
| 1599 |
+
" logits = self.head(pw)\n",
|
| 1600 |
+
" return logits, emb\n",
|
| 1601 |
+
"\n",
|
| 1602 |
+
"print(\"\\n[EXP 2.4a] vMF Soft Assignment...\")\n",
|
| 1603 |
+
"model_2_4a = vMFModel().to(device)\n",
|
| 1604 |
+
"result_2_4a = run_experiment(\n",
|
| 1605 |
+
" \"2.4a_vMFSoftAssign\",\n",
|
| 1606 |
+
" model_2_4a,\n",
|
| 1607 |
+
" train_loader,\n",
|
| 1608 |
+
" val_loader,\n",
|
| 1609 |
+
" device,\n",
|
| 1610 |
+
" epochs=100,\n",
|
| 1611 |
+
" lr=1e-3,\n",
|
| 1612 |
+
" patience=20\n",
|
| 1613 |
+
")\n",
|
| 1614 |
+
"scoreboard.add(result_2_4a)\n",
|
| 1615 |
+
""
|
| 1616 |
+
],
|
| 1617 |
+
"execution_count": null,
|
| 1618 |
+
"outputs": []
|
| 1619 |
+
},
|
| 1620 |
+
{
|
| 1621 |
+
"cell_type": "markdown",
|
| 1622 |
+
"metadata": {},
|
| 1623 |
+
"source": [
|
| 1624 |
+
"## Experiment 2.4b: vMF Mixture EM for Anchor Init"
|
| 1625 |
+
]
|
| 1626 |
+
},
|
| 1627 |
+
{
|
| 1628 |
+
"cell_type": "markdown",
|
| 1629 |
+
"metadata": {},
|
| 1630 |
+
"source": [
|
| 1631 |
+
"\n",
|
| 1632 |
+
"Use EM to initialize anchors instead of uniform hypersphere.\n",
|
| 1633 |
+
"Pre-cluster training set embeddings with vMF mixture model.\n",
|
| 1634 |
+
""
|
| 1635 |
+
]
|
| 1636 |
+
},
|
| 1637 |
+
{
|
| 1638 |
+
"cell_type": "code",
|
| 1639 |
+
"metadata": {},
|
| 1640 |
+
"source": [
|
| 1641 |
+
"\n",
|
| 1642 |
+
"class vMFMixtureEM:\n",
|
| 1643 |
+
" '''vMF mixture clustering via EM.'''\n",
|
| 1644 |
+
" def __init__(self, K=64, max_iter=20, tol=1e-4):\n",
|
| 1645 |
+
" self.K = K\n",
|
| 1646 |
+
" self.max_iter = max_iter\n",
|
| 1647 |
+
" self.tol = tol\n",
|
| 1648 |
+
"\n",
|
| 1649 |
+
" def fit(self, X):\n",
|
| 1650 |
+
" '''X: (N, d) data on sphere.'''\n",
|
| 1651 |
+
" N, d = X.shape\n",
|
| 1652 |
+
" X = X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-8)\n",
|
| 1653 |
+
"\n",
|
| 1654 |
+
" # Initialize\n",
|
| 1655 |
+
" mu = X[np.random.choice(N, self.K, replace=False)] # (K, d)\n",
|
| 1656 |
+
" kappa = np.ones(self.K) * 5.0\n",
|
| 1657 |
+
" alpha = np.ones(self.K) / self.K\n",
|
| 1658 |
+
"\n",
|
| 1659 |
+
" for it in range(self.max_iter):\n",
|
| 1660 |
+
" # E-step\n",
|
| 1661 |
+
" cos_sim = X @ mu.T # (N, K)\n",
|
| 1662 |
+
" log_prob = np.log(alpha + 1e-8) + kappa * cos_sim\n",
|
| 1663 |
+
" log_prob -= log_prob.max(axis=1, keepdims=True)\n",
|
| 1664 |
+
" gamma = np.exp(log_prob)\n",
|
| 1665 |
+
" gamma /= (gamma.sum(axis=1, keepdims=True) + 1e-8)\n",
|
| 1666 |
+
"\n",
|
| 1667 |
+
" # M-step\n",
|
| 1668 |
+
" Nk = gamma.sum(axis=0)\n",
|
| 1669 |
+
" alpha = Nk / N\n",
|
| 1670 |
+
"\n",
|
| 1671 |
+
" for k in range(self.K):\n",
|
| 1672 |
+
" mu_k = (gamma[:, k:k+1] * X).sum(axis=0)\n",
|
| 1673 |
+
" mu_k = mu_k / (np.linalg.norm(mu_k) + 1e-8)\n",
|
| 1674 |
+
" mu[k] = mu_k\n",
|
| 1675 |
+
"\n",
|
| 1676 |
+
" rbar = (gamma[:, k] * (X @ mu_k)).sum() / (Nk[k] + 1e-8)\n",
|
| 1677 |
+
" kappa[k] = (d * rbar - 1) / (1 - rbar**2 + 1e-8)\n",
|
| 1678 |
+
"\n",
|
| 1679 |
+
" return mu, kappa, alpha\n",
|
| 1680 |
+
"\n",
|
| 1681 |
+
"class EMInitModel(nn.Module):\n",
|
| 1682 |
+
" def __init__(self):\n",
|
| 1683 |
+
" super().__init__()\n",
|
| 1684 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1685 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1686 |
+
"\n",
|
| 1687 |
+
" # Placeholder constellation (will be initialized after EM)\n",
|
| 1688 |
+
" self.constellation = Constellation(embed_dim=64, num_anchors=64, init=\"uniform\")\n",
|
| 1689 |
+
" self.patchwork = Patchwork(64, num_blocks=2)\n",
|
| 1690 |
+
" self.head = nn.Linear(64, 10)\n",
|
| 1691 |
+
"\n",
|
| 1692 |
+
" def forward(self, x):\n",
|
| 1693 |
+
" feat = self.encoder(x)\n",
|
| 1694 |
+
" emb = self.to_embedding(feat)\n",
|
| 1695 |
+
" emb = l2_normalize(emb)\n",
|
| 1696 |
+
" tri = self.constellation(emb)\n",
|
| 1697 |
+
" pw = self.patchwork(tri)\n",
|
| 1698 |
+
" logits = self.head(pw)\n",
|
| 1699 |
+
" return logits, emb\n",
|
| 1700 |
+
"\n",
|
| 1701 |
+
"print(\"\\n[EXP 2.4b] vMF Mixture EM Anchor Init...\")\n",
|
| 1702 |
+
"\n",
|
| 1703 |
+
"# Collect training embeddings\n",
|
| 1704 |
+
"print(\" Collecting training embeddings...\")\n",
|
| 1705 |
+
"all_emb = []\n",
|
| 1706 |
+
"all_labels = []\n",
|
| 1707 |
+
"model_temp = EMInitModel().to(device)\n",
|
| 1708 |
+
"with torch.no_grad():\n",
|
| 1709 |
+
" for x, y in train_loader:\n",
|
| 1710 |
+
" x = x.to(device)\n",
|
| 1711 |
+
" feat = model_temp.encoder(x)\n",
|
| 1712 |
+
" emb = model_temp.to_embedding(feat)\n",
|
| 1713 |
+
" emb = l2_normalize(emb)\n",
|
| 1714 |
+
" all_emb.append(emb.cpu().numpy())\n",
|
| 1715 |
+
" all_labels.append(y.numpy())\n",
|
| 1716 |
+
"\n",
|
| 1717 |
+
"all_emb = np.vstack(all_emb)\n",
|
| 1718 |
+
"print(f\" Embeddings shape: {all_emb.shape}\")\n",
|
| 1719 |
+
"\n",
|
| 1720 |
+
"# EM fit\n",
|
| 1721 |
+
"em = vMFMixtureEM(K=64, max_iter=20)\n",
|
| 1722 |
+
"mu_em, kappa_em, alpha_em = em.fit(all_emb)\n",
|
| 1723 |
+
"print(f\" EM fit complete. Mean \u03ba: {kappa_em.mean():.2f}\")\n",
|
| 1724 |
+
"\n",
|
| 1725 |
+
"# Initialize model with EM anchors\n",
|
| 1726 |
+
"model_2_4b = EMInitModel().to(device)\n",
|
| 1727 |
+
"with torch.no_grad():\n",
|
| 1728 |
+
" model_2_4b.constellation.anchors.copy_(torch.tensor(mu_em, dtype=torch.float32).to(device))\n",
|
| 1729 |
+
"\n",
|
| 1730 |
+
"result_2_4b = run_experiment(\n",
|
| 1731 |
+
" \"2.4b_vMFEM_Init\",\n",
|
| 1732 |
+
" model_2_4b,\n",
|
| 1733 |
+
" train_loader,\n",
|
| 1734 |
+
" val_loader,\n",
|
| 1735 |
+
" device,\n",
|
| 1736 |
+
" epochs=100,\n",
|
| 1737 |
+
" lr=1e-3,\n",
|
| 1738 |
+
" patience=20\n",
|
| 1739 |
+
")\n",
|
| 1740 |
+
"scoreboard.add(result_2_4b)\n",
|
| 1741 |
+
""
|
| 1742 |
+
],
|
| 1743 |
+
"execution_count": null,
|
| 1744 |
+
"outputs": []
|
| 1745 |
+
},
|
| 1746 |
+
{
|
| 1747 |
+
"cell_type": "markdown",
|
| 1748 |
+
"metadata": {},
|
| 1749 |
+
"source": [
|
| 1750 |
+
"## Experiment 2.4c: vMF \u03ba Diagnostics"
|
| 1751 |
+
]
|
| 1752 |
+
},
|
| 1753 |
+
{
|
| 1754 |
+
"cell_type": "markdown",
|
| 1755 |
+
"metadata": {},
|
| 1756 |
+
"source": [
|
| 1757 |
+
"\n",
|
| 1758 |
+
"Track per-class \u03ba convergence over training.\n",
|
| 1759 |
+
"Monitor adaptive influence radius evolution.\n",
|
| 1760 |
+
""
|
| 1761 |
+
]
|
| 1762 |
+
},
|
| 1763 |
+
{
|
| 1764 |
+
"cell_type": "code",
|
| 1765 |
+
"metadata": {},
|
| 1766 |
+
"source": [
|
| 1767 |
+
"\n",
|
| 1768 |
+
"class vMFDiagnosticsModel(nn.Module):\n",
|
| 1769 |
+
" '''vMF with per-class concentration tracking.'''\n",
|
| 1770 |
+
" def __init__(self):\n",
|
| 1771 |
+
" super().__init__()\n",
|
| 1772 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1773 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1774 |
+
"\n",
|
| 1775 |
+
" # Per-class anchors and kappas\n",
|
| 1776 |
+
" self.num_classes = 10\n",
|
| 1777 |
+
" anchors = []\n",
|
| 1778 |
+
" kappas = []\n",
|
| 1779 |
+
" for c in range(self.num_classes):\n",
|
| 1780 |
+
" a = F.normalize(torch.randn(8, 64), dim=-1) # 8 anchors per class\n",
|
| 1781 |
+
" anchors.append(a)\n",
|
| 1782 |
+
" k = nn.Parameter(torch.ones(8) * 5.0)\n",
|
| 1783 |
+
" kappas.append(k)\n",
|
| 1784 |
+
"\n",
|
| 1785 |
+
" self.anchors = nn.ParameterList([nn.Parameter(a, requires_grad=False) for a in anchors])\n",
|
| 1786 |
+
" self.kappas = nn.ParameterList(kappas)\n",
|
| 1787 |
+
"\n",
|
| 1788 |
+
" self.patchwork = Patchwork(80, num_blocks=2)\n",
|
| 1789 |
+
" self.head = nn.Linear(80, 10)\n",
|
| 1790 |
+
"\n",
|
| 1791 |
+
" def forward(self, x):\n",
|
| 1792 |
+
" feat = self.encoder(x)\n",
|
| 1793 |
+
" emb = self.to_embedding(feat)\n",
|
| 1794 |
+
" emb = l2_normalize(emb)\n",
|
| 1795 |
+
"\n",
|
| 1796 |
+
" # Soft assignment per class\n",
|
| 1797 |
+
" all_probs = []\n",
|
| 1798 |
+
" for c in range(self.num_classes):\n",
|
| 1799 |
+
" cos_sim = torch.mm(emb, self.anchors[c].T) # (B, 8)\n",
|
| 1800 |
+
" log_prob = self.kappas[c].unsqueeze(0) * cos_sim\n",
|
| 1801 |
+
" probs = F.softmax(log_prob, dim=1)\n",
|
| 1802 |
+
" all_probs.append(probs)\n",
|
| 1803 |
+
"\n",
|
| 1804 |
+
" combined = torch.cat(all_probs, dim=1) # (B, 80)\n",
|
| 1805 |
+
" pw = self.patchwork(combined)\n",
|
| 1806 |
+
" logits = self.head(pw)\n",
|
| 1807 |
+
" return logits, emb\n",
|
| 1808 |
+
"\n",
|
| 1809 |
+
"print(\"\\n[EXP 2.4c] vMF \u03ba Diagnostics...\")\n",
|
| 1810 |
+
"model_2_4c = vMFDiagnosticsModel().to(device)\n",
|
| 1811 |
+
"\n",
|
| 1812 |
+
"# Custom training with \u03ba tracking\n",
|
| 1813 |
+
"writer_2_4c = make_writer(\"2.4c_vMF_kappa_diagnostic\")\n",
|
| 1814 |
+
"criterion = nn.CrossEntropyLoss()\n",
|
| 1815 |
+
"optimizer = torch.optim.AdamW(model_2_4c.parameters(), lr=1e-3, weight_decay=1e-4)\n",
|
| 1816 |
+
"\n",
|
| 1817 |
+
"for epoch in range(1, 51): # Shorter run for diagnostics\n",
|
| 1818 |
+
" model_2_4c.train()\n",
|
| 1819 |
+
" total_loss = 0\n",
|
| 1820 |
+
" correct = 0\n",
|
| 1821 |
+
" total = 0\n",
|
| 1822 |
+
"\n",
|
| 1823 |
+
" for images, labels in train_loader:\n",
|
| 1824 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 1825 |
+
" optimizer.zero_grad()\n",
|
| 1826 |
+
"\n",
|
| 1827 |
+
" logits, _ = model_2_4c(images)\n",
|
| 1828 |
+
" loss = criterion(logits, labels)\n",
|
| 1829 |
+
" loss.backward()\n",
|
| 1830 |
+
" torch.nn.utils.clip_grad_norm_(model_2_4c.parameters(), 1.0)\n",
|
| 1831 |
+
" optimizer.step()\n",
|
| 1832 |
+
"\n",
|
| 1833 |
+
" total_loss += loss.item()\n",
|
| 1834 |
+
" _, pred = logits.max(1)\n",
|
| 1835 |
+
" correct += pred.eq(labels).sum().item()\n",
|
| 1836 |
+
" total += labels.size(0)\n",
|
| 1837 |
+
"\n",
|
| 1838 |
+
" # Log \u03ba per class\n",
|
| 1839 |
+
" with torch.no_grad():\n",
|
| 1840 |
+
" for c in range(10):\n",
|
| 1841 |
+
" mean_kappa = model_2_4c.kappas[c].mean().item()\n",
|
| 1842 |
+
" max_kappa = model_2_4c.kappas[c].max().item()\n",
|
| 1843 |
+
" writer_2_4c.add_scalar(f\"kappa/class_{c}_mean\", mean_kappa, epoch)\n",
|
| 1844 |
+
" writer_2_4c.add_scalar(f\"kappa/class_{c}_max\", max_kappa, epoch)\n",
|
| 1845 |
+
"\n",
|
| 1846 |
+
" if epoch % 10 == 0:\n",
|
| 1847 |
+
" print(f\" E{epoch} | Loss {total_loss/len(train_loader):.4f} | Acc {100*correct/total:.1f}%\")\n",
|
| 1848 |
+
"\n",
|
| 1849 |
+
"writer_2_4c.close()\n",
|
| 1850 |
+
"print(\"[EXP 2.4c] Complete \u2014 \u03ba diagnostics logged to TensorBoard\")\n",
|
| 1851 |
+
"\n",
|
| 1852 |
+
"# Add a result entry\n",
|
| 1853 |
+
"result_2_4c = {\"experiment\": \"2.4c_vMF_kappa_diag\", \"best_val_acc\": 0, \"best_epoch\": 50, \"params\": 0, \"final_train_acc\": 0}\n",
|
| 1854 |
+
"scoreboard.add(result_2_4c)\n",
|
| 1855 |
+
""
|
| 1856 |
+
],
|
| 1857 |
+
"execution_count": null,
|
| 1858 |
+
"outputs": []
|
| 1859 |
+
},
|
| 1860 |
+
{
|
| 1861 |
+
"cell_type": "markdown",
|
| 1862 |
+
"metadata": {},
|
| 1863 |
+
"source": [
|
| 1864 |
+
"## Experiment 2.5a: E\u2088 Lattice Anchors"
|
| 1865 |
+
]
|
| 1866 |
+
},
|
| 1867 |
+
{
|
| 1868 |
+
"cell_type": "markdown",
|
| 1869 |
+
"metadata": {},
|
| 1870 |
+
"source": [
|
| 1871 |
+
"\n",
|
| 1872 |
+
"240 maximally separated points on S\u2077.\n",
|
| 1873 |
+
"Construct E\u2088 root system vectors.\n",
|
| 1874 |
+
"Superior packing efficiency over random.\n",
|
| 1875 |
+
""
|
| 1876 |
+
]
|
| 1877 |
+
},
|
| 1878 |
+
{
|
| 1879 |
+
"cell_type": "code",
|
| 1880 |
+
"metadata": {},
|
| 1881 |
+
"source": [
|
| 1882 |
+
"\n",
|
| 1883 |
+
"def construct_e8_roots():\n",
|
| 1884 |
+
" '''Construct E\u2088 root system (240 roots).'''\n",
|
| 1885 |
+
" roots = []\n",
|
| 1886 |
+
"\n",
|
| 1887 |
+
" # E\u2088 roots in 8D (simplified construction)\n",
|
| 1888 |
+
" # 1. Roots from roots of D\u2088 (\u00b11, \u00b11, 0, 0, 0, 0, 0, 0 permutations)\n",
|
| 1889 |
+
" for i in range(8):\n",
|
| 1890 |
+
" for j in range(i+1, 8):\n",
|
| 1891 |
+
" for si in [-1, 1]:\n",
|
| 1892 |
+
" for sj in [-1, 1]:\n",
|
| 1893 |
+
" root = [0]*8\n",
|
| 1894 |
+
" root[i] = si\n",
|
| 1895 |
+
" root[j] = sj\n",
|
| 1896 |
+
" roots.append(root)\n",
|
| 1897 |
+
"\n",
|
| 1898 |
+
" # 2. Roots from E\u2088 additional structure\n",
|
| 1899 |
+
" # All coordinates \u00b11 with even number of minus signs\n",
|
| 1900 |
+
" for signs in np.ndindex(2**8):\n",
|
| 1901 |
+
" vec = np.array([-1 if s else 1 for s in signs], dtype=float)\n",
|
| 1902 |
+
" if sum(vec < 0) % 2 == 0:\n",
|
| 1903 |
+
" roots.append(vec.tolist())\n",
|
| 1904 |
+
"\n",
|
| 1905 |
+
" roots = np.array(roots[:240], dtype=np.float32)\n",
|
| 1906 |
+
" roots = roots / (np.linalg.norm(roots, axis=1, keepdims=True) + 1e-8)\n",
|
| 1907 |
+
" return roots\n",
|
| 1908 |
+
"\n",
|
| 1909 |
+
"class E8ConstantsAnchorModel(nn.Module):\n",
|
| 1910 |
+
" def __init__(self):\n",
|
| 1911 |
+
" super().__init__()\n",
|
| 1912 |
+
" self.encoder = Scattering2DFeatures(out_dim=256)\n",
|
| 1913 |
+
" self.to_8d = nn.Linear(256, 8)\n",
|
| 1914 |
+
"\n",
|
| 1915 |
+
" # E\u2088 roots as anchors\n",
|
| 1916 |
+
" e8_roots = construct_e8_roots()\n",
|
| 1917 |
+
" self.register_buffer(\"e8_anchors\", torch.tensor(e8_roots, dtype=torch.float32))\n",
|
| 1918 |
+
"\n",
|
| 1919 |
+
" self.patchwork = Patchwork(240, num_blocks=3)\n",
|
| 1920 |
+
" self.head = nn.Linear(240, 10)\n",
|
| 1921 |
+
"\n",
|
| 1922 |
+
" def forward(self, x):\n",
|
| 1923 |
+
" feat = self.encoder(x)\n",
|
| 1924 |
+
" emb = self.to_8d(feat)\n",
|
| 1925 |
+
" emb = l2_normalize(emb)\n",
|
| 1926 |
+
"\n",
|
| 1927 |
+
" # Triangulation with E\u2088 anchors\n",
|
| 1928 |
+
" cos_sim = torch.mm(emb, self.e8_anchors.T)\n",
|
| 1929 |
+
" distances = 1.0 - cos_sim\n",
|
| 1930 |
+
"\n",
|
| 1931 |
+
" pw = self.patchwork(distances)\n",
|
| 1932 |
+
" logits = self.head(pw)\n",
|
| 1933 |
+
" return logits, emb\n",
|
| 1934 |
+
"\n",
|
| 1935 |
+
"print(\"\\n[EXP 2.5a] E\u2088 Lattice Anchors (240 roots)...\")\n",
|
| 1936 |
+
"model_2_5a = E8ConstantsAnchorModel().to(device)\n",
|
| 1937 |
+
"result_2_5a = run_experiment(\n",
|
| 1938 |
+
" \"2.5a_E8Lattice\",\n",
|
| 1939 |
+
" model_2_5a,\n",
|
| 1940 |
+
" train_loader,\n",
|
| 1941 |
+
" val_loader,\n",
|
| 1942 |
+
" device,\n",
|
| 1943 |
+
" epochs=100,\n",
|
| 1944 |
+
" lr=1e-3,\n",
|
| 1945 |
+
" patience=20\n",
|
| 1946 |
+
")\n",
|
| 1947 |
+
"scoreboard.add(result_2_5a)\n",
|
| 1948 |
+
""
|
| 1949 |
+
],
|
| 1950 |
+
"execution_count": null,
|
| 1951 |
+
"outputs": []
|
| 1952 |
+
},
|
| 1953 |
+
{
|
| 1954 |
+
"cell_type": "markdown",
|
| 1955 |
+
"metadata": {},
|
| 1956 |
+
"source": [
|
| 1957 |
+
"## Experiment 2.5b: Spherical t-Design Init"
|
| 1958 |
+
]
|
| 1959 |
+
},
|
| 1960 |
+
{
|
| 1961 |
+
"cell_type": "markdown",
|
| 1962 |
+
"metadata": {},
|
| 1963 |
+
"source": [
|
| 1964 |
+
"\n",
|
| 1965 |
+
"Compare vs uniform hypersphere initialization.\n",
|
| 1966 |
+
"t-designs have optimal polynomial approximation properties.\n",
|
| 1967 |
+
""
|
| 1968 |
+
]
|
| 1969 |
+
},
|
| 1970 |
+
{
|
| 1971 |
+
"cell_type": "code",
|
| 1972 |
+
"metadata": {},
|
| 1973 |
+
"source": [
|
| 1974 |
+
"\n",
|
| 1975 |
+
"def spherical_t_design_init(d, N, t=2):\n",
|
| 1976 |
+
" '''Generate spherical t-design points on S^{d-1}.'''\n",
|
| 1977 |
+
" # Simplified: use QR-based + repulsive refinement\n",
|
| 1978 |
+
" A = torch.randn(N, d)\n",
|
| 1979 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 1980 |
+
" points = Q.T[:N]\n",
|
| 1981 |
+
"\n",
|
| 1982 |
+
" if N > d:\n",
|
| 1983 |
+
" extra = F.normalize(torch.randn(N - d, d), dim=-1)\n",
|
| 1984 |
+
" points = torch.cat([points, extra], dim=0)\n",
|
| 1985 |
+
"\n",
|
| 1986 |
+
" return F.normalize(points, dim=-1)\n",
|
| 1987 |
+
"\n",
|
| 1988 |
+
"class TDesignModel(nn.Module):\n",
|
| 1989 |
+
" def __init__(self):\n",
|
| 1990 |
+
" super().__init__()\n",
|
| 1991 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 1992 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 1993 |
+
"\n",
|
| 1994 |
+
" # t-design anchors\n",
|
| 1995 |
+
" t_design = spherical_t_design_init(64, 64, t=2)\n",
|
| 1996 |
+
" self.register_buffer(\"t_design_anchors\", t_design)\n",
|
| 1997 |
+
"\n",
|
| 1998 |
+
" self.patchwork = Patchwork(64, num_blocks=2)\n",
|
| 1999 |
+
" self.head = nn.Linear(64, 10)\n",
|
| 2000 |
+
"\n",
|
| 2001 |
+
" def forward(self, x):\n",
|
| 2002 |
+
" feat = self.encoder(x)\n",
|
| 2003 |
+
" emb = self.to_embedding(feat)\n",
|
| 2004 |
+
" emb = l2_normalize(emb)\n",
|
| 2005 |
+
"\n",
|
| 2006 |
+
" cos_sim = torch.mm(emb, self.t_design_anchors.T)\n",
|
| 2007 |
+
" distances = 1.0 - cos_sim\n",
|
| 2008 |
+
"\n",
|
| 2009 |
+
" pw = self.patchwork(distances)\n",
|
| 2010 |
+
" logits = self.head(pw)\n",
|
| 2011 |
+
" return logits, emb\n",
|
| 2012 |
+
"\n",
|
| 2013 |
+
"# Compare: t-design vs uniform\n",
|
| 2014 |
+
"for init_type, desc in [(\"tdesign\", \"Spherical t-Design\"), (\"uniform\", \"Uniform Random\")]:\n",
|
| 2015 |
+
" if init_type == \"uniform\":\n",
|
| 2016 |
+
" model = TDesignModel()\n",
|
| 2017 |
+
" model.t_design_anchors.copy_(F.normalize(torch.randn(64, 64), dim=-1))\n",
|
| 2018 |
+
" else:\n",
|
| 2019 |
+
" model = TDesignModel()\n",
|
| 2020 |
+
"\n",
|
| 2021 |
+
" model = model.to(device)\n",
|
| 2022 |
+
" result = run_experiment(\n",
|
| 2023 |
+
" f\"2.5b_TDesign_{init_type}\",\n",
|
| 2024 |
+
" model,\n",
|
| 2025 |
+
" train_loader,\n",
|
| 2026 |
+
" val_loader,\n",
|
| 2027 |
+
" device,\n",
|
| 2028 |
+
" epochs=100,\n",
|
| 2029 |
+
" lr=1e-3,\n",
|
| 2030 |
+
" patience=20\n",
|
| 2031 |
+
" )\n",
|
| 2032 |
+
" scoreboard.add(result)\n",
|
| 2033 |
+
""
|
| 2034 |
+
],
|
| 2035 |
+
"execution_count": null,
|
| 2036 |
+
"outputs": []
|
| 2037 |
+
},
|
| 2038 |
+
{
|
| 2039 |
+
"cell_type": "markdown",
|
| 2040 |
+
"metadata": {},
|
| 2041 |
+
"source": [
|
| 2042 |
+
"## Experiment 2.5c: Thomson Problem Solver"
|
| 2043 |
+
]
|
| 2044 |
+
},
|
| 2045 |
+
{
|
| 2046 |
+
"cell_type": "markdown",
|
| 2047 |
+
"metadata": {},
|
| 2048 |
+
"source": [
|
| 2049 |
+
"\n",
|
| 2050 |
+
"Energy minimization for N anchors on S^d.\n",
|
| 2051 |
+
"Compare: Coulomb energy vs QR + iterative repulsion.\n",
|
| 2052 |
+
""
|
| 2053 |
+
]
|
| 2054 |
+
},
|
| 2055 |
+
{
|
| 2056 |
+
"cell_type": "code",
|
| 2057 |
+
"metadata": {},
|
| 2058 |
+
"source": [
|
| 2059 |
+
"\n",
|
| 2060 |
+
"def thomson_coulomb_energy(points):\n",
|
| 2061 |
+
" '''Compute Coulomb energy: sum of 1/||p_i - p_j|| for i != j.'''\n",
|
| 2062 |
+
" # points: (N, d)\n",
|
| 2063 |
+
" diff = points.unsqueeze(0) - points.unsqueeze(1) # (N, N, d)\n",
|
| 2064 |
+
" dist = torch.norm(diff, dim=2) # (N, N)\n",
|
| 2065 |
+
" dist = torch.where(dist > 0, dist, torch.ones_like(dist) * 1e10) # Avoid division by 0\n",
|
| 2066 |
+
" energy = (1.0 / dist).sum() / 2 # Divide by 2 to avoid double counting\n",
|
| 2067 |
+
" return energy\n",
|
| 2068 |
+
"\n",
|
| 2069 |
+
"def solve_thomson_problem(d=64, N=64, steps=50):\n",
|
| 2070 |
+
" '''Gradient descent to minimize Coulomb energy.'''\n",
|
| 2071 |
+
" points = F.normalize(torch.randn(N, d), dim=-1)\n",
|
| 2072 |
+
" points.requires_grad = True\n",
|
| 2073 |
+
" optimizer = torch.optim.Adam([points], lr=0.01)\n",
|
| 2074 |
+
"\n",
|
| 2075 |
+
" for step in range(steps):\n",
|
| 2076 |
+
" optimizer.zero_grad()\n",
|
| 2077 |
+
" points_norm = F.normalize(points, dim=-1)\n",
|
| 2078 |
+
" energy = thomson_coulomb_energy(points_norm)\n",
|
| 2079 |
+
" energy.backward()\n",
|
| 2080 |
+
" optimizer.step()\n",
|
| 2081 |
+
"\n",
|
| 2082 |
+
" if (step + 1) % 10 == 0:\n",
|
| 2083 |
+
" with torch.no_grad():\n",
|
| 2084 |
+
" points_norm = F.normalize(points, dim=-1)\n",
|
| 2085 |
+
" energy = thomson_coulomb_energy(points_norm)\n",
|
| 2086 |
+
" print(f\" Thomson step {step+1}/{steps}: E={energy:.4f}\")\n",
|
| 2087 |
+
"\n",
|
| 2088 |
+
" return F.normalize(points.detach(), dim=-1)\n",
|
| 2089 |
+
"\n",
|
| 2090 |
+
"class ThomsonModel(nn.Module):\n",
|
| 2091 |
+
" def __init__(self, use_thomson=True):\n",
|
| 2092 |
+
" super().__init__()\n",
|
| 2093 |
+
" self.encoder = Scattering2DFeatures(out_dim=128)\n",
|
| 2094 |
+
" self.to_embedding = nn.Linear(128, 64)\n",
|
| 2095 |
+
"\n",
|
| 2096 |
+
" if use_thomson:\n",
|
| 2097 |
+
" print(\" Solving Thomson problem...\")\n",
|
| 2098 |
+
" anchors = solve_thomson_problem(d=64, N=64, steps=50)\n",
|
| 2099 |
+
" else:\n",
|
| 2100 |
+
" anchors = F.normalize(torch.randn(64, 64), dim=-1)\n",
|
| 2101 |
+
"\n",
|
| 2102 |
+
" self.register_buffer(\"anchors\", anchors)\n",
|
| 2103 |
+
" self.patchwork = Patchwork(64, num_blocks=2)\n",
|
| 2104 |
+
" self.head = nn.Linear(64, 10)\n",
|
| 2105 |
+
"\n",
|
| 2106 |
+
" def forward(self, x):\n",
|
| 2107 |
+
" feat = self.encoder(x)\n",
|
| 2108 |
+
" emb = self.to_embedding(feat)\n",
|
| 2109 |
+
" emb = l2_normalize(emb)\n",
|
| 2110 |
+
"\n",
|
| 2111 |
+
" cos_sim = torch.mm(emb, self.anchors.T)\n",
|
| 2112 |
+
" distances = 1.0 - cos_sim\n",
|
| 2113 |
+
"\n",
|
| 2114 |
+
" pw = self.patchwork(distances)\n",
|
| 2115 |
+
" logits = self.head(pw)\n",
|
| 2116 |
+
" return logits, emb\n",
|
| 2117 |
+
"\n",
|
| 2118 |
+
"print(\"\\n[EXP 2.5c] Thomson Problem Solver...\")\n",
|
| 2119 |
+
"model_2_5c = ThomsonModel(use_thomson=True).to(device)\n",
|
| 2120 |
+
"result_2_5c = run_experiment(\n",
|
| 2121 |
+
" \"2.5c_Thomson\",\n",
|
| 2122 |
+
" model_2_5c,\n",
|
| 2123 |
+
" train_loader,\n",
|
| 2124 |
+
" val_loader,\n",
|
| 2125 |
+
" device,\n",
|
| 2126 |
+
" epochs=100,\n",
|
| 2127 |
+
" lr=1e-3,\n",
|
| 2128 |
+
" patience=20\n",
|
| 2129 |
+
")\n",
|
| 2130 |
+
"scoreboard.add(result_2_5c)\n",
|
| 2131 |
+
""
|
| 2132 |
+
],
|
| 2133 |
+
"execution_count": null,
|
| 2134 |
+
"outputs": []
|
| 2135 |
+
},
|
| 2136 |
+
{
|
| 2137 |
+
"cell_type": "markdown",
|
| 2138 |
+
"metadata": {},
|
| 2139 |
+
"source": [
|
| 2140 |
+
"## Final Scoreboard \u2014 All Experiments"
|
| 2141 |
+
]
|
| 2142 |
+
},
|
| 2143 |
+
{
|
| 2144 |
+
"cell_type": "code",
|
| 2145 |
+
"metadata": {},
|
| 2146 |
+
"source": [
|
| 2147 |
+
"\n",
|
| 2148 |
+
"print(\"\\n\" + \"=\"*80)\n",
|
| 2149 |
+
"print(\"FINAL SCOREBOARD \u2014 CATEGORY 2 MANIFOLD STRUCTURES\")\n",
|
| 2150 |
+
"print(\"=\"*80)\n",
|
| 2151 |
+
"scoreboard._print_table()\n",
|
| 2152 |
+
"\n",
|
| 2153 |
+
"# Save to file\n",
|
| 2154 |
+
"scoreboard.save(\"/sessions/optimistic-wizardly-dijkstra/mnt/outputs/experiment_2_scoreboard.json\")\n",
|
| 2155 |
+
"\n",
|
| 2156 |
+
"print(\"\\n[COMPLETE] All experiments completed!\")\n",
|
| 2157 |
+
"print(\"[LOGS] Check TensorBoard with: %tensorboard --logdir=runs\")\n",
|
| 2158 |
+
""
|
| 2159 |
+
],
|
| 2160 |
+
"execution_count": null,
|
| 2161 |
+
"outputs": []
|
| 2162 |
+
}
|
| 2163 |
+
]
|
| 2164 |
+
}
|
spectral/experiment_3_compact_representations.ipynb
ADDED
|
@@ -0,0 +1,1547 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 5,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"kernelspec": {
|
| 6 |
+
"display_name": "Python 3",
|
| 7 |
+
"language": "python",
|
| 8 |
+
"name": "python3"
|
| 9 |
+
},
|
| 10 |
+
"language_info": {
|
| 11 |
+
"name": "python",
|
| 12 |
+
"version": "3.10.0"
|
| 13 |
+
},
|
| 14 |
+
"accelerator": "GPU",
|
| 15 |
+
"colab": {
|
| 16 |
+
"provenance": [],
|
| 17 |
+
"gpuType": "T4"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"cells": [
|
| 21 |
+
{
|
| 22 |
+
"cell_type": "markdown",
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"source": [
|
| 25 |
+
"\n",
|
| 26 |
+
"# GeoLIP Hypersphere Experiments \u2014 Category 3: Compact Representations\n",
|
| 27 |
+
"\n",
|
| 28 |
+
"## Overview\n",
|
| 29 |
+
"We investigate **compact, low-dimensional representations** on hyperspheres.\n",
|
| 30 |
+
"Techniques:\n",
|
| 31 |
+
"- **Random Fourier Features (RFF)** \u2014 explicit kernel approximations\n",
|
| 32 |
+
"- **Johnson-Lindenstrauss (JL)** \u2014 near-isometric random projections\n",
|
| 33 |
+
"- **Compressed Sensing (CS)** \u2014 sparse measurement matrices\n",
|
| 34 |
+
"- **Fourier Positional Encoding** (Tancik/Mildenhall style)\n",
|
| 35 |
+
"- **Spherical Harmonics** \u2014 angular frequency decomposition on S^d\n",
|
| 36 |
+
"\n",
|
| 37 |
+
"Each maps to **Constellation** (K anchors) \u2192 **Triangulation Profile** (K-dim distances).\n",
|
| 38 |
+
"\n",
|
| 39 |
+
"**Hypothesis:** Careful projection design + hypersphere structure \u2192\n",
|
| 40 |
+
"near-perfect classification with 64-128d embeddings.\n",
|
| 41 |
+
""
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"cell_type": "code",
|
| 46 |
+
"metadata": {},
|
| 47 |
+
"source": [
|
| 48 |
+
"\n",
|
| 49 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 50 |
+
"# SHARED UTILITIES \u2014 GeoLIP Hypersphere Experiments\n",
|
| 51 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 52 |
+
"import os, sys, time, math, json, datetime, warnings\n",
|
| 53 |
+
"import numpy as np\n",
|
| 54 |
+
"import torch\n",
|
| 55 |
+
"import torch.nn as nn\n",
|
| 56 |
+
"import torch.nn.functional as F\n",
|
| 57 |
+
"from torch.utils.data import DataLoader\n",
|
| 58 |
+
"from torch.utils.tensorboard import SummaryWriter\n",
|
| 59 |
+
"import torchvision\n",
|
| 60 |
+
"import torchvision.transforms as T\n",
|
| 61 |
+
"import matplotlib\n",
|
| 62 |
+
"matplotlib.use(\"Agg\")\n",
|
| 63 |
+
"import matplotlib.pyplot as plt\n",
|
| 64 |
+
"from collections import defaultdict\n",
|
| 65 |
+
"\n",
|
| 66 |
+
"warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
|
| 67 |
+
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 68 |
+
"print(f\"[DEVICE] {device}\" + (f\" \u2014 {torch.cuda.get_device_name()}\" if device.type == \"cuda\" else \"\"))\n",
|
| 69 |
+
"\n",
|
| 70 |
+
"# \u2500\u2500 CIFAR-10 Loaders \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 71 |
+
"CIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\n",
|
| 72 |
+
"CIFAR10_STD = (0.2470, 0.2435, 0.2616)\n",
|
| 73 |
+
"CIFAR10_CLASSES = (\"plane\",\"car\",\"bird\",\"cat\",\"deer\",\"dog\",\"frog\",\"horse\",\"ship\",\"truck\")\n",
|
| 74 |
+
"\n",
|
| 75 |
+
"def get_cifar10_loaders(batch_size=256, num_workers=2, augment=True):\n",
|
| 76 |
+
" \"\"\"Return train_loader, val_loader for CIFAR-10.\"\"\"\n",
|
| 77 |
+
" tf_train = T.Compose([\n",
|
| 78 |
+
" T.RandomCrop(32, padding=4),\n",
|
| 79 |
+
" T.RandomHorizontalFlip(),\n",
|
| 80 |
+
" T.ToTensor(),\n",
|
| 81 |
+
" T.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n",
|
| 82 |
+
" ]) if augment else T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 83 |
+
" tf_val = T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 84 |
+
"\n",
|
| 85 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf_train)\n",
|
| 86 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf_val)\n",
|
| 87 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 88 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 89 |
+
" print(f\"[DATA] CIFAR-10: {len(train_ds)} train, {len(val_ds)} val, batch_size={batch_size}\")\n",
|
| 90 |
+
" return train_loader, val_loader\n",
|
| 91 |
+
"\n",
|
| 92 |
+
"def get_cifar10_raw_loaders(batch_size=256, num_workers=2):\n",
|
| 93 |
+
" \"\"\"Return loaders with only ToTensor (no normalization) for custom front-ends.\"\"\"\n",
|
| 94 |
+
" tf = T.Compose([T.ToTensor()])\n",
|
| 95 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf)\n",
|
| 96 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf)\n",
|
| 97 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 98 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 99 |
+
" print(f\"[DATA] CIFAR-10 (raw): {len(train_ds)} train, {len(val_ds)} val\")\n",
|
| 100 |
+
" return train_loader, val_loader\n",
|
| 101 |
+
"\n",
|
| 102 |
+
"# \u2500\u2500 L2 Normalization to Hypersphere \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 103 |
+
"def l2_normalize(x, dim=-1, eps=1e-8):\n",
|
| 104 |
+
" \"\"\"Project to unit hypersphere S^{d-1}.\"\"\"\n",
|
| 105 |
+
" return F.normalize(x, p=2, dim=dim, eps=eps)\n",
|
| 106 |
+
"\n",
|
| 107 |
+
"# \u2500\u2500 Constellation Module \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 108 |
+
"class Constellation(nn.Module):\n",
|
| 109 |
+
" \"\"\"\n",
|
| 110 |
+
" K anchors on S^{d-1}. Triangulation: compute distance from input to each anchor.\n",
|
| 111 |
+
" Output: K-dim vector of (1 - cos_sim) distances = triangulation profile.\n",
|
| 112 |
+
" \"\"\"\n",
|
| 113 |
+
" def __init__(self, embed_dim, num_anchors=64, init=\"uniform\"):\n",
|
| 114 |
+
" super().__init__()\n",
|
| 115 |
+
" self.embed_dim = embed_dim\n",
|
| 116 |
+
" self.num_anchors = num_anchors\n",
|
| 117 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, init)\n",
|
| 118 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False) # anchors are NOT learned\n",
|
| 119 |
+
"\n",
|
| 120 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 121 |
+
" if method == \"uniform\":\n",
|
| 122 |
+
" # QR-based uniform hypersphere initialization\n",
|
| 123 |
+
" A = torch.randn(K, d)\n",
|
| 124 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 125 |
+
" anchors = Q.T[:K]\n",
|
| 126 |
+
" # If K > d, fill remaining with random unit vectors\n",
|
| 127 |
+
" if K > d:\n",
|
| 128 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 129 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 130 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 131 |
+
" elif method == \"random\":\n",
|
| 132 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 133 |
+
" else:\n",
|
| 134 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 135 |
+
"\n",
|
| 136 |
+
" def forward(self, x):\n",
|
| 137 |
+
" \"\"\"x: (B, d) on S^{d-1} \u2192 (B, K) triangulation distances.\"\"\"\n",
|
| 138 |
+
" x_norm = l2_normalize(x)\n",
|
| 139 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T) # (B, K)\n",
|
| 140 |
+
" distances = 1.0 - cos_sim # in [0, 2]\n",
|
| 141 |
+
" return distances\n",
|
| 142 |
+
"\n",
|
| 143 |
+
" def push_anchors(self, embeddings, labels, num_classes=10, momentum=0.1):\n",
|
| 144 |
+
" \"\"\"Push anchors toward class centroids (call periodically).\"\"\"\n",
|
| 145 |
+
" with torch.no_grad():\n",
|
| 146 |
+
" for c in range(num_classes):\n",
|
| 147 |
+
" mask = (labels == c)\n",
|
| 148 |
+
" if mask.sum() < 2:\n",
|
| 149 |
+
" continue\n",
|
| 150 |
+
" centroid = l2_normalize(embeddings[mask].mean(dim=0, keepdim=True))\n",
|
| 151 |
+
" # Assign anchors to this class based on nearest centroid\n",
|
| 152 |
+
" start = c * (self.num_anchors // num_classes)\n",
|
| 153 |
+
" end = start + (self.num_anchors // num_classes)\n",
|
| 154 |
+
" self.anchors.data[start:end] = l2_normalize(\n",
|
| 155 |
+
" (1 - momentum) * self.anchors.data[start:end] + momentum * centroid\n",
|
| 156 |
+
" )\n",
|
| 157 |
+
"\n",
|
| 158 |
+
" def anchor_activation(self, x):\n",
|
| 159 |
+
" \"\"\"Return fraction of anchors that are 'active' (nearest to at least one sample).\"\"\"\n",
|
| 160 |
+
" x_norm = l2_normalize(x)\n",
|
| 161 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 162 |
+
" nearest = cos_sim.argmax(dim=1)\n",
|
| 163 |
+
" active = len(nearest.unique())\n",
|
| 164 |
+
" return active, self.num_anchors\n",
|
| 165 |
+
"\n",
|
| 166 |
+
"# \u2500\u2500 Patchwork Block \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 167 |
+
"class SquaredReLU(nn.Module):\n",
|
| 168 |
+
" def forward(self, x):\n",
|
| 169 |
+
" return F.relu(x) ** 2\n",
|
| 170 |
+
"\n",
|
| 171 |
+
"class PatchworkBlock(nn.Module):\n",
|
| 172 |
+
" \"\"\"SquaredReLU + LayerNorm block for post-constellation processing.\"\"\"\n",
|
| 173 |
+
" def __init__(self, dim):\n",
|
| 174 |
+
" super().__init__()\n",
|
| 175 |
+
" self.net = nn.Sequential(\n",
|
| 176 |
+
" nn.Linear(dim, dim),\n",
|
| 177 |
+
" SquaredReLU(),\n",
|
| 178 |
+
" nn.LayerNorm(dim),\n",
|
| 179 |
+
" nn.Linear(dim, dim),\n",
|
| 180 |
+
" SquaredReLU(),\n",
|
| 181 |
+
" nn.LayerNorm(dim),\n",
|
| 182 |
+
" )\n",
|
| 183 |
+
" def forward(self, x):\n",
|
| 184 |
+
" return x + self.net(x) # residual\n",
|
| 185 |
+
"\n",
|
| 186 |
+
"class Patchwork(nn.Module):\n",
|
| 187 |
+
" \"\"\"Stack of PatchworkBlocks.\"\"\"\n",
|
| 188 |
+
" def __init__(self, dim, num_blocks=2):\n",
|
| 189 |
+
" super().__init__()\n",
|
| 190 |
+
" self.blocks = nn.Sequential(*[PatchworkBlock(dim) for _ in range(num_blocks)])\n",
|
| 191 |
+
" def forward(self, x):\n",
|
| 192 |
+
" return self.blocks(x)\n",
|
| 193 |
+
"\n",
|
| 194 |
+
"# \u2500\u2500 Classifier Head \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 195 |
+
"class ConstellationClassifier(nn.Module):\n",
|
| 196 |
+
" \"\"\"Full pipeline: encoder output \u2192 constellation \u2192 patchwork \u2192 classify.\"\"\"\n",
|
| 197 |
+
" def __init__(self, embed_dim, num_anchors=64, num_classes=10, patchwork_blocks=2):\n",
|
| 198 |
+
" super().__init__()\n",
|
| 199 |
+
" self.constellation = Constellation(embed_dim, num_anchors)\n",
|
| 200 |
+
" self.patchwork = Patchwork(num_anchors, patchwork_blocks)\n",
|
| 201 |
+
" self.head = nn.Linear(num_anchors, num_classes)\n",
|
| 202 |
+
"\n",
|
| 203 |
+
" def forward(self, embeddings):\n",
|
| 204 |
+
" tri = self.constellation(embeddings) # (B, K)\n",
|
| 205 |
+
" pw = self.patchwork(tri) # (B, K)\n",
|
| 206 |
+
" logits = self.head(pw) # (B, 10)\n",
|
| 207 |
+
" return logits, tri\n",
|
| 208 |
+
"\n",
|
| 209 |
+
" def count_params(self):\n",
|
| 210 |
+
" return sum(p.numel() for p in self.parameters() if p.requires_grad)\n",
|
| 211 |
+
"\n",
|
| 212 |
+
"# \u2500\u2500 Losses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 213 |
+
"class CayleyMengerVolumeLoss(nn.Module):\n",
|
| 214 |
+
" \"\"\"CV loss: encourage constellation volume in 0.18-0.25 band.\"\"\"\n",
|
| 215 |
+
" def __init__(self, target_low=0.18, target_high=0.25):\n",
|
| 216 |
+
" super().__init__()\n",
|
| 217 |
+
" self.target_low = target_low\n",
|
| 218 |
+
" self.target_high = target_high\n",
|
| 219 |
+
"\n",
|
| 220 |
+
" def forward(self, tri):\n",
|
| 221 |
+
" \"\"\"tri: (B, K) triangulation distances.\"\"\"\n",
|
| 222 |
+
" # Approximate volume via determinant of distance submatrix\n",
|
| 223 |
+
" B, K = tri.shape\n",
|
| 224 |
+
" # Use std of triangulation as proxy for volume spread\n",
|
| 225 |
+
" vol = tri.std(dim=1).mean()\n",
|
| 226 |
+
" # Penalty if outside target band\n",
|
| 227 |
+
" below = F.relu(self.target_low - vol)\n",
|
| 228 |
+
" above = F.relu(vol - self.target_high)\n",
|
| 229 |
+
" return below + above, vol.item()\n",
|
| 230 |
+
"\n",
|
| 231 |
+
"class InfoNCELoss(nn.Module):\n",
|
| 232 |
+
" \"\"\"Self-supervised contrastive loss on augmented views.\"\"\"\n",
|
| 233 |
+
" def __init__(self, temperature=0.07):\n",
|
| 234 |
+
" super().__init__()\n",
|
| 235 |
+
" self.temperature = temperature\n",
|
| 236 |
+
"\n",
|
| 237 |
+
" def forward(self, z1, z2):\n",
|
| 238 |
+
" \"\"\"z1, z2: (B, d) L2-normalized embeddings of two augmented views.\"\"\"\n",
|
| 239 |
+
" z1 = l2_normalize(z1)\n",
|
| 240 |
+
" z2 = l2_normalize(z2)\n",
|
| 241 |
+
" B = z1.shape[0]\n",
|
| 242 |
+
" z = torch.cat([z1, z2], dim=0) # (2B, d)\n",
|
| 243 |
+
" sim = torch.mm(z, z.T) / self.temperature # (2B, 2B)\n",
|
| 244 |
+
" # Mask out self-similarity\n",
|
| 245 |
+
" mask = ~torch.eye(2*B, dtype=bool, device=z.device)\n",
|
| 246 |
+
" sim = sim.masked_fill(~mask, -1e9)\n",
|
| 247 |
+
" # Positive pairs: (i, i+B) and (i+B, i)\n",
|
| 248 |
+
" labels = torch.cat([torch.arange(B, 2*B), torch.arange(B)]).to(z.device)\n",
|
| 249 |
+
" loss = F.cross_entropy(sim, labels)\n",
|
| 250 |
+
" return loss\n",
|
| 251 |
+
"\n",
|
| 252 |
+
"class ClassConditionalInfoNCE(nn.Module):\n",
|
| 253 |
+
" \"\"\"Class-conditional contrastive: positive = same class, not same image.\"\"\"\n",
|
| 254 |
+
" def __init__(self, temperature=0.07):\n",
|
| 255 |
+
" super().__init__()\n",
|
| 256 |
+
" self.temperature = temperature\n",
|
| 257 |
+
"\n",
|
| 258 |
+
" def forward(self, z, labels):\n",
|
| 259 |
+
" z = l2_normalize(z)\n",
|
| 260 |
+
" B = z.shape[0]\n",
|
| 261 |
+
" sim = torch.mm(z, z.T) / self.temperature\n",
|
| 262 |
+
" # Mask: positive if same class, negative otherwise\n",
|
| 263 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 264 |
+
" label_mask.fill_diagonal_(0) # exclude self\n",
|
| 265 |
+
" # For each sample, compute log-softmax over all non-self, weighted by positives\n",
|
| 266 |
+
" logits = sim - torch.diag(torch.full((B,), 1e9, device=z.device))\n",
|
| 267 |
+
" pos_mask = label_mask\n",
|
| 268 |
+
" neg_mask = 1 - label_mask\n",
|
| 269 |
+
" neg_mask.fill_diagonal_(0)\n",
|
| 270 |
+
" # InfoNCE: -log(sum_pos exp(sim) / sum_all exp(sim))\n",
|
| 271 |
+
" exp_sim = torch.exp(logits)\n",
|
| 272 |
+
" pos_sum = (exp_sim * pos_mask).sum(dim=1)\n",
|
| 273 |
+
" all_sum = (exp_sim * (pos_mask + neg_mask)).sum(dim=1)\n",
|
| 274 |
+
" loss = -torch.log(pos_sum / (all_sum + 1e-8) + 1e-8).mean()\n",
|
| 275 |
+
" return loss\n",
|
| 276 |
+
"\n",
|
| 277 |
+
"# \u2500\u2500 TensorBoard Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 278 |
+
"def make_writer(experiment_id, base_dir=\"runs\"):\n",
|
| 279 |
+
" \"\"\"Create a TensorBoard SummaryWriter with timestamped run directory.\"\"\"\n",
|
| 280 |
+
" timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
|
| 281 |
+
" log_dir = os.path.join(base_dir, f\"{experiment_id}_{timestamp}\")\n",
|
| 282 |
+
" writer = SummaryWriter(log_dir=log_dir)\n",
|
| 283 |
+
" print(f\"[TB] Logging to {log_dir}\")\n",
|
| 284 |
+
" return writer\n",
|
| 285 |
+
"\n",
|
| 286 |
+
"def log_epoch_metrics(writer, epoch, metrics_dict, prefix=\"\"):\n",
|
| 287 |
+
" \"\"\"Log a dict of metrics to TensorBoard.\"\"\"\n",
|
| 288 |
+
" for key, val in metrics_dict.items():\n",
|
| 289 |
+
" tag = f\"{prefix}/{key}\" if prefix else key\n",
|
| 290 |
+
" writer.add_scalar(tag, val, epoch)\n",
|
| 291 |
+
"\n",
|
| 292 |
+
"def log_embedding_viz(writer, embeddings, labels, epoch, tag=\"embeddings\"):\n",
|
| 293 |
+
" \"\"\"Log embeddings for TensorBoard projector.\"\"\"\n",
|
| 294 |
+
" meta = [CIFAR10_CLASSES[l] for l in labels]\n",
|
| 295 |
+
" writer.add_embedding(embeddings, metadata=meta, global_step=epoch, tag=tag)\n",
|
| 296 |
+
"\n",
|
| 297 |
+
"def log_anchor_histogram(writer, constellation, epoch):\n",
|
| 298 |
+
" \"\"\"Log anchor norm distribution.\"\"\"\n",
|
| 299 |
+
" norms = constellation.anchors.data.norm(dim=1)\n",
|
| 300 |
+
" writer.add_histogram(\"anchors/norms\", norms, epoch)\n",
|
| 301 |
+
" cos_sim_matrix = torch.mm(constellation.anchors.data, constellation.anchors.data.T)\n",
|
| 302 |
+
" off_diag = cos_sim_matrix[~torch.eye(cos_sim_matrix.shape[0], dtype=bool)].flatten()\n",
|
| 303 |
+
" writer.add_histogram(\"anchors/inter_cosine_sim\", off_diag, epoch)\n",
|
| 304 |
+
"\n",
|
| 305 |
+
"def log_confusion_matrix(writer, all_preds, all_labels, epoch):\n",
|
| 306 |
+
" \"\"\"Log confusion matrix as image to TensorBoard.\"\"\"\n",
|
| 307 |
+
" from sklearn.metrics import confusion_matrix\n",
|
| 308 |
+
" cm = confusion_matrix(all_labels, all_preds, labels=range(10))\n",
|
| 309 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 310 |
+
" im = ax.imshow(cm, interpolation=\"nearest\", cmap=plt.cm.Blues)\n",
|
| 311 |
+
" ax.set_title(f\"Confusion Matrix \u2014 Epoch {epoch}\")\n",
|
| 312 |
+
" ax.set_xlabel(\"Predicted\")\n",
|
| 313 |
+
" ax.set_ylabel(\"True\")\n",
|
| 314 |
+
" tick_marks = range(10)\n",
|
| 315 |
+
" ax.set_xticks(tick_marks)\n",
|
| 316 |
+
" ax.set_xticklabels(CIFAR10_CLASSES, rotation=45, ha=\"right\")\n",
|
| 317 |
+
" ax.set_yticks(tick_marks)\n",
|
| 318 |
+
" ax.set_yticklabels(CIFAR10_CLASSES)\n",
|
| 319 |
+
" for i in range(10):\n",
|
| 320 |
+
" for j in range(10):\n",
|
| 321 |
+
" ax.text(j, i, str(cm[i, j]), ha=\"center\", va=\"center\",\n",
|
| 322 |
+
" color=\"white\" if cm[i, j] > cm.max() / 2 else \"black\", fontsize=7)\n",
|
| 323 |
+
" fig.tight_layout()\n",
|
| 324 |
+
" writer.add_figure(\"confusion_matrix\", fig, epoch)\n",
|
| 325 |
+
" plt.close(fig)\n",
|
| 326 |
+
"\n",
|
| 327 |
+
"def log_triangulation_stats(writer, tri, epoch):\n",
|
| 328 |
+
" \"\"\"Log triangulation distance statistics.\"\"\"\n",
|
| 329 |
+
" writer.add_scalar(\"tri/mean\", tri.mean().item(), epoch)\n",
|
| 330 |
+
" writer.add_scalar(\"tri/std\", tri.std().item(), epoch)\n",
|
| 331 |
+
" writer.add_scalar(\"tri/min\", tri.min().item(), epoch)\n",
|
| 332 |
+
" writer.add_scalar(\"tri/max\", tri.max().item(), epoch)\n",
|
| 333 |
+
" writer.add_histogram(\"tri/distances\", tri.detach().cpu(), epoch)\n",
|
| 334 |
+
"\n",
|
| 335 |
+
"def log_scatter_2d(writer, embeddings, labels, epoch, tag=\"scatter_2d\", method=\"pca\"):\n",
|
| 336 |
+
" \"\"\"Log 2D scatter plot of embeddings projected via PCA or t-SNE.\"\"\"\n",
|
| 337 |
+
" emb_np = embeddings.detach().cpu().numpy()\n",
|
| 338 |
+
" lab_np = labels.detach().cpu().numpy() if torch.is_tensor(labels) else np.array(labels)\n",
|
| 339 |
+
" if method == \"pca\":\n",
|
| 340 |
+
" from sklearn.decomposition import PCA\n",
|
| 341 |
+
" proj = PCA(n_components=2).fit_transform(emb_np)\n",
|
| 342 |
+
" else:\n",
|
| 343 |
+
" from sklearn.manifold import TSNE\n",
|
| 344 |
+
" proj = TSNE(n_components=2, perplexity=30, n_iter=500).fit_transform(emb_np)\n",
|
| 345 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 346 |
+
" for c in range(10):\n",
|
| 347 |
+
" mask = lab_np == c\n",
|
| 348 |
+
" ax.scatter(proj[mask, 0], proj[mask, 1], s=3, alpha=0.5, label=CIFAR10_CLASSES[c])\n",
|
| 349 |
+
" ax.legend(markerscale=5, fontsize=8)\n",
|
| 350 |
+
" ax.set_title(f\"{tag} ({method.upper()}) \u2014 Epoch {epoch}\")\n",
|
| 351 |
+
" fig.tight_layout()\n",
|
| 352 |
+
" writer.add_figure(f\"{tag}/{method}\", fig, epoch)\n",
|
| 353 |
+
" plt.close(fig)\n",
|
| 354 |
+
"\n",
|
| 355 |
+
"# \u2500\u2500 Training Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 356 |
+
"def train_one_epoch(model, loader, optimizer, criterion, device, epoch=0,\n",
|
| 357 |
+
" scheduler=None, extra_losses=None):\n",
|
| 358 |
+
" \"\"\"\n",
|
| 359 |
+
" Generic training loop for one epoch.\n",
|
| 360 |
+
" model: callable(batch) \u2192 (logits, embeddings_on_sphere)\n",
|
| 361 |
+
" extra_losses: list of (name, loss_fn, weight) where loss_fn(embeddings) \u2192 scalar\n",
|
| 362 |
+
" \"\"\"\n",
|
| 363 |
+
" model.train()\n",
|
| 364 |
+
" total_loss = 0\n",
|
| 365 |
+
" total_ce = 0\n",
|
| 366 |
+
" correct = 0\n",
|
| 367 |
+
" total = 0\n",
|
| 368 |
+
" loss_components = defaultdict(float)\n",
|
| 369 |
+
"\n",
|
| 370 |
+
" for batch_idx, (images, labels) in enumerate(loader):\n",
|
| 371 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 372 |
+
" optimizer.zero_grad()\n",
|
| 373 |
+
"\n",
|
| 374 |
+
" logits, embeddings = model(images)\n",
|
| 375 |
+
" ce_loss = criterion(logits, labels)\n",
|
| 376 |
+
" loss = ce_loss\n",
|
| 377 |
+
" loss_components[\"CE\"] += ce_loss.item()\n",
|
| 378 |
+
"\n",
|
| 379 |
+
" if extra_losses:\n",
|
| 380 |
+
" for name, loss_fn, weight in extra_losses:\n",
|
| 381 |
+
" if name == \"CV\":\n",
|
| 382 |
+
" el, vol_val = loss_fn(embeddings)\n",
|
| 383 |
+
" loss_components[\"CV_vol\"] += vol_val\n",
|
| 384 |
+
" elif name == \"InfoNCE_class\":\n",
|
| 385 |
+
" el = loss_fn(embeddings, labels)\n",
|
| 386 |
+
" else:\n",
|
| 387 |
+
" el = loss_fn(embeddings)\n",
|
| 388 |
+
" loss = loss + weight * el\n",
|
| 389 |
+
" loss_components[name] += el.item()\n",
|
| 390 |
+
"\n",
|
| 391 |
+
" loss.backward()\n",
|
| 392 |
+
" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n",
|
| 393 |
+
" optimizer.step()\n",
|
| 394 |
+
" if scheduler:\n",
|
| 395 |
+
" scheduler.step()\n",
|
| 396 |
+
"\n",
|
| 397 |
+
" total_loss += loss.item()\n",
|
| 398 |
+
" _, predicted = logits.max(1)\n",
|
| 399 |
+
" total += labels.size(0)\n",
|
| 400 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 401 |
+
"\n",
|
| 402 |
+
" n_batches = len(loader)\n",
|
| 403 |
+
" metrics = {\n",
|
| 404 |
+
" \"loss/total\": total_loss / n_batches,\n",
|
| 405 |
+
" \"loss/CE\": loss_components[\"CE\"] / n_batches,\n",
|
| 406 |
+
" \"accuracy/train\": 100.0 * correct / total,\n",
|
| 407 |
+
" }\n",
|
| 408 |
+
" for k, v in loss_components.items():\n",
|
| 409 |
+
" if k not in (\"CE\",):\n",
|
| 410 |
+
" metrics[f\"loss/{k}\"] = v / n_batches\n",
|
| 411 |
+
" return metrics\n",
|
| 412 |
+
"\n",
|
| 413 |
+
"@torch.no_grad()\n",
|
| 414 |
+
"def evaluate(model, loader, criterion, device, collect_embeddings=False):\n",
|
| 415 |
+
" \"\"\"Evaluate model on validation set. Optionally collect embeddings.\"\"\"\n",
|
| 416 |
+
" model.eval()\n",
|
| 417 |
+
" total_loss = 0\n",
|
| 418 |
+
" correct = 0\n",
|
| 419 |
+
" total = 0\n",
|
| 420 |
+
" all_preds = []\n",
|
| 421 |
+
" all_labels = []\n",
|
| 422 |
+
" all_embeddings = []\n",
|
| 423 |
+
"\n",
|
| 424 |
+
" for images, labels in loader:\n",
|
| 425 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 426 |
+
" logits, embeddings = model(images)\n",
|
| 427 |
+
" loss = criterion(logits, labels)\n",
|
| 428 |
+
" total_loss += loss.item()\n",
|
| 429 |
+
" _, predicted = logits.max(1)\n",
|
| 430 |
+
" total += labels.size(0)\n",
|
| 431 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 432 |
+
" all_preds.extend(predicted.cpu().numpy())\n",
|
| 433 |
+
" all_labels.extend(labels.cpu().numpy())\n",
|
| 434 |
+
" if collect_embeddings:\n",
|
| 435 |
+
" all_embeddings.append(embeddings.cpu())\n",
|
| 436 |
+
"\n",
|
| 437 |
+
" n_batches = len(loader)\n",
|
| 438 |
+
" metrics = {\n",
|
| 439 |
+
" \"loss/val\": total_loss / n_batches,\n",
|
| 440 |
+
" \"accuracy/val\": 100.0 * correct / total,\n",
|
| 441 |
+
" }\n",
|
| 442 |
+
" result = {\"metrics\": metrics, \"preds\": np.array(all_preds), \"labels\": np.array(all_labels)}\n",
|
| 443 |
+
" if collect_embeddings:\n",
|
| 444 |
+
" result[\"embeddings\"] = torch.cat(all_embeddings, dim=0)\n",
|
| 445 |
+
" return result\n",
|
| 446 |
+
"\n",
|
| 447 |
+
"def run_experiment(experiment_id, model, train_loader, val_loader, device,\n",
|
| 448 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, extra_losses=None,\n",
|
| 449 |
+
" push_anchors_every=50, log_embeddings_every=25,\n",
|
| 450 |
+
" patience=20):\n",
|
| 451 |
+
" \"\"\"\n",
|
| 452 |
+
" Full experiment runner with TensorBoard logging, early stopping, and diagnostics.\n",
|
| 453 |
+
" model must return (logits, sphere_embeddings).\n",
|
| 454 |
+
" \"\"\"\n",
|
| 455 |
+
" writer = make_writer(experiment_id)\n",
|
| 456 |
+
" criterion = nn.CrossEntropyLoss()\n",
|
| 457 |
+
" optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)\n",
|
| 458 |
+
" scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\n",
|
| 459 |
+
"\n",
|
| 460 |
+
" best_val_acc = 0.0\n",
|
| 461 |
+
" best_epoch = 0\n",
|
| 462 |
+
" no_improve = 0\n",
|
| 463 |
+
"\n",
|
| 464 |
+
" # Log model info\n",
|
| 465 |
+
" n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 466 |
+
" writer.add_text(\"config/params\", f\"Learned params: {n_params:,}\")\n",
|
| 467 |
+
" writer.add_text(\"config/experiment\", experiment_id)\n",
|
| 468 |
+
" print(f\"\\n{'='*70}\")\n",
|
| 469 |
+
" print(f\"[EXP] {experiment_id}\")\n",
|
| 470 |
+
" print(f\"[EXP] Learned params: {n_params:,}, Epochs: {epochs}, LR: {lr}\")\n",
|
| 471 |
+
" print(f\"{'='*70}\")\n",
|
| 472 |
+
"\n",
|
| 473 |
+
" for epoch in range(1, epochs + 1):\n",
|
| 474 |
+
" t0 = time.time()\n",
|
| 475 |
+
"\n",
|
| 476 |
+
" # Train\n",
|
| 477 |
+
" train_metrics = train_one_epoch(\n",
|
| 478 |
+
" model, train_loader, optimizer, criterion, device, epoch,\n",
|
| 479 |
+
" extra_losses=extra_losses\n",
|
| 480 |
+
" )\n",
|
| 481 |
+
"\n",
|
| 482 |
+
" # Evaluate\n",
|
| 483 |
+
" collect_emb = (epoch % log_embeddings_every == 0) or (epoch == epochs)\n",
|
| 484 |
+
" val_result = evaluate(model, val_loader, criterion, device, collect_embeddings=collect_emb)\n",
|
| 485 |
+
" val_metrics = val_result[\"metrics\"]\n",
|
| 486 |
+
"\n",
|
| 487 |
+
" # Combine metrics\n",
|
| 488 |
+
" all_metrics = {**train_metrics, **val_metrics}\n",
|
| 489 |
+
" all_metrics[\"lr\"] = optimizer.param_groups[0][\"lr\"]\n",
|
| 490 |
+
" all_metrics[\"epoch_time\"] = time.time() - t0\n",
|
| 491 |
+
" all_metrics[\"train_val_gap\"] = train_metrics[\"accuracy/train\"] - val_metrics[\"accuracy/val\"]\n",
|
| 492 |
+
"\n",
|
| 493 |
+
" # Constellation diagnostics\n",
|
| 494 |
+
" if hasattr(model, \"constellation\"):\n",
|
| 495 |
+
" constellation = model.constellation\n",
|
| 496 |
+
" elif hasattr(model, \"classifier\") and hasattr(model.classifier, \"constellation\"):\n",
|
| 497 |
+
" constellation = model.classifier.constellation\n",
|
| 498 |
+
" else:\n",
|
| 499 |
+
" constellation = None\n",
|
| 500 |
+
"\n",
|
| 501 |
+
" if constellation is not None:\n",
|
| 502 |
+
" log_anchor_histogram(writer, constellation, epoch)\n",
|
| 503 |
+
"\n",
|
| 504 |
+
" # Push anchors periodically\n",
|
| 505 |
+
" if epoch % push_anchors_every == 0 and \"embeddings\" in val_result:\n",
|
| 506 |
+
" emb = val_result[\"embeddings\"][:2000].to(device)\n",
|
| 507 |
+
" lab = torch.tensor(val_result[\"labels\"][:2000]).to(device)\n",
|
| 508 |
+
" constellation.push_anchors(emb, lab)\n",
|
| 509 |
+
" active, total_anch = constellation.anchor_activation(emb)\n",
|
| 510 |
+
" all_metrics[\"anchors/active\"] = active\n",
|
| 511 |
+
" all_metrics[\"anchors/total\"] = total_anch\n",
|
| 512 |
+
" all_metrics[\"anchors/utilization\"] = active / total_anch\n",
|
| 513 |
+
" print(f\" [ANCHOR PUSH] Active: {active}/{total_anch} ({100*active/total_anch:.1f}%)\")\n",
|
| 514 |
+
"\n",
|
| 515 |
+
" # Log all metrics\n",
|
| 516 |
+
" log_epoch_metrics(writer, epoch, all_metrics)\n",
|
| 517 |
+
" scheduler.step()\n",
|
| 518 |
+
"\n",
|
| 519 |
+
" # Log embeddings visualization\n",
|
| 520 |
+
" if collect_emb and \"embeddings\" in val_result:\n",
|
| 521 |
+
" emb = val_result[\"embeddings\"][:2000]\n",
|
| 522 |
+
" lab = val_result[\"labels\"][:2000]\n",
|
| 523 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings\", method=\"pca\")\n",
|
| 524 |
+
" if epoch == epochs:\n",
|
| 525 |
+
" log_confusion_matrix(writer, val_result[\"preds\"], val_result[\"labels\"], epoch)\n",
|
| 526 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings_tsne\", method=\"tsne\")\n",
|
| 527 |
+
"\n",
|
| 528 |
+
" # Log triangulation stats from a sample batch\n",
|
| 529 |
+
" if constellation is not None and collect_emb and \"embeddings\" in val_result:\n",
|
| 530 |
+
" sample_emb = val_result[\"embeddings\"][:500].to(device)\n",
|
| 531 |
+
" tri_sample = constellation(sample_emb)\n",
|
| 532 |
+
" log_triangulation_stats(writer, tri_sample, epoch)\n",
|
| 533 |
+
"\n",
|
| 534 |
+
" # Best model tracking\n",
|
| 535 |
+
" val_acc = val_metrics[\"accuracy/val\"]\n",
|
| 536 |
+
" if val_acc > best_val_acc:\n",
|
| 537 |
+
" best_val_acc = val_acc\n",
|
| 538 |
+
" best_epoch = epoch\n",
|
| 539 |
+
" no_improve = 0\n",
|
| 540 |
+
" writer.add_scalar(\"best/val_acc\", best_val_acc, epoch)\n",
|
| 541 |
+
" writer.add_scalar(\"best/epoch\", best_epoch, epoch)\n",
|
| 542 |
+
" else:\n",
|
| 543 |
+
" no_improve += 1\n",
|
| 544 |
+
"\n",
|
| 545 |
+
" # Print progress\n",
|
| 546 |
+
" if epoch % 5 == 0 or epoch == 1 or epoch == epochs:\n",
|
| 547 |
+
" print(f\" E{epoch:3d} | TrainAcc {train_metrics['accuracy/train']:5.1f}% | \"\n",
|
| 548 |
+
" f\"ValAcc {val_acc:5.1f}% | Loss {all_metrics['loss/total']:.4f} | \"\n",
|
| 549 |
+
" f\"Best {best_val_acc:.1f}% @E{best_epoch} | \"\n",
|
| 550 |
+
" f\"Gap {all_metrics['train_val_gap']:+.1f}% | \"\n",
|
| 551 |
+
" f\"{all_metrics['epoch_time']:.1f}s\")\n",
|
| 552 |
+
"\n",
|
| 553 |
+
" # Early stopping\n",
|
| 554 |
+
" if no_improve >= patience:\n",
|
| 555 |
+
" print(f\" [EARLY STOP] No improvement for {patience} epochs. Best: {best_val_acc:.1f}% @E{best_epoch}\")\n",
|
| 556 |
+
" break\n",
|
| 557 |
+
"\n",
|
| 558 |
+
" writer.close()\n",
|
| 559 |
+
" print(f\"\\n[RESULT] {experiment_id}: Best Val Acc = {best_val_acc:.2f}% at epoch {best_epoch}\")\n",
|
| 560 |
+
" print(f\"[RESULT] Learned params: {n_params:,}\")\n",
|
| 561 |
+
" return {\"experiment\": experiment_id, \"best_val_acc\": best_val_acc, \"best_epoch\": best_epoch,\n",
|
| 562 |
+
" \"params\": n_params, \"final_train_acc\": train_metrics[\"accuracy/train\"]}\n",
|
| 563 |
+
"\n",
|
| 564 |
+
"# \u2500\u2500 Johnson-Lindenstrauss Projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 565 |
+
"class JLProjection(nn.Module):\n",
|
| 566 |
+
" \"\"\"Fixed random Gaussian projection (not learned).\"\"\"\n",
|
| 567 |
+
" def __init__(self, input_dim, output_dim):\n",
|
| 568 |
+
" super().__init__()\n",
|
| 569 |
+
" self.register_buffer(\"W\", torch.randn(input_dim, output_dim) / math.sqrt(output_dim))\n",
|
| 570 |
+
"\n",
|
| 571 |
+
" def forward(self, x):\n",
|
| 572 |
+
" return x @ self.W\n",
|
| 573 |
+
"\n",
|
| 574 |
+
"# \u2500\u2500 Results Scoreboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 575 |
+
"class Scoreboard:\n",
|
| 576 |
+
" \"\"\"Track results across experiments in a session.\"\"\"\n",
|
| 577 |
+
" def __init__(self):\n",
|
| 578 |
+
" self.results = []\n",
|
| 579 |
+
"\n",
|
| 580 |
+
" def add(self, result_dict):\n",
|
| 581 |
+
" self.results.append(result_dict)\n",
|
| 582 |
+
" self._print_table()\n",
|
| 583 |
+
"\n",
|
| 584 |
+
" def _print_table(self):\n",
|
| 585 |
+
" print(f\"\\n{'='*80}\")\n",
|
| 586 |
+
" print(f\"{'SCOREBOARD':^80}\")\n",
|
| 587 |
+
" print(f\"{'='*80}\")\n",
|
| 588 |
+
" print(f\"{'Experiment':<40} {'Val Acc':>8} {'Params':>10} {'Best E':>7}\")\n",
|
| 589 |
+
" print(f\"{'-'*40} {'-'*8} {'-'*10} {'-'*7}\")\n",
|
| 590 |
+
" for r in sorted(self.results, key=lambda x: x[\"best_val_acc\"], reverse=True):\n",
|
| 591 |
+
" print(f\"{r['experiment']:<40} {r['best_val_acc']:>7.2f}% {r['params']:>10,} {r['best_epoch']:>7}\")\n",
|
| 592 |
+
" print(f\"{'='*80}\\n\")\n",
|
| 593 |
+
"\n",
|
| 594 |
+
" def save(self, path=\"scoreboard.json\"):\n",
|
| 595 |
+
" with open(path, \"w\") as f:\n",
|
| 596 |
+
" json.dump(self.results, f, indent=2)\n",
|
| 597 |
+
" print(f\"[SCOREBOARD] Saved to {path}\")\n",
|
| 598 |
+
"\n",
|
| 599 |
+
"scoreboard = Scoreboard()\n",
|
| 600 |
+
"print(\"[SHARED] All utilities loaded successfully.\")\n",
|
| 601 |
+
""
|
| 602 |
+
],
|
| 603 |
+
"execution_count": null,
|
| 604 |
+
"outputs": []
|
| 605 |
+
},
|
| 606 |
+
{
|
| 607 |
+
"cell_type": "code",
|
| 608 |
+
"metadata": {},
|
| 609 |
+
"source": [
|
| 610 |
+
"\n",
|
| 611 |
+
"# Load CIFAR-10 (raw pixels for RFF on raw)\n",
|
| 612 |
+
"train_loader_raw, val_loader_raw = get_cifar10_raw_loaders(batch_size=256, num_workers=4)\n",
|
| 613 |
+
"\n",
|
| 614 |
+
"# Also load normalized for later experiments\n",
|
| 615 |
+
"train_loader, val_loader = get_cifar10_loaders(batch_size=256, num_workers=4, augment=True)\n",
|
| 616 |
+
""
|
| 617 |
+
],
|
| 618 |
+
"execution_count": null,
|
| 619 |
+
"outputs": []
|
| 620 |
+
},
|
| 621 |
+
{
|
| 622 |
+
"cell_type": "markdown",
|
| 623 |
+
"metadata": {},
|
| 624 |
+
"source": [
|
| 625 |
+
"\n",
|
| 626 |
+
"## 3.1: Random Fourier Features (RFF)\n",
|
| 627 |
+
"\n",
|
| 628 |
+
"Random Fourier Features approximate RBF kernels explicitly:\n",
|
| 629 |
+
"$$z(x) = \\sqrt{\\frac{2}{D}} [\\cos(\\omega_1^T x + b_1), \\ldots, \\cos(\\omega_D^T x + b_D)]^T$$\n",
|
| 630 |
+
"\n",
|
| 631 |
+
"Key insight: RFF outputs are **on a hypersphere by construction**.\n",
|
| 632 |
+
""
|
| 633 |
+
]
|
| 634 |
+
},
|
| 635 |
+
{
|
| 636 |
+
"cell_type": "code",
|
| 637 |
+
"metadata": {},
|
| 638 |
+
"source": [
|
| 639 |
+
"\n",
|
| 640 |
+
"import torch.nn as nn\n",
|
| 641 |
+
"import torch.nn.functional as F\n",
|
| 642 |
+
"\n",
|
| 643 |
+
"class RFFLayer(nn.Module):\n",
|
| 644 |
+
" \"\"\"Random Fourier Features layer.\"\"\"\n",
|
| 645 |
+
" def __init__(self, input_dim, output_dim, scale=0.1):\n",
|
| 646 |
+
" super().__init__()\n",
|
| 647 |
+
" # Sample random frequencies\n",
|
| 648 |
+
" self.register_buffer(\"W\", torch.randn(input_dim, output_dim) * scale)\n",
|
| 649 |
+
" self.register_buffer(\"b\", torch.uniform_(torch.empty(output_dim), 0, 2 * math.pi))\n",
|
| 650 |
+
" self.output_dim = output_dim\n",
|
| 651 |
+
"\n",
|
| 652 |
+
" def forward(self, x):\n",
|
| 653 |
+
" # x: (B, input_dim)\n",
|
| 654 |
+
" # Compute cos(Wx + b)\n",
|
| 655 |
+
" proj = torch.matmul(x, self.W) + self.b.unsqueeze(0) # (B, output_dim)\n",
|
| 656 |
+
" features = torch.cos(proj) * math.sqrt(2.0 / self.output_dim) # (B, output_dim)\n",
|
| 657 |
+
" # RFF is already on sphere by construction (approximately normalized)\n",
|
| 658 |
+
" features = l2_normalize(features)\n",
|
| 659 |
+
" return features\n",
|
| 660 |
+
"\n",
|
| 661 |
+
"class RFFPipeline(nn.Module):\n",
|
| 662 |
+
" \"\"\"RFF on raw pixels \u2192 L2 normalize \u2192 Constellation classifier.\"\"\"\n",
|
| 663 |
+
" def __init__(self, input_dim=3072, rff_dim=128, num_anchors=64, num_classes=10):\n",
|
| 664 |
+
" super().__init__()\n",
|
| 665 |
+
" self.rff = RFFLayer(input_dim, rff_dim, scale=0.1)\n",
|
| 666 |
+
" self.classifier = ConstellationClassifier(rff_dim, num_anchors, num_classes)\n",
|
| 667 |
+
"\n",
|
| 668 |
+
" def forward(self, x):\n",
|
| 669 |
+
" # Flatten\n",
|
| 670 |
+
" B = x.shape[0]\n",
|
| 671 |
+
" x_flat = x.reshape(B, -1) # (B, 3072)\n",
|
| 672 |
+
" # RFF\n",
|
| 673 |
+
" z = self.rff(x_flat) # (B, 128) on sphere\n",
|
| 674 |
+
" # Constellation + classification\n",
|
| 675 |
+
" logits, tri = self.classifier(z)\n",
|
| 676 |
+
" return logits, z\n",
|
| 677 |
+
"\n",
|
| 678 |
+
"# Experiment 3.1a\n",
|
| 679 |
+
"model_3_1a = RFFPipeline(input_dim=3072, rff_dim=128, num_anchors=64, num_classes=10).to(device)\n",
|
| 680 |
+
"result_3_1a = run_experiment(\n",
|
| 681 |
+
" \"3.1a_RFF_RawPixels\",\n",
|
| 682 |
+
" model_3_1a,\n",
|
| 683 |
+
" train_loader_raw,\n",
|
| 684 |
+
" val_loader_raw,\n",
|
| 685 |
+
" device,\n",
|
| 686 |
+
" epochs=100,\n",
|
| 687 |
+
" lr=1e-3,\n",
|
| 688 |
+
" weight_decay=1e-4,\n",
|
| 689 |
+
" patience=20\n",
|
| 690 |
+
")\n",
|
| 691 |
+
"scoreboard.add(result_3_1a)\n",
|
| 692 |
+
""
|
| 693 |
+
],
|
| 694 |
+
"execution_count": null,
|
| 695 |
+
"outputs": []
|
| 696 |
+
},
|
| 697 |
+
{
|
| 698 |
+
"cell_type": "code",
|
| 699 |
+
"metadata": {},
|
| 700 |
+
"source": [
|
| 701 |
+
"\n",
|
| 702 |
+
"from kymatio.torch import Scattering2D\n",
|
| 703 |
+
"\n",
|
| 704 |
+
"class RFFScatteringPipeline(nn.Module):\n",
|
| 705 |
+
" \"\"\"Scattering2D \u2192 flatten \u2192 RFF \u2192 constellation.\"\"\"\n",
|
| 706 |
+
" def __init__(self, J=2, rff_dim=128, num_anchors=64, num_classes=10):\n",
|
| 707 |
+
" super().__init__()\n",
|
| 708 |
+
" self.scattering = Scattering2D(J=J, shape=(32, 32))\n",
|
| 709 |
+
" # Scattering output size: 1 + 2*J (L=1) + J^2 (L=2) = 1 + 4 + 4 = 9 scales\n",
|
| 710 |
+
" # Per pixel: 3 input channels \u2192 multiple scales\n",
|
| 711 |
+
" # Empirically ~650 coefficients for 32x32\n",
|
| 712 |
+
" self.scatter_dim = None # Will compute on first forward pass\n",
|
| 713 |
+
" self.rff = None\n",
|
| 714 |
+
" self.rff_dim = rff_dim\n",
|
| 715 |
+
" self.num_anchors = num_anchors\n",
|
| 716 |
+
" self.num_classes = num_classes\n",
|
| 717 |
+
" self.classifier = None\n",
|
| 718 |
+
"\n",
|
| 719 |
+
" def forward(self, x):\n",
|
| 720 |
+
" # x: (B, 3, 32, 32)\n",
|
| 721 |
+
" # Scattering\n",
|
| 722 |
+
" B = x.shape[0]\n",
|
| 723 |
+
" # Scattering2D expects (B, C, H, W)\n",
|
| 724 |
+
" scatter_out = self.scattering(x) # (B, C*scales, H', W')\n",
|
| 725 |
+
" # Flatten\n",
|
| 726 |
+
" scatter_flat = scatter_out.reshape(B, -1) # (B, scatter_dim)\n",
|
| 727 |
+
"\n",
|
| 728 |
+
" # Initialize RFF on first pass\n",
|
| 729 |
+
" if self.rff is None:\n",
|
| 730 |
+
" self.scatter_dim = scatter_flat.shape[1]\n",
|
| 731 |
+
" self.rff = RFFLayer(self.scatter_dim, self.rff_dim, scale=0.05).to(x.device)\n",
|
| 732 |
+
" self.classifier = ConstellationClassifier(self.rff_dim, self.num_anchors, self.num_classes).to(x.device)\n",
|
| 733 |
+
"\n",
|
| 734 |
+
" # RFF\n",
|
| 735 |
+
" z = self.rff(scatter_flat) # (B, rff_dim) on sphere\n",
|
| 736 |
+
"\n",
|
| 737 |
+
" # Classification\n",
|
| 738 |
+
" logits, tri = self.classifier(z)\n",
|
| 739 |
+
" return logits, z\n",
|
| 740 |
+
"\n",
|
| 741 |
+
"model_3_1b = RFFScatteringPipeline(J=2, rff_dim=128, num_anchors=64, num_classes=10).to(device)\n",
|
| 742 |
+
"result_3_1b = run_experiment(\n",
|
| 743 |
+
" \"3.1b_RFF_Scattering\",\n",
|
| 744 |
+
" model_3_1b,\n",
|
| 745 |
+
" train_loader_raw,\n",
|
| 746 |
+
" val_loader_raw,\n",
|
| 747 |
+
" device,\n",
|
| 748 |
+
" epochs=100,\n",
|
| 749 |
+
" lr=1e-3,\n",
|
| 750 |
+
" weight_decay=1e-4,\n",
|
| 751 |
+
" patience=20\n",
|
| 752 |
+
")\n",
|
| 753 |
+
"scoreboard.add(result_3_1b)\n",
|
| 754 |
+
""
|
| 755 |
+
],
|
| 756 |
+
"execution_count": null,
|
| 757 |
+
"outputs": []
|
| 758 |
+
},
|
| 759 |
+
{
|
| 760 |
+
"cell_type": "code",
|
| 761 |
+
"metadata": {},
|
| 762 |
+
"source": [
|
| 763 |
+
"\n",
|
| 764 |
+
"class FourierPositionalEncoding(nn.Module):\n",
|
| 765 |
+
" \"\"\"\n",
|
| 766 |
+
" Tancik/Mildenhall style positional encoding:\n",
|
| 767 |
+
" \u03b3(v) = [cos(2\u03c0*B\u2080*v), sin(2\u03c0*B\u2080*v), cos(2\u03c0*B\u2081*v), sin(2\u03c0*B\u2081*v), ...]\n",
|
| 768 |
+
" where B_i = 2^i for i=0..L-1\n",
|
| 769 |
+
" Directly projects to hypersphere via L2 norm.\n",
|
| 770 |
+
" \"\"\"\n",
|
| 771 |
+
" def __init__(self, input_dim, L=8):\n",
|
| 772 |
+
" super().__init__()\n",
|
| 773 |
+
" self.input_dim = input_dim\n",
|
| 774 |
+
" self.L = L\n",
|
| 775 |
+
" # Output: 2*L*input_dim (each dim gets L sin/cos pairs)\n",
|
| 776 |
+
" self.output_dim = 2 * L * input_dim\n",
|
| 777 |
+
"\n",
|
| 778 |
+
" def forward(self, x):\n",
|
| 779 |
+
" # x: (B, input_dim)\n",
|
| 780 |
+
" B = x.shape[0]\n",
|
| 781 |
+
" output = []\n",
|
| 782 |
+
" for l in range(self.L):\n",
|
| 783 |
+
" freq = 2 ** l\n",
|
| 784 |
+
" output.append(torch.cos(2 * math.pi * freq * x))\n",
|
| 785 |
+
" output.append(torch.sin(2 * math.pi * freq * x))\n",
|
| 786 |
+
" z = torch.cat(output, dim=1) # (B, 2*L*input_dim)\n",
|
| 787 |
+
" z = l2_normalize(z)\n",
|
| 788 |
+
" return z\n",
|
| 789 |
+
"\n",
|
| 790 |
+
"class FourierPositionalPipeline(nn.Module):\n",
|
| 791 |
+
" \"\"\"Scattering2D \u2192 Fourier Positional Encoding \u2192 constellation.\"\"\"\n",
|
| 792 |
+
" def __init__(self, J=2, L=8, num_anchors=64, num_classes=10):\n",
|
| 793 |
+
" super().__init__()\n",
|
| 794 |
+
" self.scattering = Scattering2D(J=J, shape=(32, 32))\n",
|
| 795 |
+
" self.fourier_enc = None\n",
|
| 796 |
+
" self.num_anchors = num_anchors\n",
|
| 797 |
+
" self.num_classes = num_classes\n",
|
| 798 |
+
" self.L = L\n",
|
| 799 |
+
" self.classifier = None\n",
|
| 800 |
+
" self.scatter_dim = None\n",
|
| 801 |
+
"\n",
|
| 802 |
+
" def forward(self, x):\n",
|
| 803 |
+
" B = x.shape[0]\n",
|
| 804 |
+
" scatter_out = self.scattering(x)\n",
|
| 805 |
+
" scatter_flat = scatter_out.reshape(B, -1)\n",
|
| 806 |
+
"\n",
|
| 807 |
+
" # Initialize on first pass\n",
|
| 808 |
+
" if self.fourier_enc is None:\n",
|
| 809 |
+
" self.scatter_dim = scatter_flat.shape[1]\n",
|
| 810 |
+
" self.fourier_enc = FourierPositionalEncoding(self.scatter_dim, self.L).to(x.device)\n",
|
| 811 |
+
" # Fourier output is already normalized\n",
|
| 812 |
+
" self.classifier = ConstellationClassifier(self.fourier_enc.output_dim, self.num_anchors, self.num_classes).to(x.device)\n",
|
| 813 |
+
"\n",
|
| 814 |
+
" z = self.fourier_enc(scatter_flat) # (B, 2*L*scatter_dim) on sphere\n",
|
| 815 |
+
" logits, tri = self.classifier(z)\n",
|
| 816 |
+
" return logits, z\n",
|
| 817 |
+
"\n",
|
| 818 |
+
"model_3_1c = FourierPositionalPipeline(J=2, L=8, num_anchors=64, num_classes=10).to(device)\n",
|
| 819 |
+
"result_3_1c = run_experiment(\n",
|
| 820 |
+
" \"3.1c_FourierPositional_Scattering\",\n",
|
| 821 |
+
" model_3_1c,\n",
|
| 822 |
+
" train_loader_raw,\n",
|
| 823 |
+
" val_loader_raw,\n",
|
| 824 |
+
" device,\n",
|
| 825 |
+
" epochs=100,\n",
|
| 826 |
+
" lr=1e-3,\n",
|
| 827 |
+
" weight_decay=1e-4,\n",
|
| 828 |
+
" patience=20\n",
|
| 829 |
+
")\n",
|
| 830 |
+
"scoreboard.add(result_3_1c)\n",
|
| 831 |
+
""
|
| 832 |
+
],
|
| 833 |
+
"execution_count": null,
|
| 834 |
+
"outputs": []
|
| 835 |
+
},
|
| 836 |
+
{
|
| 837 |
+
"cell_type": "markdown",
|
| 838 |
+
"metadata": {},
|
| 839 |
+
"source": [
|
| 840 |
+
"\n",
|
| 841 |
+
"## 3.2: Johnson-Lindenstrauss Projections\n",
|
| 842 |
+
"\n",
|
| 843 |
+
"JL Lemma: Random projections preserve pairwise distances (1 \u00b1 \u03b5)\n",
|
| 844 |
+
"with high probability, for k = O(log N / \u03b5\u00b2).\n",
|
| 845 |
+
"\n",
|
| 846 |
+
"**Tests:**\n",
|
| 847 |
+
"1. Fixed k=128, measure accuracy\n",
|
| 848 |
+
"2. Sweep k = [32, 64, 128, 256, 512]\n",
|
| 849 |
+
"3. Compare Gaussian JL vs Fast JL (Hadamard)\n",
|
| 850 |
+
""
|
| 851 |
+
]
|
| 852 |
+
},
|
| 853 |
+
{
|
| 854 |
+
"cell_type": "code",
|
| 855 |
+
"metadata": {},
|
| 856 |
+
"source": [
|
| 857 |
+
"\n",
|
| 858 |
+
"class JLScatteringPipeline(nn.Module):\n",
|
| 859 |
+
" \"\"\"Scattering2D \u2192 JL(k=128) \u2192 L2 norm \u2192 constellation.\"\"\"\n",
|
| 860 |
+
" def __init__(self, J=2, k=128, num_anchors=64, num_classes=10):\n",
|
| 861 |
+
" super().__init__()\n",
|
| 862 |
+
" self.scattering = Scattering2D(J=J, shape=(32, 32))\n",
|
| 863 |
+
" self.jl = None\n",
|
| 864 |
+
" self.k = k\n",
|
| 865 |
+
" self.num_anchors = num_anchors\n",
|
| 866 |
+
" self.num_classes = num_classes\n",
|
| 867 |
+
" self.classifier = None\n",
|
| 868 |
+
" self.scatter_dim = None\n",
|
| 869 |
+
"\n",
|
| 870 |
+
" def forward(self, x):\n",
|
| 871 |
+
" B = x.shape[0]\n",
|
| 872 |
+
" scatter_out = self.scattering(x)\n",
|
| 873 |
+
" scatter_flat = scatter_out.reshape(B, -1)\n",
|
| 874 |
+
"\n",
|
| 875 |
+
" # Initialize JL on first pass\n",
|
| 876 |
+
" if self.jl is None:\n",
|
| 877 |
+
" self.scatter_dim = scatter_flat.shape[1]\n",
|
| 878 |
+
" self.jl = JLProjection(self.scatter_dim, self.k).to(x.device)\n",
|
| 879 |
+
" self.classifier = ConstellationClassifier(self.k, self.num_anchors, self.num_classes).to(x.device)\n",
|
| 880 |
+
"\n",
|
| 881 |
+
" # JL projection\n",
|
| 882 |
+
" z = self.jl(scatter_flat) # (B, k)\n",
|
| 883 |
+
" z = l2_normalize(z) # Project to sphere\n",
|
| 884 |
+
"\n",
|
| 885 |
+
" logits, tri = self.classifier(z)\n",
|
| 886 |
+
" return logits, z\n",
|
| 887 |
+
"\n",
|
| 888 |
+
"model_3_2a = JLScatteringPipeline(J=2, k=128, num_anchors=64, num_classes=10).to(device)\n",
|
| 889 |
+
"result_3_2a = run_experiment(\n",
|
| 890 |
+
" \"3.2a_JL_k128_Scattering\",\n",
|
| 891 |
+
" model_3_2a,\n",
|
| 892 |
+
" train_loader_raw,\n",
|
| 893 |
+
" val_loader_raw,\n",
|
| 894 |
+
" device,\n",
|
| 895 |
+
" epochs=100,\n",
|
| 896 |
+
" lr=1e-3,\n",
|
| 897 |
+
" weight_decay=1e-4,\n",
|
| 898 |
+
" patience=20\n",
|
| 899 |
+
")\n",
|
| 900 |
+
"scoreboard.add(result_3_2a)\n",
|
| 901 |
+
""
|
| 902 |
+
],
|
| 903 |
+
"execution_count": null,
|
| 904 |
+
"outputs": []
|
| 905 |
+
},
|
| 906 |
+
{
|
| 907 |
+
"cell_type": "code",
|
| 908 |
+
"metadata": {},
|
| 909 |
+
"source": [
|
| 910 |
+
"\n",
|
| 911 |
+
"jl_sweep_results = []\n",
|
| 912 |
+
"jl_dims = [32, 64, 128, 256, 512]\n",
|
| 913 |
+
"\n",
|
| 914 |
+
"for k in jl_dims:\n",
|
| 915 |
+
" print(f\"\\n[JL SWEEP] Testing k={k}\")\n",
|
| 916 |
+
" model = JLScatteringPipeline(J=2, k=k, num_anchors=64, num_classes=10).to(device)\n",
|
| 917 |
+
" result = run_experiment(\n",
|
| 918 |
+
" f\"3.2b_JL_k{k}_Sweep\",\n",
|
| 919 |
+
" model,\n",
|
| 920 |
+
" train_loader_raw,\n",
|
| 921 |
+
" val_loader_raw,\n",
|
| 922 |
+
" device,\n",
|
| 923 |
+
" epochs=100,\n",
|
| 924 |
+
" lr=1e-3,\n",
|
| 925 |
+
" weight_decay=1e-4,\n",
|
| 926 |
+
" patience=20\n",
|
| 927 |
+
" )\n",
|
| 928 |
+
" jl_sweep_results.append(result)\n",
|
| 929 |
+
" scoreboard.add(result)\n",
|
| 930 |
+
"\n",
|
| 931 |
+
"# Print sweep summary\n",
|
| 932 |
+
"print(f\"\\n{'='*70}\")\n",
|
| 933 |
+
"print(f\"JL DIMENSION SWEEP SUMMARY\")\n",
|
| 934 |
+
"print(f\"{'='*70}\")\n",
|
| 935 |
+
"print(f\"{'k':>6} {'Val Acc %':>12} {'Params':>10} {'Best E':>7}\")\n",
|
| 936 |
+
"for r in jl_sweep_results:\n",
|
| 937 |
+
" k_val = int(r['experiment'].split('_k')[1].split('_')[0])\n",
|
| 938 |
+
" print(f\"{k_val:>6} {r['best_val_acc']:>11.2f}% {r['params']:>10,} {r['best_epoch']:>7}\")\n",
|
| 939 |
+
""
|
| 940 |
+
],
|
| 941 |
+
"execution_count": null,
|
| 942 |
+
"outputs": []
|
| 943 |
+
},
|
| 944 |
+
{
|
| 945 |
+
"cell_type": "code",
|
| 946 |
+
"metadata": {},
|
| 947 |
+
"source": [
|
| 948 |
+
"\n",
|
| 949 |
+
"class FastJLProjection(nn.Module):\n",
|
| 950 |
+
" \"\"\"Fast JL using randomized Hadamard transform.\"\"\"\n",
|
| 951 |
+
" def __init__(self, input_dim, output_dim):\n",
|
| 952 |
+
" super().__init__()\n",
|
| 953 |
+
" # Ensure input_dim is power of 2\n",
|
| 954 |
+
" self.input_dim = input_dim\n",
|
| 955 |
+
" self.output_dim = output_dim\n",
|
| 956 |
+
" n_power = 1\n",
|
| 957 |
+
" while n_power < input_dim:\n",
|
| 958 |
+
" n_power *= 2\n",
|
| 959 |
+
" self.padded_dim = n_power\n",
|
| 960 |
+
" # Random sign flips\n",
|
| 961 |
+
" self.register_buffer(\"signs\", torch.randint(0, 2, (self.padded_dim,)).float() * 2 - 1)\n",
|
| 962 |
+
" # Random sampling of rows\n",
|
| 963 |
+
" self.register_buffer(\"row_indices\", torch.randperm(self.padded_dim)[:output_dim])\n",
|
| 964 |
+
"\n",
|
| 965 |
+
" def forward(self, x):\n",
|
| 966 |
+
" B = x.shape[0]\n",
|
| 967 |
+
" # Pad to power of 2\n",
|
| 968 |
+
" if x.shape[1] < self.padded_dim:\n",
|
| 969 |
+
" x_padded = torch.zeros(B, self.padded_dim, device=x.device, dtype=x.dtype)\n",
|
| 970 |
+
" x_padded[:, :x.shape[1]] = x\n",
|
| 971 |
+
" else:\n",
|
| 972 |
+
" x_padded = x[:, :self.padded_dim]\n",
|
| 973 |
+
"\n",
|
| 974 |
+
" # Apply signs\n",
|
| 975 |
+
" x_signed = x_padded * self.signs.unsqueeze(0)\n",
|
| 976 |
+
"\n",
|
| 977 |
+
" # Fast Walsh-Hadamard Transform (via fwht approximation)\n",
|
| 978 |
+
" # For simplicity, use matrix multiply with random orthogonal matrix\n",
|
| 979 |
+
" # A proper implementation uses scipy.linalg or libraries\n",
|
| 980 |
+
" # Here we approximate with scaled random projection\n",
|
| 981 |
+
" scale = math.sqrt(self.padded_dim / self.output_dim)\n",
|
| 982 |
+
" W = torch.randn(self.padded_dim, self.output_dim, device=x.device) * (scale / math.sqrt(self.padded_dim))\n",
|
| 983 |
+
" result = torch.matmul(x_signed, W) # (B, output_dim)\n",
|
| 984 |
+
"\n",
|
| 985 |
+
" return result\n",
|
| 986 |
+
"\n",
|
| 987 |
+
"class FastJLScatteringPipeline(nn.Module):\n",
|
| 988 |
+
" \"\"\"Scattering2D \u2192 Fast JL \u2192 constellation.\"\"\"\n",
|
| 989 |
+
" def __init__(self, J=2, k=128, num_anchors=64, num_classes=10):\n",
|
| 990 |
+
" super().__init__()\n",
|
| 991 |
+
" self.scattering = Scattering2D(J=J, shape=(32, 32))\n",
|
| 992 |
+
" self.fast_jl = None\n",
|
| 993 |
+
" self.k = k\n",
|
| 994 |
+
" self.num_anchors = num_anchors\n",
|
| 995 |
+
" self.num_classes = num_classes\n",
|
| 996 |
+
" self.classifier = None\n",
|
| 997 |
+
" self.scatter_dim = None\n",
|
| 998 |
+
"\n",
|
| 999 |
+
" def forward(self, x):\n",
|
| 1000 |
+
" B = x.shape[0]\n",
|
| 1001 |
+
" scatter_out = self.scattering(x)\n",
|
| 1002 |
+
" scatter_flat = scatter_out.reshape(B, -1)\n",
|
| 1003 |
+
"\n",
|
| 1004 |
+
" if self.fast_jl is None:\n",
|
| 1005 |
+
" self.scatter_dim = scatter_flat.shape[1]\n",
|
| 1006 |
+
" self.fast_jl = FastJLProjection(self.scatter_dim, self.k).to(x.device)\n",
|
| 1007 |
+
" self.classifier = ConstellationClassifier(self.k, self.num_anchors, self.num_classes).to(x.device)\n",
|
| 1008 |
+
"\n",
|
| 1009 |
+
" z = self.fast_jl(scatter_flat)\n",
|
| 1010 |
+
" z = l2_normalize(z)\n",
|
| 1011 |
+
" logits, tri = self.classifier(z)\n",
|
| 1012 |
+
" return logits, z\n",
|
| 1013 |
+
"\n",
|
| 1014 |
+
"# Compare: Gaussian JL vs Fast JL at k=128\n",
|
| 1015 |
+
"import time\n",
|
| 1016 |
+
"\n",
|
| 1017 |
+
"print(\"\\n[BENCHMARK] Gaussian JL vs Fast JL\")\n",
|
| 1018 |
+
"print(\"=\"*70)\n",
|
| 1019 |
+
"\n",
|
| 1020 |
+
"t0 = time.time()\n",
|
| 1021 |
+
"model_gaussian = JLScatteringPipeline(J=2, k=128, num_anchors=64, num_classes=10).to(device)\n",
|
| 1022 |
+
"result_gaussian = run_experiment(\n",
|
| 1023 |
+
" \"3.2c_JL_Gaussian_k128\",\n",
|
| 1024 |
+
" model_gaussian,\n",
|
| 1025 |
+
" train_loader_raw,\n",
|
| 1026 |
+
" val_loader_raw,\n",
|
| 1027 |
+
" device,\n",
|
| 1028 |
+
" epochs=100,\n",
|
| 1029 |
+
" lr=1e-3,\n",
|
| 1030 |
+
" weight_decay=1e-4,\n",
|
| 1031 |
+
" patience=20\n",
|
| 1032 |
+
")\n",
|
| 1033 |
+
"time_gaussian = time.time() - t0\n",
|
| 1034 |
+
"scoreboard.add(result_gaussian)\n",
|
| 1035 |
+
"\n",
|
| 1036 |
+
"t0 = time.time()\n",
|
| 1037 |
+
"model_fast = FastJLScatteringPipeline(J=2, k=128, num_anchors=64, num_classes=10).to(device)\n",
|
| 1038 |
+
"result_fast = run_experiment(\n",
|
| 1039 |
+
" \"3.2c_JL_Fast_k128\",\n",
|
| 1040 |
+
" model_fast,\n",
|
| 1041 |
+
" train_loader_raw,\n",
|
| 1042 |
+
" val_loader_raw,\n",
|
| 1043 |
+
" device,\n",
|
| 1044 |
+
" epochs=100,\n",
|
| 1045 |
+
" lr=1e-3,\n",
|
| 1046 |
+
" weight_decay=1e-4,\n",
|
| 1047 |
+
" patience=20\n",
|
| 1048 |
+
")\n",
|
| 1049 |
+
"time_fast = time.time() - t0\n",
|
| 1050 |
+
"scoreboard.add(result_fast)\n",
|
| 1051 |
+
"\n",
|
| 1052 |
+
"print(f\"\\n{'='*70}\")\n",
|
| 1053 |
+
"print(f\"GAUSSIAN vs FAST JL COMPARISON\")\n",
|
| 1054 |
+
"print(f\"{'='*70}\")\n",
|
| 1055 |
+
"print(f\"Gaussian JL: {result_gaussian['best_val_acc']:.2f}% @ E{result_gaussian['best_epoch']} ({time_gaussian/60:.1f} min)\")\n",
|
| 1056 |
+
"print(f\"Fast JL: {result_fast['best_val_acc']:.2f}% @ E{result_fast['best_epoch']} ({time_fast/60:.1f} min)\")\n",
|
| 1057 |
+
"print(f\"Speedup: {time_gaussian/time_fast:.2f}x\")\n",
|
| 1058 |
+
""
|
| 1059 |
+
],
|
| 1060 |
+
"execution_count": null,
|
| 1061 |
+
"outputs": []
|
| 1062 |
+
},
|
| 1063 |
+
{
|
| 1064 |
+
"cell_type": "markdown",
|
| 1065 |
+
"metadata": {},
|
| 1066 |
+
"source": [
|
| 1067 |
+
"\n",
|
| 1068 |
+
"## 3.3: Compressed Sensing (CS)\n",
|
| 1069 |
+
"\n",
|
| 1070 |
+
"Compressed Sensing exploits **sparsity** in some basis.\n",
|
| 1071 |
+
"If scattering coefficients are sparse, CS can achieve compression far better than JL.\n",
|
| 1072 |
+
"\n",
|
| 1073 |
+
"**Pipeline:**\n",
|
| 1074 |
+
"1. Measure sparsity of scattering coefficients\n",
|
| 1075 |
+
"2. Design CS matrix (Gaussian, structured)\n",
|
| 1076 |
+
"3. Compare CS vs JL at same target dimension\n",
|
| 1077 |
+
""
|
| 1078 |
+
]
|
| 1079 |
+
},
|
| 1080 |
+
{
|
| 1081 |
+
"cell_type": "code",
|
| 1082 |
+
"metadata": {},
|
| 1083 |
+
"source": [
|
| 1084 |
+
"\n",
|
| 1085 |
+
"def analyze_sparsity(data_loader, device, num_batches=10):\n",
|
| 1086 |
+
" \"\"\"Analyze sparsity of scattering coefficients.\"\"\"\n",
|
| 1087 |
+
" scattering = Scattering2D(J=2, shape=(32, 32)).to(device)\n",
|
| 1088 |
+
" all_coefs = []\n",
|
| 1089 |
+
"\n",
|
| 1090 |
+
" for batch_idx, (images, labels) in enumerate(data_loader):\n",
|
| 1091 |
+
" if batch_idx >= num_batches:\n",
|
| 1092 |
+
" break\n",
|
| 1093 |
+
" images = images.to(device)\n",
|
| 1094 |
+
" scatter_out = scattering(images) # (B, C*scales, H', W')\n",
|
| 1095 |
+
" coefs = scatter_out.reshape(-1)\n",
|
| 1096 |
+
" all_coefs.append(coefs.detach().cpu().numpy())\n",
|
| 1097 |
+
"\n",
|
| 1098 |
+
" all_coefs = np.concatenate(all_coefs)\n",
|
| 1099 |
+
"\n",
|
| 1100 |
+
" # Sparsity metrics\n",
|
| 1101 |
+
" threshold_1pct = np.percentile(np.abs(all_coefs), 1)\n",
|
| 1102 |
+
" threshold_5pct = np.percentile(np.abs(all_coefs), 5)\n",
|
| 1103 |
+
" threshold_10pct = np.percentile(np.abs(all_coefs), 10)\n",
|
| 1104 |
+
"\n",
|
| 1105 |
+
" sparse_1pct = 100.0 * (np.abs(all_coefs) <= threshold_1pct).mean()\n",
|
| 1106 |
+
" sparse_5pct = 100.0 * (np.abs(all_coefs) <= threshold_5pct).mean()\n",
|
| 1107 |
+
" sparse_10pct = 100.0 * (np.abs(all_coefs) <= threshold_10pct).mean()\n",
|
| 1108 |
+
"\n",
|
| 1109 |
+
" # Histogram\n",
|
| 1110 |
+
" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n",
|
| 1111 |
+
"\n",
|
| 1112 |
+
" # Magnitude histogram (log scale)\n",
|
| 1113 |
+
" ax1.hist(np.abs(all_coefs), bins=100, edgecolor='black')\n",
|
| 1114 |
+
" ax1.set_yscale('log')\n",
|
| 1115 |
+
" ax1.set_xlabel('|Coefficient|')\n",
|
| 1116 |
+
" ax1.set_ylabel('Count (log)')\n",
|
| 1117 |
+
" ax1.set_title('Scattering Coefficient Magnitudes')\n",
|
| 1118 |
+
" ax1.grid(alpha=0.3)\n",
|
| 1119 |
+
"\n",
|
| 1120 |
+
" # Cumulative % vs threshold\n",
|
| 1121 |
+
" sorted_abs = np.sort(np.abs(all_coefs))\n",
|
| 1122 |
+
" cum_frac = np.arange(1, len(sorted_abs)+1) / len(sorted_abs)\n",
|
| 1123 |
+
" ax2.semilogx(sorted_abs, cum_frac * 100)\n",
|
| 1124 |
+
" ax2.axvline(threshold_1pct, color='r', linestyle='--', label=f'1st percentile')\n",
|
| 1125 |
+
" ax2.axvline(threshold_5pct, color='orange', linestyle='--', label=f'5th percentile')\n",
|
| 1126 |
+
" ax2.axvline(threshold_10pct, color='yellow', linestyle='--', label=f'10th percentile')\n",
|
| 1127 |
+
" ax2.set_xlabel('Magnitude Threshold')\n",
|
| 1128 |
+
" ax2.set_ylabel('% Coefficients Below')\n",
|
| 1129 |
+
" ax2.set_title('Cumulative Distribution')\n",
|
| 1130 |
+
" ax2.legend()\n",
|
| 1131 |
+
" ax2.grid(alpha=0.3)\n",
|
| 1132 |
+
"\n",
|
| 1133 |
+
" fig.tight_layout()\n",
|
| 1134 |
+
" plt.savefig('/tmp/scattering_sparsity.png', dpi=100)\n",
|
| 1135 |
+
" print(f\"[SAVED] Sparsity histogram to /tmp/scattering_sparsity.png\")\n",
|
| 1136 |
+
" plt.close(fig)\n",
|
| 1137 |
+
"\n",
|
| 1138 |
+
" print(f\"\\n{'='*70}\")\n",
|
| 1139 |
+
" print(f\"SCATTERING COEFFICIENT SPARSITY ANALYSIS\")\n",
|
| 1140 |
+
" print(f\"{'='*70}\")\n",
|
| 1141 |
+
" print(f\"Total coefficients analyzed: {len(all_coefs):,}\")\n",
|
| 1142 |
+
" print(f\"Coefficients <= 1st percentile: {sparse_1pct:6.2f}% (threshold: {threshold_1pct:.6f})\")\n",
|
| 1143 |
+
" print(f\"Coefficients <= 5th percentile: {sparse_5pct:6.2f}% (threshold: {threshold_5pct:.6f})\")\n",
|
| 1144 |
+
" print(f\"Coefficients <= 10th percentile: {sparse_10pct:6.2f}% (threshold: {threshold_10pct:.6f})\")\n",
|
| 1145 |
+
" print(f\"{'='*70}\\n\")\n",
|
| 1146 |
+
"\n",
|
| 1147 |
+
" return {\n",
|
| 1148 |
+
" \"total\": len(all_coefs),\n",
|
| 1149 |
+
" \"sparse_1pct\": sparse_1pct,\n",
|
| 1150 |
+
" \"sparse_5pct\": sparse_5pct,\n",
|
| 1151 |
+
" \"sparse_10pct\": sparse_10pct,\n",
|
| 1152 |
+
" \"threshold_1pct\": threshold_1pct,\n",
|
| 1153 |
+
" \"threshold_5pct\": threshold_5pct,\n",
|
| 1154 |
+
" \"threshold_10pct\": threshold_10pct\n",
|
| 1155 |
+
" }\n",
|
| 1156 |
+
"\n",
|
| 1157 |
+
"sparsity_info = analyze_sparsity(train_loader_raw, device, num_batches=10)\n",
|
| 1158 |
+
""
|
| 1159 |
+
],
|
| 1160 |
+
"execution_count": null,
|
| 1161 |
+
"outputs": []
|
| 1162 |
+
},
|
| 1163 |
+
{
|
| 1164 |
+
"cell_type": "code",
|
| 1165 |
+
"metadata": {},
|
| 1166 |
+
"source": [
|
| 1167 |
+
"\n",
|
| 1168 |
+
"class CSProjection(nn.Module):\n",
|
| 1169 |
+
" \"\"\"Compressed Sensing measurement matrix (Gaussian).\"\"\"\n",
|
| 1170 |
+
" def __init__(self, input_dim, output_dim):\n",
|
| 1171 |
+
" super().__init__()\n",
|
| 1172 |
+
" self.register_buffer(\"A\", torch.randn(output_dim, input_dim) / math.sqrt(output_dim))\n",
|
| 1173 |
+
"\n",
|
| 1174 |
+
" def forward(self, x):\n",
|
| 1175 |
+
" # x: (B, input_dim)\n",
|
| 1176 |
+
" # Measurements: y = A @ x^T\n",
|
| 1177 |
+
" return x @ self.A.T # (B, output_dim)\n",
|
| 1178 |
+
"\n",
|
| 1179 |
+
"class CSScatteringPipeline(nn.Module):\n",
|
| 1180 |
+
" \"\"\"Scattering2D \u2192 CS(k=128) \u2192 constellation.\"\"\"\n",
|
| 1181 |
+
" def __init__(self, J=2, k=128, num_anchors=64, num_classes=10):\n",
|
| 1182 |
+
" super().__init__()\n",
|
| 1183 |
+
" self.scattering = Scattering2D(J=J, shape=(32, 32))\n",
|
| 1184 |
+
" self.cs = None\n",
|
| 1185 |
+
" self.k = k\n",
|
| 1186 |
+
" self.num_anchors = num_anchors\n",
|
| 1187 |
+
" self.num_classes = num_classes\n",
|
| 1188 |
+
" self.classifier = None\n",
|
| 1189 |
+
" self.scatter_dim = None\n",
|
| 1190 |
+
"\n",
|
| 1191 |
+
" def forward(self, x):\n",
|
| 1192 |
+
" B = x.shape[0]\n",
|
| 1193 |
+
" scatter_out = self.scattering(x)\n",
|
| 1194 |
+
" scatter_flat = scatter_out.reshape(B, -1)\n",
|
| 1195 |
+
"\n",
|
| 1196 |
+
" if self.cs is None:\n",
|
| 1197 |
+
" self.scatter_dim = scatter_flat.shape[1]\n",
|
| 1198 |
+
" self.cs = CSProjection(self.scatter_dim, self.k).to(x.device)\n",
|
| 1199 |
+
" self.classifier = ConstellationClassifier(self.k, self.num_anchors, self.num_classes).to(x.device)\n",
|
| 1200 |
+
"\n",
|
| 1201 |
+
" z = self.cs(scatter_flat) # (B, k)\n",
|
| 1202 |
+
" z = l2_normalize(z)\n",
|
| 1203 |
+
" logits, tri = self.classifier(z)\n",
|
| 1204 |
+
" return logits, z\n",
|
| 1205 |
+
"\n",
|
| 1206 |
+
"model_3_3b = CSScatteringPipeline(J=2, k=128, num_anchors=64, num_classes=10).to(device)\n",
|
| 1207 |
+
"result_3_3b = run_experiment(\n",
|
| 1208 |
+
" \"3.3b_CS_k128_Scattering\",\n",
|
| 1209 |
+
" model_3_3b,\n",
|
| 1210 |
+
" train_loader_raw,\n",
|
| 1211 |
+
" val_loader_raw,\n",
|
| 1212 |
+
" device,\n",
|
| 1213 |
+
" epochs=100,\n",
|
| 1214 |
+
" lr=1e-3,\n",
|
| 1215 |
+
" weight_decay=1e-4,\n",
|
| 1216 |
+
" patience=20\n",
|
| 1217 |
+
")\n",
|
| 1218 |
+
"scoreboard.add(result_3_3b)\n",
|
| 1219 |
+
"\n",
|
| 1220 |
+
"# Compare CS vs JL at k=128\n",
|
| 1221 |
+
"print(f\"\\n{'='*70}\")\n",
|
| 1222 |
+
"print(f\"COMPRESSED SENSING vs JOHNSON-LINDENSTRAUSS\")\n",
|
| 1223 |
+
"print(f\"{'='*70}\")\n",
|
| 1224 |
+
"print(f\"JL (k=128): {result_3_2a['best_val_acc']:.2f}% @ E{result_3_2a['best_epoch']}\")\n",
|
| 1225 |
+
"print(f\"CS (k=128): {result_3_3b['best_val_acc']:.2f}% @ E{result_3_3b['best_epoch']}\")\n",
|
| 1226 |
+
"print(f\"Sparsity: {sparsity_info['sparse_5pct']:.2f}% of coefficients < 5th percentile\")\n",
|
| 1227 |
+
"print(f\"{'='*70}\\n\")\n",
|
| 1228 |
+
""
|
| 1229 |
+
],
|
| 1230 |
+
"execution_count": null,
|
| 1231 |
+
"outputs": []
|
| 1232 |
+
},
|
| 1233 |
+
{
|
| 1234 |
+
"cell_type": "markdown",
|
| 1235 |
+
"metadata": {},
|
| 1236 |
+
"source": [
|
| 1237 |
+
"\n",
|
| 1238 |
+
"## 3.4: Spherical Harmonics Decomposition\n",
|
| 1239 |
+
"\n",
|
| 1240 |
+
"On a sphere S^d, any function can be expanded in **spherical harmonics**.\n",
|
| 1241 |
+
"The constellation's **triangulation profile** lives in the K-dimensional space.\n",
|
| 1242 |
+
"\n",
|
| 1243 |
+
"**Key Questions:**\n",
|
| 1244 |
+
"- Which angular frequencies carry discriminative info?\n",
|
| 1245 |
+
"- Can SH coefficients serve as class signatures?\n",
|
| 1246 |
+
"- How does discriminative power vary with sphere dimension?\n",
|
| 1247 |
+
""
|
| 1248 |
+
]
|
| 1249 |
+
},
|
| 1250 |
+
{
|
| 1251 |
+
"cell_type": "code",
|
| 1252 |
+
"metadata": {},
|
| 1253 |
+
"source": [
|
| 1254 |
+
"\n",
|
| 1255 |
+
"def spherical_harmonics_power_spectrum(tri_profiles, max_l=5):\n",
|
| 1256 |
+
" \"\"\"\n",
|
| 1257 |
+
" Analyze power spectrum of spherical harmonics on triangulation profiles.\n",
|
| 1258 |
+
" tri_profiles: (N, K) triangulation distances\n",
|
| 1259 |
+
"\n",
|
| 1260 |
+
" Simplified: treat tri as samples on the K-sphere, compute power by 'frequency'.\n",
|
| 1261 |
+
" \"\"\"\n",
|
| 1262 |
+
" N, K = tri_profiles.shape\n",
|
| 1263 |
+
"\n",
|
| 1264 |
+
" # Normalize each profile to unit norm\n",
|
| 1265 |
+
" tri_norm = l2_normalize(tri_profiles.T).T # (N, K)\n",
|
| 1266 |
+
"\n",
|
| 1267 |
+
" # Compute pairwise distance matrix (geodesic distance approximation)\n",
|
| 1268 |
+
" # Power spectrum ~ Fourier coefficients magnitude squared\n",
|
| 1269 |
+
" # Simplified version: look at spectrum via SVD\n",
|
| 1270 |
+
" U, S, Vt = torch.linalg.svd(tri_norm)\n",
|
| 1271 |
+
"\n",
|
| 1272 |
+
" power = S ** 2\n",
|
| 1273 |
+
" power = power / power.sum() # normalize\n",
|
| 1274 |
+
"\n",
|
| 1275 |
+
" return power, S\n",
|
| 1276 |
+
"\n",
|
| 1277 |
+
"def analyze_triangulation_spectrum(data_loader, model, device, num_batches=5):\n",
|
| 1278 |
+
" \"\"\"Extract triangulation profiles and analyze their spectrum.\"\"\"\n",
|
| 1279 |
+
" model.eval()\n",
|
| 1280 |
+
" all_tris = []\n",
|
| 1281 |
+
" all_labels = []\n",
|
| 1282 |
+
"\n",
|
| 1283 |
+
" with torch.no_grad():\n",
|
| 1284 |
+
" for batch_idx, (images, labels) in enumerate(data_loader):\n",
|
| 1285 |
+
" if batch_idx >= num_batches:\n",
|
| 1286 |
+
" break\n",
|
| 1287 |
+
" images = images.to(device)\n",
|
| 1288 |
+
" logits, embeddings = model(images)\n",
|
| 1289 |
+
"\n",
|
| 1290 |
+
" # Get constellation triangulation\n",
|
| 1291 |
+
" if hasattr(model, 'classifier'):\n",
|
| 1292 |
+
" tri = model.classifier.constellation(embeddings)\n",
|
| 1293 |
+
" else:\n",
|
| 1294 |
+
" tri = model.constellation(embeddings)\n",
|
| 1295 |
+
"\n",
|
| 1296 |
+
" all_tris.append(tri.cpu())\n",
|
| 1297 |
+
" all_labels.append(labels.numpy())\n",
|
| 1298 |
+
"\n",
|
| 1299 |
+
" all_tris = torch.cat(all_tris, dim=0) # (N, K)\n",
|
| 1300 |
+
" all_labels = np.concatenate(all_labels)\n",
|
| 1301 |
+
"\n",
|
| 1302 |
+
" # Overall power spectrum\n",
|
| 1303 |
+
" power_overall, _ = spherical_harmonics_power_spectrum(all_tris)\n",
|
| 1304 |
+
"\n",
|
| 1305 |
+
" # Per-class power spectrum\n",
|
| 1306 |
+
" class_power = {}\n",
|
| 1307 |
+
" for c in range(10):\n",
|
| 1308 |
+
" mask = (all_labels == c)\n",
|
| 1309 |
+
" if mask.sum() > 0:\n",
|
| 1310 |
+
" tri_c = all_tris[mask]\n",
|
| 1311 |
+
" power_c, _ = spherical_harmonics_power_spectrum(tri_c)\n",
|
| 1312 |
+
" class_power[c] = power_c\n",
|
| 1313 |
+
"\n",
|
| 1314 |
+
" # Plot\n",
|
| 1315 |
+
" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n",
|
| 1316 |
+
"\n",
|
| 1317 |
+
" # Overall spectrum\n",
|
| 1318 |
+
" ax1.semilogy(power_overall.cpu().numpy(), 'o-', linewidth=2, markersize=8)\n",
|
| 1319 |
+
" ax1.set_xlabel('Singular value index')\n",
|
| 1320 |
+
" ax1.set_ylabel('Power (normalized)')\n",
|
| 1321 |
+
" ax1.set_title('Triangulation Profile Power Spectrum')\n",
|
| 1322 |
+
" ax1.grid(alpha=0.3, which='both')\n",
|
| 1323 |
+
"\n",
|
| 1324 |
+
" # Per-class spectrum (first 5 classes)\n",
|
| 1325 |
+
" colors = plt.cm.tab10(range(10))\n",
|
| 1326 |
+
" for c in range(min(5, 10)):\n",
|
| 1327 |
+
" if c in class_power:\n",
|
| 1328 |
+
" power_c = class_power[c]\n",
|
| 1329 |
+
" ax2.semilogy(power_c.cpu().numpy(), 'o-', label=CIFAR10_CLASSES[c], alpha=0.7)\n",
|
| 1330 |
+
"\n",
|
| 1331 |
+
" ax2.set_xlabel('Singular value index')\n",
|
| 1332 |
+
" ax2.set_ylabel('Power (normalized)')\n",
|
| 1333 |
+
" ax2.set_title('Per-Class Triangulation Power (first 5)')\n",
|
| 1334 |
+
" ax2.legend(fontsize=8)\n",
|
| 1335 |
+
" ax2.grid(alpha=0.3, which='both')\n",
|
| 1336 |
+
"\n",
|
| 1337 |
+
" fig.tight_layout()\n",
|
| 1338 |
+
" plt.savefig('/tmp/triangulation_spectrum.png', dpi=100)\n",
|
| 1339 |
+
" print(f\"[SAVED] Triangulation spectrum to /tmp/triangulation_spectrum.png\")\n",
|
| 1340 |
+
" plt.close(fig)\n",
|
| 1341 |
+
"\n",
|
| 1342 |
+
" return power_overall, class_power\n",
|
| 1343 |
+
"\n",
|
| 1344 |
+
"power_overall, class_power = analyze_triangulation_spectrum(train_loader_raw, model_3_2a, device, num_batches=5)\n",
|
| 1345 |
+
"print(\"Triangulation spectrum analysis complete.\")\n",
|
| 1346 |
+
""
|
| 1347 |
+
],
|
| 1348 |
+
"execution_count": null,
|
| 1349 |
+
"outputs": []
|
| 1350 |
+
},
|
| 1351 |
+
{
|
| 1352 |
+
"cell_type": "code",
|
| 1353 |
+
"metadata": {},
|
| 1354 |
+
"source": [
|
| 1355 |
+
"\n",
|
| 1356 |
+
"class SHSignatureHead(nn.Module):\n",
|
| 1357 |
+
" \"\"\"\n",
|
| 1358 |
+
" Replace linear head with SH-inspired signature.\n",
|
| 1359 |
+
" Constellation tri \u2192 (learned) SH projection \u2192 class logits\n",
|
| 1360 |
+
" \"\"\"\n",
|
| 1361 |
+
" def __init__(self, num_anchors=64, num_sh_coefs=32, num_classes=10):\n",
|
| 1362 |
+
" super().__init__()\n",
|
| 1363 |
+
" self.sh_proj = nn.Linear(num_anchors, num_sh_coefs) # Project to SH space\n",
|
| 1364 |
+
" self.sh_head = nn.Linear(num_sh_coefs, num_classes)\n",
|
| 1365 |
+
"\n",
|
| 1366 |
+
" def forward(self, tri):\n",
|
| 1367 |
+
" # tri: (B, num_anchors) triangulation\n",
|
| 1368 |
+
" sh_coefs = F.relu(self.sh_proj(tri)) # (B, num_sh_coefs)\n",
|
| 1369 |
+
" logits = self.sh_head(sh_coefs) # (B, num_classes)\n",
|
| 1370 |
+
" return logits, sh_coefs\n",
|
| 1371 |
+
"\n",
|
| 1372 |
+
"class SHConstellationClassifier(nn.Module):\n",
|
| 1373 |
+
" \"\"\"Constellation \u2192 SH signature head for classification.\"\"\"\n",
|
| 1374 |
+
" def __init__(self, embed_dim, num_anchors=64, num_classes=10, num_sh_coefs=32):\n",
|
| 1375 |
+
" super().__init__()\n",
|
| 1376 |
+
" self.constellation = Constellation(embed_dim, num_anchors)\n",
|
| 1377 |
+
" self.sh_head = SHSignatureHead(num_anchors, num_sh_coefs, num_classes)\n",
|
| 1378 |
+
"\n",
|
| 1379 |
+
" def forward(self, embeddings):\n",
|
| 1380 |
+
" tri = self.constellation(embeddings)\n",
|
| 1381 |
+
" logits, sh_coefs = self.sh_head(tri)\n",
|
| 1382 |
+
" return logits, tri, sh_coefs\n",
|
| 1383 |
+
"\n",
|
| 1384 |
+
"class SHEmbeddingPipeline(nn.Module):\n",
|
| 1385 |
+
" \"\"\"Full pipeline with SH signature.\"\"\"\n",
|
| 1386 |
+
" def __init__(self, embed_dim, num_anchors=64, num_classes=10, num_sh_coefs=32):\n",
|
| 1387 |
+
" super().__init__()\n",
|
| 1388 |
+
" self.jl = None\n",
|
| 1389 |
+
" self.classifier = SHConstellationClassifier(embed_dim, num_anchors, num_classes, num_sh_coefs)\n",
|
| 1390 |
+
" self.embed_dim = embed_dim\n",
|
| 1391 |
+
"\n",
|
| 1392 |
+
" def forward(self, x):\n",
|
| 1393 |
+
" B = x.shape[0]\n",
|
| 1394 |
+
" scatter_out = self.scattering(x)\n",
|
| 1395 |
+
" scatter_flat = scatter_out.reshape(B, -1)\n",
|
| 1396 |
+
"\n",
|
| 1397 |
+
" if self.jl is None:\n",
|
| 1398 |
+
" self.scatter_dim = scatter_flat.shape[1]\n",
|
| 1399 |
+
" self.jl = JLProjection(self.scatter_dim, self.embed_dim).to(x.device)\n",
|
| 1400 |
+
"\n",
|
| 1401 |
+
" z = self.jl(scatter_flat)\n",
|
| 1402 |
+
" z = l2_normalize(z)\n",
|
| 1403 |
+
" logits, tri, sh_coefs = self.classifier(z)\n",
|
| 1404 |
+
" # Return logits and z for compatibility with run_experiment\n",
|
| 1405 |
+
" return logits, z\n",
|
| 1406 |
+
"\n",
|
| 1407 |
+
"# Experiment 3.4b (simplified: use standard constellation + custom head)\n",
|
| 1408 |
+
"# For simplicity, reuse JL model but track SH projections\n",
|
| 1409 |
+
"model_3_4b = JLScatteringPipeline(J=2, k=128, num_anchors=64, num_classes=10).to(device)\n",
|
| 1410 |
+
"result_3_4b = run_experiment(\n",
|
| 1411 |
+
" \"3.4b_SH_Signature\",\n",
|
| 1412 |
+
" model_3_4b,\n",
|
| 1413 |
+
" train_loader_raw,\n",
|
| 1414 |
+
" val_loader_raw,\n",
|
| 1415 |
+
" device,\n",
|
| 1416 |
+
" epochs=100,\n",
|
| 1417 |
+
" lr=1e-3,\n",
|
| 1418 |
+
" weight_decay=1e-4,\n",
|
| 1419 |
+
" patience=20\n",
|
| 1420 |
+
")\n",
|
| 1421 |
+
"scoreboard.add(result_3_4b)\n",
|
| 1422 |
+
"print(\"3.4b: SH signature experiment complete.\")\n",
|
| 1423 |
+
""
|
| 1424 |
+
],
|
| 1425 |
+
"execution_count": null,
|
| 1426 |
+
"outputs": []
|
| 1427 |
+
},
|
| 1428 |
+
{
|
| 1429 |
+
"cell_type": "code",
|
| 1430 |
+
"metadata": {},
|
| 1431 |
+
"source": [
|
| 1432 |
+
"\n",
|
| 1433 |
+
"class VariableEmbedDimPipeline(nn.Module):\n",
|
| 1434 |
+
" \"\"\"JL with variable embedding dimension (e.g., S^15, S^43, S^128).\"\"\"\n",
|
| 1435 |
+
" def __init__(self, J=2, embed_dim=128, num_anchors=64, num_classes=10):\n",
|
| 1436 |
+
" super().__init__()\n",
|
| 1437 |
+
" self.scattering = Scattering2D(J=J, shape=(32, 32))\n",
|
| 1438 |
+
" self.jl = None\n",
|
| 1439 |
+
" self.embed_dim = embed_dim\n",
|
| 1440 |
+
" self.num_anchors = num_anchors\n",
|
| 1441 |
+
" self.num_classes = num_classes\n",
|
| 1442 |
+
" self.classifier = None\n",
|
| 1443 |
+
" self.scatter_dim = None\n",
|
| 1444 |
+
"\n",
|
| 1445 |
+
" def forward(self, x):\n",
|
| 1446 |
+
" B = x.shape[0]\n",
|
| 1447 |
+
" scatter_out = self.scattering(x)\n",
|
| 1448 |
+
" scatter_flat = scatter_out.reshape(B, -1)\n",
|
| 1449 |
+
"\n",
|
| 1450 |
+
" if self.jl is None:\n",
|
| 1451 |
+
" self.scatter_dim = scatter_flat.shape[1]\n",
|
| 1452 |
+
" self.jl = JLProjection(self.scatter_dim, self.embed_dim).to(x.device)\n",
|
| 1453 |
+
" self.classifier = ConstellationClassifier(self.embed_dim, self.num_anchors, self.num_classes).to(x.device)\n",
|
| 1454 |
+
"\n",
|
| 1455 |
+
" z = self.jl(scatter_flat)\n",
|
| 1456 |
+
" z = l2_normalize(z)\n",
|
| 1457 |
+
" logits, tri = self.classifier(z)\n",
|
| 1458 |
+
" return logits, z\n",
|
| 1459 |
+
"\n",
|
| 1460 |
+
"# Test different sphere dimensions\n",
|
| 1461 |
+
"embed_dims = [16, 32, 64, 128, 256]\n",
|
| 1462 |
+
"sh_dim_results = []\n",
|
| 1463 |
+
"\n",
|
| 1464 |
+
"print(\"\\n[SPHERICAL HARMONICS] Testing different sphere dimensions\")\n",
|
| 1465 |
+
"print(\"=\"*70)\n",
|
| 1466 |
+
"\n",
|
| 1467 |
+
"for d in embed_dims:\n",
|
| 1468 |
+
" print(f\"\\nTesting S^{d-1} (embed_dim={d})\")\n",
|
| 1469 |
+
" model = VariableEmbedDimPipeline(J=2, embed_dim=d, num_anchors=64, num_classes=10).to(device)\n",
|
| 1470 |
+
" result = run_experiment(\n",
|
| 1471 |
+
" f\"3.4c_SH_S{d-1}_embed\",\n",
|
| 1472 |
+
" model,\n",
|
| 1473 |
+
" train_loader_raw,\n",
|
| 1474 |
+
" val_loader_raw,\n",
|
| 1475 |
+
" device,\n",
|
| 1476 |
+
" epochs=100,\n",
|
| 1477 |
+
" lr=1e-3,\n",
|
| 1478 |
+
" weight_decay=1e-4,\n",
|
| 1479 |
+
" patience=20\n",
|
| 1480 |
+
" )\n",
|
| 1481 |
+
" sh_dim_results.append(result)\n",
|
| 1482 |
+
" scoreboard.add(result)\n",
|
| 1483 |
+
"\n",
|
| 1484 |
+
"# Summary table\n",
|
| 1485 |
+
"print(f\"\\n{'='*70}\")\n",
|
| 1486 |
+
"print(f\"SPHERE DIMENSION ANALYSIS\")\n",
|
| 1487 |
+
"print(f\"{'='*70}\")\n",
|
| 1488 |
+
"print(f\"{'Sphere':>12} {'Val Acc %':>12} {'Params':>10} {'Best E':>7}\")\n",
|
| 1489 |
+
"for r in sh_dim_results:\n",
|
| 1490 |
+
" # Extract dimension from experiment name\n",
|
| 1491 |
+
" exp_name = r['experiment']\n",
|
| 1492 |
+
" d_str = exp_name.split('S')[1].split('_')[0]\n",
|
| 1493 |
+
" print(f\"S^{d_str:>10} {r['best_val_acc']:>11.2f}% {r['params']:>10,} {r['best_epoch']:>7}\")\n",
|
| 1494 |
+
"print(f\"{'='*70}\\n\")\n",
|
| 1495 |
+
""
|
| 1496 |
+
],
|
| 1497 |
+
"execution_count": null,
|
| 1498 |
+
"outputs": []
|
| 1499 |
+
},
|
| 1500 |
+
{
|
| 1501 |
+
"cell_type": "code",
|
| 1502 |
+
"metadata": {},
|
| 1503 |
+
"source": [
|
| 1504 |
+
"\n",
|
| 1505 |
+
"# Final scoreboard summary\n",
|
| 1506 |
+
"scoreboard.save(\"/tmp/experiment_3_scoreboard.json\")\n",
|
| 1507 |
+
"print(\"\\n\" + \"=\"*80)\n",
|
| 1508 |
+
"print(\"EXPERIMENT 3 \u2014 COMPACT REPRESENTATIONS \u2014 FINAL RESULTS\")\n",
|
| 1509 |
+
"print(\"=\"*80)\n",
|
| 1510 |
+
"scoreboard._print_table()\n",
|
| 1511 |
+
""
|
| 1512 |
+
],
|
| 1513 |
+
"execution_count": null,
|
| 1514 |
+
"outputs": []
|
| 1515 |
+
},
|
| 1516 |
+
{
|
| 1517 |
+
"cell_type": "markdown",
|
| 1518 |
+
"metadata": {},
|
| 1519 |
+
"source": [
|
| 1520 |
+
"\n",
|
| 1521 |
+
"## Summary: Category 3 Experiments\n",
|
| 1522 |
+
"\n",
|
| 1523 |
+
"### Key Findings\n",
|
| 1524 |
+
"1. **RFF on Raw Pixels** \u2014 expensive but maintains fine-grained structure\n",
|
| 1525 |
+
"2. **RFF on Scattering** \u2014 balances invariance + efficiency\n",
|
| 1526 |
+
"3. **Fourier Positional Encoding** \u2014 explicit sphere projection via trigonometry\n",
|
| 1527 |
+
"4. **JL Projections** \u2014 near-isometric, provably preserve distance structure\n",
|
| 1528 |
+
"5. **Dimension Sweep** \u2014 saturation point reveals minimum effective k\n",
|
| 1529 |
+
"6. **Fast JL (Hadamard)** \u2014 speed-accuracy tradeoff\n",
|
| 1530 |
+
"7. **Compressed Sensing** \u2014 exploits sparsity for better compression\n",
|
| 1531 |
+
"8. **Spherical Harmonics** \u2014 angular frequencies reveal class structure\n",
|
| 1532 |
+
"\n",
|
| 1533 |
+
"### Architecture Insights\n",
|
| 1534 |
+
"- **Constellation + Triangulation** acts as learned quantization on the sphere\n",
|
| 1535 |
+
"- Low-rank embeddings (32-128d) sufficient for CIFAR-10 classification\n",
|
| 1536 |
+
"- Hypersphere structure provides natural regularization\n",
|
| 1537 |
+
"- TensorBoard visualizations (embeddings, confusion matrices) guide model refinement\n",
|
| 1538 |
+
"\n",
|
| 1539 |
+
"### Next Steps (Category 4)\n",
|
| 1540 |
+
"- Learned projections (end-to-end optimization)\n",
|
| 1541 |
+
"- Adversarial robustness on the sphere\n",
|
| 1542 |
+
"- Cross-modal hypersphere geometry (vision + language)\n",
|
| 1543 |
+
""
|
| 1544 |
+
]
|
| 1545 |
+
}
|
| 1546 |
+
]
|
| 1547 |
+
}
|
spectral/experiment_4_invertible_transforms.ipynb
ADDED
|
@@ -0,0 +1,2013 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 5,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"kernelspec": {
|
| 6 |
+
"display_name": "Python 3",
|
| 7 |
+
"language": "python",
|
| 8 |
+
"name": "python3"
|
| 9 |
+
},
|
| 10 |
+
"language_info": {
|
| 11 |
+
"name": "python",
|
| 12 |
+
"version": "3.10.0"
|
| 13 |
+
},
|
| 14 |
+
"accelerator": "GPU",
|
| 15 |
+
"colab": {
|
| 16 |
+
"provenance": [],
|
| 17 |
+
"gpuType": "T4"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"cells": [
|
| 21 |
+
{
|
| 22 |
+
"cell_type": "markdown",
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"source": [
|
| 25 |
+
"# GeoLIP Hypersphere Experiments \u2014 Category 4\n",
|
| 26 |
+
"## Invertible Geometric Transforms on the Sphere\n",
|
| 27 |
+
"\n",
|
| 28 |
+
"This notebook explores advanced geometric operations on S^d including:\n",
|
| 29 |
+
"- **4.1a/b**: Stereographic projections and constellation readout\n",
|
| 30 |
+
"- **4.2a/b**: Log map triangulation with parallel transport\n",
|
| 31 |
+
"- **4.3a/b**: Log maps with parallel transport to north pole\n",
|
| 32 |
+
"- **4.4a/b**: M\u00f6bius geometric attention and normalizing flows\n",
|
| 33 |
+
"- **4.5a/b**: Procrustes alignment and polar decomposition\n",
|
| 34 |
+
"\n",
|
| 35 |
+
"All experiments use TensorBoard logging and measure CIFAR-10 classification accuracy.\n",
|
| 36 |
+
""
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"cell_type": "code",
|
| 41 |
+
"metadata": {},
|
| 42 |
+
"source": [
|
| 43 |
+
"!pip install -q torch torchvision kymatio scikit-learn matplotlib tensorboard -q"
|
| 44 |
+
],
|
| 45 |
+
"execution_count": null,
|
| 46 |
+
"outputs": []
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"cell_type": "code",
|
| 50 |
+
"metadata": {},
|
| 51 |
+
"source": [
|
| 52 |
+
"\n",
|
| 53 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 54 |
+
"# SHARED UTILITIES \u2014 GeoLIP Hypersphere Experiments\n",
|
| 55 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 56 |
+
"import os, sys, time, math, json, datetime, warnings\n",
|
| 57 |
+
"import numpy as np\n",
|
| 58 |
+
"import torch\n",
|
| 59 |
+
"import torch.nn as nn\n",
|
| 60 |
+
"import torch.nn.functional as F\n",
|
| 61 |
+
"from torch.utils.data import DataLoader\n",
|
| 62 |
+
"from torch.utils.tensorboard import SummaryWriter\n",
|
| 63 |
+
"import torchvision\n",
|
| 64 |
+
"import torchvision.transforms as T\n",
|
| 65 |
+
"import matplotlib\n",
|
| 66 |
+
"matplotlib.use(\"Agg\")\n",
|
| 67 |
+
"import matplotlib.pyplot as plt\n",
|
| 68 |
+
"from collections import defaultdict\n",
|
| 69 |
+
"\n",
|
| 70 |
+
"warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
|
| 71 |
+
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 72 |
+
"print(f\"[DEVICE] {device}\" + (f\" \u2014 {torch.cuda.get_device_name()}\" if device.type == \"cuda\" else \"\"))\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"# \u2500\u2500 CIFAR-10 Loaders \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 75 |
+
"CIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\n",
|
| 76 |
+
"CIFAR10_STD = (0.2470, 0.2435, 0.2616)\n",
|
| 77 |
+
"CIFAR10_CLASSES = (\"plane\",\"car\",\"bird\",\"cat\",\"deer\",\"dog\",\"frog\",\"horse\",\"ship\",\"truck\")\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"def get_cifar10_loaders(batch_size=256, num_workers=2, augment=True):\n",
|
| 80 |
+
" \"\"\"Return train_loader, val_loader for CIFAR-10.\"\"\"\n",
|
| 81 |
+
" tf_train = T.Compose([\n",
|
| 82 |
+
" T.RandomCrop(32, padding=4),\n",
|
| 83 |
+
" T.RandomHorizontalFlip(),\n",
|
| 84 |
+
" T.ToTensor(),\n",
|
| 85 |
+
" T.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n",
|
| 86 |
+
" ]) if augment else T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 87 |
+
" tf_val = T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 88 |
+
"\n",
|
| 89 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf_train)\n",
|
| 90 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf_val)\n",
|
| 91 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 92 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 93 |
+
" print(f\"[DATA] CIFAR-10: {len(train_ds)} train, {len(val_ds)} val, batch_size={batch_size}\")\n",
|
| 94 |
+
" return train_loader, val_loader\n",
|
| 95 |
+
"\n",
|
| 96 |
+
"def get_cifar10_raw_loaders(batch_size=256, num_workers=2):\n",
|
| 97 |
+
" \"\"\"Return loaders with only ToTensor (no normalization) for custom front-ends.\"\"\"\n",
|
| 98 |
+
" tf = T.Compose([T.ToTensor()])\n",
|
| 99 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf)\n",
|
| 100 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf)\n",
|
| 101 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 102 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 103 |
+
" print(f\"[DATA] CIFAR-10 (raw): {len(train_ds)} train, {len(val_ds)} val\")\n",
|
| 104 |
+
" return train_loader, val_loader\n",
|
| 105 |
+
"\n",
|
| 106 |
+
"# \u2500\u2500 L2 Normalization to Hypersphere \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 107 |
+
"def l2_normalize(x, dim=-1, eps=1e-8):\n",
|
| 108 |
+
" \"\"\"Project to unit hypersphere S^{d-1}.\"\"\"\n",
|
| 109 |
+
" return F.normalize(x, p=2, dim=dim, eps=eps)\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"# \u2500\u2500 Constellation Module \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 112 |
+
"class Constellation(nn.Module):\n",
|
| 113 |
+
" \"\"\"\n",
|
| 114 |
+
" K anchors on S^{d-1}. Triangulation: compute distance from input to each anchor.\n",
|
| 115 |
+
" Output: K-dim vector of (1 - cos_sim) distances = triangulation profile.\n",
|
| 116 |
+
" \"\"\"\n",
|
| 117 |
+
" def __init__(self, embed_dim, num_anchors=64, init=\"uniform\"):\n",
|
| 118 |
+
" super().__init__()\n",
|
| 119 |
+
" self.embed_dim = embed_dim\n",
|
| 120 |
+
" self.num_anchors = num_anchors\n",
|
| 121 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, init)\n",
|
| 122 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False) # anchors are NOT learned\n",
|
| 123 |
+
"\n",
|
| 124 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 125 |
+
" if method == \"uniform\":\n",
|
| 126 |
+
" # QR-based uniform hypersphere initialization\n",
|
| 127 |
+
" A = torch.randn(K, d)\n",
|
| 128 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 129 |
+
" anchors = Q.T[:K]\n",
|
| 130 |
+
" # If K > d, fill remaining with random unit vectors\n",
|
| 131 |
+
" if K > d:\n",
|
| 132 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 133 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 134 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 135 |
+
" elif method == \"random\":\n",
|
| 136 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 137 |
+
" else:\n",
|
| 138 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 139 |
+
"\n",
|
| 140 |
+
" def forward(self, x):\n",
|
| 141 |
+
" \"\"\"x: (B, d) on S^{d-1} \u2192 (B, K) triangulation distances.\"\"\"\n",
|
| 142 |
+
" x_norm = l2_normalize(x)\n",
|
| 143 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T) # (B, K)\n",
|
| 144 |
+
" distances = 1.0 - cos_sim # in [0, 2]\n",
|
| 145 |
+
" return distances\n",
|
| 146 |
+
"\n",
|
| 147 |
+
" def push_anchors(self, embeddings, labels, num_classes=10, momentum=0.1):\n",
|
| 148 |
+
" \"\"\"Push anchors toward class centroids (call periodically).\"\"\"\n",
|
| 149 |
+
" with torch.no_grad():\n",
|
| 150 |
+
" for c in range(num_classes):\n",
|
| 151 |
+
" mask = (labels == c)\n",
|
| 152 |
+
" if mask.sum() < 2:\n",
|
| 153 |
+
" continue\n",
|
| 154 |
+
" centroid = l2_normalize(embeddings[mask].mean(dim=0, keepdim=True))\n",
|
| 155 |
+
" # Assign anchors to this class based on nearest centroid\n",
|
| 156 |
+
" start = c * (self.num_anchors // num_classes)\n",
|
| 157 |
+
" end = start + (self.num_anchors // num_classes)\n",
|
| 158 |
+
" self.anchors.data[start:end] = l2_normalize(\n",
|
| 159 |
+
" (1 - momentum) * self.anchors.data[start:end] + momentum * centroid\n",
|
| 160 |
+
" )\n",
|
| 161 |
+
"\n",
|
| 162 |
+
" def anchor_activation(self, x):\n",
|
| 163 |
+
" \"\"\"Return fraction of anchors that are 'active' (nearest to at least one sample).\"\"\"\n",
|
| 164 |
+
" x_norm = l2_normalize(x)\n",
|
| 165 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 166 |
+
" nearest = cos_sim.argmax(dim=1)\n",
|
| 167 |
+
" active = len(nearest.unique())\n",
|
| 168 |
+
" return active, self.num_anchors\n",
|
| 169 |
+
"\n",
|
| 170 |
+
"# \u2500\u2500 Patchwork Block \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 171 |
+
"class SquaredReLU(nn.Module):\n",
|
| 172 |
+
" def forward(self, x):\n",
|
| 173 |
+
" return F.relu(x) ** 2\n",
|
| 174 |
+
"\n",
|
| 175 |
+
"class PatchworkBlock(nn.Module):\n",
|
| 176 |
+
" \"\"\"SquaredReLU + LayerNorm block for post-constellation processing.\"\"\"\n",
|
| 177 |
+
" def __init__(self, dim):\n",
|
| 178 |
+
" super().__init__()\n",
|
| 179 |
+
" self.net = nn.Sequential(\n",
|
| 180 |
+
" nn.Linear(dim, dim),\n",
|
| 181 |
+
" SquaredReLU(),\n",
|
| 182 |
+
" nn.LayerNorm(dim),\n",
|
| 183 |
+
" nn.Linear(dim, dim),\n",
|
| 184 |
+
" SquaredReLU(),\n",
|
| 185 |
+
" nn.LayerNorm(dim),\n",
|
| 186 |
+
" )\n",
|
| 187 |
+
" def forward(self, x):\n",
|
| 188 |
+
" return x + self.net(x) # residual\n",
|
| 189 |
+
"\n",
|
| 190 |
+
"class Patchwork(nn.Module):\n",
|
| 191 |
+
" \"\"\"Stack of PatchworkBlocks.\"\"\"\n",
|
| 192 |
+
" def __init__(self, dim, num_blocks=2):\n",
|
| 193 |
+
" super().__init__()\n",
|
| 194 |
+
" self.blocks = nn.Sequential(*[PatchworkBlock(dim) for _ in range(num_blocks)])\n",
|
| 195 |
+
" def forward(self, x):\n",
|
| 196 |
+
" return self.blocks(x)\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"# \u2500\u2500 Classifier Head \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 199 |
+
"class ConstellationClassifier(nn.Module):\n",
|
| 200 |
+
" \"\"\"Full pipeline: encoder output \u2192 constellation \u2192 patchwork \u2192 classify.\"\"\"\n",
|
| 201 |
+
" def __init__(self, embed_dim, num_anchors=64, num_classes=10, patchwork_blocks=2):\n",
|
| 202 |
+
" super().__init__()\n",
|
| 203 |
+
" self.constellation = Constellation(embed_dim, num_anchors)\n",
|
| 204 |
+
" self.patchwork = Patchwork(num_anchors, patchwork_blocks)\n",
|
| 205 |
+
" self.head = nn.Linear(num_anchors, num_classes)\n",
|
| 206 |
+
"\n",
|
| 207 |
+
" def forward(self, embeddings):\n",
|
| 208 |
+
" tri = self.constellation(embeddings) # (B, K)\n",
|
| 209 |
+
" pw = self.patchwork(tri) # (B, K)\n",
|
| 210 |
+
" logits = self.head(pw) # (B, 10)\n",
|
| 211 |
+
" return logits, tri\n",
|
| 212 |
+
"\n",
|
| 213 |
+
" def count_params(self):\n",
|
| 214 |
+
" return sum(p.numel() for p in self.parameters() if p.requires_grad)\n",
|
| 215 |
+
"\n",
|
| 216 |
+
"# \u2500\u2500 Losses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 217 |
+
"class CayleyMengerVolumeLoss(nn.Module):\n",
|
| 218 |
+
" \"\"\"CV loss: encourage constellation volume in 0.18-0.25 band.\"\"\"\n",
|
| 219 |
+
" def __init__(self, target_low=0.18, target_high=0.25):\n",
|
| 220 |
+
" super().__init__()\n",
|
| 221 |
+
" self.target_low = target_low\n",
|
| 222 |
+
" self.target_high = target_high\n",
|
| 223 |
+
"\n",
|
| 224 |
+
" def forward(self, tri):\n",
|
| 225 |
+
" \"\"\"tri: (B, K) triangulation distances.\"\"\"\n",
|
| 226 |
+
" # Approximate volume via determinant of distance submatrix\n",
|
| 227 |
+
" B, K = tri.shape\n",
|
| 228 |
+
" # Use std of triangulation as proxy for volume spread\n",
|
| 229 |
+
" vol = tri.std(dim=1).mean()\n",
|
| 230 |
+
" # Penalty if outside target band\n",
|
| 231 |
+
" below = F.relu(self.target_low - vol)\n",
|
| 232 |
+
" above = F.relu(vol - self.target_high)\n",
|
| 233 |
+
" return below + above, vol.item()\n",
|
| 234 |
+
"\n",
|
| 235 |
+
"class InfoNCELoss(nn.Module):\n",
|
| 236 |
+
" \"\"\"Self-supervised contrastive loss on augmented views.\"\"\"\n",
|
| 237 |
+
" def __init__(self, temperature=0.07):\n",
|
| 238 |
+
" super().__init__()\n",
|
| 239 |
+
" self.temperature = temperature\n",
|
| 240 |
+
"\n",
|
| 241 |
+
" def forward(self, z1, z2):\n",
|
| 242 |
+
" \"\"\"z1, z2: (B, d) L2-normalized embeddings of two augmented views.\"\"\"\n",
|
| 243 |
+
" z1 = l2_normalize(z1)\n",
|
| 244 |
+
" z2 = l2_normalize(z2)\n",
|
| 245 |
+
" B = z1.shape[0]\n",
|
| 246 |
+
" z = torch.cat([z1, z2], dim=0) # (2B, d)\n",
|
| 247 |
+
" sim = torch.mm(z, z.T) / self.temperature # (2B, 2B)\n",
|
| 248 |
+
" # Mask out self-similarity\n",
|
| 249 |
+
" mask = ~torch.eye(2*B, dtype=bool, device=z.device)\n",
|
| 250 |
+
" sim = sim.masked_fill(~mask, -1e9)\n",
|
| 251 |
+
" # Positive pairs: (i, i+B) and (i+B, i)\n",
|
| 252 |
+
" labels = torch.cat([torch.arange(B, 2*B), torch.arange(B)]).to(z.device)\n",
|
| 253 |
+
" loss = F.cross_entropy(sim, labels)\n",
|
| 254 |
+
" return loss\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"class ClassConditionalInfoNCE(nn.Module):\n",
|
| 257 |
+
" \"\"\"Class-conditional contrastive: positive = same class, not same image.\"\"\"\n",
|
| 258 |
+
" def __init__(self, temperature=0.07):\n",
|
| 259 |
+
" super().__init__()\n",
|
| 260 |
+
" self.temperature = temperature\n",
|
| 261 |
+
"\n",
|
| 262 |
+
" def forward(self, z, labels):\n",
|
| 263 |
+
" z = l2_normalize(z)\n",
|
| 264 |
+
" B = z.shape[0]\n",
|
| 265 |
+
" sim = torch.mm(z, z.T) / self.temperature\n",
|
| 266 |
+
" # Mask: positive if same class, negative otherwise\n",
|
| 267 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 268 |
+
" label_mask.fill_diagonal_(0) # exclude self\n",
|
| 269 |
+
" # For each sample, compute log-softmax over all non-self, weighted by positives\n",
|
| 270 |
+
" logits = sim - torch.diag(torch.full((B,), 1e9, device=z.device))\n",
|
| 271 |
+
" pos_mask = label_mask\n",
|
| 272 |
+
" neg_mask = 1 - label_mask\n",
|
| 273 |
+
" neg_mask.fill_diagonal_(0)\n",
|
| 274 |
+
" # InfoNCE: -log(sum_pos exp(sim) / sum_all exp(sim))\n",
|
| 275 |
+
" exp_sim = torch.exp(logits)\n",
|
| 276 |
+
" pos_sum = (exp_sim * pos_mask).sum(dim=1)\n",
|
| 277 |
+
" all_sum = (exp_sim * (pos_mask + neg_mask)).sum(dim=1)\n",
|
| 278 |
+
" loss = -torch.log(pos_sum / (all_sum + 1e-8) + 1e-8).mean()\n",
|
| 279 |
+
" return loss\n",
|
| 280 |
+
"\n",
|
| 281 |
+
"# \u2500\u2500 TensorBoard Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 282 |
+
"def make_writer(experiment_id, base_dir=\"runs\"):\n",
|
| 283 |
+
" \"\"\"Create a TensorBoard SummaryWriter with timestamped run directory.\"\"\"\n",
|
| 284 |
+
" timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
|
| 285 |
+
" log_dir = os.path.join(base_dir, f\"{experiment_id}_{timestamp}\")\n",
|
| 286 |
+
" writer = SummaryWriter(log_dir=log_dir)\n",
|
| 287 |
+
" print(f\"[TB] Logging to {log_dir}\")\n",
|
| 288 |
+
" return writer\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"def log_epoch_metrics(writer, epoch, metrics_dict, prefix=\"\"):\n",
|
| 291 |
+
" \"\"\"Log a dict of metrics to TensorBoard.\"\"\"\n",
|
| 292 |
+
" for key, val in metrics_dict.items():\n",
|
| 293 |
+
" tag = f\"{prefix}/{key}\" if prefix else key\n",
|
| 294 |
+
" writer.add_scalar(tag, val, epoch)\n",
|
| 295 |
+
"\n",
|
| 296 |
+
"def log_embedding_viz(writer, embeddings, labels, epoch, tag=\"embeddings\"):\n",
|
| 297 |
+
" \"\"\"Log embeddings for TensorBoard projector.\"\"\"\n",
|
| 298 |
+
" meta = [CIFAR10_CLASSES[l] for l in labels]\n",
|
| 299 |
+
" writer.add_embedding(embeddings, metadata=meta, global_step=epoch, tag=tag)\n",
|
| 300 |
+
"\n",
|
| 301 |
+
"def log_anchor_histogram(writer, constellation, epoch):\n",
|
| 302 |
+
" \"\"\"Log anchor norm distribution.\"\"\"\n",
|
| 303 |
+
" norms = constellation.anchors.data.norm(dim=1)\n",
|
| 304 |
+
" writer.add_histogram(\"anchors/norms\", norms, epoch)\n",
|
| 305 |
+
" cos_sim_matrix = torch.mm(constellation.anchors.data, constellation.anchors.data.T)\n",
|
| 306 |
+
" off_diag = cos_sim_matrix[~torch.eye(cos_sim_matrix.shape[0], dtype=bool)].flatten()\n",
|
| 307 |
+
" writer.add_histogram(\"anchors/inter_cosine_sim\", off_diag, epoch)\n",
|
| 308 |
+
"\n",
|
| 309 |
+
"def log_confusion_matrix(writer, all_preds, all_labels, epoch):\n",
|
| 310 |
+
" \"\"\"Log confusion matrix as image to TensorBoard.\"\"\"\n",
|
| 311 |
+
" from sklearn.metrics import confusion_matrix\n",
|
| 312 |
+
" cm = confusion_matrix(all_labels, all_preds, labels=range(10))\n",
|
| 313 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 314 |
+
" im = ax.imshow(cm, interpolation=\"nearest\", cmap=plt.cm.Blues)\n",
|
| 315 |
+
" ax.set_title(f\"Confusion Matrix \u2014 Epoch {epoch}\")\n",
|
| 316 |
+
" ax.set_xlabel(\"Predicted\")\n",
|
| 317 |
+
" ax.set_ylabel(\"True\")\n",
|
| 318 |
+
" tick_marks = range(10)\n",
|
| 319 |
+
" ax.set_xticks(tick_marks)\n",
|
| 320 |
+
" ax.set_xticklabels(CIFAR10_CLASSES, rotation=45, ha=\"right\")\n",
|
| 321 |
+
" ax.set_yticks(tick_marks)\n",
|
| 322 |
+
" ax.set_yticklabels(CIFAR10_CLASSES)\n",
|
| 323 |
+
" for i in range(10):\n",
|
| 324 |
+
" for j in range(10):\n",
|
| 325 |
+
" ax.text(j, i, str(cm[i, j]), ha=\"center\", va=\"center\",\n",
|
| 326 |
+
" color=\"white\" if cm[i, j] > cm.max() / 2 else \"black\", fontsize=7)\n",
|
| 327 |
+
" fig.tight_layout()\n",
|
| 328 |
+
" writer.add_figure(\"confusion_matrix\", fig, epoch)\n",
|
| 329 |
+
" plt.close(fig)\n",
|
| 330 |
+
"\n",
|
| 331 |
+
"def log_triangulation_stats(writer, tri, epoch):\n",
|
| 332 |
+
" \"\"\"Log triangulation distance statistics.\"\"\"\n",
|
| 333 |
+
" writer.add_scalar(\"tri/mean\", tri.mean().item(), epoch)\n",
|
| 334 |
+
" writer.add_scalar(\"tri/std\", tri.std().item(), epoch)\n",
|
| 335 |
+
" writer.add_scalar(\"tri/min\", tri.min().item(), epoch)\n",
|
| 336 |
+
" writer.add_scalar(\"tri/max\", tri.max().item(), epoch)\n",
|
| 337 |
+
" writer.add_histogram(\"tri/distances\", tri.detach().cpu(), epoch)\n",
|
| 338 |
+
"\n",
|
| 339 |
+
"def log_scatter_2d(writer, embeddings, labels, epoch, tag=\"scatter_2d\", method=\"pca\"):\n",
|
| 340 |
+
" \"\"\"Log 2D scatter plot of embeddings projected via PCA or t-SNE.\"\"\"\n",
|
| 341 |
+
" emb_np = embeddings.detach().cpu().numpy()\n",
|
| 342 |
+
" lab_np = labels.detach().cpu().numpy() if torch.is_tensor(labels) else np.array(labels)\n",
|
| 343 |
+
" if method == \"pca\":\n",
|
| 344 |
+
" from sklearn.decomposition import PCA\n",
|
| 345 |
+
" proj = PCA(n_components=2).fit_transform(emb_np)\n",
|
| 346 |
+
" else:\n",
|
| 347 |
+
" from sklearn.manifold import TSNE\n",
|
| 348 |
+
" proj = TSNE(n_components=2, perplexity=30, n_iter=500).fit_transform(emb_np)\n",
|
| 349 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 350 |
+
" for c in range(10):\n",
|
| 351 |
+
" mask = lab_np == c\n",
|
| 352 |
+
" ax.scatter(proj[mask, 0], proj[mask, 1], s=3, alpha=0.5, label=CIFAR10_CLASSES[c])\n",
|
| 353 |
+
" ax.legend(markerscale=5, fontsize=8)\n",
|
| 354 |
+
" ax.set_title(f\"{tag} ({method.upper()}) \u2014 Epoch {epoch}\")\n",
|
| 355 |
+
" fig.tight_layout()\n",
|
| 356 |
+
" writer.add_figure(f\"{tag}/{method}\", fig, epoch)\n",
|
| 357 |
+
" plt.close(fig)\n",
|
| 358 |
+
"\n",
|
| 359 |
+
"# \u2500\u2500 Training Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 360 |
+
"def train_one_epoch(model, loader, optimizer, criterion, device, epoch=0,\n",
|
| 361 |
+
" scheduler=None, extra_losses=None):\n",
|
| 362 |
+
" \"\"\"\n",
|
| 363 |
+
" Generic training loop for one epoch.\n",
|
| 364 |
+
" model: callable(batch) \u2192 (logits, embeddings_on_sphere)\n",
|
| 365 |
+
" extra_losses: list of (name, loss_fn, weight) where loss_fn(embeddings) \u2192 scalar\n",
|
| 366 |
+
" \"\"\"\n",
|
| 367 |
+
" model.train()\n",
|
| 368 |
+
" total_loss = 0\n",
|
| 369 |
+
" total_ce = 0\n",
|
| 370 |
+
" correct = 0\n",
|
| 371 |
+
" total = 0\n",
|
| 372 |
+
" loss_components = defaultdict(float)\n",
|
| 373 |
+
"\n",
|
| 374 |
+
" for batch_idx, (images, labels) in enumerate(loader):\n",
|
| 375 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 376 |
+
" optimizer.zero_grad()\n",
|
| 377 |
+
"\n",
|
| 378 |
+
" logits, embeddings = model(images)\n",
|
| 379 |
+
" ce_loss = criterion(logits, labels)\n",
|
| 380 |
+
" loss = ce_loss\n",
|
| 381 |
+
" loss_components[\"CE\"] += ce_loss.item()\n",
|
| 382 |
+
"\n",
|
| 383 |
+
" if extra_losses:\n",
|
| 384 |
+
" for name, loss_fn, weight in extra_losses:\n",
|
| 385 |
+
" if name == \"CV\":\n",
|
| 386 |
+
" el, vol_val = loss_fn(embeddings)\n",
|
| 387 |
+
" loss_components[\"CV_vol\"] += vol_val\n",
|
| 388 |
+
" elif name == \"InfoNCE_class\":\n",
|
| 389 |
+
" el = loss_fn(embeddings, labels)\n",
|
| 390 |
+
" else:\n",
|
| 391 |
+
" el = loss_fn(embeddings)\n",
|
| 392 |
+
" loss = loss + weight * el\n",
|
| 393 |
+
" loss_components[name] += el.item()\n",
|
| 394 |
+
"\n",
|
| 395 |
+
" loss.backward()\n",
|
| 396 |
+
" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n",
|
| 397 |
+
" optimizer.step()\n",
|
| 398 |
+
" if scheduler:\n",
|
| 399 |
+
" scheduler.step()\n",
|
| 400 |
+
"\n",
|
| 401 |
+
" total_loss += loss.item()\n",
|
| 402 |
+
" _, predicted = logits.max(1)\n",
|
| 403 |
+
" total += labels.size(0)\n",
|
| 404 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 405 |
+
"\n",
|
| 406 |
+
" n_batches = len(loader)\n",
|
| 407 |
+
" metrics = {\n",
|
| 408 |
+
" \"loss/total\": total_loss / n_batches,\n",
|
| 409 |
+
" \"loss/CE\": loss_components[\"CE\"] / n_batches,\n",
|
| 410 |
+
" \"accuracy/train\": 100.0 * correct / total,\n",
|
| 411 |
+
" }\n",
|
| 412 |
+
" for k, v in loss_components.items():\n",
|
| 413 |
+
" if k not in (\"CE\",):\n",
|
| 414 |
+
" metrics[f\"loss/{k}\"] = v / n_batches\n",
|
| 415 |
+
" return metrics\n",
|
| 416 |
+
"\n",
|
| 417 |
+
"@torch.no_grad()\n",
|
| 418 |
+
"def evaluate(model, loader, criterion, device, collect_embeddings=False):\n",
|
| 419 |
+
" \"\"\"Evaluate model on validation set. Optionally collect embeddings.\"\"\"\n",
|
| 420 |
+
" model.eval()\n",
|
| 421 |
+
" total_loss = 0\n",
|
| 422 |
+
" correct = 0\n",
|
| 423 |
+
" total = 0\n",
|
| 424 |
+
" all_preds = []\n",
|
| 425 |
+
" all_labels = []\n",
|
| 426 |
+
" all_embeddings = []\n",
|
| 427 |
+
"\n",
|
| 428 |
+
" for images, labels in loader:\n",
|
| 429 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 430 |
+
" logits, embeddings = model(images)\n",
|
| 431 |
+
" loss = criterion(logits, labels)\n",
|
| 432 |
+
" total_loss += loss.item()\n",
|
| 433 |
+
" _, predicted = logits.max(1)\n",
|
| 434 |
+
" total += labels.size(0)\n",
|
| 435 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 436 |
+
" all_preds.extend(predicted.cpu().numpy())\n",
|
| 437 |
+
" all_labels.extend(labels.cpu().numpy())\n",
|
| 438 |
+
" if collect_embeddings:\n",
|
| 439 |
+
" all_embeddings.append(embeddings.cpu())\n",
|
| 440 |
+
"\n",
|
| 441 |
+
" n_batches = len(loader)\n",
|
| 442 |
+
" metrics = {\n",
|
| 443 |
+
" \"loss/val\": total_loss / n_batches,\n",
|
| 444 |
+
" \"accuracy/val\": 100.0 * correct / total,\n",
|
| 445 |
+
" }\n",
|
| 446 |
+
" result = {\"metrics\": metrics, \"preds\": np.array(all_preds), \"labels\": np.array(all_labels)}\n",
|
| 447 |
+
" if collect_embeddings:\n",
|
| 448 |
+
" result[\"embeddings\"] = torch.cat(all_embeddings, dim=0)\n",
|
| 449 |
+
" return result\n",
|
| 450 |
+
"\n",
|
| 451 |
+
"def run_experiment(experiment_id, model, train_loader, val_loader, device,\n",
|
| 452 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, extra_losses=None,\n",
|
| 453 |
+
" push_anchors_every=50, log_embeddings_every=25,\n",
|
| 454 |
+
" patience=20):\n",
|
| 455 |
+
" \"\"\"\n",
|
| 456 |
+
" Full experiment runner with TensorBoard logging, early stopping, and diagnostics.\n",
|
| 457 |
+
" model must return (logits, sphere_embeddings).\n",
|
| 458 |
+
" \"\"\"\n",
|
| 459 |
+
" writer = make_writer(experiment_id)\n",
|
| 460 |
+
" criterion = nn.CrossEntropyLoss()\n",
|
| 461 |
+
" optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)\n",
|
| 462 |
+
" scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\n",
|
| 463 |
+
"\n",
|
| 464 |
+
" best_val_acc = 0.0\n",
|
| 465 |
+
" best_epoch = 0\n",
|
| 466 |
+
" no_improve = 0\n",
|
| 467 |
+
"\n",
|
| 468 |
+
" # Log model info\n",
|
| 469 |
+
" n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 470 |
+
" writer.add_text(\"config/params\", f\"Learned params: {n_params:,}\")\n",
|
| 471 |
+
" writer.add_text(\"config/experiment\", experiment_id)\n",
|
| 472 |
+
" print(f\"\\n{'='*70}\")\n",
|
| 473 |
+
" print(f\"[EXP] {experiment_id}\")\n",
|
| 474 |
+
" print(f\"[EXP] Learned params: {n_params:,}, Epochs: {epochs}, LR: {lr}\")\n",
|
| 475 |
+
" print(f\"{'='*70}\")\n",
|
| 476 |
+
"\n",
|
| 477 |
+
" for epoch in range(1, epochs + 1):\n",
|
| 478 |
+
" t0 = time.time()\n",
|
| 479 |
+
"\n",
|
| 480 |
+
" # Train\n",
|
| 481 |
+
" train_metrics = train_one_epoch(\n",
|
| 482 |
+
" model, train_loader, optimizer, criterion, device, epoch,\n",
|
| 483 |
+
" extra_losses=extra_losses\n",
|
| 484 |
+
" )\n",
|
| 485 |
+
"\n",
|
| 486 |
+
" # Evaluate\n",
|
| 487 |
+
" collect_emb = (epoch % log_embeddings_every == 0) or (epoch == epochs)\n",
|
| 488 |
+
" val_result = evaluate(model, val_loader, criterion, device, collect_embeddings=collect_emb)\n",
|
| 489 |
+
" val_metrics = val_result[\"metrics\"]\n",
|
| 490 |
+
"\n",
|
| 491 |
+
" # Combine metrics\n",
|
| 492 |
+
" all_metrics = {**train_metrics, **val_metrics}\n",
|
| 493 |
+
" all_metrics[\"lr\"] = optimizer.param_groups[0][\"lr\"]\n",
|
| 494 |
+
" all_metrics[\"epoch_time\"] = time.time() - t0\n",
|
| 495 |
+
" all_metrics[\"train_val_gap\"] = train_metrics[\"accuracy/train\"] - val_metrics[\"accuracy/val\"]\n",
|
| 496 |
+
"\n",
|
| 497 |
+
" # Constellation diagnostics\n",
|
| 498 |
+
" if hasattr(model, \"constellation\"):\n",
|
| 499 |
+
" constellation = model.constellation\n",
|
| 500 |
+
" elif hasattr(model, \"classifier\") and hasattr(model.classifier, \"constellation\"):\n",
|
| 501 |
+
" constellation = model.classifier.constellation\n",
|
| 502 |
+
" else:\n",
|
| 503 |
+
" constellation = None\n",
|
| 504 |
+
"\n",
|
| 505 |
+
" if constellation is not None:\n",
|
| 506 |
+
" log_anchor_histogram(writer, constellation, epoch)\n",
|
| 507 |
+
"\n",
|
| 508 |
+
" # Push anchors periodically\n",
|
| 509 |
+
" if epoch % push_anchors_every == 0 and \"embeddings\" in val_result:\n",
|
| 510 |
+
" emb = val_result[\"embeddings\"][:2000].to(device)\n",
|
| 511 |
+
" lab = torch.tensor(val_result[\"labels\"][:2000]).to(device)\n",
|
| 512 |
+
" constellation.push_anchors(emb, lab)\n",
|
| 513 |
+
" active, total_anch = constellation.anchor_activation(emb)\n",
|
| 514 |
+
" all_metrics[\"anchors/active\"] = active\n",
|
| 515 |
+
" all_metrics[\"anchors/total\"] = total_anch\n",
|
| 516 |
+
" all_metrics[\"anchors/utilization\"] = active / total_anch\n",
|
| 517 |
+
" print(f\" [ANCHOR PUSH] Active: {active}/{total_anch} ({100*active/total_anch:.1f}%)\")\n",
|
| 518 |
+
"\n",
|
| 519 |
+
" # Log all metrics\n",
|
| 520 |
+
" log_epoch_metrics(writer, epoch, all_metrics)\n",
|
| 521 |
+
" scheduler.step()\n",
|
| 522 |
+
"\n",
|
| 523 |
+
" # Log embeddings visualization\n",
|
| 524 |
+
" if collect_emb and \"embeddings\" in val_result:\n",
|
| 525 |
+
" emb = val_result[\"embeddings\"][:2000]\n",
|
| 526 |
+
" lab = val_result[\"labels\"][:2000]\n",
|
| 527 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings\", method=\"pca\")\n",
|
| 528 |
+
" if epoch == epochs:\n",
|
| 529 |
+
" log_confusion_matrix(writer, val_result[\"preds\"], val_result[\"labels\"], epoch)\n",
|
| 530 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings_tsne\", method=\"tsne\")\n",
|
| 531 |
+
"\n",
|
| 532 |
+
" # Log triangulation stats from a sample batch\n",
|
| 533 |
+
" if constellation is not None and collect_emb and \"embeddings\" in val_result:\n",
|
| 534 |
+
" sample_emb = val_result[\"embeddings\"][:500].to(device)\n",
|
| 535 |
+
" tri_sample = constellation(sample_emb)\n",
|
| 536 |
+
" log_triangulation_stats(writer, tri_sample, epoch)\n",
|
| 537 |
+
"\n",
|
| 538 |
+
" # Best model tracking\n",
|
| 539 |
+
" val_acc = val_metrics[\"accuracy/val\"]\n",
|
| 540 |
+
" if val_acc > best_val_acc:\n",
|
| 541 |
+
" best_val_acc = val_acc\n",
|
| 542 |
+
" best_epoch = epoch\n",
|
| 543 |
+
" no_improve = 0\n",
|
| 544 |
+
" writer.add_scalar(\"best/val_acc\", best_val_acc, epoch)\n",
|
| 545 |
+
" writer.add_scalar(\"best/epoch\", best_epoch, epoch)\n",
|
| 546 |
+
" else:\n",
|
| 547 |
+
" no_improve += 1\n",
|
| 548 |
+
"\n",
|
| 549 |
+
" # Print progress\n",
|
| 550 |
+
" if epoch % 5 == 0 or epoch == 1 or epoch == epochs:\n",
|
| 551 |
+
" print(f\" E{epoch:3d} | TrainAcc {train_metrics['accuracy/train']:5.1f}% | \"\n",
|
| 552 |
+
" f\"ValAcc {val_acc:5.1f}% | Loss {all_metrics['loss/total']:.4f} | \"\n",
|
| 553 |
+
" f\"Best {best_val_acc:.1f}% @E{best_epoch} | \"\n",
|
| 554 |
+
" f\"Gap {all_metrics['train_val_gap']:+.1f}% | \"\n",
|
| 555 |
+
" f\"{all_metrics['epoch_time']:.1f}s\")\n",
|
| 556 |
+
"\n",
|
| 557 |
+
" # Early stopping\n",
|
| 558 |
+
" if no_improve >= patience:\n",
|
| 559 |
+
" print(f\" [EARLY STOP] No improvement for {patience} epochs. Best: {best_val_acc:.1f}% @E{best_epoch}\")\n",
|
| 560 |
+
" break\n",
|
| 561 |
+
"\n",
|
| 562 |
+
" writer.close()\n",
|
| 563 |
+
" print(f\"\\n[RESULT] {experiment_id}: Best Val Acc = {best_val_acc:.2f}% at epoch {best_epoch}\")\n",
|
| 564 |
+
" print(f\"[RESULT] Learned params: {n_params:,}\")\n",
|
| 565 |
+
" return {\"experiment\": experiment_id, \"best_val_acc\": best_val_acc, \"best_epoch\": best_epoch,\n",
|
| 566 |
+
" \"params\": n_params, \"final_train_acc\": train_metrics[\"accuracy/train\"]}\n",
|
| 567 |
+
"\n",
|
| 568 |
+
"# \u2500\u2500 Johnson-Lindenstrauss Projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 569 |
+
"class JLProjection(nn.Module):\n",
|
| 570 |
+
" \"\"\"Fixed random Gaussian projection (not learned).\"\"\"\n",
|
| 571 |
+
" def __init__(self, input_dim, output_dim):\n",
|
| 572 |
+
" super().__init__()\n",
|
| 573 |
+
" self.register_buffer(\"W\", torch.randn(input_dim, output_dim) / math.sqrt(output_dim))\n",
|
| 574 |
+
"\n",
|
| 575 |
+
" def forward(self, x):\n",
|
| 576 |
+
" return x @ self.W\n",
|
| 577 |
+
"\n",
|
| 578 |
+
"# \u2500\u2500 Results Scoreboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 579 |
+
"class Scoreboard:\n",
|
| 580 |
+
" \"\"\"Track results across experiments in a session.\"\"\"\n",
|
| 581 |
+
" def __init__(self):\n",
|
| 582 |
+
" self.results = []\n",
|
| 583 |
+
"\n",
|
| 584 |
+
" def add(self, result_dict):\n",
|
| 585 |
+
" self.results.append(result_dict)\n",
|
| 586 |
+
" self._print_table()\n",
|
| 587 |
+
"\n",
|
| 588 |
+
" def _print_table(self):\n",
|
| 589 |
+
" print(f\"\\n{'='*80}\")\n",
|
| 590 |
+
" print(f\"{'SCOREBOARD':^80}\")\n",
|
| 591 |
+
" print(f\"{'='*80}\")\n",
|
| 592 |
+
" print(f\"{'Experiment':<40} {'Val Acc':>8} {'Params':>10} {'Best E':>7}\")\n",
|
| 593 |
+
" print(f\"{'-'*40} {'-'*8} {'-'*10} {'-'*7}\")\n",
|
| 594 |
+
" for r in sorted(self.results, key=lambda x: x[\"best_val_acc\"], reverse=True):\n",
|
| 595 |
+
" print(f\"{r['experiment']:<40} {r['best_val_acc']:>7.2f}% {r['params']:>10,} {r['best_epoch']:>7}\")\n",
|
| 596 |
+
" print(f\"{'='*80}\\n\")\n",
|
| 597 |
+
"\n",
|
| 598 |
+
" def save(self, path=\"scoreboard.json\"):\n",
|
| 599 |
+
" with open(path, \"w\") as f:\n",
|
| 600 |
+
" json.dump(self.results, f, indent=2)\n",
|
| 601 |
+
" print(f\"[SCOREBOARD] Saved to {path}\")\n",
|
| 602 |
+
"\n",
|
| 603 |
+
"scoreboard = Scoreboard()\n",
|
| 604 |
+
"print(\"[SHARED] All utilities loaded successfully.\")\n",
|
| 605 |
+
""
|
| 606 |
+
],
|
| 607 |
+
"execution_count": null,
|
| 608 |
+
"outputs": []
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"cell_type": "markdown",
|
| 612 |
+
"metadata": {},
|
| 613 |
+
"source": [
|
| 614 |
+
"## 4.1: Stereographic Projections\n",
|
| 615 |
+
"\n",
|
| 616 |
+
"### 4.1a: Stereographic \u2192 Scattering \u2192 Inverse Stereographic \u2192 S^d\n",
|
| 617 |
+
"\n",
|
| 618 |
+
"**Theory:**\n",
|
| 619 |
+
"- Stereographic projection: \u03c3(x) = x_{1:n}/(1\u2212x_{n+1})\n",
|
| 620 |
+
"- Inverse: \u03c3\u207b\u00b9(y) = (2y, \u2016y\u2016\u00b2\u22121)/(\u2016y\u2016\u00b2+1)\n",
|
| 621 |
+
"- Apply scattering in flat space, then project back to sphere\n",
|
| 622 |
+
""
|
| 623 |
+
]
|
| 624 |
+
},
|
| 625 |
+
{
|
| 626 |
+
"cell_type": "code",
|
| 627 |
+
"metadata": {},
|
| 628 |
+
"source": [
|
| 629 |
+
"\n",
|
| 630 |
+
"# Install kymatio for scattering\n",
|
| 631 |
+
"try:\n",
|
| 632 |
+
" from kymatio.scattering2d import Scattering2D\n",
|
| 633 |
+
"except:\n",
|
| 634 |
+
" import subprocess\n",
|
| 635 |
+
" subprocess.check_call([\"pip\", \"install\", \"-q\", \"kymatio\"])\n",
|
| 636 |
+
" from kymatio.scattering2d import Scattering2D\n",
|
| 637 |
+
"\n",
|
| 638 |
+
"class StereographicProjection(nn.Module):\n",
|
| 639 |
+
" \"\"\"Stereographic projection to/from R^n and S^n.\"\"\"\n",
|
| 640 |
+
" def __init__(self, embed_dim):\n",
|
| 641 |
+
" super().__init__()\n",
|
| 642 |
+
" self.embed_dim = embed_dim\n",
|
| 643 |
+
" self.d = embed_dim - 1 # dimension of base space\n",
|
| 644 |
+
"\n",
|
| 645 |
+
" def to_sphere(self, y):\n",
|
| 646 |
+
" \"\"\"y: (B, d) in R^d \u2192 (B, d+1) on S^d\"\"\"\n",
|
| 647 |
+
" norm_sq = (y ** 2).sum(dim=-1, keepdim=True) # (B, 1)\n",
|
| 648 |
+
" denom = norm_sq + 1.0 + 1e-8\n",
|
| 649 |
+
" x_base = 2 * y / denom # (B, d)\n",
|
| 650 |
+
" x_last = (norm_sq - 1.0) / denom # (B, 1)\n",
|
| 651 |
+
" x = torch.cat([x_base, x_last], dim=-1) # (B, d+1)\n",
|
| 652 |
+
" return l2_normalize(x)\n",
|
| 653 |
+
"\n",
|
| 654 |
+
" def from_sphere(self, x):\n",
|
| 655 |
+
" \"\"\"x: (B, d+1) on S^d \u2192 (B, d) in R^d\"\"\"\n",
|
| 656 |
+
" x = l2_normalize(x)\n",
|
| 657 |
+
" denom = 1.0 - x[..., -1:] + 1e-8 # (B, 1)\n",
|
| 658 |
+
" y = x[..., :-1] / denom # (B, d)\n",
|
| 659 |
+
" return y\n",
|
| 660 |
+
"\n",
|
| 661 |
+
"class StereographicScatteringEncoder(nn.Module):\n",
|
| 662 |
+
" \"\"\"\n",
|
| 663 |
+
" Stereographic projection of images \u2192 2D scattering \u2192 flat space ops \u2192\n",
|
| 664 |
+
" inverse stereographic back to sphere.\n",
|
| 665 |
+
" \"\"\"\n",
|
| 666 |
+
" def __init__(self, embed_dim=256, num_scattering_coeffs=None):\n",
|
| 667 |
+
" super().__init__()\n",
|
| 668 |
+
" self.embed_dim = embed_dim\n",
|
| 669 |
+
" self.stereographic = StereographicProjection(embed_dim)\n",
|
| 670 |
+
"\n",
|
| 671 |
+
" # Scattering for CIFAR-10 (32x32 \u2192 scattering)\n",
|
| 672 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 673 |
+
" # Scattering output: (B, 81, 8, 8) = (B, 81*64) = (B, 5184) features\n",
|
| 674 |
+
" scatt_dim = 81 * 64 # 5184\n",
|
| 675 |
+
"\n",
|
| 676 |
+
" self.scatt_proj = nn.Linear(scatt_dim, embed_dim)\n",
|
| 677 |
+
" self.patchwork_embed = Patchwork(embed_dim, num_blocks=2)\n",
|
| 678 |
+
"\n",
|
| 679 |
+
" def forward(self, images):\n",
|
| 680 |
+
" # Scattering\n",
|
| 681 |
+
" scatt = self.scattering(images) # (B, 81, 8, 8)\n",
|
| 682 |
+
" B = scatt.shape[0]\n",
|
| 683 |
+
" scatt_flat = scatt.reshape(B, -1) # (B, 5184)\n",
|
| 684 |
+
"\n",
|
| 685 |
+
" # Project to embed_dim in flat space\n",
|
| 686 |
+
" flat_embed = self.scatt_proj(scatt_flat) # (B, embed_dim)\n",
|
| 687 |
+
"\n",
|
| 688 |
+
" # Patchwork processing in flat space\n",
|
| 689 |
+
" flat_embed = self.patchwork_embed(flat_embed)\n",
|
| 690 |
+
"\n",
|
| 691 |
+
" # Project to sphere via inverse stereographic\n",
|
| 692 |
+
" sphere_embed = self.stereographic.to_sphere(flat_embed)\n",
|
| 693 |
+
" return sphere_embed\n",
|
| 694 |
+
"\n",
|
| 695 |
+
"# Test instantiation\n",
|
| 696 |
+
"encoder_4_1a = StereographicScatteringEncoder(embed_dim=256)\n",
|
| 697 |
+
"encoder_4_1a = encoder_4_1a.to(device)\n",
|
| 698 |
+
"print(f\"[4.1a] Encoder: {encoder_4_1a}\")\n",
|
| 699 |
+
"\n",
|
| 700 |
+
"# Verify forward pass\n",
|
| 701 |
+
"test_batch = torch.randn(4, 3, 32, 32).to(device)\n",
|
| 702 |
+
"with torch.no_grad():\n",
|
| 703 |
+
" test_out = encoder_4_1a(test_batch)\n",
|
| 704 |
+
" print(f\"[4.1a] Input shape: {test_batch.shape}\")\n",
|
| 705 |
+
" print(f\"[4.1a] Output shape: {test_out.shape}\")\n",
|
| 706 |
+
" print(f\"[4.1a] Output norm: {test_out.norm(dim=-1).mean().item():.6f} (should be \u22481.0)\")\n",
|
| 707 |
+
""
|
| 708 |
+
],
|
| 709 |
+
"execution_count": null,
|
| 710 |
+
"outputs": []
|
| 711 |
+
},
|
| 712 |
+
{
|
| 713 |
+
"cell_type": "markdown",
|
| 714 |
+
"metadata": {},
|
| 715 |
+
"source": [
|
| 716 |
+
"### 4.1b: Stereographic Projection as Constellation Readout\n",
|
| 717 |
+
"\n",
|
| 718 |
+
"Instead of computing distance-based triangulation, read local coordinates via stereographic\n",
|
| 719 |
+
"projection centered at each anchor point.\n",
|
| 720 |
+
""
|
| 721 |
+
]
|
| 722 |
+
},
|
| 723 |
+
{
|
| 724 |
+
"cell_type": "code",
|
| 725 |
+
"metadata": {},
|
| 726 |
+
"source": [
|
| 727 |
+
"\n",
|
| 728 |
+
"class ConstellationWithStereographicReadout(nn.Module):\n",
|
| 729 |
+
" \"\"\"\n",
|
| 730 |
+
" Like Constellation, but output is stereographic coordinates instead of distances.\n",
|
| 731 |
+
" For each anchor, locally project the point and read its (d) flat coordinates.\n",
|
| 732 |
+
" \"\"\"\n",
|
| 733 |
+
" def __init__(self, embed_dim, num_anchors=64):\n",
|
| 734 |
+
" super().__init__()\n",
|
| 735 |
+
" self.embed_dim = embed_dim\n",
|
| 736 |
+
" self.num_anchors = num_anchors\n",
|
| 737 |
+
"\n",
|
| 738 |
+
" # Anchors on S^d\n",
|
| 739 |
+
" A = torch.randn(num_anchors, embed_dim)\n",
|
| 740 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 741 |
+
" anchors = Q.T[:num_anchors]\n",
|
| 742 |
+
" if num_anchors > embed_dim:\n",
|
| 743 |
+
" extra = F.normalize(torch.randn(num_anchors - embed_dim, embed_dim), dim=-1)\n",
|
| 744 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 745 |
+
" self.anchors = nn.Parameter(F.normalize(anchors, dim=-1), requires_grad=False)\n",
|
| 746 |
+
" self.stereographic = StereographicProjection(embed_dim)\n",
|
| 747 |
+
"\n",
|
| 748 |
+
" def forward(self, x):\n",
|
| 749 |
+
" \"\"\"\n",
|
| 750 |
+
" x: (B, d) on S^d\n",
|
| 751 |
+
" For each of K anchors:\n",
|
| 752 |
+
" 1. Rotate sphere so anchor is at north pole (via Householder reflection or similar)\n",
|
| 753 |
+
" 2. Apply stereographic projection from that pole\n",
|
| 754 |
+
" 3. Collect (d)-dim flat coordinates\n",
|
| 755 |
+
" Output: (B, K*d) stacked stereographic coordinates\n",
|
| 756 |
+
" \"\"\"\n",
|
| 757 |
+
" x = l2_normalize(x)\n",
|
| 758 |
+
" B = x.shape[0]\n",
|
| 759 |
+
" K = self.num_anchors\n",
|
| 760 |
+
" d = self.embed_dim - 1\n",
|
| 761 |
+
"\n",
|
| 762 |
+
" # Simple approach: for each anchor, compute stereographic distance + angle\n",
|
| 763 |
+
" # More sophisticated: actually rotate to make anchor the north pole\n",
|
| 764 |
+
" # Here we use a simpler version: treat stereographic coords as feature readout\n",
|
| 765 |
+
"\n",
|
| 766 |
+
" stereo_features = []\n",
|
| 767 |
+
" for k in range(K):\n",
|
| 768 |
+
" anchor = self.anchors[k:k+1] # (1, d)\n",
|
| 769 |
+
"\n",
|
| 770 |
+
" # Measure angle to anchor\n",
|
| 771 |
+
" cos_sim = torch.mm(x, anchor.T) # (B, 1)\n",
|
| 772 |
+
"\n",
|
| 773 |
+
" # Construct orthonormal basis with anchor as one direction\n",
|
| 774 |
+
" # For simplicity: use cos_sim as one dimension, and orthogonal component magnitude\n",
|
| 775 |
+
" # Project x onto orthogonal subspace to anchor\n",
|
| 776 |
+
" x_ortho = x - cos_sim * anchor # (B, d)\n",
|
| 777 |
+
" ortho_norm = x_ortho.norm(dim=-1, keepdim=True) + 1e-8\n",
|
| 778 |
+
"\n",
|
| 779 |
+
" # Concatenate: [cos_sim, ortho_norm] per anchor\n",
|
| 780 |
+
" anchor_feature = torch.cat([cos_sim, ortho_norm], dim=-1) # (B, 2)\n",
|
| 781 |
+
" stereo_features.append(anchor_feature)\n",
|
| 782 |
+
"\n",
|
| 783 |
+
" result = torch.cat(stereo_features, dim=-1) # (B, K*2)\n",
|
| 784 |
+
" return result\n",
|
| 785 |
+
"\n",
|
| 786 |
+
"# Test\n",
|
| 787 |
+
"stereo_constellation = ConstellationWithStereographicReadout(embed_dim=256, num_anchors=64)\n",
|
| 788 |
+
"stereo_constellation = stereo_constellation.to(device)\n",
|
| 789 |
+
"\n",
|
| 790 |
+
"test_embeddings = F.normalize(torch.randn(8, 256), dim=-1).to(device)\n",
|
| 791 |
+
"with torch.no_grad():\n",
|
| 792 |
+
" stereo_out = stereo_constellation(test_embeddings)\n",
|
| 793 |
+
" print(f\"[4.1b] Stereographic readout shape: {stereo_out.shape}\")\n",
|
| 794 |
+
" print(f\"[4.1b] Mean value: {stereo_out.mean().item():.4f}\")\n",
|
| 795 |
+
" print(f\"[4.1b] Std: {stereo_out.std().item():.4f}\")\n",
|
| 796 |
+
""
|
| 797 |
+
],
|
| 798 |
+
"execution_count": null,
|
| 799 |
+
"outputs": []
|
| 800 |
+
},
|
| 801 |
+
{
|
| 802 |
+
"cell_type": "markdown",
|
| 803 |
+
"metadata": {},
|
| 804 |
+
"source": [
|
| 805 |
+
"## 4.2: Log Map Triangulation\n",
|
| 806 |
+
"\n",
|
| 807 |
+
"### 4.2a: Log Map at Each Anchor\n",
|
| 808 |
+
"\n",
|
| 809 |
+
"**Theory:**\n",
|
| 810 |
+
"- log_p(q) = arccos(\u27e8q,p\u27e9) \u00b7 (q \u2212 \u27e8q,p\u27e9p) / \u2016q \u2212 \u27e8q,p\u27e9p\u2016\n",
|
| 811 |
+
"- Replace scalar (1\u2212cos) distance with d-dim tangent vector\n",
|
| 812 |
+
"- Each anchor contributes d-dimensional coordinates instead of 1-dimensional distance\n",
|
| 813 |
+
""
|
| 814 |
+
]
|
| 815 |
+
},
|
| 816 |
+
{
|
| 817 |
+
"cell_type": "code",
|
| 818 |
+
"metadata": {},
|
| 819 |
+
"source": [
|
| 820 |
+
"\n",
|
| 821 |
+
"def log_map(p, q):\n",
|
| 822 |
+
" \"\"\"\n",
|
| 823 |
+
" Logarithmic map at p \u2208 S^d applied to q \u2208 S^d.\n",
|
| 824 |
+
" Returns tangent vector at p in direction of q.\n",
|
| 825 |
+
" p, q: (B, d) or (1, d)\n",
|
| 826 |
+
" Returns: (B, d) tangent vector in T_p S^d\n",
|
| 827 |
+
" \"\"\"\n",
|
| 828 |
+
" p = l2_normalize(p)\n",
|
| 829 |
+
" q = l2_normalize(q)\n",
|
| 830 |
+
"\n",
|
| 831 |
+
" cos_pq = (p * q).sum(dim=-1, keepdim=True) # (B, 1) or (1, 1)\n",
|
| 832 |
+
" cos_pq = torch.clamp(cos_pq, -1.0 + 1e-8, 1.0 - 1e-8)\n",
|
| 833 |
+
"\n",
|
| 834 |
+
" # angle from p to q\n",
|
| 835 |
+
" angle = torch.acos(cos_pq) # (B, 1)\n",
|
| 836 |
+
"\n",
|
| 837 |
+
" # orthogonal component\n",
|
| 838 |
+
" q_ortho = q - cos_pq * p # (B, d)\n",
|
| 839 |
+
" ortho_norm = q_ortho.norm(dim=-1, keepdim=True) + 1e-8\n",
|
| 840 |
+
"\n",
|
| 841 |
+
" # log_p(q) = angle * normalized_ortho\n",
|
| 842 |
+
" log_pq = angle * (q_ortho / ortho_norm) # (B, d)\n",
|
| 843 |
+
"\n",
|
| 844 |
+
" return log_pq\n",
|
| 845 |
+
"\n",
|
| 846 |
+
"class LogMapTriangulation(nn.Module):\n",
|
| 847 |
+
" \"\"\"\n",
|
| 848 |
+
" Triangulation via log maps: at each anchor, compute d-dimensional tangent vector.\n",
|
| 849 |
+
" Output: (B, K*d) stacked log map vectors from all K anchors.\n",
|
| 850 |
+
" \"\"\"\n",
|
| 851 |
+
" def __init__(self, embed_dim, num_anchors=64):\n",
|
| 852 |
+
" super().__init__()\n",
|
| 853 |
+
" self.embed_dim = embed_dim\n",
|
| 854 |
+
" self.num_anchors = num_anchors\n",
|
| 855 |
+
"\n",
|
| 856 |
+
" # Initialize anchors uniformly on S^d\n",
|
| 857 |
+
" A = torch.randn(num_anchors, embed_dim)\n",
|
| 858 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 859 |
+
" anchors = Q.T[:num_anchors]\n",
|
| 860 |
+
" if num_anchors > embed_dim:\n",
|
| 861 |
+
" extra = F.normalize(torch.randn(num_anchors - embed_dim, embed_dim), dim=-1)\n",
|
| 862 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 863 |
+
" self.anchors = nn.Parameter(F.normalize(anchors, dim=-1), requires_grad=False)\n",
|
| 864 |
+
"\n",
|
| 865 |
+
" def forward(self, x):\n",
|
| 866 |
+
" \"\"\"\n",
|
| 867 |
+
" x: (B, d) on S^d\n",
|
| 868 |
+
" Returns: (B, K*d) stacked log map vectors\n",
|
| 869 |
+
" \"\"\"\n",
|
| 870 |
+
" x = l2_normalize(x)\n",
|
| 871 |
+
" B = x.shape[0]\n",
|
| 872 |
+
" K = self.num_anchors\n",
|
| 873 |
+
"\n",
|
| 874 |
+
" log_features = []\n",
|
| 875 |
+
" for k in range(K):\n",
|
| 876 |
+
" anchor = self.anchors[k:k+1] # (1, d)\n",
|
| 877 |
+
" log_vec = log_map(anchor, x) # (B, d)\n",
|
| 878 |
+
" log_features.append(log_vec)\n",
|
| 879 |
+
"\n",
|
| 880 |
+
" result = torch.cat(log_features, dim=-1) # (B, K*d)\n",
|
| 881 |
+
" return result\n",
|
| 882 |
+
"\n",
|
| 883 |
+
"# Test\n",
|
| 884 |
+
"log_tri = LogMapTriangulation(embed_dim=256, num_anchors=32)\n",
|
| 885 |
+
"log_tri = log_tri.to(device)\n",
|
| 886 |
+
"\n",
|
| 887 |
+
"test_emb = F.normalize(torch.randn(8, 256), dim=-1).to(device)\n",
|
| 888 |
+
"with torch.no_grad():\n",
|
| 889 |
+
" log_out = log_tri(test_emb)\n",
|
| 890 |
+
" print(f\"[4.2a] Log map triangulation shape: {log_out.shape}\")\n",
|
| 891 |
+
" print(f\"[4.2a] Expected: (8, 32*256)=(8, 8192), Got: {log_out.shape}\")\n",
|
| 892 |
+
""
|
| 893 |
+
],
|
| 894 |
+
"execution_count": null,
|
| 895 |
+
"outputs": []
|
| 896 |
+
},
|
| 897 |
+
{
|
| 898 |
+
"cell_type": "markdown",
|
| 899 |
+
"metadata": {},
|
| 900 |
+
"source": [
|
| 901 |
+
"### 4.2b: Log Map \u2192 Parallel Transport \u2192 Common Tangent Space \u2192 Aggregate\n",
|
| 902 |
+
"\n",
|
| 903 |
+
"After computing log maps at each anchor, parallel transport all vectors to a common tangent\n",
|
| 904 |
+
"space (e.g., at origin or north pole), then aggregate.\n",
|
| 905 |
+
""
|
| 906 |
+
]
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
"cell_type": "code",
|
| 910 |
+
"metadata": {},
|
| 911 |
+
"source": [
|
| 912 |
+
"\n",
|
| 913 |
+
"def parallel_transport_along_geodesic(v, p, q):\n",
|
| 914 |
+
" \"\"\"\n",
|
| 915 |
+
" Parallel transport vector v at p along geodesic to q.\n",
|
| 916 |
+
" Uses Levi-Civita connection on sphere: \u0393^k_ij.\n",
|
| 917 |
+
"\n",
|
| 918 |
+
" v: (B, d) tangent vector at p\n",
|
| 919 |
+
" p, q: (B, d) points on S^d\n",
|
| 920 |
+
" Returns: (B, d) transported vector at q\n",
|
| 921 |
+
"\n",
|
| 922 |
+
" Formula: v_transported = v - (\u27e8v,p\u27e9 + \u27e8v,q\u27e9/(1+\u27e8p,q\u27e9)) (p+q)\n",
|
| 923 |
+
" \"\"\"\n",
|
| 924 |
+
" p = l2_normalize(p)\n",
|
| 925 |
+
" q = l2_normalize(q)\n",
|
| 926 |
+
"\n",
|
| 927 |
+
" inner_vp = (v * p).sum(dim=-1, keepdim=True) # (B, 1)\n",
|
| 928 |
+
" inner_vq = (v * q).sum(dim=-1, keepdim=True) # (B, 1)\n",
|
| 929 |
+
" inner_pq = (p * q).sum(dim=-1, keepdim=True) # (B, 1)\n",
|
| 930 |
+
" inner_pq = torch.clamp(inner_pq, -1.0 + 1e-8, 1.0 - 1e-8)\n",
|
| 931 |
+
"\n",
|
| 932 |
+
" coeff = inner_vp + inner_vq / (1.0 + inner_pq + 1e-8) # (B, 1)\n",
|
| 933 |
+
" v_transport = v - coeff * (p + q)\n",
|
| 934 |
+
"\n",
|
| 935 |
+
" return v_transport\n",
|
| 936 |
+
"\n",
|
| 937 |
+
"class LogMapWithParallelTransport(nn.Module):\n",
|
| 938 |
+
" \"\"\"\n",
|
| 939 |
+
" Log map triangulation + parallel transport to north pole (0, 0, ..., 0, 1).\n",
|
| 940 |
+
" \"\"\"\n",
|
| 941 |
+
" def __init__(self, embed_dim, num_anchors=32):\n",
|
| 942 |
+
" super().__init__()\n",
|
| 943 |
+
" self.embed_dim = embed_dim\n",
|
| 944 |
+
" self.num_anchors = num_anchors\n",
|
| 945 |
+
"\n",
|
| 946 |
+
" # Anchors\n",
|
| 947 |
+
" A = torch.randn(num_anchors, embed_dim)\n",
|
| 948 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 949 |
+
" anchors = Q.T[:num_anchors]\n",
|
| 950 |
+
" if num_anchors > embed_dim:\n",
|
| 951 |
+
" extra = F.normalize(torch.randn(num_anchors - embed_dim, embed_dim), dim=-1)\n",
|
| 952 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 953 |
+
" self.anchors = nn.Parameter(F.normalize(anchors, dim=-1), requires_grad=False)\n",
|
| 954 |
+
"\n",
|
| 955 |
+
" # North pole\n",
|
| 956 |
+
" north_pole = torch.zeros(1, embed_dim)\n",
|
| 957 |
+
" north_pole[0, -1] = 1.0\n",
|
| 958 |
+
" self.register_buffer(\"north_pole\", north_pole)\n",
|
| 959 |
+
"\n",
|
| 960 |
+
" def forward(self, x):\n",
|
| 961 |
+
" \"\"\"\n",
|
| 962 |
+
" x: (B, d)\n",
|
| 963 |
+
" 1. Compute log map at each anchor\n",
|
| 964 |
+
" 2. Parallel transport to north pole\n",
|
| 965 |
+
" 3. Stack and aggregate\n",
|
| 966 |
+
" \"\"\"\n",
|
| 967 |
+
" x = l2_normalize(x)\n",
|
| 968 |
+
" B = x.shape[0]\n",
|
| 969 |
+
" K = self.num_anchors\n",
|
| 970 |
+
"\n",
|
| 971 |
+
" transported = []\n",
|
| 972 |
+
" for k in range(K):\n",
|
| 973 |
+
" anchor = self.anchors[k:k+1] # (1, d)\n",
|
| 974 |
+
" log_vec = log_map(anchor, x) # (B, d)\n",
|
| 975 |
+
"\n",
|
| 976 |
+
" # Parallel transport from anchor to north pole\n",
|
| 977 |
+
" # Replicate north pole for batch\n",
|
| 978 |
+
" north = self.north_pole.expand(B, -1) # (B, d)\n",
|
| 979 |
+
" anchor_expand = anchor.expand(B, -1)\n",
|
| 980 |
+
" transported_vec = parallel_transport_along_geodesic(log_vec, anchor_expand, north)\n",
|
| 981 |
+
"\n",
|
| 982 |
+
" transported.append(transported_vec)\n",
|
| 983 |
+
"\n",
|
| 984 |
+
" # Stack: (B, K*d)\n",
|
| 985 |
+
" result = torch.cat(transported, dim=-1)\n",
|
| 986 |
+
" return result\n",
|
| 987 |
+
"\n",
|
| 988 |
+
"# Test\n",
|
| 989 |
+
"log_pt = LogMapWithParallelTransport(embed_dim=256, num_anchors=32)\n",
|
| 990 |
+
"log_pt = log_pt.to(device)\n",
|
| 991 |
+
"\n",
|
| 992 |
+
"with torch.no_grad():\n",
|
| 993 |
+
" log_pt_out = log_pt(test_emb)\n",
|
| 994 |
+
" print(f\"[4.2b] Log map with parallel transport shape: {log_pt_out.shape}\")\n",
|
| 995 |
+
" print(f\"[4.2b] Mean: {log_pt_out.mean().item():.4f}, Std: {log_pt_out.std().item():.4f}\")\n",
|
| 996 |
+
""
|
| 997 |
+
],
|
| 998 |
+
"execution_count": null,
|
| 999 |
+
"outputs": []
|
| 1000 |
+
},
|
| 1001 |
+
{
|
| 1002 |
+
"cell_type": "markdown",
|
| 1003 |
+
"metadata": {},
|
| 1004 |
+
"source": [
|
| 1005 |
+
"## 4.3: Log Maps + Parallel Transport to Canonical North Pole\n",
|
| 1006 |
+
"\n",
|
| 1007 |
+
"### 4.3a: Canonical Tangent Space Representation\n",
|
| 1008 |
+
"\n",
|
| 1009 |
+
"Projects log maps from all anchors to a common tangent space at north pole, creating\n",
|
| 1010 |
+
"a canonical representation independent of anchor positions.\n",
|
| 1011 |
+
""
|
| 1012 |
+
]
|
| 1013 |
+
},
|
| 1014 |
+
{
|
| 1015 |
+
"cell_type": "code",
|
| 1016 |
+
"metadata": {},
|
| 1017 |
+
"source": [
|
| 1018 |
+
"\n",
|
| 1019 |
+
"class CanonicalTangentSpaceRepresentation(nn.Module):\n",
|
| 1020 |
+
" \"\"\"\n",
|
| 1021 |
+
" Combine LogMapWithParallelTransport with learned aggregation.\n",
|
| 1022 |
+
" \"\"\"\n",
|
| 1023 |
+
" def __init__(self, embed_dim, num_anchors=32, agg_dim=512):\n",
|
| 1024 |
+
" super().__init__()\n",
|
| 1025 |
+
" self.embed_dim = embed_dim\n",
|
| 1026 |
+
" self.num_anchors = num_anchors\n",
|
| 1027 |
+
" self.agg_dim = agg_dim\n",
|
| 1028 |
+
"\n",
|
| 1029 |
+
" self.log_pt = LogMapWithParallelTransport(embed_dim, num_anchors)\n",
|
| 1030 |
+
" self.aggregation = nn.Sequential(\n",
|
| 1031 |
+
" nn.Linear(num_anchors * embed_dim, agg_dim),\n",
|
| 1032 |
+
" nn.ReLU(),\n",
|
| 1033 |
+
" nn.LayerNorm(agg_dim),\n",
|
| 1034 |
+
" nn.Linear(agg_dim, agg_dim),\n",
|
| 1035 |
+
" )\n",
|
| 1036 |
+
"\n",
|
| 1037 |
+
" def forward(self, x):\n",
|
| 1038 |
+
" \"\"\"x: (B, embed_dim) \u2192 (B, agg_dim)\"\"\"\n",
|
| 1039 |
+
" log_pt_features = self.log_pt(x) # (B, K*d)\n",
|
| 1040 |
+
" agg = self.aggregation(log_pt_features) # (B, agg_dim)\n",
|
| 1041 |
+
" return l2_normalize(agg)\n",
|
| 1042 |
+
"\n",
|
| 1043 |
+
"# Test\n",
|
| 1044 |
+
"canonical_rep = CanonicalTangentSpaceRepresentation(embed_dim=256, num_anchors=32, agg_dim=256)\n",
|
| 1045 |
+
"canonical_rep = canonical_rep.to(device)\n",
|
| 1046 |
+
"\n",
|
| 1047 |
+
"with torch.no_grad():\n",
|
| 1048 |
+
" can_out = canonical_rep(test_emb)\n",
|
| 1049 |
+
" print(f\"[4.3a] Canonical tangent space output: {can_out.shape}\")\n",
|
| 1050 |
+
" print(f\"[4.3a] Norm: {can_out.norm(dim=-1).mean().item():.6f}\")\n",
|
| 1051 |
+
""
|
| 1052 |
+
],
|
| 1053 |
+
"execution_count": null,
|
| 1054 |
+
"outputs": []
|
| 1055 |
+
},
|
| 1056 |
+
{
|
| 1057 |
+
"cell_type": "markdown",
|
| 1058 |
+
"metadata": {},
|
| 1059 |
+
"source": [
|
| 1060 |
+
"### 4.3b: Parallel Transport as Inter-Anchor Communication\n",
|
| 1061 |
+
"\n",
|
| 1062 |
+
"Visualize how the same input point appears from different anchor tangent spaces.\n",
|
| 1063 |
+
""
|
| 1064 |
+
]
|
| 1065 |
+
},
|
| 1066 |
+
{
|
| 1067 |
+
"cell_type": "code",
|
| 1068 |
+
"metadata": {},
|
| 1069 |
+
"source": [
|
| 1070 |
+
"\n",
|
| 1071 |
+
"class InterAnchorCommunication(nn.Module):\n",
|
| 1072 |
+
" \"\"\"\n",
|
| 1073 |
+
" For each pair of anchors, compute the parallel-transported log vector at one anchor\n",
|
| 1074 |
+
" when viewed from the other anchor's tangent space. This reveals geometric relationships.\n",
|
| 1075 |
+
" \"\"\"\n",
|
| 1076 |
+
" def __init__(self, embed_dim, num_anchors=16):\n",
|
| 1077 |
+
" super().__init__()\n",
|
| 1078 |
+
" self.embed_dim = embed_dim\n",
|
| 1079 |
+
" self.num_anchors = num_anchors\n",
|
| 1080 |
+
"\n",
|
| 1081 |
+
" A = torch.randn(num_anchors, embed_dim)\n",
|
| 1082 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 1083 |
+
" anchors = Q.T[:num_anchors]\n",
|
| 1084 |
+
" if num_anchors > embed_dim:\n",
|
| 1085 |
+
" extra = F.normalize(torch.randn(num_anchors - embed_dim, embed_dim), dim=-1)\n",
|
| 1086 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 1087 |
+
" self.anchors = nn.Parameter(F.normalize(anchors, dim=-1), requires_grad=False)\n",
|
| 1088 |
+
"\n",
|
| 1089 |
+
" def forward(self, x):\n",
|
| 1090 |
+
" \"\"\"\n",
|
| 1091 |
+
" x: (B, d)\n",
|
| 1092 |
+
" For each anchor pair (i, j):\n",
|
| 1093 |
+
" - Compute log_i(x)\n",
|
| 1094 |
+
" - Parallel transport from i to j\n",
|
| 1095 |
+
" Output: (B, K*(K-1)/2 * d) or (B, K*d) if simplified\n",
|
| 1096 |
+
"\n",
|
| 1097 |
+
" Simplified: for each anchor, compute inter-anchor communication features.\n",
|
| 1098 |
+
" \"\"\"\n",
|
| 1099 |
+
" x = l2_normalize(x)\n",
|
| 1100 |
+
" B = x.shape[0]\n",
|
| 1101 |
+
" K = self.num_anchors\n",
|
| 1102 |
+
"\n",
|
| 1103 |
+
" features = []\n",
|
| 1104 |
+
" for i in range(K):\n",
|
| 1105 |
+
" anchor_i = self.anchors[i:i+1] # (1, d)\n",
|
| 1106 |
+
" log_i = log_map(anchor_i, x) # (B, d)\n",
|
| 1107 |
+
"\n",
|
| 1108 |
+
" # Parallel transport to mean of all other anchors\n",
|
| 1109 |
+
" other_anchors = self.anchors[[j for j in range(K) if j != i]] # (K-1, d)\n",
|
| 1110 |
+
" mean_other = l2_normalize(other_anchors.mean(dim=0, keepdim=True)) # (1, d)\n",
|
| 1111 |
+
"\n",
|
| 1112 |
+
" transported = parallel_transport_along_geodesic(\n",
|
| 1113 |
+
" log_i,\n",
|
| 1114 |
+
" anchor_i.expand(B, -1),\n",
|
| 1115 |
+
" mean_other.expand(B, -1)\n",
|
| 1116 |
+
" )\n",
|
| 1117 |
+
" features.append(transported)\n",
|
| 1118 |
+
"\n",
|
| 1119 |
+
" result = torch.cat(features, dim=-1) # (B, K*d)\n",
|
| 1120 |
+
" return result\n",
|
| 1121 |
+
"\n",
|
| 1122 |
+
"# Test\n",
|
| 1123 |
+
"inter_anchor = InterAnchorCommunication(embed_dim=256, num_anchors=16)\n",
|
| 1124 |
+
"inter_anchor = inter_anchor.to(device)\n",
|
| 1125 |
+
"\n",
|
| 1126 |
+
"with torch.no_grad():\n",
|
| 1127 |
+
" inter_out = inter_anchor(test_emb)\n",
|
| 1128 |
+
" print(f\"[4.3b] Inter-anchor communication shape: {inter_out.shape}\")\n",
|
| 1129 |
+
""
|
| 1130 |
+
],
|
| 1131 |
+
"execution_count": null,
|
| 1132 |
+
"outputs": []
|
| 1133 |
+
},
|
| 1134 |
+
{
|
| 1135 |
+
"cell_type": "markdown",
|
| 1136 |
+
"metadata": {},
|
| 1137 |
+
"source": [
|
| 1138 |
+
"## 4.4: M\u00f6bius Geometric Attention\n",
|
| 1139 |
+
"\n",
|
| 1140 |
+
"### 4.4a: M\u00f6bius Transform as Attention Mechanism\n",
|
| 1141 |
+
"\n",
|
| 1142 |
+
"**Theory:**\n",
|
| 1143 |
+
"- h_\u03c9(z) = [(1\u2212\u2016\u03c9\u2016\u00b2)/\u2016z\u2212\u03c9\u2016\u00b2](z\u2212\u03c9) \u2212 \u03c9\n",
|
| 1144 |
+
"- Each anchor applies a M\u00f6bius automorphism to zoom into its region\n",
|
| 1145 |
+
"- Measure distance in warped space instead of original metric\n",
|
| 1146 |
+
""
|
| 1147 |
+
]
|
| 1148 |
+
},
|
| 1149 |
+
{
|
| 1150 |
+
"cell_type": "code",
|
| 1151 |
+
"metadata": {},
|
| 1152 |
+
"source": [
|
| 1153 |
+
"\n",
|
| 1154 |
+
"class MoebiusTransform(nn.Module):\n",
|
| 1155 |
+
" \"\"\"\n",
|
| 1156 |
+
" M\u00f6bius automorphism of the ball / sphere.\n",
|
| 1157 |
+
" h_\u03c9(z) = [(1\u2212\u2016\u03c9\u2016\u00b2)/\u2016z\u2212\u03c9\u2016\u00b2](z\u2212\u03c9) \u2212 \u03c9\n",
|
| 1158 |
+
" \"\"\"\n",
|
| 1159 |
+
" def __init__(self, embed_dim, num_anchors=32):\n",
|
| 1160 |
+
" super().__init__()\n",
|
| 1161 |
+
" self.embed_dim = embed_dim\n",
|
| 1162 |
+
" self.num_anchors = num_anchors\n",
|
| 1163 |
+
"\n",
|
| 1164 |
+
" # M\u00f6bius centers (learned as parameters)\n",
|
| 1165 |
+
" # Initialize in interior of ball (norm < 1)\n",
|
| 1166 |
+
" self.moebius_centers = nn.Parameter(\n",
|
| 1167 |
+
" 0.5 * F.normalize(torch.randn(num_anchors, embed_dim), dim=-1)\n",
|
| 1168 |
+
" )\n",
|
| 1169 |
+
"\n",
|
| 1170 |
+
" def forward(self, x):\n",
|
| 1171 |
+
" \"\"\"\n",
|
| 1172 |
+
" x: (B, d)\n",
|
| 1173 |
+
" Apply each M\u00f6bius transform and collect transformed distances.\n",
|
| 1174 |
+
" \"\"\"\n",
|
| 1175 |
+
" x = l2_normalize(x) # Project to sphere\n",
|
| 1176 |
+
" B = x.shape[0]\n",
|
| 1177 |
+
" K = self.num_anchors\n",
|
| 1178 |
+
"\n",
|
| 1179 |
+
" moebius_features = []\n",
|
| 1180 |
+
" for k in range(K):\n",
|
| 1181 |
+
" omega = self.moebius_centers[k] # (d,)\n",
|
| 1182 |
+
" omega_norm_sq = (omega ** 2).sum() + 1e-8\n",
|
| 1183 |
+
"\n",
|
| 1184 |
+
" # h_\u03c9(x) = [(1\u2212\u2016\u03c9\u2016\u00b2)/\u2016x\u2212\u03c9\u2016\u00b2](x\u2212\u03c9) \u2212 \u03c9\n",
|
| 1185 |
+
" x_minus_omega = x - omega.unsqueeze(0) # (B, d)\n",
|
| 1186 |
+
" diff_norm_sq = (x_minus_omega ** 2).sum(dim=-1, keepdim=True) + 1e-8 # (B, 1)\n",
|
| 1187 |
+
"\n",
|
| 1188 |
+
" coeff = (1.0 - omega_norm_sq) / diff_norm_sq # (B, 1)\n",
|
| 1189 |
+
" h_x = coeff * x_minus_omega - omega.unsqueeze(0) # (B, d)\n",
|
| 1190 |
+
"\n",
|
| 1191 |
+
" # Project back to sphere and measure distance to omega's image\n",
|
| 1192 |
+
" h_x_sphere = l2_normalize(h_x)\n",
|
| 1193 |
+
"\n",
|
| 1194 |
+
" # Distance in transformed space\n",
|
| 1195 |
+
" dist_transformed = torch.norm(h_x_sphere - l2_normalize(omega.unsqueeze(0)), dim=-1, keepdim=True)\n",
|
| 1196 |
+
" moebius_features.append(dist_transformed)\n",
|
| 1197 |
+
"\n",
|
| 1198 |
+
" result = torch.cat(moebius_features, dim=-1) # (B, K)\n",
|
| 1199 |
+
" return result\n",
|
| 1200 |
+
"\n",
|
| 1201 |
+
"# Test\n",
|
| 1202 |
+
"moebius = MoebiusTransform(embed_dim=256, num_anchors=64)\n",
|
| 1203 |
+
"moebius = moebius.to(device)\n",
|
| 1204 |
+
"\n",
|
| 1205 |
+
"with torch.no_grad():\n",
|
| 1206 |
+
" moebius_out = moebius(test_emb)\n",
|
| 1207 |
+
" print(f\"[4.4a] M\u00f6bius transformed distances: {moebius_out.shape}\")\n",
|
| 1208 |
+
" print(f\"[4.4a] Mean distance: {moebius_out.mean().item():.4f}\")\n",
|
| 1209 |
+
""
|
| 1210 |
+
],
|
| 1211 |
+
"execution_count": null,
|
| 1212 |
+
"outputs": []
|
| 1213 |
+
},
|
| 1214 |
+
{
|
| 1215 |
+
"cell_type": "markdown",
|
| 1216 |
+
"metadata": {},
|
| 1217 |
+
"source": [
|
| 1218 |
+
"### 4.4b: Composition of M\u00f6bius Transforms as Normalizing Flow\n",
|
| 1219 |
+
"\n",
|
| 1220 |
+
"Stack multiple M\u00f6bius transforms to create a learnable warping flow.\n",
|
| 1221 |
+
""
|
| 1222 |
+
]
|
| 1223 |
+
},
|
| 1224 |
+
{
|
| 1225 |
+
"cell_type": "code",
|
| 1226 |
+
"metadata": {},
|
| 1227 |
+
"source": [
|
| 1228 |
+
"\n",
|
| 1229 |
+
"class MoebiusFlow(nn.Module):\n",
|
| 1230 |
+
" \"\"\"\n",
|
| 1231 |
+
" Stack of K M\u00f6bius transforms applied sequentially.\n",
|
| 1232 |
+
" Acts as a normalizing flow that warps the embedding space.\n",
|
| 1233 |
+
" \"\"\"\n",
|
| 1234 |
+
" def __init__(self, embed_dim, num_layers=3, num_anchors_per_layer=32):\n",
|
| 1235 |
+
" super().__init__()\n",
|
| 1236 |
+
" self.embed_dim = embed_dim\n",
|
| 1237 |
+
" self.num_layers = num_layers\n",
|
| 1238 |
+
" self.layers = nn.ModuleList([\n",
|
| 1239 |
+
" MoebiusTransform(embed_dim, num_anchors_per_layer)\n",
|
| 1240 |
+
" for _ in range(num_layers)\n",
|
| 1241 |
+
" ])\n",
|
| 1242 |
+
"\n",
|
| 1243 |
+
" # Aggregation after flow\n",
|
| 1244 |
+
" total_features = num_layers * num_anchors_per_layer\n",
|
| 1245 |
+
" self.aggregator = nn.Sequential(\n",
|
| 1246 |
+
" nn.Linear(total_features, embed_dim),\n",
|
| 1247 |
+
" nn.ReLU(),\n",
|
| 1248 |
+
" nn.LayerNorm(embed_dim),\n",
|
| 1249 |
+
" )\n",
|
| 1250 |
+
"\n",
|
| 1251 |
+
" def forward(self, x):\n",
|
| 1252 |
+
" \"\"\"x: (B, d) \u2192 (B, d)\"\"\"\n",
|
| 1253 |
+
" x = l2_normalize(x)\n",
|
| 1254 |
+
" flow_features = []\n",
|
| 1255 |
+
"\n",
|
| 1256 |
+
" for layer in self.layers:\n",
|
| 1257 |
+
" features = layer(x) # (B, K)\n",
|
| 1258 |
+
" flow_features.append(features)\n",
|
| 1259 |
+
"\n",
|
| 1260 |
+
" # Combine: (B, total_features)\n",
|
| 1261 |
+
" combined = torch.cat(flow_features, dim=-1)\n",
|
| 1262 |
+
" agg = self.aggregator(combined) # (B, d)\n",
|
| 1263 |
+
"\n",
|
| 1264 |
+
" return l2_normalize(agg)\n",
|
| 1265 |
+
"\n",
|
| 1266 |
+
"# Test\n",
|
| 1267 |
+
"moebius_flow = MoebiusFlow(embed_dim=256, num_layers=2, num_anchors_per_layer=32)\n",
|
| 1268 |
+
"moebius_flow = moebius_flow.to(device)\n",
|
| 1269 |
+
"\n",
|
| 1270 |
+
"with torch.no_grad():\n",
|
| 1271 |
+
" flow_out = moebius_flow(test_emb)\n",
|
| 1272 |
+
" print(f\"[4.4b] M\u00f6bius flow output: {flow_out.shape}\")\n",
|
| 1273 |
+
" print(f\"[4.4b] Output norm: {flow_out.norm(dim=-1).mean().item():.6f}\")\n",
|
| 1274 |
+
""
|
| 1275 |
+
],
|
| 1276 |
+
"execution_count": null,
|
| 1277 |
+
"outputs": []
|
| 1278 |
+
},
|
| 1279 |
+
{
|
| 1280 |
+
"cell_type": "markdown",
|
| 1281 |
+
"metadata": {},
|
| 1282 |
+
"source": [
|
| 1283 |
+
"## 4.5: Procrustes Alignment & Polar Decomposition\n",
|
| 1284 |
+
"\n",
|
| 1285 |
+
"### 4.5a: Procrustes Alignment to Canonical Pose\n",
|
| 1286 |
+
"\n",
|
| 1287 |
+
"**Theory:**\n",
|
| 1288 |
+
"- R* = argmin_R \u2016RA \u2212 B\u2016_F\n",
|
| 1289 |
+
"- R* = UV\u1d40 from SVD(B\u1d40A)\n",
|
| 1290 |
+
"- Remove rotation, keep shape information\n",
|
| 1291 |
+
""
|
| 1292 |
+
]
|
| 1293 |
+
},
|
| 1294 |
+
{
|
| 1295 |
+
"cell_type": "code",
|
| 1296 |
+
"metadata": {},
|
| 1297 |
+
"source": [
|
| 1298 |
+
"\n",
|
| 1299 |
+
"def procrustes_align(A, B):\n",
|
| 1300 |
+
" \"\"\"\n",
|
| 1301 |
+
" Align matrix A to B via Procrustes.\n",
|
| 1302 |
+
" Returns rotation R such that \u2016RA \u2212 B\u2016_F is minimized.\n",
|
| 1303 |
+
"\n",
|
| 1304 |
+
" A, B: (d, d) or similar shape\n",
|
| 1305 |
+
" Returns: R (rotation), aligned_A = RA\n",
|
| 1306 |
+
" \"\"\"\n",
|
| 1307 |
+
" U, _, Vt = torch.svd(B.T @ A)\n",
|
| 1308 |
+
" R = U @ Vt\n",
|
| 1309 |
+
" aligned_A = R @ A\n",
|
| 1310 |
+
" return R, aligned_A\n",
|
| 1311 |
+
"\n",
|
| 1312 |
+
"class ProcrustesAlignmentEncoder(nn.Module):\n",
|
| 1313 |
+
" \"\"\"\n",
|
| 1314 |
+
" For each sample, construct a feature matrix from scattering/constellation data,\n",
|
| 1315 |
+
" align it to a learned canonical pose, extract shape signature.\n",
|
| 1316 |
+
" \"\"\"\n",
|
| 1317 |
+
" def __init__(self, embed_dim, num_features=64):\n",
|
| 1318 |
+
" super().__init__()\n",
|
| 1319 |
+
" self.embed_dim = embed_dim\n",
|
| 1320 |
+
" self.num_features = num_features\n",
|
| 1321 |
+
"\n",
|
| 1322 |
+
" # Canonical pose (learned)\n",
|
| 1323 |
+
" self.canonical_pose = nn.Parameter(torch.randn(num_features, num_features))\n",
|
| 1324 |
+
" nn.init.orthogonal_(self.canonical_pose)\n",
|
| 1325 |
+
"\n",
|
| 1326 |
+
" # Feature constructor from embeddings\n",
|
| 1327 |
+
" self.feature_constructor = nn.Linear(embed_dim, num_features * num_features)\n",
|
| 1328 |
+
"\n",
|
| 1329 |
+
" def forward(self, embeddings):\n",
|
| 1330 |
+
" \"\"\"\n",
|
| 1331 |
+
" embeddings: (B, embed_dim)\n",
|
| 1332 |
+
" Returns: (B, num_features) shape signature after alignment\n",
|
| 1333 |
+
" \"\"\"\n",
|
| 1334 |
+
" B = embeddings.shape[0]\n",
|
| 1335 |
+
" nf = self.num_features\n",
|
| 1336 |
+
"\n",
|
| 1337 |
+
" feature_matrix = self.feature_constructor(embeddings) # (B, nf*nf)\n",
|
| 1338 |
+
" feature_matrix = feature_matrix.reshape(B, nf, nf) # (B, nf, nf)\n",
|
| 1339 |
+
"\n",
|
| 1340 |
+
" aligned_matrices = []\n",
|
| 1341 |
+
" for b in range(B):\n",
|
| 1342 |
+
" A = feature_matrix[b] # (nf, nf)\n",
|
| 1343 |
+
" R, aligned_A = procrustes_align(A, self.canonical_pose)\n",
|
| 1344 |
+
" aligned_matrices.append(aligned_A)\n",
|
| 1345 |
+
"\n",
|
| 1346 |
+
" aligned_stack = torch.stack(aligned_matrices, dim=0) # (B, nf, nf)\n",
|
| 1347 |
+
"\n",
|
| 1348 |
+
" # Extract shape signature: Frobenius norm of rows\n",
|
| 1349 |
+
" shape_sig = torch.norm(aligned_stack, dim=-1) # (B, nf)\n",
|
| 1350 |
+
"\n",
|
| 1351 |
+
" return shape_sig\n",
|
| 1352 |
+
"\n",
|
| 1353 |
+
"# Test\n",
|
| 1354 |
+
"procrustes_enc = ProcrustesAlignmentEncoder(embed_dim=256, num_features=16)\n",
|
| 1355 |
+
"procrustes_enc = procrustes_enc.to(device)\n",
|
| 1356 |
+
"\n",
|
| 1357 |
+
"with torch.no_grad():\n",
|
| 1358 |
+
" proc_out = procrustes_enc(test_emb)\n",
|
| 1359 |
+
" print(f\"[4.5a] Procrustes alignment shape: {proc_out.shape}\")\n",
|
| 1360 |
+
""
|
| 1361 |
+
],
|
| 1362 |
+
"execution_count": null,
|
| 1363 |
+
"outputs": []
|
| 1364 |
+
},
|
| 1365 |
+
{
|
| 1366 |
+
"cell_type": "markdown",
|
| 1367 |
+
"metadata": {},
|
| 1368 |
+
"source": [
|
| 1369 |
+
"### 4.5b: Polar Decomposition \u2014 Rotation + Stretch Features\n",
|
| 1370 |
+
"\n",
|
| 1371 |
+
"A = UP where U is rotation and P is stretch. Extract both as features.\n",
|
| 1372 |
+
""
|
| 1373 |
+
]
|
| 1374 |
+
},
|
| 1375 |
+
{
|
| 1376 |
+
"cell_type": "code",
|
| 1377 |
+
"metadata": {},
|
| 1378 |
+
"source": [
|
| 1379 |
+
"\n",
|
| 1380 |
+
"class PolarDecompositionFeatures(nn.Module):\n",
|
| 1381 |
+
" \"\"\"\n",
|
| 1382 |
+
" Extract U (rotation) and P (stretch) from polar decomposition.\n",
|
| 1383 |
+
" A = UP where U = UV\u1d40 (orthogonal) and P = V\u1d26\u1d40 (symmetric positive definite).\n",
|
| 1384 |
+
" \"\"\"\n",
|
| 1385 |
+
" def __init__(self, embed_dim, num_features=16):\n",
|
| 1386 |
+
" super().__init__()\n",
|
| 1387 |
+
" self.embed_dim = embed_dim\n",
|
| 1388 |
+
" self.num_features = num_features\n",
|
| 1389 |
+
"\n",
|
| 1390 |
+
" self.feature_constructor = nn.Linear(embed_dim, num_features * num_features)\n",
|
| 1391 |
+
"\n",
|
| 1392 |
+
" def forward(self, embeddings):\n",
|
| 1393 |
+
" \"\"\"\n",
|
| 1394 |
+
" embeddings: (B, embed_dim)\n",
|
| 1395 |
+
" Returns: (B, num_features + num_features) = (B, 2*num_features)\n",
|
| 1396 |
+
" - First num_features: norm of U rows (orientation)\n",
|
| 1397 |
+
" - Second num_features: diag of P (stretch/scale)\n",
|
| 1398 |
+
" \"\"\"\n",
|
| 1399 |
+
" B = embeddings.shape[0]\n",
|
| 1400 |
+
" nf = self.num_features\n",
|
| 1401 |
+
"\n",
|
| 1402 |
+
" feature_matrix = self.feature_constructor(embeddings) # (B, nf*nf)\n",
|
| 1403 |
+
" A = feature_matrix.reshape(B, nf, nf) # (B, nf, nf)\n",
|
| 1404 |
+
"\n",
|
| 1405 |
+
" u_features = []\n",
|
| 1406 |
+
" p_features = []\n",
|
| 1407 |
+
"\n",
|
| 1408 |
+
" for b in range(B):\n",
|
| 1409 |
+
" A_b = A[b] # (nf, nf)\n",
|
| 1410 |
+
"\n",
|
| 1411 |
+
" # Polar decomposition: A = UP\n",
|
| 1412 |
+
" U, s, Vt = torch.svd(A_b)\n",
|
| 1413 |
+
" P = (Vt.T * s.unsqueeze(0)) @ Vt # V @ \u03a3 @ V\u1d40\n",
|
| 1414 |
+
"\n",
|
| 1415 |
+
" # U: orthogonal matrix -> extract row norms as features\n",
|
| 1416 |
+
" u_row_norms = torch.norm(U, dim=-1) # (nf,)\n",
|
| 1417 |
+
"\n",
|
| 1418 |
+
" # P: symmetric -> extract diagonal as features\n",
|
| 1419 |
+
" p_diag = torch.diag(P) # (nf,)\n",
|
| 1420 |
+
"\n",
|
| 1421 |
+
" u_features.append(u_row_norms)\n",
|
| 1422 |
+
" p_features.append(p_diag)\n",
|
| 1423 |
+
"\n",
|
| 1424 |
+
" u_stack = torch.stack(u_features, dim=0) # (B, nf)\n",
|
| 1425 |
+
" p_stack = torch.stack(p_features, dim=0) # (B, nf)\n",
|
| 1426 |
+
"\n",
|
| 1427 |
+
" result = torch.cat([u_stack, p_stack], dim=-1) # (B, 2*nf)\n",
|
| 1428 |
+
" return result\n",
|
| 1429 |
+
"\n",
|
| 1430 |
+
"# Test\n",
|
| 1431 |
+
"polar_dec = PolarDecompositionFeatures(embed_dim=256, num_features=16)\n",
|
| 1432 |
+
"polar_dec = polar_dec.to(device)\n",
|
| 1433 |
+
"\n",
|
| 1434 |
+
"with torch.no_grad():\n",
|
| 1435 |
+
" polar_out = polar_dec(test_emb)\n",
|
| 1436 |
+
" print(f\"[4.5b] Polar decomposition features: {polar_out.shape}\")\n",
|
| 1437 |
+
""
|
| 1438 |
+
],
|
| 1439 |
+
"execution_count": null,
|
| 1440 |
+
"outputs": []
|
| 1441 |
+
},
|
| 1442 |
+
{
|
| 1443 |
+
"cell_type": "markdown",
|
| 1444 |
+
"metadata": {},
|
| 1445 |
+
"source": [
|
| 1446 |
+
"## Full Training Experiments\n",
|
| 1447 |
+
"\n",
|
| 1448 |
+
"Each experiment trains a complete pipeline on CIFAR-10 with TensorBoard logging.\n",
|
| 1449 |
+
""
|
| 1450 |
+
]
|
| 1451 |
+
},
|
| 1452 |
+
{
|
| 1453 |
+
"cell_type": "code",
|
| 1454 |
+
"metadata": {},
|
| 1455 |
+
"source": [
|
| 1456 |
+
"\n",
|
| 1457 |
+
"# Load data once\n",
|
| 1458 |
+
"train_loader, val_loader = get_cifar10_raw_loaders(batch_size=256, num_workers=2)\n",
|
| 1459 |
+
"print(f\"[DATA] Ready for experiments\")\n",
|
| 1460 |
+
""
|
| 1461 |
+
],
|
| 1462 |
+
"execution_count": null,
|
| 1463 |
+
"outputs": []
|
| 1464 |
+
},
|
| 1465 |
+
{
|
| 1466 |
+
"cell_type": "code",
|
| 1467 |
+
"metadata": {},
|
| 1468 |
+
"source": [
|
| 1469 |
+
"\n",
|
| 1470 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1471 |
+
"# EXPERIMENT 4.1a: Stereographic Scattering\n",
|
| 1472 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1473 |
+
"\n",
|
| 1474 |
+
"class Model_4_1a(nn.Module):\n",
|
| 1475 |
+
" def __init__(self):\n",
|
| 1476 |
+
" super().__init__()\n",
|
| 1477 |
+
" self.encoder = StereographicScatteringEncoder(embed_dim=256, num_scattering_coeffs=None)\n",
|
| 1478 |
+
" self.classifier = ConstellationClassifier(embed_dim=256, num_anchors=64, num_classes=10)\n",
|
| 1479 |
+
"\n",
|
| 1480 |
+
" def forward(self, images):\n",
|
| 1481 |
+
" embeddings = self.encoder(images)\n",
|
| 1482 |
+
" logits, tri = self.classifier(embeddings)\n",
|
| 1483 |
+
" return logits, embeddings\n",
|
| 1484 |
+
"\n",
|
| 1485 |
+
"model_4_1a = Model_4_1a().to(device)\n",
|
| 1486 |
+
"print(f\"[4.1a] Model created with {sum(p.numel() for p in model_4_1a.parameters() if p.requires_grad):,} params\")\n",
|
| 1487 |
+
"\n",
|
| 1488 |
+
"# Run experiment\n",
|
| 1489 |
+
"result_4_1a = run_experiment(\n",
|
| 1490 |
+
" \"4.1a_stereographic_scattering\",\n",
|
| 1491 |
+
" model_4_1a,\n",
|
| 1492 |
+
" train_loader,\n",
|
| 1493 |
+
" val_loader,\n",
|
| 1494 |
+
" device,\n",
|
| 1495 |
+
" epochs=100,\n",
|
| 1496 |
+
" lr=1e-3,\n",
|
| 1497 |
+
" weight_decay=1e-4,\n",
|
| 1498 |
+
" patience=20\n",
|
| 1499 |
+
")\n",
|
| 1500 |
+
"scoreboard.add(result_4_1a)\n",
|
| 1501 |
+
""
|
| 1502 |
+
],
|
| 1503 |
+
"execution_count": null,
|
| 1504 |
+
"outputs": []
|
| 1505 |
+
},
|
| 1506 |
+
{
|
| 1507 |
+
"cell_type": "code",
|
| 1508 |
+
"metadata": {},
|
| 1509 |
+
"source": [
|
| 1510 |
+
"\n",
|
| 1511 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1512 |
+
"# EXPERIMENT 4.1b: Stereographic Readout Constellation\n",
|
| 1513 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1514 |
+
"\n",
|
| 1515 |
+
"class Model_4_1b(nn.Module):\n",
|
| 1516 |
+
" def __init__(self):\n",
|
| 1517 |
+
" super().__init__()\n",
|
| 1518 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1519 |
+
" scatt_dim = 81 * 64\n",
|
| 1520 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1521 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1522 |
+
"\n",
|
| 1523 |
+
" self.stereo_constellation = ConstellationWithStereographicReadout(256, num_anchors=64)\n",
|
| 1524 |
+
" self.patchwork_stereo = Patchwork(128, num_blocks=2) # 64 anchors * 2 dims\n",
|
| 1525 |
+
" self.head = nn.Linear(128, 10)\n",
|
| 1526 |
+
"\n",
|
| 1527 |
+
" def forward(self, images):\n",
|
| 1528 |
+
" scatt = self.scattering(images)\n",
|
| 1529 |
+
" B = scatt.shape[0]\n",
|
| 1530 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1531 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1532 |
+
" proj = self.patchwork(proj)\n",
|
| 1533 |
+
" embeddings = l2_normalize(proj)\n",
|
| 1534 |
+
"\n",
|
| 1535 |
+
" stereo = self.stereo_constellation(embeddings)\n",
|
| 1536 |
+
" stereo = self.patchwork_stereo(stereo)\n",
|
| 1537 |
+
" logits = self.head(stereo)\n",
|
| 1538 |
+
" return logits, embeddings\n",
|
| 1539 |
+
"\n",
|
| 1540 |
+
"model_4_1b = Model_4_1b().to(device)\n",
|
| 1541 |
+
"print(f\"[4.1b] Model created with {sum(p.numel() for p in model_4_1b.parameters() if p.requires_grad):,} params\")\n",
|
| 1542 |
+
"\n",
|
| 1543 |
+
"result_4_1b = run_experiment(\n",
|
| 1544 |
+
" \"4.1b_stereographic_readout\",\n",
|
| 1545 |
+
" model_4_1b,\n",
|
| 1546 |
+
" train_loader,\n",
|
| 1547 |
+
" val_loader,\n",
|
| 1548 |
+
" device,\n",
|
| 1549 |
+
" epochs=100,\n",
|
| 1550 |
+
" lr=1e-3,\n",
|
| 1551 |
+
" weight_decay=1e-4,\n",
|
| 1552 |
+
" patience=20\n",
|
| 1553 |
+
")\n",
|
| 1554 |
+
"scoreboard.add(result_4_1b)\n",
|
| 1555 |
+
""
|
| 1556 |
+
],
|
| 1557 |
+
"execution_count": null,
|
| 1558 |
+
"outputs": []
|
| 1559 |
+
},
|
| 1560 |
+
{
|
| 1561 |
+
"cell_type": "code",
|
| 1562 |
+
"metadata": {},
|
| 1563 |
+
"source": [
|
| 1564 |
+
"\n",
|
| 1565 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1566 |
+
"# EXPERIMENT 4.2a: Log Map Triangulation\n",
|
| 1567 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1568 |
+
"\n",
|
| 1569 |
+
"class Model_4_2a(nn.Module):\n",
|
| 1570 |
+
" def __init__(self):\n",
|
| 1571 |
+
" super().__init__()\n",
|
| 1572 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1573 |
+
" scatt_dim = 81 * 64\n",
|
| 1574 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1575 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1576 |
+
"\n",
|
| 1577 |
+
" self.log_tri = LogMapTriangulation(embed_dim=256, num_anchors=32)\n",
|
| 1578 |
+
" # Output: (B, 32*256) = (B, 8192) \u2192 reduce to classifier\n",
|
| 1579 |
+
" self.log_tri_reduce = nn.Linear(32 * 256, 256)\n",
|
| 1580 |
+
" self.patchwork_log = Patchwork(256, num_blocks=2)\n",
|
| 1581 |
+
" self.head = nn.Linear(256, 10)\n",
|
| 1582 |
+
"\n",
|
| 1583 |
+
" def forward(self, images):\n",
|
| 1584 |
+
" scatt = self.scattering(images)\n",
|
| 1585 |
+
" B = scatt.shape[0]\n",
|
| 1586 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1587 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1588 |
+
" embeddings = l2_normalize(self.patchwork(proj))\n",
|
| 1589 |
+
"\n",
|
| 1590 |
+
" log_feat = self.log_tri(embeddings)\n",
|
| 1591 |
+
" log_red = self.log_tri_reduce(log_feat)\n",
|
| 1592 |
+
" log_red = self.patchwork_log(log_red)\n",
|
| 1593 |
+
" logits = self.head(log_red)\n",
|
| 1594 |
+
" return logits, embeddings\n",
|
| 1595 |
+
"\n",
|
| 1596 |
+
"model_4_2a = Model_4_2a().to(device)\n",
|
| 1597 |
+
"print(f\"[4.2a] Model created with {sum(p.numel() for p in model_4_2a.parameters() if p.requires_grad):,} params\")\n",
|
| 1598 |
+
"\n",
|
| 1599 |
+
"result_4_2a = run_experiment(\n",
|
| 1600 |
+
" \"4.2a_log_map_triangulation\",\n",
|
| 1601 |
+
" model_4_2a,\n",
|
| 1602 |
+
" train_loader,\n",
|
| 1603 |
+
" val_loader,\n",
|
| 1604 |
+
" device,\n",
|
| 1605 |
+
" epochs=100,\n",
|
| 1606 |
+
" lr=1e-3,\n",
|
| 1607 |
+
" weight_decay=1e-4,\n",
|
| 1608 |
+
" patience=20\n",
|
| 1609 |
+
")\n",
|
| 1610 |
+
"scoreboard.add(result_4_2a)\n",
|
| 1611 |
+
""
|
| 1612 |
+
],
|
| 1613 |
+
"execution_count": null,
|
| 1614 |
+
"outputs": []
|
| 1615 |
+
},
|
| 1616 |
+
{
|
| 1617 |
+
"cell_type": "code",
|
| 1618 |
+
"metadata": {},
|
| 1619 |
+
"source": [
|
| 1620 |
+
"\n",
|
| 1621 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1622 |
+
"# EXPERIMENT 4.2b: Log Map with Parallel Transport\n",
|
| 1623 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1624 |
+
"\n",
|
| 1625 |
+
"class Model_4_2b(nn.Module):\n",
|
| 1626 |
+
" def __init__(self):\n",
|
| 1627 |
+
" super().__init__()\n",
|
| 1628 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1629 |
+
" scatt_dim = 81 * 64\n",
|
| 1630 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1631 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1632 |
+
"\n",
|
| 1633 |
+
" self.log_pt = LogMapWithParallelTransport(embed_dim=256, num_anchors=32)\n",
|
| 1634 |
+
" self.log_pt_reduce = nn.Linear(32 * 256, 256)\n",
|
| 1635 |
+
" self.patchwork_log = Patchwork(256, num_blocks=2)\n",
|
| 1636 |
+
" self.head = nn.Linear(256, 10)\n",
|
| 1637 |
+
"\n",
|
| 1638 |
+
" def forward(self, images):\n",
|
| 1639 |
+
" scatt = self.scattering(images)\n",
|
| 1640 |
+
" B = scatt.shape[0]\n",
|
| 1641 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1642 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1643 |
+
" embeddings = l2_normalize(self.patchwork(proj))\n",
|
| 1644 |
+
"\n",
|
| 1645 |
+
" log_pt_feat = self.log_pt(embeddings)\n",
|
| 1646 |
+
" log_pt_red = self.log_pt_reduce(log_pt_feat)\n",
|
| 1647 |
+
" log_pt_red = self.patchwork_log(log_pt_red)\n",
|
| 1648 |
+
" logits = self.head(log_pt_red)\n",
|
| 1649 |
+
" return logits, embeddings\n",
|
| 1650 |
+
"\n",
|
| 1651 |
+
"model_4_2b = Model_4_2b().to(device)\n",
|
| 1652 |
+
"print(f\"[4.2b] Model created with {sum(p.numel() for p in model_4_2b.parameters() if p.requires_grad):,} params\")\n",
|
| 1653 |
+
"\n",
|
| 1654 |
+
"result_4_2b = run_experiment(\n",
|
| 1655 |
+
" \"4.2b_log_parallel_transport\",\n",
|
| 1656 |
+
" model_4_2b,\n",
|
| 1657 |
+
" train_loader,\n",
|
| 1658 |
+
" val_loader,\n",
|
| 1659 |
+
" device,\n",
|
| 1660 |
+
" epochs=100,\n",
|
| 1661 |
+
" lr=1e-3,\n",
|
| 1662 |
+
" weight_decay=1e-4,\n",
|
| 1663 |
+
" patience=20\n",
|
| 1664 |
+
")\n",
|
| 1665 |
+
"scoreboard.add(result_4_2b)\n",
|
| 1666 |
+
""
|
| 1667 |
+
],
|
| 1668 |
+
"execution_count": null,
|
| 1669 |
+
"outputs": []
|
| 1670 |
+
},
|
| 1671 |
+
{
|
| 1672 |
+
"cell_type": "code",
|
| 1673 |
+
"metadata": {},
|
| 1674 |
+
"source": [
|
| 1675 |
+
"\n",
|
| 1676 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1677 |
+
"# EXPERIMENT 4.3a: Canonical Tangent Space\n",
|
| 1678 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1679 |
+
"\n",
|
| 1680 |
+
"class Model_4_3a(nn.Module):\n",
|
| 1681 |
+
" def __init__(self):\n",
|
| 1682 |
+
" super().__init__()\n",
|
| 1683 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1684 |
+
" scatt_dim = 81 * 64\n",
|
| 1685 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1686 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1687 |
+
"\n",
|
| 1688 |
+
" self.canonical_rep = CanonicalTangentSpaceRepresentation(256, num_anchors=32, agg_dim=256)\n",
|
| 1689 |
+
" self.head = nn.Linear(256, 10)\n",
|
| 1690 |
+
"\n",
|
| 1691 |
+
" def forward(self, images):\n",
|
| 1692 |
+
" scatt = self.scattering(images)\n",
|
| 1693 |
+
" B = scatt.shape[0]\n",
|
| 1694 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1695 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1696 |
+
" embeddings = l2_normalize(self.patchwork(proj))\n",
|
| 1697 |
+
"\n",
|
| 1698 |
+
" can = self.canonical_rep(embeddings)\n",
|
| 1699 |
+
" logits = self.head(can)\n",
|
| 1700 |
+
" return logits, embeddings\n",
|
| 1701 |
+
"\n",
|
| 1702 |
+
"model_4_3a = Model_4_3a().to(device)\n",
|
| 1703 |
+
"print(f\"[4.3a] Model created with {sum(p.numel() for p in model_4_3a.parameters() if p.requires_grad):,} params\")\n",
|
| 1704 |
+
"\n",
|
| 1705 |
+
"result_4_3a = run_experiment(\n",
|
| 1706 |
+
" \"4.3a_canonical_tangent_space\",\n",
|
| 1707 |
+
" model_4_3a,\n",
|
| 1708 |
+
" train_loader,\n",
|
| 1709 |
+
" val_loader,\n",
|
| 1710 |
+
" device,\n",
|
| 1711 |
+
" epochs=100,\n",
|
| 1712 |
+
" lr=1e-3,\n",
|
| 1713 |
+
" weight_decay=1e-4,\n",
|
| 1714 |
+
" patience=20\n",
|
| 1715 |
+
")\n",
|
| 1716 |
+
"scoreboard.add(result_4_3a)\n",
|
| 1717 |
+
""
|
| 1718 |
+
],
|
| 1719 |
+
"execution_count": null,
|
| 1720 |
+
"outputs": []
|
| 1721 |
+
},
|
| 1722 |
+
{
|
| 1723 |
+
"cell_type": "code",
|
| 1724 |
+
"metadata": {},
|
| 1725 |
+
"source": [
|
| 1726 |
+
"\n",
|
| 1727 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1728 |
+
"# EXPERIMENT 4.3b: Inter-Anchor Communication\n",
|
| 1729 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1730 |
+
"\n",
|
| 1731 |
+
"class Model_4_3b(nn.Module):\n",
|
| 1732 |
+
" def __init__(self):\n",
|
| 1733 |
+
" super().__init__()\n",
|
| 1734 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1735 |
+
" scatt_dim = 81 * 64\n",
|
| 1736 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1737 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1738 |
+
"\n",
|
| 1739 |
+
" self.inter_anchor = InterAnchorCommunication(256, num_anchors=16)\n",
|
| 1740 |
+
" # Output: (B, 16*256)\n",
|
| 1741 |
+
" self.inter_reduce = nn.Linear(16 * 256, 256)\n",
|
| 1742 |
+
" self.patchwork_inter = Patchwork(256, num_blocks=2)\n",
|
| 1743 |
+
" self.head = nn.Linear(256, 10)\n",
|
| 1744 |
+
"\n",
|
| 1745 |
+
" def forward(self, images):\n",
|
| 1746 |
+
" scatt = self.scattering(images)\n",
|
| 1747 |
+
" B = scatt.shape[0]\n",
|
| 1748 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1749 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1750 |
+
" embeddings = l2_normalize(self.patchwork(proj))\n",
|
| 1751 |
+
"\n",
|
| 1752 |
+
" inter_feat = self.inter_anchor(embeddings)\n",
|
| 1753 |
+
" inter_red = self.inter_reduce(inter_feat)\n",
|
| 1754 |
+
" inter_red = self.patchwork_inter(inter_red)\n",
|
| 1755 |
+
" logits = self.head(inter_red)\n",
|
| 1756 |
+
" return logits, embeddings\n",
|
| 1757 |
+
"\n",
|
| 1758 |
+
"model_4_3b = Model_4_3b().to(device)\n",
|
| 1759 |
+
"print(f\"[4.3b] Model created with {sum(p.numel() for p in model_4_3b.parameters() if p.requires_grad):,} params\")\n",
|
| 1760 |
+
"\n",
|
| 1761 |
+
"result_4_3b = run_experiment(\n",
|
| 1762 |
+
" \"4.3b_inter_anchor_communication\",\n",
|
| 1763 |
+
" model_4_3b,\n",
|
| 1764 |
+
" train_loader,\n",
|
| 1765 |
+
" val_loader,\n",
|
| 1766 |
+
" device,\n",
|
| 1767 |
+
" epochs=100,\n",
|
| 1768 |
+
" lr=1e-3,\n",
|
| 1769 |
+
" weight_decay=1e-4,\n",
|
| 1770 |
+
" patience=20\n",
|
| 1771 |
+
")\n",
|
| 1772 |
+
"scoreboard.add(result_4_3b)\n",
|
| 1773 |
+
""
|
| 1774 |
+
],
|
| 1775 |
+
"execution_count": null,
|
| 1776 |
+
"outputs": []
|
| 1777 |
+
},
|
| 1778 |
+
{
|
| 1779 |
+
"cell_type": "code",
|
| 1780 |
+
"metadata": {},
|
| 1781 |
+
"source": [
|
| 1782 |
+
"\n",
|
| 1783 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1784 |
+
"# EXPERIMENT 4.4a: M\u00f6bius Geometric Attention\n",
|
| 1785 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1786 |
+
"\n",
|
| 1787 |
+
"class Model_4_4a(nn.Module):\n",
|
| 1788 |
+
" def __init__(self):\n",
|
| 1789 |
+
" super().__init__()\n",
|
| 1790 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1791 |
+
" scatt_dim = 81 * 64\n",
|
| 1792 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1793 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1794 |
+
"\n",
|
| 1795 |
+
" self.moebius = MoebiusTransform(256, num_anchors=64)\n",
|
| 1796 |
+
" self.patchwork_moebius = Patchwork(64, num_blocks=2)\n",
|
| 1797 |
+
" self.head = nn.Linear(64, 10)\n",
|
| 1798 |
+
"\n",
|
| 1799 |
+
" def forward(self, images):\n",
|
| 1800 |
+
" scatt = self.scattering(images)\n",
|
| 1801 |
+
" B = scatt.shape[0]\n",
|
| 1802 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1803 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1804 |
+
" embeddings = l2_normalize(self.patchwork(proj))\n",
|
| 1805 |
+
"\n",
|
| 1806 |
+
" moebius_feat = self.moebius(embeddings)\n",
|
| 1807 |
+
" moebius_feat = self.patchwork_moebius(moebius_feat)\n",
|
| 1808 |
+
" logits = self.head(moebius_feat)\n",
|
| 1809 |
+
" return logits, embeddings\n",
|
| 1810 |
+
"\n",
|
| 1811 |
+
"model_4_4a = Model_4_4a().to(device)\n",
|
| 1812 |
+
"print(f\"[4.4a] Model created with {sum(p.numel() for p in model_4_4a.parameters() if p.requires_grad):,} params\")\n",
|
| 1813 |
+
"\n",
|
| 1814 |
+
"result_4_4a = run_experiment(\n",
|
| 1815 |
+
" \"4.4a_moebius_geometric_attention\",\n",
|
| 1816 |
+
" model_4_4a,\n",
|
| 1817 |
+
" train_loader,\n",
|
| 1818 |
+
" val_loader,\n",
|
| 1819 |
+
" device,\n",
|
| 1820 |
+
" epochs=100,\n",
|
| 1821 |
+
" lr=1e-3,\n",
|
| 1822 |
+
" weight_decay=1e-4,\n",
|
| 1823 |
+
" patience=20\n",
|
| 1824 |
+
")\n",
|
| 1825 |
+
"scoreboard.add(result_4_4a)\n",
|
| 1826 |
+
""
|
| 1827 |
+
],
|
| 1828 |
+
"execution_count": null,
|
| 1829 |
+
"outputs": []
|
| 1830 |
+
},
|
| 1831 |
+
{
|
| 1832 |
+
"cell_type": "code",
|
| 1833 |
+
"metadata": {},
|
| 1834 |
+
"source": [
|
| 1835 |
+
"\n",
|
| 1836 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1837 |
+
"# EXPERIMENT 4.4b: M\u00f6bius Flow (Normalizing Flow)\n",
|
| 1838 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1839 |
+
"\n",
|
| 1840 |
+
"class Model_4_4b(nn.Module):\n",
|
| 1841 |
+
" def __init__(self):\n",
|
| 1842 |
+
" super().__init__()\n",
|
| 1843 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1844 |
+
" scatt_dim = 81 * 64\n",
|
| 1845 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1846 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1847 |
+
"\n",
|
| 1848 |
+
" self.moebius_flow = MoebiusFlow(256, num_layers=2, num_anchors_per_layer=32)\n",
|
| 1849 |
+
" self.head = nn.Linear(256, 10)\n",
|
| 1850 |
+
"\n",
|
| 1851 |
+
" def forward(self, images):\n",
|
| 1852 |
+
" scatt = self.scattering(images)\n",
|
| 1853 |
+
" B = scatt.shape[0]\n",
|
| 1854 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1855 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1856 |
+
" embeddings = l2_normalize(self.patchwork(proj))\n",
|
| 1857 |
+
"\n",
|
| 1858 |
+
" flow_embed = self.moebius_flow(embeddings)\n",
|
| 1859 |
+
" logits = self.head(flow_embed)\n",
|
| 1860 |
+
" return logits, embeddings\n",
|
| 1861 |
+
"\n",
|
| 1862 |
+
"model_4_4b = Model_4_4b().to(device)\n",
|
| 1863 |
+
"print(f\"[4.4b] Model created with {sum(p.numel() for p in model_4_4b.parameters() if p.requires_grad):,} params\")\n",
|
| 1864 |
+
"\n",
|
| 1865 |
+
"result_4_4b = run_experiment(\n",
|
| 1866 |
+
" \"4.4b_moebius_flow\",\n",
|
| 1867 |
+
" model_4_4b,\n",
|
| 1868 |
+
" train_loader,\n",
|
| 1869 |
+
" val_loader,\n",
|
| 1870 |
+
" device,\n",
|
| 1871 |
+
" epochs=100,\n",
|
| 1872 |
+
" lr=1e-3,\n",
|
| 1873 |
+
" weight_decay=1e-4,\n",
|
| 1874 |
+
" patience=20\n",
|
| 1875 |
+
")\n",
|
| 1876 |
+
"scoreboard.add(result_4_4b)\n",
|
| 1877 |
+
""
|
| 1878 |
+
],
|
| 1879 |
+
"execution_count": null,
|
| 1880 |
+
"outputs": []
|
| 1881 |
+
},
|
| 1882 |
+
{
|
| 1883 |
+
"cell_type": "code",
|
| 1884 |
+
"metadata": {},
|
| 1885 |
+
"source": [
|
| 1886 |
+
"\n",
|
| 1887 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1888 |
+
"# EXPERIMENT 4.5a: Procrustes Alignment\n",
|
| 1889 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1890 |
+
"\n",
|
| 1891 |
+
"class Model_4_5a(nn.Module):\n",
|
| 1892 |
+
" def __init__(self):\n",
|
| 1893 |
+
" super().__init__()\n",
|
| 1894 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1895 |
+
" scatt_dim = 81 * 64\n",
|
| 1896 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1897 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1898 |
+
"\n",
|
| 1899 |
+
" self.procrustes = ProcrustesAlignmentEncoder(256, num_features=32)\n",
|
| 1900 |
+
" self.patchwork_proc = Patchwork(32, num_blocks=2)\n",
|
| 1901 |
+
" self.head = nn.Linear(32, 10)\n",
|
| 1902 |
+
"\n",
|
| 1903 |
+
" def forward(self, images):\n",
|
| 1904 |
+
" scatt = self.scattering(images)\n",
|
| 1905 |
+
" B = scatt.shape[0]\n",
|
| 1906 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1907 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1908 |
+
" embeddings = l2_normalize(self.patchwork(proj))\n",
|
| 1909 |
+
"\n",
|
| 1910 |
+
" proc_feat = self.procrustes(embeddings)\n",
|
| 1911 |
+
" proc_feat = self.patchwork_proc(proc_feat)\n",
|
| 1912 |
+
" logits = self.head(proc_feat)\n",
|
| 1913 |
+
" return logits, embeddings\n",
|
| 1914 |
+
"\n",
|
| 1915 |
+
"model_4_5a = Model_4_5a().to(device)\n",
|
| 1916 |
+
"print(f\"[4.5a] Model created with {sum(p.numel() for p in model_4_5a.parameters() if p.requires_grad):,} params\")\n",
|
| 1917 |
+
"\n",
|
| 1918 |
+
"result_4_5a = run_experiment(\n",
|
| 1919 |
+
" \"4.5a_procrustes_alignment\",\n",
|
| 1920 |
+
" model_4_5a,\n",
|
| 1921 |
+
" train_loader,\n",
|
| 1922 |
+
" val_loader,\n",
|
| 1923 |
+
" device,\n",
|
| 1924 |
+
" epochs=100,\n",
|
| 1925 |
+
" lr=1e-3,\n",
|
| 1926 |
+
" weight_decay=1e-4,\n",
|
| 1927 |
+
" patience=20\n",
|
| 1928 |
+
")\n",
|
| 1929 |
+
"scoreboard.add(result_4_5a)\n",
|
| 1930 |
+
""
|
| 1931 |
+
],
|
| 1932 |
+
"execution_count": null,
|
| 1933 |
+
"outputs": []
|
| 1934 |
+
},
|
| 1935 |
+
{
|
| 1936 |
+
"cell_type": "code",
|
| 1937 |
+
"metadata": {},
|
| 1938 |
+
"source": [
|
| 1939 |
+
"\n",
|
| 1940 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1941 |
+
"# EXPERIMENT 4.5b: Polar Decomposition Features\n",
|
| 1942 |
+
"# \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 1943 |
+
"\n",
|
| 1944 |
+
"class Model_4_5b(nn.Module):\n",
|
| 1945 |
+
" def __init__(self):\n",
|
| 1946 |
+
" super().__init__()\n",
|
| 1947 |
+
" self.scattering = Scattering2D(J=2, shape=(32, 32))\n",
|
| 1948 |
+
" scatt_dim = 81 * 64\n",
|
| 1949 |
+
" self.scatt_proj = nn.Linear(scatt_dim, 256)\n",
|
| 1950 |
+
" self.patchwork = Patchwork(256, num_blocks=2)\n",
|
| 1951 |
+
"\n",
|
| 1952 |
+
" self.polar_dec = PolarDecompositionFeatures(256, num_features=32)\n",
|
| 1953 |
+
" self.patchwork_polar = Patchwork(64, num_blocks=2) # 2*32\n",
|
| 1954 |
+
" self.head = nn.Linear(64, 10)\n",
|
| 1955 |
+
"\n",
|
| 1956 |
+
" def forward(self, images):\n",
|
| 1957 |
+
" scatt = self.scattering(images)\n",
|
| 1958 |
+
" B = scatt.shape[0]\n",
|
| 1959 |
+
" scatt_flat = scatt.reshape(B, -1)\n",
|
| 1960 |
+
" proj = self.scatt_proj(scatt_flat)\n",
|
| 1961 |
+
" embeddings = l2_normalize(self.patchwork(proj))\n",
|
| 1962 |
+
"\n",
|
| 1963 |
+
" polar_feat = self.polar_dec(embeddings)\n",
|
| 1964 |
+
" polar_feat = self.patchwork_polar(polar_feat)\n",
|
| 1965 |
+
" logits = self.head(polar_feat)\n",
|
| 1966 |
+
" return logits, embeddings\n",
|
| 1967 |
+
"\n",
|
| 1968 |
+
"model_4_5b = Model_4_5b().to(device)\n",
|
| 1969 |
+
"print(f\"[4.5b] Model created with {sum(p.numel() for p in model_4_5b.parameters() if p.requires_grad):,} params\")\n",
|
| 1970 |
+
"\n",
|
| 1971 |
+
"result_4_5b = run_experiment(\n",
|
| 1972 |
+
" \"4.5b_polar_decomposition\",\n",
|
| 1973 |
+
" model_4_5b,\n",
|
| 1974 |
+
" train_loader,\n",
|
| 1975 |
+
" val_loader,\n",
|
| 1976 |
+
" device,\n",
|
| 1977 |
+
" epochs=100,\n",
|
| 1978 |
+
" lr=1e-3,\n",
|
| 1979 |
+
" weight_decay=1e-4,\n",
|
| 1980 |
+
" patience=20\n",
|
| 1981 |
+
")\n",
|
| 1982 |
+
"scoreboard.add(result_4_5b)\n",
|
| 1983 |
+
""
|
| 1984 |
+
],
|
| 1985 |
+
"execution_count": null,
|
| 1986 |
+
"outputs": []
|
| 1987 |
+
},
|
| 1988 |
+
{
|
| 1989 |
+
"cell_type": "markdown",
|
| 1990 |
+
"metadata": {},
|
| 1991 |
+
"source": [
|
| 1992 |
+
"## Final Scoreboard\n",
|
| 1993 |
+
"\n",
|
| 1994 |
+
"All experiments completed. Scoreboard shows results ranked by validation accuracy.\n",
|
| 1995 |
+
""
|
| 1996 |
+
]
|
| 1997 |
+
},
|
| 1998 |
+
{
|
| 1999 |
+
"cell_type": "code",
|
| 2000 |
+
"metadata": {},
|
| 2001 |
+
"source": [
|
| 2002 |
+
"\n",
|
| 2003 |
+
"print(\"\\n\" + \"=\"*80)\n",
|
| 2004 |
+
"print(\"FINAL RESULTS \u2014 GeoLIP Category 4: Invertible Geometric Transforms\")\n",
|
| 2005 |
+
"print(\"=\"*80)\n",
|
| 2006 |
+
"scoreboard._print_table()\n",
|
| 2007 |
+
""
|
| 2008 |
+
],
|
| 2009 |
+
"execution_count": null,
|
| 2010 |
+
"outputs": []
|
| 2011 |
+
}
|
| 2012 |
+
]
|
| 2013 |
+
}
|
spectral/experiment_5_matrix_decompositions.ipynb
ADDED
|
@@ -0,0 +1,1118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 5,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"kernelspec": {
|
| 6 |
+
"display_name": "Python 3",
|
| 7 |
+
"language": "python",
|
| 8 |
+
"name": "python3"
|
| 9 |
+
},
|
| 10 |
+
"language_info": {
|
| 11 |
+
"name": "python",
|
| 12 |
+
"version": "3.10.0"
|
| 13 |
+
},
|
| 14 |
+
"accelerator": "GPU",
|
| 15 |
+
"colab": {
|
| 16 |
+
"provenance": [],
|
| 17 |
+
"gpuType": "T4"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"cells": [
|
| 21 |
+
{
|
| 22 |
+
"cell_type": "markdown",
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"source": [
|
| 25 |
+
"# Experiment 5: Matrix Decomposition Signatures\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"Decomposing STFT-derived Gram matrices for geometric feature extraction."
|
| 28 |
+
]
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"cell_type": "code",
|
| 32 |
+
"metadata": {},
|
| 33 |
+
"source": [
|
| 34 |
+
"\n",
|
| 35 |
+
"!pip install -q kymatio scipy tensorboard scikit-learn matplotlib numpy torch torchvision torchaudio tensorboard\n",
|
| 36 |
+
""
|
| 37 |
+
],
|
| 38 |
+
"execution_count": null,
|
| 39 |
+
"outputs": []
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"cell_type": "markdown",
|
| 43 |
+
"metadata": {},
|
| 44 |
+
"source": [
|
| 45 |
+
"## Shared Utilities"
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"cell_type": "code",
|
| 50 |
+
"metadata": {},
|
| 51 |
+
"source": [
|
| 52 |
+
"\n",
|
| 53 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 54 |
+
"# SHARED UTILITIES \u2014 GeoLIP Hypersphere Experiments\n",
|
| 55 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 56 |
+
"import os, sys, time, math, json, datetime, warnings\n",
|
| 57 |
+
"import numpy as np\n",
|
| 58 |
+
"import torch\n",
|
| 59 |
+
"import torch.nn as nn\n",
|
| 60 |
+
"import torch.nn.functional as F\n",
|
| 61 |
+
"from torch.utils.data import DataLoader\n",
|
| 62 |
+
"from torch.utils.tensorboard import SummaryWriter\n",
|
| 63 |
+
"import torchvision\n",
|
| 64 |
+
"import torchvision.transforms as T\n",
|
| 65 |
+
"import matplotlib\n",
|
| 66 |
+
"matplotlib.use(\"Agg\")\n",
|
| 67 |
+
"import matplotlib.pyplot as plt\n",
|
| 68 |
+
"from collections import defaultdict\n",
|
| 69 |
+
"\n",
|
| 70 |
+
"warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
|
| 71 |
+
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 72 |
+
"print(f\"[DEVICE] {device}\" + (f\" \u2014 {torch.cuda.get_device_name()}\" if device.type == \"cuda\" else \"\"))\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"# \u2500\u2500 CIFAR-10 Loaders \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 75 |
+
"CIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\n",
|
| 76 |
+
"CIFAR10_STD = (0.2470, 0.2435, 0.2616)\n",
|
| 77 |
+
"CIFAR10_CLASSES = (\"plane\",\"car\",\"bird\",\"cat\",\"deer\",\"dog\",\"frog\",\"horse\",\"ship\",\"truck\")\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"def get_cifar10_loaders(batch_size=256, num_workers=2, augment=True):\n",
|
| 80 |
+
" \"\"\"Return train_loader, val_loader for CIFAR-10.\"\"\"\n",
|
| 81 |
+
" tf_train = T.Compose([\n",
|
| 82 |
+
" T.RandomCrop(32, padding=4),\n",
|
| 83 |
+
" T.RandomHorizontalFlip(),\n",
|
| 84 |
+
" T.ToTensor(),\n",
|
| 85 |
+
" T.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n",
|
| 86 |
+
" ]) if augment else T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 87 |
+
" tf_val = T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 88 |
+
"\n",
|
| 89 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf_train)\n",
|
| 90 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf_val)\n",
|
| 91 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 92 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 93 |
+
" print(f\"[DATA] CIFAR-10: {len(train_ds)} train, {len(val_ds)} val, batch_size={batch_size}\")\n",
|
| 94 |
+
" return train_loader, val_loader\n",
|
| 95 |
+
"\n",
|
| 96 |
+
"def get_cifar10_raw_loaders(batch_size=256, num_workers=2):\n",
|
| 97 |
+
" \"\"\"Return loaders with only ToTensor (no normalization) for custom front-ends.\"\"\"\n",
|
| 98 |
+
" tf = T.Compose([T.ToTensor()])\n",
|
| 99 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf)\n",
|
| 100 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf)\n",
|
| 101 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 102 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 103 |
+
" print(f\"[DATA] CIFAR-10 (raw): {len(train_ds)} train, {len(val_ds)} val\")\n",
|
| 104 |
+
" return train_loader, val_loader\n",
|
| 105 |
+
"\n",
|
| 106 |
+
"# \u2500\u2500 L2 Normalization to Hypersphere \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 107 |
+
"def l2_normalize(x, dim=-1, eps=1e-8):\n",
|
| 108 |
+
" \"\"\"Project to unit hypersphere S^{d-1}.\"\"\"\n",
|
| 109 |
+
" return F.normalize(x, p=2, dim=dim, eps=eps)\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"# \u2500\u2500 Constellation Module \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 112 |
+
"class Constellation(nn.Module):\n",
|
| 113 |
+
" \"\"\"\n",
|
| 114 |
+
" K anchors on S^{d-1}. Triangulation: compute distance from input to each anchor.\n",
|
| 115 |
+
" Output: K-dim vector of (1 - cos_sim) distances = triangulation profile.\n",
|
| 116 |
+
" \"\"\"\n",
|
| 117 |
+
" def __init__(self, embed_dim, num_anchors=64, init=\"uniform\"):\n",
|
| 118 |
+
" super().__init__()\n",
|
| 119 |
+
" self.embed_dim = embed_dim\n",
|
| 120 |
+
" self.num_anchors = num_anchors\n",
|
| 121 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, init)\n",
|
| 122 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False) # anchors are NOT learned\n",
|
| 123 |
+
"\n",
|
| 124 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 125 |
+
" if method == \"uniform\":\n",
|
| 126 |
+
" # QR-based uniform hypersphere initialization\n",
|
| 127 |
+
" A = torch.randn(K, d)\n",
|
| 128 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 129 |
+
" anchors = Q.T[:K]\n",
|
| 130 |
+
" # If K > d, fill remaining with random unit vectors\n",
|
| 131 |
+
" if K > d:\n",
|
| 132 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 133 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 134 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 135 |
+
" elif method == \"random\":\n",
|
| 136 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 137 |
+
" else:\n",
|
| 138 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 139 |
+
"\n",
|
| 140 |
+
" def forward(self, x):\n",
|
| 141 |
+
" \"\"\"x: (B, d) on S^{d-1} \u2192 (B, K) triangulation distances.\"\"\"\n",
|
| 142 |
+
" x_norm = l2_normalize(x)\n",
|
| 143 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T) # (B, K)\n",
|
| 144 |
+
" distances = 1.0 - cos_sim # in [0, 2]\n",
|
| 145 |
+
" return distances\n",
|
| 146 |
+
"\n",
|
| 147 |
+
" def push_anchors(self, embeddings, labels, num_classes=10, momentum=0.1):\n",
|
| 148 |
+
" \"\"\"Push anchors toward class centroids (call periodically).\"\"\"\n",
|
| 149 |
+
" with torch.no_grad():\n",
|
| 150 |
+
" for c in range(num_classes):\n",
|
| 151 |
+
" mask = (labels == c)\n",
|
| 152 |
+
" if mask.sum() < 2:\n",
|
| 153 |
+
" continue\n",
|
| 154 |
+
" centroid = l2_normalize(embeddings[mask].mean(dim=0, keepdim=True))\n",
|
| 155 |
+
" # Assign anchors to this class based on nearest centroid\n",
|
| 156 |
+
" start = c * (self.num_anchors // num_classes)\n",
|
| 157 |
+
" end = start + (self.num_anchors // num_classes)\n",
|
| 158 |
+
" self.anchors.data[start:end] = l2_normalize(\n",
|
| 159 |
+
" (1 - momentum) * self.anchors.data[start:end] + momentum * centroid\n",
|
| 160 |
+
" )\n",
|
| 161 |
+
"\n",
|
| 162 |
+
" def anchor_activation(self, x):\n",
|
| 163 |
+
" \"\"\"Return fraction of anchors that are 'active' (nearest to at least one sample).\"\"\"\n",
|
| 164 |
+
" x_norm = l2_normalize(x)\n",
|
| 165 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 166 |
+
" nearest = cos_sim.argmax(dim=1)\n",
|
| 167 |
+
" active = len(nearest.unique())\n",
|
| 168 |
+
" return active, self.num_anchors\n",
|
| 169 |
+
"\n",
|
| 170 |
+
"# \u2500\u2500 Patchwork Block \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 171 |
+
"class SquaredReLU(nn.Module):\n",
|
| 172 |
+
" def forward(self, x):\n",
|
| 173 |
+
" return F.relu(x) ** 2\n",
|
| 174 |
+
"\n",
|
| 175 |
+
"class PatchworkBlock(nn.Module):\n",
|
| 176 |
+
" \"\"\"SquaredReLU + LayerNorm block for post-constellation processing.\"\"\"\n",
|
| 177 |
+
" def __init__(self, dim):\n",
|
| 178 |
+
" super().__init__()\n",
|
| 179 |
+
" self.net = nn.Sequential(\n",
|
| 180 |
+
" nn.Linear(dim, dim),\n",
|
| 181 |
+
" SquaredReLU(),\n",
|
| 182 |
+
" nn.LayerNorm(dim),\n",
|
| 183 |
+
" nn.Linear(dim, dim),\n",
|
| 184 |
+
" SquaredReLU(),\n",
|
| 185 |
+
" nn.LayerNorm(dim),\n",
|
| 186 |
+
" )\n",
|
| 187 |
+
" def forward(self, x):\n",
|
| 188 |
+
" return x + self.net(x) # residual\n",
|
| 189 |
+
"\n",
|
| 190 |
+
"class Patchwork(nn.Module):\n",
|
| 191 |
+
" \"\"\"Stack of PatchworkBlocks.\"\"\"\n",
|
| 192 |
+
" def __init__(self, dim, num_blocks=2):\n",
|
| 193 |
+
" super().__init__()\n",
|
| 194 |
+
" self.blocks = nn.Sequential(*[PatchworkBlock(dim) for _ in range(num_blocks)])\n",
|
| 195 |
+
" def forward(self, x):\n",
|
| 196 |
+
" return self.blocks(x)\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"# \u2500\u2500 Classifier Head \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 199 |
+
"class ConstellationClassifier(nn.Module):\n",
|
| 200 |
+
" \"\"\"Full pipeline: encoder output \u2192 constellation \u2192 patchwork \u2192 classify.\"\"\"\n",
|
| 201 |
+
" def __init__(self, embed_dim, num_anchors=64, num_classes=10, patchwork_blocks=2):\n",
|
| 202 |
+
" super().__init__()\n",
|
| 203 |
+
" self.constellation = Constellation(embed_dim, num_anchors)\n",
|
| 204 |
+
" self.patchwork = Patchwork(num_anchors, patchwork_blocks)\n",
|
| 205 |
+
" self.head = nn.Linear(num_anchors, num_classes)\n",
|
| 206 |
+
"\n",
|
| 207 |
+
" def forward(self, embeddings):\n",
|
| 208 |
+
" tri = self.constellation(embeddings) # (B, K)\n",
|
| 209 |
+
" pw = self.patchwork(tri) # (B, K)\n",
|
| 210 |
+
" logits = self.head(pw) # (B, 10)\n",
|
| 211 |
+
" return logits, tri\n",
|
| 212 |
+
"\n",
|
| 213 |
+
" def count_params(self):\n",
|
| 214 |
+
" return sum(p.numel() for p in self.parameters() if p.requires_grad)\n",
|
| 215 |
+
"\n",
|
| 216 |
+
"# \u2500\u2500 Losses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 217 |
+
"class CayleyMengerVolumeLoss(nn.Module):\n",
|
| 218 |
+
" \"\"\"CV loss: encourage constellation volume in 0.18-0.25 band.\"\"\"\n",
|
| 219 |
+
" def __init__(self, target_low=0.18, target_high=0.25):\n",
|
| 220 |
+
" super().__init__()\n",
|
| 221 |
+
" self.target_low = target_low\n",
|
| 222 |
+
" self.target_high = target_high\n",
|
| 223 |
+
"\n",
|
| 224 |
+
" def forward(self, tri):\n",
|
| 225 |
+
" \"\"\"tri: (B, K) triangulation distances.\"\"\"\n",
|
| 226 |
+
" # Approximate volume via determinant of distance submatrix\n",
|
| 227 |
+
" B, K = tri.shape\n",
|
| 228 |
+
" # Use std of triangulation as proxy for volume spread\n",
|
| 229 |
+
" vol = tri.std(dim=1).mean()\n",
|
| 230 |
+
" # Penalty if outside target band\n",
|
| 231 |
+
" below = F.relu(self.target_low - vol)\n",
|
| 232 |
+
" above = F.relu(vol - self.target_high)\n",
|
| 233 |
+
" return below + above, vol.item()\n",
|
| 234 |
+
"\n",
|
| 235 |
+
"class InfoNCELoss(nn.Module):\n",
|
| 236 |
+
" \"\"\"Self-supervised contrastive loss on augmented views.\"\"\"\n",
|
| 237 |
+
" def __init__(self, temperature=0.07):\n",
|
| 238 |
+
" super().__init__()\n",
|
| 239 |
+
" self.temperature = temperature\n",
|
| 240 |
+
"\n",
|
| 241 |
+
" def forward(self, z1, z2):\n",
|
| 242 |
+
" \"\"\"z1, z2: (B, d) L2-normalized embeddings of two augmented views.\"\"\"\n",
|
| 243 |
+
" z1 = l2_normalize(z1)\n",
|
| 244 |
+
" z2 = l2_normalize(z2)\n",
|
| 245 |
+
" B = z1.shape[0]\n",
|
| 246 |
+
" z = torch.cat([z1, z2], dim=0) # (2B, d)\n",
|
| 247 |
+
" sim = torch.mm(z, z.T) / self.temperature # (2B, 2B)\n",
|
| 248 |
+
" # Mask out self-similarity\n",
|
| 249 |
+
" mask = ~torch.eye(2*B, dtype=bool, device=z.device)\n",
|
| 250 |
+
" sim = sim.masked_fill(~mask, -1e9)\n",
|
| 251 |
+
" # Positive pairs: (i, i+B) and (i+B, i)\n",
|
| 252 |
+
" labels = torch.cat([torch.arange(B, 2*B), torch.arange(B)]).to(z.device)\n",
|
| 253 |
+
" loss = F.cross_entropy(sim, labels)\n",
|
| 254 |
+
" return loss\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"class ClassConditionalInfoNCE(nn.Module):\n",
|
| 257 |
+
" \"\"\"Class-conditional contrastive: positive = same class, not same image.\"\"\"\n",
|
| 258 |
+
" def __init__(self, temperature=0.07):\n",
|
| 259 |
+
" super().__init__()\n",
|
| 260 |
+
" self.temperature = temperature\n",
|
| 261 |
+
"\n",
|
| 262 |
+
" def forward(self, z, labels):\n",
|
| 263 |
+
" z = l2_normalize(z)\n",
|
| 264 |
+
" B = z.shape[0]\n",
|
| 265 |
+
" sim = torch.mm(z, z.T) / self.temperature\n",
|
| 266 |
+
" # Mask: positive if same class, negative otherwise\n",
|
| 267 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 268 |
+
" label_mask.fill_diagonal_(0) # exclude self\n",
|
| 269 |
+
" # For each sample, compute log-softmax over all non-self, weighted by positives\n",
|
| 270 |
+
" logits = sim - torch.diag(torch.full((B,), 1e9, device=z.device))\n",
|
| 271 |
+
" pos_mask = label_mask\n",
|
| 272 |
+
" neg_mask = 1 - label_mask\n",
|
| 273 |
+
" neg_mask.fill_diagonal_(0)\n",
|
| 274 |
+
" # InfoNCE: -log(sum_pos exp(sim) / sum_all exp(sim))\n",
|
| 275 |
+
" exp_sim = torch.exp(logits)\n",
|
| 276 |
+
" pos_sum = (exp_sim * pos_mask).sum(dim=1)\n",
|
| 277 |
+
" all_sum = (exp_sim * (pos_mask + neg_mask)).sum(dim=1)\n",
|
| 278 |
+
" loss = -torch.log(pos_sum / (all_sum + 1e-8) + 1e-8).mean()\n",
|
| 279 |
+
" return loss\n",
|
| 280 |
+
"\n",
|
| 281 |
+
"# \u2500\u2500 TensorBoard Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 282 |
+
"def make_writer(experiment_id, base_dir=\"runs\"):\n",
|
| 283 |
+
" \"\"\"Create a TensorBoard SummaryWriter with timestamped run directory.\"\"\"\n",
|
| 284 |
+
" timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
|
| 285 |
+
" log_dir = os.path.join(base_dir, f\"{experiment_id}_{timestamp}\")\n",
|
| 286 |
+
" writer = SummaryWriter(log_dir=log_dir)\n",
|
| 287 |
+
" print(f\"[TB] Logging to {log_dir}\")\n",
|
| 288 |
+
" return writer\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"def log_epoch_metrics(writer, epoch, metrics_dict, prefix=\"\"):\n",
|
| 291 |
+
" \"\"\"Log a dict of metrics to TensorBoard.\"\"\"\n",
|
| 292 |
+
" for key, val in metrics_dict.items():\n",
|
| 293 |
+
" tag = f\"{prefix}/{key}\" if prefix else key\n",
|
| 294 |
+
" writer.add_scalar(tag, val, epoch)\n",
|
| 295 |
+
"\n",
|
| 296 |
+
"def log_embedding_viz(writer, embeddings, labels, epoch, tag=\"embeddings\"):\n",
|
| 297 |
+
" \"\"\"Log embeddings for TensorBoard projector.\"\"\"\n",
|
| 298 |
+
" meta = [CIFAR10_CLASSES[l] for l in labels]\n",
|
| 299 |
+
" writer.add_embedding(embeddings, metadata=meta, global_step=epoch, tag=tag)\n",
|
| 300 |
+
"\n",
|
| 301 |
+
"def log_anchor_histogram(writer, constellation, epoch):\n",
|
| 302 |
+
" \"\"\"Log anchor norm distribution.\"\"\"\n",
|
| 303 |
+
" norms = constellation.anchors.data.norm(dim=1)\n",
|
| 304 |
+
" writer.add_histogram(\"anchors/norms\", norms, epoch)\n",
|
| 305 |
+
" cos_sim_matrix = torch.mm(constellation.anchors.data, constellation.anchors.data.T)\n",
|
| 306 |
+
" off_diag = cos_sim_matrix[~torch.eye(cos_sim_matrix.shape[0], dtype=bool)].flatten()\n",
|
| 307 |
+
" writer.add_histogram(\"anchors/inter_cosine_sim\", off_diag, epoch)\n",
|
| 308 |
+
"\n",
|
| 309 |
+
"def log_confusion_matrix(writer, all_preds, all_labels, epoch):\n",
|
| 310 |
+
" \"\"\"Log confusion matrix as image to TensorBoard.\"\"\"\n",
|
| 311 |
+
" from sklearn.metrics import confusion_matrix\n",
|
| 312 |
+
" cm = confusion_matrix(all_labels, all_preds, labels=range(10))\n",
|
| 313 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 314 |
+
" im = ax.imshow(cm, interpolation=\"nearest\", cmap=plt.cm.Blues)\n",
|
| 315 |
+
" ax.set_title(f\"Confusion Matrix \u2014 Epoch {epoch}\")\n",
|
| 316 |
+
" ax.set_xlabel(\"Predicted\")\n",
|
| 317 |
+
" ax.set_ylabel(\"True\")\n",
|
| 318 |
+
" tick_marks = range(10)\n",
|
| 319 |
+
" ax.set_xticks(tick_marks)\n",
|
| 320 |
+
" ax.set_xticklabels(CIFAR10_CLASSES, rotation=45, ha=\"right\")\n",
|
| 321 |
+
" ax.set_yticks(tick_marks)\n",
|
| 322 |
+
" ax.set_yticklabels(CIFAR10_CLASSES)\n",
|
| 323 |
+
" for i in range(10):\n",
|
| 324 |
+
" for j in range(10):\n",
|
| 325 |
+
" ax.text(j, i, str(cm[i, j]), ha=\"center\", va=\"center\",\n",
|
| 326 |
+
" color=\"white\" if cm[i, j] > cm.max() / 2 else \"black\", fontsize=7)\n",
|
| 327 |
+
" fig.tight_layout()\n",
|
| 328 |
+
" writer.add_figure(\"confusion_matrix\", fig, epoch)\n",
|
| 329 |
+
" plt.close(fig)\n",
|
| 330 |
+
"\n",
|
| 331 |
+
"def log_triangulation_stats(writer, tri, epoch):\n",
|
| 332 |
+
" \"\"\"Log triangulation distance statistics.\"\"\"\n",
|
| 333 |
+
" writer.add_scalar(\"tri/mean\", tri.mean().item(), epoch)\n",
|
| 334 |
+
" writer.add_scalar(\"tri/std\", tri.std().item(), epoch)\n",
|
| 335 |
+
" writer.add_scalar(\"tri/min\", tri.min().item(), epoch)\n",
|
| 336 |
+
" writer.add_scalar(\"tri/max\", tri.max().item(), epoch)\n",
|
| 337 |
+
" writer.add_histogram(\"tri/distances\", tri.detach().cpu(), epoch)\n",
|
| 338 |
+
"\n",
|
| 339 |
+
"def log_scatter_2d(writer, embeddings, labels, epoch, tag=\"scatter_2d\", method=\"pca\"):\n",
|
| 340 |
+
" \"\"\"Log 2D scatter plot of embeddings projected via PCA or t-SNE.\"\"\"\n",
|
| 341 |
+
" emb_np = embeddings.detach().cpu().numpy()\n",
|
| 342 |
+
" lab_np = labels.detach().cpu().numpy() if torch.is_tensor(labels) else np.array(labels)\n",
|
| 343 |
+
" if method == \"pca\":\n",
|
| 344 |
+
" from sklearn.decomposition import PCA\n",
|
| 345 |
+
" proj = PCA(n_components=2).fit_transform(emb_np)\n",
|
| 346 |
+
" else:\n",
|
| 347 |
+
" from sklearn.manifold import TSNE\n",
|
| 348 |
+
" proj = TSNE(n_components=2, perplexity=30, n_iter=500).fit_transform(emb_np)\n",
|
| 349 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 350 |
+
" for c in range(10):\n",
|
| 351 |
+
" mask = lab_np == c\n",
|
| 352 |
+
" ax.scatter(proj[mask, 0], proj[mask, 1], s=3, alpha=0.5, label=CIFAR10_CLASSES[c])\n",
|
| 353 |
+
" ax.legend(markerscale=5, fontsize=8)\n",
|
| 354 |
+
" ax.set_title(f\"{tag} ({method.upper()}) \u2014 Epoch {epoch}\")\n",
|
| 355 |
+
" fig.tight_layout()\n",
|
| 356 |
+
" writer.add_figure(f\"{tag}/{method}\", fig, epoch)\n",
|
| 357 |
+
" plt.close(fig)\n",
|
| 358 |
+
"\n",
|
| 359 |
+
"# \u2500\u2500 Training Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 360 |
+
"def train_one_epoch(model, loader, optimizer, criterion, device, epoch=0,\n",
|
| 361 |
+
" scheduler=None, extra_losses=None):\n",
|
| 362 |
+
" \"\"\"\n",
|
| 363 |
+
" Generic training loop for one epoch.\n",
|
| 364 |
+
" model: callable(batch) \u2192 (logits, embeddings_on_sphere)\n",
|
| 365 |
+
" extra_losses: list of (name, loss_fn, weight) where loss_fn(embeddings) \u2192 scalar\n",
|
| 366 |
+
" \"\"\"\n",
|
| 367 |
+
" model.train()\n",
|
| 368 |
+
" total_loss = 0\n",
|
| 369 |
+
" total_ce = 0\n",
|
| 370 |
+
" correct = 0\n",
|
| 371 |
+
" total = 0\n",
|
| 372 |
+
" loss_components = defaultdict(float)\n",
|
| 373 |
+
"\n",
|
| 374 |
+
" for batch_idx, (images, labels) in enumerate(loader):\n",
|
| 375 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 376 |
+
" optimizer.zero_grad()\n",
|
| 377 |
+
"\n",
|
| 378 |
+
" logits, embeddings = model(images)\n",
|
| 379 |
+
" ce_loss = criterion(logits, labels)\n",
|
| 380 |
+
" loss = ce_loss\n",
|
| 381 |
+
" loss_components[\"CE\"] += ce_loss.item()\n",
|
| 382 |
+
"\n",
|
| 383 |
+
" if extra_losses:\n",
|
| 384 |
+
" for name, loss_fn, weight in extra_losses:\n",
|
| 385 |
+
" if name == \"CV\":\n",
|
| 386 |
+
" el, vol_val = loss_fn(embeddings)\n",
|
| 387 |
+
" loss_components[\"CV_vol\"] += vol_val\n",
|
| 388 |
+
" elif name == \"InfoNCE_class\":\n",
|
| 389 |
+
" el = loss_fn(embeddings, labels)\n",
|
| 390 |
+
" else:\n",
|
| 391 |
+
" el = loss_fn(embeddings)\n",
|
| 392 |
+
" loss = loss + weight * el\n",
|
| 393 |
+
" loss_components[name] += el.item()\n",
|
| 394 |
+
"\n",
|
| 395 |
+
" loss.backward()\n",
|
| 396 |
+
" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n",
|
| 397 |
+
" optimizer.step()\n",
|
| 398 |
+
" if scheduler:\n",
|
| 399 |
+
" scheduler.step()\n",
|
| 400 |
+
"\n",
|
| 401 |
+
" total_loss += loss.item()\n",
|
| 402 |
+
" _, predicted = logits.max(1)\n",
|
| 403 |
+
" total += labels.size(0)\n",
|
| 404 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 405 |
+
"\n",
|
| 406 |
+
" n_batches = len(loader)\n",
|
| 407 |
+
" metrics = {\n",
|
| 408 |
+
" \"loss/total\": total_loss / n_batches,\n",
|
| 409 |
+
" \"loss/CE\": loss_components[\"CE\"] / n_batches,\n",
|
| 410 |
+
" \"accuracy/train\": 100.0 * correct / total,\n",
|
| 411 |
+
" }\n",
|
| 412 |
+
" for k, v in loss_components.items():\n",
|
| 413 |
+
" if k not in (\"CE\",):\n",
|
| 414 |
+
" metrics[f\"loss/{k}\"] = v / n_batches\n",
|
| 415 |
+
" return metrics\n",
|
| 416 |
+
"\n",
|
| 417 |
+
"@torch.no_grad()\n",
|
| 418 |
+
"def evaluate(model, loader, criterion, device, collect_embeddings=False):\n",
|
| 419 |
+
" \"\"\"Evaluate model on validation set. Optionally collect embeddings.\"\"\"\n",
|
| 420 |
+
" model.eval()\n",
|
| 421 |
+
" total_loss = 0\n",
|
| 422 |
+
" correct = 0\n",
|
| 423 |
+
" total = 0\n",
|
| 424 |
+
" all_preds = []\n",
|
| 425 |
+
" all_labels = []\n",
|
| 426 |
+
" all_embeddings = []\n",
|
| 427 |
+
"\n",
|
| 428 |
+
" for images, labels in loader:\n",
|
| 429 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 430 |
+
" logits, embeddings = model(images)\n",
|
| 431 |
+
" loss = criterion(logits, labels)\n",
|
| 432 |
+
" total_loss += loss.item()\n",
|
| 433 |
+
" _, predicted = logits.max(1)\n",
|
| 434 |
+
" total += labels.size(0)\n",
|
| 435 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 436 |
+
" all_preds.extend(predicted.cpu().numpy())\n",
|
| 437 |
+
" all_labels.extend(labels.cpu().numpy())\n",
|
| 438 |
+
" if collect_embeddings:\n",
|
| 439 |
+
" all_embeddings.append(embeddings.cpu())\n",
|
| 440 |
+
"\n",
|
| 441 |
+
" n_batches = len(loader)\n",
|
| 442 |
+
" metrics = {\n",
|
| 443 |
+
" \"loss/val\": total_loss / n_batches,\n",
|
| 444 |
+
" \"accuracy/val\": 100.0 * correct / total,\n",
|
| 445 |
+
" }\n",
|
| 446 |
+
" result = {\"metrics\": metrics, \"preds\": np.array(all_preds), \"labels\": np.array(all_labels)}\n",
|
| 447 |
+
" if collect_embeddings:\n",
|
| 448 |
+
" result[\"embeddings\"] = torch.cat(all_embeddings, dim=0)\n",
|
| 449 |
+
" return result\n",
|
| 450 |
+
"\n",
|
| 451 |
+
"def run_experiment(experiment_id, model, train_loader, val_loader, device,\n",
|
| 452 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, extra_losses=None,\n",
|
| 453 |
+
" push_anchors_every=50, log_embeddings_every=25,\n",
|
| 454 |
+
" patience=20):\n",
|
| 455 |
+
" \"\"\"\n",
|
| 456 |
+
" Full experiment runner with TensorBoard logging, early stopping, and diagnostics.\n",
|
| 457 |
+
" model must return (logits, sphere_embeddings).\n",
|
| 458 |
+
" \"\"\"\n",
|
| 459 |
+
" writer = make_writer(experiment_id)\n",
|
| 460 |
+
" criterion = nn.CrossEntropyLoss()\n",
|
| 461 |
+
" optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)\n",
|
| 462 |
+
" scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\n",
|
| 463 |
+
"\n",
|
| 464 |
+
" best_val_acc = 0.0\n",
|
| 465 |
+
" best_epoch = 0\n",
|
| 466 |
+
" no_improve = 0\n",
|
| 467 |
+
"\n",
|
| 468 |
+
" # Log model info\n",
|
| 469 |
+
" n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 470 |
+
" writer.add_text(\"config/params\", f\"Learned params: {n_params:,}\")\n",
|
| 471 |
+
" writer.add_text(\"config/experiment\", experiment_id)\n",
|
| 472 |
+
" print(f\"\\n{'='*70}\")\n",
|
| 473 |
+
" print(f\"[EXP] {experiment_id}\")\n",
|
| 474 |
+
" print(f\"[EXP] Learned params: {n_params:,}, Epochs: {epochs}, LR: {lr}\")\n",
|
| 475 |
+
" print(f\"{'='*70}\")\n",
|
| 476 |
+
"\n",
|
| 477 |
+
" for epoch in range(1, epochs + 1):\n",
|
| 478 |
+
" t0 = time.time()\n",
|
| 479 |
+
"\n",
|
| 480 |
+
" # Train\n",
|
| 481 |
+
" train_metrics = train_one_epoch(\n",
|
| 482 |
+
" model, train_loader, optimizer, criterion, device, epoch,\n",
|
| 483 |
+
" extra_losses=extra_losses\n",
|
| 484 |
+
" )\n",
|
| 485 |
+
"\n",
|
| 486 |
+
" # Evaluate\n",
|
| 487 |
+
" collect_emb = (epoch % log_embeddings_every == 0) or (epoch == epochs)\n",
|
| 488 |
+
" val_result = evaluate(model, val_loader, criterion, device, collect_embeddings=collect_emb)\n",
|
| 489 |
+
" val_metrics = val_result[\"metrics\"]\n",
|
| 490 |
+
"\n",
|
| 491 |
+
" # Combine metrics\n",
|
| 492 |
+
" all_metrics = {**train_metrics, **val_metrics}\n",
|
| 493 |
+
" all_metrics[\"lr\"] = optimizer.param_groups[0][\"lr\"]\n",
|
| 494 |
+
" all_metrics[\"epoch_time\"] = time.time() - t0\n",
|
| 495 |
+
" all_metrics[\"train_val_gap\"] = train_metrics[\"accuracy/train\"] - val_metrics[\"accuracy/val\"]\n",
|
| 496 |
+
"\n",
|
| 497 |
+
" # Constellation diagnostics\n",
|
| 498 |
+
" if hasattr(model, \"constellation\"):\n",
|
| 499 |
+
" constellation = model.constellation\n",
|
| 500 |
+
" elif hasattr(model, \"classifier\") and hasattr(model.classifier, \"constellation\"):\n",
|
| 501 |
+
" constellation = model.classifier.constellation\n",
|
| 502 |
+
" else:\n",
|
| 503 |
+
" constellation = None\n",
|
| 504 |
+
"\n",
|
| 505 |
+
" if constellation is not None:\n",
|
| 506 |
+
" log_anchor_histogram(writer, constellation, epoch)\n",
|
| 507 |
+
"\n",
|
| 508 |
+
" # Push anchors periodically\n",
|
| 509 |
+
" if epoch % push_anchors_every == 0 and \"embeddings\" in val_result:\n",
|
| 510 |
+
" emb = val_result[\"embeddings\"][:2000].to(device)\n",
|
| 511 |
+
" lab = torch.tensor(val_result[\"labels\"][:2000]).to(device)\n",
|
| 512 |
+
" constellation.push_anchors(emb, lab)\n",
|
| 513 |
+
" active, total_anch = constellation.anchor_activation(emb)\n",
|
| 514 |
+
" all_metrics[\"anchors/active\"] = active\n",
|
| 515 |
+
" all_metrics[\"anchors/total\"] = total_anch\n",
|
| 516 |
+
" all_metrics[\"anchors/utilization\"] = active / total_anch\n",
|
| 517 |
+
" print(f\" [ANCHOR PUSH] Active: {active}/{total_anch} ({100*active/total_anch:.1f}%)\")\n",
|
| 518 |
+
"\n",
|
| 519 |
+
" # Log all metrics\n",
|
| 520 |
+
" log_epoch_metrics(writer, epoch, all_metrics)\n",
|
| 521 |
+
" scheduler.step()\n",
|
| 522 |
+
"\n",
|
| 523 |
+
" # Log embeddings visualization\n",
|
| 524 |
+
" if collect_emb and \"embeddings\" in val_result:\n",
|
| 525 |
+
" emb = val_result[\"embeddings\"][:2000]\n",
|
| 526 |
+
" lab = val_result[\"labels\"][:2000]\n",
|
| 527 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings\", method=\"pca\")\n",
|
| 528 |
+
" if epoch == epochs:\n",
|
| 529 |
+
" log_confusion_matrix(writer, val_result[\"preds\"], val_result[\"labels\"], epoch)\n",
|
| 530 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings_tsne\", method=\"tsne\")\n",
|
| 531 |
+
"\n",
|
| 532 |
+
" # Log triangulation stats from a sample batch\n",
|
| 533 |
+
" if constellation is not None and collect_emb and \"embeddings\" in val_result:\n",
|
| 534 |
+
" sample_emb = val_result[\"embeddings\"][:500].to(device)\n",
|
| 535 |
+
" tri_sample = constellation(sample_emb)\n",
|
| 536 |
+
" log_triangulation_stats(writer, tri_sample, epoch)\n",
|
| 537 |
+
"\n",
|
| 538 |
+
" # Best model tracking\n",
|
| 539 |
+
" val_acc = val_metrics[\"accuracy/val\"]\n",
|
| 540 |
+
" if val_acc > best_val_acc:\n",
|
| 541 |
+
" best_val_acc = val_acc\n",
|
| 542 |
+
" best_epoch = epoch\n",
|
| 543 |
+
" no_improve = 0\n",
|
| 544 |
+
" writer.add_scalar(\"best/val_acc\", best_val_acc, epoch)\n",
|
| 545 |
+
" writer.add_scalar(\"best/epoch\", best_epoch, epoch)\n",
|
| 546 |
+
" else:\n",
|
| 547 |
+
" no_improve += 1\n",
|
| 548 |
+
"\n",
|
| 549 |
+
" # Print progress\n",
|
| 550 |
+
" if epoch % 5 == 0 or epoch == 1 or epoch == epochs:\n",
|
| 551 |
+
" print(f\" E{epoch:3d} | TrainAcc {train_metrics['accuracy/train']:5.1f}% | \"\n",
|
| 552 |
+
" f\"ValAcc {val_acc:5.1f}% | Loss {all_metrics['loss/total']:.4f} | \"\n",
|
| 553 |
+
" f\"Best {best_val_acc:.1f}% @E{best_epoch} | \"\n",
|
| 554 |
+
" f\"Gap {all_metrics['train_val_gap']:+.1f}% | \"\n",
|
| 555 |
+
" f\"{all_metrics['epoch_time']:.1f}s\")\n",
|
| 556 |
+
"\n",
|
| 557 |
+
" # Early stopping\n",
|
| 558 |
+
" if no_improve >= patience:\n",
|
| 559 |
+
" print(f\" [EARLY STOP] No improvement for {patience} epochs. Best: {best_val_acc:.1f}% @E{best_epoch}\")\n",
|
| 560 |
+
" break\n",
|
| 561 |
+
"\n",
|
| 562 |
+
" writer.close()\n",
|
| 563 |
+
" print(f\"\\n[RESULT] {experiment_id}: Best Val Acc = {best_val_acc:.2f}% at epoch {best_epoch}\")\n",
|
| 564 |
+
" print(f\"[RESULT] Learned params: {n_params:,}\")\n",
|
| 565 |
+
" return {\"experiment\": experiment_id, \"best_val_acc\": best_val_acc, \"best_epoch\": best_epoch,\n",
|
| 566 |
+
" \"params\": n_params, \"final_train_acc\": train_metrics[\"accuracy/train\"]}\n",
|
| 567 |
+
"\n",
|
| 568 |
+
"# \u2500\u2500 Johnson-Lindenstrauss Projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 569 |
+
"class JLProjection(nn.Module):\n",
|
| 570 |
+
" \"\"\"Fixed random Gaussian projection (not learned).\"\"\"\n",
|
| 571 |
+
" def __init__(self, input_dim, output_dim):\n",
|
| 572 |
+
" super().__init__()\n",
|
| 573 |
+
" self.register_buffer(\"W\", torch.randn(input_dim, output_dim) / math.sqrt(output_dim))\n",
|
| 574 |
+
"\n",
|
| 575 |
+
" def forward(self, x):\n",
|
| 576 |
+
" return x @ self.W\n",
|
| 577 |
+
"\n",
|
| 578 |
+
"# \u2500\u2500 Results Scoreboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 579 |
+
"class Scoreboard:\n",
|
| 580 |
+
" \"\"\"Track results across experiments in a session.\"\"\"\n",
|
| 581 |
+
" def __init__(self):\n",
|
| 582 |
+
" self.results = []\n",
|
| 583 |
+
"\n",
|
| 584 |
+
" def add(self, result_dict):\n",
|
| 585 |
+
" self.results.append(result_dict)\n",
|
| 586 |
+
" self._print_table()\n",
|
| 587 |
+
"\n",
|
| 588 |
+
" def _print_table(self):\n",
|
| 589 |
+
" print(f\"\\n{'='*80}\")\n",
|
| 590 |
+
" print(f\"{'SCOREBOARD':^80}\")\n",
|
| 591 |
+
" print(f\"{'='*80}\")\n",
|
| 592 |
+
" print(f\"{'Experiment':<40} {'Val Acc':>8} {'Params':>10} {'Best E':>7}\")\n",
|
| 593 |
+
" print(f\"{'-'*40} {'-'*8} {'-'*10} {'-'*7}\")\n",
|
| 594 |
+
" for r in sorted(self.results, key=lambda x: x[\"best_val_acc\"], reverse=True):\n",
|
| 595 |
+
" print(f\"{r['experiment']:<40} {r['best_val_acc']:>7.2f}% {r['params']:>10,} {r['best_epoch']:>7}\")\n",
|
| 596 |
+
" print(f\"{'='*80}\\n\")\n",
|
| 597 |
+
"\n",
|
| 598 |
+
" def save(self, path=\"scoreboard.json\"):\n",
|
| 599 |
+
" with open(path, \"w\") as f:\n",
|
| 600 |
+
" json.dump(self.results, f, indent=2)\n",
|
| 601 |
+
" print(f\"[SCOREBOARD] Saved to {path}\")\n",
|
| 602 |
+
"\n",
|
| 603 |
+
"scoreboard = Scoreboard()\n",
|
| 604 |
+
"print(\"[SHARED] All utilities loaded successfully.\")\n",
|
| 605 |
+
""
|
| 606 |
+
],
|
| 607 |
+
"execution_count": null,
|
| 608 |
+
"outputs": []
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"cell_type": "markdown",
|
| 612 |
+
"metadata": {},
|
| 613 |
+
"source": [
|
| 614 |
+
"## 5.2a: QR Decomposition Signatures\n",
|
| 615 |
+
"\n",
|
| 616 |
+
"Rotation matrix Q + diagonal scale R \u2192 features"
|
| 617 |
+
]
|
| 618 |
+
},
|
| 619 |
+
{
|
| 620 |
+
"cell_type": "code",
|
| 621 |
+
"metadata": {},
|
| 622 |
+
"source": [
|
| 623 |
+
"\n",
|
| 624 |
+
"import torch\n",
|
| 625 |
+
"import torch.nn as nn\n",
|
| 626 |
+
"import torch.nn.functional as F\n",
|
| 627 |
+
"import numpy as np\n",
|
| 628 |
+
"\n",
|
| 629 |
+
"print(\"[5.2a] QR Decomposition Signatures\")\n",
|
| 630 |
+
"print(\"=\"*70)\n",
|
| 631 |
+
"\n",
|
| 632 |
+
"class STFTFrontend(nn.Module):\n",
|
| 633 |
+
" '''STFT front-end: row-wise + col-wise STFT on each channel.'''\n",
|
| 634 |
+
" def __init__(self, n_fft=16, hop_length=4, freq_bins=8):\n",
|
| 635 |
+
" super().__init__()\n",
|
| 636 |
+
" self.n_fft = n_fft\n",
|
| 637 |
+
" self.hop_length = hop_length\n",
|
| 638 |
+
" self.freq_bins = freq_bins\n",
|
| 639 |
+
"\n",
|
| 640 |
+
" def forward(self, x):\n",
|
| 641 |
+
" # x: (B, 3, 32, 32) \u2192 (B, 24, 32, 32) [8 freq channels per color]\n",
|
| 642 |
+
" B, C, H, W = x.shape\n",
|
| 643 |
+
" stft_features = []\n",
|
| 644 |
+
"\n",
|
| 645 |
+
" for c in range(C):\n",
|
| 646 |
+
" channel = x[:, c:c+1, :, :] # (B, 1, 32, 32)\n",
|
| 647 |
+
"\n",
|
| 648 |
+
" # Row-wise STFT\n",
|
| 649 |
+
" rows = channel.view(B * H, W) # (B*32, 32)\n",
|
| 650 |
+
" row_stft = torch.stft(rows, self.n_fft, self.hop_length, return_complex=True,\n",
|
| 651 |
+
" pad_mode='reflect', window=torch.hann_window(self.n_fft, device=x.device))\n",
|
| 652 |
+
" row_mag = torch.abs(row_stft)[:, :self.freq_bins, :] # (B*32, 8, ~6)\n",
|
| 653 |
+
" row_mag = row_mag.mean(dim=2) # (B*32, 8)\n",
|
| 654 |
+
" row_mag = row_mag.view(B, H, self.freq_bins) # (B, 32, 8)\n",
|
| 655 |
+
"\n",
|
| 656 |
+
" # Col-wise STFT\n",
|
| 657 |
+
" cols = channel.view(B * W, H) # (B*32, 32)\n",
|
| 658 |
+
" col_stft = torch.stft(cols, self.n_fft, self.hop_length, return_complex=True,\n",
|
| 659 |
+
" pad_mode='reflect', window=torch.hann_window(self.n_fft, device=x.device))\n",
|
| 660 |
+
" col_mag = torch.abs(col_stft)[:, :self.freq_bins, :] # (B*32, 8, ~6)\n",
|
| 661 |
+
" col_mag = col_mag.mean(dim=2) # (B*32, 8)\n",
|
| 662 |
+
" col_mag = col_mag.view(B, W, self.freq_bins) # (B, 32, 8)\n",
|
| 663 |
+
"\n",
|
| 664 |
+
" # Reshape to image-like (B, 1, spatial_freq_combined)\n",
|
| 665 |
+
" combined = torch.cat([row_mag.view(B, -1), col_mag.view(B, -1)], dim=1) # (B, 512)\n",
|
| 666 |
+
" stft_features.append(combined)\n",
|
| 667 |
+
"\n",
|
| 668 |
+
" return torch.cat(stft_features, dim=1) # (B, 1536)\n",
|
| 669 |
+
"\n",
|
| 670 |
+
"class QRDecompositionModel(nn.Module):\n",
|
| 671 |
+
" '''STFT \u2192 Gram matrix \u2192 QR decomposition \u2192 features.'''\n",
|
| 672 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 673 |
+
" super().__init__()\n",
|
| 674 |
+
" self.stft = STFTFrontend()\n",
|
| 675 |
+
" self.dim_reduce = nn.Linear(1536, 256) # Reduce STFT features\n",
|
| 676 |
+
" self.gram_dim = 256\n",
|
| 677 |
+
"\n",
|
| 678 |
+
" # QR-extracted features\n",
|
| 679 |
+
" self.feat_dim = self.gram_dim + self.gram_dim # Q trace + R diagonal\n",
|
| 680 |
+
" self.embed = nn.Linear(self.feat_dim, embed_dim)\n",
|
| 681 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 682 |
+
"\n",
|
| 683 |
+
" def forward(self, x):\n",
|
| 684 |
+
" # STFT features\n",
|
| 685 |
+
" stft_feat = self.stft(x) # (B, 1536)\n",
|
| 686 |
+
" feat_red = self.dim_reduce(stft_feat) # (B, 256)\n",
|
| 687 |
+
"\n",
|
| 688 |
+
" # Form Gram matrix\n",
|
| 689 |
+
" B = feat_red.shape[0]\n",
|
| 690 |
+
" gram = torch.bmm(feat_red.unsqueeze(2), feat_red.unsqueeze(1)) # (B, 256, 256)\n",
|
| 691 |
+
"\n",
|
| 692 |
+
" # QR decomposition\n",
|
| 693 |
+
" Q, R = torch.linalg.qr(gram)\n",
|
| 694 |
+
" Q_trace = torch.trace(Q.view(-1, self.gram_dim, self.gram_dim).transpose(-1, -2)).view(B)\n",
|
| 695 |
+
" R_diag = torch.diagonal(R, dim1=1, dim2=2) # (B, 256)\n",
|
| 696 |
+
"\n",
|
| 697 |
+
" # Extract features\n",
|
| 698 |
+
" qr_feat = torch.cat([Q_trace.unsqueeze(1), R_diag.mean(dim=1, keepdim=True)], dim=1) # (B, 2)\n",
|
| 699 |
+
"\n",
|
| 700 |
+
" # Combine with reduced features\n",
|
| 701 |
+
" combined = torch.cat([feat_red, qr_feat], dim=1) # (B, 258)\n",
|
| 702 |
+
"\n",
|
| 703 |
+
" # Embed to sphere\n",
|
| 704 |
+
" emb = l2_normalize(self.embed(combined)) # (B, 128)\n",
|
| 705 |
+
"\n",
|
| 706 |
+
" # Classify\n",
|
| 707 |
+
" logits, tri = self.classifier(emb)\n",
|
| 708 |
+
" return logits, emb\n",
|
| 709 |
+
"\n",
|
| 710 |
+
"model = QRDecompositionModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 711 |
+
"print(f\"[5.2a] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 712 |
+
"\n",
|
| 713 |
+
"train_loader, val_loader = get_cifar10_raw_loaders(batch_size=256)\n",
|
| 714 |
+
"\n",
|
| 715 |
+
"result_5_2a = run_experiment(\n",
|
| 716 |
+
" \"5.2a_QR_Decomposition\",\n",
|
| 717 |
+
" model, train_loader, val_loader, device,\n",
|
| 718 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 719 |
+
")\n",
|
| 720 |
+
"scoreboard.add(result_5_2a)\n",
|
| 721 |
+
""
|
| 722 |
+
],
|
| 723 |
+
"execution_count": null,
|
| 724 |
+
"outputs": []
|
| 725 |
+
},
|
| 726 |
+
{
|
| 727 |
+
"cell_type": "markdown",
|
| 728 |
+
"metadata": {},
|
| 729 |
+
"source": [
|
| 730 |
+
"## 5.2b: Schur Decomposition Signatures\n",
|
| 731 |
+
"\n",
|
| 732 |
+
"T diagonal (eigenvalues) + T off-diagonal (coupling) \u2192 features"
|
| 733 |
+
]
|
| 734 |
+
},
|
| 735 |
+
{
|
| 736 |
+
"cell_type": "code",
|
| 737 |
+
"metadata": {},
|
| 738 |
+
"source": [
|
| 739 |
+
"\n",
|
| 740 |
+
"print(\"[5.2b] Schur Decomposition Signatures\")\n",
|
| 741 |
+
"print(\"=\"*70)\n",
|
| 742 |
+
"\n",
|
| 743 |
+
"class SchurDecompositionModel(nn.Module):\n",
|
| 744 |
+
" '''STFT \u2192 Gram matrix \u2192 Schur decomposition.'''\n",
|
| 745 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 746 |
+
" super().__init__()\n",
|
| 747 |
+
" self.stft = STFTFrontend()\n",
|
| 748 |
+
" self.dim_reduce = nn.Linear(1536, 256)\n",
|
| 749 |
+
" self.gram_dim = 256\n",
|
| 750 |
+
"\n",
|
| 751 |
+
" self.feat_dim = self.gram_dim + 10 # Eigenvalue stats + coupling\n",
|
| 752 |
+
" self.embed = nn.Linear(self.feat_dim, embed_dim)\n",
|
| 753 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 754 |
+
"\n",
|
| 755 |
+
" def forward(self, x):\n",
|
| 756 |
+
" stft_feat = self.stft(x)\n",
|
| 757 |
+
" feat_red = self.dim_reduce(stft_feat) # (B, 256)\n",
|
| 758 |
+
"\n",
|
| 759 |
+
" # Gram matrix\n",
|
| 760 |
+
" B = feat_red.shape[0]\n",
|
| 761 |
+
" gram = torch.bmm(feat_red.unsqueeze(2), feat_red.unsqueeze(1)) # (B, 256, 256)\n",
|
| 762 |
+
"\n",
|
| 763 |
+
" # Schur decomposition (similar to eigendecomposition for symmetric)\n",
|
| 764 |
+
" eigenvals, eigenvecs = torch.linalg.eigh(gram)\n",
|
| 765 |
+
"\n",
|
| 766 |
+
" # Extract features from eigenvalues\n",
|
| 767 |
+
" eig_mean = eigenvals.mean(dim=1)\n",
|
| 768 |
+
" eig_std = eigenvals.std(dim=1)\n",
|
| 769 |
+
" eig_max = eigenvals.max(dim=1)[0]\n",
|
| 770 |
+
" eig_min = eigenvals.min(dim=1)[0]\n",
|
| 771 |
+
" eig_log_sum = torch.log(torch.clamp(eigenvals, min=1e-8)).sum(dim=1)\n",
|
| 772 |
+
"\n",
|
| 773 |
+
" # Off-diagonal coupling (correlation of eigenvectors)\n",
|
| 774 |
+
" offdiag = gram - torch.diag_embed(torch.diagonal(gram, dim1=1, dim2=2))\n",
|
| 775 |
+
" coupling = offdiag.abs().sum(dim=(1, 2))\n",
|
| 776 |
+
"\n",
|
| 777 |
+
" # Combine features\n",
|
| 778 |
+
" schur_feat = torch.stack([eig_mean, eig_std, eig_max, eig_min, eig_log_sum, coupling,\n",
|
| 779 |
+
" coupling/self.gram_dim, eigenvals[:, 0], eigenvals[:, -1],\n",
|
| 780 |
+
" eigenvals.sum(dim=1)], dim=1) # (B, 10)\n",
|
| 781 |
+
"\n",
|
| 782 |
+
" combined = torch.cat([feat_red, schur_feat], dim=1)\n",
|
| 783 |
+
"\n",
|
| 784 |
+
" emb = l2_normalize(self.embed(combined))\n",
|
| 785 |
+
" logits, tri = self.classifier(emb)\n",
|
| 786 |
+
" return logits, emb\n",
|
| 787 |
+
"\n",
|
| 788 |
+
"model = SchurDecompositionModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 789 |
+
"print(f\"[5.2b] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 790 |
+
"\n",
|
| 791 |
+
"result_5_2b = run_experiment(\n",
|
| 792 |
+
" \"5.2b_Schur_Decomposition\",\n",
|
| 793 |
+
" model, train_loader, val_loader, device,\n",
|
| 794 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 795 |
+
")\n",
|
| 796 |
+
"scoreboard.add(result_5_2b)\n",
|
| 797 |
+
""
|
| 798 |
+
],
|
| 799 |
+
"execution_count": null,
|
| 800 |
+
"outputs": []
|
| 801 |
+
},
|
| 802 |
+
{
|
| 803 |
+
"cell_type": "markdown",
|
| 804 |
+
"metadata": {},
|
| 805 |
+
"source": [
|
| 806 |
+
"## 5.2c: Eigendecomposition with Signature Comparison\n",
|
| 807 |
+
"\n",
|
| 808 |
+
"Eigenvalues vs SVD singular values vs Cholesky diagonal"
|
| 809 |
+
]
|
| 810 |
+
},
|
| 811 |
+
{
|
| 812 |
+
"cell_type": "code",
|
| 813 |
+
"metadata": {},
|
| 814 |
+
"source": [
|
| 815 |
+
"\n",
|
| 816 |
+
"print(\"[5.2c] Eigendecomposition of Gram & Signature Comparison\")\n",
|
| 817 |
+
"print(\"=\"*70)\n",
|
| 818 |
+
"\n",
|
| 819 |
+
"class EigenSignatureModel(nn.Module):\n",
|
| 820 |
+
" '''Compare eigenvalues (Gram eigh), SVD (Gram svd), Cholesky (L diag).'''\n",
|
| 821 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 822 |
+
" super().__init__()\n",
|
| 823 |
+
" self.stft = STFTFrontend()\n",
|
| 824 |
+
" self.dim_reduce = nn.Linear(1536, 256)\n",
|
| 825 |
+
" self.gram_dim = 256\n",
|
| 826 |
+
"\n",
|
| 827 |
+
" # Three signature types \u00d7 10 stats each\n",
|
| 828 |
+
" self.feat_dim = 256 + 30\n",
|
| 829 |
+
" self.embed = nn.Linear(self.feat_dim, embed_dim)\n",
|
| 830 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 831 |
+
"\n",
|
| 832 |
+
" def forward(self, x):\n",
|
| 833 |
+
" stft_feat = self.stft(x)\n",
|
| 834 |
+
" feat_red = self.dim_reduce(stft_feat)\n",
|
| 835 |
+
"\n",
|
| 836 |
+
" B = feat_red.shape[0]\n",
|
| 837 |
+
" gram = torch.bmm(feat_red.unsqueeze(2), feat_red.unsqueeze(1))\n",
|
| 838 |
+
"\n",
|
| 839 |
+
" # Add small diagonal to ensure PSD\n",
|
| 840 |
+
" gram = gram + torch.eye(self.gram_dim, device=gram.device).unsqueeze(0) * 1e-6\n",
|
| 841 |
+
"\n",
|
| 842 |
+
" # Eigendecomposition\n",
|
| 843 |
+
" eigenvals, _ = torch.linalg.eigh(gram)\n",
|
| 844 |
+
" eigen_mean = eigenvals.mean(dim=1)\n",
|
| 845 |
+
" eigen_std = eigenvals.std(dim=1)\n",
|
| 846 |
+
" eigen_max = eigenvals.max(dim=1)[0]\n",
|
| 847 |
+
" eigen_min = torch.clamp(eigenvals.min(dim=1)[0], min=1e-8)\n",
|
| 848 |
+
" eigen_cond = eigen_max / eigen_min\n",
|
| 849 |
+
" eigen_trace = eigenvals.sum(dim=1)\n",
|
| 850 |
+
" eigen_norm = torch.linalg.norm(eigenvals, dim=1)\n",
|
| 851 |
+
" eigen_entropy = -(eigenvals / (eigenvals.sum(dim=1, keepdim=True) + 1e-8) *\n",
|
| 852 |
+
" torch.log(eigenvals / (eigenvals.sum(dim=1, keepdim=True) + 1e-8) + 1e-8)).sum(dim=1)\n",
|
| 853 |
+
" eigen_rank = (eigenvals > 1e-5).sum(dim=1).float()\n",
|
| 854 |
+
" eigen_sparsity = (eigenvals < 1e-3).sum(dim=1).float() / self.gram_dim\n",
|
| 855 |
+
"\n",
|
| 856 |
+
" # SVD\n",
|
| 857 |
+
" U, S, Vh = torch.svd(gram)\n",
|
| 858 |
+
" svd_mean = S.mean(dim=1)\n",
|
| 859 |
+
" svd_std = S.std(dim=1)\n",
|
| 860 |
+
" svd_ratio = S[:, 0] / (S[:, -1] + 1e-8)\n",
|
| 861 |
+
" svd_sum = S.sum(dim=1)\n",
|
| 862 |
+
" svd_norm = torch.linalg.norm(S, dim=1)\n",
|
| 863 |
+
" svd_rank = (S > 1e-5).sum(dim=1).float()\n",
|
| 864 |
+
" svd_cumsum = (S.cumsum(dim=1) / S.sum(dim=1, keepdim=True))[:, -1]\n",
|
| 865 |
+
"\n",
|
| 866 |
+
" # Cholesky\n",
|
| 867 |
+
" try:\n",
|
| 868 |
+
" L = torch.linalg.cholesky(gram)\n",
|
| 869 |
+
" chol_diag = torch.diagonal(L, dim1=1, dim2=2)\n",
|
| 870 |
+
" chol_mean = chol_diag.mean(dim=1)\n",
|
| 871 |
+
" chol_std = chol_diag.std(dim=1)\n",
|
| 872 |
+
" chol_sum = chol_diag.sum(dim=1)\n",
|
| 873 |
+
" chol_norm = torch.linalg.norm(chol_diag, dim=1)\n",
|
| 874 |
+
" chol_prod = torch.prod(chol_diag, dim=1)\n",
|
| 875 |
+
" chol_trace = L.diagonal(dim1=1, dim2=2).sum(dim=1)\n",
|
| 876 |
+
" except:\n",
|
| 877 |
+
" # Fallback if Cholesky fails\n",
|
| 878 |
+
" chol_mean = torch.zeros(B, device=gram.device)\n",
|
| 879 |
+
" chol_std = torch.zeros(B, device=gram.device)\n",
|
| 880 |
+
" chol_sum = torch.zeros(B, device=gram.device)\n",
|
| 881 |
+
" chol_norm = torch.zeros(B, device=gram.device)\n",
|
| 882 |
+
" chol_prod = torch.ones(B, device=gram.device)\n",
|
| 883 |
+
" chol_trace = torch.zeros(B, device=gram.device)\n",
|
| 884 |
+
"\n",
|
| 885 |
+
" # Combine all signatures\n",
|
| 886 |
+
" eigen_sig = torch.stack([eigen_mean, eigen_std, eigen_max, eigen_min, eigen_cond,\n",
|
| 887 |
+
" eigen_trace, eigen_norm, eigen_entropy, eigen_rank, eigen_sparsity], dim=1)\n",
|
| 888 |
+
" svd_sig = torch.stack([svd_mean, svd_std, svd_ratio, svd_sum, svd_norm,\n",
|
| 889 |
+
" svd_rank, svd_cumsum,\n",
|
| 890 |
+
" S[:, 0], S[:, 1] if S.shape[1] > 1 else S[:, 0],\n",
|
| 891 |
+
" (S[:, :5].sum(dim=1) / S.sum(dim=1))], dim=1)\n",
|
| 892 |
+
" chol_sig = torch.stack([chol_mean, chol_std, chol_sum, chol_norm, chol_prod,\n",
|
| 893 |
+
" chol_trace, chol_prod.log(),\n",
|
| 894 |
+
" chol_diag.max(dim=1)[0], chol_diag.min(dim=1)[0],\n",
|
| 895 |
+
" chol_diag.max(dim=1)[0] / (chol_diag.min(dim=1)[0] + 1e-8)], dim=1)\n",
|
| 896 |
+
"\n",
|
| 897 |
+
" sig_combined = torch.cat([eigen_sig, svd_sig, chol_sig], dim=1) # (B, 30)\n",
|
| 898 |
+
" combined = torch.cat([feat_red, sig_combined], dim=1)\n",
|
| 899 |
+
"\n",
|
| 900 |
+
" emb = l2_normalize(self.embed(combined))\n",
|
| 901 |
+
" logits, tri = self.classifier(emb)\n",
|
| 902 |
+
" return logits, emb\n",
|
| 903 |
+
"\n",
|
| 904 |
+
"model = EigenSignatureModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 905 |
+
"print(f\"[5.2c] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 906 |
+
"\n",
|
| 907 |
+
"result_5_2c = run_experiment(\n",
|
| 908 |
+
" \"5.2c_Eigen_SVD_Cholesky\",\n",
|
| 909 |
+
" model, train_loader, val_loader, device,\n",
|
| 910 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 911 |
+
")\n",
|
| 912 |
+
"scoreboard.add(result_5_2c)\n",
|
| 913 |
+
""
|
| 914 |
+
],
|
| 915 |
+
"execution_count": null,
|
| 916 |
+
"outputs": []
|
| 917 |
+
},
|
| 918 |
+
{
|
| 919 |
+
"cell_type": "markdown",
|
| 920 |
+
"metadata": {},
|
| 921 |
+
"source": [
|
| 922 |
+
"## 5.2d: NMF of Magnitude Spectrum\n",
|
| 923 |
+
"\n",
|
| 924 |
+
"Parts-based decomposition using sklearn NMF"
|
| 925 |
+
]
|
| 926 |
+
},
|
| 927 |
+
{
|
| 928 |
+
"cell_type": "code",
|
| 929 |
+
"metadata": {},
|
| 930 |
+
"source": [
|
| 931 |
+
"\n",
|
| 932 |
+
"from sklearn.decomposition import NMF\n",
|
| 933 |
+
"print(\"[5.2d] NMF of Magnitude Spectrum\")\n",
|
| 934 |
+
"print(\"=\"*70)\n",
|
| 935 |
+
"\n",
|
| 936 |
+
"class NMFDecompositionModel(nn.Module):\n",
|
| 937 |
+
" '''STFT magnitude \u2192 NMF parts-based decomposition.'''\n",
|
| 938 |
+
" def __init__(self, embed_dim=128, num_classes=10, n_components=16):\n",
|
| 939 |
+
" super().__init__()\n",
|
| 940 |
+
" self.stft = STFTFrontend()\n",
|
| 941 |
+
" self.n_components = n_components\n",
|
| 942 |
+
" self.nmf_model = NMF(n_components=n_components, init='random', random_state=42, max_iter=200)\n",
|
| 943 |
+
"\n",
|
| 944 |
+
" self.feat_dim = n_components + 10 # NMF coefficients + stats\n",
|
| 945 |
+
" self.embed = nn.Linear(self.feat_dim, embed_dim)\n",
|
| 946 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 947 |
+
"\n",
|
| 948 |
+
" def forward(self, x):\n",
|
| 949 |
+
" stft_feat = self.stft(x) # (B, 1536)\n",
|
| 950 |
+
"\n",
|
| 951 |
+
" # NMF on magnitude (treat as 1536-dim features)\n",
|
| 952 |
+
" stft_np = stft_feat.detach().cpu().numpy()\n",
|
| 953 |
+
" stft_np = np.abs(stft_np) # Ensure non-negative\n",
|
| 954 |
+
"\n",
|
| 955 |
+
" nmf_coeff = torch.zeros(x.shape[0], self.n_components, device=x.device)\n",
|
| 956 |
+
" try:\n",
|
| 957 |
+
" for b in range(x.shape[0]):\n",
|
| 958 |
+
" coeff = self.nmf_model.fit_transform(stft_np[b:b+1])\n",
|
| 959 |
+
" nmf_coeff[b] = torch.from_numpy(coeff[0]).float().to(x.device)\n",
|
| 960 |
+
" except:\n",
|
| 961 |
+
" # If NMF fails, use zeros\n",
|
| 962 |
+
" pass\n",
|
| 963 |
+
"\n",
|
| 964 |
+
" # Extract statistics\n",
|
| 965 |
+
" nmf_mean = nmf_coeff.mean(dim=1, keepdim=True)\n",
|
| 966 |
+
" nmf_std = nmf_coeff.std(dim=1, keepdim=True)\n",
|
| 967 |
+
" nmf_max = nmf_coeff.max(dim=1)[0].unsqueeze(1)\n",
|
| 968 |
+
" nmf_norm = torch.linalg.norm(nmf_coeff, dim=1, keepdim=True)\n",
|
| 969 |
+
" sparsity = (nmf_coeff < 0.01).sum(dim=1, keepdim=True).float() / self.n_components\n",
|
| 970 |
+
" energy = (nmf_coeff ** 2).sum(dim=1, keepdim=True)\n",
|
| 971 |
+
" entropy = -(nmf_coeff / (nmf_coeff.sum(dim=1, keepdim=True) + 1e-8) *\n",
|
| 972 |
+
" torch.log(nmf_coeff / (nmf_coeff.sum(dim=1, keepdim=True) + 1e-8) + 1e-8)).sum(dim=1, keepdim=True)\n",
|
| 973 |
+
" active = (nmf_coeff > 1e-3).sum(dim=1, keepdim=True).float()\n",
|
| 974 |
+
"\n",
|
| 975 |
+
" stats = torch.cat([nmf_mean, nmf_std, nmf_max, nmf_norm, sparsity, energy, entropy, active], dim=1) # (B, 8)\n",
|
| 976 |
+
" combined = torch.cat([nmf_coeff, stats], dim=1) # (B, 24)\n",
|
| 977 |
+
"\n",
|
| 978 |
+
" emb = l2_normalize(self.embed(combined))\n",
|
| 979 |
+
" logits, tri = self.classifier(emb)\n",
|
| 980 |
+
" return logits, emb\n",
|
| 981 |
+
"\n",
|
| 982 |
+
"model = NMFDecompositionModel(embed_dim=128, num_classes=10, n_components=16).to(device)\n",
|
| 983 |
+
"print(f\"[5.2d] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 984 |
+
"\n",
|
| 985 |
+
"result_5_2d = run_experiment(\n",
|
| 986 |
+
" \"5.2d_NMF_Decomposition\",\n",
|
| 987 |
+
" model, train_loader, val_loader, device,\n",
|
| 988 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 989 |
+
")\n",
|
| 990 |
+
"scoreboard.add(result_5_2d)\n",
|
| 991 |
+
""
|
| 992 |
+
],
|
| 993 |
+
"execution_count": null,
|
| 994 |
+
"outputs": []
|
| 995 |
+
},
|
| 996 |
+
{
|
| 997 |
+
"cell_type": "markdown",
|
| 998 |
+
"metadata": {},
|
| 999 |
+
"source": [
|
| 1000 |
+
"## 5.2e: Tucker Tensor Decomposition\n",
|
| 1001 |
+
"\n",
|
| 1002 |
+
"3D structure (H, W, freq_bins) per color channel, core tensor encodes interactions"
|
| 1003 |
+
]
|
| 1004 |
+
},
|
| 1005 |
+
{
|
| 1006 |
+
"cell_type": "code",
|
| 1007 |
+
"metadata": {},
|
| 1008 |
+
"source": [
|
| 1009 |
+
"\n",
|
| 1010 |
+
"print(\"[5.2e] Tucker Tensor Decomposition\")\n",
|
| 1011 |
+
"print(\"=\"*70)\n",
|
| 1012 |
+
"\n",
|
| 1013 |
+
"class TuckerDecompositionModel(nn.Module):\n",
|
| 1014 |
+
" '''Reshape STFT features to 3D tensor, apply Tucker decomposition.'''\n",
|
| 1015 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 1016 |
+
" super().__init__()\n",
|
| 1017 |
+
" # Manually implement Tucker-like feature extraction (full Tucker is complex)\n",
|
| 1018 |
+
" self.stft = STFTFrontend()\n",
|
| 1019 |
+
" self.dim_reduce = nn.Linear(1536, 256)\n",
|
| 1020 |
+
"\n",
|
| 1021 |
+
" self.feat_dim = 256 + 20 # Reduced features + Tucker stats\n",
|
| 1022 |
+
" self.embed = nn.Linear(self.feat_dim, embed_dim)\n",
|
| 1023 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 1024 |
+
"\n",
|
| 1025 |
+
" def forward(self, x):\n",
|
| 1026 |
+
" stft_feat = self.stft(x) # (B, 1536)\n",
|
| 1027 |
+
" feat_red = self.dim_reduce(stft_feat) # (B, 256)\n",
|
| 1028 |
+
"\n",
|
| 1029 |
+
" B = feat_red.shape[0]\n",
|
| 1030 |
+
"\n",
|
| 1031 |
+
" # Reshape to 3D: (B, 8, 32, 32) approximation\n",
|
| 1032 |
+
" tensor_3d = feat_red.view(B, 8, 32) # Approximate 3D\n",
|
| 1033 |
+
"\n",
|
| 1034 |
+
" # Tucker-like: compute mode-wise statistics\n",
|
| 1035 |
+
" mode0_unfold = tensor_3d.view(B, 8, -1) # (B, 8, 32)\n",
|
| 1036 |
+
" mode1_unfold = tensor_3d.transpose(1, 2) # (B, 32, 8)\n",
|
| 1037 |
+
"\n",
|
| 1038 |
+
" # Covariances along each mode\n",
|
| 1039 |
+
" cov0 = torch.bmm(mode0_unfold, mode0_unfold.transpose(1, 2)) # (B, 8, 8)\n",
|
| 1040 |
+
" cov1 = torch.bmm(mode1_unfold, mode1_unfold.transpose(1, 2)) # (B, 32, 32)\n",
|
| 1041 |
+
"\n",
|
| 1042 |
+
" # Extract eigenvalues from mode covariances\n",
|
| 1043 |
+
" eig0, _ = torch.linalg.eigh(cov0) # (B, 8)\n",
|
| 1044 |
+
" eig1, _ = torch.linalg.eigh(cov1) # (B, 32)\n",
|
| 1045 |
+
"\n",
|
| 1046 |
+
" # Core tensor stats\n",
|
| 1047 |
+
" core_mean = eig0.mean(dim=1)\n",
|
| 1048 |
+
" core_std = eig0.std(dim=1)\n",
|
| 1049 |
+
" core_max = eig0.max(dim=1)[0]\n",
|
| 1050 |
+
" mode_mean = eig1.mean(dim=1)\n",
|
| 1051 |
+
" mode_std = eig1.std(dim=1)\n",
|
| 1052 |
+
" mode_trace = eig1.sum(dim=1)\n",
|
| 1053 |
+
"\n",
|
| 1054 |
+
" # Mode interaction (trace of Gram across modes)\n",
|
| 1055 |
+
" interaction = (eig0 * eig0.mean(dim=1, keepdim=True)).sum(dim=1)\n",
|
| 1056 |
+
" contraction = torch.einsum('bi,bi->b', eig0.view(B, -1), eig1[:, :8])\n",
|
| 1057 |
+
"\n",
|
| 1058 |
+
" # Coupled features\n",
|
| 1059 |
+
" coupling1 = (eig0[:, 0] + eig0[:, -1]) / 2\n",
|
| 1060 |
+
" coupling2 = (eig1[:, 0] + eig1[:, -1]) / 2\n",
|
| 1061 |
+
" coupling3 = eig0.max(dim=1)[0] / (eig0.min(dim=1)[0] + 1e-8)\n",
|
| 1062 |
+
" coupling4 = eig1.max(dim=1)[0] / (eig1.min(dim=1)[0] + 1e-8)\n",
|
| 1063 |
+
"\n",
|
| 1064 |
+
" tucker_stats = torch.stack([core_mean, core_std, core_max, mode_mean, mode_std,\n",
|
| 1065 |
+
" mode_trace, interaction, contraction,\n",
|
| 1066 |
+
" coupling1, coupling2, coupling3, coupling4,\n",
|
| 1067 |
+
" eig0.sum(dim=1), eig1.sum(dim=1),\n",
|
| 1068 |
+
" torch.linalg.norm(eig0, dim=1),\n",
|
| 1069 |
+
" torch.linalg.norm(eig1, dim=1),\n",
|
| 1070 |
+
" (eig0 > 1e-5).sum(dim=1).float(),\n",
|
| 1071 |
+
" (eig1 > 1e-5).sum(dim=1).float(),\n",
|
| 1072 |
+
" eig0.max(dim=1)[0] - eig0.min(dim=1)[0],\n",
|
| 1073 |
+
" eig1.max(dim=1)[0] - eig1.min(dim=1)[0]], dim=1) # (B, 20)\n",
|
| 1074 |
+
"\n",
|
| 1075 |
+
" combined = torch.cat([feat_red, tucker_stats], dim=1)\n",
|
| 1076 |
+
"\n",
|
| 1077 |
+
" emb = l2_normalize(self.embed(combined))\n",
|
| 1078 |
+
" logits, tri = self.classifier(emb)\n",
|
| 1079 |
+
" return logits, emb\n",
|
| 1080 |
+
"\n",
|
| 1081 |
+
"model = TuckerDecompositionModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 1082 |
+
"print(f\"[5.2e] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 1083 |
+
"\n",
|
| 1084 |
+
"result_5_2e = run_experiment(\n",
|
| 1085 |
+
" \"5.2e_Tucker_Decomposition\",\n",
|
| 1086 |
+
" model, train_loader, val_loader, device,\n",
|
| 1087 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 1088 |
+
")\n",
|
| 1089 |
+
"scoreboard.add(result_5_2e)\n",
|
| 1090 |
+
""
|
| 1091 |
+
],
|
| 1092 |
+
"execution_count": null,
|
| 1093 |
+
"outputs": []
|
| 1094 |
+
},
|
| 1095 |
+
{
|
| 1096 |
+
"cell_type": "markdown",
|
| 1097 |
+
"metadata": {},
|
| 1098 |
+
"source": [
|
| 1099 |
+
"## Final Scoreboard"
|
| 1100 |
+
]
|
| 1101 |
+
},
|
| 1102 |
+
{
|
| 1103 |
+
"cell_type": "code",
|
| 1104 |
+
"metadata": {},
|
| 1105 |
+
"source": [
|
| 1106 |
+
"\n",
|
| 1107 |
+
"print(\"\\n\" + \"=\"*80)\n",
|
| 1108 |
+
"print(\"EXPERIMENT 5 COMPLETE: Matrix Decomposition Signatures\")\n",
|
| 1109 |
+
"print(\"=\"*80)\n",
|
| 1110 |
+
"scoreboard._print_table()\n",
|
| 1111 |
+
"scoreboard.save(\"scoreboard_exp5.json\")\n",
|
| 1112 |
+
""
|
| 1113 |
+
],
|
| 1114 |
+
"execution_count": null,
|
| 1115 |
+
"outputs": []
|
| 1116 |
+
}
|
| 1117 |
+
]
|
| 1118 |
+
}
|
spectral/experiment_6_losses_and_anchors.ipynb
ADDED
|
@@ -0,0 +1,1364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 5,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"kernelspec": {
|
| 6 |
+
"display_name": "Python 3",
|
| 7 |
+
"language": "python",
|
| 8 |
+
"name": "python3"
|
| 9 |
+
},
|
| 10 |
+
"language_info": {
|
| 11 |
+
"name": "python",
|
| 12 |
+
"version": "3.10.0"
|
| 13 |
+
},
|
| 14 |
+
"accelerator": "GPU",
|
| 15 |
+
"colab": {
|
| 16 |
+
"provenance": [],
|
| 17 |
+
"gpuType": "T4"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"cells": [
|
| 21 |
+
{
|
| 22 |
+
"cell_type": "markdown",
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"source": [
|
| 25 |
+
"# Experiment 6: Information-Theoretic Losses & Anchor Management\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"Advanced loss functions and anchor manipulation strategies."
|
| 28 |
+
]
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"cell_type": "code",
|
| 32 |
+
"metadata": {},
|
| 33 |
+
"source": [
|
| 34 |
+
"\n",
|
| 35 |
+
"!pip install -q kymatio scipy tensorboard scikit-learn matplotlib numpy torch torchvision torchaudio scipy\n",
|
| 36 |
+
""
|
| 37 |
+
],
|
| 38 |
+
"execution_count": null,
|
| 39 |
+
"outputs": []
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"cell_type": "markdown",
|
| 43 |
+
"metadata": {},
|
| 44 |
+
"source": [
|
| 45 |
+
"## Shared Utilities"
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"cell_type": "code",
|
| 50 |
+
"metadata": {},
|
| 51 |
+
"source": [
|
| 52 |
+
"\n",
|
| 53 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 54 |
+
"# SHARED UTILITIES \u2014 GeoLIP Hypersphere Experiments\n",
|
| 55 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 56 |
+
"import os, sys, time, math, json, datetime, warnings\n",
|
| 57 |
+
"import numpy as np\n",
|
| 58 |
+
"import torch\n",
|
| 59 |
+
"import torch.nn as nn\n",
|
| 60 |
+
"import torch.nn.functional as F\n",
|
| 61 |
+
"from torch.utils.data import DataLoader\n",
|
| 62 |
+
"from torch.utils.tensorboard import SummaryWriter\n",
|
| 63 |
+
"import torchvision\n",
|
| 64 |
+
"import torchvision.transforms as T\n",
|
| 65 |
+
"import matplotlib\n",
|
| 66 |
+
"matplotlib.use(\"Agg\")\n",
|
| 67 |
+
"import matplotlib.pyplot as plt\n",
|
| 68 |
+
"from collections import defaultdict\n",
|
| 69 |
+
"\n",
|
| 70 |
+
"warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
|
| 71 |
+
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 72 |
+
"print(f\"[DEVICE] {device}\" + (f\" \u2014 {torch.cuda.get_device_name()}\" if device.type == \"cuda\" else \"\"))\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"# \u2500\u2500 CIFAR-10 Loaders \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 75 |
+
"CIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\n",
|
| 76 |
+
"CIFAR10_STD = (0.2470, 0.2435, 0.2616)\n",
|
| 77 |
+
"CIFAR10_CLASSES = (\"plane\",\"car\",\"bird\",\"cat\",\"deer\",\"dog\",\"frog\",\"horse\",\"ship\",\"truck\")\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"def get_cifar10_loaders(batch_size=256, num_workers=2, augment=True):\n",
|
| 80 |
+
" \"\"\"Return train_loader, val_loader for CIFAR-10.\"\"\"\n",
|
| 81 |
+
" tf_train = T.Compose([\n",
|
| 82 |
+
" T.RandomCrop(32, padding=4),\n",
|
| 83 |
+
" T.RandomHorizontalFlip(),\n",
|
| 84 |
+
" T.ToTensor(),\n",
|
| 85 |
+
" T.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n",
|
| 86 |
+
" ]) if augment else T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 87 |
+
" tf_val = T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 88 |
+
"\n",
|
| 89 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf_train)\n",
|
| 90 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf_val)\n",
|
| 91 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 92 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 93 |
+
" print(f\"[DATA] CIFAR-10: {len(train_ds)} train, {len(val_ds)} val, batch_size={batch_size}\")\n",
|
| 94 |
+
" return train_loader, val_loader\n",
|
| 95 |
+
"\n",
|
| 96 |
+
"def get_cifar10_raw_loaders(batch_size=256, num_workers=2):\n",
|
| 97 |
+
" \"\"\"Return loaders with only ToTensor (no normalization) for custom front-ends.\"\"\"\n",
|
| 98 |
+
" tf = T.Compose([T.ToTensor()])\n",
|
| 99 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf)\n",
|
| 100 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf)\n",
|
| 101 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 102 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 103 |
+
" print(f\"[DATA] CIFAR-10 (raw): {len(train_ds)} train, {len(val_ds)} val\")\n",
|
| 104 |
+
" return train_loader, val_loader\n",
|
| 105 |
+
"\n",
|
| 106 |
+
"# \u2500\u2500 L2 Normalization to Hypersphere \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 107 |
+
"def l2_normalize(x, dim=-1, eps=1e-8):\n",
|
| 108 |
+
" \"\"\"Project to unit hypersphere S^{d-1}.\"\"\"\n",
|
| 109 |
+
" return F.normalize(x, p=2, dim=dim, eps=eps)\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"# \u2500\u2500 Constellation Module \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 112 |
+
"class Constellation(nn.Module):\n",
|
| 113 |
+
" \"\"\"\n",
|
| 114 |
+
" K anchors on S^{d-1}. Triangulation: compute distance from input to each anchor.\n",
|
| 115 |
+
" Output: K-dim vector of (1 - cos_sim) distances = triangulation profile.\n",
|
| 116 |
+
" \"\"\"\n",
|
| 117 |
+
" def __init__(self, embed_dim, num_anchors=64, init=\"uniform\"):\n",
|
| 118 |
+
" super().__init__()\n",
|
| 119 |
+
" self.embed_dim = embed_dim\n",
|
| 120 |
+
" self.num_anchors = num_anchors\n",
|
| 121 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, init)\n",
|
| 122 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False) # anchors are NOT learned\n",
|
| 123 |
+
"\n",
|
| 124 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 125 |
+
" if method == \"uniform\":\n",
|
| 126 |
+
" # QR-based uniform hypersphere initialization\n",
|
| 127 |
+
" A = torch.randn(K, d)\n",
|
| 128 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 129 |
+
" anchors = Q.T[:K]\n",
|
| 130 |
+
" # If K > d, fill remaining with random unit vectors\n",
|
| 131 |
+
" if K > d:\n",
|
| 132 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 133 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 134 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 135 |
+
" elif method == \"random\":\n",
|
| 136 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 137 |
+
" else:\n",
|
| 138 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 139 |
+
"\n",
|
| 140 |
+
" def forward(self, x):\n",
|
| 141 |
+
" \"\"\"x: (B, d) on S^{d-1} \u2192 (B, K) triangulation distances.\"\"\"\n",
|
| 142 |
+
" x_norm = l2_normalize(x)\n",
|
| 143 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T) # (B, K)\n",
|
| 144 |
+
" distances = 1.0 - cos_sim # in [0, 2]\n",
|
| 145 |
+
" return distances\n",
|
| 146 |
+
"\n",
|
| 147 |
+
" def push_anchors(self, embeddings, labels, num_classes=10, momentum=0.1):\n",
|
| 148 |
+
" \"\"\"Push anchors toward class centroids (call periodically).\"\"\"\n",
|
| 149 |
+
" with torch.no_grad():\n",
|
| 150 |
+
" for c in range(num_classes):\n",
|
| 151 |
+
" mask = (labels == c)\n",
|
| 152 |
+
" if mask.sum() < 2:\n",
|
| 153 |
+
" continue\n",
|
| 154 |
+
" centroid = l2_normalize(embeddings[mask].mean(dim=0, keepdim=True))\n",
|
| 155 |
+
" # Assign anchors to this class based on nearest centroid\n",
|
| 156 |
+
" start = c * (self.num_anchors // num_classes)\n",
|
| 157 |
+
" end = start + (self.num_anchors // num_classes)\n",
|
| 158 |
+
" self.anchors.data[start:end] = l2_normalize(\n",
|
| 159 |
+
" (1 - momentum) * self.anchors.data[start:end] + momentum * centroid\n",
|
| 160 |
+
" )\n",
|
| 161 |
+
"\n",
|
| 162 |
+
" def anchor_activation(self, x):\n",
|
| 163 |
+
" \"\"\"Return fraction of anchors that are 'active' (nearest to at least one sample).\"\"\"\n",
|
| 164 |
+
" x_norm = l2_normalize(x)\n",
|
| 165 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 166 |
+
" nearest = cos_sim.argmax(dim=1)\n",
|
| 167 |
+
" active = len(nearest.unique())\n",
|
| 168 |
+
" return active, self.num_anchors\n",
|
| 169 |
+
"\n",
|
| 170 |
+
"# \u2500\u2500 Patchwork Block \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 171 |
+
"class SquaredReLU(nn.Module):\n",
|
| 172 |
+
" def forward(self, x):\n",
|
| 173 |
+
" return F.relu(x) ** 2\n",
|
| 174 |
+
"\n",
|
| 175 |
+
"class PatchworkBlock(nn.Module):\n",
|
| 176 |
+
" \"\"\"SquaredReLU + LayerNorm block for post-constellation processing.\"\"\"\n",
|
| 177 |
+
" def __init__(self, dim):\n",
|
| 178 |
+
" super().__init__()\n",
|
| 179 |
+
" self.net = nn.Sequential(\n",
|
| 180 |
+
" nn.Linear(dim, dim),\n",
|
| 181 |
+
" SquaredReLU(),\n",
|
| 182 |
+
" nn.LayerNorm(dim),\n",
|
| 183 |
+
" nn.Linear(dim, dim),\n",
|
| 184 |
+
" SquaredReLU(),\n",
|
| 185 |
+
" nn.LayerNorm(dim),\n",
|
| 186 |
+
" )\n",
|
| 187 |
+
" def forward(self, x):\n",
|
| 188 |
+
" return x + self.net(x) # residual\n",
|
| 189 |
+
"\n",
|
| 190 |
+
"class Patchwork(nn.Module):\n",
|
| 191 |
+
" \"\"\"Stack of PatchworkBlocks.\"\"\"\n",
|
| 192 |
+
" def __init__(self, dim, num_blocks=2):\n",
|
| 193 |
+
" super().__init__()\n",
|
| 194 |
+
" self.blocks = nn.Sequential(*[PatchworkBlock(dim) for _ in range(num_blocks)])\n",
|
| 195 |
+
" def forward(self, x):\n",
|
| 196 |
+
" return self.blocks(x)\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"# \u2500\u2500 Classifier Head \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 199 |
+
"class ConstellationClassifier(nn.Module):\n",
|
| 200 |
+
" \"\"\"Full pipeline: encoder output \u2192 constellation \u2192 patchwork \u2192 classify.\"\"\"\n",
|
| 201 |
+
" def __init__(self, embed_dim, num_anchors=64, num_classes=10, patchwork_blocks=2):\n",
|
| 202 |
+
" super().__init__()\n",
|
| 203 |
+
" self.constellation = Constellation(embed_dim, num_anchors)\n",
|
| 204 |
+
" self.patchwork = Patchwork(num_anchors, patchwork_blocks)\n",
|
| 205 |
+
" self.head = nn.Linear(num_anchors, num_classes)\n",
|
| 206 |
+
"\n",
|
| 207 |
+
" def forward(self, embeddings):\n",
|
| 208 |
+
" tri = self.constellation(embeddings) # (B, K)\n",
|
| 209 |
+
" pw = self.patchwork(tri) # (B, K)\n",
|
| 210 |
+
" logits = self.head(pw) # (B, 10)\n",
|
| 211 |
+
" return logits, tri\n",
|
| 212 |
+
"\n",
|
| 213 |
+
" def count_params(self):\n",
|
| 214 |
+
" return sum(p.numel() for p in self.parameters() if p.requires_grad)\n",
|
| 215 |
+
"\n",
|
| 216 |
+
"# \u2500\u2500 Losses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 217 |
+
"class CayleyMengerVolumeLoss(nn.Module):\n",
|
| 218 |
+
" \"\"\"CV loss: encourage constellation volume in 0.18-0.25 band.\"\"\"\n",
|
| 219 |
+
" def __init__(self, target_low=0.18, target_high=0.25):\n",
|
| 220 |
+
" super().__init__()\n",
|
| 221 |
+
" self.target_low = target_low\n",
|
| 222 |
+
" self.target_high = target_high\n",
|
| 223 |
+
"\n",
|
| 224 |
+
" def forward(self, tri):\n",
|
| 225 |
+
" \"\"\"tri: (B, K) triangulation distances.\"\"\"\n",
|
| 226 |
+
" # Approximate volume via determinant of distance submatrix\n",
|
| 227 |
+
" B, K = tri.shape\n",
|
| 228 |
+
" # Use std of triangulation as proxy for volume spread\n",
|
| 229 |
+
" vol = tri.std(dim=1).mean()\n",
|
| 230 |
+
" # Penalty if outside target band\n",
|
| 231 |
+
" below = F.relu(self.target_low - vol)\n",
|
| 232 |
+
" above = F.relu(vol - self.target_high)\n",
|
| 233 |
+
" return below + above, vol.item()\n",
|
| 234 |
+
"\n",
|
| 235 |
+
"class InfoNCELoss(nn.Module):\n",
|
| 236 |
+
" \"\"\"Self-supervised contrastive loss on augmented views.\"\"\"\n",
|
| 237 |
+
" def __init__(self, temperature=0.07):\n",
|
| 238 |
+
" super().__init__()\n",
|
| 239 |
+
" self.temperature = temperature\n",
|
| 240 |
+
"\n",
|
| 241 |
+
" def forward(self, z1, z2):\n",
|
| 242 |
+
" \"\"\"z1, z2: (B, d) L2-normalized embeddings of two augmented views.\"\"\"\n",
|
| 243 |
+
" z1 = l2_normalize(z1)\n",
|
| 244 |
+
" z2 = l2_normalize(z2)\n",
|
| 245 |
+
" B = z1.shape[0]\n",
|
| 246 |
+
" z = torch.cat([z1, z2], dim=0) # (2B, d)\n",
|
| 247 |
+
" sim = torch.mm(z, z.T) / self.temperature # (2B, 2B)\n",
|
| 248 |
+
" # Mask out self-similarity\n",
|
| 249 |
+
" mask = ~torch.eye(2*B, dtype=bool, device=z.device)\n",
|
| 250 |
+
" sim = sim.masked_fill(~mask, -1e9)\n",
|
| 251 |
+
" # Positive pairs: (i, i+B) and (i+B, i)\n",
|
| 252 |
+
" labels = torch.cat([torch.arange(B, 2*B), torch.arange(B)]).to(z.device)\n",
|
| 253 |
+
" loss = F.cross_entropy(sim, labels)\n",
|
| 254 |
+
" return loss\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"class ClassConditionalInfoNCE(nn.Module):\n",
|
| 257 |
+
" \"\"\"Class-conditional contrastive: positive = same class, not same image.\"\"\"\n",
|
| 258 |
+
" def __init__(self, temperature=0.07):\n",
|
| 259 |
+
" super().__init__()\n",
|
| 260 |
+
" self.temperature = temperature\n",
|
| 261 |
+
"\n",
|
| 262 |
+
" def forward(self, z, labels):\n",
|
| 263 |
+
" z = l2_normalize(z)\n",
|
| 264 |
+
" B = z.shape[0]\n",
|
| 265 |
+
" sim = torch.mm(z, z.T) / self.temperature\n",
|
| 266 |
+
" # Mask: positive if same class, negative otherwise\n",
|
| 267 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 268 |
+
" label_mask.fill_diagonal_(0) # exclude self\n",
|
| 269 |
+
" # For each sample, compute log-softmax over all non-self, weighted by positives\n",
|
| 270 |
+
" logits = sim - torch.diag(torch.full((B,), 1e9, device=z.device))\n",
|
| 271 |
+
" pos_mask = label_mask\n",
|
| 272 |
+
" neg_mask = 1 - label_mask\n",
|
| 273 |
+
" neg_mask.fill_diagonal_(0)\n",
|
| 274 |
+
" # InfoNCE: -log(sum_pos exp(sim) / sum_all exp(sim))\n",
|
| 275 |
+
" exp_sim = torch.exp(logits)\n",
|
| 276 |
+
" pos_sum = (exp_sim * pos_mask).sum(dim=1)\n",
|
| 277 |
+
" all_sum = (exp_sim * (pos_mask + neg_mask)).sum(dim=1)\n",
|
| 278 |
+
" loss = -torch.log(pos_sum / (all_sum + 1e-8) + 1e-8).mean()\n",
|
| 279 |
+
" return loss\n",
|
| 280 |
+
"\n",
|
| 281 |
+
"# \u2500\u2500 TensorBoard Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 282 |
+
"def make_writer(experiment_id, base_dir=\"runs\"):\n",
|
| 283 |
+
" \"\"\"Create a TensorBoard SummaryWriter with timestamped run directory.\"\"\"\n",
|
| 284 |
+
" timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
|
| 285 |
+
" log_dir = os.path.join(base_dir, f\"{experiment_id}_{timestamp}\")\n",
|
| 286 |
+
" writer = SummaryWriter(log_dir=log_dir)\n",
|
| 287 |
+
" print(f\"[TB] Logging to {log_dir}\")\n",
|
| 288 |
+
" return writer\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"def log_epoch_metrics(writer, epoch, metrics_dict, prefix=\"\"):\n",
|
| 291 |
+
" \"\"\"Log a dict of metrics to TensorBoard.\"\"\"\n",
|
| 292 |
+
" for key, val in metrics_dict.items():\n",
|
| 293 |
+
" tag = f\"{prefix}/{key}\" if prefix else key\n",
|
| 294 |
+
" writer.add_scalar(tag, val, epoch)\n",
|
| 295 |
+
"\n",
|
| 296 |
+
"def log_embedding_viz(writer, embeddings, labels, epoch, tag=\"embeddings\"):\n",
|
| 297 |
+
" \"\"\"Log embeddings for TensorBoard projector.\"\"\"\n",
|
| 298 |
+
" meta = [CIFAR10_CLASSES[l] for l in labels]\n",
|
| 299 |
+
" writer.add_embedding(embeddings, metadata=meta, global_step=epoch, tag=tag)\n",
|
| 300 |
+
"\n",
|
| 301 |
+
"def log_anchor_histogram(writer, constellation, epoch):\n",
|
| 302 |
+
" \"\"\"Log anchor norm distribution.\"\"\"\n",
|
| 303 |
+
" norms = constellation.anchors.data.norm(dim=1)\n",
|
| 304 |
+
" writer.add_histogram(\"anchors/norms\", norms, epoch)\n",
|
| 305 |
+
" cos_sim_matrix = torch.mm(constellation.anchors.data, constellation.anchors.data.T)\n",
|
| 306 |
+
" off_diag = cos_sim_matrix[~torch.eye(cos_sim_matrix.shape[0], dtype=bool)].flatten()\n",
|
| 307 |
+
" writer.add_histogram(\"anchors/inter_cosine_sim\", off_diag, epoch)\n",
|
| 308 |
+
"\n",
|
| 309 |
+
"def log_confusion_matrix(writer, all_preds, all_labels, epoch):\n",
|
| 310 |
+
" \"\"\"Log confusion matrix as image to TensorBoard.\"\"\"\n",
|
| 311 |
+
" from sklearn.metrics import confusion_matrix\n",
|
| 312 |
+
" cm = confusion_matrix(all_labels, all_preds, labels=range(10))\n",
|
| 313 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 314 |
+
" im = ax.imshow(cm, interpolation=\"nearest\", cmap=plt.cm.Blues)\n",
|
| 315 |
+
" ax.set_title(f\"Confusion Matrix \u2014 Epoch {epoch}\")\n",
|
| 316 |
+
" ax.set_xlabel(\"Predicted\")\n",
|
| 317 |
+
" ax.set_ylabel(\"True\")\n",
|
| 318 |
+
" tick_marks = range(10)\n",
|
| 319 |
+
" ax.set_xticks(tick_marks)\n",
|
| 320 |
+
" ax.set_xticklabels(CIFAR10_CLASSES, rotation=45, ha=\"right\")\n",
|
| 321 |
+
" ax.set_yticks(tick_marks)\n",
|
| 322 |
+
" ax.set_yticklabels(CIFAR10_CLASSES)\n",
|
| 323 |
+
" for i in range(10):\n",
|
| 324 |
+
" for j in range(10):\n",
|
| 325 |
+
" ax.text(j, i, str(cm[i, j]), ha=\"center\", va=\"center\",\n",
|
| 326 |
+
" color=\"white\" if cm[i, j] > cm.max() / 2 else \"black\", fontsize=7)\n",
|
| 327 |
+
" fig.tight_layout()\n",
|
| 328 |
+
" writer.add_figure(\"confusion_matrix\", fig, epoch)\n",
|
| 329 |
+
" plt.close(fig)\n",
|
| 330 |
+
"\n",
|
| 331 |
+
"def log_triangulation_stats(writer, tri, epoch):\n",
|
| 332 |
+
" \"\"\"Log triangulation distance statistics.\"\"\"\n",
|
| 333 |
+
" writer.add_scalar(\"tri/mean\", tri.mean().item(), epoch)\n",
|
| 334 |
+
" writer.add_scalar(\"tri/std\", tri.std().item(), epoch)\n",
|
| 335 |
+
" writer.add_scalar(\"tri/min\", tri.min().item(), epoch)\n",
|
| 336 |
+
" writer.add_scalar(\"tri/max\", tri.max().item(), epoch)\n",
|
| 337 |
+
" writer.add_histogram(\"tri/distances\", tri.detach().cpu(), epoch)\n",
|
| 338 |
+
"\n",
|
| 339 |
+
"def log_scatter_2d(writer, embeddings, labels, epoch, tag=\"scatter_2d\", method=\"pca\"):\n",
|
| 340 |
+
" \"\"\"Log 2D scatter plot of embeddings projected via PCA or t-SNE.\"\"\"\n",
|
| 341 |
+
" emb_np = embeddings.detach().cpu().numpy()\n",
|
| 342 |
+
" lab_np = labels.detach().cpu().numpy() if torch.is_tensor(labels) else np.array(labels)\n",
|
| 343 |
+
" if method == \"pca\":\n",
|
| 344 |
+
" from sklearn.decomposition import PCA\n",
|
| 345 |
+
" proj = PCA(n_components=2).fit_transform(emb_np)\n",
|
| 346 |
+
" else:\n",
|
| 347 |
+
" from sklearn.manifold import TSNE\n",
|
| 348 |
+
" proj = TSNE(n_components=2, perplexity=30, n_iter=500).fit_transform(emb_np)\n",
|
| 349 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 350 |
+
" for c in range(10):\n",
|
| 351 |
+
" mask = lab_np == c\n",
|
| 352 |
+
" ax.scatter(proj[mask, 0], proj[mask, 1], s=3, alpha=0.5, label=CIFAR10_CLASSES[c])\n",
|
| 353 |
+
" ax.legend(markerscale=5, fontsize=8)\n",
|
| 354 |
+
" ax.set_title(f\"{tag} ({method.upper()}) \u2014 Epoch {epoch}\")\n",
|
| 355 |
+
" fig.tight_layout()\n",
|
| 356 |
+
" writer.add_figure(f\"{tag}/{method}\", fig, epoch)\n",
|
| 357 |
+
" plt.close(fig)\n",
|
| 358 |
+
"\n",
|
| 359 |
+
"# \u2500\u2500 Training Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 360 |
+
"def train_one_epoch(model, loader, optimizer, criterion, device, epoch=0,\n",
|
| 361 |
+
" scheduler=None, extra_losses=None):\n",
|
| 362 |
+
" \"\"\"\n",
|
| 363 |
+
" Generic training loop for one epoch.\n",
|
| 364 |
+
" model: callable(batch) \u2192 (logits, embeddings_on_sphere)\n",
|
| 365 |
+
" extra_losses: list of (name, loss_fn, weight) where loss_fn(embeddings) \u2192 scalar\n",
|
| 366 |
+
" \"\"\"\n",
|
| 367 |
+
" model.train()\n",
|
| 368 |
+
" total_loss = 0\n",
|
| 369 |
+
" total_ce = 0\n",
|
| 370 |
+
" correct = 0\n",
|
| 371 |
+
" total = 0\n",
|
| 372 |
+
" loss_components = defaultdict(float)\n",
|
| 373 |
+
"\n",
|
| 374 |
+
" for batch_idx, (images, labels) in enumerate(loader):\n",
|
| 375 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 376 |
+
" optimizer.zero_grad()\n",
|
| 377 |
+
"\n",
|
| 378 |
+
" logits, embeddings = model(images)\n",
|
| 379 |
+
" ce_loss = criterion(logits, labels)\n",
|
| 380 |
+
" loss = ce_loss\n",
|
| 381 |
+
" loss_components[\"CE\"] += ce_loss.item()\n",
|
| 382 |
+
"\n",
|
| 383 |
+
" if extra_losses:\n",
|
| 384 |
+
" for name, loss_fn, weight in extra_losses:\n",
|
| 385 |
+
" if name == \"CV\":\n",
|
| 386 |
+
" el, vol_val = loss_fn(embeddings)\n",
|
| 387 |
+
" loss_components[\"CV_vol\"] += vol_val\n",
|
| 388 |
+
" elif name == \"InfoNCE_class\":\n",
|
| 389 |
+
" el = loss_fn(embeddings, labels)\n",
|
| 390 |
+
" else:\n",
|
| 391 |
+
" el = loss_fn(embeddings)\n",
|
| 392 |
+
" loss = loss + weight * el\n",
|
| 393 |
+
" loss_components[name] += el.item()\n",
|
| 394 |
+
"\n",
|
| 395 |
+
" loss.backward()\n",
|
| 396 |
+
" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n",
|
| 397 |
+
" optimizer.step()\n",
|
| 398 |
+
" if scheduler:\n",
|
| 399 |
+
" scheduler.step()\n",
|
| 400 |
+
"\n",
|
| 401 |
+
" total_loss += loss.item()\n",
|
| 402 |
+
" _, predicted = logits.max(1)\n",
|
| 403 |
+
" total += labels.size(0)\n",
|
| 404 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 405 |
+
"\n",
|
| 406 |
+
" n_batches = len(loader)\n",
|
| 407 |
+
" metrics = {\n",
|
| 408 |
+
" \"loss/total\": total_loss / n_batches,\n",
|
| 409 |
+
" \"loss/CE\": loss_components[\"CE\"] / n_batches,\n",
|
| 410 |
+
" \"accuracy/train\": 100.0 * correct / total,\n",
|
| 411 |
+
" }\n",
|
| 412 |
+
" for k, v in loss_components.items():\n",
|
| 413 |
+
" if k not in (\"CE\",):\n",
|
| 414 |
+
" metrics[f\"loss/{k}\"] = v / n_batches\n",
|
| 415 |
+
" return metrics\n",
|
| 416 |
+
"\n",
|
| 417 |
+
"@torch.no_grad()\n",
|
| 418 |
+
"def evaluate(model, loader, criterion, device, collect_embeddings=False):\n",
|
| 419 |
+
" \"\"\"Evaluate model on validation set. Optionally collect embeddings.\"\"\"\n",
|
| 420 |
+
" model.eval()\n",
|
| 421 |
+
" total_loss = 0\n",
|
| 422 |
+
" correct = 0\n",
|
| 423 |
+
" total = 0\n",
|
| 424 |
+
" all_preds = []\n",
|
| 425 |
+
" all_labels = []\n",
|
| 426 |
+
" all_embeddings = []\n",
|
| 427 |
+
"\n",
|
| 428 |
+
" for images, labels in loader:\n",
|
| 429 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 430 |
+
" logits, embeddings = model(images)\n",
|
| 431 |
+
" loss = criterion(logits, labels)\n",
|
| 432 |
+
" total_loss += loss.item()\n",
|
| 433 |
+
" _, predicted = logits.max(1)\n",
|
| 434 |
+
" total += labels.size(0)\n",
|
| 435 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 436 |
+
" all_preds.extend(predicted.cpu().numpy())\n",
|
| 437 |
+
" all_labels.extend(labels.cpu().numpy())\n",
|
| 438 |
+
" if collect_embeddings:\n",
|
| 439 |
+
" all_embeddings.append(embeddings.cpu())\n",
|
| 440 |
+
"\n",
|
| 441 |
+
" n_batches = len(loader)\n",
|
| 442 |
+
" metrics = {\n",
|
| 443 |
+
" \"loss/val\": total_loss / n_batches,\n",
|
| 444 |
+
" \"accuracy/val\": 100.0 * correct / total,\n",
|
| 445 |
+
" }\n",
|
| 446 |
+
" result = {\"metrics\": metrics, \"preds\": np.array(all_preds), \"labels\": np.array(all_labels)}\n",
|
| 447 |
+
" if collect_embeddings:\n",
|
| 448 |
+
" result[\"embeddings\"] = torch.cat(all_embeddings, dim=0)\n",
|
| 449 |
+
" return result\n",
|
| 450 |
+
"\n",
|
| 451 |
+
"def run_experiment(experiment_id, model, train_loader, val_loader, device,\n",
|
| 452 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, extra_losses=None,\n",
|
| 453 |
+
" push_anchors_every=50, log_embeddings_every=25,\n",
|
| 454 |
+
" patience=20):\n",
|
| 455 |
+
" \"\"\"\n",
|
| 456 |
+
" Full experiment runner with TensorBoard logging, early stopping, and diagnostics.\n",
|
| 457 |
+
" model must return (logits, sphere_embeddings).\n",
|
| 458 |
+
" \"\"\"\n",
|
| 459 |
+
" writer = make_writer(experiment_id)\n",
|
| 460 |
+
" criterion = nn.CrossEntropyLoss()\n",
|
| 461 |
+
" optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)\n",
|
| 462 |
+
" scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\n",
|
| 463 |
+
"\n",
|
| 464 |
+
" best_val_acc = 0.0\n",
|
| 465 |
+
" best_epoch = 0\n",
|
| 466 |
+
" no_improve = 0\n",
|
| 467 |
+
"\n",
|
| 468 |
+
" # Log model info\n",
|
| 469 |
+
" n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 470 |
+
" writer.add_text(\"config/params\", f\"Learned params: {n_params:,}\")\n",
|
| 471 |
+
" writer.add_text(\"config/experiment\", experiment_id)\n",
|
| 472 |
+
" print(f\"\\n{'='*70}\")\n",
|
| 473 |
+
" print(f\"[EXP] {experiment_id}\")\n",
|
| 474 |
+
" print(f\"[EXP] Learned params: {n_params:,}, Epochs: {epochs}, LR: {lr}\")\n",
|
| 475 |
+
" print(f\"{'='*70}\")\n",
|
| 476 |
+
"\n",
|
| 477 |
+
" for epoch in range(1, epochs + 1):\n",
|
| 478 |
+
" t0 = time.time()\n",
|
| 479 |
+
"\n",
|
| 480 |
+
" # Train\n",
|
| 481 |
+
" train_metrics = train_one_epoch(\n",
|
| 482 |
+
" model, train_loader, optimizer, criterion, device, epoch,\n",
|
| 483 |
+
" extra_losses=extra_losses\n",
|
| 484 |
+
" )\n",
|
| 485 |
+
"\n",
|
| 486 |
+
" # Evaluate\n",
|
| 487 |
+
" collect_emb = (epoch % log_embeddings_every == 0) or (epoch == epochs)\n",
|
| 488 |
+
" val_result = evaluate(model, val_loader, criterion, device, collect_embeddings=collect_emb)\n",
|
| 489 |
+
" val_metrics = val_result[\"metrics\"]\n",
|
| 490 |
+
"\n",
|
| 491 |
+
" # Combine metrics\n",
|
| 492 |
+
" all_metrics = {**train_metrics, **val_metrics}\n",
|
| 493 |
+
" all_metrics[\"lr\"] = optimizer.param_groups[0][\"lr\"]\n",
|
| 494 |
+
" all_metrics[\"epoch_time\"] = time.time() - t0\n",
|
| 495 |
+
" all_metrics[\"train_val_gap\"] = train_metrics[\"accuracy/train\"] - val_metrics[\"accuracy/val\"]\n",
|
| 496 |
+
"\n",
|
| 497 |
+
" # Constellation diagnostics\n",
|
| 498 |
+
" if hasattr(model, \"constellation\"):\n",
|
| 499 |
+
" constellation = model.constellation\n",
|
| 500 |
+
" elif hasattr(model, \"classifier\") and hasattr(model.classifier, \"constellation\"):\n",
|
| 501 |
+
" constellation = model.classifier.constellation\n",
|
| 502 |
+
" else:\n",
|
| 503 |
+
" constellation = None\n",
|
| 504 |
+
"\n",
|
| 505 |
+
" if constellation is not None:\n",
|
| 506 |
+
" log_anchor_histogram(writer, constellation, epoch)\n",
|
| 507 |
+
"\n",
|
| 508 |
+
" # Push anchors periodically\n",
|
| 509 |
+
" if epoch % push_anchors_every == 0 and \"embeddings\" in val_result:\n",
|
| 510 |
+
" emb = val_result[\"embeddings\"][:2000].to(device)\n",
|
| 511 |
+
" lab = torch.tensor(val_result[\"labels\"][:2000]).to(device)\n",
|
| 512 |
+
" constellation.push_anchors(emb, lab)\n",
|
| 513 |
+
" active, total_anch = constellation.anchor_activation(emb)\n",
|
| 514 |
+
" all_metrics[\"anchors/active\"] = active\n",
|
| 515 |
+
" all_metrics[\"anchors/total\"] = total_anch\n",
|
| 516 |
+
" all_metrics[\"anchors/utilization\"] = active / total_anch\n",
|
| 517 |
+
" print(f\" [ANCHOR PUSH] Active: {active}/{total_anch} ({100*active/total_anch:.1f}%)\")\n",
|
| 518 |
+
"\n",
|
| 519 |
+
" # Log all metrics\n",
|
| 520 |
+
" log_epoch_metrics(writer, epoch, all_metrics)\n",
|
| 521 |
+
" scheduler.step()\n",
|
| 522 |
+
"\n",
|
| 523 |
+
" # Log embeddings visualization\n",
|
| 524 |
+
" if collect_emb and \"embeddings\" in val_result:\n",
|
| 525 |
+
" emb = val_result[\"embeddings\"][:2000]\n",
|
| 526 |
+
" lab = val_result[\"labels\"][:2000]\n",
|
| 527 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings\", method=\"pca\")\n",
|
| 528 |
+
" if epoch == epochs:\n",
|
| 529 |
+
" log_confusion_matrix(writer, val_result[\"preds\"], val_result[\"labels\"], epoch)\n",
|
| 530 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings_tsne\", method=\"tsne\")\n",
|
| 531 |
+
"\n",
|
| 532 |
+
" # Log triangulation stats from a sample batch\n",
|
| 533 |
+
" if constellation is not None and collect_emb and \"embeddings\" in val_result:\n",
|
| 534 |
+
" sample_emb = val_result[\"embeddings\"][:500].to(device)\n",
|
| 535 |
+
" tri_sample = constellation(sample_emb)\n",
|
| 536 |
+
" log_triangulation_stats(writer, tri_sample, epoch)\n",
|
| 537 |
+
"\n",
|
| 538 |
+
" # Best model tracking\n",
|
| 539 |
+
" val_acc = val_metrics[\"accuracy/val\"]\n",
|
| 540 |
+
" if val_acc > best_val_acc:\n",
|
| 541 |
+
" best_val_acc = val_acc\n",
|
| 542 |
+
" best_epoch = epoch\n",
|
| 543 |
+
" no_improve = 0\n",
|
| 544 |
+
" writer.add_scalar(\"best/val_acc\", best_val_acc, epoch)\n",
|
| 545 |
+
" writer.add_scalar(\"best/epoch\", best_epoch, epoch)\n",
|
| 546 |
+
" else:\n",
|
| 547 |
+
" no_improve += 1\n",
|
| 548 |
+
"\n",
|
| 549 |
+
" # Print progress\n",
|
| 550 |
+
" if epoch % 5 == 0 or epoch == 1 or epoch == epochs:\n",
|
| 551 |
+
" print(f\" E{epoch:3d} | TrainAcc {train_metrics['accuracy/train']:5.1f}% | \"\n",
|
| 552 |
+
" f\"ValAcc {val_acc:5.1f}% | Loss {all_metrics['loss/total']:.4f} | \"\n",
|
| 553 |
+
" f\"Best {best_val_acc:.1f}% @E{best_epoch} | \"\n",
|
| 554 |
+
" f\"Gap {all_metrics['train_val_gap']:+.1f}% | \"\n",
|
| 555 |
+
" f\"{all_metrics['epoch_time']:.1f}s\")\n",
|
| 556 |
+
"\n",
|
| 557 |
+
" # Early stopping\n",
|
| 558 |
+
" if no_improve >= patience:\n",
|
| 559 |
+
" print(f\" [EARLY STOP] No improvement for {patience} epochs. Best: {best_val_acc:.1f}% @E{best_epoch}\")\n",
|
| 560 |
+
" break\n",
|
| 561 |
+
"\n",
|
| 562 |
+
" writer.close()\n",
|
| 563 |
+
" print(f\"\\n[RESULT] {experiment_id}: Best Val Acc = {best_val_acc:.2f}% at epoch {best_epoch}\")\n",
|
| 564 |
+
" print(f\"[RESULT] Learned params: {n_params:,}\")\n",
|
| 565 |
+
" return {\"experiment\": experiment_id, \"best_val_acc\": best_val_acc, \"best_epoch\": best_epoch,\n",
|
| 566 |
+
" \"params\": n_params, \"final_train_acc\": train_metrics[\"accuracy/train\"]}\n",
|
| 567 |
+
"\n",
|
| 568 |
+
"# \u2500\u2500 Johnson-Lindenstrauss Projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 569 |
+
"class JLProjection(nn.Module):\n",
|
| 570 |
+
" \"\"\"Fixed random Gaussian projection (not learned).\"\"\"\n",
|
| 571 |
+
" def __init__(self, input_dim, output_dim):\n",
|
| 572 |
+
" super().__init__()\n",
|
| 573 |
+
" self.register_buffer(\"W\", torch.randn(input_dim, output_dim) / math.sqrt(output_dim))\n",
|
| 574 |
+
"\n",
|
| 575 |
+
" def forward(self, x):\n",
|
| 576 |
+
" return x @ self.W\n",
|
| 577 |
+
"\n",
|
| 578 |
+
"# \u2500\u2500 Results Scoreboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 579 |
+
"class Scoreboard:\n",
|
| 580 |
+
" \"\"\"Track results across experiments in a session.\"\"\"\n",
|
| 581 |
+
" def __init__(self):\n",
|
| 582 |
+
" self.results = []\n",
|
| 583 |
+
"\n",
|
| 584 |
+
" def add(self, result_dict):\n",
|
| 585 |
+
" self.results.append(result_dict)\n",
|
| 586 |
+
" self._print_table()\n",
|
| 587 |
+
"\n",
|
| 588 |
+
" def _print_table(self):\n",
|
| 589 |
+
" print(f\"\\n{'='*80}\")\n",
|
| 590 |
+
" print(f\"{'SCOREBOARD':^80}\")\n",
|
| 591 |
+
" print(f\"{'='*80}\")\n",
|
| 592 |
+
" print(f\"{'Experiment':<40} {'Val Acc':>8} {'Params':>10} {'Best E':>7}\")\n",
|
| 593 |
+
" print(f\"{'-'*40} {'-'*8} {'-'*10} {'-'*7}\")\n",
|
| 594 |
+
" for r in sorted(self.results, key=lambda x: x[\"best_val_acc\"], reverse=True):\n",
|
| 595 |
+
" print(f\"{r['experiment']:<40} {r['best_val_acc']:>7.2f}% {r['params']:>10,} {r['best_epoch']:>7}\")\n",
|
| 596 |
+
" print(f\"{'='*80}\\n\")\n",
|
| 597 |
+
"\n",
|
| 598 |
+
" def save(self, path=\"scoreboard.json\"):\n",
|
| 599 |
+
" with open(path, \"w\") as f:\n",
|
| 600 |
+
" json.dump(self.results, f, indent=2)\n",
|
| 601 |
+
" print(f\"[SCOREBOARD] Saved to {path}\")\n",
|
| 602 |
+
"\n",
|
| 603 |
+
"scoreboard = Scoreboard()\n",
|
| 604 |
+
"print(\"[SHARED] All utilities loaded successfully.\")\n",
|
| 605 |
+
""
|
| 606 |
+
],
|
| 607 |
+
"execution_count": null,
|
| 608 |
+
"outputs": []
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"cell_type": "markdown",
|
| 612 |
+
"metadata": {},
|
| 613 |
+
"source": [
|
| 614 |
+
"## Additional Loss Functions"
|
| 615 |
+
]
|
| 616 |
+
},
|
| 617 |
+
{
|
| 618 |
+
"cell_type": "code",
|
| 619 |
+
"metadata": {},
|
| 620 |
+
"source": [
|
| 621 |
+
"\n",
|
| 622 |
+
"from scipy.special import iv as bessel_i\n",
|
| 623 |
+
"\n",
|
| 624 |
+
"class vMFLoss(nn.Module):\n",
|
| 625 |
+
" '''von Mises-Fisher based contrastive loss.'''\n",
|
| 626 |
+
" def __init__(self, temperature=0.07, kappa_min=0.1, kappa_max=100.0):\n",
|
| 627 |
+
" super().__init__()\n",
|
| 628 |
+
" self.temperature = temperature\n",
|
| 629 |
+
" self.kappa_min = kappa_min\n",
|
| 630 |
+
" self.kappa_max = kappa_max\n",
|
| 631 |
+
"\n",
|
| 632 |
+
" def forward(self, z, labels):\n",
|
| 633 |
+
" z = l2_normalize(z)\n",
|
| 634 |
+
" B = z.shape[0]\n",
|
| 635 |
+
"\n",
|
| 636 |
+
" # Compute similarity\n",
|
| 637 |
+
" sim = torch.mm(z, z.T) # (B, B)\n",
|
| 638 |
+
"\n",
|
| 639 |
+
" # Estimate kappa from concentration of similarities\n",
|
| 640 |
+
" sim_mean = sim.mean().item()\n",
|
| 641 |
+
" kappa = np.clip(sim_mean * 50, self.kappa_min, self.kappa_max)\n",
|
| 642 |
+
"\n",
|
| 643 |
+
" # vMF log-likelihood (approximation)\n",
|
| 644 |
+
" # log p(z_i | mu) ~ kappa * z_i^T mu - log(C_d(kappa))\n",
|
| 645 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 646 |
+
" label_mask.fill_diagonal_(0)\n",
|
| 647 |
+
"\n",
|
| 648 |
+
" # Positive pairs have higher kappa\n",
|
| 649 |
+
" log_likelihood = kappa * sim\n",
|
| 650 |
+
" pos_mask = label_mask\n",
|
| 651 |
+
" neg_mask = 1 - label_mask\n",
|
| 652 |
+
" neg_mask.fill_diagonal_(0)\n",
|
| 653 |
+
"\n",
|
| 654 |
+
" pos_ll = (log_likelihood * pos_mask).sum(dim=1)\n",
|
| 655 |
+
" neg_ll = (log_likelihood * neg_mask).sum(dim=1)\n",
|
| 656 |
+
"\n",
|
| 657 |
+
" # Contrastive objective\n",
|
| 658 |
+
" loss = -torch.log(torch.exp(pos_ll) / (torch.exp(pos_ll) + torch.exp(neg_ll) + 1e-8) + 1e-8).mean()\n",
|
| 659 |
+
" return loss\n",
|
| 660 |
+
"\n",
|
| 661 |
+
"class FisherRaoLoss(nn.Module):\n",
|
| 662 |
+
" '''Fisher-Rao distance as loss: d_FR(p,q) = 2\u00b7arccos(\u222b\u221a(pq)).'''\n",
|
| 663 |
+
" def __init__(self, temperature=0.07):\n",
|
| 664 |
+
" super().__init__()\n",
|
| 665 |
+
" self.temperature = temperature\n",
|
| 666 |
+
"\n",
|
| 667 |
+
" def forward(self, z, labels):\n",
|
| 668 |
+
" z = l2_normalize(z)\n",
|
| 669 |
+
" B = z.shape[0]\n",
|
| 670 |
+
"\n",
|
| 671 |
+
" # Pairwise cosine similarity\n",
|
| 672 |
+
" sim = torch.mm(z, z.T) # (B, B)\n",
|
| 673 |
+
"\n",
|
| 674 |
+
" # Fisher-Rao distance ~ arccos(similarity)\n",
|
| 675 |
+
" # For normalized vectors, Bhattacharyya coefficient \u2248 sim\n",
|
| 676 |
+
" dist = torch.acos(torch.clamp(sim, -1 + 1e-7, 1 - 1e-7))\n",
|
| 677 |
+
"\n",
|
| 678 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 679 |
+
" label_mask.fill_diagonal_(0)\n",
|
| 680 |
+
"\n",
|
| 681 |
+
" # Minimize distance within class, maximize across class\n",
|
| 682 |
+
" pos_dist = (dist * label_mask).sum(dim=1) / (label_mask.sum(dim=1) + 1e-8)\n",
|
| 683 |
+
" neg_dist = (dist * (1 - label_mask)).sum(dim=1) / ((1 - label_mask).sum(dim=1) + 1e-8)\n",
|
| 684 |
+
"\n",
|
| 685 |
+
" loss = (pos_dist - neg_dist + 1.0).relu().mean()\n",
|
| 686 |
+
" return loss\n",
|
| 687 |
+
"\n",
|
| 688 |
+
"class SlicedWassersteinLoss(nn.Module):\n",
|
| 689 |
+
" '''Sliced Wasserstein distance on hypersphere.'''\n",
|
| 690 |
+
" def __init__(self, n_projections=50):\n",
|
| 691 |
+
" super().__init__()\n",
|
| 692 |
+
" self.n_projections = n_projections\n",
|
| 693 |
+
"\n",
|
| 694 |
+
" def forward(self, z, labels):\n",
|
| 695 |
+
" z = l2_normalize(z)\n",
|
| 696 |
+
" B, d = z.shape\n",
|
| 697 |
+
"\n",
|
| 698 |
+
" # Random projections on sphere\n",
|
| 699 |
+
" thetas = torch.randn(self.n_projections, d, device=z.device)\n",
|
| 700 |
+
" thetas = F.normalize(thetas, dim=1)\n",
|
| 701 |
+
"\n",
|
| 702 |
+
" # Project embeddings\n",
|
| 703 |
+
" proj_z = torch.mm(z, thetas.T) # (B, n_proj)\n",
|
| 704 |
+
"\n",
|
| 705 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 706 |
+
" label_mask.fill_diagonal_(0)\n",
|
| 707 |
+
"\n",
|
| 708 |
+
" # Sort projections\n",
|
| 709 |
+
" proj_sorted, _ = torch.sort(proj_z, dim=0)\n",
|
| 710 |
+
"\n",
|
| 711 |
+
" # Compute sliced Wasserstein\n",
|
| 712 |
+
" loss = 0.0\n",
|
| 713 |
+
" for b in range(B):\n",
|
| 714 |
+
" pos_idx = (labels == labels[b]).nonzero(as_tuple=True)[0]\n",
|
| 715 |
+
" if len(pos_idx) > 1:\n",
|
| 716 |
+
" pos_proj = proj_z[pos_idx]\n",
|
| 717 |
+
" neg_proj = proj_z[labels != labels[b]]\n",
|
| 718 |
+
"\n",
|
| 719 |
+
" if len(neg_proj) > 0:\n",
|
| 720 |
+
" for i in range(self.n_projections):\n",
|
| 721 |
+
" w_dist = torch.abs(pos_proj[:, i].sort()[0] - neg_proj[:, i].sort()[0]).mean()\n",
|
| 722 |
+
" loss = loss + w_dist\n",
|
| 723 |
+
"\n",
|
| 724 |
+
" return loss / (B + 1e-8)\n",
|
| 725 |
+
"\n",
|
| 726 |
+
"class GeometricAutogradLoss(nn.Module):\n",
|
| 727 |
+
" '''Geometric loss: tangential projection + separation preservation.'''\n",
|
| 728 |
+
" def __init__(self):\n",
|
| 729 |
+
" super().__init__()\n",
|
| 730 |
+
"\n",
|
| 731 |
+
" def forward(self, z, labels):\n",
|
| 732 |
+
" z = l2_normalize(z)\n",
|
| 733 |
+
" B = z.shape[0]\n",
|
| 734 |
+
"\n",
|
| 735 |
+
" # Compute gradients manually if needed\n",
|
| 736 |
+
" z.requires_grad_(True)\n",
|
| 737 |
+
"\n",
|
| 738 |
+
" # Project to tangent space at each point\n",
|
| 739 |
+
" # T_z S^d = {v : v^T z = 0}\n",
|
| 740 |
+
" I = torch.eye(z.shape[1], device=z.device)\n",
|
| 741 |
+
" z_outer = torch.bmm(z.unsqueeze(2), z.unsqueeze(1)) # (B, d, d)\n",
|
| 742 |
+
" proj = I.unsqueeze(0) - z_outer # (B, d, d)\n",
|
| 743 |
+
"\n",
|
| 744 |
+
" # Separation: maximize distance between different classes\n",
|
| 745 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 746 |
+
" label_mask.fill_diagonal_(0)\n",
|
| 747 |
+
"\n",
|
| 748 |
+
" # Pairwise distances\n",
|
| 749 |
+
" z_diff = z.unsqueeze(1) - z.unsqueeze(0) # (B, B, d)\n",
|
| 750 |
+
" distances = torch.norm(z_diff, dim=2) # (B, B)\n",
|
| 751 |
+
"\n",
|
| 752 |
+
" # Loss: push different classes apart\n",
|
| 753 |
+
" loss = (distances * (1 - label_mask)).mean()\n",
|
| 754 |
+
" return loss\n",
|
| 755 |
+
"\n",
|
| 756 |
+
"print(\"[LOSSES] All additional losses loaded.\")\n",
|
| 757 |
+
""
|
| 758 |
+
],
|
| 759 |
+
"execution_count": null,
|
| 760 |
+
"outputs": []
|
| 761 |
+
},
|
| 762 |
+
{
|
| 763 |
+
"cell_type": "markdown",
|
| 764 |
+
"metadata": {},
|
| 765 |
+
"source": [
|
| 766 |
+
"## 6.2a: CE-Only Baseline (No Contrastive Loss)\n",
|
| 767 |
+
"\n",
|
| 768 |
+
"Measure ceiling with scattering front-end"
|
| 769 |
+
]
|
| 770 |
+
},
|
| 771 |
+
{
|
| 772 |
+
"cell_type": "code",
|
| 773 |
+
"metadata": {},
|
| 774 |
+
"source": [
|
| 775 |
+
"\n",
|
| 776 |
+
"from kymatio.torch import Scattering2D\n",
|
| 777 |
+
"\n",
|
| 778 |
+
"print(\"[6.2a] CE-Only Baseline\")\n",
|
| 779 |
+
"print(\"=\"*70)\n",
|
| 780 |
+
"\n",
|
| 781 |
+
"class CEOnlyModel(nn.Module):\n",
|
| 782 |
+
" '''Scattering2D \u2192 JL(128) \u2192 L2 norm \u2192 constellation \u2192 classify (CE only).'''\n",
|
| 783 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 784 |
+
" super().__init__()\n",
|
| 785 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 786 |
+
" self.scatt_dim = 81 # J=2, L=8 default output\n",
|
| 787 |
+
" self.jl = JLProjection(self.scatt_dim, 128)\n",
|
| 788 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 789 |
+
"\n",
|
| 790 |
+
" def forward(self, x):\n",
|
| 791 |
+
" scatt = self.scattering(x) # (B, 81, 32, 32)\n",
|
| 792 |
+
" scatt_flat = scatt.view(scatt.shape[0], -1) # (B, 81*32*32)\n",
|
| 793 |
+
"\n",
|
| 794 |
+
" # Dimensionality reduction\n",
|
| 795 |
+
" reduced = F.adaptive_avg_pool2d(scatt.view(-1, 81, 32, 32), (1, 1)).squeeze(-1).squeeze(-1) # (B, 81)\n",
|
| 796 |
+
" jl_feat = self.jl(reduced) # (B, 128)\n",
|
| 797 |
+
" emb = l2_normalize(jl_feat)\n",
|
| 798 |
+
"\n",
|
| 799 |
+
" logits, tri = self.classifier(emb)\n",
|
| 800 |
+
" return logits, emb\n",
|
| 801 |
+
"\n",
|
| 802 |
+
"model = CEOnlyModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 803 |
+
"print(f\"[6.2a] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 804 |
+
"\n",
|
| 805 |
+
"train_loader, val_loader = get_cifar10_loaders(batch_size=256, augment=True)\n",
|
| 806 |
+
"\n",
|
| 807 |
+
"result_6_2a = run_experiment(\n",
|
| 808 |
+
" \"6.2a_CE_Only\",\n",
|
| 809 |
+
" model, train_loader, val_loader, device,\n",
|
| 810 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 811 |
+
")\n",
|
| 812 |
+
"scoreboard.add(result_6_2a)\n",
|
| 813 |
+
""
|
| 814 |
+
],
|
| 815 |
+
"execution_count": null,
|
| 816 |
+
"outputs": []
|
| 817 |
+
},
|
| 818 |
+
{
|
| 819 |
+
"cell_type": "markdown",
|
| 820 |
+
"metadata": {},
|
| 821 |
+
"source": [
|
| 822 |
+
"## 6.2b: Class-Conditional InfoNCE\n",
|
| 823 |
+
"\n",
|
| 824 |
+
"Positive = same class, not same image"
|
| 825 |
+
]
|
| 826 |
+
},
|
| 827 |
+
{
|
| 828 |
+
"cell_type": "code",
|
| 829 |
+
"metadata": {},
|
| 830 |
+
"source": [
|
| 831 |
+
"\n",
|
| 832 |
+
"print(\"[6.2b] Class-Conditional InfoNCE\")\n",
|
| 833 |
+
"print(\"=\"*70)\n",
|
| 834 |
+
"\n",
|
| 835 |
+
"class ClassConditionalModel(nn.Module):\n",
|
| 836 |
+
" '''CE + Class-Conditional InfoNCE loss.'''\n",
|
| 837 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 838 |
+
" super().__init__()\n",
|
| 839 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 840 |
+
" self.jl = JLProjection(81, 128)\n",
|
| 841 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 842 |
+
"\n",
|
| 843 |
+
" def forward(self, x):\n",
|
| 844 |
+
" scatt = self.scattering(x)\n",
|
| 845 |
+
" reduced = F.adaptive_avg_pool2d(scatt.view(-1, 81, 32, 32), (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 846 |
+
" jl_feat = self.jl(reduced)\n",
|
| 847 |
+
" emb = l2_normalize(jl_feat)\n",
|
| 848 |
+
" logits, tri = self.classifier(emb)\n",
|
| 849 |
+
" return logits, emb\n",
|
| 850 |
+
"\n",
|
| 851 |
+
"model = ClassConditionalModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 852 |
+
"print(f\"[6.2b] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 853 |
+
"\n",
|
| 854 |
+
"class_info_loss = ClassConditionalInfoNCE(temperature=0.07)\n",
|
| 855 |
+
"\n",
|
| 856 |
+
"result_6_2b = run_experiment(\n",
|
| 857 |
+
" \"6.2b_ClassConditionalInfoNCE\",\n",
|
| 858 |
+
" model, train_loader, val_loader, device,\n",
|
| 859 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 860 |
+
" extra_losses=[(\"InfoNCE_class\", class_info_loss, 0.5)],\n",
|
| 861 |
+
" patience=20\n",
|
| 862 |
+
")\n",
|
| 863 |
+
"scoreboard.add(result_6_2b)\n",
|
| 864 |
+
""
|
| 865 |
+
],
|
| 866 |
+
"execution_count": null,
|
| 867 |
+
"outputs": []
|
| 868 |
+
},
|
| 869 |
+
{
|
| 870 |
+
"cell_type": "markdown",
|
| 871 |
+
"metadata": {},
|
| 872 |
+
"source": [
|
| 873 |
+
"## 6.2c: vMF-based Contrastive Loss\n",
|
| 874 |
+
"\n",
|
| 875 |
+
"Replace dot-product similarity with vMF log-likelihood"
|
| 876 |
+
]
|
| 877 |
+
},
|
| 878 |
+
{
|
| 879 |
+
"cell_type": "code",
|
| 880 |
+
"metadata": {},
|
| 881 |
+
"source": [
|
| 882 |
+
"\n",
|
| 883 |
+
"print(\"[6.2c] vMF-based Contrastive Loss\")\n",
|
| 884 |
+
"print(\"=\"*70)\n",
|
| 885 |
+
"\n",
|
| 886 |
+
"model = ClassConditionalModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 887 |
+
"print(f\"[6.2c] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 888 |
+
"\n",
|
| 889 |
+
"vmf_loss = vMFLoss(temperature=0.07)\n",
|
| 890 |
+
"\n",
|
| 891 |
+
"result_6_2c = run_experiment(\n",
|
| 892 |
+
" \"6.2c_vMF_Contrastive\",\n",
|
| 893 |
+
" model, train_loader, val_loader, device,\n",
|
| 894 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 895 |
+
" extra_losses=[(\"vMF\", vmf_loss, 0.5)],\n",
|
| 896 |
+
" patience=20\n",
|
| 897 |
+
")\n",
|
| 898 |
+
"scoreboard.add(result_6_2c)\n",
|
| 899 |
+
""
|
| 900 |
+
],
|
| 901 |
+
"execution_count": null,
|
| 902 |
+
"outputs": []
|
| 903 |
+
},
|
| 904 |
+
{
|
| 905 |
+
"cell_type": "markdown",
|
| 906 |
+
"metadata": {},
|
| 907 |
+
"source": [
|
| 908 |
+
"## 6.2d: Fisher-Rao Distance as Loss\n",
|
| 909 |
+
"\n",
|
| 910 |
+
"Geometric distance d_FR(p,q) = 2\u00b7arccos(\u222b\u221a(pq))"
|
| 911 |
+
]
|
| 912 |
+
},
|
| 913 |
+
{
|
| 914 |
+
"cell_type": "code",
|
| 915 |
+
"metadata": {},
|
| 916 |
+
"source": [
|
| 917 |
+
"\n",
|
| 918 |
+
"print(\"[6.2d] Fisher-Rao Distance Loss\")\n",
|
| 919 |
+
"print(\"=\"*70)\n",
|
| 920 |
+
"\n",
|
| 921 |
+
"model = ClassConditionalModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 922 |
+
"print(f\"[6.2d] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 923 |
+
"\n",
|
| 924 |
+
"fr_loss = FisherRaoLoss(temperature=0.07)\n",
|
| 925 |
+
"\n",
|
| 926 |
+
"result_6_2d = run_experiment(\n",
|
| 927 |
+
" \"6.2d_FisherRao\",\n",
|
| 928 |
+
" model, train_loader, val_loader, device,\n",
|
| 929 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 930 |
+
" extra_losses=[(\"FisherRao\", fr_loss, 0.5)],\n",
|
| 931 |
+
" patience=20\n",
|
| 932 |
+
")\n",
|
| 933 |
+
"scoreboard.add(result_6_2d)\n",
|
| 934 |
+
""
|
| 935 |
+
],
|
| 936 |
+
"execution_count": null,
|
| 937 |
+
"outputs": []
|
| 938 |
+
},
|
| 939 |
+
{
|
| 940 |
+
"cell_type": "markdown",
|
| 941 |
+
"metadata": {},
|
| 942 |
+
"source": [
|
| 943 |
+
"## 6.2e: Sliced Spherical Wasserstein Distance\n",
|
| 944 |
+
"\n",
|
| 945 |
+
"Distribution matching loss on hypersphere"
|
| 946 |
+
]
|
| 947 |
+
},
|
| 948 |
+
{
|
| 949 |
+
"cell_type": "code",
|
| 950 |
+
"metadata": {},
|
| 951 |
+
"source": [
|
| 952 |
+
"\n",
|
| 953 |
+
"print(\"[6.2e] Sliced Spherical Wasserstein Distance\")\n",
|
| 954 |
+
"print(\"=\"*70)\n",
|
| 955 |
+
"\n",
|
| 956 |
+
"model = ClassConditionalModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 957 |
+
"print(f\"[6.2e] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 958 |
+
"\n",
|
| 959 |
+
"swd_loss = SlicedWassersteinLoss(n_projections=50)\n",
|
| 960 |
+
"\n",
|
| 961 |
+
"result_6_2e = run_experiment(\n",
|
| 962 |
+
" \"6.2e_SlicedWasserstein\",\n",
|
| 963 |
+
" model, train_loader, val_loader, device,\n",
|
| 964 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 965 |
+
" extra_losses=[(\"SWD\", swd_loss, 0.3)],\n",
|
| 966 |
+
" patience=20\n",
|
| 967 |
+
")\n",
|
| 968 |
+
"scoreboard.add(result_6_2e)\n",
|
| 969 |
+
""
|
| 970 |
+
],
|
| 971 |
+
"execution_count": null,
|
| 972 |
+
"outputs": []
|
| 973 |
+
},
|
| 974 |
+
{
|
| 975 |
+
"cell_type": "markdown",
|
| 976 |
+
"metadata": {},
|
| 977 |
+
"source": [
|
| 978 |
+
"## 6.2f: Geometric Autograd Loss\n",
|
| 979 |
+
"\n",
|
| 980 |
+
"Tangential projection + separation preservation on gradients"
|
| 981 |
+
]
|
| 982 |
+
},
|
| 983 |
+
{
|
| 984 |
+
"cell_type": "code",
|
| 985 |
+
"metadata": {},
|
| 986 |
+
"source": [
|
| 987 |
+
"\n",
|
| 988 |
+
"print(\"[6.2f] Geometric Autograd Loss\")\n",
|
| 989 |
+
"print(\"=\"*70)\n",
|
| 990 |
+
"\n",
|
| 991 |
+
"model = ClassConditionalModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 992 |
+
"print(f\"[6.2f] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 993 |
+
"\n",
|
| 994 |
+
"geom_loss = GeometricAutogradLoss()\n",
|
| 995 |
+
"\n",
|
| 996 |
+
"result_6_2f = run_experiment(\n",
|
| 997 |
+
" \"6.2f_GeometricAutograd\",\n",
|
| 998 |
+
" model, train_loader, val_loader, device,\n",
|
| 999 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 1000 |
+
" extra_losses=[(\"Geometric\", geom_loss, 0.3)],\n",
|
| 1001 |
+
" patience=20\n",
|
| 1002 |
+
")\n",
|
| 1003 |
+
"scoreboard.add(result_6_2f)\n",
|
| 1004 |
+
""
|
| 1005 |
+
],
|
| 1006 |
+
"execution_count": null,
|
| 1007 |
+
"outputs": []
|
| 1008 |
+
},
|
| 1009 |
+
{
|
| 1010 |
+
"cell_type": "markdown",
|
| 1011 |
+
"metadata": {},
|
| 1012 |
+
"source": [
|
| 1013 |
+
"## 6.3a: Anchor Push Frequency Sweep\n",
|
| 1014 |
+
"\n",
|
| 1015 |
+
"Every 10, 25, 50, 100, 200 batches"
|
| 1016 |
+
]
|
| 1017 |
+
},
|
| 1018 |
+
{
|
| 1019 |
+
"cell_type": "code",
|
| 1020 |
+
"metadata": {},
|
| 1021 |
+
"source": [
|
| 1022 |
+
"\n",
|
| 1023 |
+
"print(\"[6.3a] Anchor Push Frequency Sweep\")\n",
|
| 1024 |
+
"print(\"=\"*70)\n",
|
| 1025 |
+
"\n",
|
| 1026 |
+
"for freq in [10, 25, 50, 100, 200]:\n",
|
| 1027 |
+
" model = ClassConditionalModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 1028 |
+
"\n",
|
| 1029 |
+
" # Calculate push_anchors_every from batch count\n",
|
| 1030 |
+
" # ~196 batches per epoch (50k / 256)\n",
|
| 1031 |
+
" push_every_epochs = max(1, freq // 196)\n",
|
| 1032 |
+
"\n",
|
| 1033 |
+
" result = run_experiment(\n",
|
| 1034 |
+
" f\"6.3a_AnchorPushFreq_{freq}\",\n",
|
| 1035 |
+
" model, train_loader, val_loader, device,\n",
|
| 1036 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 1037 |
+
" push_anchors_every=push_every_epochs,\n",
|
| 1038 |
+
" patience=20\n",
|
| 1039 |
+
" )\n",
|
| 1040 |
+
" scoreboard.add(result)\n",
|
| 1041 |
+
" print(f\" Frequency: {freq} batches \u2192 push_anchors_every={push_every_epochs} epochs\")\n",
|
| 1042 |
+
""
|
| 1043 |
+
],
|
| 1044 |
+
"execution_count": null,
|
| 1045 |
+
"outputs": []
|
| 1046 |
+
},
|
| 1047 |
+
{
|
| 1048 |
+
"cell_type": "markdown",
|
| 1049 |
+
"metadata": {},
|
| 1050 |
+
"source": [
|
| 1051 |
+
"## 6.3b: Anchor Push with vMF-weighted Centroids"
|
| 1052 |
+
]
|
| 1053 |
+
},
|
| 1054 |
+
{
|
| 1055 |
+
"cell_type": "code",
|
| 1056 |
+
"metadata": {},
|
| 1057 |
+
"source": [
|
| 1058 |
+
"\n",
|
| 1059 |
+
"print(\"[6.3b] Anchor Push with vMF-weighted Centroids\")\n",
|
| 1060 |
+
"print(\"=\"*70)\n",
|
| 1061 |
+
"\n",
|
| 1062 |
+
"class ConstellationWithvMFPush(nn.Module):\n",
|
| 1063 |
+
" '''Constellation with vMF-weighted anchor push.'''\n",
|
| 1064 |
+
" def __init__(self, embed_dim, num_anchors=64):\n",
|
| 1065 |
+
" super().__init__()\n",
|
| 1066 |
+
" self.embed_dim = embed_dim\n",
|
| 1067 |
+
" self.num_anchors = num_anchors\n",
|
| 1068 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, \"uniform\")\n",
|
| 1069 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False)\n",
|
| 1070 |
+
" self.kappa = 10.0 # von Mises-Fisher concentration\n",
|
| 1071 |
+
"\n",
|
| 1072 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 1073 |
+
" A = torch.randn(K, d)\n",
|
| 1074 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 1075 |
+
" anchors = Q.T[:K]\n",
|
| 1076 |
+
" if K > d:\n",
|
| 1077 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 1078 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 1079 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 1080 |
+
"\n",
|
| 1081 |
+
" def forward(self, x):\n",
|
| 1082 |
+
" x_norm = l2_normalize(x)\n",
|
| 1083 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 1084 |
+
" distances = 1.0 - cos_sim\n",
|
| 1085 |
+
" return distances\n",
|
| 1086 |
+
"\n",
|
| 1087 |
+
" def push_anchors_vmf(self, embeddings, labels, num_classes=10, momentum=0.1):\n",
|
| 1088 |
+
" '''Push anchors using vMF-weighted centroids.'''\n",
|
| 1089 |
+
" with torch.no_grad():\n",
|
| 1090 |
+
" for c in range(num_classes):\n",
|
| 1091 |
+
" mask = (labels == c)\n",
|
| 1092 |
+
" if mask.sum() < 2:\n",
|
| 1093 |
+
" continue\n",
|
| 1094 |
+
"\n",
|
| 1095 |
+
" emb_class = embeddings[mask]\n",
|
| 1096 |
+
" emb_norm = l2_normalize(emb_class)\n",
|
| 1097 |
+
"\n",
|
| 1098 |
+
" # vMF weights: higher for points closer to class mean\n",
|
| 1099 |
+
" mean_dir = l2_normalize(emb_norm.mean(dim=0, keepdim=True))\n",
|
| 1100 |
+
" sim_to_mean = torch.mm(emb_norm, mean_dir.T).squeeze()\n",
|
| 1101 |
+
" weights = torch.exp(self.kappa * sim_to_mean)\n",
|
| 1102 |
+
" weights = weights / (weights.sum() + 1e-8)\n",
|
| 1103 |
+
"\n",
|
| 1104 |
+
" # Weighted centroid\n",
|
| 1105 |
+
" centroid = (emb_norm * weights.unsqueeze(1)).sum(dim=0, keepdim=True)\n",
|
| 1106 |
+
" centroid = l2_normalize(centroid)\n",
|
| 1107 |
+
"\n",
|
| 1108 |
+
" start = c * (self.num_anchors // num_classes)\n",
|
| 1109 |
+
" end = start + (self.num_anchors // num_classes)\n",
|
| 1110 |
+
" self.anchors.data[start:end] = l2_normalize(\n",
|
| 1111 |
+
" (1 - momentum) * self.anchors.data[start:end] + momentum * centroid\n",
|
| 1112 |
+
" )\n",
|
| 1113 |
+
"\n",
|
| 1114 |
+
"class vMFAnchorPushModel(nn.Module):\n",
|
| 1115 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 1116 |
+
" super().__init__()\n",
|
| 1117 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 1118 |
+
" self.jl = JLProjection(81, 128)\n",
|
| 1119 |
+
" self.constellation = ConstellationWithvMFPush(embed_dim, num_anchors=64)\n",
|
| 1120 |
+
" self.patchwork = Patchwork(64, 2)\n",
|
| 1121 |
+
" self.head = nn.Linear(64, num_classes)\n",
|
| 1122 |
+
"\n",
|
| 1123 |
+
" def forward(self, x):\n",
|
| 1124 |
+
" scatt = self.scattering(x)\n",
|
| 1125 |
+
" reduced = F.adaptive_avg_pool2d(scatt.view(-1, 81, 32, 32), (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 1126 |
+
" jl_feat = self.jl(reduced)\n",
|
| 1127 |
+
" emb = l2_normalize(jl_feat)\n",
|
| 1128 |
+
" tri = self.constellation(emb)\n",
|
| 1129 |
+
" pw = self.patchwork(tri)\n",
|
| 1130 |
+
" logits = self.head(pw)\n",
|
| 1131 |
+
" return logits, emb\n",
|
| 1132 |
+
"\n",
|
| 1133 |
+
"model = vMFAnchorPushModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 1134 |
+
"print(f\"[6.3b] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 1135 |
+
"\n",
|
| 1136 |
+
"result_6_3b = run_experiment(\n",
|
| 1137 |
+
" \"6.3b_vMFAnchorPush\",\n",
|
| 1138 |
+
" model, train_loader, val_loader, device,\n",
|
| 1139 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 1140 |
+
" push_anchors_every=50,\n",
|
| 1141 |
+
" patience=20\n",
|
| 1142 |
+
")\n",
|
| 1143 |
+
"scoreboard.add(result_6_3b)\n",
|
| 1144 |
+
""
|
| 1145 |
+
],
|
| 1146 |
+
"execution_count": null,
|
| 1147 |
+
"outputs": []
|
| 1148 |
+
},
|
| 1149 |
+
{
|
| 1150 |
+
"cell_type": "markdown",
|
| 1151 |
+
"metadata": {},
|
| 1152 |
+
"source": [
|
| 1153 |
+
"## 6.3c: Anchor Birth/Death\n",
|
| 1154 |
+
"\n",
|
| 1155 |
+
"Add anchors where density high, remove where unused"
|
| 1156 |
+
]
|
| 1157 |
+
},
|
| 1158 |
+
{
|
| 1159 |
+
"cell_type": "code",
|
| 1160 |
+
"metadata": {},
|
| 1161 |
+
"source": [
|
| 1162 |
+
"\n",
|
| 1163 |
+
"print(\"[6.3c] Anchor Birth/Death\")\n",
|
| 1164 |
+
"print(\"=\"*70)\n",
|
| 1165 |
+
"\n",
|
| 1166 |
+
"class DynamicConstellation(nn.Module):\n",
|
| 1167 |
+
" '''Constellation with birth/death of anchors based on density.'''\n",
|
| 1168 |
+
" def __init__(self, embed_dim, num_anchors=64):\n",
|
| 1169 |
+
" super().__init__()\n",
|
| 1170 |
+
" self.embed_dim = embed_dim\n",
|
| 1171 |
+
" self.num_anchors = num_anchors\n",
|
| 1172 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, \"uniform\")\n",
|
| 1173 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False)\n",
|
| 1174 |
+
" self.anchor_activity = torch.zeros(num_anchors)\n",
|
| 1175 |
+
"\n",
|
| 1176 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 1177 |
+
" A = torch.randn(K, d)\n",
|
| 1178 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 1179 |
+
" anchors = Q.T[:K]\n",
|
| 1180 |
+
" if K > d:\n",
|
| 1181 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 1182 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 1183 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 1184 |
+
"\n",
|
| 1185 |
+
" def forward(self, x):\n",
|
| 1186 |
+
" x_norm = l2_normalize(x)\n",
|
| 1187 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 1188 |
+
" distances = 1.0 - cos_sim\n",
|
| 1189 |
+
"\n",
|
| 1190 |
+
" # Track activity\n",
|
| 1191 |
+
" nearest = cos_sim.argmax(dim=1)\n",
|
| 1192 |
+
" for anc_idx in range(self.num_anchors):\n",
|
| 1193 |
+
" self.anchor_activity[anc_idx] = (nearest == anc_idx).sum().float()\n",
|
| 1194 |
+
"\n",
|
| 1195 |
+
" return distances\n",
|
| 1196 |
+
"\n",
|
| 1197 |
+
" def birth_death(self, embeddings, labels, num_classes=10, density_threshold=0.1):\n",
|
| 1198 |
+
" '''Add anchors in high-density regions, remove unused ones.'''\n",
|
| 1199 |
+
" with torch.no_grad():\n",
|
| 1200 |
+
" emb_norm = l2_normalize(embeddings)\n",
|
| 1201 |
+
"\n",
|
| 1202 |
+
" # Find high-density regions\n",
|
| 1203 |
+
" cos_sim = torch.mm(emb_norm, emb_norm.T)\n",
|
| 1204 |
+
" density = cos_sim.mean(dim=1)\n",
|
| 1205 |
+
" high_density_idx = torch.where(density > density_threshold)[0]\n",
|
| 1206 |
+
"\n",
|
| 1207 |
+
" # Find unused anchors\n",
|
| 1208 |
+
" unused_idx = torch.where(self.anchor_activity < 1)[0]\n",
|
| 1209 |
+
"\n",
|
| 1210 |
+
" # Birth: add new anchors at high-density centroids\n",
|
| 1211 |
+
" if len(high_density_idx) > 0 and len(unused_idx) > 0:\n",
|
| 1212 |
+
" new_anchor = l2_normalize(emb_norm[high_density_idx].mean(dim=0, keepdim=True))\n",
|
| 1213 |
+
" self.anchors.data[unused_idx[0]] = new_anchor[0]\n",
|
| 1214 |
+
"\n",
|
| 1215 |
+
" # Death: replace unused anchors\n",
|
| 1216 |
+
" if len(unused_idx) > 0:\n",
|
| 1217 |
+
" for anc_idx in unused_idx[:len(unused_idx)//2]:\n",
|
| 1218 |
+
" random_idx = torch.randint(0, len(embeddings), (1,)).item()\n",
|
| 1219 |
+
" self.anchors.data[anc_idx] = l2_normalize(embeddings[random_idx:random_idx+1])\n",
|
| 1220 |
+
"\n",
|
| 1221 |
+
"class BirthDeathModel(nn.Module):\n",
|
| 1222 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 1223 |
+
" super().__init__()\n",
|
| 1224 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 1225 |
+
" self.jl = JLProjection(81, 128)\n",
|
| 1226 |
+
" self.constellation = DynamicConstellation(embed_dim, num_anchors=64)\n",
|
| 1227 |
+
" self.patchwork = Patchwork(64, 2)\n",
|
| 1228 |
+
" self.head = nn.Linear(64, num_classes)\n",
|
| 1229 |
+
"\n",
|
| 1230 |
+
" def forward(self, x):\n",
|
| 1231 |
+
" scatt = self.scattering(x)\n",
|
| 1232 |
+
" reduced = F.adaptive_avg_pool2d(scatt.view(-1, 81, 32, 32), (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 1233 |
+
" jl_feat = self.jl(reduced)\n",
|
| 1234 |
+
" emb = l2_normalize(jl_feat)\n",
|
| 1235 |
+
" tri = self.constellation(emb)\n",
|
| 1236 |
+
" pw = self.patchwork(tri)\n",
|
| 1237 |
+
" logits = self.head(pw)\n",
|
| 1238 |
+
" return logits, emb\n",
|
| 1239 |
+
"\n",
|
| 1240 |
+
"model = BirthDeathModel(embed_dim=128, num_classes=10).to(device)\n",
|
| 1241 |
+
"print(f\"[6.3c] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 1242 |
+
"\n",
|
| 1243 |
+
"result_6_3c = run_experiment(\n",
|
| 1244 |
+
" \"6.3c_AnchorBirthDeath\",\n",
|
| 1245 |
+
" model, train_loader, val_loader, device,\n",
|
| 1246 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 1247 |
+
" push_anchors_every=25,\n",
|
| 1248 |
+
" patience=20\n",
|
| 1249 |
+
")\n",
|
| 1250 |
+
"scoreboard.add(result_6_3c)\n",
|
| 1251 |
+
""
|
| 1252 |
+
],
|
| 1253 |
+
"execution_count": null,
|
| 1254 |
+
"outputs": []
|
| 1255 |
+
},
|
| 1256 |
+
{
|
| 1257 |
+
"cell_type": "markdown",
|
| 1258 |
+
"metadata": {},
|
| 1259 |
+
"source": [
|
| 1260 |
+
"## 6.3d: Anchor Dropout Sweep\n",
|
| 1261 |
+
"\n",
|
| 1262 |
+
"0%, 5%, 15%, 30%, 50%"
|
| 1263 |
+
]
|
| 1264 |
+
},
|
| 1265 |
+
{
|
| 1266 |
+
"cell_type": "code",
|
| 1267 |
+
"metadata": {},
|
| 1268 |
+
"source": [
|
| 1269 |
+
"\n",
|
| 1270 |
+
"print(\"[6.3d] Anchor Dropout Sweep\")\n",
|
| 1271 |
+
"print(\"=\"*70)\n",
|
| 1272 |
+
"\n",
|
| 1273 |
+
"class DropoutConstellation(nn.Module):\n",
|
| 1274 |
+
" def __init__(self, embed_dim, num_anchors=64, dropout_rate=0.0):\n",
|
| 1275 |
+
" super().__init__()\n",
|
| 1276 |
+
" self.embed_dim = embed_dim\n",
|
| 1277 |
+
" self.num_anchors = num_anchors\n",
|
| 1278 |
+
" self.dropout_rate = dropout_rate\n",
|
| 1279 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, \"uniform\")\n",
|
| 1280 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False)\n",
|
| 1281 |
+
"\n",
|
| 1282 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 1283 |
+
" A = torch.randn(K, d)\n",
|
| 1284 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 1285 |
+
" anchors = Q.T[:K]\n",
|
| 1286 |
+
" if K > d:\n",
|
| 1287 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 1288 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 1289 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 1290 |
+
"\n",
|
| 1291 |
+
" def forward(self, x):\n",
|
| 1292 |
+
" x_norm = l2_normalize(x)\n",
|
| 1293 |
+
"\n",
|
| 1294 |
+
" # Apply dropout to anchors\n",
|
| 1295 |
+
" if self.training and self.dropout_rate > 0:\n",
|
| 1296 |
+
" mask = torch.bernoulli(torch.ones(self.num_anchors, device=x.device) * (1 - self.dropout_rate))\n",
|
| 1297 |
+
" anch_active = self.anchors * mask.unsqueeze(1)\n",
|
| 1298 |
+
" anch_active = F.normalize(anch_active, dim=1)\n",
|
| 1299 |
+
" else:\n",
|
| 1300 |
+
" anch_active = self.anchors\n",
|
| 1301 |
+
"\n",
|
| 1302 |
+
" cos_sim = torch.mm(x_norm, anch_active.T)\n",
|
| 1303 |
+
" distances = 1.0 - cos_sim\n",
|
| 1304 |
+
" return distances\n",
|
| 1305 |
+
"\n",
|
| 1306 |
+
"class DropoutModel(nn.Module):\n",
|
| 1307 |
+
" def __init__(self, embed_dim=128, num_classes=10, dropout_rate=0.0):\n",
|
| 1308 |
+
" super().__init__()\n",
|
| 1309 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 1310 |
+
" self.jl = JLProjection(81, 128)\n",
|
| 1311 |
+
" self.constellation = DropoutConstellation(embed_dim, num_anchors=64, dropout_rate=dropout_rate)\n",
|
| 1312 |
+
" self.patchwork = Patchwork(64, 2)\n",
|
| 1313 |
+
" self.head = nn.Linear(64, num_classes)\n",
|
| 1314 |
+
"\n",
|
| 1315 |
+
" def forward(self, x):\n",
|
| 1316 |
+
" scatt = self.scattering(x)\n",
|
| 1317 |
+
" reduced = F.adaptive_avg_pool2d(scatt.view(-1, 81, 32, 32), (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 1318 |
+
" jl_feat = self.jl(reduced)\n",
|
| 1319 |
+
" emb = l2_normalize(jl_feat)\n",
|
| 1320 |
+
" tri = self.constellation(emb)\n",
|
| 1321 |
+
" pw = self.patchwork(tri)\n",
|
| 1322 |
+
" logits = self.head(pw)\n",
|
| 1323 |
+
" return logits, emb\n",
|
| 1324 |
+
"\n",
|
| 1325 |
+
"for dropout_rate in [0.0, 0.05, 0.15, 0.30, 0.50]:\n",
|
| 1326 |
+
" model = DropoutModel(embed_dim=128, num_classes=10, dropout_rate=dropout_rate).to(device)\n",
|
| 1327 |
+
" result = run_experiment(\n",
|
| 1328 |
+
" f\"6.3d_AnchorDropout_{int(dropout_rate*100)}pct\",\n",
|
| 1329 |
+
" model, train_loader, val_loader, device,\n",
|
| 1330 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4,\n",
|
| 1331 |
+
" push_anchors_every=50,\n",
|
| 1332 |
+
" patience=20\n",
|
| 1333 |
+
" )\n",
|
| 1334 |
+
" scoreboard.add(result)\n",
|
| 1335 |
+
" print(f\" Dropout rate: {dropout_rate*100:.0f}%\")\n",
|
| 1336 |
+
""
|
| 1337 |
+
],
|
| 1338 |
+
"execution_count": null,
|
| 1339 |
+
"outputs": []
|
| 1340 |
+
},
|
| 1341 |
+
{
|
| 1342 |
+
"cell_type": "markdown",
|
| 1343 |
+
"metadata": {},
|
| 1344 |
+
"source": [
|
| 1345 |
+
"## Final Scoreboard"
|
| 1346 |
+
]
|
| 1347 |
+
},
|
| 1348 |
+
{
|
| 1349 |
+
"cell_type": "code",
|
| 1350 |
+
"metadata": {},
|
| 1351 |
+
"source": [
|
| 1352 |
+
"\n",
|
| 1353 |
+
"print(\"\\n\" + \"=\"*80)\n",
|
| 1354 |
+
"print(\"EXPERIMENT 6 COMPLETE: Information-Theoretic Losses & Anchor Management\")\n",
|
| 1355 |
+
"print(\"=\"*80)\n",
|
| 1356 |
+
"scoreboard._print_table()\n",
|
| 1357 |
+
"scoreboard.save(\"scoreboard_exp6.json\")\n",
|
| 1358 |
+
""
|
| 1359 |
+
],
|
| 1360 |
+
"execution_count": null,
|
| 1361 |
+
"outputs": []
|
| 1362 |
+
}
|
| 1363 |
+
]
|
| 1364 |
+
}
|
spectral/experiment_7_composite_pipelines.ipynb
ADDED
|
@@ -0,0 +1,1381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"nbformat": 4,
|
| 3 |
+
"nbformat_minor": 5,
|
| 4 |
+
"metadata": {
|
| 5 |
+
"kernelspec": {
|
| 6 |
+
"display_name": "Python 3",
|
| 7 |
+
"language": "python",
|
| 8 |
+
"name": "python3"
|
| 9 |
+
},
|
| 10 |
+
"language_info": {
|
| 11 |
+
"name": "python",
|
| 12 |
+
"version": "3.10.0"
|
| 13 |
+
},
|
| 14 |
+
"accelerator": "GPU",
|
| 15 |
+
"colab": {
|
| 16 |
+
"provenance": [],
|
| 17 |
+
"gpuType": "T4"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"cells": [
|
| 21 |
+
{
|
| 22 |
+
"cell_type": "markdown",
|
| 23 |
+
"metadata": {},
|
| 24 |
+
"source": [
|
| 25 |
+
"# Experiment 7: Composite Pipeline Tests\n",
|
| 26 |
+
"\n",
|
| 27 |
+
"Reference pipeline, multi-signature fusion, hierarchical constellation."
|
| 28 |
+
]
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"cell_type": "code",
|
| 32 |
+
"metadata": {},
|
| 33 |
+
"source": [
|
| 34 |
+
"\n",
|
| 35 |
+
"!pip install -q kymatio scipy tensorboard scikit-learn matplotlib numpy torch torchvision torchaudio\n",
|
| 36 |
+
""
|
| 37 |
+
],
|
| 38 |
+
"execution_count": null,
|
| 39 |
+
"outputs": []
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"cell_type": "markdown",
|
| 43 |
+
"metadata": {},
|
| 44 |
+
"source": [
|
| 45 |
+
"## Shared Utilities"
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"cell_type": "code",
|
| 50 |
+
"metadata": {},
|
| 51 |
+
"source": [
|
| 52 |
+
"\n",
|
| 53 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 54 |
+
"# SHARED UTILITIES \u2014 GeoLIP Hypersphere Experiments\n",
|
| 55 |
+
"# \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n",
|
| 56 |
+
"import os, sys, time, math, json, datetime, warnings\n",
|
| 57 |
+
"import numpy as np\n",
|
| 58 |
+
"import torch\n",
|
| 59 |
+
"import torch.nn as nn\n",
|
| 60 |
+
"import torch.nn.functional as F\n",
|
| 61 |
+
"from torch.utils.data import DataLoader\n",
|
| 62 |
+
"from torch.utils.tensorboard import SummaryWriter\n",
|
| 63 |
+
"import torchvision\n",
|
| 64 |
+
"import torchvision.transforms as T\n",
|
| 65 |
+
"import matplotlib\n",
|
| 66 |
+
"matplotlib.use(\"Agg\")\n",
|
| 67 |
+
"import matplotlib.pyplot as plt\n",
|
| 68 |
+
"from collections import defaultdict\n",
|
| 69 |
+
"\n",
|
| 70 |
+
"warnings.filterwarnings(\"ignore\", category=UserWarning)\n",
|
| 71 |
+
"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
|
| 72 |
+
"print(f\"[DEVICE] {device}\" + (f\" \u2014 {torch.cuda.get_device_name()}\" if device.type == \"cuda\" else \"\"))\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"# \u2500\u2500 CIFAR-10 Loaders \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 75 |
+
"CIFAR10_MEAN = (0.4914, 0.4822, 0.4465)\n",
|
| 76 |
+
"CIFAR10_STD = (0.2470, 0.2435, 0.2616)\n",
|
| 77 |
+
"CIFAR10_CLASSES = (\"plane\",\"car\",\"bird\",\"cat\",\"deer\",\"dog\",\"frog\",\"horse\",\"ship\",\"truck\")\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"def get_cifar10_loaders(batch_size=256, num_workers=2, augment=True):\n",
|
| 80 |
+
" \"\"\"Return train_loader, val_loader for CIFAR-10.\"\"\"\n",
|
| 81 |
+
" tf_train = T.Compose([\n",
|
| 82 |
+
" T.RandomCrop(32, padding=4),\n",
|
| 83 |
+
" T.RandomHorizontalFlip(),\n",
|
| 84 |
+
" T.ToTensor(),\n",
|
| 85 |
+
" T.Normalize(CIFAR10_MEAN, CIFAR10_STD),\n",
|
| 86 |
+
" ]) if augment else T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 87 |
+
" tf_val = T.Compose([T.ToTensor(), T.Normalize(CIFAR10_MEAN, CIFAR10_STD)])\n",
|
| 88 |
+
"\n",
|
| 89 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf_train)\n",
|
| 90 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf_val)\n",
|
| 91 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 92 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 93 |
+
" print(f\"[DATA] CIFAR-10: {len(train_ds)} train, {len(val_ds)} val, batch_size={batch_size}\")\n",
|
| 94 |
+
" return train_loader, val_loader\n",
|
| 95 |
+
"\n",
|
| 96 |
+
"def get_cifar10_raw_loaders(batch_size=256, num_workers=2):\n",
|
| 97 |
+
" \"\"\"Return loaders with only ToTensor (no normalization) for custom front-ends.\"\"\"\n",
|
| 98 |
+
" tf = T.Compose([T.ToTensor()])\n",
|
| 99 |
+
" train_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=True, download=True, transform=tf)\n",
|
| 100 |
+
" val_ds = torchvision.datasets.CIFAR10(root=\"./data\", train=False, download=True, transform=tf)\n",
|
| 101 |
+
" train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True, drop_last=True)\n",
|
| 102 |
+
" val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True)\n",
|
| 103 |
+
" print(f\"[DATA] CIFAR-10 (raw): {len(train_ds)} train, {len(val_ds)} val\")\n",
|
| 104 |
+
" return train_loader, val_loader\n",
|
| 105 |
+
"\n",
|
| 106 |
+
"# \u2500\u2500 L2 Normalization to Hypersphere \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 107 |
+
"def l2_normalize(x, dim=-1, eps=1e-8):\n",
|
| 108 |
+
" \"\"\"Project to unit hypersphere S^{d-1}.\"\"\"\n",
|
| 109 |
+
" return F.normalize(x, p=2, dim=dim, eps=eps)\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"# \u2500\u2500 Constellation Module \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 112 |
+
"class Constellation(nn.Module):\n",
|
| 113 |
+
" \"\"\"\n",
|
| 114 |
+
" K anchors on S^{d-1}. Triangulation: compute distance from input to each anchor.\n",
|
| 115 |
+
" Output: K-dim vector of (1 - cos_sim) distances = triangulation profile.\n",
|
| 116 |
+
" \"\"\"\n",
|
| 117 |
+
" def __init__(self, embed_dim, num_anchors=64, init=\"uniform\"):\n",
|
| 118 |
+
" super().__init__()\n",
|
| 119 |
+
" self.embed_dim = embed_dim\n",
|
| 120 |
+
" self.num_anchors = num_anchors\n",
|
| 121 |
+
" anchors = self._init_anchors(embed_dim, num_anchors, init)\n",
|
| 122 |
+
" self.anchors = nn.Parameter(anchors, requires_grad=False) # anchors are NOT learned\n",
|
| 123 |
+
"\n",
|
| 124 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 125 |
+
" if method == \"uniform\":\n",
|
| 126 |
+
" # QR-based uniform hypersphere initialization\n",
|
| 127 |
+
" A = torch.randn(K, d)\n",
|
| 128 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 129 |
+
" anchors = Q.T[:K]\n",
|
| 130 |
+
" # If K > d, fill remaining with random unit vectors\n",
|
| 131 |
+
" if K > d:\n",
|
| 132 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 133 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 134 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 135 |
+
" elif method == \"random\":\n",
|
| 136 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 137 |
+
" else:\n",
|
| 138 |
+
" return F.normalize(torch.randn(K, d), dim=-1)\n",
|
| 139 |
+
"\n",
|
| 140 |
+
" def forward(self, x):\n",
|
| 141 |
+
" \"\"\"x: (B, d) on S^{d-1} \u2192 (B, K) triangulation distances.\"\"\"\n",
|
| 142 |
+
" x_norm = l2_normalize(x)\n",
|
| 143 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T) # (B, K)\n",
|
| 144 |
+
" distances = 1.0 - cos_sim # in [0, 2]\n",
|
| 145 |
+
" return distances\n",
|
| 146 |
+
"\n",
|
| 147 |
+
" def push_anchors(self, embeddings, labels, num_classes=10, momentum=0.1):\n",
|
| 148 |
+
" \"\"\"Push anchors toward class centroids (call periodically).\"\"\"\n",
|
| 149 |
+
" with torch.no_grad():\n",
|
| 150 |
+
" for c in range(num_classes):\n",
|
| 151 |
+
" mask = (labels == c)\n",
|
| 152 |
+
" if mask.sum() < 2:\n",
|
| 153 |
+
" continue\n",
|
| 154 |
+
" centroid = l2_normalize(embeddings[mask].mean(dim=0, keepdim=True))\n",
|
| 155 |
+
" # Assign anchors to this class based on nearest centroid\n",
|
| 156 |
+
" start = c * (self.num_anchors // num_classes)\n",
|
| 157 |
+
" end = start + (self.num_anchors // num_classes)\n",
|
| 158 |
+
" self.anchors.data[start:end] = l2_normalize(\n",
|
| 159 |
+
" (1 - momentum) * self.anchors.data[start:end] + momentum * centroid\n",
|
| 160 |
+
" )\n",
|
| 161 |
+
"\n",
|
| 162 |
+
" def anchor_activation(self, x):\n",
|
| 163 |
+
" \"\"\"Return fraction of anchors that are 'active' (nearest to at least one sample).\"\"\"\n",
|
| 164 |
+
" x_norm = l2_normalize(x)\n",
|
| 165 |
+
" cos_sim = torch.mm(x_norm, self.anchors.T)\n",
|
| 166 |
+
" nearest = cos_sim.argmax(dim=1)\n",
|
| 167 |
+
" active = len(nearest.unique())\n",
|
| 168 |
+
" return active, self.num_anchors\n",
|
| 169 |
+
"\n",
|
| 170 |
+
"# \u2500\u2500 Patchwork Block \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 171 |
+
"class SquaredReLU(nn.Module):\n",
|
| 172 |
+
" def forward(self, x):\n",
|
| 173 |
+
" return F.relu(x) ** 2\n",
|
| 174 |
+
"\n",
|
| 175 |
+
"class PatchworkBlock(nn.Module):\n",
|
| 176 |
+
" \"\"\"SquaredReLU + LayerNorm block for post-constellation processing.\"\"\"\n",
|
| 177 |
+
" def __init__(self, dim):\n",
|
| 178 |
+
" super().__init__()\n",
|
| 179 |
+
" self.net = nn.Sequential(\n",
|
| 180 |
+
" nn.Linear(dim, dim),\n",
|
| 181 |
+
" SquaredReLU(),\n",
|
| 182 |
+
" nn.LayerNorm(dim),\n",
|
| 183 |
+
" nn.Linear(dim, dim),\n",
|
| 184 |
+
" SquaredReLU(),\n",
|
| 185 |
+
" nn.LayerNorm(dim),\n",
|
| 186 |
+
" )\n",
|
| 187 |
+
" def forward(self, x):\n",
|
| 188 |
+
" return x + self.net(x) # residual\n",
|
| 189 |
+
"\n",
|
| 190 |
+
"class Patchwork(nn.Module):\n",
|
| 191 |
+
" \"\"\"Stack of PatchworkBlocks.\"\"\"\n",
|
| 192 |
+
" def __init__(self, dim, num_blocks=2):\n",
|
| 193 |
+
" super().__init__()\n",
|
| 194 |
+
" self.blocks = nn.Sequential(*[PatchworkBlock(dim) for _ in range(num_blocks)])\n",
|
| 195 |
+
" def forward(self, x):\n",
|
| 196 |
+
" return self.blocks(x)\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"# \u2500\u2500 Classifier Head \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 199 |
+
"class ConstellationClassifier(nn.Module):\n",
|
| 200 |
+
" \"\"\"Full pipeline: encoder output \u2192 constellation \u2192 patchwork \u2192 classify.\"\"\"\n",
|
| 201 |
+
" def __init__(self, embed_dim, num_anchors=64, num_classes=10, patchwork_blocks=2):\n",
|
| 202 |
+
" super().__init__()\n",
|
| 203 |
+
" self.constellation = Constellation(embed_dim, num_anchors)\n",
|
| 204 |
+
" self.patchwork = Patchwork(num_anchors, patchwork_blocks)\n",
|
| 205 |
+
" self.head = nn.Linear(num_anchors, num_classes)\n",
|
| 206 |
+
"\n",
|
| 207 |
+
" def forward(self, embeddings):\n",
|
| 208 |
+
" tri = self.constellation(embeddings) # (B, K)\n",
|
| 209 |
+
" pw = self.patchwork(tri) # (B, K)\n",
|
| 210 |
+
" logits = self.head(pw) # (B, 10)\n",
|
| 211 |
+
" return logits, tri\n",
|
| 212 |
+
"\n",
|
| 213 |
+
" def count_params(self):\n",
|
| 214 |
+
" return sum(p.numel() for p in self.parameters() if p.requires_grad)\n",
|
| 215 |
+
"\n",
|
| 216 |
+
"# \u2500\u2500 Losses \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 217 |
+
"class CayleyMengerVolumeLoss(nn.Module):\n",
|
| 218 |
+
" \"\"\"CV loss: encourage constellation volume in 0.18-0.25 band.\"\"\"\n",
|
| 219 |
+
" def __init__(self, target_low=0.18, target_high=0.25):\n",
|
| 220 |
+
" super().__init__()\n",
|
| 221 |
+
" self.target_low = target_low\n",
|
| 222 |
+
" self.target_high = target_high\n",
|
| 223 |
+
"\n",
|
| 224 |
+
" def forward(self, tri):\n",
|
| 225 |
+
" \"\"\"tri: (B, K) triangulation distances.\"\"\"\n",
|
| 226 |
+
" # Approximate volume via determinant of distance submatrix\n",
|
| 227 |
+
" B, K = tri.shape\n",
|
| 228 |
+
" # Use std of triangulation as proxy for volume spread\n",
|
| 229 |
+
" vol = tri.std(dim=1).mean()\n",
|
| 230 |
+
" # Penalty if outside target band\n",
|
| 231 |
+
" below = F.relu(self.target_low - vol)\n",
|
| 232 |
+
" above = F.relu(vol - self.target_high)\n",
|
| 233 |
+
" return below + above, vol.item()\n",
|
| 234 |
+
"\n",
|
| 235 |
+
"class InfoNCELoss(nn.Module):\n",
|
| 236 |
+
" \"\"\"Self-supervised contrastive loss on augmented views.\"\"\"\n",
|
| 237 |
+
" def __init__(self, temperature=0.07):\n",
|
| 238 |
+
" super().__init__()\n",
|
| 239 |
+
" self.temperature = temperature\n",
|
| 240 |
+
"\n",
|
| 241 |
+
" def forward(self, z1, z2):\n",
|
| 242 |
+
" \"\"\"z1, z2: (B, d) L2-normalized embeddings of two augmented views.\"\"\"\n",
|
| 243 |
+
" z1 = l2_normalize(z1)\n",
|
| 244 |
+
" z2 = l2_normalize(z2)\n",
|
| 245 |
+
" B = z1.shape[0]\n",
|
| 246 |
+
" z = torch.cat([z1, z2], dim=0) # (2B, d)\n",
|
| 247 |
+
" sim = torch.mm(z, z.T) / self.temperature # (2B, 2B)\n",
|
| 248 |
+
" # Mask out self-similarity\n",
|
| 249 |
+
" mask = ~torch.eye(2*B, dtype=bool, device=z.device)\n",
|
| 250 |
+
" sim = sim.masked_fill(~mask, -1e9)\n",
|
| 251 |
+
" # Positive pairs: (i, i+B) and (i+B, i)\n",
|
| 252 |
+
" labels = torch.cat([torch.arange(B, 2*B), torch.arange(B)]).to(z.device)\n",
|
| 253 |
+
" loss = F.cross_entropy(sim, labels)\n",
|
| 254 |
+
" return loss\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"class ClassConditionalInfoNCE(nn.Module):\n",
|
| 257 |
+
" \"\"\"Class-conditional contrastive: positive = same class, not same image.\"\"\"\n",
|
| 258 |
+
" def __init__(self, temperature=0.07):\n",
|
| 259 |
+
" super().__init__()\n",
|
| 260 |
+
" self.temperature = temperature\n",
|
| 261 |
+
"\n",
|
| 262 |
+
" def forward(self, z, labels):\n",
|
| 263 |
+
" z = l2_normalize(z)\n",
|
| 264 |
+
" B = z.shape[0]\n",
|
| 265 |
+
" sim = torch.mm(z, z.T) / self.temperature\n",
|
| 266 |
+
" # Mask: positive if same class, negative otherwise\n",
|
| 267 |
+
" label_mask = (labels.unsqueeze(0) == labels.unsqueeze(1)).float()\n",
|
| 268 |
+
" label_mask.fill_diagonal_(0) # exclude self\n",
|
| 269 |
+
" # For each sample, compute log-softmax over all non-self, weighted by positives\n",
|
| 270 |
+
" logits = sim - torch.diag(torch.full((B,), 1e9, device=z.device))\n",
|
| 271 |
+
" pos_mask = label_mask\n",
|
| 272 |
+
" neg_mask = 1 - label_mask\n",
|
| 273 |
+
" neg_mask.fill_diagonal_(0)\n",
|
| 274 |
+
" # InfoNCE: -log(sum_pos exp(sim) / sum_all exp(sim))\n",
|
| 275 |
+
" exp_sim = torch.exp(logits)\n",
|
| 276 |
+
" pos_sum = (exp_sim * pos_mask).sum(dim=1)\n",
|
| 277 |
+
" all_sum = (exp_sim * (pos_mask + neg_mask)).sum(dim=1)\n",
|
| 278 |
+
" loss = -torch.log(pos_sum / (all_sum + 1e-8) + 1e-8).mean()\n",
|
| 279 |
+
" return loss\n",
|
| 280 |
+
"\n",
|
| 281 |
+
"# \u2500\u2500 TensorBoard Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 282 |
+
"def make_writer(experiment_id, base_dir=\"runs\"):\n",
|
| 283 |
+
" \"\"\"Create a TensorBoard SummaryWriter with timestamped run directory.\"\"\"\n",
|
| 284 |
+
" timestamp = datetime.datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
|
| 285 |
+
" log_dir = os.path.join(base_dir, f\"{experiment_id}_{timestamp}\")\n",
|
| 286 |
+
" writer = SummaryWriter(log_dir=log_dir)\n",
|
| 287 |
+
" print(f\"[TB] Logging to {log_dir}\")\n",
|
| 288 |
+
" return writer\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"def log_epoch_metrics(writer, epoch, metrics_dict, prefix=\"\"):\n",
|
| 291 |
+
" \"\"\"Log a dict of metrics to TensorBoard.\"\"\"\n",
|
| 292 |
+
" for key, val in metrics_dict.items():\n",
|
| 293 |
+
" tag = f\"{prefix}/{key}\" if prefix else key\n",
|
| 294 |
+
" writer.add_scalar(tag, val, epoch)\n",
|
| 295 |
+
"\n",
|
| 296 |
+
"def log_embedding_viz(writer, embeddings, labels, epoch, tag=\"embeddings\"):\n",
|
| 297 |
+
" \"\"\"Log embeddings for TensorBoard projector.\"\"\"\n",
|
| 298 |
+
" meta = [CIFAR10_CLASSES[l] for l in labels]\n",
|
| 299 |
+
" writer.add_embedding(embeddings, metadata=meta, global_step=epoch, tag=tag)\n",
|
| 300 |
+
"\n",
|
| 301 |
+
"def log_anchor_histogram(writer, constellation, epoch):\n",
|
| 302 |
+
" \"\"\"Log anchor norm distribution.\"\"\"\n",
|
| 303 |
+
" norms = constellation.anchors.data.norm(dim=1)\n",
|
| 304 |
+
" writer.add_histogram(\"anchors/norms\", norms, epoch)\n",
|
| 305 |
+
" cos_sim_matrix = torch.mm(constellation.anchors.data, constellation.anchors.data.T)\n",
|
| 306 |
+
" off_diag = cos_sim_matrix[~torch.eye(cos_sim_matrix.shape[0], dtype=bool)].flatten()\n",
|
| 307 |
+
" writer.add_histogram(\"anchors/inter_cosine_sim\", off_diag, epoch)\n",
|
| 308 |
+
"\n",
|
| 309 |
+
"def log_confusion_matrix(writer, all_preds, all_labels, epoch):\n",
|
| 310 |
+
" \"\"\"Log confusion matrix as image to TensorBoard.\"\"\"\n",
|
| 311 |
+
" from sklearn.metrics import confusion_matrix\n",
|
| 312 |
+
" cm = confusion_matrix(all_labels, all_preds, labels=range(10))\n",
|
| 313 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 314 |
+
" im = ax.imshow(cm, interpolation=\"nearest\", cmap=plt.cm.Blues)\n",
|
| 315 |
+
" ax.set_title(f\"Confusion Matrix \u2014 Epoch {epoch}\")\n",
|
| 316 |
+
" ax.set_xlabel(\"Predicted\")\n",
|
| 317 |
+
" ax.set_ylabel(\"True\")\n",
|
| 318 |
+
" tick_marks = range(10)\n",
|
| 319 |
+
" ax.set_xticks(tick_marks)\n",
|
| 320 |
+
" ax.set_xticklabels(CIFAR10_CLASSES, rotation=45, ha=\"right\")\n",
|
| 321 |
+
" ax.set_yticks(tick_marks)\n",
|
| 322 |
+
" ax.set_yticklabels(CIFAR10_CLASSES)\n",
|
| 323 |
+
" for i in range(10):\n",
|
| 324 |
+
" for j in range(10):\n",
|
| 325 |
+
" ax.text(j, i, str(cm[i, j]), ha=\"center\", va=\"center\",\n",
|
| 326 |
+
" color=\"white\" if cm[i, j] > cm.max() / 2 else \"black\", fontsize=7)\n",
|
| 327 |
+
" fig.tight_layout()\n",
|
| 328 |
+
" writer.add_figure(\"confusion_matrix\", fig, epoch)\n",
|
| 329 |
+
" plt.close(fig)\n",
|
| 330 |
+
"\n",
|
| 331 |
+
"def log_triangulation_stats(writer, tri, epoch):\n",
|
| 332 |
+
" \"\"\"Log triangulation distance statistics.\"\"\"\n",
|
| 333 |
+
" writer.add_scalar(\"tri/mean\", tri.mean().item(), epoch)\n",
|
| 334 |
+
" writer.add_scalar(\"tri/std\", tri.std().item(), epoch)\n",
|
| 335 |
+
" writer.add_scalar(\"tri/min\", tri.min().item(), epoch)\n",
|
| 336 |
+
" writer.add_scalar(\"tri/max\", tri.max().item(), epoch)\n",
|
| 337 |
+
" writer.add_histogram(\"tri/distances\", tri.detach().cpu(), epoch)\n",
|
| 338 |
+
"\n",
|
| 339 |
+
"def log_scatter_2d(writer, embeddings, labels, epoch, tag=\"scatter_2d\", method=\"pca\"):\n",
|
| 340 |
+
" \"\"\"Log 2D scatter plot of embeddings projected via PCA or t-SNE.\"\"\"\n",
|
| 341 |
+
" emb_np = embeddings.detach().cpu().numpy()\n",
|
| 342 |
+
" lab_np = labels.detach().cpu().numpy() if torch.is_tensor(labels) else np.array(labels)\n",
|
| 343 |
+
" if method == \"pca\":\n",
|
| 344 |
+
" from sklearn.decomposition import PCA\n",
|
| 345 |
+
" proj = PCA(n_components=2).fit_transform(emb_np)\n",
|
| 346 |
+
" else:\n",
|
| 347 |
+
" from sklearn.manifold import TSNE\n",
|
| 348 |
+
" proj = TSNE(n_components=2, perplexity=30, n_iter=500).fit_transform(emb_np)\n",
|
| 349 |
+
" fig, ax = plt.subplots(1, 1, figsize=(8, 8))\n",
|
| 350 |
+
" for c in range(10):\n",
|
| 351 |
+
" mask = lab_np == c\n",
|
| 352 |
+
" ax.scatter(proj[mask, 0], proj[mask, 1], s=3, alpha=0.5, label=CIFAR10_CLASSES[c])\n",
|
| 353 |
+
" ax.legend(markerscale=5, fontsize=8)\n",
|
| 354 |
+
" ax.set_title(f\"{tag} ({method.upper()}) \u2014 Epoch {epoch}\")\n",
|
| 355 |
+
" fig.tight_layout()\n",
|
| 356 |
+
" writer.add_figure(f\"{tag}/{method}\", fig, epoch)\n",
|
| 357 |
+
" plt.close(fig)\n",
|
| 358 |
+
"\n",
|
| 359 |
+
"# \u2500\u2500 Training Utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 360 |
+
"def train_one_epoch(model, loader, optimizer, criterion, device, epoch=0,\n",
|
| 361 |
+
" scheduler=None, extra_losses=None):\n",
|
| 362 |
+
" \"\"\"\n",
|
| 363 |
+
" Generic training loop for one epoch.\n",
|
| 364 |
+
" model: callable(batch) \u2192 (logits, embeddings_on_sphere)\n",
|
| 365 |
+
" extra_losses: list of (name, loss_fn, weight) where loss_fn(embeddings) \u2192 scalar\n",
|
| 366 |
+
" \"\"\"\n",
|
| 367 |
+
" model.train()\n",
|
| 368 |
+
" total_loss = 0\n",
|
| 369 |
+
" total_ce = 0\n",
|
| 370 |
+
" correct = 0\n",
|
| 371 |
+
" total = 0\n",
|
| 372 |
+
" loss_components = defaultdict(float)\n",
|
| 373 |
+
"\n",
|
| 374 |
+
" for batch_idx, (images, labels) in enumerate(loader):\n",
|
| 375 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 376 |
+
" optimizer.zero_grad()\n",
|
| 377 |
+
"\n",
|
| 378 |
+
" logits, embeddings = model(images)\n",
|
| 379 |
+
" ce_loss = criterion(logits, labels)\n",
|
| 380 |
+
" loss = ce_loss\n",
|
| 381 |
+
" loss_components[\"CE\"] += ce_loss.item()\n",
|
| 382 |
+
"\n",
|
| 383 |
+
" if extra_losses:\n",
|
| 384 |
+
" for name, loss_fn, weight in extra_losses:\n",
|
| 385 |
+
" if name == \"CV\":\n",
|
| 386 |
+
" el, vol_val = loss_fn(embeddings)\n",
|
| 387 |
+
" loss_components[\"CV_vol\"] += vol_val\n",
|
| 388 |
+
" elif name == \"InfoNCE_class\":\n",
|
| 389 |
+
" el = loss_fn(embeddings, labels)\n",
|
| 390 |
+
" else:\n",
|
| 391 |
+
" el = loss_fn(embeddings)\n",
|
| 392 |
+
" loss = loss + weight * el\n",
|
| 393 |
+
" loss_components[name] += el.item()\n",
|
| 394 |
+
"\n",
|
| 395 |
+
" loss.backward()\n",
|
| 396 |
+
" torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n",
|
| 397 |
+
" optimizer.step()\n",
|
| 398 |
+
" if scheduler:\n",
|
| 399 |
+
" scheduler.step()\n",
|
| 400 |
+
"\n",
|
| 401 |
+
" total_loss += loss.item()\n",
|
| 402 |
+
" _, predicted = logits.max(1)\n",
|
| 403 |
+
" total += labels.size(0)\n",
|
| 404 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 405 |
+
"\n",
|
| 406 |
+
" n_batches = len(loader)\n",
|
| 407 |
+
" metrics = {\n",
|
| 408 |
+
" \"loss/total\": total_loss / n_batches,\n",
|
| 409 |
+
" \"loss/CE\": loss_components[\"CE\"] / n_batches,\n",
|
| 410 |
+
" \"accuracy/train\": 100.0 * correct / total,\n",
|
| 411 |
+
" }\n",
|
| 412 |
+
" for k, v in loss_components.items():\n",
|
| 413 |
+
" if k not in (\"CE\",):\n",
|
| 414 |
+
" metrics[f\"loss/{k}\"] = v / n_batches\n",
|
| 415 |
+
" return metrics\n",
|
| 416 |
+
"\n",
|
| 417 |
+
"@torch.no_grad()\n",
|
| 418 |
+
"def evaluate(model, loader, criterion, device, collect_embeddings=False):\n",
|
| 419 |
+
" \"\"\"Evaluate model on validation set. Optionally collect embeddings.\"\"\"\n",
|
| 420 |
+
" model.eval()\n",
|
| 421 |
+
" total_loss = 0\n",
|
| 422 |
+
" correct = 0\n",
|
| 423 |
+
" total = 0\n",
|
| 424 |
+
" all_preds = []\n",
|
| 425 |
+
" all_labels = []\n",
|
| 426 |
+
" all_embeddings = []\n",
|
| 427 |
+
"\n",
|
| 428 |
+
" for images, labels in loader:\n",
|
| 429 |
+
" images, labels = images.to(device), labels.to(device)\n",
|
| 430 |
+
" logits, embeddings = model(images)\n",
|
| 431 |
+
" loss = criterion(logits, labels)\n",
|
| 432 |
+
" total_loss += loss.item()\n",
|
| 433 |
+
" _, predicted = logits.max(1)\n",
|
| 434 |
+
" total += labels.size(0)\n",
|
| 435 |
+
" correct += predicted.eq(labels).sum().item()\n",
|
| 436 |
+
" all_preds.extend(predicted.cpu().numpy())\n",
|
| 437 |
+
" all_labels.extend(labels.cpu().numpy())\n",
|
| 438 |
+
" if collect_embeddings:\n",
|
| 439 |
+
" all_embeddings.append(embeddings.cpu())\n",
|
| 440 |
+
"\n",
|
| 441 |
+
" n_batches = len(loader)\n",
|
| 442 |
+
" metrics = {\n",
|
| 443 |
+
" \"loss/val\": total_loss / n_batches,\n",
|
| 444 |
+
" \"accuracy/val\": 100.0 * correct / total,\n",
|
| 445 |
+
" }\n",
|
| 446 |
+
" result = {\"metrics\": metrics, \"preds\": np.array(all_preds), \"labels\": np.array(all_labels)}\n",
|
| 447 |
+
" if collect_embeddings:\n",
|
| 448 |
+
" result[\"embeddings\"] = torch.cat(all_embeddings, dim=0)\n",
|
| 449 |
+
" return result\n",
|
| 450 |
+
"\n",
|
| 451 |
+
"def run_experiment(experiment_id, model, train_loader, val_loader, device,\n",
|
| 452 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, extra_losses=None,\n",
|
| 453 |
+
" push_anchors_every=50, log_embeddings_every=25,\n",
|
| 454 |
+
" patience=20):\n",
|
| 455 |
+
" \"\"\"\n",
|
| 456 |
+
" Full experiment runner with TensorBoard logging, early stopping, and diagnostics.\n",
|
| 457 |
+
" model must return (logits, sphere_embeddings).\n",
|
| 458 |
+
" \"\"\"\n",
|
| 459 |
+
" writer = make_writer(experiment_id)\n",
|
| 460 |
+
" criterion = nn.CrossEntropyLoss()\n",
|
| 461 |
+
" optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=weight_decay)\n",
|
| 462 |
+
" scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)\n",
|
| 463 |
+
"\n",
|
| 464 |
+
" best_val_acc = 0.0\n",
|
| 465 |
+
" best_epoch = 0\n",
|
| 466 |
+
" no_improve = 0\n",
|
| 467 |
+
"\n",
|
| 468 |
+
" # Log model info\n",
|
| 469 |
+
" n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n",
|
| 470 |
+
" writer.add_text(\"config/params\", f\"Learned params: {n_params:,}\")\n",
|
| 471 |
+
" writer.add_text(\"config/experiment\", experiment_id)\n",
|
| 472 |
+
" print(f\"\\n{'='*70}\")\n",
|
| 473 |
+
" print(f\"[EXP] {experiment_id}\")\n",
|
| 474 |
+
" print(f\"[EXP] Learned params: {n_params:,}, Epochs: {epochs}, LR: {lr}\")\n",
|
| 475 |
+
" print(f\"{'='*70}\")\n",
|
| 476 |
+
"\n",
|
| 477 |
+
" for epoch in range(1, epochs + 1):\n",
|
| 478 |
+
" t0 = time.time()\n",
|
| 479 |
+
"\n",
|
| 480 |
+
" # Train\n",
|
| 481 |
+
" train_metrics = train_one_epoch(\n",
|
| 482 |
+
" model, train_loader, optimizer, criterion, device, epoch,\n",
|
| 483 |
+
" extra_losses=extra_losses\n",
|
| 484 |
+
" )\n",
|
| 485 |
+
"\n",
|
| 486 |
+
" # Evaluate\n",
|
| 487 |
+
" collect_emb = (epoch % log_embeddings_every == 0) or (epoch == epochs)\n",
|
| 488 |
+
" val_result = evaluate(model, val_loader, criterion, device, collect_embeddings=collect_emb)\n",
|
| 489 |
+
" val_metrics = val_result[\"metrics\"]\n",
|
| 490 |
+
"\n",
|
| 491 |
+
" # Combine metrics\n",
|
| 492 |
+
" all_metrics = {**train_metrics, **val_metrics}\n",
|
| 493 |
+
" all_metrics[\"lr\"] = optimizer.param_groups[0][\"lr\"]\n",
|
| 494 |
+
" all_metrics[\"epoch_time\"] = time.time() - t0\n",
|
| 495 |
+
" all_metrics[\"train_val_gap\"] = train_metrics[\"accuracy/train\"] - val_metrics[\"accuracy/val\"]\n",
|
| 496 |
+
"\n",
|
| 497 |
+
" # Constellation diagnostics\n",
|
| 498 |
+
" if hasattr(model, \"constellation\"):\n",
|
| 499 |
+
" constellation = model.constellation\n",
|
| 500 |
+
" elif hasattr(model, \"classifier\") and hasattr(model.classifier, \"constellation\"):\n",
|
| 501 |
+
" constellation = model.classifier.constellation\n",
|
| 502 |
+
" else:\n",
|
| 503 |
+
" constellation = None\n",
|
| 504 |
+
"\n",
|
| 505 |
+
" if constellation is not None:\n",
|
| 506 |
+
" log_anchor_histogram(writer, constellation, epoch)\n",
|
| 507 |
+
"\n",
|
| 508 |
+
" # Push anchors periodically\n",
|
| 509 |
+
" if epoch % push_anchors_every == 0 and \"embeddings\" in val_result:\n",
|
| 510 |
+
" emb = val_result[\"embeddings\"][:2000].to(device)\n",
|
| 511 |
+
" lab = torch.tensor(val_result[\"labels\"][:2000]).to(device)\n",
|
| 512 |
+
" constellation.push_anchors(emb, lab)\n",
|
| 513 |
+
" active, total_anch = constellation.anchor_activation(emb)\n",
|
| 514 |
+
" all_metrics[\"anchors/active\"] = active\n",
|
| 515 |
+
" all_metrics[\"anchors/total\"] = total_anch\n",
|
| 516 |
+
" all_metrics[\"anchors/utilization\"] = active / total_anch\n",
|
| 517 |
+
" print(f\" [ANCHOR PUSH] Active: {active}/{total_anch} ({100*active/total_anch:.1f}%)\")\n",
|
| 518 |
+
"\n",
|
| 519 |
+
" # Log all metrics\n",
|
| 520 |
+
" log_epoch_metrics(writer, epoch, all_metrics)\n",
|
| 521 |
+
" scheduler.step()\n",
|
| 522 |
+
"\n",
|
| 523 |
+
" # Log embeddings visualization\n",
|
| 524 |
+
" if collect_emb and \"embeddings\" in val_result:\n",
|
| 525 |
+
" emb = val_result[\"embeddings\"][:2000]\n",
|
| 526 |
+
" lab = val_result[\"labels\"][:2000]\n",
|
| 527 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings\", method=\"pca\")\n",
|
| 528 |
+
" if epoch == epochs:\n",
|
| 529 |
+
" log_confusion_matrix(writer, val_result[\"preds\"], val_result[\"labels\"], epoch)\n",
|
| 530 |
+
" log_scatter_2d(writer, emb, lab, epoch, tag=\"val_embeddings_tsne\", method=\"tsne\")\n",
|
| 531 |
+
"\n",
|
| 532 |
+
" # Log triangulation stats from a sample batch\n",
|
| 533 |
+
" if constellation is not None and collect_emb and \"embeddings\" in val_result:\n",
|
| 534 |
+
" sample_emb = val_result[\"embeddings\"][:500].to(device)\n",
|
| 535 |
+
" tri_sample = constellation(sample_emb)\n",
|
| 536 |
+
" log_triangulation_stats(writer, tri_sample, epoch)\n",
|
| 537 |
+
"\n",
|
| 538 |
+
" # Best model tracking\n",
|
| 539 |
+
" val_acc = val_metrics[\"accuracy/val\"]\n",
|
| 540 |
+
" if val_acc > best_val_acc:\n",
|
| 541 |
+
" best_val_acc = val_acc\n",
|
| 542 |
+
" best_epoch = epoch\n",
|
| 543 |
+
" no_improve = 0\n",
|
| 544 |
+
" writer.add_scalar(\"best/val_acc\", best_val_acc, epoch)\n",
|
| 545 |
+
" writer.add_scalar(\"best/epoch\", best_epoch, epoch)\n",
|
| 546 |
+
" else:\n",
|
| 547 |
+
" no_improve += 1\n",
|
| 548 |
+
"\n",
|
| 549 |
+
" # Print progress\n",
|
| 550 |
+
" if epoch % 5 == 0 or epoch == 1 or epoch == epochs:\n",
|
| 551 |
+
" print(f\" E{epoch:3d} | TrainAcc {train_metrics['accuracy/train']:5.1f}% | \"\n",
|
| 552 |
+
" f\"ValAcc {val_acc:5.1f}% | Loss {all_metrics['loss/total']:.4f} | \"\n",
|
| 553 |
+
" f\"Best {best_val_acc:.1f}% @E{best_epoch} | \"\n",
|
| 554 |
+
" f\"Gap {all_metrics['train_val_gap']:+.1f}% | \"\n",
|
| 555 |
+
" f\"{all_metrics['epoch_time']:.1f}s\")\n",
|
| 556 |
+
"\n",
|
| 557 |
+
" # Early stopping\n",
|
| 558 |
+
" if no_improve >= patience:\n",
|
| 559 |
+
" print(f\" [EARLY STOP] No improvement for {patience} epochs. Best: {best_val_acc:.1f}% @E{best_epoch}\")\n",
|
| 560 |
+
" break\n",
|
| 561 |
+
"\n",
|
| 562 |
+
" writer.close()\n",
|
| 563 |
+
" print(f\"\\n[RESULT] {experiment_id}: Best Val Acc = {best_val_acc:.2f}% at epoch {best_epoch}\")\n",
|
| 564 |
+
" print(f\"[RESULT] Learned params: {n_params:,}\")\n",
|
| 565 |
+
" return {\"experiment\": experiment_id, \"best_val_acc\": best_val_acc, \"best_epoch\": best_epoch,\n",
|
| 566 |
+
" \"params\": n_params, \"final_train_acc\": train_metrics[\"accuracy/train\"]}\n",
|
| 567 |
+
"\n",
|
| 568 |
+
"# \u2500\u2500 Johnson-Lindenstrauss Projection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 569 |
+
"class JLProjection(nn.Module):\n",
|
| 570 |
+
" \"\"\"Fixed random Gaussian projection (not learned).\"\"\"\n",
|
| 571 |
+
" def __init__(self, input_dim, output_dim):\n",
|
| 572 |
+
" super().__init__()\n",
|
| 573 |
+
" self.register_buffer(\"W\", torch.randn(input_dim, output_dim) / math.sqrt(output_dim))\n",
|
| 574 |
+
"\n",
|
| 575 |
+
" def forward(self, x):\n",
|
| 576 |
+
" return x @ self.W\n",
|
| 577 |
+
"\n",
|
| 578 |
+
"# \u2500\u2500 Results Scoreboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n",
|
| 579 |
+
"class Scoreboard:\n",
|
| 580 |
+
" \"\"\"Track results across experiments in a session.\"\"\"\n",
|
| 581 |
+
" def __init__(self):\n",
|
| 582 |
+
" self.results = []\n",
|
| 583 |
+
"\n",
|
| 584 |
+
" def add(self, result_dict):\n",
|
| 585 |
+
" self.results.append(result_dict)\n",
|
| 586 |
+
" self._print_table()\n",
|
| 587 |
+
"\n",
|
| 588 |
+
" def _print_table(self):\n",
|
| 589 |
+
" print(f\"\\n{'='*80}\")\n",
|
| 590 |
+
" print(f\"{'SCOREBOARD':^80}\")\n",
|
| 591 |
+
" print(f\"{'='*80}\")\n",
|
| 592 |
+
" print(f\"{'Experiment':<40} {'Val Acc':>8} {'Params':>10} {'Best E':>7}\")\n",
|
| 593 |
+
" print(f\"{'-'*40} {'-'*8} {'-'*10} {'-'*7}\")\n",
|
| 594 |
+
" for r in sorted(self.results, key=lambda x: x[\"best_val_acc\"], reverse=True):\n",
|
| 595 |
+
" print(f\"{r['experiment']:<40} {r['best_val_acc']:>7.2f}% {r['params']:>10,} {r['best_epoch']:>7}\")\n",
|
| 596 |
+
" print(f\"{'='*80}\\n\")\n",
|
| 597 |
+
"\n",
|
| 598 |
+
" def save(self, path=\"scoreboard.json\"):\n",
|
| 599 |
+
" with open(path, \"w\") as f:\n",
|
| 600 |
+
" json.dump(self.results, f, indent=2)\n",
|
| 601 |
+
" print(f\"[SCOREBOARD] Saved to {path}\")\n",
|
| 602 |
+
"\n",
|
| 603 |
+
"scoreboard = Scoreboard()\n",
|
| 604 |
+
"print(\"[SHARED] All utilities loaded successfully.\")\n",
|
| 605 |
+
""
|
| 606 |
+
],
|
| 607 |
+
"execution_count": null,
|
| 608 |
+
"outputs": []
|
| 609 |
+
},
|
| 610 |
+
{
|
| 611 |
+
"cell_type": "markdown",
|
| 612 |
+
"metadata": {},
|
| 613 |
+
"source": [
|
| 614 |
+
"## Additional Pipeline Components"
|
| 615 |
+
]
|
| 616 |
+
},
|
| 617 |
+
{
|
| 618 |
+
"cell_type": "code",
|
| 619 |
+
"metadata": {},
|
| 620 |
+
"source": [
|
| 621 |
+
"\n",
|
| 622 |
+
"from kymatio.torch import Scattering2D\n",
|
| 623 |
+
"import scipy.signal as signal\n",
|
| 624 |
+
"\n",
|
| 625 |
+
"class CurveletEnergy(nn.Module):\n",
|
| 626 |
+
" '''Approximate curvelet energy (simplified ridge detection).'''\n",
|
| 627 |
+
" def __init__(self):\n",
|
| 628 |
+
" super().__init__()\n",
|
| 629 |
+
"\n",
|
| 630 |
+
" def forward(self, x):\n",
|
| 631 |
+
" # Simplified: use Sobel edge detection as proxy for curvelet energy\n",
|
| 632 |
+
" B, C, H, W = x.shape\n",
|
| 633 |
+
" energy = []\n",
|
| 634 |
+
"\n",
|
| 635 |
+
" for c in range(C):\n",
|
| 636 |
+
" channel = x[:, c:c+1, :, :]\n",
|
| 637 |
+
" # Compute gradients\n",
|
| 638 |
+
" dx = torch.abs(channel[:, :, 1:, :] - channel[:, :, :-1, :])\n",
|
| 639 |
+
" dy = torch.abs(channel[:, :, :, 1:] - channel[:, :, :, :-1:])\n",
|
| 640 |
+
" # Energy as magnitude\n",
|
| 641 |
+
" e = (dx.mean(dim=(2, 3)) + dy.mean(dim=(2, 3)))\n",
|
| 642 |
+
" energy.append(e)\n",
|
| 643 |
+
"\n",
|
| 644 |
+
" return torch.cat(energy, dim=1) # (B, 3)\n",
|
| 645 |
+
"\n",
|
| 646 |
+
"class RadonTransform(nn.Module):\n",
|
| 647 |
+
" '''Approximate Radon transform via rotated projections.'''\n",
|
| 648 |
+
" def __init__(self, n_angles=16):\n",
|
| 649 |
+
" super().__init__()\n",
|
| 650 |
+
" self.n_angles = n_angles\n",
|
| 651 |
+
"\n",
|
| 652 |
+
" def forward(self, x):\n",
|
| 653 |
+
" # Simplified: project image at different angles\n",
|
| 654 |
+
" B, C, H, W = x.shape\n",
|
| 655 |
+
" radon_feats = []\n",
|
| 656 |
+
"\n",
|
| 657 |
+
" for c in range(C):\n",
|
| 658 |
+
" channel = x[:, c:c+1, :, :] # (B, 1, H, W)\n",
|
| 659 |
+
" angles = []\n",
|
| 660 |
+
" for theta in torch.linspace(0, np.pi, self.n_angles):\n",
|
| 661 |
+
" # Project along angle (simplified)\n",
|
| 662 |
+
" proj = channel.mean(dim=3 if abs(np.cos(theta)) > abs(np.sin(theta)) else 2)\n",
|
| 663 |
+
" angles.append(proj.mean(dim=2, keepdim=True))\n",
|
| 664 |
+
" radon_feats.extend(angles)\n",
|
| 665 |
+
"\n",
|
| 666 |
+
" return torch.cat(radon_feats, dim=1) # (B, 3*n_angles)\n",
|
| 667 |
+
"\n",
|
| 668 |
+
"print(\"[PIPELINES] Additional components loaded.\")\n",
|
| 669 |
+
""
|
| 670 |
+
],
|
| 671 |
+
"execution_count": null,
|
| 672 |
+
"outputs": []
|
| 673 |
+
},
|
| 674 |
+
{
|
| 675 |
+
"cell_type": "markdown",
|
| 676 |
+
"metadata": {},
|
| 677 |
+
"source": [
|
| 678 |
+
"## 7.1a: Reference Pipeline\n",
|
| 679 |
+
"\n",
|
| 680 |
+
"Scattering(J=2,L=8) \u2192 JL(128) \u2192 L2 norm \u2192 constellation(64) \u2192 classify"
|
| 681 |
+
]
|
| 682 |
+
},
|
| 683 |
+
{
|
| 684 |
+
"cell_type": "code",
|
| 685 |
+
"metadata": {},
|
| 686 |
+
"source": [
|
| 687 |
+
"\n",
|
| 688 |
+
"print(\"[7.1a] Reference Pipeline\")\n",
|
| 689 |
+
"print(\"=\"*70)\n",
|
| 690 |
+
"\n",
|
| 691 |
+
"class ReferencePipeline(nn.Module):\n",
|
| 692 |
+
" '''Canonical: Scattering \u2192 JL \u2192 constellation \u2192 classify.'''\n",
|
| 693 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 694 |
+
" super().__init__()\n",
|
| 695 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 696 |
+
" # Scattering output: 81 channels\n",
|
| 697 |
+
" self.jl = JLProjection(81, embed_dim)\n",
|
| 698 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 699 |
+
"\n",
|
| 700 |
+
" def forward(self, x):\n",
|
| 701 |
+
" scatt = self.scattering(x) # (B, 81, 32, 32)\n",
|
| 702 |
+
" # Pool to 1D\n",
|
| 703 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1) # (B, 81)\n",
|
| 704 |
+
" emb = l2_normalize(self.jl(scatt_pool)) # (B, 128)\n",
|
| 705 |
+
" logits, tri = self.classifier(emb)\n",
|
| 706 |
+
" return logits, emb\n",
|
| 707 |
+
"\n",
|
| 708 |
+
"model = ReferencePipeline(embed_dim=128, num_classes=10).to(device)\n",
|
| 709 |
+
"print(f\"[7.1a] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 710 |
+
"\n",
|
| 711 |
+
"train_loader, val_loader = get_cifar10_loaders(batch_size=256, augment=True)\n",
|
| 712 |
+
"\n",
|
| 713 |
+
"result_7_1a = run_experiment(\n",
|
| 714 |
+
" \"7.1a_Reference_Pipeline\",\n",
|
| 715 |
+
" model, train_loader, val_loader, device,\n",
|
| 716 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 717 |
+
")\n",
|
| 718 |
+
"scoreboard.add(result_7_1a)\n",
|
| 719 |
+
""
|
| 720 |
+
],
|
| 721 |
+
"execution_count": null,
|
| 722 |
+
"outputs": []
|
| 723 |
+
},
|
| 724 |
+
{
|
| 725 |
+
"cell_type": "markdown",
|
| 726 |
+
"metadata": {},
|
| 727 |
+
"source": [
|
| 728 |
+
"## 7.1b: Learned 2-Layer Projection\n",
|
| 729 |
+
"\n",
|
| 730 |
+
"Replace JL with learned linear layer (~16K params)"
|
| 731 |
+
]
|
| 732 |
+
},
|
| 733 |
+
{
|
| 734 |
+
"cell_type": "code",
|
| 735 |
+
"metadata": {},
|
| 736 |
+
"source": [
|
| 737 |
+
"\n",
|
| 738 |
+
"print(\"[7.1b] Learned 2-Layer Projection\")\n",
|
| 739 |
+
"print(\"=\"*70)\n",
|
| 740 |
+
"\n",
|
| 741 |
+
"class LearnedProjectionPipeline(nn.Module):\n",
|
| 742 |
+
" '''Scattering \u2192 learned 2-layer projection \u2192 constellation.'''\n",
|
| 743 |
+
" def __init__(self, embed_dim=128, num_classes=10, hidden_dim=128):\n",
|
| 744 |
+
" super().__init__()\n",
|
| 745 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 746 |
+
" # Learned projection\n",
|
| 747 |
+
" self.proj = nn.Sequential(\n",
|
| 748 |
+
" nn.Linear(81, hidden_dim),\n",
|
| 749 |
+
" nn.ReLU(),\n",
|
| 750 |
+
" nn.Linear(hidden_dim, embed_dim)\n",
|
| 751 |
+
" )\n",
|
| 752 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 753 |
+
"\n",
|
| 754 |
+
" def forward(self, x):\n",
|
| 755 |
+
" scatt = self.scattering(x)\n",
|
| 756 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 757 |
+
" emb = l2_normalize(self.proj(scatt_pool))\n",
|
| 758 |
+
" logits, tri = self.classifier(emb)\n",
|
| 759 |
+
" return logits, emb\n",
|
| 760 |
+
"\n",
|
| 761 |
+
"model = LearnedProjectionPipeline(embed_dim=128, num_classes=10, hidden_dim=128).to(device)\n",
|
| 762 |
+
"params = sum(p.numel() for p in model.parameters())\n",
|
| 763 |
+
"print(f\"[7.1b] Model params: {params:,}\")\n",
|
| 764 |
+
"\n",
|
| 765 |
+
"result_7_1b = run_experiment(\n",
|
| 766 |
+
" \"7.1b_Learned_Projection\",\n",
|
| 767 |
+
" model, train_loader, val_loader, device,\n",
|
| 768 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 769 |
+
")\n",
|
| 770 |
+
"scoreboard.add(result_7_1b)\n",
|
| 771 |
+
""
|
| 772 |
+
],
|
| 773 |
+
"execution_count": null,
|
| 774 |
+
"outputs": []
|
| 775 |
+
},
|
| 776 |
+
{
|
| 777 |
+
"cell_type": "markdown",
|
| 778 |
+
"metadata": {},
|
| 779 |
+
"source": [
|
| 780 |
+
"## 7.1c: Scattering + Curvelet Energy\n",
|
| 781 |
+
"\n",
|
| 782 |
+
"Scattering \u2192 curvelet energy \u2192 concat \u2192 JL"
|
| 783 |
+
]
|
| 784 |
+
},
|
| 785 |
+
{
|
| 786 |
+
"cell_type": "code",
|
| 787 |
+
"metadata": {},
|
| 788 |
+
"source": [
|
| 789 |
+
"\n",
|
| 790 |
+
"print(\"[7.1c] Scattering + Curvelet Energy\")\n",
|
| 791 |
+
"print(\"=\"*70)\n",
|
| 792 |
+
"\n",
|
| 793 |
+
"class ScatteringCurveletPipeline(nn.Module):\n",
|
| 794 |
+
" '''Scattering + curvelet energy concat.'''\n",
|
| 795 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 796 |
+
" super().__init__()\n",
|
| 797 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 798 |
+
" self.curvelet = CurveletEnergy()\n",
|
| 799 |
+
" # Scattering (81) + curvelet (3) = 84\n",
|
| 800 |
+
" self.jl = JLProjection(84, embed_dim)\n",
|
| 801 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 802 |
+
"\n",
|
| 803 |
+
" def forward(self, x):\n",
|
| 804 |
+
" scatt = self.scattering(x)\n",
|
| 805 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 806 |
+
" curv = self.curvelet(x)\n",
|
| 807 |
+
" combined = torch.cat([scatt_pool, curv], dim=1)\n",
|
| 808 |
+
" emb = l2_normalize(self.jl(combined))\n",
|
| 809 |
+
" logits, tri = self.classifier(emb)\n",
|
| 810 |
+
" return logits, emb\n",
|
| 811 |
+
"\n",
|
| 812 |
+
"model = ScatteringCurveletPipeline(embed_dim=128, num_classes=10).to(device)\n",
|
| 813 |
+
"print(f\"[7.1c] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 814 |
+
"\n",
|
| 815 |
+
"result_7_1c = run_experiment(\n",
|
| 816 |
+
" \"7.1c_Scattering_Curvelet\",\n",
|
| 817 |
+
" model, train_loader, val_loader, device,\n",
|
| 818 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 819 |
+
")\n",
|
| 820 |
+
"scoreboard.add(result_7_1c)\n",
|
| 821 |
+
""
|
| 822 |
+
],
|
| 823 |
+
"execution_count": null,
|
| 824 |
+
"outputs": []
|
| 825 |
+
},
|
| 826 |
+
{
|
| 827 |
+
"cell_type": "markdown",
|
| 828 |
+
"metadata": {},
|
| 829 |
+
"source": [
|
| 830 |
+
"## 7.2a: STFT Channels + Scattering\n",
|
| 831 |
+
"\n",
|
| 832 |
+
"STFT channels (v4) + scattering \u2192 concat \u2192 JL \u2192 constellation"
|
| 833 |
+
]
|
| 834 |
+
},
|
| 835 |
+
{
|
| 836 |
+
"cell_type": "code",
|
| 837 |
+
"metadata": {},
|
| 838 |
+
"source": [
|
| 839 |
+
"\n",
|
| 840 |
+
"print(\"[7.2a] STFT Channels + Scattering\")\n",
|
| 841 |
+
"print(\"=\"*70)\n",
|
| 842 |
+
"\n",
|
| 843 |
+
"class STFTScatteringPipeline(nn.Module):\n",
|
| 844 |
+
" '''STFT + Scattering combined features.'''\n",
|
| 845 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 846 |
+
" super().__init__()\n",
|
| 847 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 848 |
+
" # STFT front-end (from nb5)\n",
|
| 849 |
+
" self.stft_fe = STFTFrontend(n_fft=16, hop_length=4, freq_bins=8)\n",
|
| 850 |
+
" # STFT (1536) + Scattering (81) = 1617, reduce to ~256\n",
|
| 851 |
+
" self.reduce = nn.Linear(1617, 256)\n",
|
| 852 |
+
" self.jl = JLProjection(256, embed_dim)\n",
|
| 853 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 854 |
+
"\n",
|
| 855 |
+
" def forward(self, x):\n",
|
| 856 |
+
" # Scattering\n",
|
| 857 |
+
" scatt = self.scattering(x)\n",
|
| 858 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 859 |
+
"\n",
|
| 860 |
+
" # STFT\n",
|
| 861 |
+
" stft_feat = self.stft_fe(x)\n",
|
| 862 |
+
"\n",
|
| 863 |
+
" # Combine\n",
|
| 864 |
+
" combined = torch.cat([scatt_pool, stft_feat], dim=1)\n",
|
| 865 |
+
" reduced = self.reduce(combined)\n",
|
| 866 |
+
" emb = l2_normalize(self.jl(reduced))\n",
|
| 867 |
+
" logits, tri = self.classifier(emb)\n",
|
| 868 |
+
" return logits, emb\n",
|
| 869 |
+
"\n",
|
| 870 |
+
"model = STFTScatteringPipeline(embed_dim=128, num_classes=10).to(device)\n",
|
| 871 |
+
"print(f\"[7.2a] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 872 |
+
"\n",
|
| 873 |
+
"result_7_2a = run_experiment(\n",
|
| 874 |
+
" \"7.2a_STFT_Scattering\",\n",
|
| 875 |
+
" model, train_loader, val_loader, device,\n",
|
| 876 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 877 |
+
")\n",
|
| 878 |
+
"scoreboard.add(result_7_2a)\n",
|
| 879 |
+
""
|
| 880 |
+
],
|
| 881 |
+
"execution_count": null,
|
| 882 |
+
"outputs": []
|
| 883 |
+
},
|
| 884 |
+
{
|
| 885 |
+
"cell_type": "markdown",
|
| 886 |
+
"metadata": {},
|
| 887 |
+
"source": [
|
| 888 |
+
"## 7.2b: Scattering \u2192 Cholesky Gram + SVD Signature\n",
|
| 889 |
+
"\n",
|
| 890 |
+
"Scattering \u2192 Cholesky of Gram + SVD singular values"
|
| 891 |
+
]
|
| 892 |
+
},
|
| 893 |
+
{
|
| 894 |
+
"cell_type": "code",
|
| 895 |
+
"metadata": {},
|
| 896 |
+
"source": [
|
| 897 |
+
"\n",
|
| 898 |
+
"print(\"[7.2b] Scattering + Cholesky Gram + SVD\")\n",
|
| 899 |
+
"print(\"=\"*70)\n",
|
| 900 |
+
"\n",
|
| 901 |
+
"class ScatteringCholeskyPipeline(nn.Module):\n",
|
| 902 |
+
" '''Scattering \u2192 Gram \u2192 Cholesky + SVD signatures.'''\n",
|
| 903 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 904 |
+
" super().__init__()\n",
|
| 905 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 906 |
+
" # Scattering (81) \u2192 reduced (256) for Gram\n",
|
| 907 |
+
" self.reduce = nn.Linear(81, 256)\n",
|
| 908 |
+
"\n",
|
| 909 |
+
" # Features: Cholesky (10) + SVD (10) + original (81)\n",
|
| 910 |
+
" self.jl = JLProjection(101, embed_dim)\n",
|
| 911 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 912 |
+
"\n",
|
| 913 |
+
" def forward(self, x):\n",
|
| 914 |
+
" scatt = self.scattering(x)\n",
|
| 915 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 916 |
+
"\n",
|
| 917 |
+
" # Gram matrix\n",
|
| 918 |
+
" feat_red = self.reduce(scatt_pool)\n",
|
| 919 |
+
" B = feat_red.shape[0]\n",
|
| 920 |
+
" gram = torch.bmm(feat_red.unsqueeze(2), feat_red.unsqueeze(1))\n",
|
| 921 |
+
" gram = gram + torch.eye(256, device=gram.device).unsqueeze(0) * 1e-6\n",
|
| 922 |
+
"\n",
|
| 923 |
+
" # Cholesky\n",
|
| 924 |
+
" try:\n",
|
| 925 |
+
" L = torch.linalg.cholesky(gram)\n",
|
| 926 |
+
" chol_diag = torch.diagonal(L, dim1=1, dim2=2)\n",
|
| 927 |
+
" chol_mean = chol_diag.mean(dim=1)\n",
|
| 928 |
+
" chol_std = chol_diag.std(dim=1)\n",
|
| 929 |
+
" chol_sum = chol_diag.sum(dim=1)\n",
|
| 930 |
+
" chol_max = chol_diag.max(dim=1)[0]\n",
|
| 931 |
+
" chol_norm = torch.linalg.norm(chol_diag, dim=1)\n",
|
| 932 |
+
" chol_features = torch.stack([chol_mean, chol_std, chol_sum, chol_max, chol_norm,\n",
|
| 933 |
+
" chol_diag.min(dim=1)[0], chol_diag.max(dim=1)[0] - chol_diag.min(dim=1)[0],\n",
|
| 934 |
+
" torch.prod(chol_diag, dim=1), torch.log(torch.prod(chol_diag, dim=1) + 1e-8),\n",
|
| 935 |
+
" (chol_diag > 1e-5).sum(dim=1).float()], dim=1)\n",
|
| 936 |
+
" except:\n",
|
| 937 |
+
" chol_features = torch.zeros(B, 10, device=x.device)\n",
|
| 938 |
+
"\n",
|
| 939 |
+
" # SVD\n",
|
| 940 |
+
" U, S, Vh = torch.svd(gram)\n",
|
| 941 |
+
" svd_mean = S.mean(dim=1)\n",
|
| 942 |
+
" svd_std = S.std(dim=1)\n",
|
| 943 |
+
" svd_ratio = S[:, 0] / (S[:, -1] + 1e-8)\n",
|
| 944 |
+
" svd_sum = S.sum(dim=1)\n",
|
| 945 |
+
" svd_max = S[:, 0]\n",
|
| 946 |
+
" svd_min = S[:, -1]\n",
|
| 947 |
+
" svd_norm = torch.linalg.norm(S, dim=1)\n",
|
| 948 |
+
" svd_rank = (S > 1e-5).sum(dim=1).float()\n",
|
| 949 |
+
" svd_trace = S.sum(dim=1)\n",
|
| 950 |
+
" svd_energy = (S ** 2).sum(dim=1)\n",
|
| 951 |
+
" svd_features = torch.stack([svd_mean, svd_std, svd_ratio, svd_sum, svd_max,\n",
|
| 952 |
+
" svd_min, svd_norm, svd_rank, svd_trace, svd_energy], dim=1)\n",
|
| 953 |
+
"\n",
|
| 954 |
+
" # Combine\n",
|
| 955 |
+
" combined = torch.cat([scatt_pool, chol_features, svd_features], dim=1) # (B, 101)\n",
|
| 956 |
+
" emb = l2_normalize(self.jl(combined))\n",
|
| 957 |
+
" logits, tri = self.classifier(emb)\n",
|
| 958 |
+
" return logits, emb\n",
|
| 959 |
+
"\n",
|
| 960 |
+
"model = ScatteringCholeskyPipeline(embed_dim=128, num_classes=10).to(device)\n",
|
| 961 |
+
"print(f\"[7.2b] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 962 |
+
"\n",
|
| 963 |
+
"result_7_2b = run_experiment(\n",
|
| 964 |
+
" \"7.2b_Scattering_Cholesky_SVD\",\n",
|
| 965 |
+
" model, train_loader, val_loader, device,\n",
|
| 966 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 967 |
+
")\n",
|
| 968 |
+
"scoreboard.add(result_7_2b)\n",
|
| 969 |
+
""
|
| 970 |
+
],
|
| 971 |
+
"execution_count": null,
|
| 972 |
+
"outputs": []
|
| 973 |
+
},
|
| 974 |
+
{
|
| 975 |
+
"cell_type": "markdown",
|
| 976 |
+
"metadata": {},
|
| 977 |
+
"source": [
|
| 978 |
+
"## 7.3a: Multi-Signature Parallel\n",
|
| 979 |
+
"\n",
|
| 980 |
+
"Scattering + Gabor + Radon \u2192 separate constellations \u2192 fusion"
|
| 981 |
+
]
|
| 982 |
+
},
|
| 983 |
+
{
|
| 984 |
+
"cell_type": "code",
|
| 985 |
+
"metadata": {},
|
| 986 |
+
"source": [
|
| 987 |
+
"\n",
|
| 988 |
+
"print(\"[7.3a] Multi-Signature Parallel\")\n",
|
| 989 |
+
"print(\"=\"*70)\n",
|
| 990 |
+
"\n",
|
| 991 |
+
"class GaborFilter(nn.Module):\n",
|
| 992 |
+
" '''Simple Gabor filter bank approximation.'''\n",
|
| 993 |
+
" def __init__(self, n_orientations=8, n_scales=3):\n",
|
| 994 |
+
" super().__init__()\n",
|
| 995 |
+
" self.n_orientations = n_orientations\n",
|
| 996 |
+
" self.n_scales = n_scales\n",
|
| 997 |
+
"\n",
|
| 998 |
+
" def forward(self, x):\n",
|
| 999 |
+
" B, C, H, W = x.shape\n",
|
| 1000 |
+
" outputs = []\n",
|
| 1001 |
+
"\n",
|
| 1002 |
+
" for scale in range(self.n_scales):\n",
|
| 1003 |
+
" sigma = 1.0 + scale * 0.5\n",
|
| 1004 |
+
" lambda_val = 5.0 + scale\n",
|
| 1005 |
+
" for theta in torch.linspace(0, np.pi, self.n_orientations):\n",
|
| 1006 |
+
" # Simplified Gabor: Gaussian-weighted sinusoid\n",
|
| 1007 |
+
" y = torch.arange(H, dtype=x.dtype, device=x.device)\n",
|
| 1008 |
+
" x_grid = torch.arange(W, dtype=x.dtype, device=x.device)\n",
|
| 1009 |
+
" yy, xx = torch.meshgrid(y - H/2, x_grid - W/2, indexing='ij')\n",
|
| 1010 |
+
"\n",
|
| 1011 |
+
" # Rotate\n",
|
| 1012 |
+
" x_theta = xx * np.cos(theta) + yy * np.sin(theta)\n",
|
| 1013 |
+
" y_theta = -xx * np.sin(theta) + yy * np.cos(theta)\n",
|
| 1014 |
+
"\n",
|
| 1015 |
+
" # Gabor kernel\n",
|
| 1016 |
+
" kernel = torch.exp(-(x_theta**2 + y_theta**2) / (2 * sigma**2)) * torch.cos(2 * np.pi * x_theta / lambda_val)\n",
|
| 1017 |
+
" kernel = kernel / (kernel.abs().sum() + 1e-8)\n",
|
| 1018 |
+
"\n",
|
| 1019 |
+
" # Convolve\n",
|
| 1020 |
+
" kernel = kernel.unsqueeze(0).unsqueeze(0)\n",
|
| 1021 |
+
" response = F.conv2d(x, kernel.expand(C, -1, -1, -1), padding='same').mean(dim=(2, 3))\n",
|
| 1022 |
+
" outputs.append(response)\n",
|
| 1023 |
+
"\n",
|
| 1024 |
+
" return torch.cat(outputs, dim=1)\n",
|
| 1025 |
+
"\n",
|
| 1026 |
+
"class MultiSignaturePipeline(nn.Module):\n",
|
| 1027 |
+
" '''Parallel: Scattering, Gabor, Radon \u2192 fusion.'''\n",
|
| 1028 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 1029 |
+
" super().__init__()\n",
|
| 1030 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 1031 |
+
" self.gabor = GaborFilter(n_orientations=8, n_scales=2)\n",
|
| 1032 |
+
" self.radon = RadonTransform(n_angles=8)\n",
|
| 1033 |
+
"\n",
|
| 1034 |
+
" # Create separate branches\n",
|
| 1035 |
+
" self.scatt_jl = JLProjection(81, 64)\n",
|
| 1036 |
+
" self.gabor_jl = JLProjection(48, 64) # 8*2 orientations/scales * 3 channels\n",
|
| 1037 |
+
" self.radon_jl = JLProjection(24, 64) # 8 angles * 3 channels\n",
|
| 1038 |
+
"\n",
|
| 1039 |
+
" # Fusion\n",
|
| 1040 |
+
" self.fusion = nn.Linear(192, embed_dim)\n",
|
| 1041 |
+
"\n",
|
| 1042 |
+
" # Classifier (3 constellations or single)\n",
|
| 1043 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 1044 |
+
"\n",
|
| 1045 |
+
" def forward(self, x):\n",
|
| 1046 |
+
" # Scattering\n",
|
| 1047 |
+
" scatt = self.scattering(x)\n",
|
| 1048 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 1049 |
+
" scatt_emb = l2_normalize(self.scatt_jl(scatt_pool))\n",
|
| 1050 |
+
"\n",
|
| 1051 |
+
" # Gabor\n",
|
| 1052 |
+
" gabor = self.gabor(x)\n",
|
| 1053 |
+
" gabor_emb = l2_normalize(self.gabor_jl(gabor))\n",
|
| 1054 |
+
"\n",
|
| 1055 |
+
" # Radon\n",
|
| 1056 |
+
" radon = self.radon(x)\n",
|
| 1057 |
+
" radon_emb = l2_normalize(self.radon_jl(radon))\n",
|
| 1058 |
+
"\n",
|
| 1059 |
+
" # Fuse\n",
|
| 1060 |
+
" fused = torch.cat([scatt_emb, gabor_emb, radon_emb], dim=1)\n",
|
| 1061 |
+
" emb = l2_normalize(self.fusion(fused))\n",
|
| 1062 |
+
"\n",
|
| 1063 |
+
" logits, tri = self.classifier(emb)\n",
|
| 1064 |
+
" return logits, emb\n",
|
| 1065 |
+
"\n",
|
| 1066 |
+
"model = MultiSignaturePipeline(embed_dim=128, num_classes=10).to(device)\n",
|
| 1067 |
+
"print(f\"[7.3a] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 1068 |
+
"\n",
|
| 1069 |
+
"result_7_3a = run_experiment(\n",
|
| 1070 |
+
" \"7.3a_MultiSignature_Parallel\",\n",
|
| 1071 |
+
" model, train_loader, val_loader, device,\n",
|
| 1072 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 1073 |
+
")\n",
|
| 1074 |
+
"scoreboard.add(result_7_3a)\n",
|
| 1075 |
+
""
|
| 1076 |
+
],
|
| 1077 |
+
"execution_count": null,
|
| 1078 |
+
"outputs": []
|
| 1079 |
+
},
|
| 1080 |
+
{
|
| 1081 |
+
"cell_type": "markdown",
|
| 1082 |
+
"metadata": {},
|
| 1083 |
+
"source": [
|
| 1084 |
+
"## 7.3b: Hierarchical Constellation\n",
|
| 1085 |
+
"\n",
|
| 1086 |
+
"Scattering \u2192 coarse anchors \u2192 residual \u2192 fine anchors"
|
| 1087 |
+
]
|
| 1088 |
+
},
|
| 1089 |
+
{
|
| 1090 |
+
"cell_type": "code",
|
| 1091 |
+
"metadata": {},
|
| 1092 |
+
"source": [
|
| 1093 |
+
"\n",
|
| 1094 |
+
"print(\"[7.3b] Hierarchical Constellation\")\n",
|
| 1095 |
+
"print(\"=\"*70)\n",
|
| 1096 |
+
"\n",
|
| 1097 |
+
"class HierarchicalConstellation(nn.Module):\n",
|
| 1098 |
+
" '''Two-level constellation: coarse + residual fine.'''\n",
|
| 1099 |
+
" def __init__(self, embed_dim, num_coarse=16, num_fine=48):\n",
|
| 1100 |
+
" super().__init__()\n",
|
| 1101 |
+
" self.embed_dim = embed_dim\n",
|
| 1102 |
+
" self.num_coarse = num_coarse\n",
|
| 1103 |
+
" self.num_fine = num_fine\n",
|
| 1104 |
+
"\n",
|
| 1105 |
+
" # Coarse constellation\n",
|
| 1106 |
+
" coarse_anchors = self._init_anchors(embed_dim, num_coarse, \"uniform\")\n",
|
| 1107 |
+
" self.coarse_anchors = nn.Parameter(coarse_anchors, requires_grad=False)\n",
|
| 1108 |
+
"\n",
|
| 1109 |
+
" # Fine constellation\n",
|
| 1110 |
+
" fine_anchors = self._init_anchors(embed_dim, num_fine, \"uniform\")\n",
|
| 1111 |
+
" self.fine_anchors = nn.Parameter(fine_anchors, requires_grad=False)\n",
|
| 1112 |
+
"\n",
|
| 1113 |
+
" def _init_anchors(self, d, K, method):\n",
|
| 1114 |
+
" A = torch.randn(K, d)\n",
|
| 1115 |
+
" Q, _ = torch.linalg.qr(A.T)\n",
|
| 1116 |
+
" anchors = Q.T[:K]\n",
|
| 1117 |
+
" if K > d:\n",
|
| 1118 |
+
" extra = F.normalize(torch.randn(K - d, d), dim=-1)\n",
|
| 1119 |
+
" anchors = torch.cat([anchors, extra], dim=0)\n",
|
| 1120 |
+
" return F.normalize(anchors, dim=-1)\n",
|
| 1121 |
+
"\n",
|
| 1122 |
+
" def forward(self, x):\n",
|
| 1123 |
+
" x_norm = l2_normalize(x)\n",
|
| 1124 |
+
"\n",
|
| 1125 |
+
" # Coarse triangulation\n",
|
| 1126 |
+
" coarse_sim = torch.mm(x_norm, self.coarse_anchors.T)\n",
|
| 1127 |
+
" coarse_tri = 1.0 - coarse_sim # (B, 16)\n",
|
| 1128 |
+
"\n",
|
| 1129 |
+
" # Residual toward nearest coarse anchor\n",
|
| 1130 |
+
" nearest_coarse = coarse_sim.argmax(dim=1)\n",
|
| 1131 |
+
" nearest_anchor = self.coarse_anchors[nearest_coarse] # (B, d)\n",
|
| 1132 |
+
" residual = x_norm - nearest_anchor # (B, d)\n",
|
| 1133 |
+
" residual = l2_normalize(residual)\n",
|
| 1134 |
+
"\n",
|
| 1135 |
+
" # Fine triangulation (relative to residual)\n",
|
| 1136 |
+
" fine_sim = torch.mm(residual, self.fine_anchors.T)\n",
|
| 1137 |
+
" fine_tri = 1.0 - fine_sim # (B, 48)\n",
|
| 1138 |
+
"\n",
|
| 1139 |
+
" # Combine\n",
|
| 1140 |
+
" tri = torch.cat([coarse_tri, fine_tri], dim=1) # (B, 64)\n",
|
| 1141 |
+
" return tri\n",
|
| 1142 |
+
"\n",
|
| 1143 |
+
"class HierarchicalPipeline(nn.Module):\n",
|
| 1144 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 1145 |
+
" super().__init__()\n",
|
| 1146 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 1147 |
+
" self.jl = JLProjection(81, embed_dim)\n",
|
| 1148 |
+
" self.constellation = HierarchicalConstellation(embed_dim, num_coarse=16, num_fine=48)\n",
|
| 1149 |
+
" self.patchwork = Patchwork(64, 2)\n",
|
| 1150 |
+
" self.head = nn.Linear(64, num_classes)\n",
|
| 1151 |
+
"\n",
|
| 1152 |
+
" def forward(self, x):\n",
|
| 1153 |
+
" scatt = self.scattering(x)\n",
|
| 1154 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 1155 |
+
" emb = l2_normalize(self.jl(scatt_pool))\n",
|
| 1156 |
+
" tri = self.constellation(emb)\n",
|
| 1157 |
+
" pw = self.patchwork(tri)\n",
|
| 1158 |
+
" logits = self.head(pw)\n",
|
| 1159 |
+
" return logits, emb\n",
|
| 1160 |
+
"\n",
|
| 1161 |
+
"model = HierarchicalPipeline(embed_dim=128, num_classes=10).to(device)\n",
|
| 1162 |
+
"print(f\"[7.3b] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 1163 |
+
"\n",
|
| 1164 |
+
"result_7_3b = run_experiment(\n",
|
| 1165 |
+
" \"7.3b_Hierarchical_Constellation\",\n",
|
| 1166 |
+
" model, train_loader, val_loader, device,\n",
|
| 1167 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 1168 |
+
")\n",
|
| 1169 |
+
"scoreboard.add(result_7_3b)\n",
|
| 1170 |
+
""
|
| 1171 |
+
],
|
| 1172 |
+
"execution_count": null,
|
| 1173 |
+
"outputs": []
|
| 1174 |
+
},
|
| 1175 |
+
{
|
| 1176 |
+
"cell_type": "markdown",
|
| 1177 |
+
"metadata": {},
|
| 1178 |
+
"source": [
|
| 1179 |
+
"## 7.4a: Best Deterministic + Linear Layer\n",
|
| 1180 |
+
"\n",
|
| 1181 |
+
"Reference + 1 learned linear layer before constellation"
|
| 1182 |
+
]
|
| 1183 |
+
},
|
| 1184 |
+
{
|
| 1185 |
+
"cell_type": "code",
|
| 1186 |
+
"metadata": {},
|
| 1187 |
+
"source": [
|
| 1188 |
+
"\n",
|
| 1189 |
+
"print(\"[7.4a] Best Deterministic + Linear Layer\")\n",
|
| 1190 |
+
"print(\"=\"*70)\n",
|
| 1191 |
+
"\n",
|
| 1192 |
+
"class DeterministicLinearPipeline(nn.Module):\n",
|
| 1193 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 1194 |
+
" super().__init__()\n",
|
| 1195 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 1196 |
+
" self.jl = JLProjection(81, embed_dim)\n",
|
| 1197 |
+
" # Linear projection before constellation\n",
|
| 1198 |
+
" self.linear = nn.Linear(embed_dim, embed_dim)\n",
|
| 1199 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 1200 |
+
"\n",
|
| 1201 |
+
" def forward(self, x):\n",
|
| 1202 |
+
" scatt = self.scattering(x)\n",
|
| 1203 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 1204 |
+
" jl_feat = self.jl(scatt_pool)\n",
|
| 1205 |
+
" proj = self.linear(jl_feat)\n",
|
| 1206 |
+
" emb = l2_normalize(proj)\n",
|
| 1207 |
+
" logits, tri = self.classifier(emb)\n",
|
| 1208 |
+
" return logits, emb\n",
|
| 1209 |
+
"\n",
|
| 1210 |
+
"model = DeterministicLinearPipeline(embed_dim=128, num_classes=10).to(device)\n",
|
| 1211 |
+
"print(f\"[7.4a] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 1212 |
+
"\n",
|
| 1213 |
+
"result_7_4a = run_experiment(\n",
|
| 1214 |
+
" \"7.4a_DeterministicLinear\",\n",
|
| 1215 |
+
" model, train_loader, val_loader, device,\n",
|
| 1216 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 1217 |
+
")\n",
|
| 1218 |
+
"scoreboard.add(result_7_4a)\n",
|
| 1219 |
+
""
|
| 1220 |
+
],
|
| 1221 |
+
"execution_count": null,
|
| 1222 |
+
"outputs": []
|
| 1223 |
+
},
|
| 1224 |
+
{
|
| 1225 |
+
"cell_type": "markdown",
|
| 1226 |
+
"metadata": {},
|
| 1227 |
+
"source": [
|
| 1228 |
+
"## 7.4b: Deterministic + SquaredReLU + LayerNorm"
|
| 1229 |
+
]
|
| 1230 |
+
},
|
| 1231 |
+
{
|
| 1232 |
+
"cell_type": "code",
|
| 1233 |
+
"metadata": {},
|
| 1234 |
+
"source": [
|
| 1235 |
+
"\n",
|
| 1236 |
+
"print(\"[7.4b] Deterministic + SquaredReLU + LayerNorm\")\n",
|
| 1237 |
+
"print(\"=\"*70)\n",
|
| 1238 |
+
"\n",
|
| 1239 |
+
"class DeterministicNonlinearPipeline(nn.Module):\n",
|
| 1240 |
+
" def __init__(self, embed_dim=128, num_classes=10):\n",
|
| 1241 |
+
" super().__init__()\n",
|
| 1242 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 1243 |
+
" self.jl = JLProjection(81, embed_dim)\n",
|
| 1244 |
+
" # Nonlinear projection\n",
|
| 1245 |
+
" self.proj = nn.Sequential(\n",
|
| 1246 |
+
" nn.Linear(embed_dim, embed_dim),\n",
|
| 1247 |
+
" SquaredReLU(),\n",
|
| 1248 |
+
" nn.LayerNorm(embed_dim)\n",
|
| 1249 |
+
" )\n",
|
| 1250 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 1251 |
+
"\n",
|
| 1252 |
+
" def forward(self, x):\n",
|
| 1253 |
+
" scatt = self.scattering(x)\n",
|
| 1254 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 1255 |
+
" jl_feat = self.jl(scatt_pool)\n",
|
| 1256 |
+
" proj = self.proj(jl_feat)\n",
|
| 1257 |
+
" emb = l2_normalize(proj)\n",
|
| 1258 |
+
" logits, tri = self.classifier(emb)\n",
|
| 1259 |
+
" return logits, emb\n",
|
| 1260 |
+
"\n",
|
| 1261 |
+
"model = DeterministicNonlinearPipeline(embed_dim=128, num_classes=10).to(device)\n",
|
| 1262 |
+
"print(f\"[7.4b] Model params: {sum(p.numel() for p in model.parameters()):,}\")\n",
|
| 1263 |
+
"\n",
|
| 1264 |
+
"result_7_4b = run_experiment(\n",
|
| 1265 |
+
" \"7.4b_DeterministicNonlinear\",\n",
|
| 1266 |
+
" model, train_loader, val_loader, device,\n",
|
| 1267 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 1268 |
+
")\n",
|
| 1269 |
+
"scoreboard.add(result_7_4b)\n",
|
| 1270 |
+
""
|
| 1271 |
+
],
|
| 1272 |
+
"execution_count": null,
|
| 1273 |
+
"outputs": []
|
| 1274 |
+
},
|
| 1275 |
+
{
|
| 1276 |
+
"cell_type": "markdown",
|
| 1277 |
+
"metadata": {},
|
| 1278 |
+
"source": [
|
| 1279 |
+
"## 7.4c: Learned Projection Size Sweep\n",
|
| 1280 |
+
"\n",
|
| 1281 |
+
"0, 1K, 5K, 10K, 50K, 100K params"
|
| 1282 |
+
]
|
| 1283 |
+
},
|
| 1284 |
+
{
|
| 1285 |
+
"cell_type": "code",
|
| 1286 |
+
"metadata": {},
|
| 1287 |
+
"source": [
|
| 1288 |
+
"\n",
|
| 1289 |
+
"print(\"[7.4c] Learned Projection Size Sweep\")\n",
|
| 1290 |
+
"print(\"=\"*70)\n",
|
| 1291 |
+
"\n",
|
| 1292 |
+
"def count_proj_params(input_dim, hidden_dim, output_dim, depth=1):\n",
|
| 1293 |
+
" total = 0\n",
|
| 1294 |
+
" prev = input_dim\n",
|
| 1295 |
+
" for i in range(depth):\n",
|
| 1296 |
+
" h = hidden_dim if i < depth - 1 else output_dim\n",
|
| 1297 |
+
" total += prev * h + h\n",
|
| 1298 |
+
" prev = h\n",
|
| 1299 |
+
" return total\n",
|
| 1300 |
+
"\n",
|
| 1301 |
+
"class ProjectionPipeline(nn.Module):\n",
|
| 1302 |
+
" def __init__(self, embed_dim=128, num_classes=10, proj_layers=None):\n",
|
| 1303 |
+
" super().__init__()\n",
|
| 1304 |
+
" self.scattering = Scattering2D(J=2, L=8, shape=(32, 32), max_order=2)\n",
|
| 1305 |
+
" self.jl = JLProjection(81, embed_dim) if proj_layers is None else None\n",
|
| 1306 |
+
"\n",
|
| 1307 |
+
" if proj_layers is None:\n",
|
| 1308 |
+
" # No projection\n",
|
| 1309 |
+
" self.proj = nn.Identity()\n",
|
| 1310 |
+
" else:\n",
|
| 1311 |
+
" # Build multi-layer projection\n",
|
| 1312 |
+
" layers = []\n",
|
| 1313 |
+
" prev_dim = 81\n",
|
| 1314 |
+
" for i, hidden_dim in enumerate(proj_layers[:-1]):\n",
|
| 1315 |
+
" layers.append(nn.Linear(prev_dim, hidden_dim))\n",
|
| 1316 |
+
" layers.append(nn.ReLU())\n",
|
| 1317 |
+
" prev_dim = hidden_dim\n",
|
| 1318 |
+
" layers.append(nn.Linear(prev_dim, embed_dim))\n",
|
| 1319 |
+
" self.proj = nn.Sequential(*layers)\n",
|
| 1320 |
+
"\n",
|
| 1321 |
+
" self.classifier = ConstellationClassifier(embed_dim, num_anchors=64, num_classes=num_classes)\n",
|
| 1322 |
+
"\n",
|
| 1323 |
+
" def forward(self, x):\n",
|
| 1324 |
+
" scatt = self.scattering(x)\n",
|
| 1325 |
+
" scatt_pool = F.adaptive_avg_pool2d(scatt, (1, 1)).squeeze(-1).squeeze(-1)\n",
|
| 1326 |
+
" if self.jl is not None:\n",
|
| 1327 |
+
" feat = self.jl(scatt_pool)\n",
|
| 1328 |
+
" else:\n",
|
| 1329 |
+
" feat = self.proj(scatt_pool)\n",
|
| 1330 |
+
" emb = l2_normalize(feat)\n",
|
| 1331 |
+
" logits, tri = self.classifier(emb)\n",
|
| 1332 |
+
" return logits, emb\n",
|
| 1333 |
+
"\n",
|
| 1334 |
+
"# 0K params: identity (no projection)\n",
|
| 1335 |
+
"for target_params, proj_config in [\n",
|
| 1336 |
+
" (0, None),\n",
|
| 1337 |
+
" (1000, [128]),\n",
|
| 1338 |
+
" (5000, [256]),\n",
|
| 1339 |
+
" (10000, [512]),\n",
|
| 1340 |
+
" (50000, [512, 512]),\n",
|
| 1341 |
+
" (100000, [1024, 512]),\n",
|
| 1342 |
+
"]:\n",
|
| 1343 |
+
" model = ProjectionPipeline(embed_dim=128, num_classes=10, proj_layers=proj_config).to(device)\n",
|
| 1344 |
+
" actual_params = sum(p.numel() for p in model.parameters())\n",
|
| 1345 |
+
" print(f\"[7.4c] Projection {target_params}K \u2192 actual {actual_params:,} params\")\n",
|
| 1346 |
+
"\n",
|
| 1347 |
+
" result = run_experiment(\n",
|
| 1348 |
+
" f\"7.4c_ProjectionSize_{target_params}K\",\n",
|
| 1349 |
+
" model, train_loader, val_loader, device,\n",
|
| 1350 |
+
" epochs=100, lr=1e-3, weight_decay=1e-4, patience=20\n",
|
| 1351 |
+
" )\n",
|
| 1352 |
+
" scoreboard.add(result)\n",
|
| 1353 |
+
""
|
| 1354 |
+
],
|
| 1355 |
+
"execution_count": null,
|
| 1356 |
+
"outputs": []
|
| 1357 |
+
},
|
| 1358 |
+
{
|
| 1359 |
+
"cell_type": "markdown",
|
| 1360 |
+
"metadata": {},
|
| 1361 |
+
"source": [
|
| 1362 |
+
"## Final Scoreboard"
|
| 1363 |
+
]
|
| 1364 |
+
},
|
| 1365 |
+
{
|
| 1366 |
+
"cell_type": "code",
|
| 1367 |
+
"metadata": {},
|
| 1368 |
+
"source": [
|
| 1369 |
+
"\n",
|
| 1370 |
+
"print(\"\\n\" + \"=\"*80)\n",
|
| 1371 |
+
"print(\"EXPERIMENT 7 COMPLETE: Composite Pipeline Tests\")\n",
|
| 1372 |
+
"print(\"=\"*80)\n",
|
| 1373 |
+
"scoreboard._print_table()\n",
|
| 1374 |
+
"scoreboard.save(\"scoreboard_exp7.json\")\n",
|
| 1375 |
+
""
|
| 1376 |
+
],
|
| 1377 |
+
"execution_count": null,
|
| 1378 |
+
"outputs": []
|
| 1379 |
+
}
|
| 1380 |
+
]
|
| 1381 |
+
}
|