Сегодня нейросетевые подходы составляют большую часть решений задач в области компьютерного зрения, но при этом работа инженеров в этой области не ограничивается обучением state-of-the-art архитектур на своих данных. Часто такие задачи требуют анализа видео или фотографий в режиме реального времени или с минимальной задержкой на конечных устройствах без возможности горизонтального масштабирования. Это может быть редактирование фотографий на смартфонах или же анализ качества продукции на производстве с помощью микрокомпьютеров.
Но даже если у нас есть возможность использовать облачную инфраструктуру, затраты на нее довольно внушительны, и хочется иметь возможность их снизить.
Для того чтобы решать задачи компьютерного зрения эффективно, применяются методы оптимизации моделей нейронных сетей, или по-другому - компрессия.
Мы можем оптимизировать следующие показатели:
количество обучаемых параметров нейронной сети
количество вычислений
скорость вычислений
нагрузка на железо
В итоге мы оптимизируем скорость работы сети или её размер. Зачем оптимизировать скорость понятно, но зачем уменьшать размер? Современные модели могут занимать сотни мегабайт. Пользователи вашего мобильного приложения будут не очень рады огромному объему приложения, большая часть которого будет занимать модель компьютерного зрения. Или же вам самим будет не очень приятно обновлять модель, если она используется в условиях с низкой скоростью интернета.
Далее мы рассмотрим существующие методы для оптимизации моделей.
Оптимизация на уровне архитектуры
Хаки в конструировании более легких и быстрых архитектур нейронных сетей.
Использование Depthwise (на каждый входной канал свой кернел) + Pointwise (по сути стандартная свертка 1 на 1) сверток вместо обычного Conv2d. По сути последовательность сверток Depthwise и Pointwise является разложением стандартной свертки Conv2d. Слой Conv2d позволяет уловить как пространственную зависимость между признаками, так и межканальную. Здесь Depthwise свертка отвечает за пространственную зависимость, а Pointwise за межканальную. Такое сочетание позволяет заметно снизить количество параметров сети без большой потери качества. Используется в таких архитектурах, как Mobilenet и EfficientNet.
Стандартная свертка размера 3 на 3 с шагом 2 может заменить слой пулинга. Используется в MobileNetV2.
Использование двух сверток Nx1 и 1xN может заменить свертку NxN, при этом будет использоваться меньшее количество параметров. Используется в Inception.
Использование более простых функций активаций. Например ReLU вместо Leaky ReLU, ELU, sigmoid и тд.
Вообще, про Depthwise и Pointwise свертки есть хорошее видео от Samsung.
Pruning
Представим нейронную сеть: огромное количество нейронов и связей с весами. Такое большое количество параметров позволяет нейросети выявлять сложные зависимости в данных и решать трудные задачи. Однако практика показывает, что для хорошей работы сети не требуется все количество параметров, которые у нее есть. Получается, что можно удалить какие-то параметры из нее так, чтобы она работала так же хорошо? Это и есть основа для идеи прунинга, то есть попытка из уже готовой хорошо обученной нейронной сети удалить лишь лишние элементы.
Существует множество эвристик для определения важности весов, в основном они базируются на значениях весов или активации нейрона. К примеру, если значение веса близко к нулю, то влияние, которое он оказывает на сеть, минимальное. Если же он имеет большое значение, то он важен.
Например в PyTorch “из коробки” предлагается реализация класса, который осуществляет ранжирование по L1 или L2 норме. Так, при L1 мы будем смотреть на сумму по модулю всех весов для нейрона, а при L2 - на корень из суммы квадратов весов.
Однако удалять по одному весу из целой нейронной сети слишком ресурсозатратно, поэтому можно удалять сразу 10% весов за раз. Но так как нельзя гарантировать того, что качество модели не ухудшится, лучше дообучить модель 1-2 эпохи, чтобы компенсировать возможную потерю. Подобный алгоритм можно завернуть в цикл:
Таким образом мы должны производить эти итерации до тех пор, пока не найдем компромисс между качеством модели и ее размером.
Прунинг делится на структурированный и неструктурированный.
В структурированном прунинге удаляются целые строки/столбцы, и за счет этого уменьшается размерность весов.. Это приводит к удалению нейронов со всеми их входящими и исходящими соединениями в полносвязных слоях или целых сверточных фильтров в сверточных слоях.
При неструктурированном прунинге отдельные веса могут быть обнулены без изменения их размерности. Это приводит к обнулению отдельных связей между нейронами в full-connected слоях или обнулению отдельных весов сверточных фильтров. Важно заметить, что результирующие тензоры веса могут быть разреженными, но сохранять свою первоначальную форму. А так как разреженные массивы имеют большую часть элементов, равную нулю, то можно сэкономить много памяти, а также ресурсов процессора, если хранить или/и обрабатывать только ненулевые элементы.
Пример работы прунинга с использованием фреймворка PyTorch:
Неструктурированный
import torch, torch.nn as nn # Импортируем модули pytorch
from torch.nn.utils import prune
x = nn.Linear(2, 5) # Создаем линейный слой
x.weight # Посмотрим на веса до прунинга
output:
tensor([[ 0.6027, -0.0132],
[-0.3317, -0.4073],
[ 0.3354, -0.4877],
[-0.5949, 0.1284],
[-0.0481, 0.3109]], requires_grad=True)
p = prune.L1Unstructured(amount=0.7) # Создаем экземпляр класса L1Unstructured. Указываем что 70% весов должны быть обнулены
pruned_tensor = p.prune(x.weight) # Применяем прунинг
pruned_tensor # Вывод результатов
output:
tensor([[-0.5350, 0.0000],
[-0.4944, 0.6620],
[-0.0000, 0.0000],
[ 0.0000, 0.0000],
[ 0.0000, -0.0000]], grad_fn=<MulBackward0>)
Структурированный
import torch, torch.nn as nn # Импортируем модули pytorch
from torch.nn.utils import prune
input = torch.randn(4, 8) # Создаем тензор 4x8 заполненный случайными значениями
print(input) # Посмотрим на наш тензор
output:
tensor([[ 1.2293, 0.6055, -0.3335, 0.8573, 0.6970, -0.2022, 1.2806, -0.0069],
[ 0.3494, 2.2867, -0.4391, -1.3565, 2.2132, -0.7696, -1.4215, -0.8918],
[ 0.3112, 0.3527, 0.3800, 1.2782, -1.6047, -1.7413, -0.7175, -1.0089],
[ 0.6704, -0.9795, 0.9496, -0.1903, -1.8126, 0.0990, 0.9806, 0.3054]])
input.abs().sum(dim=1) # Ставим все элементы в модуль и суммируем значения элементов каждой строки
output:
tensor([5.2122, 9.7277, 7.3944, 5.9875])
y = prune.LnStructured(amount=0.5, n=1, dim=0) # Создаем экземпляр класса LnStructured. Указываем что 50% весов должны быть обнулены, n=1 - для оценки значимости веса используем L1 норму, dim=0 - группировка по весам
y.prune(input) # Применяем прунинг
output:
tensor([[ 0.0000, 0.0000, -0.0000, 0.0000, 0.0000, -0.0000, 0.0000, -0.0000],
[ 0.3494, 2.2867, -0.4391, -1.3565, 2.2132, -0.7696, -1.4215, -0.8918],
[ 0.3112, 0.3527, 0.3800, 1.2782, -1.6047, -1.7413, -0.7175, -1.0089],
[ 0.0000, -0.0000, 0.0000, -0.0000, -0.0000, 0.0000, 0.0000, 0.0000]])
Knowledge distillation
Дистилляция знаний является одним из самых эффективных способов уменьшения размера модели, а также скорости ее работы. Модель меньшего размера, обученная повторять поведение тяжелой и точной модели-учителя, достигает схожих с ней результатов, значительно выигрывая в размере и скорости за счет упрощенной архитектуры. В некоторых случаях, модель-ученик даже может превзойти учителя, но, в основном, ее точность будет немного ниже.
Большим плюсом этого подхода является возможность обучать ученика на неразмеченных данных, однако, для достижения максимального качества необходимо объединять предсказания учителя с разметкой. Из минусов можно выделить необходимость более длительного по сравнению с другими методами обучения.
Несмотря на кажущуюся безграничную возможность сжатия моделей за счет подбора минималистичной архитектуры ученика, эксперименты показывают, что для качественной дистилляции знаний количество параметров в архитектуре модели-ученика не должно быть более чем в два раза меньше количества параметров учителя.
Для примера возьмем архитектуры ResNet 18 и ResNet50 в качестве ученика и учителя соответственно и обучим их на данных из открытого контеста Kaggle. Перед нами стоит задача классификации типов болезни у определенного растения по фотографии. Всего есть 4 метки болезней и метка здорового листа. Разобьем имеющиеся данные на три равные по размеру выборки для обучения, валидации и тестирования моделей. Используем Adam, и следующие гиперпараметры: 20 эпох, шаг - 0.0001, размер батча - 32. Также сделаем нормализацию картинок и приведем их все к размеру 256x256. Будем замерять две метрики: точность и площадь под ROC-кривой. Сохранять модель будем по точности на валидационной выборке.
ResNet18 достигает точности 0.8309, площадь под ROC-кривой при этом - 0.9488. При тех же условиях обучения ResNet50 справляется лучше: 0.8437 для точности и 0.9530 для ROC AUC.
Далее проводим дистилляцию знаний с теми же данными, гиперпараметрами и предобработкой. Будем использовать две лосс-функции: среднеквадратичную ошибку между softmax выходами ученика и учителя (MSE), комбинация среднеквадратичной ошибки с кросс-энтропией (MSE + CE). Результаты экспериментов приведены в таблице ниже:
Ученик | Учитель | Способ дистилляции | Accuracy | ROC AUC |
ResNet50 | - | - | 0.8437 | 0.9530 |
ResNet18 | - | - | 0.8309 | 0.9488 |
ResNet18(1) | ResNet50 | MSE | 0.8346 | 0.9510 |
ResNet18(2) | ResNet50 | MSE + CE | 0.8234 | 0.9450 |
MobileNetV2 | ResNet18(1) | MSE | 0.8458 | 0.9541 |
MobileNetV2 | ResNet50 | MSE | 0.8474 | 0.9556 |
Дистилляция знаний с использованием среднеквадратичной ошибки между softmax выходами учителя и ученика:
Обучение проходит идентично обычному, за исключением измененной лосс-функции.
После дистилляции модель-ученик (ResNet18), обходит классически обученную версию. Хоть точность и не стала сильно больше, ROC AUC метрика свидетельствует о повышении стабильности ученика. Также предсказания модели-учителя позволяют обучать ученика на новых, неразмеченных данных.
Дистилляция знаний с использованием среднеквадратичной ошибки и кросс-энтропии:
Такой разделенный подход позволяет добиться качества модели, превосходящего все остальные в цепочке. Также планомерное уменьшение модели позволяет добиваться оптимального соотношения размер/качество за счет итеративного подхода.
Дистилляция знаний из ResNet50 в MobileNetV2:
Хотя умные статьи с архива говорят о том, что подход с резким уменьшением размера ученика по сравнению с учителем не работает, в нашем случае он дал лучший результат из всех. Это говорит о том, что всегда стоит пробовать все возможные варианты, особенно, когда это не сложно сделать.
Дистилляция знаний из ResNet18, обученной с помощью ResNet50, в MobileNetV2:
Комбинация функций потерь на каждой эпохе обучения мешает модели-ученику повторять за учителем. В итоге у модели не получается выполнить ни одну из поставленных задач и она работает хуже, чем при классическом варианте обучения
Отлично, с компрессией и оптимизацией разобрались. Кто научит ?
Конечно, обучиться основам машинного обучения, компьютерного зрения и компрессии нейронных сетей можно самостоятельно. В интернете достаточно учебных материалов, коротких курсов и масштабных научных исследований, посвященных этой теме. И вообще плохой программист тот, кто не находится в процессе постоянного обучения, поиска новых фреймворков и способов их применения.
Но зачем искать информацию самому, если можно совместить приятное с полезным, и параллельно получить магистерскую степень в ведущих ВУЗах России?
Мы, в Napoleon IT, занимаемся образовательными проектами уже более 7 лет, а в прошлом году открыли наши первые совместные магистерские программы.
В 2020 мы совместно с Челябинским Государственным Университетом и компанией Интерсвязь начали обучать студентов на магистерской программе Machine Learning. На протяжении двух лет специалисты наших компаний и преподаватели ЧелГУ обучат студентов основам Machine Learning, highload backend и другим дисциплинам, необходимым каждому молодому разработчику, планирующему начать свою карьеру в IT. В этом году набор продлится до 30 июля, а университет предлагает абитуриентам 27 бюджетных мест! Заявку можно оставить по ссылке.
Так же рекомендуем обратить свое внимание на нашу новейшую магистратуру, созданную совместно с Университетом ИТМО. До 9 августа вы можете стать обладателем одного из 15 бюджетных мест в ведущем техническом ВУЗе России, за выпускников которого топовые IT-компании ведут борьбу еще со студенческой скамьи! Главный трек этой программы - Computer Vision и компрессия нейронных сетей, но помимо глубоких технических навыков, полученных от опытнейших разработчиков Napoleon IT, студенты так же получат навыки управления проектами в сфере IT, что несомненно станет для них весомым преимуществом при трудоустройстве в будущем. Попробовать свои силы можно по ссылке.
Стоит также отметить, что теоретическая база магистратур основана на нашем бизнес-опыте Napoleon IT, поэтому все практические задания - это study case, а студенты получают возможность попасть на стажировку в Napoleon IT и компании-партнеры уже со 2 семестра!