liznerski / fcdd

Repository for the Explainable Deep One-Class Classification paper
MIT License
225 stars 62 forks source link

Training on new custom dataset #12

Closed salmiachraf closed 3 years ago

salmiachraf commented 3 years ago

Hello! Thank you for the code.

I am trying to train the model on my own custom dataset (folder of images). But it's a bit hard to do so. I tried to follow this:

` Create a new python script in the datasets package. Implement a dataset that inherits the fcdd.datasets.bases.TorchvisionDataset class. Your implementation needs to process all parameters of the fcdd.datasets.bases.load_dataset function in its initialization. You can use the preproc parameter to switch between different data preprocessing pipelines (augmentation, etc). In the end, your implementation needs to have at least all attributes defined in fcdd.datasets.bases.BaseADDataset class. Most importantly, the _train_set attribute and the _test_set attribute containing the corresponding torchvision-style datasets. Have a look at the already available implementations.

Add a name for your dataset to the fcdd.datasets.init.DS_CHOICES variable. Add your dataset to the "switch-case" in the fcdd.datasets.init.load_dataset function. Add the number of available class for your dataset to the fcdd.datasets.init.no_classes function and add the class names to fcdd.datasets.init.str_labels.

`

But I am finding it hard to do. I cant see where shall I put the images folder exactly and how to prepare those scripts. Can anyone help?

liznerski commented 3 years ago

Hi. You can put your image folder anywhere you want, in your implementation you just need to point to the correct location. Let's go through this step by step. Consider your dataset being named foodata.

  1. Create a script foodata.py in fcdd.datasets. Implement a typical torchvision-style dataset for your image folder. You can reuse the PyTorch default implementation torchvision.datasets.ImageFolder. Read https://pytorch.org/docs/1.4.0/torchvision/datasets.html#datasetfolder for how your folder needs to be structured for this. So I imagine something like this:
import random
import torch
import torchvision.transforms as transforms
from torchvision.datasets import ImageFolder
from typing import Tuple

class FooData(ImageFolder):
    def __init__(self, root, transform=None, target_transform=None,
                 normal_classes=None, all_transform=None):
        super().__init__(root, transform=transform, target_transform=target_transform)
        self.normal_classes = normal_classes
        self.all_transform = all_transform  # contains the OnlineSupervisor

    def __getitem__(self, index: int) -> Tuple[torch.Tensor, int]:
        target = self.targets[index]

        if self.target_transform is not None:  # transforms labels (e.g. class labels to AD labels)
            target = self.target_transform(target)

        if self.all_transform is not None:  # replaces image and label with artificial anomalies 
            replace = random.random() < 0.5
            if replace:
                img, _, target = self.all_transform(None, None, target, replace=replace)
                img = transforms.ToPILImage()(img)
            else:
                path, _ = self.samples[index]
                img = self.loader(path)

        else:
            path, _ = self.samples[index]
            img = self.loader(path)

        if self.transform is not None:  # image transformation, e.g. augmentations
            img = self.transform(img)

        return img, target
  1. In the same file, create an AD dataset that handles everything else, so that you can retrieve the required data loaders from it. Here the implementation depends on your type of data. Do you want to perform one-vs-rest? Assuming you just have a bunch of unlabeled data, so just one class with label 0 (if you have some known anomalies, they are assumed to have label 1):
    
    import os.path as pt
    import torchvision.transforms as transforms
    from fcdd.datasets.bases import TorchvisionDataset
    from fcdd.datasets.online_supervisor import OnlineSupervisor
    from fcdd.util.logging import Logger

class ADFooData(TorchvisionDataset): base_folder = 'foodata'

