Глубокое обучение и нейронные сети с Python и Pytorch. Часть V: сверточные нейронные сети

Предыдущая статья — Глубокое обучение и нейронные сети с Python и Pytorch. Часть IV: обучение нашей нейронной сети.

Сверточные нейронные сети на Pytorch

Теперь, когда мы изучили базовую полносвязанную нейронную сеть прямого распространения, самое время перейти к новой теме: сверточным нейронным сетям (convolutional neural network, иногда используется сокращение convnet или cnn).

Сверточные нейронные сети были созданы для работы с изображениями. Основная задача при работе с изображениями состоит в распознавании на них различных объектов, таких как люди, животные, машины и так далее.

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

Проиллюстрируем этот процесс на примере изображения кота.

Далее смоделируем в нем пиксели:

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

Затем эти свойства объединяются в одно в новой карте признаков.

Далее мы сдвигаем окно и повторяем операцию, получая при этом новую карту признаков.

Продолжаем этот процесс, пока не охватим все изображение.

Теперь мы будем делать пулинг (или операцию подвыборки). Допустим, наша свертка дала нам следующую матрицу (мы забыли вставить число в последний столбец второго ряда, пусть это будет, например, 3):

Сейчас берем окно для пулинга (или фильтра) размером 3 Х 3:

Самая распространенная форма операции пулинга называется макспулинг (Max-pooling). В этом случае мы просто берем максимальное число из окна и оно становиться новым значением этого сегмента.

Продолжая данный процесс до конца, мы получим что-то типа этого:

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

Итак, по своей сути, сверточные нейронные сети сначала ищут низкоуровневые объекты, такие как линии и кривые, затем ищут их комбинации и так далее, до конца.

Давайте попробуем в этом разобраться на конкретном примере сверточной нейронной сети.

Получение данных

Для начала нам нужен датасет. Здесь мы будем использовать Cats vs Dogs. Этот набор данных состоит из изображений кошек и собак различных пород, возрастов и размеров.

Загрузив датасет, вам необходимо его распаковать. Мы советуем поместить его прямо в вашу рабочую директорию.

Подготовка данных

Помните, раньше мы писали, что использование torchvision было жульничеством? Ну вот, а теперь нам нужно подготовить данные самостоятельно. Для начала убедимся, что у нас есть все необходимые библиотеки:

pip install opencv-python numpy tqdm matplotlib

Теперь произведем импорт библиотек:

import os
import cv2
import numpy as np
from tqdm import tqdm

Теперь начнем писать класс, который будет отвечать за обработку наших данных:

class DogsVSCats():
    IMG_SIZE = 50
    CATS = "PetImages/Cat"
    DOGS = "PetImages/Dog"
    TESTING = "PetImages/Testing"
    LABELS = {CATS: 0, DOGS: 1}
    training_data = [] 

Поле IMG_SIZE может принимать какое угодно значение, но мы должны остановиться на чем-то одном. Все изображения в нашем наборе данных разные по форме и по размерам. Нам нужно привести их все к одному и тому же размеру. Мы выбрали 50Х50.

Далее идут переменные, содержащие директории с изображениями кошек и собак. После распаковки архива с данными у вас должны быть две директории: одна с изображениями кошек, а другая, соответственно, с изображениями собак.

Мы хотим пройтись по этим двум директориям, взять изображения, привести их к одному размеру, отмасштабировать и поставить им в соответствие численный класс (cats = 0, dogs = 1). А затем добавить это все в переменную training_data.

Продолжая работу с кодом нашего класса, давайте добавим метод под названием make_training_data:

class DogsVSCats():
    IMG_SIZE = 50
    CATS = "PetImages/Cat"
    DOGS = "PetImages/Dog"
    TESTING = "PetImages/Testing"
    LABELS = {CATS: 0, DOGS: 1}
    training_data = []

    def make_training_data(self):
        for label in self.LABELS:
            for f in tqdm(os.listdir(label)):
                if "jpg" in f:
                    try:
                        pass
                    except Exception as e:
                        pass
                        #print(label, f, str(e)) 

Пока мы лишь перебираем каталоги кошек и собак и просматриваем все изображения. Теперь давайте напишем код для обработки этих изображений:

