Ознакомьтесь с полной реализацией PyTorch на наборе данных в других моих статьях (pt.1, pt.2).

  1. Введение

Через некоторое время, используя встроенные наборы данных, такие как MNIS и CIFAR, которые загружаются непосредственно из распространенных платформ машинного обучения, вы попрактиковались в создании своих первых классификаторов изображений для глубокого обучения. Это естественный шаг к использованию собственных наборов данных. Возможно, у вас есть очень специфический вариант использования на работе и вы хотите обучить пользовательскую модель на основе базы данных изображений вашей компании. Или вы просто хотите попрактиковаться с другими вариантами использования изображений, взятых из Интернета. К счастью, вы можете (довольно) легко сделать это с помощью PyTorch, просто аккуратно организовав изображения в папках.

В этой статье используется набор данных Intel Image Classification, который можно найти здесь. После загрузки изображения одного и того же класса группируются внутри папки, названной в честь класса (например, здания, лес...), и эти метки классов совпадают в наборах для обучения и тестирования. На практике это распространенный способ организации изображений в папки, поэтому он служит хорошим образцом набора данных. Здесь все помещается в родительскую папку с именем data , как показано на скриншоте ниже.

2. Подготовка файла аннотации

Во-первых, мы определяем каталоги папок, которые содержат данные обучения и тестирования.

train_folder = r'.\data\seg_train'
test_folder = r'.\data\seg_test'

Чтобы позже создать пользовательский набор данных, полезно найти способ организовать изображения в файл аннотаций, чтобы мы могли использовать его, чтобы указать PyTorch, что определенное изображение с определенным именем принадлежит определенному классу. Конечно, нецелесообразно переименовывать все изображения в папке класса выше в одно и то же имя класса с некоторым числовым идентификатором, чтобы отличать их друг от друга. Следовательно, мы будем использовать имя папки в качестве имени класса, а полный путь к изображению будет служить уникальным адресом к изображению.

Это можно сделать с помощью функции build_csv ниже. Объяснение каждого шага включено в комментарии. По сути, функция начинается с записи меток столбцов в пустой CSV-файл, затем просматривает каждую папку в указанном каталоге (например, папку для обучения) и извлекает имена подпапок в качестве имен классов. Затем полные пути ко всем изображениям вместе с соответствующими именами классов (и индексами) записываются в CSV-файл построчно.

def build_csv(directory_string, output_csv_name):
    """Builds a csv file for pytorch training from a directory of folders of images.
    Install csv module if not already installed.
    Args: 
    directory_string: string of directory path, e.g. r'.\data\train'
    output_csv_name: string of output csv file name, e.g. 'train.csv'
    Returns:
    csv file with file names, file paths, class names and class indices
    """
    import csv
    directory = directory_string
    class_lst = os.listdir(directory) #returns a LIST containing the names of the entries (folder names in this case) in the directory.
    class_lst.sort() #IMPORTANT 
    with open(output_csv_name, 'w', newline='') as csvfile:
        writer = csv.writer(csvfile, delimiter=',')
        writer.writerow(['file_name', 'file_path', 'class_name', 'class_index']) #create column names
        for class_name in class_lst:
            class_path = os.path.join(directory, class_name) #concatenates various path components with exactly one directory separator (‘/’) except the last path component. 
            file_list = os.listdir(class_path) #get list of files in class folder
            for file_name in file_list:
                file_path = os.path.join(directory, class_name, file_name) #concatenate class folder dir, class name and file name
                writer.writerow([file_name, file_path, class_name, class_lst.index(class_name)]) #write the file path and class name to the csv file
    return

build_csv(train_folder, 'train.csv')
build_csv(test_folder, 'test.csv')
train_df = pd.read_csv('train.csv')
test_df = pd.read_csv('test.csv')

После определения этой вспомогательной функции мы создаем два файла аннотаций csv, один для обучения и один для тестирования. Затем они повторно импортируются в виде двух фреймов данных для последующих шагов. Выходной csv выглядит примерно так.

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

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

