[PYTHON] [Challenger à la recherche] Le chargement et l'augmentation des données les plus rapides (bloc-notes Kaggle) je pense

introduction

Eh bien, comme le titre l'indique, j'ai considéré le chargement de données et l'augmentation de données les plus rapides sur le bloc-notes Kaggle, je vais donc le présenter. Si vous connaissez un moyen plus rapide, faites-le moi savoir! Le sujet de cette heure est défini comme suit.

Vous pouvez également essayer le code utilisé dans cet article à partir de ce bloc-notes. (Je n'ai pas tellement débogué, alors veuillez signaler toute lacune ...) https://www.kaggle.com/hirune924/the-fastest-data-loading-data-augmentation?scriptVersionId=41763394

Aussi, pour ceux qui meurent d'envie de connaître le résultat, je publierai le résultat en premier. ダウンロード.png

OpenCV + Albumentations Je pense que c'est une combinaison assez basique maintenant. Je pense qu'il y a plus de gens qui utilisent cela que Transform de torchvison. J'utilise souvent cette combinaison au début de la compétition Kaggle.

cv2_alb.py


import time
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd
import cv2
import albumentations as A


class DogDataset(Dataset):
    def __init__(self, transform=None):
        self.img_list = pd.read_csv('../input/dog-breed-identification/labels.csv')
        self.transform = transform
        
        breeds=list(self.img_list['breed'].unique())
        self.breed2idx = {b: i for i, b in enumerate(breeds)}

    def __len__(self):
        return len(self.img_list)

    def __getitem__(self, idx):
        img_row = self.img_list.iloc[idx]
        image = cv2.imread('../input/dog-breed-identification/train/' + img_row['id'] + '.jpg')
        label = self.breed2idx[img_row['breed']]

        if self.transform is not None:
            image = self.transform(image=image)
        image = torch.from_numpy(image['image'].transpose(2, 0, 1))
        return image, label

transform = A.Compose([A.RandomResizedCrop(height=224, width=224, p=1),
                       A.HorizontalFlip(p=0.5),
                       A.VerticalFlip(p=0.5),
                       A.MotionBlur(blur_limit=3, p=1),
                       A.Rotate(limit=45, p=1),
                       A.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5), max_pixel_value=255.0, always_apply=True, p=1.0)])

data_loader = DataLoader(DogDataset(transform=transform), batch_size=64, shuffle=True, num_workers=2)

Lisez ceci comme suit et mesurez le temps.

cv2_alb_time.py


%%timeit -r 2 -n 5
opencv_alb_times = []
start_time = time.time()
for image, label in data_loader:
    image = image.cuda()
    label = label.cuda()
    pass
opencv_alb_time = time.time() - start_time
opencv_alb_times.append(opencv_alb_time)
print(str(opencv_alb_time) + ' sec')

Le résultat est le suivant.

98.37442970275879 sec
70.52895092964172 sec
66.72178149223328 sec
61.30395317077637 sec
68.30901885032654 sec
69.6796133518219 sec
71.02722263336182 sec
70.88462662696838 sec
70.54376363754272 sec
65.67756700515747 sec
1min 11s ± 1.74 s per loop (mean ± std. dev. of 2 runs, 5 loops each)

jpeg4py + Albumentations Chaque fois que j'écris un article de ce genre, certaines personnes me disent d'essayer d'utiliser jpeg4py, je vais donc le mesurer également comme une base de référence. Eh bien, si le format d'image est jpeg, il n'y a aucun moyen de ne pas l'utiliser. Première de l'installation

install_jpeg4py.sh


!apt-get install libturbojpeg
!pip install jpeg4py

Alors voici le code. C'est presque la même chose sauf pour la lecture des données.

jpeg4py_alb.py


import time
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd
import cv2
import albumentations as A
import jpeg4py as jpeg


class DogDataset(Dataset):
    def __init__(self, transform=None):
        self.img_list = pd.read_csv('../input/dog-breed-identification/labels.csv')
        self.transform = transform
        
        breeds=list(self.img_list['breed'].unique())
        self.breed2idx = {b: i for i, b in enumerate(breeds)}

    def __len__(self):
        return len(self.img_list)

    def __getitem__(self, idx):
        img_row = self.img_list.iloc[idx]
        image = jpeg.JPEG('../input/dog-breed-identification/train/' + img_row['id'] + '.jpg').decode()
        label = self.breed2idx[img_row['breed']]

        if self.transform is not None:
            image = self.transform(image=image)
        image = torch.from_numpy(image['image'].transpose(2, 0, 1))
        return image, label

transform = A.Compose([A.RandomResizedCrop(height=224, width=224, p=1),
                       A.HorizontalFlip(p=0.5),
                       A.VerticalFlip(p=0.5),
                       A.MotionBlur(blur_limit=3, p=1),
                       A.Rotate(limit=45, p=1),
                       A.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5), max_pixel_value=255.0, always_apply=True, p=1.0)])

