Оптимизация сети¶
Введение¶
В этом туториале мы рассмотрим такую возможность библиотеки PuzzleLib
, как оптимизацию нейронных сетей за счет фиксации размерности входного тензора сети. Подобное ускорение работает как с обучением, так и с инференсом.
Мы замерим время обучения на одном батче неоптимизированной VGG-16 и оптимизированной и покажем, что подобная оптимизация действительно ускоряет обучение.
Методы оптимизации
Вообще говоря, существует еще несколько способов оптимизации сети, позволяющие при тех же ресурсах добиваться выигрыша в скорости работы сети как с минимальной потерей качества, так и без оной. Но оптимизировать с их помощью можно только инференс сети:
Оптимизация графа нейронной сети (fusion)
- в большинстве популярных нейросетевых архитектурах стандартизирован набор используемых модулей (Conv
,MaxPool
,Activation
,BatchNorm
и т.д.). Благодаря тому что мы знаем архитектуру сети после ее обучения, мы можем оптимизировать различные комбинации модулей из архитектуры с точки зрения вычисления инференса сети;Конвертация данных для вычислений в нейронных сетях в числа половинной точности (half precision)
- обычно данные для вычислений в нейронных сетях в Python имеют тип float32. То есть каждое число представляется в виде дробного с определенным количеством знаков после запятой и занимает в оперативной памяти 4 байта. Зачастую, такая точность после запятой не нужна. При этом, перемножение больших матриц, состоящих из float32 элементов, является достаточно трудоемкой операцией. Перевод данных из float32 в float16 сокращает вычисления без потерь в точности работы инференса нейронной сети. Каждое число с типом float16 занимает в оперативной памяти 2 байта. В итоге, при таком подходе в лучшем случае получается ускорение примерно в 2 раза;Квантизация данных для вычислений в нейронных сетях
- по аналогии с предыдущим пунктом происходит конвертация данных из float32 в int8. При этом есть, в основном, незначительные потери в точности работы инференса сети. Каждое число с типом int8 занимает в оперативной памяти всего 1 байт. В итоге, при таком подходе в лучшем случае получается ускорение инференса примерно в 4 раза.
С применением этих подходов можно ознакомиться в туториале Конвертация в TensorRT engine
Реализация инструментами библиотеки¶
Скрипт начинается с импортирования библиотеки numpy
- основной Python-библиотеки для работы с многомерными тензорами
import numpy as np
Далее происходит импорт из библиотеки PuzzleLib
всех необходимых нам классов и функций. Следующая строчка импортирует вспомогательную функцию бекэнда gpuarray
, необходимую для правильного размещения тензоров на GPU:
from PuzzleLib.Backend import gpuarray
На следующей строке импортируется функция подгрузки одной из реализованных в библиотеке архитектур нейросетей - VGG:
from PuzzleLib.Models.Nets.VGG import loadVGG
Здесь мы подгружаем модули, с помощью которых будет обучаться нейронная сеть:
from PuzzleLib.Optimizers import SGD
from PuzzleLib.Cost import CrossEntropy
from PuzzleLib.Handlers import Trainer
Оптимизаторы содержат реализации алгоритмов для обучения сетей, в костах хранятся реализации функций потерь (стоимости ошибок).
Хэндлеры - обработчики, которые являются вспомогательными объектами для обучения, валидации и стандартного вычисления выхода нейросети.
Сначала мы подгружаем архитектуру нейронной сети семейства VGG и инициализируем ее.
Значение параметра 16
задает количество слоев сети. В PuzzleLib
реализованы VGG с 11
, 16
или 19
слоями. Любые другие значения для данного параметра вызовут вывод сообщения об ошибке.i
net = loadVGG(None, "16")
16
.
Во второй строке мы формируем кортеж, содержащий размерности данных, подаваемых в нейросеть.
Первое число кортежа обозначает размер батча, второе число - количество карт в объекте (изображении) (глубина), третье и четвертое - высота и ширина объекта.
Объектом является тензор размера size
, и аналогом ему в реальных условиях могло бы быть изображение с идентичными размерностями (все изображения представляются в виде подобных тензоров).
batchsize = 16
size = (batchsize, 3, 224, 224)
В следующем блоке кода формируются обучающие данные:
Вообще говоря, для замера скорости нам не нужны настоящие данные, можно создать искусственные данные нужного формата, которые бессмысленны с точки зрения обучения сети (мы не решаем реальную задачу), но осмысленны с точки зрения замера времени обучения сети и экономии времени на поиск подходящего датасета.
С помочью функции np.random.normal
из библиотеки numpy
формируется батч трехмерных тензоров в соответствии с размерностями, определенными на предыдущем шаге.
Функция np.random.normal
заполняет тензор заданной размерности случайными числами нормального распределения.
После формирования батчей на CPU, они переносятся на GPU при помощи функции to_gpu
. Все дальнейшие вычисления будут происходить на GPU.
batch = np.random.normal(size=size).astype(dtype=np.float32)
batch = gpuarray.to_gpu(batch)
random.randint
из numpy
в диапазоне от 0 до 999 и хранятся в формате np.int32
:
labels = np.random.randint(low=0, high=1000, size=(batchsize, ), dtype=np.int32)
labels = gpuarray.to_gpu(labels)
Таким образом, мы сопоставили каждому объекту из батча свой класс (от 0 до 999) и сымитировали формирование настоящей обучающей выборки. После синтеза множества ответов мы переносим его на GPU аналогичным переносу данных образом.
Подготовка к обучению сети¶
Теперь, когда у нас есть данные и архитектура, пришла пора перейти к обучению нейронной сети.
В качестве оптимизатора выберем стохастический градиентный спуск. Модуль, который реализует этот метод обучения, называется SGD. Мы передадим ему инициализированную сеть, а он будет заниматься пересчётом всех весов сети в соответствии с формулами, заложенными в этом модуле:
optimizer = SGD()
optimizer.setupOn(net)
Для обучения сети еще нужна функция, оценивающая, насколько сеть ошибается при классификации входного объекта. Инициализируем ее:
cost = CrossEntropy(maxlabels=1000)
Это функция кросс-энтропии. Параметр maxlabels
отвечает за количество выходов сети. Мы рассматриваем этот вектор как распределение вероятностей по 1000 классам объектов
(вероятность, что рассматриваемый объект класса 0, ... , вероятность, что рассматриваемый объект класса 999).
Правильные ответы для каждого объекта известны, поэтому мы можем сравнить предсказание сети с ними. Кросс-энтропия делает эту операцию, выдавая число, показывающее, на сколько сеть ошиблась.
Теперь инициализируем тренера.
trainer = Trainer(net, cost, optimizer)
Тренер - это объект, реализующий процесс обучения, в ходе которого он обращается к оптимизатору для пересчета весов нейросети в соответствии с функцией стоимости ошибки. Подробнее можно прочитать о нем тут.
В данный момент тренер только инициализирован, но ничего не делает. Чтобы процесс обучения начался, надо вызвать специальный метод train
этого объекта (что мы и сделаем далее).
Обучение сети¶
Мы не будем задавать количество эпох, валидационную и тестовую выборки. Для нашей цели (сравнение времени обучения неоптимизированной и оптимизированной сетей) хватит нашего батча.
Используем функцию timeKernel
, чтобы засечь время выполнения операции обучения сети при ее текущей архитектуре на заданном количестве данных. Длину цикла для тестирования устанавливаем в 100
итераций.
Дополнительно указываем в тексте лога, что эти данные получены до оптимизации сети:
print("Started benchmarking %s ..." % net.name)
gpuarray.timeKernel(
trainer.train, args=(batch, labels), looplength=100, logname="Before optimizing %s" % net.name, normalize=True
)
Используем функцию оптимизации сети по ее размерности. Она вызывается рекурсивно. Итерация проходит по каждому узлу (модулю, операции) графа вычислений сети и вызывает сама себя от текущего shape
данных:
net.optimizeForShape(size)
После оптимизации вновь запускаем timeKernel
с такими же параметрами для сравнения времени. В логе указываем, что данные получены после оптимизации сети:
gpuarray.timeKernel(
trainer.train, args=(batch, labels), looplength=100, logname="After optimizing %s" % net.name, normalize=True
)
Оптимизированная сеть обучается быстрее, т.к. проход по одному батчу выполняется быстрее, чем у неоптимизированной сети.
Вот вы и освоили базовый навык оптимизации нейронных сетей!