You should find plenty of custom Datasets in this discussion board.
Here is an example:
class MyLazyDataset(Dataset):
    def __init__(self, image_paths, targets, transform=None):
        self.image_paths = image_paths
        self.targets = targets
        self.transform = transform
        
    def __getitem__(self, index):
        # Load actual image here
        x = Image.open(self.image_paths[index])
        if self.transform:
            x = self.transform(x)
        y = self.targets[index]
        
        return x, y
    
    def __len__(self):
        return len(self.image_paths)

As you can see, you are just passing the image paths without loading all images beforehand.
In the __getitem__ method, the actual sample is loaded and transformed.
In my example I would have to compute the targets before and pass them to MyLazyDataset.
However, you could of course create the target in __getitem__ e.g. by using the current image path and check for a class name.
If you are using ImageFolder, the images will be lazily loaded by default.
Here is a small example of the usage of Subset:
dataset = datasets.MNIST(
    root='./data',
    download=False,
    transform=transforms.ToTensor()
)


train_dataset = Subset(dataset, indices=range(50000))
val_dataset = Subset(dataset, indices=range(50000, 60000))

You just have to pass your Dataset and the indices you would like to use in the subset.