From 48ebaba69f581042c0caf19b7807ae5c07bc97d9 Mon Sep 17 00:00:00 2001 From: Nathan Painchaud Date: Tue, 25 Mar 2025 16:09:30 +0100 Subject: [PATCH 1/6] Update data download instructions to be run automatically in notebooks --- README.md | 7 ++++++- requirements.txt | 9 +++++---- tutorials/cardiac-mri-autoencoders.ipynb | 7 ++----- tutorials/mnist-autoencoders.ipynb | 7 ++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d157659..495621e 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,13 @@ When you've launched JupyterLab's web interface, you can simply navigate to any - [Basic Variational Autoencoders](tutorials/mnist-autoencoders.ipynb) - [Variational Autoencoders Applied to Cardiac MRI](tutorials/cardiac-mri-autoencoders.ipynb) -You may download the MNIST and ACDC datasets [here](https://drive.google.com/file/d/1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S/view?usp=sharing). Once downloaded, you may untar the file and copy the **data/** folder in the root of your code, at the same level than the **src/** and the **tutorials/** folders. +The datasets used in this tutorial (MNIST and ACDC) will automatically be downloaded at the beginning of the notebooks. +However, if you experience issues with the download and want to download the datasets, you can find them [here](https://drive.google.com/file/d/1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S/view?usp=sharing) or download it with the following command: +```shell script +gdown https://drive.google.com/uc?id=1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S -c -O data.tar.gz ``` +Once downloaded, you may untar the file and and copy the `data/` folder at the root of the project, i.e. at the same level as the `src/` and the `tutorials/` folders. +```shell script tar -xvzf data.tar.gz ``` diff --git a/requirements.txt b/requirements.txt index 0c890e2..847c65f 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ # Base requirements +gdown +holoviews[recommended] jupyterlab jupyter +matplotlib +numpy torch -torchvision torchinfo +torchvision tqdm -numpy -matplotlib -holoviews[recommended] umap-learn # cardiac-mri-autoencoders diff --git a/tutorials/cardiac-mri-autoencoders.ipynb b/tutorials/cardiac-mri-autoencoders.ipynb index 821e158..236e061 100644 --- a/tutorials/cardiac-mri-autoencoders.ipynb +++ b/tutorials/cardiac-mri-autoencoders.ipynb @@ -37,11 +37,8 @@ "%%capture data_download\n", "\n", "# Make sure the data is downloaded and extracted where it should be\n", - "# The data (file data.tar.gz) can be downloaded by clicking on the following link : https://drive.google.com/file/d/1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S/view?usp=sharing \n", - "# On saturn cloud, click on the upload icon to upload data.tar.gz. You may then untar the file by typing the following command:\n", - "#\n", - "# tar -xvzf data.tar.gz\n", - "#" + "!gdown https://drive.google.com/uc?id=1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S -c -O ../data.tar.gz\n", + "!tar -xvzf ../data.tar.gz -C ../" ] }, { diff --git a/tutorials/mnist-autoencoders.ipynb b/tutorials/mnist-autoencoders.ipynb index 24fa92a..95d6c03 100644 --- a/tutorials/mnist-autoencoders.ipynb +++ b/tutorials/mnist-autoencoders.ipynb @@ -39,11 +39,8 @@ "%%capture data_download\n", "\n", "# Make sure the data is downloaded and extracted where it should be\n", - "# The data (file data.tar.gz) can be downloaded by clicking on the following link : https://drive.google.com/file/d/1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S/view?usp=sharing \n", - "# On saturn cloud, click on the upload icon to upload data.tar.gz. You may then untar the file by typing the following command:\n", - "#\n", - "# tar -xvzf data.tar.gz\n", - "#" + "!gdown https://drive.google.com/uc?id=1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S -c -O ../data.tar.gz\n", + "!tar -xvzf ../data.tar.gz -C ../" ] }, { From f044c00a993a36050f18d6de124e91c3f0eb3c80 Mon Sep 17 00:00:00 2001 From: Nathan Painchaud Date: Tue, 25 Mar 2025 16:09:50 +0100 Subject: [PATCH 2/6] Git ignore downloaded/extracted data --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index b2b8e2d..4509b0b 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,7 @@ venv.bak/ # IDEs .idea + +# Project data +data.tar.gz +/data/ From 261d1d5af03e0e341e7b5ddc1980aa04725567cd Mon Sep 17 00:00:00 2001 From: Nathan Painchaud Date: Tue, 25 Mar 2025 16:37:52 +0100 Subject: [PATCH 3/6] Migrate linting/formatting to Ruff + update other pre-commit hooks --- .pre-commit-config.yaml | 51 ++++++++++++--------- README.md | 19 ++++++-- dev.txt | 4 +- pyproject.toml | 10 ++-- setup.cfg | 16 ------- tutorials/cardiac-mri-autoencoders.ipynb | 58 ++++++++++++++---------- tutorials/mnist-autoencoders.ipynb | 53 +++++++++++----------- 7 files changed, 109 insertions(+), 102 deletions(-) delete mode 100644 setup.cfg diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1e4e10..03ed174 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,36 +3,43 @@ default_language_version: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - - id: check-added-large-files - args: [ --maxkb=15000 ] - id: trailing-whitespace - id: end-of-file-fixer + - id: check-docstring-first - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: check-executables-have-shebangs - id: check-toml + - id: check-case-conflict + - id: check-added-large-files + args: [ --maxkb=15000 ] - - repo: https://github.com/timothycrosley/isort - rev: 5.12.0 - hooks: - - id: isort - types: [ python ] - - - repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - types: [ python ] - - - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + # python linting and code formatting + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.0 hooks: - - id: flake8 - args: [--config, ./setup.cfg] - types: [ python ] + - id: ruff + args: [--fix] + - id: ruff-format + # jupyter notebook cell output clearing - repo: https://github.com/kynan/nbstripout - rev: 0.6.1 + rev: 0.8.1 hooks: - id: nbstripout - files: '.ipynb' + + # md formatting + - repo: https://github.com/hukkin/mdformat + rev: 0.7.22 + hooks: + - id: mdformat + args: ["--number"] + additional_dependencies: + - mdformat-gfm + - mdformat-frontmatter + - mdformat-footnote + - mdformat-gfm-alerts + - mdformat-tables diff --git a/README.md b/README.md index 495621e..37f9c8d 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,69 @@ # Auto-encoder Tutorials + [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## Setup ### Virtual Environment -If you don't operate inside a virtual environment, or only have access to an incompatible python version (<3.8), it is + +If you don't operate inside a virtual environment, or only have access to an incompatible python version (\<3.8), it is recommended you create a virtual environment using [`conda`](https://docs.conda.io/en/latest/): + ```shell script conda env create -f environment.yml conda activate deep-learning-tutorials ``` + Creating the environment this way also takes care of installing the dependencies for you, so you can skip the rest of the setup and dive straight into one of the tutorials. ### Installing Dependencies + If you already have a python environment set aside for this project and just want to install the dependencies, you can do that using the following command: + ```shell script pip install -e . ``` - ## How to Run + Once you've went through the [setup](#setup) instructions above, you can start exploring the tutorial's notebooks. We recommend using JupyterLab to run the notebooks, which can be launched by running (from within your environment): + ```shell script jupyter-lab ``` + When you've launched JupyterLab's web interface, you can simply navigate to any of the [tutorials listed below](#available-tutorials), and follow the instructions in there! - ## Available Tutorials ### Representation Learning + - [Basic Variational Autoencoders](tutorials/mnist-autoencoders.ipynb) - [Variational Autoencoders Applied to Cardiac MRI](tutorials/cardiac-mri-autoencoders.ipynb) The datasets used in this tutorial (MNIST and ACDC) will automatically be downloaded at the beginning of the notebooks. However, if you experience issues with the download and want to download the datasets, you can find them [here](https://drive.google.com/file/d/1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S/view?usp=sharing) or download it with the following command: + ```shell script gdown https://drive.google.com/uc?id=1H5pTOYjcSFR6B5GhA0sEPW0wgPVfBq8S -c -O data.tar.gz ``` + Once downloaded, you may untar the file and and copy the `data/` folder at the root of the project, i.e. at the same level as the `src/` and the `tutorials/` folders. + ```shell script tar -xvzf data.tar.gz ``` ## How to Contribute + If you want to contribute to the project, then you have to install development dependencies and pre-commit hooks, on top of the basic setup for using the project, detailed [above](#setup). The pre-commit hooks are there to ensure that any code committed to the repository meets the project's format and quality standards. + ```shell script # Install development dependencies pip install -e .[dev] diff --git a/dev.txt b/dev.txt index 35f10a2..b4a2191 100644 --- a/dev.txt +++ b/dev.txt @@ -1,4 +1,2 @@ pre-commit -isort -black -flake8 +ruff diff --git a/pyproject.toml b/pyproject.toml index 7d492ed..2a99bef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,5 @@ -[tool.black] +[tool.ruff] line-length = 120 -target-version = ["py38"] -exclude = "(.eggs|.git|.hg|.mypy_cache|.nox|.tox|.venv|.svn|_build|buck-out|build|dist)" -[tool.isort] -profile = "black" -line_length = 120 -src_paths = ["src"] +[tool.ruff.lint.pydocstyle] +convention = "google" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d86b9bf..0000000 --- a/setup.cfg +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -max-line-length = 120 -doctests = True -exclude = .tox,*.egg,build,temp -verbose = 2 -# https://pep8.readthedocs.io/en/latest/intro.html#error-codes -format = pylint -ignore = - # E203 - whitespace before ':'. Opposite convention enforced by black - E203 - # E231 - missing whitespace after ',', ';', or ':'; for black - E231 - # E501 - line too long. Handled by black, we have longer lines - E501 - # W503 - line break before binary operator, need for black - W503 diff --git a/tutorials/cardiac-mri-autoencoders.ipynb b/tutorials/cardiac-mri-autoencoders.ipynb index 236e061..52ecba9 100644 --- a/tutorials/cardiac-mri-autoencoders.ipynb +++ b/tutorials/cardiac-mri-autoencoders.ipynb @@ -55,10 +55,10 @@ "\n", "import sys\n", "\n", - "if '../' in sys.path:\n", + "if \"../\" in sys.path:\n", " print(sys.path)\n", "else:\n", - " sys.path.append('../')\n", + " sys.path.append(\"../\")\n", " print(sys.path)" ] }, @@ -135,6 +135,7 @@ "source": [ "from torch import nn\n", "\n", + "\n", "# Let's define the encoder architecture we want,\n", "# with some options to configure the input and output size\n", "def downsampling_block(in_channels, out_channels):\n", @@ -145,23 +146,27 @@ " nn.ReLU(),\n", " )\n", "\n", + "\n", "def make_encoder(data_shape, latent_space_size):\n", " in_channels = data_shape[0]\n", " shape_at_bottleneck = data_shape[1] // 16, data_shape[2] // 16\n", " size_at_bottleneck = shape_at_bottleneck[0] * shape_at_bottleneck[1] * 48\n", " return nn.Sequential(\n", - " downsampling_block(in_channels, 48), # Block 1 (input)\n", - " downsampling_block(48, 96), # Block 2\n", - " downsampling_block(96, 192), # Block 3\n", - " downsampling_block(192, 48), # Block 4 (limits number of channels to reduce total number of parameters)\n", - " nn.Flatten(), # Flatten before FC-layer at the bottleneck\n", - " nn.Linear(size_at_bottleneck, latent_space_size), # Bottleneck\n", + " downsampling_block(in_channels, 48), # Block 1 (input)\n", + " downsampling_block(48, 96), # Block 2\n", + " downsampling_block(96, 192), # Block 3\n", + " downsampling_block(192, 48), # Block 4 (limits number of channels to reduce total number of parameters)\n", + " nn.Flatten(), # Flatten before FC-layer at the bottleneck\n", + " nn.Linear(size_at_bottleneck, latent_space_size), # Bottleneck\n", " )\n", "\n", + "\n", "# Now let's build our encoder, with an arbitrary dimensionality of the latent space\n", "# and an input size depending on the data.\n", "latent_space_size = 32\n", - "encoder = make_encoder(data_shape, latent_space_size*2) # here the latent space size is *2 because the encoder predicts a *mean* and *variance* vector" + "encoder = make_encoder(\n", + " data_shape, latent_space_size * 2\n", + ") # here the latent space size is *2 because the encoder predicts a *mean* and *variance* vector" ] }, { @@ -185,7 +190,7 @@ "\n", "summary_kwargs = dict(col_names=[\"input_size\", \"output_size\", \"kernel_size\", \"num_params\"], depth=3, verbose=0)\n", "\n", - "summary(encoder, input_size=data_shape, batch_dim=0, **summary_kwargs)" + "summary(encoder, input_size=data_shape, batch_dim=0, **summary_kwargs)" ] }, { @@ -219,6 +224,7 @@ "source": [ "from src.modules import layers\n", "\n", + "\n", "# Same building blocks for the decoder as for the encoder\n", "def upsampling_block(in_channels, out_channels):\n", " return nn.Sequential(\n", @@ -228,6 +234,7 @@ " nn.ReLU(),\n", " )\n", "\n", + "\n", "def make_decoder(data_shape, latent_space_size):\n", " out_channels = data_shape[0]\n", " shape_at_bottleneck = data_shape[1] // 16, data_shape[2] // 16\n", @@ -236,16 +243,16 @@ " # Bottleneck\n", " nn.Linear(latent_space_size, size_at_bottleneck),\n", " nn.ReLU(),\n", - " layers.Reshape((48, *shape_at_bottleneck)), # Restore shape before convolutional layers\n", - "\n", - " upsampling_block(48, 192), # Block 1\n", - " upsampling_block(192, 96), # Block 2\n", - " upsampling_block(96, 48), # Block 3\n", - " nn.ConvTranspose2d(in_channels=48, out_channels=48, kernel_size=2, stride=2), # Block 4 (output)\n", + " layers.Reshape((48, *shape_at_bottleneck)), # Restore shape before convolutional layers\n", + " upsampling_block(48, 192), # Block 1\n", + " upsampling_block(192, 96), # Block 2\n", + " upsampling_block(96, 48), # Block 3\n", + " nn.ConvTranspose2d(in_channels=48, out_channels=48, kernel_size=2, stride=2), # Block 4 (output)\n", " nn.ReLU(),\n", " nn.Conv2d(in_channels=48, out_channels=out_channels, kernel_size=3, padding=1),\n", " )\n", "\n", + "\n", "# Now let's build our decoder, with the dimensionality of the latent space matching that of the encoder\n", "# and an output size depending on the data.\n", "decoder = make_decoder(data_shape, latent_space_size)" @@ -315,10 +322,12 @@ "import torchmetrics\n", "import torch.nn.functional as F\n", "\n", + "\n", "def kl_div(mu, logvar):\n", " kl_div_by_samples = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)\n", " return torch.mean(kl_div_by_samples)\n", "\n", + "\n", "def vae_forward_pass(encoder, decoder, x):\n", " \"\"\"VAE forward pass.\n", "\n", @@ -349,9 +358,9 @@ " # Similar to the input that we didn't need to vectorize, we don't need to reshape the output to a 2D shape anymore,\n", " # since the convolutional network already produces a structured output\n", " x_hat = decoder(z) # Forward pass on the decoder (to get the reconstructed input)\n", - " loss = F.cross_entropy(x_hat, x) # Compute the reconstruction loss\n", + " loss = F.cross_entropy(x_hat, x) # Compute the reconstruction loss\n", " loss += 1e-4 * kl_div(mu, logvar) # Loss now also includes the KL divergence term\n", - " return loss, x_hat.argmax(dim=1) # Transform segmentation back to categorical so that it can be displayed easily" + " return loss, x_hat.argmax(dim=1) # Transform segmentation back to categorical so that it can be displayed easily" ] }, { @@ -391,6 +400,7 @@ "epochs = 30\n", "batch_size = 64\n", "\n", + "\n", "def train(forward_pass_fn, encoder, decoder, optimizer, train_data, val_data, device=\"cuda\"):\n", " # Create dataloaders from the data\n", " # Those are PyTorch's abstraction to help iterate over the data\n", @@ -405,17 +415,17 @@ "\n", " # Train once over all the training data\n", " for _, y in train_dataloader:\n", - " y = y.to(device) # Move the data tensor to the device\n", - " optimizer.zero_grad() # Make sure gradients are reset\n", - " train_loss, _ = forward_pass_fn(encoder, decoder, y) # Forward pass+loss\n", - " train_loss.backward() # Backward pass\n", - " optimizer.step() # Update parameters w.r.t. optimizer and gradients\n", + " y = y.to(device) # Move the data tensor to the device\n", + " optimizer.zero_grad() # Make sure gradients are reset\n", + " train_loss, _ = forward_pass_fn(encoder, decoder, y) # Forward pass+loss\n", + " train_loss.backward() # Backward pass\n", + " optimizer.step() # Update parameters w.r.t. optimizer and gradients\n", " pbar_metrics[\"train_loss\"] = train_loss.item()\n", " fit_pbar.set_postfix(pbar_metrics)\n", "\n", " # At the end of the epoch, check performance against the validation data\n", " for _, y in val_dataloader:\n", - " y = y.to(device) # Move the data tensor to the device\n", + " y = y.to(device) # Move the data tensor to the device\n", " val_loss, _ = forward_pass_fn(encoder, decoder, y)\n", " pbar_metrics[\"val_loss\"] = val_loss.item()\n", " fit_pbar.set_postfix(pbar_metrics)" diff --git a/tutorials/mnist-autoencoders.ipynb b/tutorials/mnist-autoencoders.ipynb index 95d6c03..95d008f 100644 --- a/tutorials/mnist-autoencoders.ipynb +++ b/tutorials/mnist-autoencoders.ipynb @@ -57,10 +57,10 @@ "\n", "import sys\n", "\n", - "if '../' in sys.path:\n", + "if \"../\" in sys.path:\n", " print(sys.path)\n", "else:\n", - " sys.path.append('../')\n", + " sys.path.append(\"../\")\n", " print(sys.path)" ] }, @@ -109,11 +109,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%% code\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", @@ -126,9 +122,7 @@ "data_size = data_shape[0] * data_shape[1]\n", "\n", "# Download and prepare data\n", - "transform = transforms.Compose([\n", - " transforms.ToTensor(),\n", - "])\n", + "transform = transforms.Compose([transforms.ToTensor()])\n", "mnist_train = MNIST(\"../data\", train=True, transform=transform)\n", "mnist_test = MNIST(\"../data\", train=False, transform=transform)\n", "\n", @@ -167,11 +161,11 @@ "import matplotlib.pyplot as plt\n", "\n", "# Get the first training image and its class label\n", - "sample_image = mnist_train[0][0] # sample_image is a \"PyTorch tensor\"\n", - "sample_label = mnist_train[0][1] \n", + "sample_image = mnist_train[0][0] # sample_image is a \"PyTorch tensor\"\n", + "sample_label = mnist_train[0][1]\n", "\n", "# Convert the Tensor into a numpy array\n", - "sample_image_np = sample_image.numpy() \n", + "sample_image_np = sample_image.numpy()\n", "print(\"Image size = \", sample_image_np.shape)\n", "\n", "# Call \"squeeze\" to remove the first dimension\n", @@ -208,6 +202,7 @@ "source": [ "from torch import nn\n", "\n", + "\n", "# Let's define the encoder architecture we want,\n", "# with some options to configure the input and output size\n", "def make_encoder(data_size, latent_space_size):\n", @@ -219,6 +214,7 @@ " nn.Linear(64, latent_space_size),\n", " )\n", "\n", + "\n", "# Same thing for the decoder\n", "def make_decoder(data_size, latent_space_size):\n", " return nn.Sequential(\n", @@ -230,6 +226,7 @@ " nn.Sigmoid(),\n", " )\n", "\n", + "\n", "# Now let's build our networks, with an arbitrary dimensionality of the latent space\n", "# and an input and output size depending on the data.\n", "encoder = make_encoder(data_size, 32)\n", @@ -267,6 +264,7 @@ "import torch\n", "import torch.nn.functional as F\n", "\n", + "\n", "def autoencoder_forward_pass(encoder, decoder, x):\n", " \"\"\"AE forward pass.\n", "\n", @@ -280,11 +278,11 @@ " x_hat: batch of N reconstructed images\n", " \"\"\"\n", " in_shape = x.shape # Save the input shape\n", - " encoder_input = torch.flatten(x, start_dim=1) # Flatten the 2D image to a 1D tensor (for the linear layer)\n", + " encoder_input = torch.flatten(x, start_dim=1) # Flatten the 2D image to a 1D tensor (for the linear layer)\n", " z = encoder(encoder_input) # Forward pass on the encoder (to get the latent space vector)\n", " x_hat = decoder(z) # Forward pass on the decoder (to get the reconstructed input)\n", - " x_hat = x_hat.reshape(in_shape) # Restore the output to the original shape\n", - " loss = F.binary_cross_entropy(x_hat, x) # Compute the reconstruction loss\n", + " x_hat = x_hat.reshape(in_shape) # Restore the output to the original shape\n", + " loss = F.binary_cross_entropy(x_hat, x) # Compute the reconstruction loss\n", " return loss, x_hat" ] }, @@ -315,6 +313,7 @@ "epochs = 25\n", "batch_size = 256\n", "\n", + "\n", "def train(forward_pass_fn, encoder, decoder, optimizer, train_data, val_data, device=\"cuda\"):\n", " # Create dataloaders from the data\n", " # Those are PyTorch's abstraction to help iterate over the data\n", @@ -329,20 +328,19 @@ " fit_pbar = tqdm(range(epochs), desc=\"Training\", unit=\"epoch\")\n", " pbar_metrics = {\"train_loss\": None, \"val_loss\": None}\n", " for epoch in fit_pbar:\n", - "\n", " # Train once over all the training data\n", " for x, _ in train_dataloader:\n", - " x = x.to(device) # Move the data tensor to the device\n", - " optimizer.zero_grad() # Make sure gradients are reset\n", - " train_loss, _ = forward_pass_fn(encoder, decoder, x) # Forward pass\n", - " train_loss.backward() # Backward pass\n", - " optimizer.step() # Update parameters w.r.t. optimizer and gradients\n", + " x = x.to(device) # Move the data tensor to the device\n", + " optimizer.zero_grad() # Make sure gradients are reset\n", + " train_loss, _ = forward_pass_fn(encoder, decoder, x) # Forward pass\n", + " train_loss.backward() # Backward pass\n", + " optimizer.step() # Update parameters w.r.t. optimizer and gradients\n", " pbar_metrics[\"train_loss\"] = train_loss.item()\n", " fit_pbar.set_postfix(pbar_metrics)\n", "\n", " # At the end of the epoch, check performance against the validation data\n", " for x, _ in val_dataloader:\n", - " x = x.to(device) # Move the data tensor to the device\n", + " x = x.to(device) # Move the data tensor to the device\n", " val_loss, _ = forward_pass_fn(encoder, decoder, x)\n", " pbar_metrics[\"val_loss\"] = val_loss.item()\n", " fit_pbar.set_postfix(pbar_metrics)" @@ -473,7 +471,7 @@ "\n", "# In practice, a small trick to easily implement the two heads of the encoder is to simply\n", "# double the size of its output. Then, we can slice the output in half during the forward pass!\n", - "vae_encoder = make_encoder(data_size, latent_space_size * 2) \n", + "vae_encoder = make_encoder(data_size, latent_space_size * 2)\n", "vae_decoder = make_decoder(data_size, latent_space_size)" ] }, @@ -509,6 +507,7 @@ " kl_div_by_samples = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)\n", " return torch.mean(kl_div_by_samples)\n", "\n", + "\n", "def vae_forward_pass(encoder, decoder, x):\n", " \"\"\"VAE forward pass.\n", "\n", @@ -537,8 +536,8 @@ "\n", " # Decoding mostly stays the same. The only difference is the added 4th line below\n", " x_hat = decoder(z) # Forward pass on the decoder (to get the reconstructed input)\n", - " x_hat = x_hat.reshape(in_shape) # Restore the output to the original shape\n", - " loss = F.binary_cross_entropy(x_hat, x) # Compute the reconstruction loss\n", + " x_hat = x_hat.reshape(in_shape) # Restore the output to the original shape\n", + " loss = F.binary_cross_entropy(x_hat, x) # Compute the reconstruction loss\n", " loss += 5e-3 * kl_div(mu, logvar) # Loss now also includes the KL divergence term\n", " return loss, x_hat" ] @@ -637,7 +636,7 @@ "\n", "sample = vae_decoder(z_torch).reshape(data_shape) # decode the latent vector with the VAE decoder\n", "\n", - "plt.imshow(sample.detach().cpu().numpy()) # plot the resulting image" + "plt.imshow(sample.detach().cpu().numpy()) # plot the resulting image" ] }, { From fc8d80546abbf73d7099b4159229da663a2e2dbd Mon Sep 17 00:00:00 2001 From: Nathan Painchaud Date: Tue, 25 Mar 2025 16:56:29 +0100 Subject: [PATCH 4/6] Remove `torchmetrics` dependency by copying impl. of `to_onehot` utility Also reformat import (e.g. new lines between builtin and third-party, etc.) --- requirements.txt | 1 - src/data/utils.py | 42 ++++++++++++++++++++++++ tutorials/cardiac-mri-autoencoders.ipynb | 26 +++++---------- tutorials/mnist-autoencoders.ipynb | 2 ++ 4 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 src/data/utils.py diff --git a/requirements.txt b/requirements.txt index 847c65f..44c28b1 100755 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,3 @@ umap-learn # cardiac-mri-autoencoders h5py -torchmetrics diff --git a/src/data/utils.py b/src/data/utils.py new file mode 100644 index 0000000..ab08595 --- /dev/null +++ b/src/data/utils.py @@ -0,0 +1,42 @@ +from typing import Optional + +import torch +from torch import Tensor + + +def to_onehot( + label_tensor: Tensor, + num_classes: Optional[int] = None, +) -> Tensor: + """Convert a dense label tensor to one-hot format. + + References: + Copied from `torchmetrics.utilities.data.to_onehot` (v1.6.2, 2025-03-25): + https://lightning.ai/docs/torchmetrics/stable/references/utilities.html#to-onehot + + Args: + label_tensor: dense label tensor, with shape [N, d1, d2, ...] + num_classes: number of classes C + + Returns: + A sparse label tensor with shape [N, C, d1, d2, ...] + + Example: + >>> x = torch.tensor([1, 2, 3]) + >>> to_onehot(x) + tensor([[0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1]]) + """ + if num_classes is None: + num_classes = int(label_tensor.max().detach().item() + 1) + + tensor_onehot = torch.zeros( + label_tensor.shape[0], + num_classes, + *label_tensor.shape[1:], + dtype=label_tensor.dtype, + device=label_tensor.device, + ) + index = label_tensor.long().unsqueeze(1).expand_as(tensor_onehot) + return tensor_onehot.scatter_(1, index, 1.0) diff --git a/tutorials/cardiac-mri-autoencoders.ipynb b/tutorials/cardiac-mri-autoencoders.ipynb index 52ecba9..f3d3f51 100644 --- a/tutorials/cardiac-mri-autoencoders.ipynb +++ b/tutorials/cardiac-mri-autoencoders.ipynb @@ -89,14 +89,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%% code\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", + "\n", "from src.data.acdc.dataset import Acdc\n", "from src.visualization.utils import display_data_samples\n", "\n", @@ -311,17 +308,14 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%% code\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "import torch\n", - "import torchmetrics\n", "import torch.nn.functional as F\n", "\n", + "from src.data.utils import to_onehot\n", + "\n", "\n", "def kl_div(mu, logvar):\n", " kl_div_by_samples = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)\n", @@ -342,7 +336,7 @@ " \"\"\"\n", " # We don't need to flatten the input images to (N, num_pixels) anymore,\n", " # but we need to convert them from one-channel categorical data to multi-channel one-hot format\n", - " encoder_input = torchmetrics.utilities.data.to_onehot(x, num_classes=4).float()\n", + " encoder_input = to_onehot(x, num_classes=4).float()\n", "\n", " encoding_distr = encoder(encoder_input) # Forward pass on the encoder (to get the latent space posterior)\n", "\n", @@ -492,18 +486,14 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%% code\n" - } - }, + "metadata": {}, "outputs": [], "source": [ "from src.visualization.latent_space import explore_latent_space\n", "\n", "explore_latent_space(\n", " acdc_val,\n", - " lambda x: encoder(torchmetrics.utilities.data.to_onehot(x, num_classes=4).float())[:, :latent_space_size],\n", + " lambda x: encoder(to_onehot(x, num_classes=4).float())[:, :latent_space_size],\n", " lambda z: decoder(z).argmax(dim=1),\n", " data_to_encode=\"target\",\n", " batch_size=64,\n", diff --git a/tutorials/mnist-autoencoders.ipynb b/tutorials/mnist-autoencoders.ipynb index 95d008f..11a3dbc 100644 --- a/tutorials/mnist-autoencoders.ipynb +++ b/tutorials/mnist-autoencoders.ipynb @@ -115,6 +115,7 @@ "import numpy as np\n", "from torchvision.datasets import MNIST\n", "from torchvision.transforms import transforms\n", + "\n", "from src.visualization.utils import display_data_samples\n", "\n", "# MNIST consists of 28x28 images, so the size of the data is\n", @@ -306,6 +307,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "from torch.utils.data import DataLoader\n", "from tqdm.auto import tqdm\n", "\n", From 2ccefeff95945e55b5832443f523846e453d722c Mon Sep 17 00:00:00 2001 From: Nathan Painchaud Date: Tue, 25 Mar 2025 20:48:26 +0100 Subject: [PATCH 5/6] Update action and Python versions in GitHub workflows --- .github/workflows/pre-commit.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index c42b95f..f03b851 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.12 - name: Install and run pre-commit hooks - uses: pre-commit/action@v2.0.0 + uses: pre-commit/action@v3.0.1 From ee58854c08fc444f12144871420a303d10d2231c Mon Sep 17 00:00:00 2001 From: Nathan Painchaud Date: Tue, 25 Mar 2025 21:06:07 +0100 Subject: [PATCH 6/6] Change Git file mode of `requirements.txt` to make it non-executable --- requirements.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt old mode 100755 new mode 100644