Сегодня нейросетевые подходы составляют большую часть решений задач в области компьютерного зрения, но при этом работа инженеров в этой области не ограничивается обучением state-of-the-art архитектур на своих данных. Часто такие задачи требуют анализа видео или фотографий в режиме реального времени или с минимальной задержкой на конечных устройствах без возможности горизонтального масштабирования. Это может быть редактирование фотографий на смартфонах или же анализ качества продукции на производстве с помощью микрокомпьютеров.
Но даже если у нас есть возможность использовать облачную инфраструктуру, затраты на нее довольно внушительны, и хочется иметь возможность их снизить.
Для того чтобы решать задачи компьютерного зрения эффективно, применяются методы оптимизации моделей нейронных сетей, или по-другому - компрессия.
Мы можем оптимизировать следующие показатели:
Далее мы рассмотрим существующие методы для оптимизации моделей.
Источник https://arxiv.org/pdf/1506.02626.pdf
Существует множество эвристик для определения важности весов, в основном они базируются на значениях весов или активации нейрона. К примеру, если значение веса близко к нулю, то влияние, которое он оказывает на сеть, минимальное. Если же он имеет большое значение, то он важен.
Например в 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]])
Большим плюсом этого подхода является возможность обучать ученика на неразмеченных данных, однако, для достижения максимального качества необходимо объединять предсказания учителя с разметкой. Из минусов можно выделить необходимость более длительного по сравнению с другими методами обучения.
Несмотря на кажущуюся безграничную возможность сжатия моделей за счет подбора минималистичной архитектуры ученика, эксперименты показывают, что для качественной дистилляции знаний количество параметров в архитектуре модели-ученика не должно быть более чем в два раза меньше количества параметров учителя.
[IMG alt="Источник https://devopedia.org/knowledge-distillation
"]https://habrastorage.org/getpro/hab...9e92dbe4d9abb82bbc8de5e629b.png[/IMG]Источник https://devopedia.org/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). Результаты экспериментов приведены в таблице ниже:
Дистилляция знаний с использованием среднеквадратичной ошибки между 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 семестра!
Источник статьи: https://habr.com/ru/post/567584/
Но даже если у нас есть возможность использовать облачную инфраструктуру, затраты на нее довольно внушительны, и хочется иметь возможность их снизить.
Для того чтобы решать задачи компьютерного зрения эффективно, применяются методы оптимизации моделей нейронных сетей, или по-другому - компрессия.
Мы можем оптимизировать следующие показатели:
- количество обучаемых параметров нейронной сети
- количество вычислений
- скорость вычислений
- нагрузка на железо
Далее мы рассмотрим существующие методы для оптимизации моделей.
Оптимизация на уровне архитектуры
Хаки в конструировании более легких и быстрых архитектур нейронных сетей.- Использование 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 и тд.
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
Дистилляция знаний является одним из самых эффективных способов уменьшения размера модели, а также скорости ее работы. Модель меньшего размера, обученная повторять поведение тяжелой и точной модели-учителя, достигает схожих с ней результатов, значительно выигрывая в размере и скорости за счет упрощенной архитектуры. В некоторых случаях, модель-ученик даже может превзойти учителя, но, в основном, ее точность будет немного ниже.Большим плюсом этого подхода является возможность обучать ученика на неразмеченных данных, однако, для достижения максимального качества необходимо объединять предсказания учителя с разметкой. Из минусов можно выделить необходимость более длительного по сравнению с другими методами обучения.
Несмотря на кажущуюся безграничную возможность сжатия моделей за счет подбора минималистичной архитектуры ученика, эксперименты показывают, что для качественной дистилляции знаний количество параметров в архитектуре модели-ученика не должно быть более чем в два раза меньше количества параметров учителя.
[IMG alt="Источник https://devopedia.org/knowledge-distillation
"]https://habrastorage.org/getpro/hab...9e92dbe4d9abb82bbc8de5e629b.png[/IMG]Источник https://devopedia.org/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 |
Обучение проходит идентично обычному, за исключением измененной лосс-функции.
После дистилляции модель-ученик (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 семестра!
Источник статьи: https://habr.com/ru/post/567584/