chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:19:01 +08:00
commit 3b90d1192f
2172 changed files with 594509 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
"""
---
title: U-Net
summary: >
PyTorch implementation and tutorial of U-Net model.
---
# U-Net
This is an implementation of the U-Net model from the paper,
[U-Net: Convolutional Networks for Biomedical Image Segmentation](https://arxiv.org/abs/1505.04597).
U-Net consists of a contracting path and an expansive path.
The contracting path is a series of convolutional layers and pooling layers,
where the resolution of the feature map gets progressively reduced.
Expansive path is a series of up-sampling layers and convolutional layers
where the resolution of the feature map gets progressively increased.
At every step in the expansive path the corresponding feature map from the contracting path
concatenated with the current feature map.
![U-Net diagram from paper](unet.png)
Here is the [training code](experiment.html) for an experiment that trains a U-Net
on [Carvana dataset](carvana.html).
"""
import torch
import torchvision.transforms.functional
from torch import nn
class DoubleConvolution(nn.Module):
"""
### Two $3 \times 3$ Convolution Layers
Each step in the contraction path and expansive path have two $3 \times 3$
convolutional layers followed by ReLU activations.
In the U-Net paper they used $0$ padding,
but we use $1$ padding so that final feature map is not cropped.
"""
def __init__(self, in_channels: int, out_channels: int):
"""
:param in_channels: is the number of input channels
:param out_channels: is the number of output channels
"""
super().__init__()
# First $3 \times 3$ convolutional layer
self.first = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1)
self.act1 = nn.ReLU()
# Second $3 \times 3$ convolutional layer
self.second = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
self.act2 = nn.ReLU()
def forward(self, x: torch.Tensor):
# Apply the two convolution layers and activations
x = self.first(x)
x = self.act1(x)
x = self.second(x)
return self.act2(x)
class DownSample(nn.Module):
"""
### Down-sample
Each step in the contracting path down-samples the feature map with
a $2 \times 2$ max pooling layer.
"""
def __init__(self):
super().__init__()
# Max pooling layer
self.pool = nn.MaxPool2d(2)
def forward(self, x: torch.Tensor):
return self.pool(x)
class UpSample(nn.Module):
"""
### Up-sample
Each step in the expansive path up-samples the feature map with
a $2 \times 2$ up-convolution.
"""
def __init__(self, in_channels: int, out_channels: int):
super().__init__()
# Up-convolution
self.up = nn.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2)
def forward(self, x: torch.Tensor):
return self.up(x)
class CropAndConcat(nn.Module):
"""
### Crop and Concatenate the feature map
At every step in the expansive path the corresponding feature map from the contracting path
concatenated with the current feature map.
"""
def forward(self, x: torch.Tensor, contracting_x: torch.Tensor):
"""
:param x: current feature map in the expansive path
:param contracting_x: corresponding feature map from the contracting path
"""
# Crop the feature map from the contracting path to the size of the current feature map
contracting_x = torchvision.transforms.functional.center_crop(contracting_x, [x.shape[2], x.shape[3]])
# Concatenate the feature maps
x = torch.cat([x, contracting_x], dim=1)
#
return x
class UNet(nn.Module):
"""
## U-Net
"""
def __init__(self, in_channels: int, out_channels: int):
"""
:param in_channels: number of channels in the input image
:param out_channels: number of channels in the result feature map
"""
super().__init__()
# Double convolution layers for the contracting path.
# The number of features gets doubled at each step starting from $64$.
self.down_conv = nn.ModuleList([DoubleConvolution(i, o) for i, o in
[(in_channels, 64), (64, 128), (128, 256), (256, 512)]])
# Down sampling layers for the contracting path
self.down_sample = nn.ModuleList([DownSample() for _ in range(4)])
# The two convolution layers at the lowest resolution (the bottom of the U).
self.middle_conv = DoubleConvolution(512, 1024)
# Up sampling layers for the expansive path.
# The number of features is halved with up-sampling.
self.up_sample = nn.ModuleList([UpSample(i, o) for i, o in
[(1024, 512), (512, 256), (256, 128), (128, 64)]])
# Double convolution layers for the expansive path.
# Their input is the concatenation of the current feature map and the feature map from the
# contracting path. Therefore, the number of input features is double the number of features
# from up-sampling.
self.up_conv = nn.ModuleList([DoubleConvolution(i, o) for i, o in
[(1024, 512), (512, 256), (256, 128), (128, 64)]])
# Crop and concatenate layers for the expansive path.
self.concat = nn.ModuleList([CropAndConcat() for _ in range(4)])
# Final $1 \times 1$ convolution layer to produce the output
self.final_conv = nn.Conv2d(64, out_channels, kernel_size=1)
def forward(self, x: torch.Tensor):
"""
:param x: input image
"""
# To collect the outputs of contracting path for later concatenation with the expansive path.
pass_through = []
# Contracting path
for i in range(len(self.down_conv)):
# Two $3 \times 3$ convolutional layers
x = self.down_conv[i](x)
# Collect the output
pass_through.append(x)
# Down-sample
x = self.down_sample[i](x)
# Two $3 \times 3$ convolutional layers at the bottom of the U-Net
x = self.middle_conv(x)
# Expansive path
for i in range(len(self.up_conv)):
# Up-sample
x = self.up_sample[i](x)
# Concatenate the output of the contracting path
x = self.concat[i](x, pass_through.pop())
# Two $3 \times 3$ convolutional layers
x = self.up_conv[i](x)
# Final $1 \times 1$ convolution layer
x = self.final_conv(x)
#
return x
+82
View File
@@ -0,0 +1,82 @@
"""
---
title: Carvana dataset for the U-Net experiment
summary: >
Carvana dataset for the U-Net experiment.
---
# Carvana Dataset for the [U-Net](index.html) [experiment](experiment.html)
You can find the download instructions
[on Kaggle](https://www.kaggle.com/competitions/carvana-image-masking-challenge/data).
Save the training images inside `carvana/train` folder and the masks in `carvana/train_masks` folder.
"""
from pathlib import Path
import torchvision.transforms.functional
from PIL import Image
import torch.utils.data
from labml import lab
class CarvanaDataset(torch.utils.data.Dataset):
"""
## Carvana Dataset
"""
def __init__(self, image_path: Path, mask_path: Path):
"""
:param image_path: is the path to the images
:param mask_path: is the path to the masks
"""
# Get a dictionary of images by id
self.images = {p.stem: p for p in image_path.iterdir()}
# Get a dictionary of masks by id
self.masks = {p.stem[:-5]: p for p in mask_path.iterdir()}
# Image ids list
self.ids = list(self.images.keys())
# Transformations
self.transforms = torchvision.transforms.Compose([
torchvision.transforms.Resize(572),
torchvision.transforms.ToTensor(),
])
def __getitem__(self, idx: int):
"""
#### Get an image and its mask.
:param idx: is index of the image
"""
# Get image id
id_ = self.ids[idx]
# Load image
image = Image.open(self.images[id_])
# Transform image and convert it to a PyTorch tensor
image = self.transforms(image)
# Load mask
mask = Image.open(self.masks[id_])
# Transform mask and convert it to a PyTorch tensor
mask = self.transforms(mask)
# The mask values were not $1$, so we scale it appropriately.
mask = mask / mask.max()
# Return the image and the mask
return image, mask
def __len__(self):
"""
#### Size of the dataset
"""
return len(self.ids)
# Testing code
if __name__ == '__main__':
ds = CarvanaDataset(lab.get_data_path() / 'carvana' / 'train', lab.get_data_path() / 'carvana' / 'train_masks')
+169
View File
@@ -0,0 +1,169 @@
"""
---
title: Training a U-Net on Carvana dataset
summary: >
Code for training a U-Net model on Carvana dataset.
---
# Training [U-Net](index.html)
This trains a [U-Net](index.html) model on [Carvana dataset](carvana.html).
You can find the download instructions
[on Kaggle](https://www.kaggle.com/competitions/carvana-image-masking-challenge/data).
Save the training images inside `carvana/train` folder and the masks in `carvana/train_masks` folder.
For simplicity, we do not do a training and validation split.
"""
import numpy as np
import torchvision.transforms.functional
import torch
import torch.utils.data
from labml import lab, tracker, experiment, monit
from labml.configs import BaseConfigs
from labml_nn.helpers.device import DeviceConfigs
from labml_nn.unet import UNet
from labml_nn.unet.carvana import CarvanaDataset
from torch import nn
class Configs(BaseConfigs):
"""
## Configurations
"""
# Device to train the model on.
# [`DeviceConfigs`](../helpers/device.html)
# picks up an available CUDA device or defaults to CPU.
device: torch.device = DeviceConfigs()
# [U-Net](index.html) model
model: UNet
# Number of channels in the image. $3$ for RGB.
image_channels: int = 3
# Number of channels in the output mask. $1$ for binary mask.
mask_channels: int = 1
# Batch size
batch_size: int = 1
# Learning rate
learning_rate: float = 2.5e-4
# Number of training epochs
epochs: int = 4
# Dataset
dataset: CarvanaDataset
# Dataloader
data_loader: torch.utils.data.DataLoader
# Loss function
loss_func = nn.BCELoss()
# Sigmoid function for binary classification
sigmoid = nn.Sigmoid()
# Adam optimizer
optimizer: torch.optim.Adam
def init(self):
# Initialize the [Carvana dataset](carvana.html)
self.dataset = CarvanaDataset(lab.get_data_path() / 'carvana' / 'train',
lab.get_data_path() / 'carvana' / 'train_masks')
# Initialize the model
self.model = UNet(self.image_channels, self.mask_channels).to(self.device)
# Create dataloader
self.data_loader = torch.utils.data.DataLoader(self.dataset, self.batch_size,
shuffle=True, pin_memory=True)
# Create optimizer
self.optimizer = torch.optim.Adam(self.model.parameters(), lr=self.learning_rate)
# Image logging
tracker.set_image("sample", True)
@torch.no_grad()
def sample(self, idx=-1):
"""
### Sample images
"""
# Get a random sample
x, _ = self.dataset[np.random.randint(len(self.dataset))]
# Move data to device
x = x.to(self.device)
# Get predicted mask
mask = self.sigmoid(self.model(x[None, :]))
# Crop the image to the size of the mask
x = torchvision.transforms.functional.center_crop(x, [mask.shape[2], mask.shape[3]])
# Log samples
tracker.save('sample', x * mask)
def train(self):
"""
### Train for an epoch
"""
# Iterate through the dataset.
# Use [`mix`](https://docs.labml.ai/api/monit.html#labml.monit.mix)
# to sample $50$ times per epoch.
for _, (image, mask) in monit.mix(('Train', self.data_loader), (self.sample, list(range(50)))):
# Increment global step
tracker.add_global_step()
# Move data to device
image, mask = image.to(self.device), mask.to(self.device)
# Make the gradients zero
self.optimizer.zero_grad()
# Get predicted mask logits
logits = self.model(image)
# Crop the target mask to the size of the logits. Size of the logits will be smaller if we
# don't use padding in convolutional layers in the U-Net.
mask = torchvision.transforms.functional.center_crop(mask, [logits.shape[2], logits.shape[3]])
# Calculate loss
loss = self.loss_func(self.sigmoid(logits), mask)
# Compute gradients
loss.backward()
# Take an optimization step
self.optimizer.step()
# Track the loss
tracker.save('loss', loss)
def run(self):
"""
### Training loop
"""
for _ in monit.loop(self.epochs):
# Train the model
self.train()
# New line in the console
tracker.new_line()
# Save the model
def main():
# Create experiment
experiment.create(name='unet')
# Create configurations
configs = Configs()
# Set configurations. You can override the defaults by passing the values in the dictionary.
experiment.configs(configs, {})
# Initialize
configs.init()
# Set models for saving and loading
experiment.add_pytorch_models({'model': configs.model})
# Start and run the training loop
with experiment.start():
configs.run()
#
if __name__ == '__main__':
main()