From 024b0ead1b0839d3591bad3bdd3a6ab6bd20c381 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Thu, 27 Aug 2026 12:10:55 +0200 Subject: Migrate to uv and Python 3.14 and add MNIST downloader to make the project runnable 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. --- MultiLayerPerceptron/__init__.py | 2 +- MultiLayerPerceptron/download.py | 109 +++++++++++++++++++++++++++++++++++++++ MultiLayerPerceptron/main.py | 17 +++--- 3 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 MultiLayerPerceptron/download.py (limited to 'MultiLayerPerceptron') diff --git a/MultiLayerPerceptron/__init__.py b/MultiLayerPerceptron/__init__.py index 0b2f4ea..63c9c50 100644 --- a/MultiLayerPerceptron/__init__.py +++ b/MultiLayerPerceptron/__init__.py @@ -1 +1 @@ -name="multilayer-perceptron" \ No newline at end of file +name="multilayer-perceptron" 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() diff --git a/MultiLayerPerceptron/main.py b/MultiLayerPerceptron/main.py index 2dbfa3d..d7a3138 100644 --- a/MultiLayerPerceptron/main.py +++ b/MultiLayerPerceptron/main.py @@ -49,9 +49,8 @@ class Layer(): self.grad_weights = np.outer(self.grad_inter, input) class MultiLayerPerceptron(): - layers = [] - def __init__(self, layer_sizes): + self.layers = [] for k in range(len(layer_sizes)-1): self.layers.append(Layer(layer_sizes[k], layer_sizes[k+1])) @@ -60,7 +59,8 @@ class MultiLayerPerceptron(): @staticmethod def load(filename): - array = np.load(open(filename, 'rb')) + with open(filename, 'rb') as f: + array = np.load(f, allow_pickle=True) sizes = [array[0].shape[1]] for layer in array: sizes.append(layer.shape[0]) @@ -70,8 +70,9 @@ class MultiLayerPerceptron(): return mlp def save(self, filename): - array = [layer.weights for layer in self.layers] - np.save(open(filename, 'wb'), array) + array = np.array([layer.weights for layer in self.layers], dtype=object) + with open(filename, 'wb') as f: + np.save(f, array, allow_pickle=True) def error(self, output, expected): return 1/2 * np.sum((expected - output)**2) @@ -171,10 +172,12 @@ if __name__ == "__main__": mlp = MultiLayerPerceptron.load('mnist.npy') print("OK") print("[Loading] Training set: ", end="") - space = np.load(open("data/train.npy", "rb")) + with open("data/train.npy", "rb") as f: + space = np.load(f, allow_pickle=True) space = [(input.reshape(28*28), expected) for input, expected in space] print("OK Testing set: ", end="") - test_space = np.load(open("data/test.npy", "rb")) + with open("data/test.npy", "rb") as f: + test_space = np.load(f, allow_pickle=True) test_space = [(input.reshape(28*28), expected) for input, expected in test_space] print("OK") -- cgit v1.3