shima78/pd-synth

★ 0Forks 0PythonGitHub ↗Compare

README

pd-synth

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.

Current stage: MNIST pipeline validation

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.

Structure

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.

Design practices

  • Config-driven runs. No hyperparameter or filesystem path is hardcoded; everything comes from a YAML file in configs/, loaded via pd_synth.utils.load_config.
  • Every run is seeded and logged. Each experiment script calls set_seed(config["seed"]) first, then save_config(config, ...) writes the exact config used next to that run's outputs in outputs/<run>/, so any result can be traced back to what produced it.
  • Dataset-agnostic core. data/, generation/, sampling/, classifiers/, and evaluation/ are all written against the LabeledImageDataset interface, 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.

Getting started

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.py

Results 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.

Swapping in a real dataset later

  1. Implement a new class in src/pd_synth/data/ that subclasses LabeledImageDataset and returns (image, label) tensors for the real gait/pose data (e.g. GaitPoseDataset).
  2. Register it in src/pd_synth/data/__init__.py's _REGISTRY.
  3. Copy configs/mnist_baseline.yaml to a new file (e.g. configs/gait_pose_baseline.yaml), point data.name / data.root at the new dataset, and adjust generator.image_size / classifier.image_size / classifier.num_classes to match.
  4. Run the same experiments/ scripts with --config pointing at the new file - nothing in generation/, sampling/, classifiers/, or evaluation/ needs to change. Point app/streamlit_app.py at it via PD_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.

Contributors

shima78

Issues