data_loader = DataLoader(DogDataset(transform=transform), batch_size=64, shuffle=True, num_workers=2)

Le code pour lire et mesurer le temps est presque le même que le précédent, je vais donc l'omettre. Le résultat est le suivant. Après tout, jpeg4py est rapide.

43.14848828315735 sec
42.78340029716492 sec
41.33797478675842 sec
43.24748754501343 sec
41.11549472808838 sec
41.17329430580139 sec
40.58435940742493 sec
41.16935634613037 sec
40.92542815208435 sec
39.6163330078125 sec
41.5 s ± 816 ms per loop (mean ± std. dev. of 2 runs, 5 loops each)

jpeg4py + Kornia Utilisez Kornia pour l'augmentation des données. Cela permet de traiter l'augmentation des données sur le GPU. Seul le premier recadrage aléatoirement redimensionné utilise des albumentations afin que vous puissiez créer un lot de tenseurs avec des images bien formées. Puisque Kornia peut traiter chaque lot, l'augmentation des données est exécutée pour le lot chargé par DataLoader.

Le code est ici.

jpeg4py_kornia.py


import time
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd
import numpy as np
import cv2
import jpeg4py as jpeg

import albumentations as A
import kornia.augmentation as K
import torch.nn as nn


class DogDataset(Dataset):
    def __init__(self, transform=None):
        self.img_list = pd.read_csv('../input/dog-breed-identification/labels.csv')
        self.transform = transform
        
        breeds=list(self.img_list['breed'].unique())
        self.breed2idx = {b: i for i, b in enumerate(breeds)}

    def __len__(self):
        return len(self.img_list)

    def __getitem__(self, idx):
        img_row = self.img_list.iloc[idx]
        image = jpeg.JPEG('../input/dog-breed-identification/train/' + img_row['id'] + '.jpg').decode()
        label = self.breed2idx[img_row['breed']]

        if self.transform is not None:
            image = self.transform(image=image)
        image = torch.from_numpy(image['image'].transpose(2, 0, 1).astype(np.float32))
        return image, label

alb_transform = A.Compose([A.RandomResizedCrop(height=224, width=224, p=1)])

mean_std = torch.Tensor([0.5, 0.5, 0.5])*255
kornia_transform = nn.Sequential(
    K.RandomHorizontalFlip(),
    K.RandomVerticalFlip(),
    K.RandomMotionBlur(3, 35., 0.5),
    K.RandomRotation(degrees=45.0),
    K.Normalize(mean=mean_std,std=mean_std)
)

data_loader = DataLoader(DogDataset(transform=alb_transform), batch_size=64, shuffle=True, num_workers=2)

La lecture sera la suivante. Vous pouvez voir que la conversion est effectuée par lots après avoir été chargée à partir du chargeur de données.

jpeg4py_kornia_time.py


%%timeit -r 2 -n 5
jpeg4py_kornia_times = []
start_time = time.time()
for image, label in data_loader:
    image = kornia_transform(image.cuda())
    label = label.cuda()
    pass
jpeg4py_kornia_time = time.time() - start_time
jpeg4py_kornia_times.append(jpeg4py_kornia_time)
print(str(jpeg4py_kornia_time) + ' sec')

Le résultat est le suivant. Cela devient assez rapide.

28.150899171829224 sec
24.104888916015625 sec
25.490058183670044 sec
24.111201763153076 sec
22.999730587005615 sec
25.16165590286255 sec
26.496272325515747 sec
27.150801420211792 sec
28.757362365722656 sec
29.331339836120605 sec
26.2 s ± 1.2 s per loop (mean ± std. dev. of 2 runs, 5 loops each)

DALI + Kornia C'est la combinaison la plus rapide à laquelle je puisse penser en ce moment. J'ai écrit cet article parce que je voulais dire ceci. Si vous utilisez DALI, vous pouvez utiliser le GPU à partir de l'étape de chargement de l'image, et quand il est chargé, vous pouvez dire que l'image est déjà sur le GPU. Cela a rendu les Albumentations etc. difficiles à utiliser, et il était difficile à utiliser car il y a peu de types d'Augmentations implémentées dans DALI, mais cette combinaison a été réalisée parce que l'augmentation sur GPU par Kornia a atteint un niveau pratique. Installez d'abord NVIDIA DALI.

install_dali.sh


!pip install --extra-index-url https://developer.download.nvidia.com/compute/redist nvidia-dali-cuda100

Et voici le code. La couleur des cheveux est un peu différente des précédentes. C'est comme définir un pipeline dans DALI, le construire et créer un itérateur qui renvoie un PyTorch Tensor. RandomResizedCrop sera effectué dans DALI, puis dans Kornia.

dali_kornia.py


import time
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd

import kornia.augmentation as K
import torch.nn as nn

from nvidia.dali.pipeline import Pipeline
from nvidia.dali.plugin.pytorch import DALIGenericIterator
import nvidia.dali.ops as ops
import nvidia.dali.types as types

