l0-python 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {l0_python-0.5.0/l0_python.egg-info → l0_python-0.6.0}/PKG-INFO +1 -1
- {l0_python-0.5.0 → l0_python-0.6.0}/l0/calibration.py +46 -12
- {l0_python-0.5.0 → l0_python-0.6.0/l0_python.egg-info}/PKG-INFO +1 -1
- {l0_python-0.5.0 → l0_python-0.6.0}/pyproject.toml +1 -1
- {l0_python-0.5.0 → l0_python-0.6.0}/tests/test_calibration.py +221 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/LICENSE +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/README.md +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0/__init__.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0/distributions.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0/gates.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0/layers.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0/penalties.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0/sparse.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0_python.egg-info/SOURCES.txt +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0_python.egg-info/dependency_links.txt +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0_python.egg-info/requires.txt +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/l0_python.egg-info/top_level.txt +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/setup.cfg +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/tests/test_distributions.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/tests/test_gates.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/tests/test_layers.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/tests/test_penalties.py +0 -0
- {l0_python-0.5.0 → l0_python-0.6.0}/tests/test_sparse.py +0 -0
|
@@ -310,6 +310,8 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
310
310
|
verbose: bool = False,
|
|
311
311
|
verbose_freq: int = 100,
|
|
312
312
|
target_groups: np.ndarray | None = None,
|
|
313
|
+
normalize_groups: bool = True,
|
|
314
|
+
group_multipliers: dict[int, float] | None = None,
|
|
313
315
|
) -> "SparseCalibrationWeights":
|
|
314
316
|
"""
|
|
315
317
|
Fit calibration weights using gradient descent.
|
|
@@ -338,6 +340,12 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
338
340
|
Array of group IDs for each target. Targets in the same group
|
|
339
341
|
will be averaged together so each group contributes equally to loss.
|
|
340
342
|
If None, all targets are treated independently.
|
|
343
|
+
normalize_groups : bool
|
|
344
|
+
Whether to normalize within groups so each group contributes
|
|
345
|
+
equally to the loss. Default True (backward-compatible).
|
|
346
|
+
group_multipliers : dict[int, float], optional
|
|
347
|
+
Per-group loss scaling factors. Applied after normalization
|
|
348
|
+
(if enabled). Requires target_groups to be set.
|
|
341
349
|
|
|
342
350
|
Returns
|
|
343
351
|
-------
|
|
@@ -350,27 +358,53 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
350
358
|
# Convert M to torch sparse (will be cached)
|
|
351
359
|
M_torch = self._convert_sparse_to_torch(M)
|
|
352
360
|
|
|
361
|
+
# Validate group_multipliers
|
|
362
|
+
if group_multipliers is not None and target_groups is None:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
"group_multipliers requires target_groups to be set"
|
|
365
|
+
)
|
|
366
|
+
|
|
353
367
|
# Compute group weights for loss averaging
|
|
354
368
|
if target_groups is not None:
|
|
355
|
-
# Convert to tensor
|
|
356
369
|
target_groups = torch.tensor(
|
|
357
370
|
target_groups, dtype=torch.long, device=self.device
|
|
358
371
|
)
|
|
359
|
-
|
|
360
|
-
# Calculate group weights: 1 / group_size for each target
|
|
361
372
|
unique_groups = torch.unique(target_groups)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
373
|
+
|
|
374
|
+
if normalize_groups:
|
|
375
|
+
group_weights = torch.zeros_like(y)
|
|
376
|
+
for group_id in unique_groups:
|
|
377
|
+
group_mask = target_groups == group_id
|
|
378
|
+
group_size = group_mask.sum().item()
|
|
379
|
+
group_weights[group_mask] = 1.0 / group_size
|
|
380
|
+
else:
|
|
381
|
+
group_weights = torch.ones_like(y)
|
|
382
|
+
|
|
383
|
+
if group_multipliers is not None:
|
|
384
|
+
for gid, mult in group_multipliers.items():
|
|
385
|
+
group_mask = target_groups == gid
|
|
386
|
+
if not group_mask.any():
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"group_multipliers key {gid} not found "
|
|
389
|
+
f"in target_groups"
|
|
390
|
+
)
|
|
391
|
+
group_weights[group_mask] *= mult
|
|
370
392
|
else:
|
|
371
|
-
# No grouping - all targets weighted equally
|
|
372
393
|
group_weights = torch.ones_like(y)
|
|
373
394
|
|
|
395
|
+
if verbose:
|
|
396
|
+
if target_groups is not None:
|
|
397
|
+
n_groups = len(torch.unique(target_groups))
|
|
398
|
+
parts = [f"{n_groups} groups"]
|
|
399
|
+
parts.append(
|
|
400
|
+
f"normalize={'on' if normalize_groups else 'off'}"
|
|
401
|
+
)
|
|
402
|
+
if group_multipliers is not None:
|
|
403
|
+
parts.append(f"multipliers={group_multipliers}")
|
|
404
|
+
print(f"Groups: {', '.join(parts)}")
|
|
405
|
+
else:
|
|
406
|
+
print("Groups: none (uniform weights)")
|
|
407
|
+
|
|
374
408
|
# Add jitter to weights to break symmetry (if jitter_sd > 0)
|
|
375
409
|
if self.log_weight_jitter_sd > 0:
|
|
376
410
|
with torch.no_grad():
|
|
@@ -769,3 +769,224 @@ class TestSparseCalibrationWeights:
|
|
|
769
769
|
log_alphas = model_extreme.log_alpha.numpy()
|
|
770
770
|
# Should not have inf or -inf values
|
|
771
771
|
assert np.all(np.isfinite(log_alphas))
|
|
772
|
+
|
|
773
|
+
def test_group_multipliers_without_normalization(self):
|
|
774
|
+
"""Multipliers work with normalize_groups=False."""
|
|
775
|
+
N = 100
|
|
776
|
+
Q = 39 # 3 singletons + 18 + 18
|
|
777
|
+
|
|
778
|
+
np.random.seed(42)
|
|
779
|
+
torch.manual_seed(42)
|
|
780
|
+
|
|
781
|
+
M = sp.random(Q, N, density=0.3, format="csr")
|
|
782
|
+
y_singletons = np.array([1e9, 5e8, 2e9])
|
|
783
|
+
y_group1 = np.random.uniform(1e3, 1e6, size=18)
|
|
784
|
+
y_group2 = np.random.uniform(1e3, 1e6, size=18)
|
|
785
|
+
y = np.concatenate([y_singletons, y_group1, y_group2])
|
|
786
|
+
|
|
787
|
+
target_groups = np.array([0, 1, 2] + [3] * 18 + [4] * 18)
|
|
788
|
+
|
|
789
|
+
# Without multipliers (baseline)
|
|
790
|
+
model_base = SparseCalibrationWeights(n_features=N)
|
|
791
|
+
model_base.fit(
|
|
792
|
+
M,
|
|
793
|
+
y,
|
|
794
|
+
lambda_l0=0.0001,
|
|
795
|
+
lr=0.1,
|
|
796
|
+
epochs=500,
|
|
797
|
+
loss_type="relative",
|
|
798
|
+
verbose=False,
|
|
799
|
+
target_groups=target_groups,
|
|
800
|
+
normalize_groups=False,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
# With 10x multiplier on singletons
|
|
804
|
+
model_mult = SparseCalibrationWeights(n_features=N)
|
|
805
|
+
model_mult.fit(
|
|
806
|
+
M,
|
|
807
|
+
y,
|
|
808
|
+
lambda_l0=0.0001,
|
|
809
|
+
lr=0.1,
|
|
810
|
+
epochs=500,
|
|
811
|
+
loss_type="relative",
|
|
812
|
+
verbose=False,
|
|
813
|
+
target_groups=target_groups,
|
|
814
|
+
normalize_groups=False,
|
|
815
|
+
group_multipliers={0: 10.0, 1: 10.0, 2: 10.0},
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
with torch.no_grad():
|
|
819
|
+
y_pred_base = model_base.predict(M).cpu().numpy()
|
|
820
|
+
y_pred_mult = model_mult.predict(M).cpu().numpy()
|
|
821
|
+
|
|
822
|
+
# Singleton relative errors
|
|
823
|
+
sing_err_base = np.abs(
|
|
824
|
+
(y[:3] - y_pred_base[:3]) / (y[:3] + 1)
|
|
825
|
+
).mean()
|
|
826
|
+
sing_err_mult = np.abs(
|
|
827
|
+
(y[:3] - y_pred_mult[:3]) / (y[:3] + 1)
|
|
828
|
+
).mean()
|
|
829
|
+
|
|
830
|
+
assert sing_err_mult < sing_err_base * 1.1, (
|
|
831
|
+
f"10x multiplier should improve singleton fit: "
|
|
832
|
+
f"{sing_err_mult:.4f} vs {sing_err_base:.4f}"
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
def test_group_multipliers_with_normalization(self):
|
|
836
|
+
"""Multipliers compose with normalization."""
|
|
837
|
+
N = 100
|
|
838
|
+
Q = 39
|
|
839
|
+
|
|
840
|
+
np.random.seed(42)
|
|
841
|
+
torch.manual_seed(42)
|
|
842
|
+
|
|
843
|
+
M = sp.random(Q, N, density=0.3, format="csr")
|
|
844
|
+
y_singletons = np.array([1e9, 5e8, 2e9])
|
|
845
|
+
y_group1 = np.random.uniform(1e3, 1e6, size=18)
|
|
846
|
+
y_group2 = np.random.uniform(1e3, 1e6, size=18)
|
|
847
|
+
y = np.concatenate([y_singletons, y_group1, y_group2])
|
|
848
|
+
|
|
849
|
+
target_groups = np.array([0, 1, 2] + [3] * 18 + [4] * 18)
|
|
850
|
+
|
|
851
|
+
# With normalization only (baseline)
|
|
852
|
+
model_norm = SparseCalibrationWeights(n_features=N)
|
|
853
|
+
model_norm.fit(
|
|
854
|
+
M,
|
|
855
|
+
y,
|
|
856
|
+
lambda_l0=0.0001,
|
|
857
|
+
lr=0.1,
|
|
858
|
+
epochs=500,
|
|
859
|
+
loss_type="relative",
|
|
860
|
+
verbose=False,
|
|
861
|
+
target_groups=target_groups,
|
|
862
|
+
normalize_groups=True,
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
# With normalization + 5x multiplier on group 3
|
|
866
|
+
model_norm_mult = SparseCalibrationWeights(n_features=N)
|
|
867
|
+
model_norm_mult.fit(
|
|
868
|
+
M,
|
|
869
|
+
y,
|
|
870
|
+
lambda_l0=0.0001,
|
|
871
|
+
lr=0.1,
|
|
872
|
+
epochs=500,
|
|
873
|
+
loss_type="relative",
|
|
874
|
+
verbose=False,
|
|
875
|
+
target_groups=target_groups,
|
|
876
|
+
normalize_groups=True,
|
|
877
|
+
group_multipliers={3: 5.0},
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
with torch.no_grad():
|
|
881
|
+
y_pred_norm = model_norm.predict(M).cpu().numpy()
|
|
882
|
+
y_pred_mult = model_norm_mult.predict(M).cpu().numpy()
|
|
883
|
+
|
|
884
|
+
# Group 3 error should improve with multiplier
|
|
885
|
+
g3_err_norm = np.abs(
|
|
886
|
+
(y[3:21] - y_pred_norm[3:21]) / (y[3:21] + 1)
|
|
887
|
+
).mean()
|
|
888
|
+
g3_err_mult = np.abs(
|
|
889
|
+
(y[3:21] - y_pred_mult[3:21]) / (y[3:21] + 1)
|
|
890
|
+
).mean()
|
|
891
|
+
|
|
892
|
+
assert g3_err_mult < g3_err_norm * 1.1, (
|
|
893
|
+
f"5x multiplier should improve group 3 fit: "
|
|
894
|
+
f"{g3_err_mult:.4f} vs {g3_err_norm:.4f}"
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def test_group_multipliers_requires_target_groups(self):
|
|
898
|
+
"""ValueError when group_multipliers set but target_groups is None."""
|
|
899
|
+
N = 50
|
|
900
|
+
Q = 10
|
|
901
|
+
|
|
902
|
+
M = sp.random(Q, N, density=0.3, format="csr")
|
|
903
|
+
y = np.random.uniform(100, 1000, size=Q)
|
|
904
|
+
|
|
905
|
+
model = SparseCalibrationWeights(n_features=N)
|
|
906
|
+
with pytest.raises(
|
|
907
|
+
ValueError,
|
|
908
|
+
match="group_multipliers requires target_groups",
|
|
909
|
+
):
|
|
910
|
+
model.fit(
|
|
911
|
+
M,
|
|
912
|
+
y,
|
|
913
|
+
epochs=10,
|
|
914
|
+
verbose=False,
|
|
915
|
+
group_multipliers={0: 2.0},
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
def test_group_multipliers_invalid_group_id(self):
|
|
919
|
+
"""ValueError when multiplier key doesn't exist in target_groups."""
|
|
920
|
+
N = 50
|
|
921
|
+
Q = 10
|
|
922
|
+
|
|
923
|
+
M = sp.random(Q, N, density=0.3, format="csr")
|
|
924
|
+
y = np.random.uniform(100, 1000, size=Q)
|
|
925
|
+
target_groups = np.zeros(Q, dtype=int) # All group 0
|
|
926
|
+
|
|
927
|
+
model = SparseCalibrationWeights(n_features=N)
|
|
928
|
+
with pytest.raises(
|
|
929
|
+
ValueError,
|
|
930
|
+
match="group_multipliers key 99 not found",
|
|
931
|
+
):
|
|
932
|
+
model.fit(
|
|
933
|
+
M,
|
|
934
|
+
y,
|
|
935
|
+
epochs=10,
|
|
936
|
+
verbose=False,
|
|
937
|
+
target_groups=target_groups,
|
|
938
|
+
group_multipliers={99: 2.0},
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
def test_normalize_groups_false_uniform_weights(self):
|
|
942
|
+
"""normalize_groups=False with target_groups gives uniform weights."""
|
|
943
|
+
N = 50
|
|
944
|
+
Q = 10
|
|
945
|
+
|
|
946
|
+
np.random.seed(42)
|
|
947
|
+
torch.manual_seed(42)
|
|
948
|
+
|
|
949
|
+
M = sp.random(Q, N, density=0.3, format="csr")
|
|
950
|
+
y = np.random.uniform(100, 1000, size=Q)
|
|
951
|
+
|
|
952
|
+
# Without target_groups at all
|
|
953
|
+
model_none = SparseCalibrationWeights(n_features=N)
|
|
954
|
+
model_none.fit(
|
|
955
|
+
M,
|
|
956
|
+
y,
|
|
957
|
+
lambda_l0=0.0001,
|
|
958
|
+
lr=0.1,
|
|
959
|
+
epochs=500,
|
|
960
|
+
loss_type="relative",
|
|
961
|
+
verbose=False,
|
|
962
|
+
target_groups=None,
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
# With target_groups but normalize_groups=False (should be equivalent)
|
|
966
|
+
torch.manual_seed(42)
|
|
967
|
+
target_groups = np.array([0] * 5 + [1] * 5)
|
|
968
|
+
model_no_norm = SparseCalibrationWeights(n_features=N)
|
|
969
|
+
model_no_norm.fit(
|
|
970
|
+
M,
|
|
971
|
+
y,
|
|
972
|
+
lambda_l0=0.0001,
|
|
973
|
+
lr=0.1,
|
|
974
|
+
epochs=500,
|
|
975
|
+
loss_type="relative",
|
|
976
|
+
verbose=False,
|
|
977
|
+
target_groups=target_groups,
|
|
978
|
+
normalize_groups=False,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
with torch.no_grad():
|
|
982
|
+
y_pred_none = model_none.predict(M).cpu().numpy()
|
|
983
|
+
y_pred_no_norm = model_no_norm.predict(M).cpu().numpy()
|
|
984
|
+
|
|
985
|
+
err_none = np.abs((y - y_pred_none) / (y + 1)).mean()
|
|
986
|
+
err_no_norm = np.abs((y - y_pred_no_norm) / (y + 1)).mean()
|
|
987
|
+
|
|
988
|
+
# Both should achieve similar error levels
|
|
989
|
+
assert abs(err_none - err_no_norm) < 0.1, (
|
|
990
|
+
f"normalize_groups=False should behave like no groups: "
|
|
991
|
+
f"{err_none:.4f} vs {err_no_norm:.4f}"
|
|
992
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|