Трансформации transform. Класс ImageFolder

Смотреть материал на YouTube | RuTube

На прошлом занятии мы с вами объявили и использовали свой собственный класс DigitDataset для извлечения образов выборки изображений строго определенной структуры. В действительности это типовая организация данных, когда изображения распределены по отдельным папкам, каждая для своего класса. Поэтому в PyTorch существует специальный класс ImageFolder:

from torchvision.datasets import ImageFolder

который делает практически то же самое, что и созданный нами класс DigitDataset. Давайте посмотрим, как можно им воспользоваться.

После импорта в программе достаточно создать объект класса ImageFolder с помощью следующей команды:

to_tensor = tfs.ToImage()
d_train = ImageFolder("dataset/train", transform=to_tensor)

Первым аргументом указываем путь к набору папок с классами изображений, а вторым – преобразования, которые хотим выполнить с загруженными изображениями. В частности, выполняем преобразование объекта PIL в тензор PyTorch с помощью трансформации ToImage.

Все, датасет у нас сформирован, и мы можем использовать его при создании объекта класса DataLoader так, как делали это и раньше:

train_data = data.DataLoader(d_train, batch_size=32, shuffle=True)

Давайте посмотрим, что из себя представляет объект d_train. С ним так же можно выполнять команды индексирования:

x, y = d_train[0]

Причем, тензор изображения x будет иметь размерность (3, 28, 28):

x.size() # (3, 28, 28)

а y – обычное целое число (метка класса).

Обратите внимание, что изображение по первой оси содержит три элемента, т.е. три матрицы 28x28 элементов. Это три цветовые компоненты RGB. Класс ImageFolder всегда загружаемые изображения представляет, как полноцветные в формате RGB, даже если они изначально имеют всего один цветовой канал, как в нашем случае. Кроме того, тензор x имеет тип элементов uint8:

x.dtype # torch.uint8

Как же нам сформировать одномерный тензор из 784 вещественных чисел в диапазоне [0; 1]? Для этого необходимо к изображению применить последовательно несколько преобразований. Для этого в PyTorch существует специальный класс Compose, расположенный в ветке:

torchvision.transforms.v2.Compose

Обратите внимание, на модуль v2. В ранних версиях PyTorch преобразования брались из ветки:

torchvision.transforms

Но теперь она считается устаревшей и лучше использовать классы из модуля v2. Такие преобразования работают быстрее и более разнообразны.

Итак, используя класс Compose, мы можем определить набор преобразований, применяемых к тензору, следующим образом:

to_tensor = tfs.Compose([tfs.ToImage(), tfs.ToDtype(torch.float32, scale=True)])

В качестве аргумента передается список из трансформаций. Причем отрабатывать они будут строго в указанном порядке, то есть, сначала ToImage, а затем, ToDtype. Второе новое преобразование меняет тип элементов тензора на указанный torch.float32, а аргумент scale=True задает масштабирование значений тензора в диапазоне [0; 1]. Более ничего в программе менять не нужно. После запуска команда x.dtype вернет тип данных torch.float32:

x, y = d_train[0]
x.dtype # torch.float32

Правда размер тензора пока остается прежним:

x.size() # torch.Size([3, 28, 28])

Чтобы оставить один цветовой канал, то есть, перевести изображение в градации серого, пропишем еще одно преобразование Grayscale непосредственно после ToImage:

to_tensor = tfs.Compose([tfs.ToImage(),
                         tfs.Grayscale(),
                         tfs.ToDtype(torch.float32, scale=True)])

Теперь размер тензора x равен (1, 28, 28):

x, y = d_train[0]
x.size() # torch.Size([1, 28, 28])

Наконец, нам нужно вытянуть этот тензор в один вектор. Для этого мы воспользуемся классом Lambda с указанием лямбда-функции. Этой функции в качестве аргумента _img передается тензор, который вытягивается в вектор с помощью метода ravel():

