Обучение CIFAR классификатора

Запустить в Google Colab

Roundicons.com Посмотреть на GitHub

Скачать ноутбук

Введение

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

Рекомендуется сперва пройти Обучение MNIST классификатора.

Обучающая выборка

Скачайте датасет CIFAR c оригинального сайта университета Торонто, где был собран этот датасет. Этот датасет состоит из 60000 цветных изображений размера 32 на 32. На каждом изображении представлен объект одного из десяти типов: самолёт, автомобиль, птица, кошка, олень, собака, лягушка, лошадь, корабль, грузовик. На каждый класс приходится ровно 6000 изображений.

100 изображений для всех 10 категорий из датасета CIFAR-10
100 изображений для всех 10 категорий из датасета CIFAR-10

Этот датасет, как и MNIST, считается одним из базовых в машинном обучении: на нём тестируются различные методы машинного обучения до того, как их можно будет масштабировать.

Реализация инструментами библиотеки

Начало этого туториала практически такое же, как и для MNIST туториала:

import os
import math

import numpy as np

from PuzzleLib.Datasets import Cifar10Loader

from PuzzleLib.Containers import Sequential
from PuzzleLib.Modules import Conv2D, MaxPool2D, Activation, Flatten, Linear
from PuzzleLib.Modules.Activation import relu
from PuzzleLib.Handlers import Trainer, Validator
from PuzzleLib.Optimizers import MomentumSGD
from PuzzleLib.Cost import CrossEntropy

from PuzzleLib.Visual import showImageBasedFilters, showFilters

Появились новые импорты библиотеки math и функции showImageBasedFilters - их появление мы объясним дальше. Также в импортах присутствует загрузчик CIFAR Cifar10Loader, аналогичный по смыслу загрузчику MNIST.

Загрузим данные:

cifar10 = Cifar10Loader()
path = "../TestData/"
data, labels = cifar10.load(path=path)
data, labels = data[:], labels[:]
print("Loaded cifar10")

np.random.seed(1234)

Important

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

Построение сети

Для соединения модулей между собой здесь используется контейнер Sequential:

seq = Sequential()

А дальше начинаются первые отличия от MNIST туториала, на которые стоит обратить пристальное внимание. Архитектура сети будет примерно такой же, но обратите внимание на набор параметров у модулей:

seq.append(Conv2D(3, 32, 5, pad=2, wscale=0.0001, initscheme="gaussian"))

Параметр pad (паддинг) говорит свёрточному слою, какой отступ по сторонам от фотографии нужно сделать. Т. е. в этом случае фотография увеличивается на 2 пикселя слева, справа, сверху и снизу, новые пиксели заполняются нулями (это верно для каждой из карт входных данных, т. е. в случае RGB каждая из карт R, G и B будет увеличена).

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

Параметры wscale и initscheme отвечают за то, какие значения будут у весов создаваемого слоя в самом начале, до обучения. Значение "gaussian" сообщит слою, что нужно взять веса случайными нормальными, с дефолтным средним 0.0, а дисперсия будет взята равной wscale.

Почему здесь выбраны такие параметры? Примерно такая архитектура была придумана Крижевским (конкретно этот вариант идёт под лозунгом "26% за 80 секунд на Fermi-поколении Nvidia GPU").

Добавляем макспул с параметрами, отличающихся от параметров по умолчанию:

seq.append(MaxPool2D(size=3, stride=2))

Первый параметр - это размер фильтра, второй параметр - это шаг фильтра по фотографии. По дефолту они равны 2 и 2 соответственно.

Дальше всё без нововведений:

seq.append(Activation(relu))

seq.append(Conv2D(32, 32, 5, pad=2, wscale=0.01, initscheme="gaussian"))
seq.append(MaxPool2D(size=3, stride=2))
seq.append(Activation(relu))

seq.append(Conv2D(32, 64, 5, pad=2, wscale=0.01, initscheme="gaussian"))
seq.append(MaxPool2D(size=3, stride=2))
seq.append(Activation(relu))

seq.append(Flatten())

А вот инициализация линейного слоя значительно отличается от MNIST туториала:

seq.append(Linear(seq.dataShapeFrom((1, 3, 32, 32))[1], 64, wscale=0.1, initscheme="gaussian"))