class DogsVSCats():
    IMG_SIZE = 50
    CATS = "PetImages/Cat"
    DOGS = "PetImages/Dog"
    TESTING = "PetImages/Testing"
    LABELS = {CATS: 0, DOGS: 1}
    training_data = []

    def make_training_data(self):
        for label in self.LABELS:
            for f in tqdm(os.listdir(label)):
                if "jpg" in f:
                    try:
                        path = os.path.join(label, f)
                        img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
                        img = cv2.resize(img, (self.IMG_SIZE, self.IMG_SIZE))
                        self.training_data.append([np.array(img), np.eye(2)[self.LABELS[label]]])  # do something like print(np.eye(2)[1]), just makes one_hot 
                        #print(np.eye(2)[self.LABELS[label]])
                    except Exception as e:
                        pass
                        #print(label, f, str(e)) 

Мы считываем данные, преобразуем изображение в черно-белое с оттенками серого, меняем размер изображения на выбранный нами ранее, а затем добавляем даные изображения вместе с определяющим принадлежность к кошкам или собакам классом в переменную training_data. Некоторые изображения не читаются, поэтому мы просто бросаем исключение. Если у вас не создаются изображения, то эти ошибки можно вывести на экран. Но у нас достаточно изображений, чтобы не беспокоиться о нескольких исключениях.

Итак, у нас есть данные, что еще нам необходимо с ними сделать?

Нам нужно проверить, сбалансированы ли они (равное ли количество кошек и собак) и перемешать их. Для проверки балансировки введем счетчик:

if label == self.CATS:
    self.catcount += 1
elif label == self.DOGS:
    self.dogcount += 1

Затем перемешаем список training_data следующим образом:

np.random.shuffle(self.training_data)

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

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

На данный момент наш код выглядит следующим образом:

REBUILD_DATA = True # set to true to one once, then back to false unless you want to change something in your training data.

class DogsVSCats():
    IMG_SIZE = 50
    CATS = "PetImages/Cat"
    DOGS = "PetImages/Dog"
    TESTING = "PetImages/Testing"
    LABELS = {CATS: 0, DOGS: 1}
    training_data = []

    catcount = 0
    dogcount = 0

    def make_training_data(self):
        for label in self.LABELS:
            print(label)
            for f in tqdm(os.listdir(label)):
                if "jpg" in f:
                    try:
                        path = os.path.join(label, f)
                        img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
                        img = cv2.resize(img, (self.IMG_SIZE, self.IMG_SIZE))
                        self.training_data.append([np.array(img), np.eye(2)[self.LABELS[label]]])  # do something like print(np.eye(2)[1]), just makes one_hot 
                        #print(np.eye(2)[self.LABELS[label]])

                        if label == self.CATS:
                            self.catcount += 1
                        elif label == self.DOGS:
                            self.dogcount += 1

                    except Exception as e:
                        pass
                        #print(label, f, str(e))

        np.random.shuffle(self.training_data)
        np.save("training_data.npy", self.training_data)
        print('Cats:',dogsvcats.catcount)
        print('Dogs:',dogsvcats.dogcount)

if REBUILD_DATA:
    dogsvcats = DogsVSCats()
    dogsvcats.make_training_data() 
PetImages/Cat
100%|a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^| 12501/12501 [00:13<00:00, 918.70it/s]
PetImages/Dog
100%|a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^a-^| 12501/12501 [00:14<00:00, 862.58it/s]
Cats: 12476
Dogs: 12470

После создания обработанного набора данных у вас должен появиться файл training_data.npy. Чтобы его использовать, нам нужно действовать следующим образом:

training_data = np.load("training_data.npy", allow_pickle=<strong>True</strong>)
print(len(training_data))
24946

Теперь нам нужно разбить наши данные на X и y (входящие данные и целевая переменная), а также преобразовать их в тензоры.

import torch

X = torch.Tensor([i[0] for i in training_data]).view(-1,50,50)
X = X/255.0
y = torch.Tensor([i[1] for i in training_data])

Теперь давайте взглянем на один из наших элементов:

import matplotlib.pyplot as plt

plt.imshow(X[0], cmap="gray")
<matplotlib.image.AxesImage at 0x7ff110c5bb50>
print(y[0])
tensor([1., 0.])

Отлично! Теперь у нас есть данные в виде входящих изображений и целевых переменных.

Далее нам еще раз придется поделить эти данные на обучающие и тестовые, но следующее, что мы хотим сделать, это построить нашу нейронную сеть. Этим мы и займемся в следующей статье.

Следующая статья — Глубокое обучение и нейронные сети с Python и Pytorch. Часть VI: модель сверточной нейронной сети.