Еще одно важное примечание выделено как IMPORTANT при сортировке списка классов в функции. Этот шаг необходим, потому что список классов, созданный the os.listdir, является произвольным и может давать разный порядок при применении к наборам данных для обучения и тестирования, и, следовательно, индекс класса в списке может не совпадать между набором данных для обучения и тестирования.

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

class_zip = zip(train_df['class_index'], train_df['class_name'])
my_list = []
for index, name in class_zip:
  tup = tuple((index, name))
  my_list.append(tup)
unique_list = list(set(my_list))
print('Training:')
print(sorted(unique_list))
print()

class_zip = zip(test_df['class_index'], test_df['class_name'])
my_list = []
for index, name in class_zip:
  tup = tuple((index, name))
  my_list.append(tup)
unique_list = list(set(my_list))
print('Testing:')
print(sorted(unique_list))

Выходные данные показывают, что имена классов и индексы согласованы.

Training:
[(0, 'buildings'), (1, 'forest'), (2, 'glacier'), (3, 'mountain'), (4, 'sea'), (5, 'street')]

Testing:
[(0, 'buildings'), (1, 'forest'), (2, 'glacier'), (3, 'mountain'), (4, 'sea'), (5, 'street')]

Наконец, мы можем просто взять имена классов либо из обучающего, либо из тестового набора данных (предпочтительно, обучающего набора, поскольку он должен содержать все классы), чтобы они служили окончательной ссылкой.

class_names = list(train_df['class_name'].unique())
['buildings', 'forest', 'glacier', 'mountain', 'sea', 'street']

3. Создание настраиваемых наборов данных для обучения и тестирования

Позаботившись о файлах аннотаций, мы создадим собственный набор данных для обучения и тестирования с классом Dataset в torch.utils.data. Из руководства по документации (ссылка) Dataset — это абстрактный класс, представляющий набор данных. Чтобы создать собственный набор данных, мы должны унаследовать сам Dataset и переопределить его методы, чтобы настроить их в соответствии с нашим вариантом использования.

Опять же, объяснение каждого шага включено в комментарии. Таким образом, мы передадим методу __init__ два важных аргумента: CSV-файл аннотации и преобразование изображения (подробнее о преобразовании изображения позже).

class IntelDataset(torch.utils.data.Dataset): # inheritin from Dataset class
    def __init__(self, csv_file, root_dir="", transform=None):
        self.annotation_df = pd.read_csv(csv_file)
        self.root_dir = root_dir # root directory of images, leave "" if using the image path column in the __getitem__ method
        self.transform = transform

    def __len__(self):
        return len(self.annotation_df) # return length (numer of rows) of the dataframe

    def __getitem__(self, idx):
        image_path = os.path.join(self.root_dir, self.annotation_df.iloc[idx, 1]) #use image path column (index = 1) in csv file
        image = cv2.imread(image_path) # read image by cv2
        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # convert from BGR to RGB for matplotlib
        class_name = self.annotation_df.iloc[idx, 2] # use class name column (index = 2) in csv file
        class_index = self.annotation_df.iloc[idx, 3] # use class index column (index = 3) in csv file
        if self.transform:
            image = self.transform(image)
        return image, class_name, class_index

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

Обратите внимание, что здесь мы используем полные пути к изображениям для чтения изображений с помощью CV2. При использовании CV2 важно отметить, что он считывает массивы пикселей изображения в порядке синий-зеленый-красный (BGR), а не в обычном порядке RGB. Таким образом, преобразование из BGR в RGB включено для последующих шагов, где используется RGB. Подробнее о CV2 и его сравнении с PIL можно узнать по ссылке ниже.



Еще одно замечание: мы оставим аргумент `root_dir` пустым (пустая строка), поскольку мы уже храним полные пути к изображениям в файле аннотаций.

Следовательно, вы можете видеть, что подготовка хорошего файла аннотации помогает хранить все, что нам нужно, аккуратно на месте.