Здесь размер линейного слоя устанавливается через то, что вернёт нам метод dataShapeFrom. Это метод, который есть у многих модулей нейронных слоёв: ему на вход передаётся размер входных данных (число изображений, число карт в изображении, высота карты, ширина карты), а на выходе он выдаёт размер данных после прохода через данный слой. Например, слой Activation при обращении к этому методу вернёт тот же размер, что в него был передан, так как слои активации не меняют размеров данных. А вот свёрточный слой или макспул-слой могут изменить размер данных.

Чтобы не ломать голову, какой получился размер данных перед входом в линейных слой, мы запросили у seq размер данных на выходе из него, после чего взяли компоненту размера №1 (размер - это 4 числа, например, (1, 3, 32, 32)): в этой компоненте после метода Flatten лежит полная размерность каждого из элементов данных (т.е. в компоненте №0 будет число обучающих образцов, в №1 - размер образца, в №2 - число 1, в №3 - число 1, так как Flatten всё расплющил).

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

И наконец последние блоки сети:

seq.append(Activation(relu))

seq.append(Linear(64, 10, wscale=0.1, initscheme="gaussian"))

Подготовка к обучению и обучение сети

Выбор функции ошибки, оптимизатора и установка его на сеть:

optimizer = MomentumSGD()
optimizer.setupOn(seq, useGlobalState=True)
optimizer.learnRate = 0.01
optimizer.momRate = 0.9

cost = CrossEntropy(maxlabels=10)

trainer = Trainer(seq, cost, optimizer)

validator = Validator(seq, cost)

Всё как и в туториале по MNIST, только появилась строчка:

currerror = math.inf

Которая создаёт переменную currerror, в которую мы будем записывать ошибку сети на данных в каждой эпохе. Она нам пригодится для того, чтобы менять скорость обучения, learnRate, у нашего оптимизатора. Сперва поставили ошибку равной плюс бесконечности, так как хотим хоть с чем-то сравнить ошибку уже в нулевой эпохе обучения.

Обучение сети

Запускаем обучение на 25 эпохах:

for i in range(25):
    trainer.trainFromHost(
    data[:50000], labels[:50000], macroBatchSize=50000,
    onMacroBatchFinish=lambda train: print("Train error: %s" % train.cost.getMeanError())
    )
    valerror = validator.validateFromHost(data[50000:], labels[50000:], macroBatchSize=10000)
    Accuracy = 1.0 - valerror
    print("Accuracy:", Accuracy)

Далее идут строчки, которые раньше не встречались:

    if valerror >= currerror:
        optimizer.learnRate *= 0.5
        print("Lowered learn rate: %s" % optimizer.learnRate)

    currerror = valerror

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

Далее, поскольку CIFAR - цветной, мы можем первый слой сети отобразить в цвете. Т. е. для каждого из трёх каналов RGB мы берём соответствующие им фильтры, после чего накладываем их друг на друга (фильтры R - как красные, G - как зелёную компоненту, B - голубую). Сделать это можно только для первого слоя, так как дальше уже теряется привязка к одному из цветов RGB. Эту процедуру отображения фильтров для первого слоя проводит метод showImageBasedFilters:

    showImageBasedFilters(seq[0].W.get(), os.path.join(path, "conv1.png"))

Кроме первого слоя, мы выводим в файл второй и третий свёрточный слои:

    showFilters(seq[3].W.get(), os.path.join(path, "conv2.png"))
    showFilters(seq[6].W.get(), os.path.join(path, "conv3.png"))

После 25 эпох обучения получаются вот такие изображения:

Фильтры первого свёрточного слоя сети lenet (все 3x32 штуки в формате RGB)
Фильтры первого свёрточного слоя сети lenet (все 3x32 штуки в формате RGB)

Фильтры второго свёрточного слоя сети lenet (все 32x32 штуки в ч/б-формате)
Фильтры второго свёрточного слоя сети lenet (все 32x32 штуки в ч/б-формате)

Фильтры третьего свёрточного слоя сети lenet (все 32x64 штуки в ч/б-формате)
Фильтры третьего свёрточного слоя сети lenet (все 32x64 штуки в ч/б-формате)

Точность, с которой сеть научилась классифицировать изображения:

print("Accuracy:", Accuracy)
Accuracy: 0.7629
Как видите, эта задача для нейронных сетей заметно сложнее, чем MNIST. Сеть смогла правильно определить всего 76% изображений.