Research codebase for a thesis on synthetic data generation for Parkinson's disease (PD) detection, centered on a paired, boundary-focused sampling strategy: rather than accepting generator output uniformly, keep the synthetic samples a classifier finds most ambiguous (closest to a decision boundary), on the hypothesis that these carry more training value for a downstream classifier than uniformly-sampled synthetic data.
This first stage runs the full pipeline - data loading, diffusion-based
generation, boundary-focused sampling, classifier training, evaluation - on
MNIST via torchvision.datasets.MNIST.
MNIST is used only to validate that the pipeline mechanics work end-to-end. It is not a scarce dataset, so achieving good results on it is not evidence for the boundary-sampling hypothesis itself - that requires the real, scarce gait/pose data the thesis is ultimately about. Think of this stage as integration testing for the pipeline, not a scientific result.
A minimal Streamlit demo UI (app/streamlit_app.py) shows headline metrics
and lets a viewer compare real vs. synthetic samples, generating fresh digits
live from the trained (class-conditional) generator. Run it with
streamlit run app/streamlit_app.py.
src/pd_synth/
data/ Dataset loaders behind a common LabeledImageDataset
(image, label) interface: MNISTDataset, plus InMemoryDataset
for wrapping generated (or any other in-memory) samples.
generation/ Diffusion-based generators: a lightweight UNet2DModel +
DDPMScheduler (from the diffusers library), trained from
scratch - never from a pretrained checkpoint. Optionally
class-conditional, so samples can be generated by label.
sampling/ Boundary-focused sampling strategy - the core thesis
contribution. Deliberately independent of any specific
generator/classifier implementation so it stays
unit-testable in isolation.
classifiers/ A small, dataset-agnostic CNN plus a train/eval loop.
evaluation/ Synthetic-data quality metrics (mean image difference,
a diagonal Frechet distance) and classifier metrics
(confusion matrix, precision/recall/F1).
utils/ Seeding (`set_seed`), config loading/saving, and a small
image-grid viz helper shared by experiments/ and app/.
configs/ One YAML per experiment (e.g. mnist_baseline.yaml).
experiments/ Entry-point scripts, one per pipeline stage:
train_generator.py, train_classifier.py,
train_classifier_on_synthetic.py, evaluate.py
tests/ pytest suite. Fixtures in conftest.py generate tiny
synthetic (image, label) data and write it to disk, so
tests never need network access or the real MNIST download.
app/ Minimal Streamlit demo (streamlit_app.py) that loads a
trained run's checkpoints and lets a viewer compare real vs.
synthetic samples and headline metrics. See app/README.md.
- Config-driven runs. No hyperparameter or filesystem path is
hardcoded; everything comes from a YAML file in
configs/, loaded viapd_synth.utils.load_config. - Every run is seeded and logged. Each experiment script calls
set_seed(config["seed"])first, thensave_config(config, ...)writes the exact config used next to that run's outputs inoutputs/<run>/, so any result can be traced back to what produced it. - Dataset-agnostic core.
data/,generation/,sampling/,classifiers/, andevaluation/are all written against theLabeledImageDatasetinterface, not against MNIST specifically. - Every module has a smoke test, and
sampling/(the core contribution) has focused unit tests independent of any real model. - ruff-clean, with type hints and docstrings throughout.
pip install -e ".[dev]"
pytest -q
ruff check .
python experiments/train_generator.py --config configs/mnist_baseline.yaml
python experiments/train_classifier.py --config configs/mnist_baseline.yaml
python experiments/train_classifier_on_synthetic.py --config configs/mnist_baseline.yaml
python experiments/evaluate.py --config configs/mnist_baseline.yaml
streamlit run app/streamlit_app.pyResults land in outputs/mnist_baseline/ (git-ignored), including the
generator checkpoint, classifier checkpoints (real-data and synthetic-data),
sample images, and an evaluation_report.yaml with quality and classifier
metrics - each stage's config is saved alongside its outputs.
- Implement a new class in
src/pd_synth/data/that subclassesLabeledImageDatasetand returns(image, label)tensors for the real gait/pose data (e.g.GaitPoseDataset). - Register it in
src/pd_synth/data/__init__.py's_REGISTRY. - Copy
configs/mnist_baseline.yamlto a new file (e.g.configs/gait_pose_baseline.yaml), pointdata.name/data.rootat the new dataset, and adjustgenerator.image_size/classifier.image_size/classifier.num_classesto match. - Run the same
experiments/scripts with--configpointing at the new file - nothing ingeneration/,sampling/,classifiers/, orevaluation/needs to change. Pointapp/streamlit_app.pyat it viaPD_SYNTH_CONFIG=configs/gait_pose_baseline.yaml streamlit run app/streamlit_app.py.
Real data goes under data/ (git-ignored) and is never checked in.