torch-l1-snr 0.1.1__tar.gz → 0.1.2__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.
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/PKG-INFO +2 -2
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/README.md +1 -1
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/tests/test_losses.py +121 -1
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/torch_l1_snr/__init__.py +1 -1
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/torch_l1_snr/l1snr.py +9 -7
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/torch_l1_snr.egg-info/PKG-INFO +2 -2
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/LICENSE +0 -0
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/pyproject.toml +0 -0
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/setup.cfg +0 -0
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/torch_l1_snr.egg-info/SOURCES.txt +0 -0
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/torch_l1_snr.egg-info/dependency_links.txt +0 -0
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/torch_l1_snr.egg-info/requires.txt +0 -0
- {torch_l1_snr-0.1.1 → torch_l1_snr-0.1.2}/torch_l1_snr.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: torch-l1-snr
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: L1-SNR loss functions for audio source separation in PyTorch
|
|
5
5
|
Home-page: https://github.com/crlandsc/torch-l1-snr
|
|
6
6
|
Author: Christopher Landschoot
|
|
@@ -237,7 +237,7 @@ While this can potentially reduce the "cleanliness" of separations and slightly
|
|
|
237
237
|
|
|
238
238
|
The implementation is optimized for efficiency: if `l1_weight` is `0.0` or `1.0`, the unused loss component is not computed, saving computational resources.
|
|
239
239
|
|
|
240
|
-
**Note on Gradient Balancing:** When blending losses (`0.0 < l1_weight < 1.0`), the implementation automatically scales the L1 component to approximately match
|
|
240
|
+
**Note on Gradient Balancing:** When blending losses (`0.0 < l1_weight < 1.0`), the implementation automatically scales the L1 component to approximately match gradient magnitudes while preserving distinct gradient behaviors. This helps maintain stable training without manual tuning.
|
|
241
241
|
|
|
242
242
|
## Limitations
|
|
243
243
|
|
|
@@ -209,7 +209,7 @@ While this can potentially reduce the "cleanliness" of separations and slightly
|
|
|
209
209
|
|
|
210
210
|
The implementation is optimized for efficiency: if `l1_weight` is `0.0` or `1.0`, the unused loss component is not computed, saving computational resources.
|
|
211
211
|
|
|
212
|
-
**Note on Gradient Balancing:** When blending losses (`0.0 < l1_weight < 1.0`), the implementation automatically scales the L1 component to approximately match
|
|
212
|
+
**Note on Gradient Balancing:** When blending losses (`0.0 < l1_weight < 1.0`), the implementation automatically scales the L1 component to approximately match gradient magnitudes while preserving distinct gradient behaviors. This helps maintain stable training without manual tuning.
|
|
213
213
|
|
|
214
214
|
## Limitations
|
|
215
215
|
|
|
@@ -542,4 +542,124 @@ def test_multi_wrapper_short_audio_4d():
|
|
|
542
542
|
|
|
543
543
|
assert torch.allclose(wrapped_result, direct_result, atol=1e-6)
|
|
544
544
|
assert wrapped_result.ndim == 0
|
|
545
|
-
assert not torch.isnan(wrapped_result) and not torch.isinf(wrapped_result)
|
|
545
|
+
assert not torch.isnan(wrapped_result) and not torch.isinf(wrapped_result)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
# --- Gradient Behavior Tests ---
|
|
549
|
+
def test_gradient_distinction_l1snr_vs_l1():
|
|
550
|
+
"""
|
|
551
|
+
Verify L1SNR and L1 have distinct gradient behaviors.
|
|
552
|
+
L1SNR: inverse-error scaling (larger updates for small errors)
|
|
553
|
+
L1: uniform gradients regardless of error magnitude
|
|
554
|
+
"""
|
|
555
|
+
torch.manual_seed(42)
|
|
556
|
+
|
|
557
|
+
actuals = torch.tensor([[1.0] * 100, [1.0] * 100])
|
|
558
|
+
estimates = actuals.clone()
|
|
559
|
+
estimates[0] += 0.01 # small error
|
|
560
|
+
estimates[1] += 0.5 # large error
|
|
561
|
+
|
|
562
|
+
# Pure L1SNR (l1_weight=0)
|
|
563
|
+
est_snr = estimates.clone().requires_grad_(True)
|
|
564
|
+
loss_snr = L1SNRLoss("test", l1_weight=0.0)(est_snr, actuals)
|
|
565
|
+
loss_snr.backward()
|
|
566
|
+
ratio_snr = est_snr.grad[0].abs().mean() / est_snr.grad[1].abs().mean()
|
|
567
|
+
|
|
568
|
+
# Pure L1 (l1_weight=1)
|
|
569
|
+
est_l1 = estimates.clone().requires_grad_(True)
|
|
570
|
+
loss_l1 = L1SNRLoss("test", l1_weight=1.0)(est_l1, actuals)
|
|
571
|
+
loss_l1.backward()
|
|
572
|
+
ratio_l1 = est_l1.grad[0].abs().mean() / est_l1.grad[1].abs().mean()
|
|
573
|
+
|
|
574
|
+
# L1SNR: larger gradient for small error sample (ratio >> 1)
|
|
575
|
+
assert ratio_snr > 10.0, f"L1SNR gradient ratio should be >> 1, got {ratio_snr}"
|
|
576
|
+
# L1: uniform gradients (ratio ~ 1)
|
|
577
|
+
assert 0.9 < ratio_l1 < 1.1, f"L1 gradient ratio should be ~1, got {ratio_l1}"
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def test_l1_weight_interpolation():
|
|
581
|
+
"""
|
|
582
|
+
Verify l1_weight actually affects gradient behavior.
|
|
583
|
+
Gradient ratio should decrease as l1_weight increases (from inverse-error toward uniform).
|
|
584
|
+
"""
|
|
585
|
+
torch.manual_seed(42)
|
|
586
|
+
|
|
587
|
+
actuals = torch.tensor([[1.0] * 100, [1.0] * 100])
|
|
588
|
+
estimates = actuals.clone()
|
|
589
|
+
estimates[0] += 0.01 # small error
|
|
590
|
+
estimates[1] += 0.5 # large error
|
|
591
|
+
|
|
592
|
+
ratios = []
|
|
593
|
+
for w in [0.0, 0.5, 1.0]:
|
|
594
|
+
est = estimates.clone().requires_grad_(True)
|
|
595
|
+
loss = L1SNRLoss("test", l1_weight=w)(est, actuals)
|
|
596
|
+
loss.backward()
|
|
597
|
+
ratio = (est.grad[0].abs().mean() / est.grad[1].abs().mean()).item()
|
|
598
|
+
ratios.append(ratio)
|
|
599
|
+
|
|
600
|
+
# Gradient ratio should monotonically decrease as l1_weight increases
|
|
601
|
+
assert ratios[0] > ratios[1] > ratios[2], \
|
|
602
|
+
f"Gradient ratios should decrease with l1_weight: {ratios}"
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def test_stft_gradient_distinction():
|
|
606
|
+
"""
|
|
607
|
+
Same gradient distinction test for STFTL1SNRDBLoss.
|
|
608
|
+
"""
|
|
609
|
+
torch.manual_seed(42)
|
|
610
|
+
|
|
611
|
+
# Need longer audio for STFT
|
|
612
|
+
actuals = torch.tensor([[1.0] * 4096, [1.0] * 4096])
|
|
613
|
+
estimates = actuals.clone()
|
|
614
|
+
estimates[0] += 0.01 # small error
|
|
615
|
+
estimates[1] += 0.5 # large error
|
|
616
|
+
|
|
617
|
+
# Pure L1SNR (l1_weight=0)
|
|
618
|
+
est_snr = estimates.clone().requires_grad_(True)
|
|
619
|
+
loss_fn_snr = STFTL1SNRDBLoss("test", l1_weight=0.0, n_ffts=[512], hop_lengths=[128], win_lengths=[512])
|
|
620
|
+
loss_snr = loss_fn_snr(est_snr, actuals)
|
|
621
|
+
loss_snr.backward()
|
|
622
|
+
ratio_snr = est_snr.grad[0].abs().mean() / est_snr.grad[1].abs().mean()
|
|
623
|
+
|
|
624
|
+
# Pure L1 (l1_weight=1)
|
|
625
|
+
est_l1 = estimates.clone().requires_grad_(True)
|
|
626
|
+
loss_fn_l1 = STFTL1SNRDBLoss("test", l1_weight=1.0, n_ffts=[512], hop_lengths=[128], win_lengths=[512])
|
|
627
|
+
loss_l1 = loss_fn_l1(est_l1, actuals)
|
|
628
|
+
loss_l1.backward()
|
|
629
|
+
ratio_l1 = est_l1.grad[0].abs().mean() / est_l1.grad[1].abs().mean()
|
|
630
|
+
|
|
631
|
+
# STFT processing smooths out per-sample differences, so ratios are smaller
|
|
632
|
+
# Key check: L1SNR ratio > L1 ratio (gradient behaviors differ)
|
|
633
|
+
assert ratio_snr > ratio_l1, f"STFT L1SNR ratio ({ratio_snr}) should be > L1 ratio ({ratio_l1})"
|
|
634
|
+
# L1: more uniform gradients (ratio closer to 1)
|
|
635
|
+
assert ratio_l1 < ratio_snr, f"STFT L1 should have more uniform gradients"
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def test_stft_l1_weight_interpolation():
|
|
639
|
+
"""
|
|
640
|
+
Verify l1_weight interpolation works for STFTL1SNRDBLoss.
|
|
641
|
+
L1 weighting should make gradients more uniform compared to pure SNR.
|
|
642
|
+
"""
|
|
643
|
+
torch.manual_seed(42)
|
|
644
|
+
|
|
645
|
+
actuals = torch.tensor([[1.0] * 4096, [1.0] * 4096])
|
|
646
|
+
estimates = actuals.clone()
|
|
647
|
+
estimates[0] += 0.01
|
|
648
|
+
estimates[1] += 0.5
|
|
649
|
+
|
|
650
|
+
ratios = []
|
|
651
|
+
for w in [0.0, 0.5, 1.0]:
|
|
652
|
+
est = estimates.clone().requires_grad_(True)
|
|
653
|
+
loss_fn = STFTL1SNRDBLoss("test", l1_weight=w, n_ffts=[512], hop_lengths=[128], win_lengths=[512])
|
|
654
|
+
loss = loss_fn(est, actuals)
|
|
655
|
+
loss.backward()
|
|
656
|
+
ratio = (est.grad[0].abs().mean() / est.grad[1].abs().mean()).item()
|
|
657
|
+
ratios.append(ratio)
|
|
658
|
+
|
|
659
|
+
# L1 weighting should make gradients more uniform (ratio closer to 1)
|
|
660
|
+
# Pure L1 (l1_weight=1.0) should have more uniform gradients than pure SNR (l1_weight=0.0)
|
|
661
|
+
assert ratios[2] < ratios[0], \
|
|
662
|
+
f"L1 weighting should make gradients more uniform: SNR ratio {ratios[0]} should be > L1 ratio {ratios[2]}"
|
|
663
|
+
|
|
664
|
+
# All ratios should be > 1 (signal with larger error should have larger gradients)
|
|
665
|
+
assert all(r > 1.0 for r in ratios), f"All gradient ratios should be > 1.0: {ratios}"
|
|
@@ -104,9 +104,10 @@ class L1SNRLoss(torch.nn.Module):
|
|
|
104
104
|
l1snr_loss = torch.mean(d1)
|
|
105
105
|
|
|
106
106
|
c = 10.0 / math.log(10.0)
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
|
|
107
|
+
# Scale by reference signal magnitude (not error) to preserve gradient distinction
|
|
108
|
+
# L1SNR has inverse-error gradients; L1 should have uniform gradients
|
|
109
|
+
inv_ref_mean = torch.mean(1.0 / (l1_true.detach() + self.eps))
|
|
110
|
+
scale_time = c * inv_ref_mean
|
|
110
111
|
l1_term = torch.mean(l1_error) * scale_time
|
|
111
112
|
|
|
112
113
|
loss = (1.0 - w) * l1snr_loss + w * l1_term
|
|
@@ -446,10 +447,11 @@ class STFTL1SNRDBLoss(torch.nn.Module):
|
|
|
446
447
|
w = float(self.l1_weight)
|
|
447
448
|
if 0.0 < w < 1.0:
|
|
448
449
|
c = 10.0 / math.log(10.0)
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
450
|
+
# Scale by reference signal magnitude (not error) to preserve gradient distinction
|
|
451
|
+
# L1SNR has inverse-error gradients; L1 should have uniform gradients
|
|
452
|
+
inv_ref_mean_comp = torch.mean(0.5 * (1.0 / (ref_re.detach() + self.l1snr_eps) +
|
|
453
|
+
1.0 / (ref_im.detach() + self.l1snr_eps)))
|
|
454
|
+
scale_spec = 2.0 * c * inv_ref_mean_comp
|
|
453
455
|
l1_term = 0.5 * (torch.mean(err_re) + torch.mean(err_im)) * scale_spec
|
|
454
456
|
|
|
455
457
|
loss = (1.0 - w) * d1_sum + w * l1_term
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: torch-l1-snr
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: L1-SNR loss functions for audio source separation in PyTorch
|
|
5
5
|
Home-page: https://github.com/crlandsc/torch-l1-snr
|
|
6
6
|
Author: Christopher Landschoot
|
|
@@ -237,7 +237,7 @@ While this can potentially reduce the "cleanliness" of separations and slightly
|
|
|
237
237
|
|
|
238
238
|
The implementation is optimized for efficiency: if `l1_weight` is `0.0` or `1.0`, the unused loss component is not computed, saving computational resources.
|
|
239
239
|
|
|
240
|
-
**Note on Gradient Balancing:** When blending losses (`0.0 < l1_weight < 1.0`), the implementation automatically scales the L1 component to approximately match
|
|
240
|
+
**Note on Gradient Balancing:** When blending losses (`0.0 < l1_weight < 1.0`), the implementation automatically scales the L1 component to approximately match gradient magnitudes while preserving distinct gradient behaviors. This helps maintain stable training without manual tuning.
|
|
241
241
|
|
|
242
242
|
## Limitations
|
|
243
243
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|