aboutsummaryrefslogtreecommitdiff
path: root/MultiLayerPerceptron/download.py
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 12:10:55 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 12:10:55 +0200
commit024b0ead1b0839d3591bad3bdd3a6ab6bd20c381 (patch)
treea13ec8234cdbe089b136d52ddd4d2f2fc5163d17 /MultiLayerPerceptron/download.py
parentaad7043d2f4ea303c30e9490554d66b75fb58f14 (diff)
Migrate to uv and Python 3.14 and add MNIST downloader to make the project runnableHEADmaster
Context / Problem: setup.py + requirements.txt (Python 3.5) no longer install on modern Python, matplotlib missing from deps, main.py save/load broke on numpy 2.x (inhomogeneous array / allow_pickle), and demo required manual mnist.npy/data files. What: replace setup.py/requirements.txt with pyproject.toml (hatchling, PEP 621, requires-python >=3.10, classifiers 3.10-3.14), add .python-version 3.14 and uv.lock, add MultiLayerPerceptron/download.py (mlp-download) to fetch MNIST from storage.googleapis and generate data/train.npy, data/test.npy, mnist.npy, fix MultiLayerPerceptron/main.py layers class var and pickle handling with minimal changes, ignore .venv and track uv.lock, update README with uv workflow. QA: verified uv sync --all-groups, uv run mlp-download, uv run python -m MultiLayerPerceptron.main loads and trains, ruff check passes.
Diffstat (limited to 'MultiLayerPerceptron/download.py')
-rw-r--r--MultiLayerPerceptron/download.py109
1 files changed, 109 insertions, 0 deletions
diff --git a/MultiLayerPerceptron/download.py b/MultiLayerPerceptron/download.py
new file mode 100644
index 0000000..6a0e23a
--- /dev/null
+++ b/MultiLayerPerceptron/download.py
@@ -0,0 +1,109 @@
+"""Download MNIST and prepare .npy files expected by main.py."""
+
+import argparse
+import gzip
+import shutil
+import urllib.request
+from pathlib import Path
+
+import idx2numpy
+import numpy as np
+
+BASE_URL = "https://storage.googleapis.com/cvdf-datasets/mnist"
+FILES = {
+ "train-images-idx3-ubyte.gz": f"{BASE_URL}/train-images-idx3-ubyte.gz",
+ "train-labels-idx1-ubyte.gz": f"{BASE_URL}/train-labels-idx1-ubyte.gz",
+ "t10k-images-idx3-ubyte.gz": f"{BASE_URL}/t10k-images-idx3-ubyte.gz",
+ "t10k-labels-idx1-ubyte.gz": f"{BASE_URL}/t10k-labels-idx1-ubyte.gz",
+}
+
+
+def download(url: str, dest: Path) -> None:
+ if dest.exists():
+ print(f"[skip] {dest} already exists")
+ return
+ dest.parent.mkdir(parents=True, exist_ok=True)
+ print(f"[download] {url} -> {dest}", end="", flush=True)
+ urllib.request.urlretrieve(url, dest)
+ print(" OK")
+
+
+def gunzip(src: Path, dst: Path) -> None:
+ if dst.exists():
+ print(f"[skip] {dst} already exists")
+ return
+ print(f"[gunzip] {src} -> {dst}", end="", flush=True)
+ with gzip.open(src, "rb") as f_in, open(dst, "wb") as f_out:
+ shutil.copyfileobj(f_in, f_out)
+ print(" OK")
+
+
+def convert_dataset(images_path: Path, labels_path: Path, out_path: Path) -> None:
+ print(f"[convert] {images_path.name} + {labels_path.name} -> {out_path}")
+ images = idx2numpy.convert_from_file(str(images_path)) / 255.0
+ labels = idx2numpy.convert_from_file(str(labels_path))
+ new_labels = np.zeros((len(labels), 10))
+ new_labels[np.arange(len(labels)), labels] = 1
+ dataset = list(zip(images, new_labels, strict=False))
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ with open(out_path, "wb") as f:
+ np.save(f, np.array(dataset, dtype=object))
+ print(f" saved {len(dataset)} samples to {out_path}")
+
+
+def ensure_mnist_weights(path: Path, layer_sizes=(784, 32, 16, 10)) -> None:
+ if path.exists():
+ print(f"[skip] {path} already exists")
+ return
+ print(f"[init] creating {path} with layers {layer_sizes}")
+ from MultiLayerPerceptron.main import MultiLayerPerceptron
+
+ mlp = MultiLayerPerceptron(list(layer_sizes))
+ mlp.save(str(path))
+ print(" OK")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Download MNIST and prepare npy files")
+ parser.add_argument("--raw-dir", default="data/raw", help="directory for raw idx files")
+ parser.add_argument("--force", action="store_true", help="re-download even if exists")
+ args = parser.parse_args()
+
+ raw_dir = Path(args.raw_dir)
+ if args.force:
+ for name in FILES:
+ p = raw_dir / name
+ if p.exists():
+ p.unlink()
+ ungz = raw_dir / name.replace(".gz", "")
+ if ungz.exists():
+ ungz.unlink()
+
+ # 1. download
+ for name, url in FILES.items():
+ download(url, raw_dir / name)
+
+ # 2. gunzip
+ for name in FILES:
+ gunzip(raw_dir / name, raw_dir / name.replace(".gz", ""))
+
+ # 3. convert to npy expected by main.py
+ convert_dataset(
+ raw_dir / "train-images-idx3-ubyte",
+ raw_dir / "train-labels-idx1-ubyte",
+ Path("data/train.npy"),
+ )
+ convert_dataset(
+ raw_dir / "t10k-images-idx3-ubyte",
+ raw_dir / "t10k-labels-idx1-ubyte",
+ Path("data/test.npy"),
+ )
+
+ # 4. ensure initial weights
+ ensure_mnist_weights(Path("mnist.npy"))
+
+ print("[done] all files ready")
+
+
+if __name__ == "__main__":
+ main()