class DALIPipeline(Pipeline):
    def __init__(self, batch_size, num_threads, device_id):
        super(DALIPipeline, self).__init__(batch_size, num_threads, device_id)
        self.img_list = pd.read_csv('../input/dog-breed-identification/labels.csv')

        breeds=list(self.img_list['breed'].unique())
        self.breed2idx = {b: i for i, b in enumerate(breeds)}
        
        self.img_list['label'] = self.img_list['breed'].map(self.breed2idx)
        self.img_list['data'] = '../input/dog-breed-identification/train/' + self.img_list['id'] + '.jpg'
        
        self.img_list[['data', 'label']].to_csv('dali.txt', header=False, index=False, sep=' ')
        
        self.input = ops.FileReader(file_root='.', file_list='dali.txt')
        self.decode = ops.ImageDecoder(device = "mixed", output_type = types.DALIImageType.RGB)
        #self.decode = ops.ImageDecoderRandomCrop(device = "mixed", output_type = types.DALIImageType.RGB)
        self.resize = ops.RandomResizedCrop(device = "gpu", size=(224, 224))
        self.transpose = ops.Transpose(device='gpu', perm = [2, 0, 1])
        self.cast = ops.Cast(device='gpu', dtype=types.DALIDataType.FLOAT)

    def define_graph(self):
        images, labels = self.input(name="Reader")
        images = self.decode(images)
        images = self.resize(images)
        images = self.cast(images)
        output = self.transpose(images)
        return (output, labels)

def DALIDataLoader(batch_size):
    num_gpus = 1
    pipes = [DALIPipeline(batch_size=batch_size, num_threads=2, device_id=device_id) for device_id in range(num_gpus)]

    pipes[0].build()
    dali_iter = DALIGenericIterator(pipelines=pipes, output_map=['data', 'label'], 
                                    size=pipes[0].epoch_size("Reader"), reader_name=None, 
                                    auto_reset=True, fill_last_batch=True, dynamic_shape=False, 
                                    last_batch_padded=True)
    return dali_iter

data_loader = DALIDataLoader(batch_size=64)

mean_std = torch.Tensor([0.5, 0.5, 0.5])*255
kornia_transform = nn.Sequential(
    K.RandomHorizontalFlip(),
    K.RandomVerticalFlip(),
    K.RandomMotionBlur(3, 35., 0.5),
    K.RandomRotation(degrees=45.0),
    K.Normalize(mean=mean_std,std=mean_std)
)

La partie lecture est la suivante.

dali_kornia_time.py


%%timeit -r 2 -n 5
dali_kornia_times = []
start_time = time.time()
for feed in data_loader:
    # image is already on GPU
    image = kornia_transform(feed[0]['data'])
    label = feed[0]['label'].cuda()
    pass
dali_kornia_time = time.time() - start_time
dali_kornia_times.append(dali_kornia_time)
print(str(dali_kornia_time) + ' sec')

Le résultat est le suivant: C'est explosif! C'est trop rapide! C'est déjà une violation de vitesse!

8.865531921386719 sec
7.996037721633911 sec
8.494542598724365 sec
8.241464853286743 sec
8.093241214752197 sec
8.12808108329773 sec
7.846079587936401 sec
7.849750280380249 sec
7.848227024078369 sec
7.633721828460693 sec
8.1 s ± 238 ms per loop (mean ± std. dev. of 2 runs, 5 loops each)

Résultats agrégés

J'ai essayé de créer facilement un graphique à barres. ダウンロード.png

à la fin

Cette fois, j'ai présenté la méthode de chargement explosif de données et d'augmentation des données à l'aide de NVIDIA DALI et Kornia. L'augmentation de Kornia est également inférieure au niveau des Albumentations, mais la feuille de route du problème comprend des descriptions qui sont des Albumentations à l'esprit, donc j'attends avec impatience cela dans le futur! https://github.com/kornia/kornia/issues/434

Recommended Posts

[Challenger à la recherche] Le chargement et l'augmentation des données les plus rapides (bloc-notes Kaggle) je pense
Je voulais juste extraire les données de la date et de l'heure souhaitées avec Django
J'ai essayé la même analyse de données avec kaggle notebook (python) et PowerBI en même temps ②
J'ai essayé la même analyse de données avec kaggle notebook (python) et PowerBI en même temps ①
Le modèle de projet Python auquel je pense.
J'ai lu et implémenté les variantes de UKR
Il est temps de réfléchir sérieusement à la définition et aux compétences des data scientists
le noyau du notebook jupyter ne peut plus se connecter
Pensez à la nouvelle génération de Rack et WSGI
J'ai vérifié les versions de Blender et Python
J'ai examiné le mappage de données entre ArangoDB et Java
J'ai essayé d'utiliser l'API de Sakenowa Data Project
J'ai analysé les données de combat de rang du bouclier d'épée Pokemon et je les ai visualisées sur Tableau