def __init__(self, root: str, preproc: str, supervise_mode: str,
             noise_mode: str, logger: Logger = None):
    root = pt.join(root, self.base_folder)  # assuming your data is in "root"/foodata
    trainpath = pt.join(root, 'train')  # assuming your train data is in a subfolder train
    testpath = pt.join(root, 'test')  # assuming your test data is in a subfolder test
    super().__init__(root, logger=logger)

    self.n_classes = 2  # 0: normal, 1: outlier
    self.raw_shape = (?, ?, ?)   # shape of your data samples in channels x height x width
    self.shape = (3, 224, 224)  # shape of your data samples in channels x height x width after image preprocessing
    self.normal_classes = (0, )
    self.outlier_classes = (1, )
    self.nominal_label = 0
    self.anomalous_label = 1

    # precomputed mean and std of your training data
    mean = (?, ?, ?)
    std = (?, ?, ?)

    if preproc in ['', None, 'default', 'none']:
        test_transform = transform = transforms.Compose([
            transforms.Resize((self.shape[-2], self.shape[-1])),
            transforms.ToTensor(),
            transforms.Normalize(mean, std)
        ])
    #  here you could define other pipelines with augmentations
    else:
        raise ValueError('Preprocessing pipeline {} is not known.'.format(preproc))

    target_transform = transforms.Lambda(
        lambda x: self.anomalous_label if x in self.outlier_classes else self.nominal_label
    )
    if supervise_mode not in ['unsupervised']:
        all_transform = OnlineSupervisor(self, supervise_mode, noise_mode)
    else:
        all_transform = None

    self._train_set = FooData(
        root=trainpath, normal_classes=self.normal_classes,
        transform=transform, target_transform=target_transform, all_transform=all_transform,
    )
    self._test_set = FooData(
        root=testpath, normal_classes=self.normal_classes,
        transform=test_transform, target_transform=target_transform,
    )

3. Add your dataset to `fcdd.datasets.__init__` so that it can be loaded by the trainer.

[...] from fcdd.datasets.foodata import ADFooData DS_CHOICES = ('mnist', 'cifar10', 'fmnist', 'mvtec', 'imagenet', 'pascalvoc', 'foodata') [...] elif dataset_name == 'foodata': dataset = ADFooData( root=data_path, preproc=preproc, supervise_mode=supervise_mode, noise_mode=noise_mode, logger=logger, ) [...] def no_classes(dataset_name: str) -> int: return { 'cifar10': 10, 'fmnist': 10, 'mvtec': 15, 'imagenet': 30, 'pascalvoc': 1, 'foodata': 1, }[dataset_name] [...] def str_labels(dataset_name: str) -> List[str]: return { 'cifar10': ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'], 'fmnist': [ 't-shirt/top', 'trouser', 'pullover', 'dress', 'coat', 'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot' ], 'mvtec': [ 'bottle', 'cable', 'capsule', 'carpet', 'grid', 'hazelnut', 'leather', 'metal_nut', 'pill', 'screw', 'tile', 'toothbrush', 'transistor', 'wood', 'zipper' ], 'imagenet': deepcopy(ADImageNet.ad_classes), 'pascalvoc': ['horse'], 'foodata': ['foo'], }[dataset_name]


I  simplified things here a bit and ignored some optional arguments (such as oe_limit). 
To train on your new data, just run `python runners/run_imagenet.py --dataset foodata --net FCDD_CNN224_VGG --datadir PATH_TO_YOUR_DATA`. The --datadir argument should point to your data (for example --datadir /home/salmiachraf/fcdd/data/datasets for your data being in /home/salmiachraf/fcdd/data/datasets/foodata/train/class_0/sample_x). The runner uses an imagenet-pre-trained network applicable for input images of shape 224x224 (i.e., you need to have set shape in ADFooData to 224x224) and imagenet21k data as outlier exposure. 
Using the imagenet runner might seem a bit confusing here, but the runners are all the same, they just use a different set of default parameters. 
However, you can just add your own runner with your own default parameters for more convenience. 

Does this help you? Otherwise, feel free to provide more information. I'm glad to help.
jianjuan commented 3 years ago

Hi, I just read and run you code, I'd say that your framework is nice, but it costs me very much to custom my own data. And I wonder why I can't debug it, I want to figure out its training and testing flow, but I just can't. PS: if I have the same data structure as MVtec_AD, is there a faster way to run it, you know, just change the path and class name and things like that? @liznerski

liznerski commented 3 years ago

Thanks for your feedback. Can you elaborate a bit more on why you can't debug it? If you literally refer to the debug mode of IDEs: you need to set the number of data loader workers to 0 so that it uses the main processes instead of spawning new ones. That's at least how I do it. The first data loader that is created is for previewing the data, which means you need to change https://github.com/liznerski/fcdd/blob/master/python/fcdd/datasets/bases.py#L90 for that.

Regarding the PS: in this case, it might be easier to adapt the MVTec-AD code. However, there are still a few things that need to be set; for example, the precomputed mean and std of the dataset. Considering the interest in using FCDD on custom data, I will set myself to work and implement some more general data loader for "image folder datasets" in the upcoming weeks. Stay tuned ;)

