Спикер о вебинаре: «За последнее десятилетие сверточные нейронные сети получили широкое распространение в большом спектре задач компьютерного зрения. Однако их эффективному применению для решения практических задач мешает ряд недостатков: высокая вычислительная сложность, большие затраты памяти и энергии. В докладе мы рассмотрим методы, позволяющие нивелировать данные недостатки и сделать возможным использование сверточных нейросетей на устройствах с ограниченными ресурсами.»
Запись: https://youtu.be/fX0FBmOPtxk
Презентация: https://drive.google.com/file/d/1ebubVPLCs5uqg0YxzyJmJ_ZBGdd2n3VD/view?usp=share_link
(00:00:00) (Начало записи)
Константин Соболев: В этой области очень много методов, и они все очень разные, поэтому я опишу лишь несколько тех, которые у всех на слуху. Одна из наиболее популярных идей в этой области и архитектура, которая реализует эту идею – это MobileNet. В чем заключается идея? Как вы помните, свертка – это одновременное перемножение в пространстве каналов и в пространственных направлениях данных, т.е. в ширине и в высоте наших входных feature. Вычислительно это довольно-таки сложно. Сложность описана на слайде – у нас все сложности перемножаются. Исследователи предположили: а что, если разделить обработку входных feature в пространственном направлении и в канальном направлении? Так появилась идея depth-wise separable convolution. У нас была одна исходная свертка, мы ее заменили двумя свертками. Первая свертка – это depth-wise convolution, т.е. это пространственная свертка, допустим 3х3, это групповая свертка. Если мы вернемся на предыдущий слайд, то мы не таким объемчиком действуем, а в каждом канале входного изображения у нас входит одна матричка, и мы генерируем каждой такой матричкой один выходной канал. Проблема такой свертки заключается в том, что мы не можем увеличивать количество каналов нашего изображения, плюс мы как-то должны передавать информацию между различными каналами входного feature map. Мы сопровождаем нашу групповую depth-wise свертку сверткой 1х1 (point-wise свертка), которая по факту является матричным перемножением в пространстве каналов. Сложность такой операции значительно меньше. Как вы можете видеть, там мы все параметры перемножали, а здесь мы перемножаем количество параметров входного feature map на сумму выходных каналов и произведение пространственной размерности depth-wise свертки. Эта идея была предложена в первой версии MobileNet, во второй версии ее немножко доработали, объединив с ResNet, придумали вот такой inverse bottleneck block, который очень похож на блок ResNet, но в нем вместо обычной свертки по середине находится depth-wise свертка, и применили residual connection к данной архитектуре. Это помогло существенно улучшить результаты первой версии MobileNet. Основную мысль, которую авторы заложили в данную статью – это то, что вместо того, чтобы одновременно работать с пространственным представлением признаков и с поканальным представлением признаков, мы можем разделить эти две операции. За счет этого мы можем очень сильно сократить вычислительную сложность нашей модели.
Следующая популярная архитектура – это ShuffleNet. Она очень похожа на вторую версию MobileNet, но они пошли дальше. Они заметили, что в данной структуре, когда мы чередуем point-wise и depth-wise convolution, самой сложной операцией является point-wise convolution. (00:05:07) Если расписать параметры вычислительной сложности этой матрицы, этого сверточного слоя, то вот это будет матрица C In x C Out, а эта матрица C In х 9 (так как свертка 3х3). Так как здесь у нас C In x C Out сложность порядка сотен, то она намного сложнее. Они придумали: давайте эту point-wise свертку 1х1 сделаем групповой, т.е. она будет перемножать не все входные каналы, а будет представлять из себя набор независимых 1х1 сверток, которые работают только с частью сходных каналов нашего feature map. Затем, нам же все равно нужно передать пространственную информацию от независимо обработанных feature, которые были независимо получены из вот этих feature, как-то перемешать информацию между каналами. Поэтому они применяют Shuffle операции, т.е. в данном случае у нас групповая свертка состоит из 3 групп, мы делим наши выходные feature point-wise convolution на 3 части и перемешиваем их между собой. Затем мы к этому применяет групповую depth-wise свертку и каждый кусочек обрабатываем отдельно. У нас происходит передача информации между всеми входными каналами входного feature map. Это был следующий шаг улучшения данной архитектуры.
Следующая идея, которая также привела к улучшению работы нейросетевых архитектур, это GhostNet. Давайте рассмотрим, это у нас выход первого слоя ResNet-50. Мы в него подали вот такую картинку котика. Давайте посмотрим, какие признаки у нас получаются в различных каналах feature map. Мы можем заметить, что у нас есть группа признаков, которые очень похожи друг на друга. Эти признаки называются – призрачные признаки Ghost feature, откуда и пошло название GhostNet. В чем идея? Исследователи пронаблюдали выходные каналы feature и заметили, что многие feature друг на друга похожи с довольно малыми изменениями. Они подумали, что, наверное, получив этот признак, мы можем получить альтернативный, похожий на него признак какой-то более простой операции, недели свертка со всеми входными изображениями или со всеми входными feature. Поэтому они предложили следующую оптимизацию: давайте, когда нам приходят на вход наши признаки с предыдущего слоя, мы делаем свертку не с полным количеством выходных каналов, а со значительно меньшим. Эта свертка и эти каналы получаются более простым образом, чем если бы мы делали полную свертку Input, чтобы получить Output. Затем берем наши выходные признаки этой легкой свертки и какими-то очень простыми манипуляциями генерируем дополнительные признаки, которые будут нашими искусственными Ghost feature. Данный подход помогает существенно ускорить работу сети, при этом у данной архитектуры очень высокая точность. (00:10:05) Что из себя представляют простые операции работы с этими feature? Когда я готовился к докладу, я заглянул в код к их статье, это тоже какая-то простая, легковесная групповая свертка, но смысл в том, что сложность получения этого feature map таким путем значительно легче, чем если бы мы делали честную, простую свертку, как это делается в других архитектурах.
Следующая, ортогональная предыдущим идеям идея создания эффективной архитектуры сверточных сетей, это поиск нейросетей архитектур. Как мы видели на предыдущих слайдах, у нас очень много вариантов, как мы можем сформировать сверточный слой. У каждого из этих вариантов есть множество параметров: какое количество входных каналов, выходных каналов поставить у данного слоя, как прикинуть residual connection и т.д. Перебор подобных параметров – это очень трудоемкая работа. Исследователи подумали и прикинули: почему бы нам не заменить ручной поиск более эффективной архитектурой, более эффективным алгоритмом поиска. Так они и сделали. Он работает по следующему принципу. Мы задаем пространство поиска, которое состоит из блоков, из которых мы хотим составить сеть – хотим ли мы использовать Ghost- модуль или модуль ShuffleNet, или модуль MobileNet блока. Мы задаем, какие параметры мы хотим перебирать, входные и выходные параметры, количество групп, residual connection, глубину сети, входное разрешение. Далее мы делаем поиск по данному пространству. Поиск состоит из 2 частей: у нас есть стратегия поиска и какая-то evaluation. Основываясь на нашей стратегии, мы выбираем кандидата, делаем его evaluation. Основываясь на результате, которое мы получили после evaluation, мы модифицируем стратегию выбора нейросетевой архитектуры и повторяем процедуру. По сути, это ходит по кругу: мы генерим стратегию, тестируем архитектуру, затем возвращаем результат алгоритма поиска стратегии, модифицируем выбранную архитектуру и идем по кругу. Алгоритмами поиска архитектур выступают различные популярные алгоритмы поиска. Можно использовать Random Search. Чаще всего используются методы обучения с подкреплением, также применяют различные эволюционные алгоритмы и более экзотические методы, такие как Progressive Decision Process и методы градиентного спуска. Также стоит обратить внимание, что является evaluation нашей нейросетевой архитектуры. Когда мы генерируем нашу архитектуру, нам ее еще нужно обучить. Самый простой способ обучить данную модель – посмотреть, какая у нее будет на выходе точность, и принять эти результаты к сведению. Это очень сложно, так как вычислительно затратно. Многие нейросетевые архитектуры могут тренироваться неделями, поэтому часто используются Proxy метрики, такие как обучение модели на поле простом dataset, обучение модели какое-то фиксированное количество эпох, меньшее, чем нужно для полного восхождения ее, и тестирование не всей evaluation модели, а какой-то уменьшенной ее версии с похожим поведением. Также мы можем различными предиктивными методами прогнозировать, как данная архитектура будет вести себя после файнтюна. (00:15:36)
Такой подход использовался при создании, наверное, самой популярной сейчас сверточной нейронной сети – EfficientNet. В ней авторы задались вопросом, как правильно скалировать нашу нейронную сеть.
Модератор: Давайте мы в хорошо известных вещах ускоримся. EfficientNet все знают, все используют.
Константин Соболев: Хорошо. Далее представлены результаты данных архитектур. Как можете видеть, GhostNet показывает наилучший результат, но нужно понимать, что данные архитектуры были созданы в разное время, задачей каждой последующей было победить предыдущую. GhostNet выигрывает, в основном потому что это самая свежая архитектура из представленных на слайде.
Модератор: Василий Прядченко спрашивает: «Какая мера похожести признаков?» Это про какую архитектуру вопрос? Василий пишет, что это про GhostNet.
Константин Соболев: Я затрудняюсь ответить на ваш вопрос. Скорее всего это просто какая-то корреляция между признаками, они очень сильно скоррелированы. Данный пример визуально показывает, как они выглядят. По-моему, основной смысл в корреляции самих признаков. Тут надо в самой статье более детально посмотреть, я, к сожалению, не готов ответить на данный вопрос.
Модератор: Сергей Шепелевич спрашивает: «Shuffle – это случайное перемешивание. Нет гарантии, что все будет равномерно распределено».
Константин Соболев: Там как раз идея в том, что мы не совсем случайно перемешиваем. Мы гарантировано разбиваем наш feature map на какие-то части и разделяем их между группами сверток. Идея в том, что у нас эта информация была только в этой группе. Если бы мы не делали Shuffle, она бы пошла дальше. Но когда мы разбили feature map на 3 части и переместили кусочек сюда, кусочек сюда, кусочек сюда, то мы информацию отсюда так или иначе передали в этот Output, этот Output и этот Output. (00:20:00)
Модератор: Другими словами, мы взяли некую матрицу, разложили на матрицу простой структуры и матрицу перестановок.
Константин Соболев: Да, что-то вроде того. Насколько я понимаю, это как раз фиксированная операция.
Модератор: Мы взяли некоторую фиксированную матрицу перестановок и нашу матрицу конволюции, которая у нас бы была, представили в виде произведения некой простой матрицы на матрицу перестановок.
Константин Соболев: Да, примерно так. В самом коде это используется уже не как какая-то матричная операция, т.е. с точки зрения матричной операции все выглядит так, но в самом коде просто разбивают и делят на части.
Модератор: На самом деле, эта идея имеет право на жизнь, потому что у нас многие классы матриц представляются в виде матриц простой структуры, умножить на матрицы перестановок.
Константин Соболев: Давайте теперь поговорим о втором наборе методов. Мы в лаборатории больше занимаемся сжатием существующих архитектур. К сожалению, при подготовке обзорной лекции я решил не сильно углубляться в наш метод, но концептуально я попробую заострить внимание на слайдах, где это будет задеваться.
Первый подход, то, чем мы занимаемся в нашей лаборатории, это сжатие сетей на основе Weight Decomposition. Оно основано на гипотезе, что веса, которые выучивают нейроны сетей в рамках одного слоя, лежат в низкоранговом линейном пространстве. Веса, которые относятся к данному свертовочному слою или в данной линейному слою, можно представить в виде более эффективного представления – матричного или тензорного разложения, при этом существенно не потеряв информацию, которую несут в себе эти веса. Этот подход так и работает. Мы берем какой-то сверточный слой, достаем ядро свертки, делаем его тензорную аппроксимацию и заменяем наш исходный слой на комбинацию сверточных слоев меньшего размера. Дальше мы поговорим, какие разложения соответствуют каким замещениям нашего сверточного слоя. Зачастую просле того, как мы сделали операцию аппроксимации, мы все равно теряем производительность нашей модели, потому что этот исходный сверточный слой и тензорные разложения не тождественно равны, все равно есть некая ошибка аппроксимации. Для того, чтобы восстановить эффективность нашей модели, мы файнтюним ее, дообучаем с малым … (00:24:24). Это одновременно решает все наши проблемы, модель начинают работать быстрее, требует меньше памяти, т.к. мы большее количество параметров закодировали более простым представлением. Как следствие первых двух пунктов, она требует меньше энергии.
Существует большое количество различных тензорных разложений и вариантов представить наш исходный сверточный слой в комбинации более эффективных. Здесь я показал некоторые из них. Первые два подхода – это всем известное SVD разложение. Мы его можем делать по-разному. Обычно это делается так, что мы берем наше ядро сверточного слоя, решейпим этот четырехмерный тензор в какую-то двухмерную матрицу, делаем SVD разложение этой матрицы, а затем обратно решейпим полученные 2 фактор-матрицы в формат, который подходит для сверточных слоев, и вставляем обратно в сеть. Например, в случае SVD мы можем это делать двумя различными методами. Weight SVD – мы решейпнули наше ядро в размерность k x k x c in, а вторая размерность на c out, разложили ее с рангом r и вставили обратно. Она эквивалентна двум операциям свертки. Первая операция свертки — обычная пространственная свертка, но у нее такая же пространственная размерность и такое же количество входных параметров, как у исходной свертки, но она Output у нее не c out размерности, а размерности ранга r, который мы использовали при декомпозиции. Затем мы уже проецируем пространство с каналами размера r в пространство c out. Как показывает практика, комбинация таких двух слоев эффективнее, чем наш исходный слой. Более хитра и более эффективная пространственная SVD. В данном случае мы исходное сверточное ядро решейпим на 2 части – на матрицу размера k x c in, а вторая размерность k x c out. Таким образом итоговый сверточный слой состоит из двух сверток, обе они пространственные, но первая свертка не k x k, а k x 1, мы здесь только по горизонтали сворачиваем, и output у нее не c out, а r (ранг, с которым мы складываем). Следующая свертка у нас в вертикали, она проецирует из r в c out. Есть более сложное тензорное разложение, которое сразу раскладывает не матрицу, а именно тензор. Наиболее эффективным из всех является CP-Decomposition. Работа, которая скорее всего вас бы заинтересовала, и о которой я мог бы рассказать, это работа с таким разложением сверточных нейронных сетей. Давайте, расскажу вкратце, в чем смысл нашей работы, и что мы вообще делаем. Как вы можете видеть на графике, CP-разложение очень хорошо аппроксимировать на сверточные слои. При том, что оно требует гораздо меньшее количество параметров, чем другие разложения, при этом точность модели остается довольно большой. Но у него есть очень большой недостаток в том, что оно нестабильно, это показано точностью модели до файнтюнинга. Если бы мы начали файнтюнить обычную, разложенную CP модель, то качество вот этого чекпойнта (мы брали треть параметров исходной модели) у нас упадет вниз, затем медленно начнет восстанавливаться и скорее всего не сойдется даже к этой точности, которая у нас была сразу после сжатия. (00:29:53) В нашей работе мы исследовали данный процесс и поняли, что проблема кроется именно в самом CPD разложении, и предложили метод, который позволяет увеличить его стабильность. Если говорить очень простыми словами, то нестабильность CPD разложения проявляется в том, что если мы добавим очень маленький шум фактор-матрицам, на которые мы его разложили, то у нас результирующий тензор, который будет, если нам собирать все эти фактор-матрицы обратно, будет очень сильно отличаться от исходного тензора, который мы раскладывали с помощью CPD. А этого не должно быть, потому что мы индуцировали в эти матрицы очень маленький шум, близкий к нулю. Данную нестабильность CPD-разложения можно охарактеризовать математическим термином чувствительность к подобным перетурбациям. Мы предложили алгоритм, как минимизировать данную чувствительность. Благодаря этому стало возможно дообучать модель, разложенную CPD-разложениями. Результат нашей работы заключается в том, что на многих моделях мы побили другие методы тензорного разложения именно в плане уровня сжатия и точности на выходе. Это что касается нашего оригинального исследования.
Модератор: Вы говорите, что CPD разложение неустойчиво к малым шумам. Это очень важное замечание, потому что любое разложение, с которым мы хотим работать, должно быть устойчиво к шумам и просто к ошибкам округления. Но есть более важная проблема следующего толка. Всякий раз, когда мы работаем с малыми разложениями, типа малоранговых, мы явно предполагаем, что сигнал некоторым образом гладкий, а то, что негладкое – это и есть шум. По сути любое разложение CVD, возьмем несколько старших сингулярных значений, то, что получается в некотором пространстве, будет гладким. Оно либо в обычном пространстве будет гладким, либо в Фурье-пространстве будет гладким. С другой стороны, у нас все матрицы, которые использованы в обучении нейронных сетей, инициализируются стохастическими начальными значениями, и даже после обучения они остаются похожими на стохастически-заполненные матрицы. Они на самом деле уже не случайные после обучения, но выглядят как случайные и не выглядят как гладкие. Поэтому всякий раз, когда мы аппроксимируем что-то принципиально негладкое чем-то принципиально гладким, у нас должно возникать некое неудовольствие, поскольку принципиально негладкую вещь принципиально гладкой аппроксимировать не очень хорошо. Есть следующий уровень взгляда на это все. У нас есть очень гладкая функция, в ней мало информации. Если у нас сплошной белый шум, в ней как бы информации много, с теоретико-информационной точки зрения там очень много информации, а на самом деле мало. Соответственно есть некая промежуточная категория – сложность, но не информация, которая позволяет, вероятно, описывать содержательно сложные вещи. Как пример, работы Кацнельсона-Якунина по сложности в разных штуках, там как раз делается свертка с гауссовым ядром по масштабу и говорится, что сложные системы – это системы, которые на разных масштабах имеют разные представления. Такая гауссовая свертка по пространству масштабов позволяет такую штуку считать. Наверное, интересным вопросом является то, как такие штуки сжимать, аппроксимировать как бы случайную величину, а на самом деле сложную величину, в ней, на самом деле, есть информация. Когда мы заинициализировали нейронную сеть, она у нас случайная. Когда мы обучили, оно скорее не случайное, а сложное. Там должна появиться некая структура, которую за этой случайностью не видно, но она не гладкая, не обязана быть гладкой. Поэтому возникает вопрос: раскладывать на что надо и в каком пространстве минимизировать. SVD, разложение Такера, CPD-разложение, которое вы исследовали, они все приближают в пространстве Евклидовом, где приближение – это гладкое приближение. А вероятно надо приближать в каком-то другом пространстве, где близкие функции не обязательно гладкие, ну ничего. Возможно, собака порылась где-то в тех местах.
(00:37:41) – (00:39:03) (Технический разговор)
Константин Соболев: Никита Литвиненко спрашивает: «Посоветуйте мне литературу для начинающих по теме сверточно-нейронные сети». Если вы достаточно хорошо владеете английским, то есть замечательный курс лекций Стэнфорд на YouTube CS231n. Он достаточно хорошо вводит в эту тему. Если вам только сверточные–нейронные сети нужны, советую начать с него. Когда я вижу различные русскоязычные курсы, они так или иначе ссылаются на него. Не так давно я видел хороший курс от Школы МФТИ, но он не чисто по сверточно-нейронным сетям, но там есть несколько лекций, касательно этой темы.
«Используете ли вы в своей работе CSPNet?» Так как я не знаю, что такое CSPNet, то, наверное, нет.
(00:40:53) — (00:41:52) (Технический разговор)
Константин Соболев: Мы остановились на методах скрина сверточных нейросетей, основанных на тензорных и матричных разложениях весов. Раз мы продолжаем обзорную лекцию, я думаю, можно не углубляться в то, как они раскладывают веса. Нас скорее интересует итоговый результат. Наиболее популярный метод разложения тензорных весов – это CP-decomposition и Tucker decomposition. А также Tensor-train, но я больше видел, что его применяли к различным матрицам типа fully-connected … (00:42:36). Или в каких-то матрицах внутри LSTM. Пока по эффективности аппроксимации сверточных слоев лучше всего себя показывает CP-decomposition. В недавней работе мы научились его делать более устойчивым, он теперь еще и лучше файнтюнится.
Если говорить в общем про данный набор методов, то у них есть свои плюсы и свои минусы. Плюсы – это то, что в местах, где они применимы, они обычно позволяют получить высокий уровень теоретического ускорения при достаточно низком падении качества модели. Они не требуют какой-то дополнительной поддержки с точки зрения железа, потому что те слои, на которые мы раскладываем исходный слой, они состоят из тех же операций, как обычная свертка, это такие же обычные сверточные слои, как были до этого. Но есть свои минусы. Метод отлично работает с классическими 2D Convolution, но гораздо хуже работает с Pointwise Convolution (который 1х1), Depthwise и Group Convolution, потому что эти свертки сами по себе меньше, там труднее применить какой-либо эффективный метод тензорного сжатия. Как вы могли заметить у нас каждое тензорное разложение характеризуется набором рангов. Бывает один ранг, бывает несколько рангов на разложение. Ранг – это гиперпараметр и, к сожалению, не существует какого-то метода, который позволяет хорошо выбрать ранг для разложения сверточного слоя. Зачастую приходится делать это методом проб и ошибок, и это занимает довольно много времени. Сверточная сеть – это множества слоев. Для каждого слоя нужен свой ранг. У нас есть некоторая созависимость от того, какой слой мы сожмем сильнее, какой поменьше. Поэтому это довольно сложная задача дискретной оптимизации, которая пока недостаточно хорошо решена в литературе.
Давайте перейдем к следующему методу – неструктурированное прореживание весов. На английском языке у него несколько названий, которые означают одно и тоже, но в разных местах используются по-разному. Метод заключается в том, что в нейронных сетях не все веса эквивалентно важны, поэтому мы можем взять и попробовать выкинуть веса с наименьшей важностью. Исследователи попробовали, и эффект оказался довольно хорошим. Модель при достаточно большом проценте убранных весов не сильно теряет в точности. Также исследователи заметили, что лучше всего этот процесс сделать итеративным, т.е. берется предобученная сеть, из нее убирается какая-то доля весов, допустим, 10%, она файнтюнится, чтобы немножко восстановить точность, как мы это делали в случае с малоранговым тензорным разложением слоев. Затем полученная зафайнтюненная сеть снова прунится и т.д. Данный график неплохо характеризует, как у нас распределен трейд-оф по различным уровням количества запруненных весов. По оси X отложен процент параметров, которые мы убрали, а по оси Y падение точности. Вы можете заметить, что при достаточно большом проценте убранных весов точность модели чуть выше, чем у исходной. Как показывает красная линия, лучше всего модель работает, когда мы пруним ее итеративно. На уровне, где мы убрали больше 85% весов, точность модели примерно такая же, как у исходной. Это достаточно интересное наблюдение. (00:48:23)
Теперь давайте поговорим о том, как можно определить важность весов нашей модели. Самые популярные и распространенные на данный момент методы – это критерии, основанные на магнитуде весов. В чем логика данного метода? Картинка справа отображает гистограмму весов одного из сверточных слоев. Как вы можете видеть, у нас много весов (и это характерно для очень большого количества сверточных слоев) находится в районе нуля. Когда мы при свертке умножаем входные признаки на ноль, нам это не дает никакой информации. Почему бы не приравнять значение этих весов к нулю и просто не выкинуть их. Этот метод довольно популярный, он хорошо работает. Очень большая часть работ по данной тематике использует подобный критерий, потому что он очень простой. Также есть метод, основанный на градиентах, его можно объяснить данной формулой. Давайте просмотрим лос данной модели и попробуем расписать его в ряд Тейлора по параметрам какого-то слоя. Мы можем заметить, что у нас есть здесь первые производные, есть вторые и смешанные. Мы можем заметить, что у нас коэффициенты, стоящие перед вторыми дифференциалами по различным весам имеют очень малое значение. Это значит, что по смыслу, если мы выкинем данный вес, то у нас лос модели не сильно поменяется, а значит модель не станет от этого сильно хуже. Самый первый метод, работающий на данной основе, был предложен очень давно, еще в начале 90-х годов Яном ЛеКуном, он называется Optimal Brain Damage. В основном в литературе я больше всего встречаю метод, основанный на магнитуде весов.
Последние несколько лет появилось интересное наблюдение, гипотеза, связанная с нейронными сетями. Она заключается в том, что наша сверточная сеть подержит внутри подсеть, которая работает примерно также эффективно, как и исходная сеть. Возможно будет более понятно, что она из себя представляет, когда я расскажу про метод, который предложили исследователи. Пару слайдов назад я рассказал, что часто подход к прунингу заключается в том, что мы обучаем модель, оцениваем важность весов, выкидываем долю весов и файнтюним. Исследователи заметили, что, если вместо того, чтобы файнтюнить запруненную модель, инициализировать ее заново теми же весами, которыми она была заинициализирована до обучения, то итоговая точность модели после обучения заново, но уже запруненной модели, выше, чем если бы мы ее просто прунили. Также они заметили, если мы инициализируем ее какими-то случайными весами, то выигрыша в производительности модели нет. Собственно, данное наблюдение натолкнуло их на мысль, что те веса, которые мы случайно инициализировали в сеть, содержат некоторые лотерейные билетики. У нас есть сверточная сеть, мы ее рандомно инициализируем каким-то образом, и уже на этапе инициализации среди данных рандомно инициализированных весов у нас есть группа весов, которые могут очень эффективно работать без всех остальных. (00:54:03) Это и называют подсетью. В фоллоу-апе своей собственной работы они заметили, что более эффективно не заново инициализировать сеть после прунинга исходными, а откатывать ее значение весов к значениям n эпох назад, и назвали это Rewind. На самом деле данная Lottery Ticket Hypothesis породила большое количество обсуждений в комьюнити, очень много фоллоу-апов у данной статьи, что можно этим методам посвятить целый доклад. Здесь я рассказал об основной сути. На графиках представлены результаты, показывающие, что откатывание весов у нас работает лучше, чем рандомная реинициализация сетей и откатывание весов сети к рандомной реинициализации. Если мы хотим таким образом прунить модель, мы должны хранить логи всех ее весов на различных этапах, а также на нужно выбрать этап, на котором мы хотим откатывать веса после прунинга, что довольно сложно.
Давайте немного резюмируем метод неструктурированного прунинга. Очень весомый его плюс заключается в том, что он очень хорошо работает при удалении очень большого количества весов. Можно порядка 95% весов убрать и точность сети будет примерно такой же, как и исходная. Но у него есть и существенные недостатки, которые мешают его практическому применению. Они заключаются в том, что современные алгоритмы, на которых работают устройства, которые считают матричные операции (те же видеокарты в мобильных телефонах), большинство из них практически не поддерживают перемножение разряженных матриц. Несмотря на то, что мы убираем большое количество весов, мы делаем свертку, такую же, как исходная, просто у нашего сверточного ядра большое количество нулей. Также возникает проблема, которая была Low-rank Decomposition методах, что для каждого слоя нам очень сложно подобрать уровень разреженности. У нас много весов, у каждого веса разный уровень разреженности, нам нужно как-то всю сеть оптимизировать в этой области, это тоже отнимает много времени.
Давайте сфокусируемся на данной проблеме, что у нас отсутствует поддержка sparse-перемножения матриц. Из этой проблемы вытекает идея структурированного прунинга. Она заключается в следующем – давайте, вместо того, чтобы убирать какие-то индивидуальные веса, мы сразу будем убирать какие-то структурные блоки нашей нейросети. Если на этой иллюстрации многослойного персептрона мы сразу выкидываем нейроны и все относящиеся к ним связи и веса, то уже с данной архитектурой мы можем получить ускорение работы нашей сети, потому что здесь вместо обычного матричного перемножения большой матрицы мы можем делать перемножение матрицы, гораздо меньшей, чем исходная. В случае сверточных сетей мы можем выкинуть сразу фильтр свертки или входной канал свертки, и таким образом у нас сверточное ядро по структуре будет такое же, как исходное, но меньше. Поэтому нам не нужны никакие софтверные или хардверные оптимизации, у нас просто будет вычислительно проще наша операция. Алгоритм применения данных методов примерно такой же, как и у неструктурированного прунинга. Они также более эффективно работают, если это делать итеративно. Также лучше начинать прунить итеративно маленькое количество фильтров, каналов и т.д. Как видите, можно по-разному прунить: мы можем выкидывать фильтр, выкидывать входной канал нашего сверточного ядра, можем в каждом фильтре выкидывать какую-то часть. Как мне кажется подход Group-wise не дает ускорения, потому что это тот же самый неструктурированный прунинг, просто мы выкидываем вес во всех фильтрах. (01:00:04) Результат похож на предыдущий. Stripe-wise тоже скорее всего не дает какой-то оптимизации, потому что у нас будет сверточное ядро такого же размера, просто здесь будут нули, которые также будут умножаться на входные feature. Поэтому в основном исследователи фокусируются на прунинге фильтров и каналов. Можно заметить, что такие два подхода работают друг с другом. Если мы пруним фильтр сверточного слоя, то у нас меньшее количество выходных каналов у этого сверточного слоя, а это значит, что слой, который до этого принимал 64 канала, должен принимать 48 на вход. Получается, что мы должны избыточные входные каналы у этого слоя также убрать.
Теперь давайте поговорим о критериях, как убирать избыточные веса или избыточные структурные компоненты слоя. Точно также работают методы, основанные на весах это L1/L2 норма, но уже получается фильтра или канала входного. Еще неплохо работает, если анализировать эффективность данного канала, основываясь на магнитуде скейлинг-параметра в бачнорм (BN) слое. Смысл в том, что часто у нас после свертки стоит слой бачнорма, который все входные данные в каждом канале приводит к стандартному, нормальному виду ч нулевым средним и единичной дисперсией, а потом умножает на скейлинг-фактор и добавляет шифтинг-фактор. Если у нас скейлинг-фактор домножается маленький, то у нас у стандартного нормального распределения вся информация сплющивается в одну дельта-функцию, а это значит, что данный канал информации практически не несет, его можно выкинуть. Также можно анализировать, основываясь на выходах каналов, т.е. активации, метод главных компонент применять к выходным feature нашего слоя. Также Gradient-based метод работает, т.е. то, что мы делали для весов, можно сделать для группы весов, и какие-то алгоритмы, которые просто перебирают различные комбинации прунинга весов и выбирают лучшую комбинацию из них.
Плюсы и минусы. Плюсы — он эффективен не только в уменьшении параметров, но и ускоряет модель, нам не нужно никакой поддержки, ни хардверной, ни софтверной, потому что у нас итоговый сверточный слой устроен точно также, как предыдущий, но он меньше. Недостатки – когда мы пруним один слой, мы затрагиваем следующий слой, потому что он должен принимать на вход меньше параметров, и это надо как-то учитывать, и опять же нам очень сложно подобрать комбинацию прунинга слоев всей сети, потому что это большая дискретная задача оптимизации.
Далее интересный слад, который показывает, как отличаются методы прунинга между собой. Первое – что пруним (структурны и неструктурный), локально (в рамках одного слоя) или глобально (всю сеть). Как мы пруним – это критерий, на основе которого мы выбираем, что убираем. Когда пруним – пруним ли мы предобученную модель или пруним во время обучения, или до обучения. Как часто – делаем ли мы это сразу или итеративно. Зачастую итеративно лучше работает, чем один раз. (01:04:51)
Давайте пробежимся по вопросам. Даниил Салов прислал ссылку по CSPNet. Я посмотрю, спасибо!
Василий Тратченко: «Вы что-нибудь знаете о методах прунинга, где оставляемые веса пересчитываются в рамках процедуры прунинга, когда замеряются другие веса?» Что вы имеете в виду под тем, что пересчитываются в рамках процедуры прунинга? Обычно методы работают так, что сами веса меняются в процессе обучения или файнтюнинга, а мы лишь смотрим на то, какие из весов более или менее важны для нас, и убираем наименее важные.
«Как на практике прунить сети, какими инструментами, фреймворками и т.п.?» На практике часто можно взять просто какую-то статью с кодом, посмотреть, как они это делали. Есть методы автоматического прунинга сетей, которые работают похоже на neurons architecture search, но вместо поиска архитектуры, мы ищем оптимальные комбинации фильтра. Либо на каких-то эвристиках это построено. Прунинг часто поддерживается в популярных фрейворках сейчас. В PyTorch есть модуль, связанный с этим, и в TensorFlow, насколько я знаю, тоже есть. Плюс есть библиотечки, которые пытаются это сделать более удобным. Насколько я знаю, в Catharsis есть какой-то модуль, связанный с прунингом, но я не пробовал им пользоваться. Могу дать простой рецепт. Если у вас мало опыта, просто гуглите репозиторий, который показывает хорошие результаты на данной сети, смотрите какой метод, научную статью он использует, и как-то используете их код. Плюс какие-то дефолтные инструменты можно использовать.
Василий Прядченко: «Я о том, что обычно находят маску из нулей и единиц. Можно ли находить для оставляемых весов, отличных от единиц?» Да, есть, например, метод Soft Filter Pruning, где мы можем эту маску не просто находить в момент обучения, а мы можем эту маску выучивать. Тут очень много различных вариаций данного метода. Есть такие методы, но я не включил их в презентацию, потому что она и так получилась очень нагруженной.
(01:08:50) – (01:10:10) (Техический разговор)
Дмитрий Меньщиков: У меня тоже есть небольшой вопрос, касающийся практики прунинга. Это, безусловно, очень большая академическая дисциплина. Вопрос – как применяется технология на практике? Я полагаю, что это очень актуально для мобильных платформ, для сетей, которые работают на EDGE. Есть ли какие-то лучшие практики? Какие методы прунинга используются? Какие методы прунинга лучше для каких задач?
Константин Соболев: Проблема в том, что они постоянно совершенствуются. Лучше всего смотреть какую-то последнюю конференцию для этого. Недавно прошел … (01:11:11), часто на этой конференции выкатываются несколько методов прунинга, которые бьют все предыдущие. Из раза в раз нужно что-то новое применять. Могу поделиться личными вкусами. Есть хороший метод, вышел год назад на CSV, он называется Eagle I. Он не столько про выбор критерия, а про то, как выбрать лучшую комбинацию уровня прунинга для слоев. Как мне кажется, сейчас самое сложное – это эффективно найти комбинацию фильтров, которые лучше всего запрунить. Критериев очень много. В данной области есть такая проблема, что многие методы придумывают какое-то совершенствование, и за счет этого выигрывают предыдущие методы. Но никто не проводит такого исследования – а что, если взять какой-то очень старый метод, и фишки из свежих методов к нему добавить, будет ли он их бить. Поэтому достоверно неизвестно, какой лучше всех. Насколько я понимаю, по выбору эффективности каналов чаще всего используют Magnitude Pruning, а самый большой Bottleneck в практическом применении, как нам выбрать комбинацию фильтров, чтобы у нас точность у модели была меньше. Как мне показалось, работа Eagle I предложила очень простой способ оценки того, какой способ комбинации фильтров нам выбрать для прунинга всей сети. Вкратце, они берут какую-то комбинацию запруненных фильтров, прунят сетку и без файнтюнинга калибруют у ее бачнорм статистикии оценивают ее точность. Выяснилось, что точность после калибрации бачнорм статистик – очень хорошая проэксиметрика для поведения модели после файнтюна, потому что файнтюнинг может занимать часто сутки и больше. Они заметили, что это отличная проэксиметрика и показали, что даже если мы простым рандом Random search будем выбирать комбинации каналов, основываясь на этом методе… Давайте уточню. У нас есть разные слои, мы можем понять на каждом слое эффективность фильтров или нейронов для данного слоя. Но выбор того, какой процент неэффективных здесь запрунить – это гиперпараметры, открытая задача. Они предлагают брать Random search, задавать рандомные значения фильтром и тестировать модель. Прунинг – это вычислительно особо не требующая операция. Мы просто сразу же выкидываем веса, делаем калибрацию бачнорм стилистик и тестируем на простой выборке нашу модель. Это точно очень эффективно. Мы можем порядка тысячи таких комбинаций запруненных фильтров или нейронов выбрать и выбрать наилучшую комбинацию из них. Это хорошо работает и многие существующие методы бьет. Также они скомбинировали это с другим методом автоматического сжатия с помощью прунинга, в котором как раз с помощью … (01:16:29) выбирают уровень сжатия каждого слоя. Получили соту. По-моему, им удалось очень сильно пожать RezNet 50, убрать 70% флоапсов, при этом они почти не уронили точность модели. Мне самому этот метод очень понравился, могу его порекомендовать. Но общего решения, к сожалению, нет. В области исследования есть такая проблема, что мы достоверно не знаем, действительно ли, новые методы, которые выходях, по всем параметрам бьют предыдущие или нет.
Дмитрий Меньщиков: Еще один вопрос прилетел от Константина Островского: «Сильно ли влияет Data set на эффективность сжатия?»
Константин Соболев: Да, очень сильно. Давайте сравним на задачах классификации. У нас есть C Far 10 или C Far 100 Data set, где у нас картинки 32х32. В случае C Far 10 у нас получается 10 классов маленьких пиксельных изображений. Есть Imaginet на 1000 классов, где еще и разрешение больше – 224х224 в стандартной постановке. Чтобы различать картинки из C Far 10, нам интуитивно кажется, что достаточно простой модели, чем для Imaginet. Та же самая логика работает для того же прунинга или Low-rank сжатия слоев. Если мы сжимаем C Far, то мы можем раз десять сжать модель, при этом очень мало потерять в точности модели, порядка 1%. В случае Imaginet данные уровни сжатия уже недостижимы. По факту мы также можем любой уровень сжатия взять, пожать нашу модель, но у нее будет очень большая деградация в точности. Чем сложнее Data set, тем сложнее нам нужна модель. Эта логика полностью передается на прунинг, на сжатие сетей.
Дмитрий Меньщиков: Тут бы я отметил, что это касается не только Data set, но в целом области применения.
Константин Соболев: Да. Давайте продолжим. (01:19:54) Следующий тип сжатия сетей – это квантизация. Если в предыдущих трех методах мы убирали избыточность в весах нашей модели, т.е. мы считали, что наша обычная модель избыточна, и если мы уберем часть весов, уменьшим их количество, то модель не сильно потеряет в точности. Здесь мы тоже смотрим на избыточность в весах, но смотрим на избыточность не в их количестве, а в их представлении. В чем предпосылки для появления этого метода? Во-первых, у нас слои в нейронных сетях довольно устойчивы к шуму и маленьким изменениям. Если мы немножко поменяем значение наших весов, модель не сильно потеряет в качестве. Если достаточно маленькая магнитуда шума, то она останется практически прежней. У нас веса лежат в довольно маленьком диапазоне. Если мы построим гистограмму весов, то часто они лежат в совсем маленьком диапазоне, порядка единицы у них границы. Чтобы кодировать данные веса, представлять их в численном виде, нам не нужны такие сложные представления, как какой-нибудь float32. Предлагается представить обученные веса нашей модели в более простом виде, например, в 8-битном. Это нужно затем, что арифметика с меньшим количеством бит на процессорах, на графических ускорителях значительно быстрее. Можно посмотреть пример, что у нас есть семейство весов, где веса представлены в float-формате и в 8-битном формате. Как вы видите, при квантизации 8-битной все распределение значения перфоманса модели уходит влево вверх, куда и мы и хотим попасть в итоге. Идеально для нас, считать, как можно точно за как можно меньшее время. Квантизация нам помогает в этом. Здесь точность примерно одинаковая, при этом мы это делаем значительно быстрее. Обратите внимание, что это нелинейная шкала, а логарифмическая. Я не буду вдаваться в подробности, как мы это делаем. На графике представлено распределение весов модели, здесь 2 пика, это связано с тем, что у нас модель уже запрунена, веса, которые лежат в нуле, мы запрунили. Распределение весов мы пытаемся дискретизировать, т.е. представить и заменить веса, которые лежат здесь, на какие-то дискетные значения. Это работает примерно так. У нас изначально были веса распределены так, мы задаем дискретные значения, которым хотим, чтобы веса соответствовали. Если вес попал прямо в дискретное значение, в котором он находился, то мы его туда и переводим. Если вес находился чуть со сдвигом от отчета, то нам приходится его немного округлять и за счет этого у нас могут происходить небольшие потери в точности модели, потому что мы не прямой перевод делаем. Веса, которые лежат вот здесь, мы сжимаем в эту точку, а потом проецируем сюда. Таким образом мы представляем веса в дискретных значениях. Тоже самое мы делаем с нашими признаками. Затем, когда нам нужно делать инференс старшей модели, мы делаем свертку не с float32, а с 8-битной операцией, что значительно быстрее. (01:25:20) У данного метода особо нет аналогов. Если можно сравнивать плюсы-минусы структурированного и неструктурированного прунинга или Low-rank decomposition, то квантизация стоит особняком. Мало того, она еще и активно работает в комбинации с этими методами. Если вы хотите ускорять модель до каких-то портативных устройств, то советую вам помимо всего прочего всегда применять квантизацию, это помогает. Также ее очень легко применить. Она встроена в современный фреймфорки, такие как (pytorch, tensorflow), и довольно автоматически делается. С точки зрения кода квантизовать модель и ее веса- это несколько команд и все. Она эффективна в ускорении модели и в уменьшении занимаемой ею памяти. Также у нас не так много вариантов, как понизить битность. Изначально веса обычно тренируются 32, есть 16, 8, 4, 2 и 1. Нет такого сложного поиска по различным гиперпараметрам, как в прунинге или в малоранговом сжатии и т.д. Всем рекомендую.
Последний метод, который сегодня рассмотрим – это дистилляция знаний модели. Идея данного метода заключается в том, что большие предобученные модели содержат уже какую-то информацию о данной задаче. Можно попробовать использовать эту информацию, чтобы обучить маленькие, компактные модели. Как они содержат информацию, можно рассмотреть на примере. Мы хотим классифицировать рукописные цифры. Нам приходит на вход цифра 9, у нас Softmax выдает распределение по возможным классам. Понятно, что 9 имеет самую большую вероятность, т.к. у нас модель хорошо обучена и правильно классифицирует верный класс, но помимо всего прочего, если детально посмотреть на выходы данной модели, то можно заметить, что она улавливает тот факт, что у нас 8 похожа на 9 или на 3, или на 6, или на 5. В свою очередь, 2 или 1 не похожи. Эту информацию можно использовать, чтобы дообучить более простую модель. Статьей, с которой началась популяризация Knowledge Distillation, была статья Хинтона.
Позже этот метод был развит в более сложные методы. Давайте посмотрим на них. То, что мы говорили про выходы моделей и вероятности классов (похожих и непохожих), это относится к виду дистилляции знаний, которая основана на ответах модели — response based knowledge. Как это работает? В базовой постановке, которая была предложена Джефри Хинтоном, у нас есть предобученная модель, мы ее зовем учителем, и есть какой-то Data set и наш студент, который пока еще не такой умный и опытный, как учитель. Мы хотим его обучить. Мы можем обучать нашего студента просто на элейблах из данного Data set, но мы хотим использовать информацию нашего учителя. Давайте посмотрим, как это делается. Берется учитель, берутся его выходы … (01:30:04). Затем, вместо того, чтобы делать стандартный софтмакс, мы делаем софтмакс с повышенной температурой. Он нужен для того, чтобы вот эти вероятности были более сглаженными. Не было такого, что у нас верно предсказанный класс – это 1, а здесь около 0 все вероятности. Чтобы у нас были какие-то различия в том, что 3, 9 и 6 похожи. Данный софтмакс с повышенной температурой называется мягкий таргет (soft target). Тоже самое мы делаем для нашего ученика. Мы прогоняем через него наше входное изображение, подаем выход в софтмакс такой же температуры, как у учителя, получаем soft target ученика. Стандартный софт макс, чтобы получить вероятность, и чтобы использовать стандартный loss для обучения. В итоге loss нашей модели состоит из обычного loss и destillation loss. В данном случаемы хотим, чтобы выходы нашего студента были бы похожи на выходы учителя. В стандартном методе используется … (01:31:45) между распределениями. У этого метода много различных вариаций. На слайде изображено, как выглядит софтмакс повышенной температуры. Логит Zi/T – это выход для данного класса нашей модели, мы уменьшаем его на T (на температуру). Кто знаком с физикой, часто встречался с такой формулой, где присутствует экспонента с какой-то температурой. На распределение Больцмана очень похоже. Также мы можем использовать какие-то внутренние представления нашей модели, это называется Feature based knowledge. Например, если у нас очень похож учитель и студент, то мы можем добавить такой loss, который говорит о том, что выходы соответствующих feature учителя и студента очень похожи между собой. Мы добавляем регуляризацию на обучение студента. Как правило, это помогает ему лучше выучиться.
Последний вариант дистилляции знаний – это дистилляция знаний, основанных на соотношении между признаками или объектами. Оно характеризуется следующей формулой. Есть какая-то функция, которая работает с feature учителя и студента. Мы можем также взять выход слоя 1, выход слоя 2 с нашего учителя, построить матрицу Грама между этими выходами. Тоже самое сделать со студентом и говорить, что эти 2 матрицы Грама будут похожи между собой. Мы выучиваем, чтобы соотношение между feature слоя 1 и 2 у учителя и ученика были похожими. Либо мы можем делать соотношение между различными данными. Мы можем получить выходы для 2 различных картинок (учителя и студента), какой-то функцией задать соотношение между выходами этих картинок и учить модель, чтобы у нас соотношение выходов между этими картинками учителя и студента было одинаково, и таким образом обучать нашу модель. (01:34:59)
Как видите, у нас есть архитектура учителя и архитектура студента. Как показывает исследование, то, как учится студент, очень зависит от архитектуры обоих (учителя и студента). Часто бывает, если у учителя слишком сложная модель, а у студента стишком простая, то дистилляция знаний работает плохо. Лучше, чтобы эти модели были плюс-минус близки. Например, это может быть модель одной и той же архитектуры, но взята с меньшим количеством каналов или квантизованная, или запруненная, или у нее разложены сверточные слои. Таким образом мы можем лучше дообучить студента, и он будет ближе к учителю. В этом заключается подход к дистилляции знаний. Его плюс, что в некоторых случаях он может улучшить производительность модели. Но есть момент Capacity gap – если вместимость модели учителя много больше, чем вместимость модели ученика, то они учатся плохо. Также есть различные исследования, которые ставят под сомнение данный подход, показывая, что улучшение, которое было достигнуто с помощью дистилляции знаний, может быть достигнуто и без него, при каких-то сетапах он вообще не работает. Много обсуждений, но в некоторых кейсах данный метод показывает себя как рабочий. Спасибо за внимание!
Дмитрий Меньщиков: Когда у нас неравные Capacity сетей студента и учителя, учитель может научить чему-то студента в рамках возможностей студента. Если мы возьмем один и тот же Data set, обучим студента напрямую на этом Data set и обучим точно такого же студента, но путем использования сети учителя. Были ли исследования, показывающие, насколько в данном случае дистилляция более эффективна, чем прямое обучение?
Константин Соболев: Мы хотим в качестве учителя использовать такую же модель, как студент?
Дмитрий Меньщиков: Нет-нет. У нас есть очень хороший учитель и небольшой студент. Учитель обучен на конкретном Data set (пусть это будет Imaginet). Затем заведомо простую модель обучим на Imaginet напрямую и обучим ее с помощью учителя. Были ли исследования на эту тему?
Константин Соболев: Да, были. Это как раз то, о чем я говорил. Очень сильно зависит от модели ученика. В каких-то случаях это дает выигрыш, а в каких-то не дает. Боле того, есть исследования, что в каких-то конкретных случаях дистилляция может даже вредить обучению модели. Рассматривался процесс обучения модели с помощью дистилляции и показывалось, что на каком-то этапе обучения она начинает больше фититься на destillation loss, и это мешает ей лучше зафититься на задачу самого Data set. (01:40:04) Это открытая проблема данного метода, что нет четкого понимания, в каких случаях он работает, а в каких нет. Даже в тех случаях, когда он работает, моно найти какие-то недостатки.
Дмитрий Меньщиков: Константин Островский спрашивает: «Можно ли решить Capacity gap серией дистилляций с постепенно уменьшаемой сложностью модели?»
Константин Соболев: Да, есть такие методы. Промежуточные, более простые учителя называются teacher-assistant. Я видел несколько работ, где их применяют. Вводили одного teacher-assistant, промежуточную модель, и это помогало решить данную проблему. Была работа, в которой предлагали итеративно переходить от более сложных моделей к более простым, в итоге мы сходимся к самой простой, к которой мы хотели прийти в конце.
Дмитрий Меньщиков: Большое спасибо! (01:41:33)