to_tensor = tfs.Compose([tfs.ToImage(),
                         tfs.Grayscale(),
                         tfs.ToDtype(torch.float32, scale=True),
                         tfs.Lambda(lambda _img: _img.ravel())])

Обратите внимание, что это преобразование лучше записать последним, так как после изменения формы тензора преобразования с ним, как с изображением, станут невозможны.

В результате этих четырех преобразований мы получаем желаемое представление входных данных, которые можно подавать НС для ее обучения и тестирования:

neuro_net_21.py: https://github.com/selfedu-rus/neuro-pytorch

Давайте пропишем еще одну распространенную трансформацию Normalize, которая выполняет нормализацию значений входного тензора по формуле:

output[channel] = (input[channel] - mean[channel]) / std[channel]

где channel – индекс цветового канала изображения. Воспользуемся классом Normalize следующим образом:

to_tensor = tfs.Compose([tfs.ToImage(),
                         tfs.Grayscale(),
                         tfs.ToDtype(torch.float32, scale=True),
                         tfs.Normalize(mean=[0.5], std=[0.5]),
                         tfs.Lambda(lambda _img: _img.ravel())])

У нас один цветовой канал (градации серого), поэтому в параметрах mean и std указывается список с одним значением. В результате все изображение 28x28 пикселей преобразуется по формуле:

output[0] = (input[0] - 0.5) / 0.5 = (input[0] - 0.5) * 2

Обратите внимание на порядок трансформаций. Вначале PIL-изображение переводится в тензор PyTorch, затем преобразуется градации серого, после этого меняется тип элементов с масштабированием значений к диапазону [0; 1], следом идет нормализация и вытягивание в один вектор. Например, если нормализацию поставить до преобразования к типу torch.float32, то очевидно будет ошибка, т.к. целочисленные значения нормировать было бы не правильно. Вообще рекомендуется следующий порядок трансформаций:

to_tensor = tfs.Compose([tfs.ToImage(),
                         tfs.ToDtype(torch.uint8),
                         # здесь различные трансформации с пикселами изображений
                         tfs.ToDtype(torch.float32, scale=True),
                         # здесь нормализация данных и изменение формы тензора
                         ])

Сами же изображения можно подвергать самым разным трансформациям. Например:

  • ColorJitter(brightness=яркость, contrast=контрастность, saturation=насыщенность). Выполняет изменение яркости, контрастности и насыщенности изображения.
  • RandomRotation(degrees=градусы). Выполняет случайный поворот изображения на угол в пределах [-degrees; degrees] градусов.
  • RandomResizedCrop(size=(ширина, высота)). Применяет случайную обрезку (кадрирование) масштабированного изображения.
  • RandomHorizontalFlip(). Выполняет случайное отражение изображения в горизонтальном направлении.

И это лишь малая часть классов преобразований, с которыми подробно можно ознакомиться на странице официальной документации:

https://pytorch.org/vision/main/transforms.html

Создание собственных классов трансформаций

Несмотря на большое разнообразие различных трансформаций, на практике все же может понадобиться разработать свое собственное преобразование значений элементов тензора. И PyTorch позволяет нам это делать. Для этого достаточно объявить класс, унаследованный от класса Module и переопределить один обязательный метод forward:

class RavelTransform(nn.Module):
    def forward(self, item):
        # здесь преобразование тензора item

Пусть трансформация RavelTransform просто вытягивает тензор в вектор. Реализация будет следующая:

class RavelTransform(nn.Module):
    def forward(self, item):
        return item.ravel()

Добавим объект этого класса в список преобразований:

to_tensor = tfs.Compose([tfs.ToImage(),
                         tfs.ToDtype(torch.uint8),
                         tfs.Grayscale(),
                         tfs.ToDtype(torch.float32, scale=True),
                         tfs.Normalize(mean=[0.5], std=[0.5]),
                         RavelTransform()])

И он успешно выполняет свою функцию наряду со всеми остальными стандартными трансформациями.

Вот так достаточно просто можно определять свои собственные преобразования и использовать существующие.

Видео по теме