History-Aware Sampling Algorithm for robust deep learning under label noise
Project description
HASA — History-Aware Sampling Algorithm
A lightweight, pip-installable PyTorch library for robust deep learning training under noise.
HASA tracks each training sample's loss trajectory over a sliding window and uses loss variance as a noise indicator. Clean samples stabilise quickly (low variance); noisy/mislabelled samples oscillate (high variance). After a warm-up phase the algorithm masks out high-variance samples so that gradients are computed only from likely-clean data.
Installation
pip install hasa
Or install directly from GitHub:
pip install git+https://github.com/msc35/hasa-py.git
For development:
git clone https://github.com/msc35/hasa-py.git
cd hasa-py
pip install -e ".[dev]"
Quick Start
1. Wrap your dataset to return sample indices
HASA needs to map each loss value back to a specific dataset sample.
The simplest approach is an IndexedDataset wrapper:
from torch.utils.data import Dataset
class IndexedDataset(Dataset):
def __init__(self, dataset):
self.dataset = dataset
def __len__(self):
return len(self.dataset)
def __getitem__(self, idx):
x, y = self.dataset[idx]
return idx, x, y
2. Train with HASA selection
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from hasa import HASA
dataset = IndexedDataset(my_dataset)
loader = DataLoader(dataset, batch_size=128, shuffle=True)
model = MyModel()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss(reduction='none') # MUST be unreduced
selector = HASA(num_samples=len(dataset), window_size=15, select_ratio=0.8)
for epoch in range(150):
for indices, x, y in loader:
logits = model(x)
losses = criterion(logits, y)
mask = selector.step(indices, losses.detach())
# Divide by mask.sum() (not batch_size) to preserve gradient magnitude
loss = (losses * mask).sum() / mask.sum()
loss.backward()
optimizer.step()
optimizer.zero_grad()
selector.end_epoch()
3. (Optional) Langevin noise injection
Motivated by interpreting SGD as approximate Bayesian inference (Mandt, Hoffman & Blei, 2018):
selector = HASA(
num_samples=len(dataset),
window_size=15,
select_ratio=0.8,
langevin_noise=1e-4,
)
# Inside the training loop, after optimizer.step():
selector.inject_langevin_noise(model)
How It Works
- Warm-up phase (first
window_sizeepochs): all samples are used — the per-sample loss history buffer fills up. - Selection phase (after warm-up): for each batch, compute per-sample loss variance from the history buffer. Keep only the
select_ratiofraction with the lowest variance. High-variance samples (likely mislabelled) are masked out.
Warm-up (epochs 0..T-1) Selection (epochs T+)
┌─────────────────────┐ ┌──────────────────────────────┐
per-sample │ Record losses into │ │ Var(loss history) per sample│
losses ───>│ ring buffer, train │────>│ Keep lowest-variance k% │
│ on ALL samples │ │ Mask out the rest │
└─────────────────────┘ └──────────────────────────────┘
Hyperparameters
| Parameter | Meaning | Tested Range | Default |
|---|---|---|---|
num_samples |
Total dataset size (for buffer allocation) | — | required |
window_size |
Loss values stored per sample (T) | 5, 10, 15 | 15 |
select_ratio |
Fraction of batch to keep (k) | 0.5 – 0.9 | 0.8 |
langevin_noise |
Scale of injected Gaussian noise | 0 or small | 0.0 |
Checkpointing
HASA supports full state serialisation for resuming training:
state = selector.state_dict()
torch.save(state, "hasa_checkpoint.pt")
# Restore later
selector.load_state_dict(torch.load("hasa_checkpoint.pt"))
HASATrainer (convenience wrapper)
For a simpler API that handles the full training loop:
from hasa.callbacks import HASATrainer
trainer = HASATrainer(model, optimizer, criterion, selector, device="cuda")
for epoch in range(num_epochs):
metrics = trainer.train_epoch(dataloader)
print(f"Epoch {epoch}: loss={metrics['loss']:.4f}, selected={metrics['selected_frac']:.2%}")
Works with any PyTorch model
HASA is model-agnostic — it only looks at per-sample loss values. It works with any architecture (CNNs, Transformers, MLPs, etc.) and any loss function that supports reduction='none':
nn.CrossEntropyLoss(reduction='none')— classificationnn.MSELoss(reduction='none')— regressionnn.BCEWithLogitsLoss(reduction='none')— binary classification- Any custom loss returning per-sample values
API Reference
HASA(num_samples, window_size=15, select_ratio=0.8, langevin_noise=0.0, device="cpu")
step(sample_indices, losses) -> BoolTensor— update history, return selection mask.end_epoch()— advance the epoch counter. Must be called once per epoch.inject_langevin_noise(model)— add Gaussian noise to parameters.state_dict() / load_state_dict(d)— checkpoint support.epoch— current epoch (read-only property).in_warmup— True during the warm-up phase.
LossHistoryBuffer(num_samples, window_size, device)
update(indices, losses)— write losses into the ring buffer.variance(indices) -> Tensor— compute per-sample loss variance.is_ready(epoch) -> bool— True after warm-up.
hard_select(variances, select_ratio) -> BoolTensor
Returns a mask keeping the lowest select_ratio fraction of samples by variance.
Running Tests
pip install -e ".[dev]"
pytest --cov=hasa
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hasa-0.1.1.tar.gz.
File metadata
- Download URL: hasa-0.1.1.tar.gz
- Upload date:
- Size: 16.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6ccf36b941c746c33f03915d729225298ac09bcf120ca599ebae6f28b37afed
|
|
| MD5 |
895610c243043316ae6d73a76d8d66ac
|
|
| BLAKE2b-256 |
c19b1a9d09be34d84a6bdc5595a67b97f169d476be4e49f7f9b70d5241273969
|
Provenance
The following attestation bundles were made for hasa-0.1.1.tar.gz:
Publisher:
publish.yml on msc35/hasa-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hasa-0.1.1.tar.gz -
Subject digest:
a6ccf36b941c746c33f03915d729225298ac09bcf120ca599ebae6f28b37afed - Sigstore transparency entry: 1052786736
- Sigstore integration time:
-
Permalink:
msc35/hasa-py@4aab1b26dbe2ae0e11cff90e61f47a0409c93dca -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/msc35
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4aab1b26dbe2ae0e11cff90e61f47a0409c93dca -
Trigger Event:
release
-
Statement type:
File details
Details for the file hasa-0.1.1-py3-none-any.whl.
File metadata
- Download URL: hasa-0.1.1-py3-none-any.whl
- Upload date:
- Size: 12.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1589620ac0fe38cb9d079227f9936c3f97416fa16080b2d3a4361b1fe125ef50
|
|
| MD5 |
8ae0700b7beef722d14b1c82d5214e56
|
|
| BLAKE2b-256 |
f7ddadacb84103831560b7264a08fb028ad65972d7d2f8176d0f7f0b4ca587cd
|
Provenance
The following attestation bundles were made for hasa-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on msc35/hasa-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hasa-0.1.1-py3-none-any.whl -
Subject digest:
1589620ac0fe38cb9d079227f9936c3f97416fa16080b2d3a4361b1fe125ef50 - Sigstore transparency entry: 1052786746
- Sigstore integration time:
-
Permalink:
msc35/hasa-py@4aab1b26dbe2ae0e11cff90e61f47a0409c93dca -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/msc35
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4aab1b26dbe2ae0e11cff90e61f47a0409c93dca -
Trigger Event:
release
-
Statement type: