На прошлом
занятии мы с вами объявили и использовали свой собственный класс 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 будет иметь
размерность (3, 28, 28):
а y – обычное целое
число (метка класса).
Обратите
внимание, что изображение по первой оси содержит три элемента, т.е. три матрицы
28x28 элементов.
Это три цветовые компоненты RGB. Класс ImageFolder всегда
загружаемые изображения представляет, как полноцветные в формате RGB, даже если они
изначально имеют всего один цветовой канал, как в нашем случае. Кроме того,
тензор x имеет тип
элементов 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()])
И он успешно
выполняет свою функцию наряду со всеми остальными стандартными трансформациями.
Вот так
достаточно просто можно определять свои собственные преобразования и
использовать существующие.