salmiachraf commented 3 years ago

Thank you @liznerski for the explanation. It's very clear and I could follow it. Although, I get this error:

(one_class) C:\Users\salmiachraf \Desktop\FCDD\python\fcdd>python run_imagenet.py --dataset region3 --net FCDD_CNN224_VGG --datadir C:\Users\salmiachraf\Downloads\datasets

Plotting Many ROC for completed classes up to 0...
Traceback (most recent call last):
  File "run_imagenet.py", line 19, in <module>
    runner.run()
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\runners\bases.py", line 203, in run
    self.run_classes(**vars(self.args))
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\runners\bases.py", line 222, in run_classes
    it, **kwargs
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\runners\bases.py", line 182, in run_seeds
    this_viz_ids, **kwargs
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\runners\bases.py", line 101, in run_one
    **kwargs
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\training\setup.py", line 106, in trainer_setup
    noise_mode, online_supervision, nominal_label, oe_limit, logger=logger
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\datasets\__init__.py", line 63, in load_dataset
    supervise_mode=supervise_mode, noise_mode=noise_mode, logger=logger, )
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\datasets\region3.py", line 81, in __init__
    all_transform = OnlineSupervisor(self, supervise_mode, noise_mode)
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\datasets\online_supervisor.py", line 52, in __init__
    root=ds.root
  File "C:\Users\salmiachraf \Desktop\FCDD\python\fcdd\datasets\outlier_exposure\imagenet.py", line 260, in __init__
    assert len(size) == 4 and size[2] == size[3]
AssertionError

Thank you for your time.

jianjuan commented 3 years ago

Thanks for your feedback. Can you elaborate a bit more on why you can't debug it? If you literally refer to the debug mode of IDEs: you need to set the number of data loader workers to 0 so that it uses the main processes instead of spawning new ones. That's at least how I do it. The first data loader that is created is for previewing the data, which means you need to change https://github.com/liznerski/fcdd/blob/master/python/fcdd/datasets/bases.py#L90 for that.

Regarding the PS: in this case, it might be easier to adapt the MVTec-AD code. However, there are still a few things that need to be set; for example, the precomputed mean and std of the dataset. Considering the interest in using FCDD on custom data, I will set myself to work and implement some more general data loader for "image folder datasets" in the upcoming weeks. Stay tuned ;)

That will be nicer to provide a more general data loader. And I think an external config file will be more convenient, all the variable parameters in this config file can be modified to complete our customized experiments. Because sometimes, I just want to test the algorithm on my dataset quikly to get a rough conclusion. Consider me as an user, please.

liznerski commented 3 years ago

@salmiachraf This assertion makes sure that the imagenet21k outliers are generated with the correct shape. The shape is automatically extracted from the raw_shape of your dataset (online_supervisor.py#L51). It seems that you didn't set the raw_shape attribute properly. It should be something like (3, 256, 256). The preprocessing pipeline will resize it to (3, 224, 224) later on. The distinction of raw_shape and shape is due to pipelines containing crop augmentations.

PS: Since you changed --datadir, make sure that your imagenet21k data is also located there (i.e., C:\Users\salmiachraf\Downloads\datasets\imagenet22k\fall11_whole_extracted...) instead of the default location.

liznerski commented 3 years ago

I've just pushed a custom dataset implementation (b2fc0fee466305494e07d7e83475dc8694e0d26b). Feel free to try it and report any problems.

opentld commented 2 years ago

image Would you please tell me how to compute the mean and std of training data? @liznerski

liznerski commented 2 years ago

Look here for a generic version of how to do that (with the custom dataset implementation provided in this git repo). Depending on your implementation, you can probably also do something like data.permute(1, 0, 2, 3).flatten(1).mean(1), data.permute(1, 0, 2, 3).flatten(1).std(1).