Теперь мы завершили необходимые шаги для создания нашего собственного набора данных. Давайте создадим образец непреобразованного набора данных поезда из приведенного выше класса и визуализируем несколько случайных изображений с их именами классов и индексами в наборе итерационных данных.

#test dataset class without transformation:
train_dataset_untransformed = IntelDataset(csv_file='train.csv', root_dir="", transform=None)

#visualize 10 random images from the loaded dataset
plt.figure(figsize=(12,6))
for i in range(10):
    idx = random.randint(0, len(train_dataset_untransformed))
    image, class_name, class_index = train_dataset_untransformed[idx]
    ax=plt.subplot(2,5,i+1) # create an axis
    ax.title.set_text(class_name + '-' + str(class_index)) # create a name of the axis based on the img name
    plt.imshow(image) # show the img

4. Преобразование изображения

Обычной практикой для задач классификации изображений является преобразование входных изображений. Преобразование — это преобразование изображения из одной формы в другую с точки зрения размера, формы, диапазона пикселей и т. д., при этом основная информация об изображении остается неизменной. В идеале это направлено на повышение надежности классификатора, поскольку он подвергается воздействию многих вариаций одного и того же класса изображений, а не только «красивых».

Важным шагом является преобразование информации массива изображений в тензоры. Это формат данных, который использует PyTorch вместо массивов numpy или PIL. Окончательные тензорные массивы будут иметь вид Канал * Высота * Ширина (C * H * W), вместо исходного (H * W * C). Следовательно, если мы хотим визуализировать изображение, для восстановления порядка используется «перестановка».

Еще один важный шаг преобразования, который мы будем использовать, — это нормализация. Как вы знаете, канал обычного 24-битного цветного изображения имеет диапазон интенсивности от 0 до 255 (8 бит) с потенциально очень разными распределениями на разных изображениях. При их нормализации (обычно в диапазоне от 0 до 1) обучение будет сталкиваться с менее частыми ненулевыми градиентами, что приведет к более быстрому обучению. Более подробную информацию о нормализации и других распространенных методах преобразования можно найти по ссылкам ниже.

https://inside-machinelearning.com/en/why-and-how-to-normalize-data-object-detection-on-image-in-pytorch-part-1/



В PyTorch общие методы преобразования изображений доступны в модуле torchvision.transforms. Несколько шагов преобразования, таких как изменение размера, увеличение и нормализация, могут быть объединены в цепочку с помощью Compose. Теперь давайте создадим конвейер преобразования, затем используем его в аргументе класса набора данных transform и визуализируем некоторые результирующие изображения.

# create a transform pipeline
image_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
    transforms.Resize((224, 224), interpolation=PIL.Image.BILINEAR)
])
#create datasets with transforms:
train_dataset = IntelDataset(csv_file='train.csv', root_dir="", transform=image_transform)
test_dataset = IntelDataset(csv_file='test.csv', root_dir="", transform=image_transform)

#visualize 10 random images from the loaded transformed train_dataset
plt.figure(figsize=(12, 6))
for i in range(10):
    idx = random.randint(0, len(train_dataset))
    image, class_name, class_index = train_dataset[idx]
    ax=plt.subplot(2,5,i+1) # create an axis
    ax.title.set_text(class_name + '-' + str(class_index)) # create a name of the axis based on the img name
    #The final tensor arrays will be of the form (C * H * W), instead of the original (H * W * C), 
    # hence use permute to change the order
    plt.imshow(image.permute(1, 2, 0)) # show the img

Вы можете видеть, что эти изображения кажутся «темнее», чем исходные версии, потому что значения интенсивности теперь находятся в диапазоне от 0 до 1, а не от 0 до 255.

5. Заключение

На этом мы завершили создание собственного набора данных для обучения моделей CNN. Следите за моими будущими статьями о том, как построить Dataloader и использовать его для передачи изображений для обучения модели, а также для трансферного обучения / ансамблевого трансферного обучения.