Русский
Русский
English
Статистика
Реклама

Pytorch

Перевод Как писать аккуратные конвейеры для машинного обучения

17.08.2020 18:04:48 | Автор: admin
Здравствуйте, Хабр.

Тема конвейеризации и распараллеливания машинного обучения давно фигурирует у нас в проработке. В частности, интересно, достаточно ли для этого специализированной книги с акцентом на Python, либо нужна более обзорная и, возможно, сложная литература. Мы решили перевести вводную статью об устройстве конвейеров для машинного обучения, содержащую как архитектурные, так и более прикладные соображения. Давайте обсудим, актуальны ли поиски в этом направлении.


Вы когда-нибудь писали конвейер машинного обучения, на прогон которого требовалось много времени? Или хуже того: доходили ли до этапа, когда требуется сохранить на диске промежуточные части конвейера, чтобы можно было изучить этапы конвейера по одному, опираясь на контрольные точки? Или еще хуже: вы когда-нибудь пытались рефакторить такой отвратительный код для машинного обучения перед пуском этого кода в продакшен и обнаруживали, что на это нужны месяцы? Да, с подобным приходилось сталкиваться каждому, кто достаточно долго работал над конвейерами машинного обучения. Так почему бы не построить хороший конвейер, который обеспечит нам достаточную гибкость и возможность легко рефакторить код для последующей отправки в продакшен?

Сначала давайте дадим определение конвейерам машинного обучения и обсудим идею использования контрольных точек между этапами конвейера. Затем посмотрим, как можно реализовать такие контрольные точки, чтобы не выстрелить себе в ногу при переводе конвейера в продакшен. Мы также обсудим потоковую передачу данных и компромиссы, связанные с инкапсуляцией в духе объектно-ориентированного программирования (ООП), на которые приходится идти в конвейерах при указании гиперпараметров.

ЧТО ТАКОЕ КОНВЕЙЕР?

Конвейер это последовательность шагов при преобразовании данных. Он создается в соответствии со старинным паттерном проектирования канал и фильтр (вспомните, например, команды unix bash с каналами | или операторами редиректа >). Однако конвейеры это объекты в коде. Следовательно, у вас может быть класс для каждого фильтра (то есть, для каждого этапа конвейера), а также еще один класс для комбинации всех этих этапов в готовый конвейер. Некоторые конвейеры могут объединять другие конвейеры последовательно или параллельно, иметь много входов или выходов и т.д. Конвейеры машинного обучения удобно рассматривать как:

  • Канал и фильтры. На этапах конвейера обрабатываются данные, причем, этапы управляют своим внутренним состоянием, которое можно узнать из данных.
  • Компоновка. Конвейеры можно вкладывать друг в друга; например, целый конвейер можно трактовать как один этап в рамках другого конвейера. Эта конвейера не обязательно является конвейером, но конвейер как таковой по определению не менее чем этап конвейера.
  • Ориентированные ациклические графы (DAG). Вывод этапа конвейера может направляться множеству других этапов, после чего результирующие выводы могут рекомбинироваться и так далее. Отметим: несмотря на то, что конвейеры ацикличны, они могут обрабатывать множество элементов один за другим, и, если их состояние меняется (например, при использовании метода fit_transform на каждом этапе), то их можно считать рекуррентно разворачивающимися во времени, сохраняющими при этом свои состояния (по образцу RNN).Это интересный ракурс, позволяющий рассматривать конвейеры как средство для онлайнового машинного обучения, после чего конвейеры можно переводить в продакшен и обучать на более обширных данных.


Методы конвейера

Конвейеры (или этапы конвейера) обязательно должны обладать следующими двумя методами:

  • fit для обучения на данных и приобретения состояния (напр., таким состоянием являются веса нейронной сети)
  • transform (или predict) для фактической обработки данных и генерации прогноза.
  • Примечание: если этапу конвейера не требуется один из этих методов, то этап может унаследовать от NonFittableMixin или NonTransformableMixin, где будет по умолчанию предоставляться такая реализация одного из этих методов, чтобы он ничего не дела.


На этапах конвейера также могут опционально определяться следующие методы:

  • fit_transform для подгонки и последующего преобразования данных, но в один проход, что допускает потенциальную оптимизацию кода в случаях, когда два метода должны выполняться непосредственно один после другого.
  • setup который будет вызывать метод setup на каждом из таких этапов конвейера. Например, если на этапе конвейера содержится нейронная сеть TensorFlow, PyTorch или Keras, то на этих этапах могли бы создаваться собственные нейронные графы и регистрироваться для работы с GPU в методе setup до подгонки. Не рекомендуется создавать графы прямо в конструкторах этапов до подгонки; на то есть несколько причин. Например, до запуска этапы могут многократно копироваться с разными гиперпапарметрами в рамках работы алгоритма Automatic Machine Learning, который подыскивает для вас наилучшие гиперпараметры.
  • teardown, этот метод функционально противоположен setup: он сносит ресурсы.


Следующие методы предоставляются по умолчанию, обеспечивая управление гиперпараметрами:

  • get_hyperparams возвращает словарь гиперпараметров. Если конвейер содержит другие (вложенные) конвейеры, то ключи гиперпараметров сцепляются при помощи двойных нижних подчеркиваний __.
  • set_hyperparams позволяет задавать новые гиперпараметры в том же формате, в каком вы их получаете.
  • get_hyperparams_space позволяет вам получить пространство гиперпараметра, которое будет непустым, если вы определили гиперпараметр. Поэтому, все отличие от get_hyperparams в данном случае таково, что вы получаете в качестве значений статистические распределения, а не точное значение. Например, один гиперпараметр, для количества слоев, может быть RandInt(1, 3) то есть, предусматривать от 1 до 3 слоев. Можно вызвать call .rvs() с этим словарем, чтобы случайным образом выбрать значение и отправить его к set_hyperparams, попытавшись таким образом организовать обучение.
  • set_hyperparams_space может использоваться для задания нового пространства при помощи тех же классов для распределения гиперпараметров, что и в случае с get_hyperparams_space.


Переподгонка конвейера, мини-батчинг и онлайновое обучение

Для алгоритмов, использующих мини-батчинг, например, при обучении глубоких нейронных сетей (DNN) или для алгоритмов, обучающихся онлайн, например, при обучении с подкреплением (RL), для конвейеров или их этапов идеально подходит сцепление нескольких вызовов так, чтобы они следовали точно друг за другом, и на лету происходила их подгонка под размеры мини-батчей. Такая возможность поддерживается в некоторых конвейерах и на некоторых этапах конвейеров, но на определенном этапе достигнутая подгонка может сброситься из-за того, что метод fit будет вызван заново. Все зависит от того, как вы запрограммировали ваш этап конвейера. В идеале этап конвейера должен сбрасываться только после вызова метода teardown, а затем повторного вызова метода setup до следующей подгонки, и данные не сбрасывались ни между подгонками, ни в процессе преобразования.

ИСПОЛЬЗОВАНИЕ КОНТРОЛЬНХ ТОЧЕК В КОНВЕЙЕРАХ

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

За и против использования контрольных точек в конвейерах:

  • Расстановка контрольных точек может ускорить процесс работы, если этапы программирования и отладки расположены в середине или в конце конвейера. Так отпадает необходимость каждый раз заново вычислять первые этапы конвейера.
  • При выполнении оптимизации гиперпараметров (либо путем настройки вручную, либо с использованием метаобучения), вы будете только рады обойтись без пересчета первых этапов конвейера, пока занимаетесь настройкой следующих. Например, если начало вашего конвейера не содержит гиперпараметров, то оно всякий раз может быть одинаковым или почти одинаковым если гиперпараметров всего несколько. Следовательно, при работе с контрольными точками целесообразно возобновлять работу именно с тех мест, где они расставлены, если гиперпараметры и исходный код этапа, предшествующего контрольной точке, не изменились с момента последнего выполнения.
  • Возможно, вы располагаете ограниченными вычислительными ресурсами, и единственный приемлемый вариант для вас прогонять по одному этапу за раз на имеющемся аппаратном обеспечении. Можно использовать контрольную точку, затем добавить после нее еще несколько этапов, а потом данные будут использоваться с того места, на котором вы остановились, если вы захотите повторно выполнить всю структуру.


Недостатки использования контрольных точек в конвейерах:

  • При этом используются диски, поэтому, если действовать неправильно, то выполнение вашего кода может замедляться. Чтобы ускорить работу, можно, как минимум, воспользоваться RAM Disk или монтировать папку кэша к вашей RAM.
  • Для этого может потребоваться много дискового пространства. Либо много пространства RAM, при использовании каталога, монтированного к RAM.
  • Состоянием, сохраненным на диске, управлять тяжелее: для вашей программы возникает дополнительная сложность, нужная, чтобы код работал быстрее. Обратите внимание, что, с точки зрения функционального программирования, ваши функции и код больше не будут чистыми, поскольку необходимо управлять побочными эффектами, связанными с использованием дисков. Побочные эффекты, связанные с управлением состоянием диска (вашим кэшем) могут становиться почвой для возникновения всевозможных страннейших багов
.

Известно, что одни из самых сложных багов в программировании возникают из-за проблем, связанных с инвалидацией кэша.

В Computer Science есть всего две по-настоящему сложные вещи: инвалидация кэша и именование сущностей. Фил Карлтон


Совет о том, как правильно управлять состоянием и кэшем в конвейерах.

Известно, что фреймворки для программирования и паттерны проектирования могут выступать ограничивающим фактором по той простой причине, что регламентируют определенные правила. Остается надеяться, что это делается ради максимального упрощения задач по управлению кодом, чтобы вы сами избегали ошибок, а ваш код не получался грязным. Вот мои пять копеек по поводу проектирования в контексте конвейеров и управления состоянием:

ЭТАП КОНВЕЙЕРА НЕ ДОЛЖН УПРАВЛЯТЬ РАССТАНОВКОЙ КОНТРОЛЬНХ ТОЧЕК В ТЕХ ДАННХ, КОТОРЕ ВДАЮТ


Для управления этим должна применяться специальная библиотека конвейеризации, которая сможет все это сделать за вас сама.

Почему?

Почему же этапы конвейера не должны управлять расстановкой контрольных точек в тех данных, которые выдают? По тем же веским причинам, по которым вы пользуетесь при работе библиотекой или фреймворком, а не воспроизводите соответствующий функционал самостоятельно:

  • У вас будет простой выключатель, который позволит с легкостью полностью активировать или отменить расстановку контрольных точек перед развертыванием сделанного в продакшен.
  • Когда требуется переучить систему на новых данных, окажется, что управление кэшированием поставлено настолько хорошо, что система сама заметит: ваши данные изменились и, следовательно, имеющийся кэш следует игнорировать. Вашего вмешательства при этом совершенно не потребуется, что позволит не допустить возникновения серьезных багов.
  • Вам не придется самостоятельно иметь дело с дисками и писать операции ввода/вывода (I/O) на каждом этапе конвейера. Большинство программистов предпочитают пользоваться алгоритмами машинного обучения и строить конвейеры, а не заниматься созданием методов сериализации данных. Будем честны: вы же хотите просто запрограммировать готовенькие алгоритмы, а все остальное чтобы было сделано за вас. Верно?
  • Теперь вы можете придумывать названия для каждого из ваших конвейерных экспериментов или каждой итерации, так, чтобы при каждом рестарте в кэше создавался новый подкаталог строго на данный случай даже если вы собираетесь переиспользовать одни и те же этапы конвейера. Причем, именовать этапы экспериментов даже не требуется, поскольку с изменением данных меняется и кэширование.
  • Внутренний код классов, описывающих этапы вашего конвейера, хэшируется, после чего хэши сравниваются, чтобы посмотреть, нужно ли заново выполнить кэширование для того класса, в котором вы только что изменили код. Именно так избегаются баги, связанные с инвалидацией кэша. Ура.
  • Теперь вы можете хэшировать промежуточные результаты обработки данных и пропускать этап вычисления конвейера на этих данных, если гиперпраметры не изменились, а ваш конвейер уже преобразовал (и, следовательно, хэшировал) данные ранее. Это может упростить тонкую настройку гиперпараметров в случаях, когда некоторые этапы конвейера (в том числе, промежуточные) могут меняться. Например, первые этапы конвейера могут оставаться кэшированными, поскольку изменения их не затрагивают, а, если у вас появятся дополнительные гиперпараметры, которые потребуется настроить на дальнейших этапах конвейера, то вы всегда сможете добавить нужное количество контрольных точек после этих этапов. Тогда полученные в результате многократного кэширования этапы сохраняются с уникальным именем, вычисленным на основе хэша. Можете считать такую систему блокчейном, так как это и есть блокчейн.


Это круто. Грамотно подбирая абстракции, вы теперь можете запрограммировать конвейеры для машинного обучения так, чтобы этап настройки гиперпараметров радикально ускорился; для этого нужно кэшировать промежуточный результат каждого испытания, пропуская этапы конвейера раз за разом, когда гиперпараметры промежуточных этапов конвейера остаются без изменений. Более того, когда вы будете готовы вывести код в продакшен, вы сможете сразу целиком отключить кэширование, а не рефакторить для этого код на протяжении целого месяца.

Не врезайтесь в эту стену.

ПОТОКОВАЯ ПЕРЕДАЧА ДАННХ В КОНВЕЙЕРАХ ДЛЯ МАШИННОГО ОБУЧЕНИЯ

Теория параллельной обработки гласит, что конвейеры это инструмент потоковой передачи данных, позволяющий распараллеливать этапы конвейера. Пример с прачечной хорошо иллюстрирует как эту проблему, так и ее решение. Например, на втором этапе конвейера может приступить к обработке частичной информации, полученной с первого этапа конвейера, в то время как первый этап продолжает вычислять новые данные. Причем, для работы второго этапа конвейера не требуется, чтобы первый этап полностью завершил свой этап обработки всех данных. Давайте назовем такие особые конвейеры потоковыми (см. здесь и здесь).

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

Активировать потоковую обработку может быть совсем просто: использовать класс StreamingPipeline вместо Pipeline для сцепления этапов одного за другим. При этом указывается размер мини-батча и размер очереди (во избежание чрезмерного потребления RAM, так обеспечивается более стабильная работа в продакшене). В идеале такая структура также потребовала бы многопоточных очередей с семафорами, как это описано в задаче поставщика и потребителя: чтобы организовать передачу информации от одного этапа конвейера к другому.

В нашей компании Neuraxle уже удается делать одну вещь лучше, чем она реализована в scikit-learn: речь идет о последовательных конвейерах, которыми можно пользоваться при помощи класса MiniBatchSequentialPipeline. Пока эта штука не многопоточная (но это в планах). Как минимум, она уже передает данные в конвейер в виде мини-батчей в процессе подгонки или преобразования, до сбора результатов, что позволяет работать с большими конвейерами точно как в scikit-learn, но на этот раз с применением мини-батчинга, а также с многочисленными другими возможностями, среди которых: пространства гиперпараметров, установочные методы, автоматическое машинное обучение и так далее.

Наше решение параллельной потоковой обработки данных на Python

  • Метод подгонки и/или преобразования можно вызывать много раз подряд, чтобы улучшить подгонку при помощи новых мини-батчей.
  • Многопоточные очереди внутри конвейера используются так, как в проблеме поставщика-потребителя. Между любыми двумя этапами конвейера, передаваемыми по потоковому принципу, нужна одна очередь.
  • Можно обеспечить параллельную репликацию этапов конвейера, чтобы на каждом этапе параллельно преобразовывать множество элементов. Это можно делать до того, как по всему конвейеру будут вызваны методы setup. В противном случае конвейер необходимо сериализовать, клонировать и перезагрузить с использованием механизмов, сохраняющих этапы. Код, использующий TensorFlow, и иной импортированный код, написанный на других языках, например, на C++, сложно распределить на потоки в Python, особенно если он использует память GPU. Даже joblib не так легко справляется с некоторыми из таких проблем. Поэтому благоразумно избегать подобных проблем при помощи грамотной сериализации.
  • Параметр конвейера может быть важен, а может быть и не важен для поддержания данных в правильном порядке перед отправкой их на следующий этап. По умолчанию он важен, а если нет то конвейер может продолжать обработку данных в произвольном порядке, по мере поступления, причем, так и бывает, если на разные этапы требуется разное количество времени.
  • Будет можно использовать барьерные объекты между этапами конвейера. Они будут представлять собой не настоящие этапы, а указания конвейеру, как обращаться с данными между этапами; например, должны или нет данные сохранять определенный порядок в заданных ключевых местах. Например, можно использовать барьеры, предусматривающие соблюдение порядка, не предусматривающие соблюдения порядка, либо дожидающиеся всех данных блокирующие барьеры (мы назвали такой барьер Joiner). Эти барьеры добавляют информацию о том, как обрабатывать данные между этапами или группами этапов. Например, на конкретном этапе я могу задавать или переопределять длину очереди и указывать, сколько раз нужно параллельно прогнать этап конвейера, как распараллелить этот этап.


Более того, мы хотим обеспечить возможность разделения между потоками любого объекта в Python: так он будет поддаваться сериализации и перезагрузке. В таком случае код можно будет динамически отправлять на обработку на любом воркере (это может быть другой компьютер или процесс), даже если сам нужный код на этом воркере отсутствует. Это делается при помощи цепочки сериализаторов, специфичных для каждого класса, воплощающего этап конвейера. По умолчанию на каждом из этих этапов есть сериализатор, позволяющий обрабатывать обычный код на Python, а для более заковыристого кода применять GPU и импортировать код на других языках. Модели просто сериализуются при помощи своих сейверов, а затем заново загружаются в воркер. Если воркер локальный, то объекты могут быть сериализованы на диск, расположенный в RAM, или в каталог, монтированный в RAM.

КОМПРОМИСС ПРИ ИНКАПСУЛЯЦИИ

Остается еще одна досадная вещь, присущая большинству библиотек для конвейерного машинного обучения. Речь о том, как обрабатываются гиперпараметры. Возьмем для примера scikit-learn. Пространства гиперпараметров (они же статистические распределения значений гиперпараметров) часто должны указываться вне конвейера с нижними подчеркиваниями в качестве разделительных знаков между этапами конвейера (конвейеров). Тогда как Случайный Поиск и Поиск по сетке позволяют исследовать сетки гиперпараметров или пространства вероятностей гиперпараметров, как это определяется в дистрибутивах scipy, сама scikit-learn не предоставляет пространства гиперпараметров по умолчанию для каждого классификатора и преобразователя. Ответственность за выполнение этих функций можно возложить на каждый из объектов конвейера. Таким образом, объект будет самодостаточен и будет содержать собственные гиперпараметры. Так не нарушается принцип единственной ответственности, принцип открытости/закрытоcти и принципы SOLID объектно-ориентированного программирования.

СОВМЕСТИМОСТЬ И ИНТЕГРАЦИЯ

Программируя конвейеры для машинного обучения, полезно держать в уме, что они должны сохранять совместимость со множеством других инструментов, в частности, scikit-learn, TensorFlow, Keras, PyTorch и многими другими библиотеками машинного и глубокого обучения.
Например, мы написали метод .tosklearn() позволяющий превращать этапы конвейера или целый конвейер в BaseEstimator базовый объект библиотеки scikit-learn. Что касается других библиотек машинного обучения, задача сводится к написанию нового класса, который наследует от нашего BaseStep и к переопределению в конкретном коде операций подгонки и преобразования, а также, возможно, установки и сноса. Также нужно определить сейвер, который будет сохранять и загружать вашу модель. Вот документация по классу BaseStep и примеры к ней.

ЗАКЛЮЧЕНИЕ

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

  • В коде для машинного обучения целесообразно использовать конвейеры, и каждый этап конвейера определять как экземпляр класса.
  • Затем вся такая структура может быть оптимизирована при помощи контрольных точек, помогающих найти наилучшие гиперпараметры и многократно выполнять код над одними и теми же данными (но, возможно, с разными гиперпараметрами или с измененным исходным кодом).
  • Также целесообразно выполнять подгонку и преобразование данных последовательно, чтобы не раздувать RAM. Затем всю такую структуру можно распараллелить, когда переключаешься с последовательного конвейера на потоковый.
  • Наконец, можно программировать собственные этапы конвейеров для этого достаточно наследовать от класса BaseStep, показанного в этой статье, и реализовать нужные вам методы.
Подробнее..

DARPA Challenge в песочнице

08.11.2020 18:18:22 | Автор: admin
image

Когда-то давно, бум online-образования только начинался, я прошел курс на ai-class.com. Захотелось сделать простую игру, в которой бы нейронная сеть обучалась, наблюдая за действиями пользователя. Игру хотелось сделать в стиле flappy birds, обучение должно было происходить в реальном времени, чтобы в любой момент можно было передать управление нейронной сети. В итоге я сделал маленький симулятор управления машинкой, которая обучается ездить сама. Получилась интересная комбинация pygame, pytorch и multiprocessing. Если интересно, добро пожаловать под кат.

Идею с игрой упростил до задачи управлять машинкой, которая едет по сгенерированной дороге со случайными препятствиями. Реализация симулятора была моим первым опытом с pygame.
На скриншотах, справа от дороги показаны веса всех слоев нейронной сети; слева необученная сеть, справа уже содержит тайные знания и что-то умеет.

Принцип работы


После запуска сеть инициализируется случайными значениями. На каждом кадре запоминаются нормированные показания 24х лидаров и последняя команда пользователя (left, right, straight). Таким образом получаем задачу классификации с тремя классами. Когда набирается N примеров для обучения (в данном случае 500), они отправляются в task_queue, где их ожидает модель для обучения в параллельном процессе. После обучения, состояние модели отправляется в result_queue, где в основном процессе обновляются параметры модели, рисуются новые значения весов, и пользователь может переключиться в режим автопилота.

Стоит отметить проблемы при обучении на таких данных:
  • Чаще всего приходится ехать прямо, поэтому обучающая выборка сильно не сбалансирована, и после обучения такая модель будет иметь тенденцию проезжать сквозь повороты. Исправить это можно отсечением примеров преобладающего класса (down-sample the majority class)
  • Когда автопилот попадает в критические ситуации, модель не знает что с этим делать, т.к. этого не было в обучающих данных. В моей версии машинка просто врежется, но решением было бы телепортировать машинку в критическую ситуацию и показать, как из нее выруливать.


Модель и обучение


Я использовал следующую модель из 24х входных нейронов и трех скрытых слоев, на выходе 3 нейрона, максимальное значение есть предсказанная команда. Код выглядит вот так:
class Model(nn.Module):    def __init__(self, in_features=24, hidden=[56, 48, 48], out_features=3):        super().__init__()        layer_sizes = [in_features] + hidden        layers = []        for i in range(len(layer_sizes) - 1):            layers.append(nn.Linear(layer_sizes[i], layer_sizes[i + 1]))            layers.append(nn.ReLU(inplace=True))        layers.append(nn.Linear(layer_sizes[-1], out_features))        self.layers = nn.Sequential(*layers)    def forward(self, x):        return self.layers(x)


Обучение метод обратного распространения ошибки, можно найти в статьях по pytorch:
criterion = nn.CrossEntropyLoss()optimizer = torch.optim.Adam(model.parameters(), lr=0.005)epochs = 7000for i in range(epochs):    y_pred = model.forward(X_train)    loss = criterion(y_pred, y_train)    if i % 100 == 1:        print(f'epoch: {i:2}  loss: {loss.item():10.8f}')    optimizer.zero_grad()    loss.backward()    optimizer.step()


Ниже видео, как это работает:


Исходный код всего проекта находится здесь. Предлагаю читателям поэкспериментировать с архитектурой нейронной сети, и количеством лидаров, а так же обучить машинку своему стилю вождения.

Это была моя проба пера в pytorch. Для создания и отладки модели было достаточно знаний курса на Udemy.
Хотелось бы услышать мнение опытных коллег, как в существующей постановке задачи сделать так, чтобы модель обучилась совсем не врезаться в препятствия.

Спасибо за внимание!

P.S. Картинки предоставлены ilyar
Подробнее..

Как мы обучили нейросеть генерировать тени на фотографии

25.12.2020 10:05:30 | Автор: admin

Привет, Хабр!

Я работаю Computer Vision Engineer в Everypixel и сегодня расскажу вам, как мы учили генеративно-состязательную сеть создавать тени на изображении.

Разрабатывать GAN не так трудно, как кажется на первый взгляд. В научном мире существует множество статей и публикаций на тему генеративно-состязательных сетей. В этой статье я покажу вам, как можно реализовать архитектуру нейросети и решение, предложенное в одной из научных статей. В качестве опорной статьи я выбрал ARShadowGAN публикация о GAN, генерирующей реалистичные тени для нового, вставленного в изображение объекта. Поскольку от оригинальной архитектуры я буду отклоняться, то дальше я буду называть своё решение ARShadowGAN-like.

Пример работы нейронной сети ARShadowGAN-likeПример работы нейронной сети ARShadowGAN-like

Вот что вам понадобится:

  • браузер;

  • опыт работы с Python;

  • гугл-аккаунт для того, чтобы работать в среде Google Colaboratory.

Описание генеративно-состязательной сети

Напомню, что генеративно-состязательная сеть состоит из двух сетей:

  • генератора, создающего изображение из входного шума (у нас генератор будет создавать тень, принимая изображение без тени и маску вставленного объекта);

  • дискриминатора, различающего настоящее изображение от поддельного, полученного от генератора.

Упрощённая схема ARShadowGAN-likeУпрощённая схема ARShadowGAN-like

Генератор и дискриминатор работают вместе. Генератор учится всё лучше и лучше генерировать тень, обманывать дискриминатор. Дискриминатор же учится качественно отвечать на вопрос, настоящее ли изображение.

Основная задача научить генератор создавать качественную тень. Дискриминатор нужен только для более качественного обучения, а в дальнейших этапах (тестирование, инференс, продакшн и т.д.) он участвовать не будет.

Генератор

Генератор ARShadowGAN-like состоит из двух основных блоков: attention и shadow generation (SG).

Схема генератораСхема генератора

Одна из фишек для улучшения качества результатов использование механизма внимания. Карты внимания это, иначе говоря, маски сегментации, состоящие из нулей (чёрный цвет) и единиц (белый цвет, область интереса).

Attention блок генерирует так называемые карты внимания карты тех областей изображения, на которые сети нужно обращать больше внимания. Здесь в качестве таких карт будут выступать маска соседних объектов (окклюдеров) и маска падающих от них теней. Так мы будто бы указываем сети, как нужно генерировать тень, она ориентируется, используя в качестве подсказки тени от соседних объектов. Подход очень похож на то, как человек действовал бы в жизни при построении тени вручную, в фотошопе.

Архитектура модуля: U-Net, в котором 4 канала на входе (RGB-изображение без тени и маска вставленного объекта) и 2 канала на выходе (маска окклюдеров и соответствующих им теням).

Shadow generation самый важный блок в архитектуре всей сети. Его цель: создание 3-канальной маски тени. Он, аналогично attention, имеет U-Net-архитектуру с дополнительным блоком уточнения тени на выходе (refinement). На вход блоку поступает вся известная на данный момент информация: исходное изображение без тени (3 канала), маска вставленного объекта (1 канал) и выход attention блока маска соседних объектов (1 канал) и маска теней от них (1 канал). Таким образом, на вход модулю приходит 6-канальный тензор. На выходе 3 канала цветная маска тени для вставленного объекта.

Выход shadow generation попиксельно конкатенируется (складывается) с исходным изображением, в результате чего получается изображение с тенью. Конкатенация также напоминает послойную накладку тени в фотошопе тень будто вставили поверх исходной картинки.

Дискриминатор

В качестве дискриминатора возьмем дискриминатор от SRGAN. Он привлек своей небольшой, но достаточно мощной архитектурой, а также простотой реализации.

Таким образом, полная схема ARShadowGAN-like будет выглядеть примерно так (да, мелко, но крупным планом отдельные кусочки были показаны выше ):

Полная схема обучения ARShadowGAN-likeПолная схема обучения ARShadowGAN-like

О датасете

Обучение генеративно-состязательных сетей обычно бывает paired и unpaired.

С парными данными (paired) всё достаточно прозрачно: используется подход обучения с учителем, то есть имеется правильный ответ (ground truth), с которым можно сравнить выход генератора. Для обучения сети составляются пары изображений: исходное изображение измененное исходное изображение. Нейронная сеть учится генерировать из исходного изображения его модифицированную версию.

Непарное обучение подход обучения сети без учителя. Зачастую такой подход используется, когда получить парные данные либо невозможно, либо трудно. Например, unpaired обучение часто применяется в задаче Style Transfer перенос стиля с одного изображения на другое. Здесь вообще неизвестен правильный ответ, именно поэтому происходит обучение без учителя.

Пример Style TransferПример Style Transfer

Изображение взято здесь.

Вернемся к нашей задаче генерации теней. Авторы ARShadowGAN используют парные данные для обучения своей сети. Парами здесь являются изображение без тени соответствующее ему изображение с тенью.

Как же собрать такой датасет?

Вариантов здесь достаточно много, приведу некоторые из них:

  • Можно попробовать собрать такой набор данных вручную отснять датасет. Нужно зафиксировать сцену, параметры камеры и т.д., после чего занулять тени в исходной сцене (например, с помощью регулирования света) и получать снимки без тени и с тенью. Такой подход очень трудоемок и затратен.

  • Альтернативным подходом я вижу сбор датасета из других изображений с тенями. Логика такая: возьмем изображение с тенью и тень удалим. Отсюда вытекает другая, не менее лёгкая задача Image Inpainting восстановление вырезанных мест в изображении, либо опять же ручная работа в фотошопе. Кроме того, сеть может легко переобучиться на таком датасете, поскольку могут обнаружиться артефакты, которые не видны человеческому глазу, но заметны на более глубоком семантическом уровне.

  • Еще один способ сбор синтетического датасета с помощью 3D. Авторы ARShadowGAN пошли по этому пути и собрали ShadowAR-dataset. Идея следующая: сперва авторы выбрали несколько 3D-моделей из известной библиотеки ShapeNet, затем эти модели фиксировались в правильном положении относительно сцены. Далее запускался рендер этих объектов на прозрачном фоне с включенным источником освещения и выключенным с тенью и без тени. После этого рендеры выбранных объектов просто вставлялись на 2D-изображения сцен без дополнительных обработок. Так получили пары: исходное изображение без тени (noshadow) и ground truth изображение с тенью (shadow). Подробнее о сборе ShadowAR-dataset можно почитать в оригинальной статье.

Итак, пары изображений noshadow и shadow у нас есть. Откуда берутся маски?

Напомню, что масок у нас три: маска вставленного объекта, маска соседних объектов (окклюдеров) и маска теней от них. Маска вставленного объекта легко получаются после рендера объекта на прозрачном фоне. Прозрачный фон заливается черным цветом, все остальные области, относящиеся к нашему объекту, белым. Маски же соседних объектов и теней от них были получены авторами ARShadowGAN с помощью привлечения человеческой разметки (краудсорсинга).

Пример Shadow-AR датасета.Пример Shadow-AR датасета.

Функции потерь и метрики

Attention

В этом месте отклонимся от статьи возьмем функцию потерь для решения задачи сегментации.

Генерацию карт внимания (масок) можно рассматривать как классическую задачу сегментации изображений. В качестве функции потерь возьмем Dice Loss. Она хорошо устойчива по отношению к несбалансированным данным.

В качестве метрики возьмем IoU (Intersection over Union).

Подробнее о Dice Loss и IoU можно посмотреть здесь.

Shadow generation

Функцию потерь для блока генерации возьмем подобной той, что приведена в оригинальной статье. Она будет состоять из взвешенной суммы трёх функций потерь: L2, Lper и Ladv:

L2 будет оценивать расстояние от ground truth изображения до сгенерированных (до и после refinement-блока, обозначенного как R).

Lper (perceptual loss) функция потерь, вычисляющая расстояние между картами признаков сети VGG16 при прогоне через неё изображений. Разница считается стандартным MSE между ground truth изображением с тенью и сгенерированными изображениями до и после refinement-блока соответственно.

Ladv стандартный adversarial лосс, который учитывает соревновательный момент между генератором и дискриминатором. Здесь D(.) вероятность принадлежности к классу настоящее изображение. В ходе обучения генератор пытается минимизировать Ladv, в то время как дискриминатор, наоборот, пытается его максимизировать.

Подготовка

Установка необходимых модулей

Для реализации ARShadowGAN-like будет использоваться библиотека глубокого обучения для Python pytorch.

Используемые библиотеки: что для чего?

Работу начнём с установки необходимых модулей:

  • segmentation-models-pytorch для импорта U-Net архитектуры;

  • albumentations для аугментаций;

  • piq для импорта необходимой функции потерь;

  • matplotlib для отрисовки изображений внутри ноутбуков;

  • numpy для работы с массивами;

  • opencv-python для работы с изображениями;

  • tensorboard для визуализации графиков обучения;

  • torch для нейронных сетей и глубокого обучения;

  • torchvision для импорта моделей, для глубокого обучения;

  • tqdm для progress bar визуализации.

pip install segmentation-models-pytorch==0.1.0pip install albumentations==0.5.1pip install piq==0.5.1pip install matplotlib==3.2.1pip install numpy==1.18.4pip install opencv-python>=3.4.5.20pip install tensorboard==2.2.1pip install torch>=1.5.0pip install torchvision>=0.6.0pip install tqdm>=4.41.1

Датасет

Датасет: структура, скачивание, распаковка

Для обучения и тестирования я буду использовать готовый датасет. В нём данные уже разбиты на train и test выборки. Скачаем и распакуем его.

unzip shadow_ar_dataset.zip

Структура папок в наборе данных следующая.

Каждая из выборок содержит 5 папок с изображениями:
- noshadow (изображения без теней);
- shadow (изображения с тенями);
- mask (маски вставленных объектов);
- robject (соседние объекты или окклюдеры);
- rshadow (тени от соседних объектов).

dataset train    noshadow  example1.png, ...    shadow  example1.png, ...    mask  example1.png, ...    robject  example1.png, ...    rshadow  example1.png, ... test     noshadow  example2.png, ...     shadow  example2.png, ...     mask  example2.png, ...     robject  example2.png, ...     rshadow  example2.png, ...

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

Итак, подготовим класс ARDataset для обработки изображений и выдачи i-ой порции данных по запросу.

Импорт библиотек
import osimport os.path as ospimport cv2import randomimport numpy as npimport albumentations as albuimport torchimport torch.nn as nnfrom torch.utils.data import Dataset, DataLoaderfrom torch.autograd import Variablefrom piq import ContentLossimport segmentation_models_pytorch as smp

Далее определим сам класс. Основная функция в классе __getitem__() . Она возвращает i-ое изображение и соответствующую ему маску по запросу.

Класс ARDataset
class ARDataset(Dataset):    def __init__(self, dataset_path, augmentation=None, \                 augmentation_images=None, preprocessing=None, \                 is_train=True, ):        """ Инициализация параметров датасета        dataset_path - путь до папки train или test        augmentation - аугментации, применяемые как к изображениям, так и                       к маскам        augmentation_images - аугментации, применяемые только к         изображениям        preprocessing - предобработка изображений        is_train - флаг [True - режим обучения / False - режим предсказания]        """        noshadow_path = os.path.join(dataset_path, 'noshadow')        mask_path = os.path.join(dataset_path, 'mask')        # соберём пути до файлов        self.noshadow_paths = []; self.mask_paths = [];        self.rshadow_paths = []; self.robject_paths = [];        self.shadow_paths = [];        if is_train:            rshadow_path = osp.join(dataset_path, 'rshadow')            robject_path = osp.join(dataset_path, 'robject')            shadow_path = osp.join(dataset_path, 'shadow')        files_names_list = sorted(os.listdir(noshadow_path))        for file_name in files_names_list:            self.noshadow_paths.append(osp.join(noshadow_path, file_name))            self.mask_paths.append(osp.join(mask_path, file_name))            if is_train:                self.rshadow_paths.append(osp.join(rshadow_path, file_name))                self.robject_paths.append(osp.join(robject_path, file_name))                self.shadow_paths.append(osp.join(shadow_path, file_name))        self.augmentation = augmentation        self.augmentation_images = augmentation_images        self.preprocessing = preprocessing        self.is_train = is_train    def __getitem__(self, i):        """ Получение i-го набора из датасета.        i - индекс        Возвращает:        image - изображение с нормализацией для attention блока        mask - маска с нормализацией для attention блока        image1 - изображение с нормализацией для shadow generation блока        mask1 - маска с нормализацией для shadow generaion блока        """        # исходное изображение        image = cv2.imread(self.noshadow_paths[i])        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)        # маска вставленного объекта        mask = cv2.imread(self.mask_paths[i], 0)        if self.is_train:            # маска соседних объектов            robject_mask = cv2.imread(self.robject_paths[i], 0)            # маска теней от соседних объектов            rshadow_mask = cv2.imread(self.rshadow_paths[i], 0)            # результирующее изображение            res_image = cv2.imread(self.shadow_paths[i])            res_image = cv2.cvtColor(res_image, cv2.COLOR_BGR2RGB)            # применяем аугментации отдельно к изображениям            if self.augmentation_images:                sample = self.augmentation_images(                  image=image,                   image1=res_image                )                image = sample['image']                res_image = sample['image1']            # соберём маски в одну переменную для применения аугментаций            mask = np.stack([robject_mask, rshadow_mask, mask], axis=-1)            mask = mask.astype('float')            # аналогично с изображениями            image = np.concatenate([image, res_image], axis=2)            image = image.astype('float')        # применяем аугментации        if self.augmentation:            sample = self.augmentation(image=image, mask=mask)            image, mask = sample['image'], sample['mask']        # нормализация масок        mask[mask >= 128] = 255; mask[mask < 128] = 0        # нормализация для shadow generation блока        image1, mask1 = image.astype(np.float) / 127.5 - 1.0, \                        mask.astype(np.float) / 127.5 - 1.0        # нормализация для attention блока        image, mask = image.astype(np.float) / 255.0, \                      mask.astype(np.float) / 255.0        # применяем препроцессинг        if self.preprocessing:            sample = self.preprocessing(image=image, mask=mask)            image, mask = sample['image'], sample['mask']            sample = self.preprocessing(image=image1, mask=mask1)            image1, mask1 = sample['image'], sample['mask']        return image, mask, image1, mask1    def __len__(self):        """ Возвращает длину датасета"""        return len(self.noshadow_paths)

Объявим аугментации и функции для обработки данных. Аугментации будем брать из репозитория albumentations.

Аугментации и предобработка
def get_training_augmentation():    """ Аугментации для всех изображений, тренировочная выборка. """    train_transform = [        albu.Resize(256,256),        albu.HorizontalFlip(p=0.5),        albu.Rotate(p=0.3, limit=(-10, 10), interpolation=3, border_mode=2),    ]    return albu.Compose(train_transform)def get_validation_augmentation():    """ Аугментации для всех изображений, валидационная / тестовая выборка """    test_transform = [        albu.Resize(256,256),    ]    return albu.Compose(test_transform)def get_image_augmentation():    """ Аугментации только для изображений (не для масок). """    image_transform = [        albu.OneOf([          albu.Blur(p=0.2, blur_limit=(3, 5)),          albu.GaussNoise(p=0.2, var_limit=(10.0, 50.0)),          albu.ISONoise(p=0.2, intensity=(0.1, 0.5), \                        color_shift=(0.01, 0.05)),          albu.ImageCompression(p=0.2, quality_lower=90, quality_upper=100, \                                compression_type=0),          albu.MultiplicativeNoise(p=0.2, multiplier=(0.9, 1.1), \                                   per_channel=True, \                                   elementwise=True),        ], p=1),        albu.OneOf([          albu.HueSaturationValue(p=0.2, hue_shift_limit=(-10, 10), \                                  sat_shift_limit=(-10, 10), \                                  val_shift_limit=(-10, 10)),          albu.RandomBrightness(p=0.3, limit=(-0.1, 0.1)),          albu.RandomGamma(p=0.3, gamma_limit=(80, 100), eps=1e-07),          albu.ToGray(p=0.1),          albu.ToSepia(p=0.1),        ], p=1)    ]    return albu.Compose(image_transform, additional_targets={        'image1': 'image',        'image2': 'image'    })def get_preprocessing():    """ Препроцессинг """    _transform = [        albu.Lambda(image=to_tensor, mask=to_tensor),    ]    return albu.Compose(_transform)def to_tensor(x, **kwargs):    """ Приводит изображение в формат: [channels, width, height] """    return x.transpose(2, 0, 1).astype('float32')

Обучение

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

Датасеты, даталоадеры, девайс
# число изображений, прогоняемых через нейросеть за один разbatch_size = 8dataset_path = '/path/to/your/dataset'train_path = osp.join(dataset_path, 'train')test_path = osp.join(dataset_path, 'test')# объявим датасетыtrain_dataset = ARDataset(train_path,\                          augmentation=get_training_augmentation(),\                          preprocessing=get_preprocessing(),)valid_dataset = ARDataset(test_path, \                          augmentation=get_validation_augmentation(),\                          preprocessing=get_preprocessing(),)# объявим даталоадерыtrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=False)

Определим устройство, на котором будем учить сеть:

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

Будем учить attention и shadow generation блоки по отдельности.

Обучение attention блока

В качестве модели attention блока возьмём U-Net. Архитектуру импортируем из репозитория segmentation_models.pytorch. Для повышения качества работы сети заменим стандартную кодирующую часть U-Net на сеть-классификатор resnet34.

Поскольку на вход attention блок принимает изображение без тени и маску вставленного объекта, то заменим первый сверточный слой в модели: на вход модулю поступает 4-канальный тензор (3 цветных канала + 1 черно-белый).

# объявим модель Unet с 2 классами на выходе - 2 маски (соседние объекты и их тени)model = smp.Unet(encoder_name='resnet34', classes=2, activation='sigmoid',)# заменим в модели первый сверточный слой - на входе должно быть 4 каналаmodel.encoder.conv1 = nn.Conv2d(4, 64, kernel_size=(7, 7), stride=(2, 2), \                                padding=(3, 3), bias=False)

Объявим функцию потерь, метрику и оптимизатор.

loss = smp.utils.losses.DiceLoss()metric = smp.utils.metrics.IoU(threshold=0.5)optimizer = torch.optim.Adam([dict(params=model.parameters(), lr=1e-4),])

Создадим функцию для обучения attention блока. Обучение стандартное, состоит из трех циклов: цикла по эпохам, тренировочного цикла по батчам и валидационного цикла по батчам.

На каждой итерации по даталоадеру выполняется прямой прогон данных через модель и получение предсказаний. Далее вычисляются функции потерь и метрики, после чего выполняется обратный проход алгоритма обучения (обратное распространение ошибки), происходит обновление весов.

Функция для обучения attention и её вызов
def train(n_epoch, train_loader, valid_loader, model_path, model, loss,\          metric, optimizer, device):    """ Функция обучения сети.    n_epoch -- число эпох    train_loader -- даталоадер для тренировочной выборки    valid_loader -- даталоадер для валидационной выборки    model_path -- путь для сохранения модели    model -- предварительно объявленная модель    loss -- функция потерь    metric -- метрика    optimizer -- оптимизатор    device -- определенный torch.device    """    model.to(device)    max_score = 0    total_train_steps = len(train_loader)    total_valid_steps = len(valid_loader)    # запускаем цикл обучения    print('Start training!')    for epoch in range(n_epoch):        # переведём модель в режим тренировки        model.train()        train_loss = 0.0        train_metric = 0.0        # тренировочный цикл по батчам        for data in train_loader:            noshadow_image = data[0][:, :3].to(device)            robject_mask = torch.unsqueeze(data[1][:, 0], 1).to(device)            rshadow_mask = torch.unsqueeze(data[1][:, 1], 1).to(device)            mask = torch.unsqueeze(data[1][:, 2], 1).to(device)            # прогоним через модель            model_input = torch.cat((noshadow_image, mask), axis=1)            model_output = model(model_input)            # сравним выход модели с ground truth данными            ground_truth = torch.cat((robject_mask, rshadow_mask), axis=1)            loss_result = loss(ground_truth, model_output)            train_metric += metric(ground_truth, model_output).item()            optimizer.zero_grad()            loss_result.backward()            optimizer.step()            train_loss += loss_result.item()        # переведём модель в eval-режим        model.eval()        valid_loss = 0.0        valid_metric = 0.0        # валидационный цикл по батчам        for data in valid_loader:            noshadow_image = data[0][:, :3].to(device)            robject_mask = torch.unsqueeze(data[1][:, 0], 1).to(device)            rshadow_mask = torch.unsqueeze(data[1][:, 1], 1).to(device)            mask = torch.unsqueeze(data[1][:, 2], 1).to(device)            # прогоним через модель            model_input = torch.cat((noshadow_image, mask), axis=1)            with torch.no_grad():                model_output = model(model_input)            # сравним выход модели с ground truth данными            ground_truth = torch.cat((robject_mask, rshadow_mask), axis=1)            loss_result = loss(ground_truth, model_output)            valid_metric += metric(ground_truth, model_output).item()            valid_loss += loss_result.item()        train_loss = train_loss / total_train_steps        train_metric = train_metric / total_train_steps        valid_loss = valid_loss / total_valid_steps        valid_metric = valid_metric / total_valid_steps        print(f'\nEpoch {epoch}, train_loss: {train_loss}, train_metric: {train_metric}, valid_loss: {valid_loss}, valid_metric: {valid_metric}')        # если получили новый максимум по точности - сохраняем модель        if max_score < valid_metric:            max_score = valid_metric            torch.save(model.state_dict(), model_path)            print('Model saved!')# вызовем функцию:# число эпохn_epoch = 10# путь для сохранения моделиmodel_path = '/path/for/model/saving' train(n_epoch=n_epoch,      train_loader=train_loader,      valid_loader=valid_loader,      model_path=model_path,      model=model,      loss=loss,      metric=metric,      optimizer=optimizer,      device=device)

После того, как обучение attention блока закончено, приступим к основной части сети.

Обучение shadow generation блока

В качестве модели shadow generation блока аналогично возьмём U-Net, только в качестве кодировщика возьмем сеть полегче resnet18.

Поскольку на вход shadow generation блок принимает изображение без тени и 3 маски (маску вставленного объекта, маску соседних объектов и маску теней от них), заменим первый сверточный слой в модели: на вход модулю поступает 6-канальный тензор (3 цветных канала + 3 черно-белых).

После U-Net добавим в конце 4 refinement-блока. Один такой блок состоит из последовательности: BatchNorm2d, ReLU и Conv2d.

Объявим класс генератор.

Класс генератор
class Generator_with_Refin(nn.Module):    def __init__(self, encoder):        """ Инициализация генератора."""        super(Generator_with_Refin, self).__init__()        self.generator = smp.Unet(            encoder_name=encoder,            classes=1,            activation='identity',        )        self.generator.encoder.conv1 = nn.Conv2d(6, 64, kernel_size=(7, 7), \                                                 stride=(2, 2), padding=(3, 3), \                                                 bias=False)        self.generator.segmentation_head = nn.Identity()        self.SG_head = nn.Conv2d(in_channels=16, out_channels=3, \                                 kernel_size=3, stride=1, padding=1)        self.refinement = torch.nn.Sequential()        for i in range(4):            self.refinement.add_module(f'refinement{3*i+1}', nn.BatchNorm2d(16))            self.refinement.add_module(f'refinement{3*i+2}', nn.ReLU())            refinement3 = nn.Conv2d(in_channels=16, out_channels=16, \                                    kernel_size=3, stride=1, padding=1)            self.refinement.add_module(f'refinement{3*i+3}', refinement3)        self.output1 = nn.Conv2d(in_channels=16, out_channels=3, kernel_size=3, \                                 stride=1, padding=1)    def forward(self, x):      """ Прямой проход данных через сеть."""        x = self.generator(x)        out1 = self.SG_head(x)        x = self.refinement(x)        x = self.output1(x)        return out1, x

Объявим класс дискриминатор.

Класс дискриминатор
class Discriminator(nn.Module):    def __init__(self, input_shape):        super(Discriminator, self).__init__()        self.input_shape = input_shape        in_channels, in_height, in_width = self.input_shape        patch_h, patch_w = int(in_height / 2 ** 4), int(in_width / 2 ** 4)        self.output_shape = (1, patch_h, patch_w)        def discriminator_block(in_filters, out_filters, first_block=False):            layers = []            layers.append(nn.Conv2d(in_filters, out_filters, kernel_size=3, \                                    stride=1, padding=1))            if not first_block:                layers.append(nn.BatchNorm2d(out_filters))            layers.append(nn.LeakyReLU(0.2, inplace=True))            layers.append(nn.Conv2d(out_filters, out_filters, kernel_size=3, \                                    stride=2, padding=1))            layers.append(nn.BatchNorm2d(out_filters))            layers.append(nn.LeakyReLU(0.2, inplace=True))            return layers        layers = []        in_filters = in_channels        for i, out_filters in enumerate([64, 128, 256, 512]):            layers.extend(discriminator_block(in_filters, out_filters, \                                              first_block=(i == 0)))            in_filters = out_filters        layers.append(nn.Conv2d(out_filters, 1, kernel_size=3, stride=1, \                                padding=1))        self.model = nn.Sequential(*layers)    def forward(self, img):        return self.model(img)

Объявим объекты моделей генератора и дискриминатора, а также функции потерь и оптимизатор для генератора и дискриминатора.

Генератор, дискриминатор, функции потерь, оптимизаторы
generator = Generator_with_Refin('resnet18')discriminator = Discriminator(input_shape=(3,256,256))l2loss = nn.MSELoss()perloss = ContentLoss(feature_extractor="vgg16", layers=("relu3_3", ))GANloss = nn.MSELoss()optimizer_G = torch.optim.Adam([dict(params=generator.parameters(), lr=1e-4),])optimizer_D = torch.optim.Adam([dict(params=discriminator.parameters(), lr=1e-6),])

Всё готово для обучения, определим функцию для обучения SG блока. Её вызов будет аналогичен вызову функции обучения attention.

Функция для обучения SG блока
def train(generator, discriminator, device, n_epoch, optimizer_G, optimizer_D, train_loader, valid_loader, scheduler, losses, models_paths, bettas, writer):    """Функция для обучения SG-блока        generator: модель-генератор        discriminator: модель-дискриминатор        device: torch-device для обучения        n_epoch: количество эпох        optimizer_G: оптимизатор для модели-генератора        optimizer_D: оптимизатор для модели-дискриминатора        train_loader: даталоадер для тренировочной выборки        valid_loader: даталоадер для валидационной выборки        scheduler: шедуллер для изменения скорости обучения        losses:  список функций потерь        models_paths: список путей для сохранения моделей        bettas: список коэффициентов для функций потерь        writer: tensorboard writer    """    # перенесем модели на ГПУ    generator.to(device)    discriminator.to(device)    # для валидационного минимума    val_common_min = np.inf    print('Запускаем обучение!')    for epoch in range(n_epoch):        # переводим модели в режим обучения        generator.train()        discriminator.train()        # списки для значений функций потерь        train_l2_loss = []; train_per_loss = []; train_common_loss = [];         train_D_loss = []; valid_l2_loss = []; valid_per_loss = [];         valid_common_loss = [];        print('Цикл по батчам (пакетам):')        for batch_i, data in enumerate(tqdm(train_loader)):            noshadow_image = data[2][:, :3].to(device)            shadow_image = data[2][:, 3:].to(device)            robject_mask = torch.unsqueeze(data[3][:, 0], 1).to(device)            rshadow_mask = torch.unsqueeze(data[3][:, 1], 1).to(device)            mask = torch.unsqueeze(data[3][:, 2], 1).to(device)            # подготовим входной тензор для модели            model_input = torch.cat((noshadow_image, mask, robject_mask, rshadow_mask), axis=1)            # ------------ учим генератор -------------------------------------            shadow_mask_tensor1, shadow_mask_tensor2 = generator(model_input)            result_nn_tensor1 = torch.add(noshadow_image, shadow_mask_tensor1)            result_nn_tensor2 = torch.add(noshadow_image, shadow_mask_tensor2)            for_per_shadow_image_tensor = torch.sigmoid(shadow_image)            for_per_result_nn_tensor1 = torch.sigmoid(result_nn_tensor1)            for_per_result_nn_tensor2 = torch.sigmoid(result_nn_tensor2)            # Adversarial ground truths            valid = Variable(torch.cuda.FloatTensor(np.ones((data[2].size(0), *discriminator.output_shape))), requires_grad=False)            fake = Variable(torch.cuda.FloatTensor(np.zeros((data[2].size(0), *discriminator.output_shape))), requires_grad=False)            # вычисляем функции потерь            l2_loss = losses[0](shadow_image, result_nn_tensor1) + losses[0](shadow_image, result_nn_tensor2)            per_loss = losses[1](for_per_shadow_image_tensor, for_per_result_nn_tensor1) + losses[1](for_per_shadow_image_tensor, for_per_result_nn_tensor2)            gan_loss = losses[2](discriminator(result_nn_tensor2), valid)            common_loss = bettas[0] * l2_loss + bettas[1] * per_loss + bettas[2] * gan_loss            optimizer_G.zero_grad()            common_loss.backward()            optimizer_G.step()            # ------------ учим дискриминатор ---------------------------------            optimizer_D.zero_grad()            loss_real = losses[2](discriminator(shadow_image), valid)            loss_fake = losses[2](discriminator(result_nn_tensor2.detach()), fake)            loss_D = (loss_real + loss_fake) / 2            loss_D.backward()            optimizer_D.step()            # ------------------------------------------------------------------            train_l2_loss.append((bettas[0] * l2_loss).item())            train_per_loss.append((bettas[1] * per_loss).item())            train_D_loss.append((bettas[2] * loss_D).item())            train_common_loss.append(common_loss.item())        # переводим generator в eval-режим        generator.eval()        # валидация        for batch_i, data in enumerate(valid_loader):            noshadow_image = data[2][:, :3].to(device)            shadow_image = data[2][:, 3:].to(device)            robject_mask = torch.unsqueeze(data[3][:, 0], 1).to(device)            rshadow_mask = torch.unsqueeze(data[3][:, 1], 1).to(device)            mask = torch.unsqueeze(data[3][:, 2], 1).to(device)            # подготовим вход в для модели            model_input = torch.cat((noshadow_image, mask, robject_mask, rshadow_mask), axis=1)            with torch.no_grad():                shadow_mask_tensor1, shadow_mask_tensor2 = generator(model_input)            result_nn_tensor1 = torch.add(noshadow_image, shadow_mask_tensor1)            result_nn_tensor2 = torch.add(noshadow_image, shadow_mask_tensor2)            for_per_result_shadow_image_tensor = torch.sigmoid(shadow_image)            for_per_result_nn_tensor1 = torch.sigmoid(result_nn_tensor1)            for_per_result_nn_tensor2 = torch.sigmoid(result_nn_tensor2)            # вычисляем функции потерь            l2_loss = losses[0](shadow_image, result_nn_tensor1) + losses[0](shadow_image, result_nn_tensor2)            per_loss = losses[1](for_per_result_shadow_image_tensor, for_per_result_nn_tensor1) + losses[1](for_per_result_shadow_image_tensor, for_per_result_nn_tensor2)            common_loss = bettas[0] * l2_loss + bettas[1] * per_loss            valid_per_loss.append((bettas[1] * per_loss).item())            valid_l2_loss.append((bettas[0] * l2_loss).item())            valid_common_loss.append(common_loss.item())        # усредняем значения функций потерь        tr_l2_loss = np.mean(train_l2_loss)        val_l2_loss = np.mean(valid_l2_loss)        tr_per_loss = np.mean(train_per_loss)        val_per_loss = np.mean(valid_per_loss)        tr_common_loss = np.mean(train_common_loss)        val_common_loss = np.mean(valid_common_loss)        tr_D_loss = np.mean(train_D_loss)        # добавляем результаты в tensorboard        writer.add_scalar('tr_l2_loss', tr_l2_loss, epoch)        writer.add_scalar('val_l2_loss', val_l2_loss, epoch)        writer.add_scalar('tr_per_loss', tr_per_loss, epoch)        writer.add_scalar('val_per_loss', val_per_loss, epoch)        writer.add_scalar('tr_common_loss', tr_common_loss, epoch)        writer.add_scalar('val_common_loss', val_common_loss, epoch)        writer.add_scalar('tr_D_loss', tr_D_loss, epoch)        # печатаем информацию        print(f'\nEpoch {epoch}, tr_common loss: {tr_common_loss:.4f}, val_common loss: {val_common_loss:.4f}, D_loss {tr_D_loss:.4f}')        if val_common_loss <= val_common_min:            # сохраняем лучшую модель            torch.save(generator.state_dict(), models_paths[0])            torch.save(discriminator.state_dict(), models_paths[1])            val_common_min = val_common_loss            print(f'Model saved!')        # делаем шаг шедуллера        scheduler.step(val_common_loss)

Процесс обучения

Визуализация процесса обучения Визуализация процесса обучения

Графики, общая информация

Для обучения я использовал видеокарту GTX 1080Ti на сервере hostkey. В процессе я отслеживал изменение функций потерь по построенным графикам с помощью утилиты tensorboard. Ниже, на рисунках, представлены графики обучения на тренировочной и валидационной выборке.

Графики обучения тренировочная выборкаГрафики обучения тренировочная выборка

Особенно полезен второй рисунок, поскольку валидационная выборка не участвует в процессе обучения генератора и является независимой. По графикам обучения видно, что выход на плато произошел в районе 200-250-й эпохи. Здесь можно было уже тормозить обучение генератора, поскольку монотонность у функции потерь отсутствует.

Однако полезно также смотреть на графики обучения в логарифмической шкале она более наглядно показывает монотонность графика. По графику логарифма валидационной функции потерь видим, что обучение в районе 200-250-й эпохи останавливать рановато, можно было сделать это позже, на 400-й эпохе.

Графики обучения валидационная выборкаГрафики обучения валидационная выборка

Для наглядности эксперимента периодически происходило сохранение предсказанной картинки (см. гифку визуализации процесса обучения выше).

Некоторые трудности

В процессе обучения пришлось решить достаточно простую проблему неправильное взвешивание функций потерь.

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

При неправильной балансировке лосс-функций мы можем получить неудовлетворительные результаты, например, если для L2 задать слишком сильный вклад, то обучение нейронной сети может и вовсе застопориться. L2 достаточно быстро сходится, но при этом совсем убирать её из общей суммы тоже нежелательно выходная тень будет получаться менее реалистичной, менее консистентной по цвету и прозрачности.

Пример сгенерированной тени в случае отсутствия вклада L2-лоссаПример сгенерированной тени в случае отсутствия вклада L2-лосса

На картинке слева ground truth изображение, справа сгенерированное изображение.

Инференс

Для предсказания и тестирования объединим модели attention и SG в один класс ARShadowGAN.

Класс ARShadowGAN, объединяющий attention и shadow generation блоки
class ARShadowGAN(nn.Module):    def __init__(self, model_path_attention, model_path_SG, encoder_att='resnet34', \                 encoder_SG='resnet18', device='cuda:0'):        super(ARShadowGAN, self).__init__()        self.device = torch.device(device)        self.model_att = smp.Unet(            classes=2,            encoder_name=encoder_att,            activation='sigmoid'        )        self.model_att.encoder.conv1 = nn.Conv2d(4, 64, kernel_size=(7,7), stride=(2,2), padding=(3,3), bias=False)        self.model_att.load_state_dict(torch.load(model_path_attention))        self.model_att.to(device)        self.model_SG = Generator_with_Refin(encoder_SG)        self.model_SG.load_state_dict(torch.load(model_path_SG))        self.model_SG.to(device)    def forward(self, tensor_att, tensor_SG):        self.model_att.eval()        with torch.no_grad():            robject_rshadow_tensor = self.model_att(tensor_att)        robject_rshadow_np = robject_rshadow_tensor.cpu().numpy()        robject_rshadow_np[robject_rshadow_np >= 0.5] = 1        robject_rshadow_np[robject_rshadow_np < 0.5] = 0        robject_rshadow_np = 2 * (robject_rshadow_np - 0.5)        robject_rshadow_tensor = torch.cuda.FloatTensor(robject_rshadow_np)        tensor_SG = torch.cat((tensor_SG, robject_rshadow_tensor), axis=1)        self.model_SG.eval()        with torch.no_grad():            output_mask1, output_mask2 = self.model_SG(tensor_SG)        result = torch.add(tensor_SG[:,:3, ...], output_mask2)        return result, output_mask2

Далее приведём сам код инференса.

Инференс
# укажем пути до данных и чекпоинтовdataset_path = '/content/arshadowgan/uploaded'result_path = '/content/arshadowgan/uploaded/shadow'path_att = '/content/drive/MyDrive/ARShadowGAN-like/attention.pth'path_SG = '/content/drive/MyDrive/ARShadowGAN-like/SG_generator.pth'# объявим датасет и даталоадерdataset = ARDataset(dataset_path, augmentation=get_validation_augmentation(256), preprocessing=get_preprocessing(), is_train=False)dataloader = DataLoader(dataset, batch_size=1, shuffle=False, num_workers=0)# определим устройствоdevice = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')# объявим полную модельmodel = ARShadowGAN(    encoder_att='resnet34',    encoder_SG='resnet18',    model_path_attention=path_att,    model_path_SG=path_SG,    device=device)# переведем ее в режим тестированияmodel.eval()# предсказаниеfor i, data in enumerate(dataloader):    tensor_att = torch.cat((data[0][:, :3], torch.unsqueeze(data[1][:, -1], axis=1)), axis=1).to(device)    tensor_SG = torch.cat((data[2][:, :3], torch.unsqueeze(data[3][:, -1], axis=1)), axis=1).to(device)    with torch.no_grad():        result, shadow_mask = model(tensor_att, tensor_SG)        shadow_mask = np.uint8(127.5*shadow_mask[0].cpu().numpy().transpose((1,2,0)) + 1.0)        output_image = np.uint8(127.5 * (result.cpu().numpy()[0].transpose(1,2,0) + 1.0))        cv2.imwrite(osp.join(result_path, 'test.png'), output_image)        print('Результат сохранен: ' + result_path + '/test.png')

Заключение

В данной статье рассмотрена генеративно-состязательная сеть на примере решения одной из амбициозных и непростых задач на стыке Augmented Reality и Computer Vision. В целом полученная модель умеет генерировать тени, пусть и не всегда идеально.

Отмечу, что GAN это не единственный способ генерации тени, существуют и другие подходы, в которых, например, используются техники 3D-реконструкции объекта, дифференцированный рендеринг и т.п.

Весь приведенный код в репозитории, примеры запуска в Google Colab ноутбуке.

P.S. Буду рад открытой дискуссии, каким-либо замечаниям и предложениям.

Спасибо за внимание!

Подробнее..

3D teeth instance segmentation. В темноте, но не один

23.05.2021 14:09:40 | Автор: admin

3D сегментация зубов от поиска данных до конечного результата. Почти.

Дисклеймер

Данная статья не является обучающей в любом понимании этого термина и носит сугубо информативный характер. Автор статьи не несет ответственности за время, потраченное на ее чтение.

Об авторе

Добрый - всем, зовут Андрей(27). Постараюсь коротко. Почему программирование? По образованию - бакалавр электромеханик, профессию знаю. Отработал 2 года на должности инженера-энергетика в буровой компании вполне успешно, вместо повышения написал заявление - сгорел, да не по мне оказалось это всё. Нравится создавать, находить решения сложных задач, с ПК в обнимку с сознательных лет. Выбор очевиден. Вначале (полгода назад), всерьёз думал записаться на курсы от Я или подобные. Начитался отзывов, поговорил с участниками и понял что с получением информацией проблем нет. Так нашел сайт, там получил базу по Python и с ним уже начал свой путь (сейчас там постепенно изучаю всё, что связано с ML). Сразу заинтересовало машинное обучение, CV в частности. Придумал себе задачу и вот здесь (по мне, так отличный способ учиться).

1. Введение

В результате нескольких неудачных попыток, пришел к решению использовать 2 легковесные модели для получения желаемого результата. 1-ая сегментирует все зубы как [1, 0] категорию, а вторая делит их на категории[0, 8]. Но начнем по порядку.

2. Поиск и подготовка данных

Потратив не один вечер на поиск данных для работы, пришел в выводу что в свободном доступе челюсть в хорошем качестве и формате (*.stl, *.nrrd и т.д.) не получится. Лучшее, что мне попалось - это тестовый образец головы пациента после хирургической операции на челюсти в программе 3D Slicer.

Очевидно, мне не нужна голова целиком, поэтому обрезал исходник в той же программе до размера 163*112*120рх (в данном посте {x*y*z = ш-г-в} и 1рх - 0,5мм), оставив только зубы и сопутствующие челюстно-лицевые части.

Уже больше похоже на то что нужно, дальше - интереснее. Теперь нужно создать маски всех необходимых нам объектов. Для тех, кто уже работал с этим - "autothreshold" не то чтобы совсем не работает, просто лишнего много, думаю, исправление заняло бы столько же времени, сколько и разметка вручную(через маски).

- Пиксели(срезы слева)? - Вспоминаем размер изображения- Пиксели(срезы слева)? - Вспоминаем размер изображения

Размечал часов 12~14. И да, тот факт что я не сразу разметил каждый зуб как категорию стоил мне еще порядка 4 часов. В итоге у нас есть данные, с которыми у же можно работать.

Конечный вариант маски. Smooth 0.5. (сглаживание в обучении не использовалось)Конечный вариант маски. Smooth 0.5. (сглаживание в обучении не использовалось)

Должен добавить, даже на мой (без опыта) взгляд, этих данных очень мало для обучения и последующей полноценной работы нейронной сети. На данном этапе, единственное что пришло в голову, повернуть имеющиеся данные N-раз и соединить, random-crop использовать не стал.

Код подготовки данных
import nrrdimport torchimport torchvision.transforms as tfclass DataBuilder:    def __init__(self,                 data_path,                 list_of_categories,                 num_of_chunks: int = 0,                 augmentation_coeff: int = 0,                 num_of_classes: int = 0,                 normalise: bool = False,                 fit: bool = True,                 data_format: int = 0,                 save_data: bool = False                 ):        self.data_path = data_path        self.number_of_chunks = num_of_chunks        self.augmentation_coeff = augmentation_coeff        self.list_of_cats = list_of_categories        self.num_of_cls = num_of_classes        self.normalise = normalise        self.fit = fit        self.data_format = data_format        self.save_data = save_data    def forward(self):        data = self.get_data()        data = self.fit_data(data) if self.fit else data        data = self.pre_normalize(data) if self.normalise else data        data = self.data_augmentation(data, self.augmentation_coeff) if self.augmentation_coeff != 0 else data        data = self.new_chunks(data, self.number_of_chunks) if self.number_of_chunks != 0 else data        data = self.category_splitter(data, self.num_of_cls, self.list_of_cats) if self.num_of_cls != 0 else data        torch.save(data, self.data_path[-14:]+'.pt') if self.save_data else None        return torch.unsqueeze(data, 1)    def get_data(self):        if self.data_format == 0:            return torch.from_numpy(nrrd.read(self.data_path)[0])        elif self.data_format == 1:            return torch.load(self.data_path).cpu()        elif self.data_format == 2:            return torch.unsqueeze(self.data_path, 0).cpu()        else:            print('Available types are: "nrrd", "tensor" or "self.tensor(w/o load)"')    @staticmethod    def fit_data(some_data):        data = torch.movedim(some_data, (1, 0), (0, -1))        data_add_x = torch.nn.ZeroPad2d((5, 0, 0, 0))        data = data_add_x(data)        data = torch.movedim(data, -1, 0)        data_add_z = torch.nn.ZeroPad2d((0, 0, 8, 0))        return data_add_z(data)    @staticmethod    def pre_normalize(some_data):        min_d, max_d = torch.min(some_data), torch.max(some_data)        return (some_data - min_d) / (max_d - min_d)    @staticmethod    def data_augmentation(some_data, aug_n):        torch.manual_seed(17)        tr_data = []        for e in range(aug_n):            transform = tf.RandomRotation(degrees=(20*e, 20*e))            for image in some_data:                image = torch.unsqueeze(image, 0)                image = transform(image)                tr_data.append(image)        return tr_data    def new_chunks(self, some_data, n_ch):        data = torch.stack(some_data, 0) if self.augmentation_coeff != 0 else some_data        data = torch.squeeze(data, 1)        chunks = torch.chunk(data, n_ch, 0)        return torch.stack(chunks)    @staticmethod    def category_splitter(some_data, alpha, list_of_categories):        data, _ = torch.squeeze(some_data, 1).to(torch.int64), alpha        for i in list_of_categories:            data = torch.where(data < i, _, data)            _ += 1        return data - alpha

Имейте ввиду что это финальная версия кода подготовки данных для 3D U-net. Форвард:

  • Загружаем дату (в зависимости от типа).

  • Добавляем 0 по краям чтобы подогнать размер до 168*120*120 (вместо исходных 163*112*120). *пригодится дальше.

  • Нормализуем входящие данные в 0...1 (исходные ~-2000...16000).

  • Поворачиваем N-раз и соединяем.

  • Полученные данные режем на равные части чтобы забить память видеокарты по максимуму (в моем случае это 1, 1, 72, 120, 120).

  • Эта часть распределяет по категориям 28 имеющихся зубов и фон для облегчения обучения моделей (см. Введение):

    • одну категорию для 1-ой;

    • на 9 категорий (8+фон) для 2-ой.

Dataloader стандартный
import torch.utils.data as tudclass ToothDataset(tud.Dataset):    def __init__(self, images, masks):        self.images = images        self.masks = masks    def __len__(self): return len(self.images)    def __getitem__(self, index):        if self.masks is not None:            return self.images[index, :, :, :, :],\                    self.masks[index, :, :, :, :]        else:            return self.images[index, :, :, :, :]def get_loaders(images, masks,                batch_size: int = 1,                num_workers: int = 1,                pin_memory: bool = True):    train_ds = ToothDataset(images=images,                            masks=masks)    data_loader = tud.DataLoader(train_ds,                                 batch_size=batch_size,                                 shuffle=False,                                 num_workers=num_workers,                                 pin_memory=pin_memory)    return data_loader

На выходе имеем следующее:

Semantic

Instance

Predictions

Data

(27*, 1, 56*, 120,120)[0...1]

(27*, 1, 56*, 120,120) [0, 1]

(1, 1, 168, 120, 120)[0...1]

Masks

(27*, 1, 56*, 120,120)[0, 1]

(27*, 1, 56*, 120,120)[0, 8]

-

*эти размеры менялись, в зависимости от эксперимента, подробности - дальше.

3. Выбор и настройка моделей обучения

Цель работы - обучение. Поэтому взял наиболее простую и понятную для себя модель нейросети архитектуры U-Net. Код не выкладываю, можно посмотреть тут.

2D U-Net2D U-Net

Подробно рассказывать не буду, информации в достатке в сети. Метод оптимизации - Adam, функция расчета потерь Dice-loss(implement), спусков/подъемов 4, фильтры [64, 128, 256, 512] (знаю, много, об этом - позже). Обучал в среднем 60-80 epochs на эксперимент. Transfer learning не использовал.

model.summary()
model = UNet(dim=2, in_channels=1, out_channels=1, n_blocks=4, start_filters=64).to(device)print(summary(model, (1, 168, 120)))"""----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv2d-1         [-1, 64, 168, 120]             640              ReLU-2         [-1, 64, 168, 120]               0       BatchNorm2d-3         [-1, 64, 168, 120]             128            Conv2d-4         [-1, 64, 168, 120]          36,928              ReLU-5         [-1, 64, 168, 120]               0       BatchNorm2d-6         [-1, 64, 168, 120]             128         MaxPool2d-7           [-1, 64, 84, 60]               0         DownBlock-8  [[-1, 64, 84, 60], [-1, 64, 168, 120]]  0            Conv2d-9          [-1, 128, 84, 60]          73,856             ReLU-10          [-1, 128, 84, 60]               0      BatchNorm2d-11          [-1, 128, 84, 60]             256           Conv2d-12          [-1, 128, 84, 60]         147,584             ReLU-13          [-1, 128, 84, 60]               0      BatchNorm2d-14          [-1, 128, 84, 60]             256        MaxPool2d-15          [-1, 128, 42, 30]               0        DownBlock-16  [[-1, 128, 42, 30], [-1, 128, 84, 60]]  0           Conv2d-17          [-1, 256, 42, 30]         295,168             ReLU-18          [-1, 256, 42, 30]               0      BatchNorm2d-19          [-1, 256, 42, 30]             512           Conv2d-20          [-1, 256, 42, 30]         590,080             ReLU-21          [-1, 256, 42, 30]               0      BatchNorm2d-22          [-1, 256, 42, 30]             512        MaxPool2d-23          [-1, 256, 21, 15]               0        DownBlock-24  [[-1, 256, 21, 15], [-1, 256, 42, 30]]  0           Conv2d-25          [-1, 512, 21, 15]       1,180,160             ReLU-26          [-1, 512, 21, 15]               0      BatchNorm2d-27          [-1, 512, 21, 15]           1,024           Conv2d-28          [-1, 512, 21, 15]       2,359,808             ReLU-29          [-1, 512, 21, 15]               0      BatchNorm2d-30          [-1, 512, 21, 15]           1,024        DownBlock-31  [[-1, 512, 21, 15], [-1, 512, 21, 15]]  0  ConvTranspose2d-32          [-1, 256, 42, 30]         524,544             ReLU-33          [-1, 256, 42, 30]               0      BatchNorm2d-34          [-1, 256, 42, 30]             512      Concatenate-35          [-1, 512, 42, 30]               0           Conv2d-36          [-1, 256, 42, 30]       1,179,904             ReLU-37          [-1, 256, 42, 30]               0      BatchNorm2d-38          [-1, 256, 42, 30]             512           Conv2d-39          [-1, 256, 42, 30]         590,080             ReLU-40          [-1, 256, 42, 30]               0      BatchNorm2d-41          [-1, 256, 42, 30]             512          UpBlock-42          [-1, 256, 42, 30]               0  ConvTranspose2d-43          [-1, 128, 84, 60]         131,200             ReLU-44          [-1, 128, 84, 60]               0      BatchNorm2d-45          [-1, 128, 84, 60]             256      Concatenate-46          [-1, 256, 84, 60]               0           Conv2d-47          [-1, 128, 84, 60]         295,040             ReLU-48          [-1, 128, 84, 60]               0      BatchNorm2d-49          [-1, 128, 84, 60]             256           Conv2d-50          [-1, 128, 84, 60]         147,584             ReLU-51          [-1, 128, 84, 60]               0      BatchNorm2d-52          [-1, 128, 84, 60]             256          UpBlock-53          [-1, 128, 84, 60]               0  ConvTranspose2d-54         [-1, 64, 168, 120]          32,832             ReLU-55         [-1, 64, 168, 120]               0      BatchNorm2d-56         [-1, 64, 168, 120]             128      Concatenate-57        [-1, 128, 168, 120]               0           Conv2d-58         [-1, 64, 168, 120]          73,792             ReLU-59         [-1, 64, 168, 120]               0      BatchNorm2d-60         [-1, 64, 168, 120]             128           Conv2d-61         [-1, 64, 168, 120]          36,928             ReLU-62         [-1, 64, 168, 120]               0      BatchNorm2d-63         [-1, 64, 168, 120]             128          UpBlock-64         [-1, 64, 168, 120]               0           Conv2d-65          [-1, 1, 168, 120]              65================================================================Total params: 7,702,721Trainable params: 7,702,721Non-trainable params: 0----------------------------------------------------------------Input size (MB): 0.08Forward/backward pass size (MB): 7434.08Params size (MB): 29.38Estimated Total Size (MB): 7463.54"""
Эксп.12D U-Net, подача изображений покадрово, плоскость [x, z]Эксп.12D U-Net, подача изображений покадрово, плоскость [x, z]

Определенно, это - зубы. Только кроме зубов есть много всего, нам ненужного. Подробнее о трансформации numpy - *.stl в Главе 6. Посмотрим ещё раз на фактический размер и качество изображений, которые попадают на вход нейросети:

Слева на право:1. Не видно[x, y]. 2. Немного лучше[x, z]. 3.Ещё лучше[y, z]Слева на право:1. Не видно[x, y]. 2. Немного лучше[x, z]. 3.Ещё лучше[y, z]

Если сам не видишь на 100% где там начался зуб а где нет, то как тогда эту работу выполнит нейросеть? Как минимум, необходимо изменить плоскость подачи изображения.

Проведя не один день разбираясь в том, как можно улучшить сложившуюся ситуацию, пришел к тому, что можно составлять каскад и сетей, поочередно обрабатывающих изображение, аналогично работе фильтров грубой и тонкой очистки.

Эксп.2Каскад 2-ух 2D U-Net, подача изображений покадрово, плоскость [y, z]Эксп.2Каскад 2-ух 2D U-Net, подача изображений покадрово, плоскость [y, z]

Прогресс виден, однако вместе с помехами пропадают и части зубов, дальнейшее обучение тому подтверждение:

Эксп.3Каскад 2-ух 2D U-Net, подача изображений покадрово плоскость [y, z]с увеличением времени обучения на 50%Эксп.3Каскад 2-ух 2D U-Net, подача изображений покадрово плоскость [y, z]с увеличением времени обучения на 50%

Ввиду последних событий было принято решение о переходе на 3D архитектуру нейронной сети. Переподготовил входные данные, а именно разделил на части размером (24*, 120, 120). Почему так? - изначально большая модель обучения (~22млн. параметров). Моя видеокарта(1063gtx) не могла физически вместить больше.

24*

Это размер глубины. Был подобран так чтобы:

  • количество данных(1512, 120, 120) делится нацело на это число - получается 63;

  • в свою очередь получившийся batch size (24, 120, 120) - максимум, вмещающийся в память видеокарты с текущими параметрами сети;

  • само это число (24) делилось на количество спусков/подъемов так же нацело (имеется в виду соответствие выражению 24/2/2/2=3 и 3*2*2*2=24, где количество делений/умножений на 2 соответствует количеству спусков/подъемов минус 1);

  • то же самое не только для глубины данных, но и длинны и ширины. Подробнее в .summary()

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=1, n_blocks=4, start_filters=64).to(device)print(summary(model, (1, 24, 120, 120)))"""  ----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1     [-1, 64, 24, 120, 120]             1,792              ReLU-2     [-1, 64, 24, 120, 120]                 0       BatchNorm3d-3     [-1, 64, 24, 120, 120]               128            Conv3d-4     [-1, 64, 24, 120, 120]           110,656              ReLU-5     [-1, 64, 24, 120, 120]                 0       BatchNorm3d-6     [-1, 64, 24, 120, 120]               128         MaxPool3d-7        [-1, 64, 12, 60, 60]                0         DownBlock-8  [[-1, 64, 12, 60, 60], [-1, 64, 24, 120, 120]]               0            Conv3d-9       [-1, 128, 12, 60, 60]          221,312             ReLU-10       [-1, 128, 12, 60, 60]                0      BatchNorm3d-11       [-1, 128, 12, 60, 60]              256           Conv3d-12       [-1, 128, 12, 60, 60]          442,496             ReLU-13       [-1, 128, 12, 60, 60]                0      BatchNorm3d-14       [-1, 128, 12, 60, 60]              256        MaxPool3d-15       [-1, 128, 6, 30, 30]                 0        DownBlock-16  [[-1, 128, 6, 30, 30], [-1, 128, 12, 60, 60]]               0           Conv3d-17       [-1, 256, 6, 30, 30]           884,992             ReLU-18       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-19       [-1, 256, 6, 30, 30]               512           Conv3d-20       [-1, 256, 6, 30, 30]         1,769,728             ReLU-21       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-22       [-1, 256, 6, 30, 30]               512        MaxPool3d-23       [-1, 256, 3, 15, 15]                 0        DownBlock-24  [[-1, 256, 3, 15, 15], [-1, 256, 6, 30, 30]]               0           Conv3d-25       [-1, 512, 3, 15, 15]         3,539,456             ReLU-26       [-1, 512, 3, 15, 15]                 0      BatchNorm3d-27       [-1, 512, 3, 15, 15]             1,024           Conv3d-28       [-1, 512, 3, 15, 15]         7,078,400             ReLU-29       [-1, 512, 3, 15, 15]                 0      BatchNorm3d-30       [-1, 512, 3, 15, 15]             1,024        DownBlock-31  [[-1, 512, 3, 15, 15], [-1, 512, 3, 15, 15]]               0  ConvTranspose3d-32       [-1, 256, 6, 30, 30]         1,048,832             ReLU-33       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-34       [-1, 256, 6, 30, 30]               512      Concatenate-35       [-1, 512, 6, 30, 30]                 0           Conv3d-36       [-1, 256, 6, 30, 30]         3,539,200             ReLU-37       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-38       [-1, 256, 6, 30, 30]               512           Conv3d-39       [-1, 256, 6, 30, 30]         1,769,728             ReLU-40       [-1, 256, 6, 30, 30]                 0      BatchNorm3d-41       [-1, 256, 6, 30, 30]               512          UpBlock-42       [-1, 256, 6, 30, 30]                 0  ConvTranspose3d-43       [-1, 128, 12, 60, 60]          262,272             ReLU-44       [-1, 128, 12, 60, 60]                0      BatchNorm3d-45       [-1, 128, 12, 60, 60]              256      Concatenate-46       [-1, 256, 12, 60, 60]                0           Conv3d-47       [-1, 128, 12, 60, 60]          884,864             ReLU-48       [-1, 128, 12, 60, 60]                0      BatchNorm3d-49       [-1, 128, 12, 60, 60]              256           Conv3d-50       [-1, 128, 12, 60, 60]          442,496             ReLU-51       [-1, 128, 12, 60, 60]                0      BatchNorm3d-52       [-1, 128, 12, 60, 60]              256          UpBlock-53       [-1, 128, 12, 60, 60]                0  ConvTranspose3d-54       [-1, 64, 24, 120, 120]          65,600             ReLU-55       [-1, 64, 24, 120, 120]               0      BatchNorm3d-56       [-1, 64, 24, 120, 120]             128      Concatenate-57      [-1, 128, 24, 120, 120]               0           Conv3d-58       [-1, 64, 24, 120, 120]         221,248             ReLU-59       [-1, 64, 24, 120, 120]               0      BatchNorm3d-60       [-1, 64, 24, 120, 120]             128           Conv3d-61       [-1, 64, 24, 120, 120]         110,656             ReLU-62       [-1, 64, 24, 120, 120]               0      BatchNorm3d-63       [-1, 64, 24, 120, 120]             128          UpBlock-64       [-1, 64, 24, 120, 120]               0           Conv3d-65        [-1, 1, 24, 120, 120]              65================================================================Total params: 22,400,321Trainable params: 22,400,321Non-trainable params: 0----------------------------------------------------------------Input size (MB): 0.61Forward/backward pass size (MB): 15974.12Params size (MB): 85.45Estimated Total Size (MB): 16060.18----------------------------------------------------------------"""
Эксп.43D U-Net, подача объемом, плоскость [y, z],время*0,38Эксп.43D U-Net, подача объемом, плоскость [y, z],время*0,38

С учетом сокращенного на ~60% времени обучения(25 epochs) результат меня устроил, продолжаем.

Эксп.53D U-Net, подача объемом, плоскость [y, z], 65 epochs ~ 1,5 часа Эксп.53D U-Net, подача объемом, плоскость [y, z], 65 epochs ~ 1,5 часа

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

Эксп.63D U-Net, подача объемом, плоскость [x, z], 105 epochs ~ 2,1 часа Эксп.63D U-Net, подача объемом, плоскость [x, z], 105 epochs ~ 2,1 часа

"Научный" перебор параметров в течении недели принес результат. Уменьшил количество параметров сети до ~400к (от первоначальных ~22м) путем уменьшения фильтра [18, 32, 64, 128] и спуска/подъема до 3. Изменил метод оптимизации на RSMProp. Уменьшение количества параметров нейросети позволило увеличить объем входных данных в три раза (1, 1, 72*, 120, 120). Посмотрим результат?

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=1, n_blocks=3, start_filters=18).to(device)print(summary(model, (1, 1, 72, 120, 120)))"""----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1     [-1, 18, 72, 120, 120]             504              ReLU-2     [-1, 18, 72, 120, 120]               0       BatchNorm3d-3     [-1, 18, 72, 120, 120]              36            Conv3d-4     [-1, 18, 72, 120, 120]           8,766              ReLU-5     [-1, 18, 72, 120, 120]               0       BatchNorm3d-6     [-1, 18, 72, 120, 120]              36         MaxPool3d-7       [-1, 18, 36, 60, 60]               0         DownBlock-8  [[-1, 18, 36, 60, 60], [-1, 18, 24, 120, 120]]               0            Conv3d-9       [-1, 36, 36, 60, 60]          17,532             ReLU-10       [-1, 36, 36, 60, 60]               0      BatchNorm3d-11       [-1, 36, 36, 60, 60]              72           Conv3d-12       [-1, 36, 36, 60, 60]          35,028             ReLU-13       [-1, 36, 36, 60, 60]               0      BatchNorm3d-14       [-1, 36, 36, 60, 60]              72        MaxPool3d-15        [-1, 36, 18, 30, 30]              0        DownBlock-16  [[-1, 36, 18, 30, 30], [-1, 36, 36, 60, 60]]               0           Conv3d-17        [-1, 72, 18, 30, 30]         70,056             ReLU-18        [-1, 72, 18, 30, 30]              0      BatchNorm3d-19        [-1, 72, 18, 30, 30]            144           Conv3d-20        [-1, 72, 18, 30, 30]        140,040             ReLU-21        [-1, 72, 18, 30, 30]              0      BatchNorm3d-22        [-1, 72, 18, 30, 30]            144        DownBlock-23  [[-1, 72, 18, 30, 30], [-1, 72, 18, 30, 30]]               0  ConvTranspose3d-24       [-1, 36, 36, 60, 60]          20,772             ReLU-25       [-1, 36, 36, 60, 60]               0      BatchNorm3d-26       [-1, 36, 36, 60, 60]              72      Concatenate-27       [-1, 72, 36, 60, 60]               0           Conv3d-28       [-1, 36, 36, 60, 60]          70,020             ReLU-29       [-1, 36, 36, 60, 60]               0      BatchNorm3d-30       [-1, 36, 36, 60, 60]              72           Conv3d-31       [-1, 36, 36, 60, 60]          35,028             ReLU-32       [-1, 36, 36, 60, 60]               0      BatchNorm3d-33       [-1, 36, 36, 60, 60]              72          UpBlock-34       [-1, 36, 36, 60, 60]               0  ConvTranspose3d-35     [-1, 18, 72, 120, 120]           5,202             ReLU-36     [-1, 18, 72, 120, 120]               0      BatchNorm3d-37     [-1, 18, 72, 120, 120]              36      Concatenate-38     [-1, 36, 72, 120, 120]               0           Conv3d-39     [-1, 18, 72, 120, 120]          17,514             ReLU-40     [-1, 18, 72, 120, 120]               0      BatchNorm3d-41     [-1, 18, 72, 120, 120]              36           Conv3d-42     [-1, 18, 72, 120, 120]           8,766             ReLU-43     [-1, 18, 72, 120, 120]               0      BatchNorm3d-44     [-1, 18, 72, 120, 120]              36          UpBlock-45     [-1, 18, 72, 120, 120]               0           Conv3d-46      [-1, 1, 72, 120, 120]              19================================================================Total params: 430,075Trainable params: 430,075Non-trainable params: 0----------------------------------------------------------------Input size (MB): 1.32Forward/backward pass size (MB): 5744.38Params size (MB): 1.64Estimated Total Size (MB): 5747.34----------------------------------------------------------------"""
72*

Некоторые из вас подумают, исходные данные (168, 120, 120), а часть (72, 120, 120). Назревает вопрос, как делить. Всё просто, во 2 главе мы увеличивали размер наших данных и затем делили их на части, соответствующие объему памяти видеокарты. Я увеличил данные в 9 раз (1512, 120, 120) т.е. повернул на 9 различных углов относительно одной оси, а затем разделил на 21(batch size) часть по (72, 120, 120). Так же 72 соответствует всем условиям, описанным в 24*(выше).

Эксп.73D U-Net, подача объемом, плоскость [x, z],Маска (слева) и готовая сегментация (справа),оптимизированные параметры сети,время обучения(65 epochs) ~ 14мин.Эксп.73D U-Net, подача объемом, плоскость [x, z],Маска (слева) и готовая сегментация (справа),оптимизированные параметры сети,время обучения(65 epochs) ~ 14мин.

Результат вполне удовлетворительный, есть недочеты (вроде "похудевших" зубов). Возможно, исправим их в другом посте. Для этапа semantic segmentation я думаю мы сделали достаточно, теперь необходимо задать категории.

О размере подаваемых данных

Первоначальная идея при переходе на 3D архитектуру была в том чтобы делить данные не слайсами (как в данном посте) (1512, 120, 120) --> 21*(1, 72, 120, 120), а кубиками ~х*(30, 30, 30) или около того (результат этой попытки не был сохранен оп понятным причинам). Опытным путем понял 2 вещи: чем большими порциями ты подаешь 3-х мерные объекты, тем лучше результат(для моего конкретного случая); и нужно больше изучать теорию того, с чем работаешь.

О времени обучения и размере модели

Параметры сети подобраны так, что обучение 1 epochs на моей "старушке" занимает ~13сек, а размер конечной модели не превышает 2мб (прошлая>80мб). Время рабочего цикла примерно равно 1 epochs. Однако стоит понимать, это обучение и работа на данных достаточно маленького размера.

Для разделения на категории пришлось немного повозиться с функцией расчета ошибки и визуализацией данных. Первоначально поставил себе задачу разделить на 8 категорий + фон. О loss function и визуализации поговорим подробнее.

Код training loop
import torchfrom tqdm import tqdmfrom _loss_f import LossFunctionclass TrainFunction:    def __init__(self,                 data_loader,                 device_for_training,                 model_name,                 model_name_pretrained,                 model,                 optimizer,                 scale,                 learning_rate: int = 1e-2,                 num_epochs: int = 1,                 transfer_learning: bool = False,                 binary_loss_f: bool = True                 ):        self.data_loader = data_loader        self.device = device_for_training        self.model_name_pretrained = model_name_pretrained        self.semantic_binary = binary_loss_f        self.num_epochs = num_epochs        self.model_name = model_name        self.transfer = transfer_learning        self.optimizer = optimizer        self.learning_rate = learning_rate        self.model = model        self.scale = scale    def forward(self):        print('Running on the:', torch.cuda.get_device_name(self.device))        self.model.load_state_dict(torch.load(self.model_name_pretrained)) if self.transfer else None        optimizer = self.optimizer(self.model.parameters(), lr=self.learning_rate)        for epoch in range(self.num_epochs):            self.train_loop(self.data_loader, self.model, optimizer, self.scale, epoch)            torch.save(self.model.state_dict(), 'models/' + self.model_name+str(epoch+1)                       + '_epoch.pth') if (epoch + 1) % 10 == 0 else None    def train_loop(self, loader, model, optimizer, scales, i):        loop, epoch_loss = tqdm(loader), 0        loop.set_description('Epoch %i' % (self.num_epochs - i))        for batch_idx, (data, targets) in enumerate(loop):            data, targets = data.to(device=self.device, dtype=torch.float), \                            targets.to(device=self.device, dtype=torch.long)            optimizer.zero_grad()            *тут секрет*            with torch.cuda.amp.autocast():                predictions = model(data)                loss = LossFunction(predictions, targets,                                    device_for_training=self.device,                                    semantic_binary=self.semantic_binary                                    ).forward()            scales.scale(loss).backward()            scales.step(optimizer)            scales.update()            epoch_loss += (1 - loss.item())*100            loop.set_postfix(loss=loss.item())        print('Epoch-acc', round(epoch_loss / (batch_idx+1), 2))

4. Функция расчета ошибки

Мне в целом понравилось как проявляет себя Dice-loss в сегментации, только 'проблема' в том что он работает с форматом данных [0, 1]. Однако, если предварительно разделить данные на категории (а так же привести к формату [0, 1]), и пропускать пары (имеется ввиду "предсказание" и "маска" только одной категории) в стандартную Dice-loss функцию, то это может сработать.

Код categorical_dice_loss
import torchclass LossFunction:    def __init__(self,                 prediction,                 target,                 device_for_training,                 semantic_binary: bool = True,                 ):        self.prediction = prediction        self.device = device_for_training        self.target = target        self.semantic_binary = semantic_binary    def forward(self):        if self.semantic_binary:            return self.dice_loss(self.prediction, self.target)        return self.categorical_dice_loss(self.prediction, self.target)    @staticmethod    def dice_loss(predictions, targets, alpha=1e-5):        intersection = 2. * (predictions * targets).sum()        denomination = (torch.square(predictions) + torch.square(targets)).sum()        dice_loss = 1 - torch.mean((intersection + alpha) / (denomination + alpha))        return dice_loss    def categorical_dice_loss(self, prediction, target):        pr, tr = self.prepare_for_multiclass_loss_f(prediction, target)        target_categories, losses = torch.unique(tr).tolist(), 0        for num_category in target_categories:            categorical_target = torch.where(tr == num_category, 1, 0)            categorical_prediction = pr[num_category][:][:][:]            losses += self.dice_loss(categorical_prediction, categorical_target).to(self.device)        return losses / len(target_categories)    @staticmethod    def prepare_for_multiclass_loss_f(prediction, target):        prediction_prepared = torch.squeeze(prediction, 0)        target_prepared = torch.squeeze(target, 0)        target_prepared = torch.squeeze(target_prepared, 0)        return prediction_prepared, target_prepared

Тут просто, но всё равно объясню "categorical_dice_loss":

  • подготовка данных (убираем ненужные в данном расчете измерения);

  • получения списка категорий, которые содержит каждый batch масок;

  • для каждой категории берем "прогноз" и "маску" соответствующих категорий, приводим значения к формату [0, 1] и пропускаем через стандартную Dice-loss;

  • складывая результаты и деля на количество категорий, получаем усредненное значение для каждого batct. Ну а дальше всё без изменений.

Так же, думаю, помог бы перевод данных к one-hot формату, но только не в момент формирования основного дата сета (раздует в размере), а непосредственно перед расчетом ошибки, но я не проверял. Кто в курсе, напишите, пожалуйста, буду рад. Результат работы данной функции будет в Главе(5).

5. Визуализация данных

Так и хочется добавить "..как отдельный вид искусства". Начну с того что прочитать *.nrrd оказалось самым простым.

Код
import nrrd# читает в numpyread = nrrd.read(data_path) data, meta_data = read[0], read[1]print(data.shape, np.max(data), np.min(data), meta_data, sep="\n")(163, 112, 120)14982-2254  OrderedDict([('type', 'short'), ('dimension', 3), ('space', 'left-posterior-superior'), ('sizes', array([163, 112, 120])), ('space directions', array([[-0.5,  0. ,  0. ],       [ 0. , -0.5,  0. ],       [ 0. ,  0. ,  0.5]])), ('kinds', ['domain', 'domain', 'domain']), ('endian', 'little'), ('encoding', 'gzip'), ('space origin', array([131.57200623,  80.7661972 ,  32.29940033]))])

Дальше - сложнее, как обратно перевести? Получается, если я ничего не путаю, необходимо из числа получить вершины и грани, между которыми образуется поверхность.

Неправильный путь

Иными словами, чтобы сделать куб нам необходимо 8 вершин и 12 треугольных поверхностей. В этом и состояла первая идея (до применения специальных библиотек) - заменить все пиксели (числа в 3-х мерной матрице) на такие кубики. Код я не сохранил, но смысл прост, рисуем куб на месте "пикселя" со сдвигом -1 по трем направлениям, потом следующий и т.д.

Выглядит это так же бредово, как и звучитВыглядит это так же бредово, как и звучит

Отрицательный результат - тоже результат, продолжаем. На этом этапе я уже понял, что без сторонних библиотек мне не обойтись. Первой попыткой в была пара Skimage и Stl.

from skimage.measure import marching_cubesimport nrrdimport numpy as npfrom stl import meshpath = 'some_path.nrrd'data = nrrd.read(path)[0]def three_d_creator(some_data):    vertices, faces, volume, _ = marching_cubes(some_data)    cube = mesh.Mesh(np.full(faces.shape[0], volume.shape[0], dtype=mesh.Mesh.dtype))    for i, f in enumerate(faces):        for j in range(3):            cube.vectors[i][j] = vertices[f[j]]    cube.save('name.stl')    return cubestl = three_d_creator(datas)

Пользовался этим способом, но иногда файлы "ломались" в процессе сохранения и не открывались. А на те, которые открывались, ругался встроенный в Win 10 3D Builder и постоянно пытался там что-то исправить. Так же еще придется "прикрутить" к коду модуль для просмотра 3D объектов без их сохранения. Решение "из коробки" дальше.

На момент написания статью пользуюсь v3do. Коротко, быстро, удобно и можно сразу осмотреть модель.

Код перевода npy в stl и вывода объекта на дисплей
from vedo import Volume, show, writeprediction = 'some_data_path.npy'def show_save(data, save=False):    data_multiclass = Volume(data, c='Set2', alpha=(0.1, 1), alphaUnit=0.87, mode=1)    data_multiclass.addScalarBar3D(nlabels=9)    show([(data_multiclass, "Multiclass teeth segmentation prediction")], bg='black', N=1, axes=1).close()    write(data_multiclass.isosurface(), 'some_name_.stl') if save else None    show_save(prediction, save=True)

Названия функций говорят сами за себя.

Пришло время увидеть конечный результат всего вышесказанного. Томить не буду:

model.summary()
model = UNet(dim=3, in_channels=1, out_channels=9, n_blocks=3, start_filters=9).to(device)print(summary(model, (1, 168*, 120, 120)))    """----------------------------------------------------------------        Layer (type)               Output Shape         Param #================================================================            Conv3d-1      [-1, 9, 168, 120, 120]            252              ReLU-2      [-1, 9, 168, 120, 120]              0       BatchNorm3d-3      [-1, 9, 168, 120, 120]             18            Conv3d-4      [-1, 9, 168, 120, 120]          2,196              ReLU-5      [-1, 9, 168, 120, 120]              0       BatchNorm3d-6      [-1, 9, 168, 120, 120]             18         MaxPool3d-7        [-1, 9, 84, 60, 60]               0         DownBlock-8  [[-1, 9, 84, 60, 60], [-1, 9, 168, 120, 120]]               0            Conv3d-9       [-1, 18, 84, 60, 60]           4,392             ReLU-10       [-1, 18, 84, 60, 60]               0      BatchNorm3d-11       [-1, 18, 84, 60, 60]              36           Conv3d-12       [-1, 18, 84, 60, 60]           8,766             ReLU-13       [-1, 18, 84, 60, 60]               0      BatchNorm3d-14       [-1, 18, 84, 60, 60]              36        MaxPool3d-15       [-1, 18, 42, 30, 30]               0        DownBlock-16  [[-1, 18, 18, 42, 30], [-1, 18, 84, 60, 60]]               0           Conv3d-17       [-1, 36, 42, 30, 30]          17,532             ReLU-18       [-1, 36, 42, 30, 30]               0      BatchNorm3d-19       [-1, 36, 42, 30, 30]              72           Conv3d-20       [-1, 36, 42, 30, 30]          35,028             ReLU-21       [-1, 36, 42, 30, 30]               0      BatchNorm3d-22       [-1, 36, 42, 30, 30]              72        DownBlock-23  [[-1, 36, 42, 30, 30], [-1, 36, 42, 30, 30]]               0  ConvTranspose3d-24       [-1, 18, 84, 60, 60]           5,202             ReLU-25       [-1, 18, 84, 60, 60]               0      BatchNorm3d-26       [-1, 18, 84, 60, 60]              36      Concatenate-27       [-1, 36, 84, 60, 60]               0           Conv3d-28       [-1, 18, 84, 60, 60]          17,514             ReLU-29       [-1, 18, 84, 60, 60]               0      BatchNorm3d-30       [-1, 18, 84, 60, 60]              36           Conv3d-31       [-1, 18, 84, 60, 60]           8,766             ReLU-32       [-1, 18, 84, 60, 60]               0      BatchNorm3d-33       [-1, 18, 84, 60, 60]              36          UpBlock-34       [-1, 18, 84, 60, 60]               0  ConvTranspose3d-35      [-1, 9, 168, 120, 120]          1,305             ReLU-36      [-1, 9, 168, 120, 120]              0      BatchNorm3d-37      [-1, 9, 168, 120, 120]             18      Concatenate-38     [-1, 18, 168, 120, 120]              0           Conv3d-39      [-1, 9, 168, 120, 120]          4,383             ReLU-40      [-1, 9, 168, 120, 120]              0      BatchNorm3d-41      [-1, 9, 168, 120, 120]             18           Conv3d-42      [-1, 9, 168, 120, 120]          2,196             ReLU-43      [-1, 9, 168, 120, 120]              0      BatchNorm3d-44      [-1, 9, 168, 120, 120]             18          UpBlock-45      [-1, 9, 168, 120, 120]              0           Conv3d-46      [-1, 9, 168, 120, 120]             90================================================================Total params: 108,036Trainable params: 108,036Non-trainable params: 0----------------------------------------------------------------Input size (MB): 3.96Forward/backward pass size (MB): 12170.30Params size (MB): 0.41Estimated Total Size (MB): 12174.66----------------------------------------------------------------    """

*Ввиду ещё большего уменьшения параметров сети(фильтр[9, 18, 36, 72]), удалось уместить объект в память видеокарты целиком - 9*(168, 120, 120)

6. After words

Думал, что закончил, а оказалось - только начал. Тут еще есть над чем поработать. Мне, в целом, 2 этап не нравится, хоть он и работает. Зачем заново переопределять каждый пиксель, когда мне нужен целый регион? А если, образно, есть 28 разделенных регионов, зачем мне пытаться определить их все, не проще ли определить один зуб и завязать это всё на "условный" ориентированный/неориентированный граф? Или вместо U-net использовать GCNN и вместо Pytorch - Pytorch3D? Пятна, думаю, можно убрать с помощью выравнивания данных внутри bounding box(ведь один зуб может принадлежать только 1 категории). Но, возможно, это вопросы для следующей публикации.

Прототип (набросок)
Тот самый "условный граф"
Пример неориентированного графа на 28 категорий с "разделителями"Пример неориентированного графа на 28 категорий с "разделителями"

Отдельное спасибо моей жене - Алёне, за особую поддержку во время этого "погружения в темноту".

Благодарю всех за внимание. Конструктивная критика и предложения, как исправлений, так и новых проектов - приветствуются.

Подробнее..

Маленький и быстрый BERT для русского языка

10.06.2021 02:22:28 | Автор: admin

BERT нейросеть, способная весьма неплохо понимать смысл текстов на человеческом языке. Впервые появивишись в 2018 году, эта модель совершила переворот в компьютерной лингвистике. Базовая версия модели долго предобучается, читая миллионы текстов и постепенно осваивая язык, а потом её можно дообучить на собственной прикладной задаче, например, классификации комментариев или выделении в тексте имён, названий и адресов. Стандартная версия BERT довольно большая: весит больше 600 мегабайт, обрабатывает предложение около 120 миллисекунд (на CPU). В этом посте я предлагаю уменьшенную версию BERT для русского языка 45 мегабайт, 6 мс на предложение. Уже есть tinybert для английского от Хуавея, есть моя уменьшалка FastText'а, а вот маленький (англо-)русский BERT, кажется, появился впервые. Но насколько он хорош?

Дистилляция путь к маленькости

Чтобы создать маленький BERT, я решил обучить свою нейросеть, используя при этом уже готовые модели в качестве учителей. Сейчас объясню подробнее.

Если очень коротко, то BERT работает так: сначала токенизатор разбивает текст на токены (кусочки размером от одной буквы до целого слова), от них берутся эмбеддинги из таблицы, и эти эмбеддинги несколько раз обновляются, используя механизм self-attention для учёта контекста (соседних токенов). При предобучении классический BERT выполняет две задачи: угадывает, какие токены в предложении были заменены на специальный токен [MASK], и шли ли два предложения следом друг за другом в тексте. Как потом показали, вторая задача не очень нужна. Но токен [CLS], который ставится перед началом текста и эмбеддинг которого использовался для этой второй задаче, употреблять продолжают, и я тоже сделал на него ставку.

Дистилляция способ перекладывания знаний из одной модели в другую. Это быстрее, чем учить модель только на текстах. Например, в тексте [CLS] Ехал Грека [MASK] реку "верное" решение поставить на место маски токен через, но большая модель знает, что токены на, в, за в этом контексте тоже уместны, и это знание полезно для обучения маленькой модели. Его можно передать, заставляя маленькую модель не только предсказывать высокую вероятность правильного токена через, а воспроизводить всё вероятностное распределение возможных замаскированных токенов в данном тексте.

В качестве основы для модели я взял классический bert-multilingual (веса), ибо хочу, чтобы модель понимала и русский, и английский, и его же использую на ранних стадиях дистилляции как учителя по распределению токенов. Словарь этой модели содержит 120К токенов, но я отобрал только те, которые часто встречаются в русском и английском языках, оставив 30К. Размер эмбеддинга я сократил с 768 до 312, число слоёв с 12 до 3. Эмбеддинги я инициализировал из bert-multilingual, все остальные веса случайным образом.

Поскольку я собираюсь использовать маленький BERT в первую очередь для классификации коротких текстов, мне надо, чтобы он мог построить хорошее векторное представление предложения. Поэтому в качестве учителей для дистилляции я выбрал модели, которые с этим здорово справляются: RuBERT (статья, веса), LaBSE (статья, веса), Laser (статья, пакет) и USE (статья, код). А именно, я требую, чтобы [CLS] эмбеддинг моей модели позволял предсказать эмбеддинги предложений, полученные из этих трёх моделей. Дополнительно я обучаю модель на задачу translation ranking (как LaBSE). Наконец, я решил, что неплохо было бы уметь полностью расшифровывать предложение назад из CLS-эмбеддингов, причём делать это одинаково для русских и английских предложений как в Laser. Для этих целей я примотал изолентой к своей модели декодер от уменьшенного русского T5. Таким образом, у меня получилась многозадачная модель о восьми лоссах:

  • Обычное предсказание замаскированных токенов (я использую full word masks).

  • Translation ranking по рецепту LaBSE: эмбеддинг фразы на русском должен быть ближе к эмбеддингу её перевода на английский, чем к эмбеддингу остальных примеров в батче. Пробовал добавлять наивные hard negatives, но заметной пользы они не дали.

  • Дистилляция распределения всех токенов из bert-base-multilingual-cased (через несколько эпох я отключил её, т.к. она начала мешать).

  • Приближение CLS-эмбеддингов (после линейной проекции) к эмбеддингам DeepPavlov/rubert-base-cased-sentence (усреднённым по токенам).

  • Приближение CLS-эмбеддингов (после другой линейной проекции) к CLS-эмбеддингам LaBSE.

  • Приближение CLS-эмбеддингов (после третьей проекции) к эмбеддингам LASER.

  • Приближение CLS-эмбеддингов (после ещё одной проекции) к эмбеддингам USE.

  • Расшифровка декодером от T5 предложения (на русском) из последней проекции CLS-эмбеддинга.

Скорее всего, из этих лоссов больше половины можно было безболезненно выкинуть, но ресурсов на ablation study я пока не нашёл. Обучал я это всё в течении нескольких дней на Colab, по пути нащупывая learning rate и другие параметры. В общем, не очень научно, но дешево и результативно. В качестве обучающей выборки я взял три параллельных корпуса англо-русских предложений: от Яндекс.Переводчика, OPUS-100 и Tatoeba, суммарно 2.5 млн коротких текстов. Весь процесс создания модели, включая некоторые неудачные эксперименты, содержится в блокноте. Сама модель, названная мною rubert-tiny (или просто Энкодечка), выложена в репозитории Huggingface.

И как этим пользоваться?

Если у вас есть Python и установлены пакет transformers и sentencepiece, скачать и запустить модель просто. Например, вот так вы можете получить 312-мерный CLS-эмбеддинг предложения.

Этот эмбеддинг вы можете использовать как признак для любой модели классификации или кластеризации текстов. Как (рекомендуемый) вариант, вы можете дообучить мою модель на собственной задаче. Про это будет отдельный пост, а пока отсылаю вас к обучающи материалам от Huggingface (раз, два).

Насколько быстр и мал мой Энкодечка? Я сравил его с другими BERT'ами, понимающими русский язык. Скорость указана в расчёте на одно предложение из Лейпцигского веб-корпуса русского языка.

Модель

Скорость (CPU)

Скорость (GPU)

Вес на диске

cointegrated/rubert-tiny

6 мс

3 мс

45 мб

bert-base-multilingual-cased

125 мс

8 мс

680 мб

DeepPavlov/rubert-base-cased-sentence

110 мс

8 мс

680 мб

sentence-transformers/LaBSE

120 мс

8 мс

1.8 гб

sberbank-ai/sbert_large_nlu_ru

420 мс

16 мс

1.6 гб

Все расчёты я выполнял на Colab (Intel(R) Xeon(R) CPU @ 2.00GHz и Tesla P100-PCIE c батчом размера 1 (если использовать крупные батчи, то ускорение на GPU ещё заметнее, т.к. с маленькой моделью можно собрать более большой батч).

Как видим, rubert-tiny на CPU работает раз в 20 быстрее своих ближайших соседей, и легко помещается на бюджетные хостинги типа Heroku (и даже, наверное, на мобильные устройства). Надеюсь, эта модель сделает предобученные нейросети для русского языка в целом более доступными для прикладных применений. Но надо ещё убедиться, что модель хоть чему-то научилась.

Оценка качества эмбеддингов

Рекомендованный и проверенный временем рецепт использования BERT дообучать его на конечную задачу. Но дообучение процесс небыстрый и наукоёмкий, а гипотезу об осмысленности выученных эмбеддингов хочется проверить побыстрее и попроще. Поэтому я не дообучаю модели, а использую их как готовые feature extractors, и обучаю поверх их эмбеддингов простые модели логистическую регрессию и KNN.

Для оценки русских моделей есть бенчмарк RussianSuperGLUE, но задачи там сложные, требующие подбора параметров и дообучения моделей, так что к нему я тоже перейду позже. Ещё есть бенчмарк RuSentEval, там задачи более простые, но они слишком абстрактные, ориентированные больше на лингвистические свойства, а хочется начать с чего-то прикладного. Поэтому я собрал свой маленький бенчмарк. Пока что он тоже живёт в блокноте, но надо будет допилить его и выложить в более удобном виде. И вот какие задачи туда вошли:

STS: бенчмарк по семантической близости предложений (переведённый с английского). Например, фразы "Кошка спит на фиолетовой простыне" и "Черно-белый кот спит на фиолетовом одеяле" из этого датасета оценены на 4 из 5 баллов сходства. Качество моделей на нём я мерял ранговой корреляций этих баллов с косинусной близостью эмбеддингов предложений. Для наилучшей модели, LaBSE, корреляция оказалась 77%, для моей 65%, на одном уровне с моделью от Сбера, которая в 40 раз больше моей.

Paraphraser: похожий бенчмарк по семантической близости, но с чисто русскими новостными заголовками и трёхбалльной шкалой. Эту задачу я решил так: обучил поверх косинусных близостей логистическую регрессию, и ей предсказывал, к какому из трёх классов пара заголовков относится. На этом бенчмарке моя модель выдала точность 43% и победила все остальные (впрочем, с небольшим отрывом).

XNLI: предсказание, следует ли одно предложение из другого. Например, из "это стреляющее пластиковое автоматическое оружие" не следует "это более надежно, чем металлическое оружие", но и не противоречит. Оценивал её я как предыдущую логрегом поверх косинусных близостей. Тут на первом месте оказалась модель от DeepPavlov (которая дообучалась ровно на этой задаче), а моя заняла второе.

SentiRuEval2016: классификация тональности твитов на три класса. Тут я оценивал модели по точности логистической регрессии, обученной поверх эмбеддингов, и моя модель заняла третье место из пяти. В этой и некоторых последующих задачах я сэмплировал обучающую и тестовые выборки, уменьшая их размер до 5К, скорости ради.

OKMLCup: детекция токсичных комментариев из Одноклассников. Тут моя модель заняла четвёртое место по ROC AUC, обогнав только bert-base-cased-multilingual.

Inappropriateness: детекция сообщений, неприятных для собеседника или вредящих репутации. Тут моя модель оказалась на последнем месте, но таки набрала 68% AUC (у самой лучшей, Сберовской, вышло 79%).

Классификация интентов: накраудсоршенные обращения к голосовому помощнику, покрывающие 18 доменов и 68 интентов. Они собирались на английском языке, но я перевёл их на русский простой моделькой. Часть переводов получились странными, но для бенчмарка сойдёт. Оценивал я по точности логистической регрессии или KNN (что лучше). LaBSE набрала точность 75%, модель от Сбера 68%, от DeepPavlov 60%, моя 58%, мультиязычная 56%. Есть, куда расти.

Перенос классификации интентов с английского: та же классификация интентов, но обучающая выбора на английском языке, а тестовая на русском. Тут моя модель заняла второе место после LaBSE, а качество всех остальных сильно просело (что ожидаемо, ибо на параллельных корпусах они не обучались).

factRuEval-2016: задача распознавания классических именованных сущностей (адреса, организации, личности). Я обучал логистическую регрессию поверх эмбеддингов токенов, а качество мерял макро F1 скором (относительно токенов же, что не вполне корректно). Оказалось, что на таком NER моя модель работает откровенно плохо: она набрала скор 43%, остальные 67-69%.

RuDReC: распознавание медицинских именованных сущностей. Тут моя модель тоже проиграла остальным, но с меньшим отрывом: 58% против 62-67%.

Для всех задач, кроме NER, я пробовал для каждой модели два вида эмбеддингов: из CLS токена и усреднённые по всем токенам. Для меня было неожиданностью, что на многих задачах и моделях эмбеддинг СLS-токена оказывался даже лучше, чем средний (в целом, у него так себе репутация). Поэтому рекомендую не лениться и пробовать оба варианта. Код и результаты моей оценки можно взять из этого блокнота, но лучше подождать, пока я ещё причешу этот бенчмарк.

По итогам оценки оказалось, что модель LaBSE очень крутая: она заняла первое место на 6 из 10 задач. Поэтому я решил выложить LaBSE-en-ru, у которой я отрезал эмбеддинги всех 99 языков, кроме русского и английского. Модель похудела с 1.8 до 0.5 гигабайт, и, надеюсь, таким образом стала чуть более удобной для практического применения. Ну а rubert-tiny оказался по качеству в целом близок к моделям от DeepPavlov и Sber, будучи при этом на порядок меньше и быстрее.

Заключение

Я обещал сделать компактную модель для эмбеддингов русских предложений, и я это наконец сделал. Процесс дистилляции, скорее всего, я настроил неоптимально, и его ещё можно сильно улучшать, но уже сейчас маленькая модель на некоторых задачах приближается к уровню своих учителей и даже иногда обходит его. Так что если вам нужен маленький и быстрый BERT для русского языка, то пользуйтесь: https://huggingface.co/cointegrated/rubert-tiny.

Впереди работы много: с одной стороны, хочется обучить маленький BERT решать задачи из RussianSuperGLUE (и не только), с другой затащить в русский язык хорошие небольшие модели для контролируемой генерации текста (я уже начал делать это для T5). Посему лайкайте данный пост, подписывайтесь на мой канал про NLP, подкидывайте в комментариях и в личке интересные задачи, и, если у вас доведутся руки попробовать rubert-tiny, то обязательно оставляйте обратную связь!
Мне и самому интересно, что будет дальше.

Подробнее..

3D ML. Часть 4 дифференциальный рендеринг

23.09.2020 16:18:12 | Автор: admin


В нескольких предыдущих заметках данной серии мы уже упоминали понятие дифференциального рендеринга. Сегодня пришло время разъяснить что это такое и с чем это едят.


Мы поговорим о том, почему традиционный пайплайн рендеринга не дифференцируем, зачем исследователям в области 3D ML потребовалось сделать его дифференцируемым и как это связано с нейронным рендерингом. Какие существуют подходы к конструированию таких систем, и рассмотрим конкретный пример SoftRasterizer и его реализацию в PyTorch 3D. В конце, с помощью этой технологии, восстановим все пространственные характеристики Моны Лизы Леонардо Да Винчи так, если бы картина была не написана рукой мастера, а отрендерена с помощью компьютерной графики.


Серия 3D ML на Хабре:


  1. Формы представления 3D данных
  2. Функции потерь в 3D ML
  3. Датасеты и фреймворки в 3D ML
  4. Дифференциальный рендеринг

Репозиторий на GitHub для данной серии заметок.


Заметка от партнера IT-центра МАИ и организатора магистерской программы VR/AR & AI компании PHYGITALISM.


Rendering pipeline: forward and inverse



Если мы рассматриваем все возможные задачи, в которых требуется как-то взаимодействовать с 3D моделями, то глобально появляется возможность разделить их на два класса задач:


  • Задачи, в которых из 3D сцены мы хотим сгенерировать изображение (такие задачи можно отнести к традиционным задачам компьютерной графике) т.н. forward rendering;
  • Задачи, где по изображению нам требуется восстанавливать параметры 3D объектов (такие задачи относятся скорее к компьютерному зрению) т.н. inverse rendering.

До недавнего времени два этих класса задач обычно рассматривались отдельно друг от друга, но сегодня все чаще приходится иметь дело с алгоритмами, которые должны работать в обе стороны (особенно это касается области машинного обучения).



Рис.1 Из презентации TensorFlow Graphics (github page).


В качестве примера такой задачи, можно рассмотреть задачу 3D mesh reconstruction from single image, которую мы уже упоминали в предыдущих заметках. С одной стороны, эту задачу можно решать сравнивая ошибку рассогласования между исходной моделью и предсказанной с помощью функций потерь для 3D объектов (заметка 2 данной серии). С другой стороны, можно генерировать 3D объект сначала, а после его отрендеренную картинку сравнивать с изображением-образцом (пример на рис.2).



Рис.2 Модель деформации меша с помощью модуля дифференциального рендеринга SoftRas (github page).


Далее, при разговоре про рендеринг, мы будем рассматривать несколько основных компонентов 3D сцены:


  • 3D объект, описываемый своим мешем;
  • камера с набором характеристик (позиция, направление, раствор и т.д.);
  • источники света и их характеристики;
  • глобальные характеристики расположения объекта на сцене, описываемые матрицами преобразований.

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


Давайте поговорим сначала о том, из каких этапов состоит традиционный пайплайн прямого рендеринга и почему он не является дифференцируемым.


Why is rendering not differentiable?



Рис.3 Схема традиционного рендеринга и рендеринга методом Soft Rasterizer [1]. Здесь: $M$ меш объекта на сцене, $P$ модель камеры, $L$ модель источника освещения, $A$ модель текстуры, $N$ карта нормалей для меша, $Z$ карта глубины получаемого изображения, $U$ матрица преобразования 3D в 2D для получения плоского изображения, $F$ растеризованное изображение, $D$ вероятностные карты метода Soft Rasterizer, $I,\bar I$ изображения полученные традиционным рендерингом и методом SoftRas соответственно. Красные блоки недифференцируемые операции, синии дифференцируемые.


Процедуру рендеринга можно подразделить на несколько взаимозависимых этапов (этапы традиционного рендеринга приведены на рис.3 врехняя линия в правой части). Какие-то этапы, к примеру вычисления освещения и позиции камеры, являются дифференцируемыми, поскольку в них участвуют непрерывные функциональные зависимости (см. модель Фонга, на Хабре о ней и моделях освещения писали здесь и здесь), но два последних этапа не являются дифференцируемыми. Давайте разберемся почему.



Последние два этапа, которые являются по сути и ключевыми это растеризация и шейдинг. (Про реализацию этих этапов на JavaScript на Хабре писали здесь).


Грубо говоря, проблему недифференцируемости растеризации можно описать так: пиксели дискретные структуры, с постоянным цветом, а исходная модель непрерывна, поэтому при проецировании из 3D в 2D часть информации теряется.


Проиллюстрируем две основные проблемы с дифференцируемостью при вычислении цвета и расстояния.


Проблема 1 (недифференцируемость цвета по глубине)



Предположим, что вдоль направления луча, проходящего через пиксель, есть несколько полигонов разных цветов, как на иллюстрации выше. Если придать малый сдвиг полигонов друг относительно друга, может случиться ситуация, когда ближайшим полигоном становится полигон другого цвета и при этом резко меняется цвет, в который нужно разукрашивать соответствующий пиксель. На правом графике иллюстрации изображена зависимость цвета пикселя (в барицентрических координатах) от расстояния до ближайшего пикселя конкретной модели. Из данного примера видно, что малому приращению расстояния до ближайшего полигона может соответствовать скачкообразному изменению в цвете, что приводит к недифференцируемости в классическом смысле.


Проблема 2 (недифференцируемость цвета при сдвигах)



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


Make it differentiable! Soft Rasterizer


Процесс рендеринга недифференцируемый, но если бы он таковым являлся, можно было бы решать много актуальных задач в области 3D ML мы определили проблему, посмотрим как ее можно решить.


Основные подходы к реализации дифференцируемого рендеринга можно проследить в следующей подборке публикаций:



Подходы основаны на разных идеях и приемах. Мы подробно остановимся только на одном, Soft Rasterizer, по двум причинам: во-первых, идея данного подхода математически прозрачна и легко реализуема самостоятельно, во-вторых, данный подход реализован и оптимизирован внутри библиотеки PyTorch 3D [6].


Подробно со всеми аспектами реализации дифференциального рендеринга этим методом можно ознакомиться в соответствующей статье [1], мы же отметим основные моменты.


Для решение проблемы 2, авторы метода предлагают использовать размытие границы полигонов, которое приводит к непрерывной зависимости цвета пикселя от координат смещения полигона.

Размытие границ предполагает введение некоторой гладкой вероятностной функции $D_j^i$, которая каждой внутренней или внешней точки пространства $p_i$ ставит в соответствие число от 0 до 1 вероятности принадлежности к данному полигону $f_j$ (чем-то похоже на подход нечеткой логики). Здесь $\sigma$ параметр размытия (чем больше $\sigma$, тем больше размытие), $d(i,j)$ кратчайшее расстояние в проекционной плоскости от проекции точки $p_i$ до границы проекции полигона $f_j$ (данное расстояние обычно выбирают Евклидовым, но авторы метода отмечают, что здесь есть простор для экспериментов и, например, использование барицентрического расстояния или $l_1$ также подходит для их метода), $\delta_j^i$ функция, которая равна 1 если точка находится внутри полигона и -1 если вне (на границе полигона можно доопределить значение $\delta$ нулем, однако это все равно приводит к тому, что на границе полигона данная функция разрывна, поэтому для точек границ она не применяется), $sigmoid$ сигмоидная функция активации, которая часто применяется в глубоком обучении.


Для решения проблемы 1, авторы метода предлагают использовать смешение цветов k ближайших полигонов (blending).

Коротко этот прием можно описать следующим образом: для вычисления итогового цвета $i$-го пикселя $(I^i)$, производят нормированное суммирование цветовых карт $C^i_j$ для k ближайших полигонов $(j =1,..,k)$, причем цветовые карты получают путем интерполяции барицентрических координат цвета вершин данных полигонов. Индекс $b$ в формуле отвечает за фоновый цвет (background colour), а оператор $\mathcal{A}_{S}$ оператор агрегирование цвета. $z^i_j$ глубина $i$-го пикселя относительно $j$-го полигона, а $\gamma$ параметр смешивания (чем он меньше, тем сильнее превалирует цвет ближайшего полигона).


Итоговой подход Soft Rasterizer, заключается в комбинировании этих двух идей, для одновременного плавного размытия границы и цвета.

Рис.4 Схема реализации дифференциального рендеринга в PyTorch 3D (слайд из презентации фреймворка).


Реализация Soft Rasterizer внутри библиотеки PyTorch 3D выполнена так, чтобы максимально эффективно и удобно использовать возможности как базового фреймворка PyTorch, так и возможности технологии CUDA. По сравнению с оригинальной реализацией [github page], разработчикам фреймворка удалось добиться 4-х кратного приращения скорости обработки (особенно для больших моделей), при этом возрастает расход памяти за счет того, что для каждого типа данных (ката глубины, карта нормалей, рендер текстур, карта евклидовых расстояний) нужно просчитать k слоев и хранить их в памяти.



Рис.5 Сравнение характеристик дифференциального рендеринга в PyTorch 3D (слайд из презентации фреймворка).


Поэкспериментировать с настройками дифференциального рендера можно как в PyTorch 3D, так в библиотеке с оригинальной реализацией алгоритма Soft Rasterizer. Давайте рассмотрим пример, демонстрирующий зависимость итоговой картинки отрендеренной модели от параметров дифференциального рендера \sigma, \gamma.


Удобнее всего работать с этой библиотекой в виртуальном окружении anaconda, так как данная библиотека работает уже не с самой актуальной версией pytorch 1.1.0. Также обратите внимание что вам потребуется видеокарта с поддержкой CUDA.


Импорт библиотек и задание путей до обрабатываемых моделей
import matplotlib.pyplot as pltimport osimport tqdmimport numpy as npimport imageioimport soft_renderer as srinput_file = 'path/to/input/file'output_dir = 'path/to/output/dir'

Зададим начальные параметры камеры для рендеринга, загрузим меш объекта с текстурами (есть мод для работы без текстур, в этом случае нужно указать texture_type=vertex), инициализируем дифференциальный рендер и создадим директорию для сохранения результатов.


# camera settingscamera_distance = 2.732elevation = 30azimuth = 0# load from Wavefront .obj filemesh = sr.Mesh.from_obj(                         input_file,                          load_texture=True,                          texture_res=5,                          texture_type='surface')# create renderer with SoftRasrenderer = sr.SoftRenderer(camera_mode='look_at')os.makedirs(args.output_dir, exist_ok=True)

Сначала, посмотрим на нашу модель с разных сторон и для этого отрендерим анимацию пролета камеры по кругу с помощью рендера.


# draw object from different viewloop = tqdm.tqdm(list(range(0, 360, 4)))writer = imageio.get_writer(                            os.path.join(output_dir, 'rotation.gif'),                              mode='I')for num, azimuth in enumerate(loop):    # rest mesh to initial state    mesh.reset_()    loop.set_description('Drawing rotation')    renderer.transform.set_eyes_from_angles(                                            camera_distance,                                             elevation,                                             azimuth)    images = renderer.render_mesh(mesh)    image = images.detach().cpu().numpy()[0].transpose((1, 2, 0))    writer.append_data((255*image).astype(np.uint8))writer.close()

Теперь поиграемся со степенью размытия границы и степенью смешения цветов. Для этого будем в цикле увеличивать параметр размытия $\sigma$ и одновременно увеличивать параметр смешения цвета $\gamma$.


# draw object from different sigma and gammaloop = tqdm.tqdm(list(np.arange(-4, -2, 0.2)))renderer.transform.set_eyes_from_angles(camera_distance, elevation, 45)writer = imageio.get_writer(                            os.path.join(output_dir, 'bluring.gif'),                             mode='I')for num, gamma_pow in enumerate(loop):    # rest mesh to initial state    mesh.reset_()    renderer.set_gamma(10**gamma_pow)    renderer.set_sigma(10**(gamma_pow - 1))    loop.set_description('Drawing blurring')    images = renderer.render_mesh(mesh)    image = images.detach().cpu().numpy()[0].transpose((1, 2, 0))    writer.append_data((255*image).astype(np.uint8))writer.close()# save to textured objmesh.reset_()mesh.save_obj(              os.path.join(args.output_dir, 'saved_spot.obj'),               save_texture=True)

Итоговый результат на примере стандартной модели текстурированной коровы (cow.obj, cow.mtl, cow.png удобно скачивать, например, с помощью wget) выглядит так:



Neural rendering



Дифференциальный рендеринг как базовый инструмент для 3D ML, позволяет создавать очень много интересных архитектур глубокого обучения в области, которая получила названия нейронный рендеринг (neural rendering). Нейронный рендеринг позволяет решать множество задач, связанных с процедурой рендеринга: от добавления новых объектов на фото и в видеопоток до сверхбыстрого текстурирования и рендеринга сложных физических процессов.


Сегодня мы оставим приложение дифференциального рендеринга к конструированию нейронного рендеринга за скобками повествования, однако порекомендуем всем заинтересовавшимся следующие источники:


  • большая обзорная статья SOTA архитектур в области нейронного рендеринга [7] на основе прошедшей CVPR 2020;
  • видео с записью утренней и дневной сессией по нейтронному рендерингу с CVPR 2020, на основе которых и была написана статья из предыдущего пункта;
  • видеолекция MIT DL Neural rendering с кратким обзором основных подходов и введении в тему;
  • заметка на Medium на тему дифференциального рендеринга и его приложений;
  • видео с youtube канала two minute papers на данную тему.

Experiment: Mona Liza reconstruction


Разберем пример применения дифференциального рендеринга для восстановления параметров 3D сцены по исходному изображению человеческого лица, представленный в пуле примеров библиотеки redner, которая является реализацией идей, изложенных в статье [ 4 ].


В данном примере, мы будем использовать т.н. 3D morphable model [8] технику текстурированного трехмерного моделирования человеческого лица, ставшую уже классической в области анализа 3D. Техника основана на получение такого крытого представления признаков 3D данных, которое позволяет строить линейные комбинации, сочетающие физиологические особенности человеческих лиц (если так можно выразиться, то это своеобразный Word2Vec от мира 3D моделирования человеческих лиц).


Для работы с примером вам потребуется датасет Basel face model (2017 version). Файл model2017-1_bfm_nomouth.h5 необходимо будет разместить в рабочей директории вместе с кодом.


Для начала загрузим необходимы для работы библиотеки и датасет лиц.


Загрузка библиотек
import torchimport pyrednerimport h5pyimport urllibimport timefrom matplotlib.pyplot import imshow%matplotlib inlineimport matplotlib.pyplot as pltfrom IPython.display import display, clear_outputfrom matplotlib import animationfrom IPython.display import HTML

# Load the Basel face modelwith h5py.File(r'model2017-1_bfm_nomouth.h5', 'r') as hf:    shape_mean = torch.tensor(hf['shape/model/mean'],                               device = pyredner.get_device())    shape_basis = torch.tensor(hf['shape/model/pcaBasis'],                                device = pyredner.get_device())    triangle_list = torch.tensor(hf['shape/representer/cells'],                                  device = pyredner.get_device())    color_mean = torch.tensor(hf['color/model/mean'],                               device = pyredner.get_device())    color_basis = torch.tensor(hf['color/model/pcaBasis'],                                device = pyredner.get_device())

Модель лица в таком подходе разделена отдельно на базисный вектор формы shape_basis (вектор длины 199 полученный методом PCA), базисный вектор цвета color_basis (вектор длины 199 полученный методом PCA), также имеем усредненный вектор формы и цвета shape_mean, color_mean. В triangle_list хранится геометрия усредненного лица в форме полигональной модели.


Создадим модель, которая на вход будет принимать векторы скрытого представления цвета и формы лица, параметры камеры и освещения в сцене, а на выходе будет генерировать отрендеренное изображение.


indices = triangle_list.permute(1, 0).contiguous()def model(        cam_pos,         cam_look_at,         shape_coeffs,         color_coeffs,         ambient_color,         dir_light_intensity):    vertices = (shape_mean + shape_basis @ shape_coeffs).view(-1, 3)    normals = pyredner.compute_vertex_normal(vertices, indices)    colors = (color_mean + color_basis @ color_coeffs).view(-1, 3)    m = pyredner.Material(use_vertex_color = True)    obj = pyredner.Object(vertices = vertices,                           indices = indices,                           normals = normals,                           material = m,                           colors = colors)    cam = pyredner.Camera(position = cam_pos,                          # Center of the vertices                                                    look_at = cam_look_at,                          up = torch.tensor([0.0, 1.0, 0.0]),                          fov = torch.tensor([45.0]),                          resolution = (256, 256))    scene = pyredner.Scene(camera = cam, objects = [obj])    ambient_light = pyredner.AmbientLight(ambient_color)    dir_light = pyredner.DirectionalLight(torch.tensor([0.0, 0.0, -1.0]),                                           dir_light_intensity)    img = pyredner.render_deferred(scene = scene,                                    lights = [ambient_light, dir_light])    return img

Теперь посмотрим как выглядит усредненное лицо. Для этого зададим первоначальные параметры освещения и позиции камеры и воспользуемся нашей моделью. Также загрузим целевое изображение, параметры которого мы хотим восстановить и взглянем на него:


cam_pos = torch.tensor([-0.2697, -5.7891, 373.9277])cam_look_at = torch.tensor([-0.2697, -5.7891, 54.7918])img = model(cam_pos,             cam_look_at,             torch.zeros(199, device = pyredner.get_device()),            torch.zeros(199, device = pyredner.get_device()),            torch.ones(3),             torch.zeros(3))imshow(torch.pow(img, 1.0/2.2).cpu())face_url = 'https://raw.githubusercontent.com/BachiLi/redner/master/tutorials/mona-lisa-cropped-256.png'urllib.request.urlretrieve(face_url, 'target.png')target = pyredner.imread('target.png').to(pyredner.get_device())imshow(torch.pow(target, 1.0/2.2).cpu())


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


# Set requires_grad=True since we want to optimize them latercam_pos = torch.tensor([-0.2697, -5.7891, 373.9277],                        requires_grad=True)cam_look_at = torch.tensor([-0.2697, -5.7891, 54.7918],                            requires_grad=True)shape_coeffs = torch.zeros(199, device = pyredner.get_device(),                            requires_grad=True)color_coeffs = torch.zeros(199, device = pyredner.get_device(),                            requires_grad=True)ambient_color = torch.ones(3, device = pyredner.get_device(),                            requires_grad=True)dir_light_intensity = torch.zeros(3, device = pyredner.get_device(),                                   requires_grad=True)# Use two different optimizers for different learning ratesoptimizer = torch.optim.Adam(                             [                              shape_coeffs,                               color_coeffs,                               ambient_color,                               dir_light_intensity],                              lr=0.1)cam_optimizer = torch.optim.Adam([cam_pos, cam_look_at], lr=0.5)

Остается организовать, оптимизационный цикл и логировать происходящее с функцией ошибки (в нашем случае это попиксельный MSE + квадратичные регуляризаторы параметров) и с самой 3D моделью.


plt.figure()imgs, losses = [], []# Run 500 Adam iterationsnum_iters = 500for t in range(num_iters):    optimizer.zero_grad()    cam_optimizer.zero_grad()    img = model(cam_pos, cam_look_at, shape_coeffs,                 color_coeffs, ambient_color, dir_light_intensity)    # Compute the loss function. Here it is L2 plus a regularization     # term to avoid coefficients to be too far from zero.    # Both img and target are in linear color space,     # so no gamma correction is needed.    loss = (img - target).pow(2).mean()    loss = loss          + 0.0001 * shape_coeffs.pow(2).mean()          + 0.001 * color_coeffs.pow(2).mean()    loss.backward()    optimizer.step()    cam_optimizer.step()    ambient_color.data.clamp_(0.0)    dir_light_intensity.data.clamp_(0.0)    # Plot the loss    f, (ax_loss, ax_diff_img, ax_img) = plt.subplots(1, 3)    losses.append(loss.data.item())    # Only store images every 10th iterations    if t % 10 == 0:        # Record the Gamma corrected image        imgs.append(torch.pow(img.data, 1.0/2.2).cpu())     clear_output(wait=True)    ax_loss.plot(range(len(losses)), losses, label='loss')    ax_loss.legend()    ax_diff_img.imshow((img -target).pow(2).sum(dim=2).data.cpu())    ax_img.imshow(torch.pow(img.data.cpu(), 1.0/2.2))    plt.show()


Чтобы лучше понимать что происходило со сценой в процессе обучения можем сгенерировать анимацию из наших логов:


fig = plt.figure()# Clamp to avoid complainsim = plt.imshow(imgs[0].clamp(0.0, 1.0), animated=True)def update_fig(i):    im.set_array(imgs[i].clamp(0.0, 1.0))    return im,anim = animation.FuncAnimation(fig, update_fig,                                frames=len(imgs), interval=50, blit=True)HTML(anim.to_jshtml())


Conclusions


Дифференциальный рендеринг новое интересное и важное направление на стыке компьютерной графики, компьютерного зрения и машинного обучения. Данная технология стала основой для многих архитектур в области нейронного рендеринга, который в свою очередь расширяет границы возможностей компьютерной графики и машинного зрения.


Существуют несколько популярных библиотек глубокого вычисления (например Kaolin, PyTorch 3D, TensorFlow Graphics), которые содержат дифференциальный рендеринг как составную часть. Также существуют отдельные библиотеки, реализующие функционал дифференциального рендеринга (Soft Rasterizer, redner). С их помощью можно реализовывать множество интересных проектов, вроде проекта с восстановлением параметров лица и текстуры портрета человека.


В ближайшем будущем, мы можем ожидать появление новых техник и библиотек для дифференциального рендеринга и их применения в области нейронного рендеринга. Возможно, уже завтра может появится способ делать реалистичную графику в реальном времени или генерировать 2D и 3D контент приемлемого для людей качества с помощью этой технологии. Мы будем следить за развитием этого направления и постараемся рассказывать о всех новинках и интересных экспериментах.


References
  1. Liu, S., Li, T., Chen, W. and Li, H., 2019. Soft rasterizer: A differentiable renderer for image-based 3d reasoning. In Proceedings of the IEEE International Conference on Computer Vision (pp. 7708-7717). [ paper ]
  2. Loper, M.M. and Black, M.J., 2014, September. OpenDR: An approximate differentiable renderer. In European Conference on Computer Vision (pp. 154-169). Springer, Cham. [ paper ]
  3. Kato, H., Ushiku, Y. and Harada, T., 2018. Neural 3d mesh renderer. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (pp. 3907-3916). [ paper ]
  4. Li, T.M., Aittala, M., Durand, F. and Lehtinen, J., 2018. Differentiable monte carlo ray tracing through edge sampling. ACM Transactions on Graphics (TOG), 37(6), pp.1-11. [ paper ]
  5. Chen, W., Ling, H., Gao, J., Smith, E., Lehtinen, J., Jacobson, A. and Fidler, S., 2019. Learning to predict 3d objects with an interpolation-based differentiable renderer. In Advances in Neural Information Processing Systems (pp. 9609-9619). [ paper ]
  6. Ravi, N., Reizenstein, J., Novotny, D., Gordon, T., Lo, W.Y., Johnson, J. and Gkioxari, G., 2020. Accelerating 3D Deep Learning with PyTorch3D. arXiv preprint arXiv:2007.08501. [ paper ] [ github ]
  7. Tewari, A., Fried, O., Thies, J., Sitzmann, V., Lombardi, S., Sunkavalli, K., Martin-Brualla, R., Simon, T., Saragih, J., Niener, M. and Pandey, R., 2020. State of the Art on Neural Rendering. arXiv preprint arXiv:2004.03805. [ paper ]
  8. Blanz, V. and Vetter, T., 1999, July. A morphable model for the synthesis of 3D faces. In Proceedings of the 26th annual conference on Computer graphics and interactive techniques (pp. 187-194). [ paper ][ project page ]

Подробнее..

Распознаем номера автомобилей. Разработка multihead-модели в Catalyst

11.06.2021 08:06:47 | Автор: admin

Фиксация различных нарушений, контроль доступа, розыск и отслеживание автомобилей лишь часть задач, для которых требуется по фотографии определить номер автомобиля (государственный регистрационный знак или ГРЗ).

В этой статье мы рассмотрим создание модели для распознавания с помощью Catalyst одного из самых популярных высокоуровневых фреймворков для Pytorch. Он позволяет избавиться от большого количества повторяющегося из проекта в проект кода цикла обучения, расчёта метрик, создания чек-поинтов моделей и другого и сосредоточиться непосредственно на эксперименте.

Сделать модель для распознавания можно с помощью разных подходов, например, путем поиска и определения отдельных символов, или в виде задачи image-to-text. Мы рассмотрим модель с несколькими выходами (multihead-модель). В качестве датасета возьмём датасет с российскими номерами от проекта Nomeroff Net. Примеры изображений из датасета представлены на рис. 1.

Рис. 1. Примеры изображений из датасета

Общий подход к решению задачи

Необходимо разработать модель, которая на входе будет принимать изображение ГРЗ, а на выходе отдавать строку распознанных символов. Модель будет состоять из экстрактора фичей и нескольких классификационных голов. В датасете представлены ГРЗ из 8 и 9 символов, поэтому голов будет девять. Каждая голова будет предсказывать один символ из алфавита 1234567890ABEKMHOPCTYX, плюс специальный символ - (дефис) для обозначения отсутствия девятого символа в восьмизначных ГРЗ. Архитектура схематично представлена на рис. 2.

Рис. 2. Архитектура модели

В качестве loss-функции возьмём стандартную кросс-энтропию. Будем применять её к каждой голове в отдельности, а затем просуммируем полученные значения для получения общего лосса модели. Оптимизатор Adam. Используем также OneCycleLRWithWarmup как планировщик leraning rate. Размер батча 128. Длительность обучения установим в 10 эпох.

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

Кодирование

Далее рассмотрим основные моменты кода. Класс датасета (листинг 1) в общем обычный для CV-задач на Pytorch. Обратить внимание стоит лишь на то, как мы возвращаем список кодов символов в качестве таргета. В параметре label_encoder передаётся служебный класс, который умеет преобразовывать символы алфавита в их коды и обратно.

class NpOcrDataset(Dataset):   def __init__(self, data_path, transform, label_encoder):       super().__init__()       self.data_path = data_path       self.image_fnames = glob.glob(os.path.join(data_path, "img", "*.png"))       self.transform = transform       self.label_encoder = label_encoder    def __len__(self):       return len(self.image_fnames)    def __getitem__(self, idx):       img_fname = self.image_fnames[idx]       img = cv2.imread(img_fname)       if self.transform:           transformed = self.transform(image=img)           img = transformed["image"]       img = img.transpose(2, 0, 1)             label_fname = os.path.join(self.data_path, "ann",                                  os.path.basename(img_fname).replace(".png", ".json"))       with open(label_fname, "rt") as label_file:           label_struct = json.load(label_file)           label = label_struct["description"]       label = self.label_encoder.encode(label)        return img, [c for c in label]

Листинг 1. Класс датасета

В классе модели (листинг 2) мы используем библиотеку PyTorch Image Models для создания экстрактора фичей. Каждую из классификационных голов модели мы добавляем в ModuleList, чтобы их параметры были доступны оптимизатору. Логиты с выхода каждой из голов возвращаются списком.

class MultiheadClassifier(nn.Module):   def __init__(self, backbone_name, backbone_pretrained, input_size, num_heads, num_classes):       super().__init__()        self.backbone = timm.create_model(backbone_name, backbone_pretrained, num_classes=0)       backbone_out_features_num = self.backbone(torch.randn(1, 3, input_size[1], input_size[0])).size(1)        self.heads = nn.ModuleList([           nn.Linear(backbone_out_features_num, num_classes) for _ in range(num_heads)       ])     def forward(self, x):       features = self.backbone(x)       logits = [head(features) for head in self.heads]       return logits

Листинг 2. Класс модели

Центральным звеном, связывающим все компоненты и обеспечивающим обучение модели, является Runner. Он представляет абстракцию над циклом обучения-валидации модели и отдельными его компонентами. В случае обучения multihead-модели нас будет интересовать реализация метода handle_batch и набор колбэков.

Метод handle_batch, как следует из названия, отвечает за обработку батча данных. Мы в нём будем только вызывать модель с данными батча, а обработку полученных результатов расчёт лосса, метрик и т.д. мы реализуем с помощью колбэков. Код метода представлен в листинге 3.

class MultiheadClassificationRunner(dl.Runner):   def __init__(self, num_heads, *args, **kwargs):       super().__init__(*args, **kwargs)       self.num_heads = num_heads    def handle_batch(self, batch):       x, targets = batch       logits = self.model(x)             batch_dict = { "features": x }       for i in range(self.num_heads):           batch_dict[f"targets{i}"] = targets[i]       for i in range(self.num_heads):           batch_dict[f"logits{i}"] = logits[i]             self.batch = batch_dict

Листинг 3. Реализация runnerа

Колбэки мы будем использовать следующие:

  • CriterionCallback для расчёта лосса. Нам потребуется по отдельному экземпляру для каждой из голов модели.

  • MetricAggregationCallback для агрегации лоссов отдельных голов в единый лосс модели.

  • OptimizerCallback чтобы запускать оптимизатор и обновлять веса модели.

  • SchedulerCallback для запуска LR Schedulerа.

  • AccuracyCallback чтобы иметь представление о точности классификации каждой из голов в ходе обучения модели.

  • CheckpointCallback чтобы сохранять лучшие веса модели.

Код, формирующий список колбэков, представлен в листинге 4.

def get_runner_callbacks(num_heads, num_classes_per_head, class_names, logdir):   cbs = [       *[           dl.CriterionCallback(               metric_key=f"loss{i}",               input_key=f"logits{i}",               target_key=f"targets{i}"           )           for i in range(num_heads)       ],       dl.MetricAggregationCallback(           metric_key="loss",           metrics=[f"loss{i}" for i in range(num_heads)],           mode="mean"       ),       dl.OptimizerCallback(metric_key="loss"),       dl.SchedulerCallback(),       *[           dl.AccuracyCallback(               input_key=f"logits{i}",               target_key=f"targets{i}",               num_classes=num_classes_per_head,               suffix=f"{i}"           )           for i in range(num_heads)       ],       dl.CheckpointCallback(           logdir=os.path.join(logdir, "checkpoints"),           loader_key="valid",           metric_key="loss",           minimize=True,           save_n_best=1       )   ]     return cbs

Листинг 4. Код получения колбэков

Остальные части кода являются тривиальными для Pytorch и Catalyst, поэтому мы не станем приводить их здесь. Полный код к статье доступен на GitHub.

Результаты эксперимента

Рис. 3. График лосс-функции модели в процессе обучения. Оранжевая линия train loss, синяя valid loss

В списке ниже перечислены некоторые ошибки, которые модель допустила на тест-сете:

  • Incorrect prediction: T970XT23- instead of T970XO123

  • Incorrect prediction: X399KT161 instead of X359KT163

  • Incorrect prediction: E166EP133 instead of E166EP123

  • Incorrect prediction: X225YY96- instead of X222BY96-

  • Incorrect prediction: X125KX11- instead of X125KX14-

  • Incorrect prediction: X365PC17- instead of X365PC178

Здесь присутствуют все возможные типы: некорректно распознанные буквы и цифры основной части ГРЗ, некорректно распознанные цифры кода региона, лишняя цифра в коде региона, а также неверно предсказанное отсутствие последней цифры.

Заключение

В статье мы рассмотрели способ реализации multihead-модели для распознавания ГРЗ автомобилей с помощью фреймворка Catalyst. Основными компонентами явились собственно модель, а также раннер и набор колбэков для него. Модель успешно обучилась и показала высокую точность на тестовой выборке.

Спасибо за внимание! Надеемся, что наш опыт был вам полезен.

Больше наших статей по машинному обучению и обработке изображений:

Подробнее..

Как запихать нейронку в кофеварку

27.10.2020 10:11:01 | Автор: admin
Мир машинного обучения продолжает стремительно развиваться. Всего за год технология может стать мейнстримом, и разительно измениться, придя в повседневность.
За прошедший год-полтора, одной из таких технологий, стали фреймворки выполнения моделей машинного обучения. Не то, что их не было. Но, за этот год, те которые были стали сильно проще, удобнее, мощнее.

В статье я попробую осветить всё что повылезало за последнее время. Чтобы вы, решив использовать нейронную сеть в очередном калькуляторе, знали куда смотреть.

Перед началом статьи нужно сразу сказать несколько дисклеймеров:
  • Мой обзор будет со стороны ComputerVision. Не стоит забывать что ML это не только CV. Это ещё классические алгоритмы бустинга, это различный NLP (от трансформеров до синтеза речи). И далеко не все задачи ML можно будет исполнять на тех фреймворках про которые я буду говорить.
  • Из обозначенных технологий я сам работал где-то с третью. Про остальные читал/минимально щупал/общался с людьми которые интегрировали. Отсюда могут возникнуть какие-то ошибки в статье. Если вы видите что-то что вас покоробило/с чем вы не согласны пишите в комментариях/в личку попробую поправить.
  • Мир стремительно меняется. Я пробую верифицировать то что пишу на момент выхода статьи. Но не факт что даже существующая документация соответствует истинному положению дел. Не говоря уже о том, что в ближайшие пару недель любой из указанных фреймворков может катастрофически измениться. Если вдруг такие апдейты вам будут интересны то скорее всего я буду разбирать их в своём блоге CVML (он же в телеге), где я такую мелочь пишу. Тут буду стараться оставлять ссылки на разбор.

Для тех кому лень читать записал видео с содержимым статьи:

Начнём.

Часть 1. Что такое инференс


Мир нейронных сетей можно разбить на две части:
  • обучение
  • использование

Обучение сложнее. Оно требует больше математики, возможность анализировать какие-то параметры по ходу. Нормой является подключение к TensorBoard и прочим инструментам мониторинга. Нужны простые способы интеграции новых слоёв, быстрота модификации.
Чаще всего для обучения используются Nvidia GPU (да, есть TPU от Google, или Intel Xe, но скорее это редкость). Так что софт для обучения должен хорошо поддерживать одну платформу.
Нужно ли обучение при использовании нейронной сети в проде? Очень редко. Да, у нас было несколько проектов с автоматическим дообучением. Но лучше этого избегать. Это сложно и нестабильно.
Да и если нужно, то проще утащить на внешний сервак и там дообучить.
image
Как следствие можно отбросить 90% математики и обвеса, использовать только выполнение. Это и называется инференс. Он быстрее, чем обучение. Требует сильно меньше математики.
Но вот засада. Не тащить же для инференса GPU. Инференс может быть и на десктопах, и на мобильниках, и на серверах, и в браузере.
В чём его сложность?
Нейронные сети едят много производительности нужно либо специальное железо и его поддержка, либо максимальная утилизация существующего, чтобы хоть как-то достать производительность.
А железо очень-очень разное. Например в телефонах Android может существовать с десяток различных вычислителей, каждый из которых имеет свою архитектуру. А значит ваш модуль должен быть универсален для большинства.

Часть 2. Железо.


Железо в ML бывает очень разное. Его хотя бы примерное описание на текущий момент будет требовать десятка статей. Могу вам посоветовать очень крутую статью от 3Dvideo про технологии аппаратного ускорения нейронных сетей. И свою статью про то как устроены embedding системы в последнее время.
И то и то уже немного неактуально, ведь всё быстро-быстро меняется;)

Для упрощения понимания статьи, или для тех кто не хочет углубляться я накидал упрощённую схему которой мы будем оперировать (кликабельно):

Тут мы видим несколько направлений инференса:
  • Серверный инференс. Мы не полезем в него глубоко. Обычно когда вам нужно чтобы сеть выполнялась на сверхпроизводительном сервере редко нужно чтобы данный алгоритм крутился на старой мобилке. Но, тем не менее, тут будет что помянуть
  • Десктопный инференс. Это то, что может крутиться у вас на домашнем компе. Обработка фотографий. Анализ видео, компы которые ставятся на предприятия, и прочее и прочее будет именно здесь.
  • Мобильный инференс всё что касается телефонов, Android и Ios. Тут огромное поле. Есть GPU, есть специальные ускорители, есть сопроцессоры, и прочее и прочее.
  • Embedded. Частично эта часть пересекается с десктопами, а частично с мобильными решениями. Но я вынес её в отдельную ветку, так как часть ускорителей ни на что не похожи.

Основная проблема видна уже тут. Большинство конечных точек требуют свою систему инференса. Если вы хотите универсальную систему выполнения нейронной сети которая будет использовать 100% мощности, тогда вам нужно иметь модуль для каждой конечной точки этого дерева.
Есть фреймворки которые теряют в разветвленности, но неплохо работают там где работают. Есть которые используют максимально популярные устройства, но не работают на других. Опять же, если показать это ооочень примерно, то выйдет как-то так:

Поговорим подробнее, скорее слева на право.

Специализированное железо: Gyrfalcon, Khadas, Hikvision, и.т.д.


Всё это добро я решил выделить в одну группу. Фреймворком это назвать сложно. Обычно это специализированный софт который может перенести нейронку на железо, чтобы она выполнялась там оптимально. При этом железяка у производителей зачастую одна или две.
Отличительной особенностью таких платформ является:
  • Не все слои поддерживаются
  • Софт часто не выложен в OpenSource. Поставляется вместе с аппаратной частью или требует подписания дополнительного NDA

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

Специализированное железо но от крупных фирм


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

Nvidia TensorRT


Наверное это самая классика, и не надо рассказывать почему. Больше 90% ресёрча использует именно видюхи NVIDIA. Инференс часто на них же => все стараются выжать максимум, а для этого нужны TensorRT специальный фреймворк который максимально утилизирует мощь видеокарты для нейронных сетей.
Более того, если вы пишите на CUDA то можете в рамках одного обработчика обрабатывать данные. Самый классический пример NMS. Например, когда-то давно мы переписывали кусок одного детектора поз на CUDA чтобы не гонять данные на процессор. И это очень ускоряло его работу.
Нужно понимать, что NVIDIA целит в три области, и везде TensorRT используется:
  • Серверные платформы (Tesla)
  • Десктопы (видеокарты обычные и полу специализированные)
  • Embedded платформы серии Jetson.

Плюс TensorRT в том, что он достаточно стандартен. Он есть в TensorFlow (tf-trt), есть в OpenCV. Основной минус для меня под Windows нет поддержки в Python, только через сторонние проекты. А я люблю иногда что-то под виндой потыкать.
Мне кажется, что если вы делаете инференс на Nvidia, то у вас просто нет альтернатив надо использовать TensorRT. Всё остальное приведёт к падению производительности.

Triton Inference Server


Но, так как мы говорим про инференс, то нужно упомянуть Triton Inference Server ( github.com/triton-inference-server/server ). Это не совсем TensorRT (хотя TensorRT подразумевается как оптимальный для него фреймворк). Triton может использовать TensorFlow, PyTorch, Caffe. Он сам ограничивает память и настраивает любой из упомянутых фреймворков, управляя выполняющимися сетями.
Triton сам решает какие модели загружать-выгружать из памяти, сам решает какой batch использовать. Может использовать не только TensorRT модели, но и модели *.pd, *.pth, ONNX, и.т.д., (ведь далеко не все можно сконвертировать в tensorrt). Triton может раскладывать по нескольким GPU. И прочее и прочее.
Мы использовали его в продакшне в нескольких проектах и остались ужасно довольны. Максимальная утилизация GPU с минимумом проблем.
Но Он не сможет выполнить модель где-то за пределами Nvidia

Intel OpenVino


Intel представлен на рынке:
  • Серверных вычислителей (Intel FPGA, Xe GPU, Xeon)
  • Десктопов (с i3 начиная года с 2015 поддерживается почти всё). Intel GPU работает но не сверх круто.
  • Embedded платформ (movidius)

И для всего можно сварить модель через OpenVino.
Мне нравиться OpenVino так как он достаточно стабилен, прост, и имеет инференс почти везде. Я писал на Хабре статью в которой рассказывал опыт одного хоббийного проекта под Intel. Там я отлаживал на десктопах, а тестировал на RPi с мовидиусом. И все было норм.
В целом, все 2-3 проекта которые мы делали в своей практике под OpenVino, прошли примерно так же. Минимум сложностей, удобный инференс, заказчик доволен.
Open Vino интегрирован в OpenCV про который мы ещё поговорим. OpenCV тоже от Intel, но я бы рассматривал его как отдельный фреймворк/способ инференса и подхода к данным.
Отдельно я бы хотел отметить один забавный момент. Аренда GPU сервера для того чтобы развернуть модель в онлайне будет стоить где-то от 10к. рублей, где-то до 100к. рублей, в зависимости от используемых видюх.
Аренда сервака с I5 на каком-нибудь клауде зачастую возможна за 500-1000 рублей в месяц.
Разница в производительности между TensorRT и ONNX на сравнимых по цене процах может быть в 2-20 раз (зависит от сети). Как следствие часто можно неплохо сэкономить перенеся онлайн инференс на ONNX.

Google Edge


Google очень неоднозначная фирма сама по себе. Мало того, что Google имеет свой стек аппаратуры для инференса нейронных сетей. Ещё у Google целый стек различных фреймворков про которые мы поговорим позже. Запутанный и местами бажный. Но в целом это:
  • TensorFlow Edge
  • TensorFlow lite
  • TensorFlow JS
  • TensorFlow (чистый, или оптимизированный под какую-то платформу, например Intel TensorFlow)

В этой части мы говорим именно про отдельную ветку Edge ( coral.ai ). Она используется в Embedded продуктах, и в некоторых mobile продуктах.
Минусом Edge является то, что из коробки поддерживаются не все модели, гарантирована поддержка лишь нескольких ( coral.ai/models ). А конвертация достаточно усложнена (TF -> TF Lite -> Edge TPU):
image
Конвертация в Cloud TPU чуть проще, но и там есть свои особенности.
Я не буду более подробно рассказывать про особенности этого фреймворка. Скажу лишь что каждый раз когда с ним сталкивался оставались неприятные впечатления, так что не стали тащить его нигде в прод.

Универсализация с хорошей аппаратной поддержкой


Мы переходим в область подходов, которые достаточно оптимально используют железо, но будут требовать немного разный код на разных платформах (нельзя же на Android всё сделать на Python).
Тут я бы упорядочил как-то так (опять же, очень условно):

Пройдем по этому списку

OpenCV


OpenCV это легендарный фреймворк ComputerVision появившийся более 15 лет назад. За свои 12 лет занятий ComputerVision я запускал его на Arm году в 2010, на BlackFin примерно тогда же, на мобильниках, на серверах, на RPi, на Jetson, и много-много где. У него есть классные биндинги на Python, Java, JavaScript, когда-то я вовсю использовал его на C#.
OpenCV более чем живой и сейчас. И нейронные сети активно просачиваются туда с самого их появления.
Мне кажется, что на сегодня у OpenCV есть несколько глобальных минусов:
  • Очень мало актуальной документации. Попробуйте найти где-нибудь гайд по всем бекендам модуля DNN в OpenCV и по поддержке их на всех платформах?:) Конечно, можно изучить хедеры, но это не говорит о том что где будет работать. Порог входа и первого прототипа достаточно высок. Не в пример того же OpenVino.
  • Нейронные сети под Android поддерживаются ( docs.opencv.org/master/d0/d6c/tutorial_dnn_android.html ). Но я не находил актуального гайда про аппаратную поддержку GPU или других вычислителей.

Тут кажется, что всё грустно. Но! Стоит поставить НО даже большими буквами. Главный плюс OpenCV очень хорошая поддержка CUDA и OpenVINO. Судя по всему запланирована и поддержка любых OpenCL устройств, но пока информации мало.
По нашим тестам в OpenCV нейронные сети на CUDA выполняются медленнее чем в TensorRT всего на 5-10%, что отлично.
Это делает OpenCV весьма ценным фреймворком для серверных решений, где нужно выжимать максимум из имеющегося ускорителя, какой бы он не был.

Так же OpenCV очень неплохо поддерживает различные CPU на ARM-устройствах. На том же RPi он использует NEON.

Tensorflow lite


Чуть ближе к мобильному миру лежит TensorFlow lite. Он может исполнять нейронные сети как и на обычном GPU мобильного устройства, так и на возможных сопроцессорах, если производитель телефона соблюдает какой-то набор стандартов. Для мобильников возможные варианты выглядят примерно так:
image
В большинстве случаев вы автоматически можете проверить максимальный уровень ускорения и запустить именно на нём. Детальной карты того какие вендоры предоставляют какую поддержку железа я не нашёл. Но я видел несколько примеров которые начались поддерживаться. Например под Snapdragon мы когда-то портировали сети, а потом TFLite начал его поддерживать.
Чуть более подробно о том что насколько даёт прирост можно посмотреть, например, тут ai-benchmark.com/ranking_detailed
Так же, стоит упомянуть, что изначально поддержка GPU шла за счёт OpenGL, но в последнее время Google добавила и OpenCL, чем почти в 2 раза ускорила выполнение сетей.
Но прелесть TFlite не только в том что он работает под мобильниками. Он ещё достаточно неплох для части embedded устройств. Например, он достаточно эффективно работает под RaspberryPI.
На последнем DataFest ребята из X5 рассказывали что они используют эту конструкцию в продакшне.
Так же, TFlite, за счёт XNNPACK неплохо работает на процессорах (но не так эффективно как OpenVino на Intel). Это даёт возможность использовать TFLite как способ инферить модели на десктопах. Хоть и без поддержки GPU. Зато без тонны лишних зависимостей.

ONNX runtime


В какой-то момент в гонку ворвался Microsoft, который решил зайти со стороны. Сначала они выпустили универсальный формат сохранения нейронных сетей, который сейчас достаточно популярен, ONNX.
И, когда уже форматом многие начали пользоваться, решили сделать свой рантайм для него.

Если честно, то я даже не знаю половину того что представлено в списке..:)
Выглядит всё идеально (а ещё всё поддерживается на Python, Java, C, C++, C# и.т.д.)
Но пока что мне не довелось использовать ONNX-runtime на практике, кроме ONNX.js, о котором будет ниже. Так что не могу гарантировать что всё работает идеально. В интернетах слишком мало примеров того как всё работает. И знакомых которые бы на этом разворачивали продакшн полноценный тоже не знаю (максимум тестировали но не решили развернуть).
Но в гайдах уверяется что даже для Raspberry PI и Jetson есть поддержка.
Про поддержку ios явно ничего не сказано. Но местами ios встречается по коду. А кто-то билдит.

PyTorch


Одна из проблем использования OpenCV, TFlite, ONNX Runtime: А почему мне надо обучать в одном фреймворке, а использовать в другом?. И авторы PyTorch тоже задают себе такой вопрос. Так что, прямо из коробки, предоставляют способ использования PyTorch на мобильниках:
image
Скажу честно, я не тестировал скорости инференса. Но по опросу знакомых в целом все считают это медленным вариантом. По крайней мере инференс PyTorch на процессорах и GPU тоже не самый быстрый. Хотя, опять же, XNNPACK используют.
Но, данный вариант вполне удобен для разработки и пуша в продакшн (нет лишних конвертаций, и.т.д.). Мне кажется, что иногда это хороший вариант (например когда нет требований на очень высокую производительность).

TensorFlow


Кроме использования TFlite можно использовать и оригинальный TF, для развертывания на embedded устройствах и десктопах. При этом TF имеет достаточно высокую степень конфигурируемости. Но, на мой взгляд, это достаточно мертвый вариант в будущем.

Китайское вторжение


Теперь мы переходим к двум вариантам, которые не тестировал почти никто из моих знакомых. Но которые выглядят весьма перспективно.
Первый из них MNN, вариант от alibaba.
image
Авторы уверяют, что MNN самый легковесный из фреймворков, который обеспечивает инференс на большом числе устройств, при использовании GPU или CPU. При этом поддерживает большой спектр моделей.

Второй вариант интереснее, это ncnn от Tencent. Согласно документации библиотека работает на большом числе устройств. Реализована на c++:

На неё даже Yolov4 спортировано. И в целом, AlexeyAB очень позитивно отзывался.
Но, опять же, у меня достаточно мало информации о практическом применении. Сам не использовал, в проектах которые видел/консультировал никто не использовал.
При этом можно отметить, что не так много нативных решений одновременно поддерживают и телефоны и nvidia.

JS Tensorflow.JS, ONNX.js


Я бы объединил эти две категории в общий раздел, хотя, конечно, у TensorFlow.js и ONNX.js есть много различий. TensorFlow чуть лучше поддерживает оптимизацию. ONNX чуть быстрее. LSTM не присутствует в ONNX. И прочее и прочее.
image
В чем особенность этого класса инференс фреймворков? В том, что они выполняются только на CPU, или на GPU. По сути через WebGL или через WebAssembly. Это наборы инструкций, которые доступны через браузерный JS.
Как плюс имеем офигенную универсальность такого подхода. Написав один раз код на JS можно исполнять его на любом устройстве. Если есть GPU на нём. Если только CPU на нём.
Основной минус тоже понятен. Никакой поддержки за пределами CPU или GPU (ускорители/сопроцессоры). Невозможно использовать эффективные способы утилизации CPU и GPU (те же OpenVino или TensorRT).
Правда, под Node.js, TF.js умеет в TPU:
image

При этом этот вариант явно оптимальный, если вам нужно использовать простые модели, имея кроссплатформенность.

Мы использовали в проектах оба инференса, всё работало хорошо. Но, конечно, производительность немного жалко.

Как небольшое резюме


Выводы сложно делать. Я не смог придумать какого-то однозначного алгоритма который бы помогал выбрать платформу на которой нужно разворачивать ML решение в зависимости от бизнес требований, аппаратуры и сложности сетей.
Как мне кажется:
  • Если вам нужно максимально широкое использование на всех платформах, то, похоже, наиболее универсален ONNX runtime. Единственное, он может местами сыроват + ios поддержан весьма просто. Не уверен, что все ускорители одинаково хорошо работают. Вариант Tencent-ncnn
  • Если вам нужно максимум платформ, без мобильных, то я бы взял OpenCV. Он достаточно удобен и стандартен.
  • Если вам не нужны десктопные платформы, тогда я бы использовал TFlite
  • Если же вы хотите делать на какой-то стандартной платформе, и чётко понимаете что никуда за её пределы не пойдёте то я бы использовал специализированную платформу под эту платформу. Будь это TensorRT или OpenVino. И на TensorRT и на OpenVino мы разворачивали очень сложные проекты. Непреодолимых проблем не встретили.

В целом (очень условно) как-то так:


А так Тема объёмная. Ведь чтобы достаточно подробно рассмотреть TensorFlow lite со всеми плюсами и минусами надо написать две таких статьи. А для полного обзора OpenCV и десяти не хватит. Но сил написать столько нет.
С другой стороны, я надеюсь, написанное поможет кому-то хоть немного структурировать логику при выборе платформы для инференса.
А если у вас есть и другие идеи как выбирать пишите!

P.S.


В последнее время делаю много мелких статей/видеороликов. Так как это не формат Хабра то публикую их в блоге или на ютубе. Подборка всего есть в телеге и вк.
На Хабре обычно публикую, когда рассказ становится уже более самозамкнутым, иногда собрав 2-3 разных мини-рассказа на соседние темы.
Подробнее..

Подборка статей о машинном обучении кейсы, гайды и исследования за июль 2020

03.08.2020 22:10:54 | Автор: admin


Похоже, не один наш дайджест не обходится без упоминания разработок Open AI: в июле самой обсуждаемой темой в области машинного обучения стал новый алгоритм GPT-3. Технически это не одна модель, а целое семейство, которое для удобства обобщают под единым названием. В самой крупной модели используется 175 млрд параметров, а для обучения использовался датасет размером 570 Gb, в который вошли отфильтрованные данные из архивов Common Crawl и высококачественные данные WebText2, Books1, Books2 и Wikipedia.

Здесь стоит отметить, что модель предобучена, и не требует файн тюнинга под конкретные задачи: для достижения лучших результатов рекомендуется предоставлять ей хотя бы один (one-shot) или несколько (few-shot) примеров решения задач на входе, но можно обойтись вообще без них (zero-shot). Чтобы модель сгенерировала решение задачи, достаточно описать задачу на английском языке. Принято считать, что это алгоритм генерации текстов, но уже видно, что потенциал намного богаче.

Модель была представлена еще в мае, уже тогда Open AI продемонстрировали, что обученная на репозиториях GitHub GPT-3 способна успешно генерировать код Python, и вот, спустя полтора месяца первые счастливчики получили доступ к API и показали свои наработки. Результаты просто потрясающие. Нам, как разработчикам, конечно же, интересно, насколько этот алгоритм упростит нашу жизнь, а может, и создаст конкуренцию.

Уже появился сервис debuild.co, который по текстовому описанию функции создает работающий код и неплохо верстает.



Использовать наработки можно не только в веб-программировании, но и в дизайне. Модель способна по текстовому описанию генерировать JSON данные и переводить их в макет Figma.



А также ей практически удалось пройти собеседование на должность Ruby-разработчика.

На этом новости об использовании машинного обучения в программировании не заканчиваются.

TransCoder

Миграция кодовой базы с архаичного языка программирования, такого как COBOL, на современную альтернативу, вроде Java или C ++, является сложной, ресурсоемкой задачей, требующей от специалистов владения обеими технологиями. При этом архаичные языки по сей день используется в мэйнфреймах по всему миру, из-за чего перед владельцами часто возникает сложный выбор либо вручную переводить кодовую базу на современный язык, либо продолжать поддерживать легаси-код.

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

По оценкам создателей, модель правильно переводит более 90% функций Java в C ++, 74,8% функций C ++ в Java и 68,7% функций из Java в Python. Что выше показателей коммерческих аналогов.

ContraCode

Инструменты разработки все чаще используют машинное обучение для понимания и изменения написанного человеком кода. Главная сложность при работе алгоритмов с кодом заключается в нехватке размеченных наборов данных.

Исследователи из Беркли предлагают решить эту проблему с помощью метода ContraCode. Авторы считают, что программы с одинаковой функциональностью должны обладать одинаковыми репрезентациями, и наоборот. Поэтому они генерируют варианты кода для сравнительного обучения (Contrastive Learning). Для создания данных переименовывают переменные, переформатируют и обфусцируют код.

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

DeepSIM



Авторы этого исследования показывают, что генеративно-состязательная сеть на одном целевом изображении способна справляться со сложными манипуляциями.
Модель учится сопоставлять примитивное представление изображения (например, только края предметов на фото) с самим изображением. Во время манипуляции генератор позволяет модифицировать изображения, изменяя их примитивное представление на входе и мэппируя его через сеть. Этот подход решает проблему DNN, которая требует огромного набора данных для обучения. Результаты впечатляют.

3D Photo Inpainting



Очередной способ преобразования двухмерных RGB-D изображений в трехмерные. Алгоритм воссоздает области, которые скрыты объектами на оригинальном изображении. В качестве базового представления использовалось многослойное изображение глубины, на основе которого модель итеративно синтезирует новые данные о цвете и глубине для невидимой области с учетом контекста. На выходе получаются фотографии, к которым можно добавить эффект параллакса с помощью стандартных графических движков. Доступен колаб, где можно протестировать модель самостоятельно.

HiDT



Команда российских исследователей представила алгоритм с открытым исходным кодом, который меняет время суток на фотографиях. Моделирование изменений освещения на фотографиях с высоким разрешением является сложной задачей. Представленный алгоритм сочетает в себе генеративную модель image-to-image и схему апсемплинга, которая позволяет производить преобразования на изображениях с высоким разрешением. Важно отметить, что модель обучалась на статических изображениях разных ландшафтов без временных меток.

Swapping Autoencoder



Если HiDT умеет качественно изменять освещение на изображениях, то эта нейронная сеть, обученная на разных наборах данных, способна менять не только время суток, но и ландшафт. К сожалению, возможности посмотреть исходный код нет, поэтому остается только восхищаться видео, которое демонстрирует возможности этой модели.

SCAN

Нейронная сеть с открытым исходным кодом, которая самостоятельно группирует изображения в семантически значимые кластеры. Новизна авторского подхода в том, что разделяются этапы обучения и кластеризации. Сначала запускается задача по обучению признакам, потом модель опирается на полученные на первом этапе данные при кластеризации. Это позволяет добиваться лучших результатов, чем другие подобные модели.

RetrieveGAN



Генерирующие нейросети стремительно развиваются, и RetrieveGAN очередное тому подтверждение. Алгоритм на основе текстового описания сцены использует фрагменты существующих изображений для создания уникальных новых. Пока на получаемых изображениях есть множество артефактов, и выглядят они не очень правдоподобно, но в будущем это может открыть новые возможности в сфере фотомонтажа.

Слежение за пассажирами лифта

Благодаря достижениям в области компьютерного зрения и машинного обучения, слежение за людьми становится еще более эффективным. Группа исследователей из Шанхая по заказу крупного девелопера разработали систему социального контроля поведения в лифтах в реальном времени. Система способна выявлять подозрительную активность в лифтах. Таким образом создатели надеются предотвращать вандализм, сексуальные домогательства, наркоторговлю. Также система заметит, если на каком-то этаже люди будут останавливаться чаще: так, например, уже удалось выявить общепит, который незаконно работает в квартире. Система уже установлена и наблюдает за сотней тысяч лифтов.

Вот таким насыщенным получился июль. Посмотрим, какие новости принесет нам следующий месяц. Спасибо за внимание!
Подробнее..

Подборка статей о машинном обучении кейсы, гайды и исследования за август 2020

07.09.2020 18:22:46 | Автор: admin

Продолжаем собирать для вас материалы из области ML. Как и всегда предпочтение отдаем проектам, которые содержат ссылки на непустые репозитории, или предоставляют высокоуровневые API.


Iris

Компания MediaPipe, специализирующаяся на открытых ML-решениях по распознованию объектов в пространстве вроде FaceMesh и Handpose, на основе которых мы собирали демку представила новый инструмент Iris. Как можно догадаться по названию, эта модель машинного обучения распознает радужную оболочку, зрачок и контур глаза, используя простую RGB камеру в режиме реального времени. С погрешностью менее 10% она также определяет расстояние между субъектом и камерой без датчиков глубины. К сожалению, пока алгоритм не умеет определять, в каком направлении смотрит человек, ровно как и не способен на идентификацию личности, но зато в сочетании с Pose Animator он позволяет создавать более живых анимированных персонажей, так что ждем тренд на мультяшные маски.



FMKit

Не только Mediapipe пытаются решить проблему дорогостоющего переферийного оборудования с помощью алгоритмов машинного обучения исследователи из университета штата Аризона разработали способ взаимодействия с VR или AR окружением без специальных контроллеров.
Их алгоритм распознает написанные пальцем в воздухе слова. Обойтись совсем без устройств ввода не получилось, разработчики используют датчик захвата движения Leap Мotion. GitHub с исходным кодом и датасетами FMKit.



Style and Semantics

Исследователи из Швейцарской высшей технической школы Цюриха разработали открытую нейронную сеть, которая позволяет управлять сгенерированным изображением с помощью высокоуровневых атрибутов и текстовых описаний. На входе модели можно отдать маску объектов с их классами. Нейронная сеть создаст изображение, похожее по структуре. Редактировать содержимое изображения можно с помощью текстовых запросов. Модель работает в два этапа. На первом создается фон изображения, на втором генератор синтезирует передний план изображения с учетом созданного фона. Таким образом решается проблема артефактов, которые возникают на фоне при удалении или перемещении изображений с переднего плана.



Semantic Reactor

Если вы создаете приложение на основе языка например, чатбот для обслуживания клиентов или игру-квест, то вас может заинтересовать этот инструмент. Semantic Reactor это плагин для Google таблиц, который позволяет запускать модели понимания естественного языка на ваших собственных данных. На примере этой браузерной игры видно, на что способен инструмент. К счастью, он поддерживает и мультиязычную модель, обученную на 16 парах, среди которых есть русский.

Fawkes

Машинное обучение порождает множество этических споров, решить которые поможет то же машинное обучение. Исследователи из чикагского университета разработали алгоритм, который вносит изменения на уровне пикселей, невидимые человеческому глазу в фотографии, чтобы они стали непригодными для других моделей. Этот процесс они называют маскировкой изображений. Назвали инструмент в честь Гая Фокса, который всем известен благодаря маске анонимуса. Создатели утверждают, что замаскированные фото можно выкладывать в соцсетях, и если их будут использовать для обучения моделей распознавания лиц, то маскировка не позволит модели узнать вас на снимке из-за искажений. Как говорится, клин клином.



See & Spray

Кейс применениия машинного обучения в сельскохозяйственной промышленности. Крупнейший в мире производитель сельскохозяйственной техники John Deere обратился к технологиям машинного обучения и компьютерного зрения, чтобы эфективнее бороться с сорняками. Нейронная сеть определяет по снимкам сорняки, после чего мнгновенно опрыскивает их гербицидами. Это позволяет экономить ресурсы, не повреждая урожай. Для обучения всех моделей использовался фреймворк PyTorch. Первая трудность, с который столкнулись создатели подготовка и маркирование датасетов, из-за внешнего сходства сорняков с другими культурами. Также непросто далось развертывание моделей на устройствах, так как робот должен быстро принимать решения и перемещаться по полю.

AI Economist

Разработка экономической политики и оценка ее эффективности не поспевает за переменами окружающего мира, как например это заметно на фоне всемирной пандемии. К тому же экономические модели требуют множества допущений, что ограничивает их способность полностью описывать современные экономические условия: например, они могут изолировано изучать налог на прибыль, но без учета налога на потребление.
Компания Salesforce предлагает использовать для решений этих проблем ML-алгоритмы и опубликовала фреймворк, который применяет обучение с подкреплением и экономическое моделирование для быстрой разработки и оценки новой экономической политики на основе данных.

ScaNN

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

Модели машинного обучения очень преуспели в понимании языка и могут преобразовывать входные данные в эмбеддинги, векторные представления слов, обученные объединять мнять похожие входные данные в кластеры. Google представила опенсорсный инструмент для поиска похожести таких векторов.



Ре-рендеринг людей из одного изображения

Ре-рендеринг человека но основе одного изображения сложная задача. Современные алгоритмы часто создают артефакты, вроде нереалистичного искажения частей тела и одежды.
В данном исследовани продемонстрирован новый алгоритм, который позволяет через ре-рендеринг текстуры переодевать людей в новую одежду. Он представляет позу и форму тела в виде параметрической сетки, которую можно реконструировать из одного изображения и легко изменять. Вместо цветовых UV-карт, создатели предлагают использовать карты характеристик для кодирования внешнего вида. Пока качество слабое и исходников нет, но уже можно представить как эта технология скоро будет применяться в Ecommerce.

Бонус: в мае мы рассказывали про алгоритм, который определяет глубину кадров в видео, но тогда можно было оценить лишь видео демонстрацию, теперь стал доступен исходный код проекта.

На этом все, спасибо за внимание!
Подробнее..

Подборка статей о машинном обучении кейсы, гайды и исследования за сентябрь 2020

05.10.2020 22:22:55 | Автор: admin


Сентябрьскую подборку мы начнем с кейса. В этот раз он всего один, но зато какой!

Мы не устаем восхищаться возможностями GPT-3 и рассказывать о сферах ее применения, но многие при этом видят в алгоритме угрозу своей профессии.
И компания VMO, которая занимается A/B тестированием, решила провести соревнование профессиональные копирайтеры против GPT-3.

Они интегрировали алгоритм в свой визуальный редактор так, чтобы пользователи могли выбирать между сгенерированными и авторскими текстами. Пока что сервис позволяет только генерировать заголовки, описания товаров и услуг, а также кнопки-призывы к действию.

Почему это так интересно? Дело в том, что в продуктовом менеджменте и маркетинге очень много ресурсов уходит на проверку гипотез. Какой заголовок лучше повысит вовлеченность, или какого цвета и формы должна быть кнопка, чтобы клиент совершил целевое действие. Ответы на эти вопросы позволяют продуктам становится успешными.

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

Ну а теперь к остальным находкам прошлого месяца:

Wav2Lip

Модель генерирует движения губ под речь, синхронизируя таким образом аудио и видео потоки. Её можно использовать для онлайн-трансляций, пресс-конференций, а также дубляжа фильмов. На демо можно увидеть, как губы Тони Старка подстраиваются под дубляж на разных языках. Также при ухудшении связи во время скайп-звонков модель может генерировать кадры, которые были потеряны из-за сбоя в сигнале, и дорисовывать их на основе звукового потока. Еще создатели предлагают анимировать губы персонажей мемов для большей персонализации контента. Как и цифровые дикторы, эта модель умеет подстраивать движение губ под речь, сгенерированную из текста.

Примечательно, что в мае авторы опубликовали модель Lip2Wav, которая наоборот читает по губам и генерирует текст и звук. Свёрточная нейронная сеть извлекает визуальные характеристики, после чего речевой декодер генерирует на их основе мел-спектрограмму, и с помощью вокодера синтезируется голос.



Flow-edge Guided Video Completion

Новый алгоритм дополнения видео, который убирает водяные знаки и целые движущиеся объекты, а также расширяет поле зрения видео с учетом движения кадра. Как и другие похожие алгоритмы, сначала он определяет и восстанавливает края движущихся объектов. Дорисованные границы в таком случае не выглядят естественно в сцене. Особенность метода в том, что он отслеживает пять типов не локально соседствующих пикселей, то есть, находящихся на разных кадрах, затем определяет, каким из них можно доверять, и использует эти данные для восстановления недостающих областей. В результате видео получается более плавным. Уже можно ознакомиться с исходным кодом, скоро будет добавлен колаб.



X-Fields

Нейросеть обучали на сериях снимков одной сцены с размеченными координатами угла обзора, временными метками и параметрами освещения. Так она научилась интерполировать эти параметры и выводить промежуточные изображения. То есть, получив несколько снимков с постепенно тающим кубиком льда или пустеющим бокалом на входе, модель в реальном времени может генерировать изображения с учетом всевозможных комбинаций параметров. Чтобы было проще понять, о чем речь, советуем просто посмотреть видео-демонстрацию. Исходный код обещают в скором времени опубликовать.



Generative Image Inpainting

Очередной инструмент для удаления объектов с фотографий на основе генеративной нейронной сети. На этот раз это полноценный фреймворк с открытым исходным кодом и публичным API. Работает очень просто загружаете изображение и рисуете маску объекта, который хотите удалить, и готово, никакого дополнительного постпроцессинга. Проект развернут на веб сервере, поэтому можете легко протестировать прямо в браузере. Артефакты, конечно, есть, но с простыми изображениями справляется неплохо.



Portrait Shadow Manipulation

Портретные фотографии часто страдают от неправильного освещения. Положение и мягкость теней а также распределение света являются ограничениями окружающей среды, которые влияют на эстетические качества снимка. Для устранения нежелательных затенений больше не обязателен фоторедактор исследователи из Беркли представили алгоритм с открытым исходным кодом, который реалистично убирает затенения с фото и позволяет управлять освещением.

PSFR-GAN

Не менее распространенная задача при работе с фотографиями это их реставрация и улучшение качества. Этот опенсорсный инструмент неплохо повышает разрешение портретных снимков.



FrankMocap

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

Facebook AI представил систему создания 3D-мокапов рук и тела на основе анализа монокулярного видео. Захват движений работает в режиме, близком к реальному времени (9,5 кадра в секунду), и создает трехмерные изображения тела и рук в виде унифицированной параметрической модели. В отличии от других существующих подходов, этот позволяет одновременно захватывать и жесты рук, и движения всего тела. Исходный код уже доступен.

3DDFA

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



PSOHA

Еще одна технология от Facebook AI, которая также призвана упростить процесс 3D-моделирования нейросеть извлекает множество связей между человеком на изображении и остальными объектами и генерирует трехмерные мокапы. Таким образом, на основе всего одной фотографии, на которой изображен человек с каким-то повседневным предметом, создается 3D-модель. Алгоритм определяет формы людей и объектов, а также их пространственное расположение в естественных условиях, в неконтролируемой среде. Создатели обещают скоро выложить исходный код, поэтому пока остается верить примерам из демонстрации, которые, не будем лукавить, впечатляют.



Monster Mash

Новый фреймворк позволяет создавать и анимировать трехмерные объекты, используя всего один набросок. Это существенно упрощает процесс анимирования объектов, так как не нужно работать с кейфреймами, многоракурсной сеткой и скелетной анимацией. Модель создает трехмерную модель, которая сразу готова к созданию анимаций без долгой предварительной настройки разных параметров, которые, например, не позволяют объектам проходить сквозь друг друга.



ShapeAssembly

Алгоритм создает трехмерные модели мебели из прямоугольных параллелепипедов. Подход ShapeAssembly использует сильные стороны процедурных и глубоких генеративных моделей: с помощью первого подхода фиксируется подмножество изменчивости формы, которое можно интерпретировать и редактировать, а с помощью второго модель фиксирует изменчивость и корреляции между формами, которые трудно выразить процедурно. В сети уже шутят, что на следующем шаге нужно обучить эмбеддер на основе инструкций IKEA.

На этом закончим тему с 3D моделированием для этой области месяц выдался особенно насыщенным. Спасибо за внимание!
Подробнее..

Перевод Необходимый инструмент для каждого дата-сайентиста

28.10.2020 16:07:32 | Автор: admin
image

Давайте посмотрим правде в глаза: обучение модели машинного обучения отнимает много времени, даже с учетом развития вычислительной техники за последние несколько лет. Даже самые тривиальные модели имеют более миллиона параметров. В масштабе крупнее у моделей бывает более миллиарда параметров у GPT-3 их более 175 миллиардов! и обучение этих моделей занимает дни, а то и недели. Как дата-сайентисты мы хотели бы следить за метриками модели, чтобы знать, работает ли она, как мы того ожидаем. Но нет смысла сидеть рядом с компьютером, часами отслеживая показатели. Хорошо было бы получить все эти данные на телефон.

Эту задачу можно решить с помощью TensorDash инструмента удаленного мониторинга ваших моделей машинного обучения с открытым исходным кодом, о котором и пойдет речь дальше. Под катом примеры применения TensorDash с TensorFlow, Keras, PyTorch и Fast.ai.



Когда в последний раз вы оставляли модель тренироваться в течение нескольких часов и покидали место работы, а вернувшись, обнаруживали модель поврежденной на полпути? Это расстраивает, а поскольку у большинства людей нет возможности обучать модели машинного обучения локально, предпочтительны облачные сервисы, такие как GCP, Google Colab, AWS и Azure: пользователи платят за использование ресурсов. В этом случае, если модель потерпела крах в промежутке между тренировками, вы платите за неиспользуемый сервис. Уведомления о статусе модели помогут избавиться от страха что-то пропустить.

TensorDash


TensorDash бесплатное приложение с открытым исходным кодом, позволяющее удаленно отслеживать ваши модели машинного обучения с помощью подробных графиков всех ваших показателей. Оно также уведомляет вас, когда обучение завершилось или в случае, если модель потерпела крах. Поддерживаются все основные фреймворки машинного обучения: TensorFlow, Pytorch, Keras и Fastai. Теперь не нужно напрягаться, сидя перед компьютером и молясь о повышении точности. Вы можете отслеживать модель, не выходя из дома.


Работа TensorDash

Применение TensorDash


  1. Установите TensorDash из Play Store. Поддержка устройств iOS скоро появится.
  2. Cоздайте учетную запись.
  3. Установите пакет TensorDash для Python, выполнив команду pip install tensor-dash.
  4. Следуйте приведенным ниже инструкциям о том, как использовать TensorDash с фреймворками.

Поддержка Keras/tf.keras


Удаленный мониторинг с помощью Keras и tf.keras работает с использованием функции обратного вызова. Импортируйте TensorDash. Определите объект TensorDash, используя в качестве параметров имя модели, электронную почту вашей учетной записи и пароль. Через обратные вызовы передайте объект TensorDash в функцию fit(). Чтобы получить уведомление о сбое модели, добавьте функцию fit() в раздел обработки исключений, а в except вызовите метод sendCrash(). Ниже приведен пример кода для использования TensorDash в Keras/tf.keras

from tensordash.tensordash import Tensordashhistories = Tensordash(    ModelName = '<YOUR_MODEL_NAME>',    email = '<YOUR_EMAIL_ID>',     password = '<YOUR PASSWORD>')    try:    model.fit(    X_train,     y_train,     epochs = epochs,     validation_data = validation_data,     batch_size = batch_size,     callbacks = [histories])except:    histories.sendCrash()

Поддержка PyTorch


Импортируйте Torchdash из пакета Tensordash. Создайте объект Torchdash с именем модели, электронной почтой и паролем в качестве параметров. Используйте метод Torchdash sendLoss() внутри цикла обучения, чтобы отправить метрики модели в вашу учетную запись. Передайте метрики потери, точности, учебную потерю и точность валидации в качестве параметров. Обратите внимание, что вы должны добавить хотя бы одну метрику. Чтобы получить уведомление о сбое модели, добавьте обучающий цикл в раздел обработки исключений и в except вызовите метод sendCrash(). Ниже приведен пример кода использования TensorDash вместе с PyTorch.

from tensordash.torchdash import Torchdashhistories = Torchdash(    ModelName = '<YOUR_MODEL_NAME>',    email = '<YOUR_EMAIL_ID>',     password = '<YOUR PASSWORD>')try:    for epoch in range(epochs):        losses = []        for data in trainset:            X, y = data            net.zero_grad()            output = net(X.view(data_shape))            loss = F.nll_loss(output, y)            loss.backward()            optimizer.step()        losses = np.asarray(losses)        histories.sendLoss(loss = np.mean(losses), epoch = epoch, total_epochs = epochs) // Add this line to your training loopexcept:    histories.sendCrash()

Поддержка Fast.ai


Дистанционный мониторинг с помощью fast.ai работает через обратные вызовы. Импортируйте Fastdash. Определите объект Fastdash, используя в качестве параметров имя модели, электронную почту вашей учетной записи и пароль. Передайте объект Fastdash через обратные вызовы в функцию fit(). Чтобы получить уведомление о сбое модели, добавьте функцию fit() в раздел обработки исключений и в except вызовите метод sendCrash(). Ниже приведен пример кода использования TensorDash с Fast.ai.

from tensordash.fastdash import Fastdashmy_cb = Tensordash(    ModelName = '<YOUR_MODEL_NAME>',    email = '<YOUR_EMAIL_ID>',     password = '<YOUR PASSWORD>')try:    learn.fit(epochs, learning_rate, callbacks = my_cb)except:    my_cb.sendCrash()

Поддержка TensorFlow


Импортируйте Customdash из пакета Tensordash. Создайте объект Customdash с именем модели, электронной почтой и паролем в качестве параметров. Используйте метод sendLoss() объекта CustomDash внутри цикла обучения, чтобы отправить метрики модели в вашу учетную запись. Передайте метрики потери, точности, учебной потери и точности проверки в качестве параметров. Обратите внимание, что вы должны добавить хотя бы одну метрику. Чтобы получить уведомление о сбое модели, добавьте обучающий цикл в раздел обработки исключений и в except вызовите метод sendCrash(). Обратите внимание, что Customdash можно использовать с любым фреймворком, где вы указываете пользовательский цикл обучения. Ниже приведен пример кода для использования TensorDash в пользовательском цикле TensorFlow.

from tensordash.tensordash import Customdashhistories = Customdash(    ModelName = '<YOUR_MODEL_NAME>',    email = '<YOUR_EMAIL_ID>',     password = '<YOUR PASSWORD>')try:    for epoch in range(num_epochs):        epoch_loss_avg = tf.keras.metrics.Mean()        epoch_accuracy = tf.keras.metrics.SparseCategoricalAccuracy()        for x, y in train_dataset:            loss_value, grads = grad(model, x, y)            optimizer.apply_gradients(zip(grads, model.trainable_variables))            epoch_loss_avg(loss_value)            epoch_accuracy(y, model(x, training=True))        train_loss_results.append(epoch_loss_avg.result())        train_accuracy_results.append(epoch_accuracy.result())        histories.sendLoss(loss = epoch_loss_avg.result(), accuracy = epoch_accuracy.result(), epoch = epoch, total_epochs = epochs) // Add this line to your training loopexcept:    histories.sendCrash()

Заключение


TensorDash полностью бесплатный продукт и открытым исходным кодом. Github Issues и ваш вклад в него приветствуются. Посмотрите репозиторий здесь и детальную документацию здесь.

На тот случай, если вы подумываете повысить свою квалификацию или сменить сферу промокод HABR даст вам получить дополнительные 10% к скидке указанной на баннере.

image




Рекомендуемые статьи


Подробнее..

Перевод Как искусственный интеллект борется с вредителями

02.11.2020 18:18:10 | Автор: admin
Сегодня, в преддверии старта набора на новый поток курса Machine Learning, делимся с вами переводом поста из блога PyTorch, в котором рассказывается о работе ИИ для борьбы с вредителями, который анализирует ситуацию по фотографиям феромоновых ловушек в условиях индийской глубинки, где выращивают хлопок, о применении PyTorch Mobile для развертывания моделей прямо на смартфоне в оффлайне, о сжатии моделей и, конечно, немного о том, как работать с аномальными изображениями, которые отправляют индийские фермеры.

image



Хлопок одна из основных культур волокон во всем мире, его выращивают в более чем 80 странах. Почти 100 миллионов семей во всем мире занимаются выращиванием хлопка для получения средств к существованию. При таком значении хлопка его особая уязвимость к заражению вредителями вызывает тревогу у многих. Однако заражение вредителями это одновременно одна из наиболее значимых, но предотвратимых проблем, с которой сталкиваются фермеры, причем 55% всех используемых в Индии пестицидов приходится на разведение хлопка.

ИИ для мониторинга вредителей



Феромоновая ловушка

Wadhwani AI оздавался, чтобы применять ИИ в решении проблем на благо общества. Когда речь идет о борьбе и предотвращении атак на урожай, ловушки для вредителей можно использовать как систему раннего предупреждения, при этом количество захваченных вредителей сообщает о вторжении. Применяя глубокое обучение на изображениях ловушек, AI дает фермерам подробные рекомендации по опрыскиванию с целью упреждающего решения определенных проблем с вредителями.

С помощью PyTorch исследователи ИИ в Wadhwani создали модель, способную точно предсказать местонахождение вредителей в хлопковых культурах. После захвата изображения оно проходит через многозадачную сеть, которая проверяет изображение на валидность. Если это так, то ветка обнаружения указывает на потенциальное местоположение обнаруженных вредителей. Окончательные рекомендации даются, исходя из количества обнаруженных вредителей и установленных энтомологами правил. Весь процесс подробно изложен ниже.


Весь рабочий процесс с решением по мониторингу вредителей

Еще одно преимущество PyTorch способность использовать выводы в оффлайне. Множество мелких фермерских хозяйств расположено в таких местах, где имеются проблемы с доступом в Интернет. Для удобства пользователей Wadhwani AI может развернуть свои модели с помощью алгоритмов сжатия моделей, уменьшая их размер на 98 %: с 250 до менее чем 5 MB.

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

Построение решения машинного обучения



Задействованные в компонентах конвейера инструменты

Наша работа поддерживается целым рядом инструментов, например PyTorch, чтобы разрабатывать и развертывать модели, а также Weights & Biases, чтобы отслеживать эксперименты и передавать их результаты заинтересованным сторонам. Ниже приводится исчерпывающее резюме всех используемых нами инструментов.

Многозадачная модель


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

Single Shot MultiBox Detector (SSD) был одним из первых одноступенчатых детекторов, обеспечивший производительность, сравнимую с его двухступенчатыми аналогами, и в то же время высокую скорость. Так как наш набор данных кардинально отличается от стандартных наборов данных обнаружения объектов, мы столкнулись со значительной проблемой. Качество наших изображений ниже потому, что их делают со смартфонов в отдаленных деревнях. Вредители часто склонны группироваться, а это затрудняет определение границ на изображениях. Границы (конечности и крылья) это в конечном счете ключевые классификаторы, которых нет во входном изображении.


Некоторые проблемы при работе с реальными данными

Используемая для обучения SSD целевая функция называется MultiBox loss. Она состоит из локализующего и классификационного компонентов для учета герметичности ограничивающего поля и точности прогнозируемого класса соответственно. Это то место, где удобно отслеживать различные аспекты функций потерь, чтобы понять, как на них влияют изменения оптимизации, увеличение размера данных, архитектура сети или размер входных данных. Например, на рисунке 4 видно, что в общих потерях (в отношении train/loss) всегда преобладает потеря классификации (train/loss_c), а не потеря локализации (train/loss_l).


Функция SSD Loss на дашборде Weights & Biases

Другой способ наблюдать за эволюцией производительности модели визуализировать развитие прогнозов с течением времени. Weights & Biases помогает визуализировать прогноз: мы можем отслеживать, как прогнозируемые границы изменяются во времени:


Визуализация эволюции прогнозов границ в Weights & Biases

Развернув модель в полевых условиях, мы обнаружили, что пользователи обычно отправляют фотографии без ловушек. Известно, что модели глубокого обучения плохо работают с аномальными изображениями, часто будучи излишне уверенными в своих ошибочных прогнозах. Для решения этой проблемы и после использования различных подходов был обучен отдельный классификатор, чтобы отклонять широкий спектр изображений вне распределения. Далее модель VGGNet обучалась использованию изображений из набора данных COCO в качестве невалидного класса, а также изображений из нашего набора данных вредителей как валидного класса.

Однако во время вывода изображение должно было проходить через две сети, что значительно увеличивало время вывода. Так как и модели проверки изображений, и модели обнаружения объектов использовали одну и ту же базовую сеть VGGNet, на которую приходилась большая часть времени вывода, мы объединили их, добавив новую ветку с последнего слоя базовой сети в SSD для проверки изображений, чтобы у нас была единая модель. Сеть совместно обучалась работе в многозадачной учебной инсталляции, при этом руководители обнаружения и проверки были обучены работе с потерями MultiBox и c кросс-энтропией.


Иллюстрация нашей модели машинного обучения

Оценка модели


Наиболее распространенный подход к оценке сетей обнаружения объектов использование средней точности (Average Precision AP) для каждого класса. Однако связь между AP и целью этого решения не всегда проста. AP определяется с применением диапазона порогов доверия для каждого класса, тогда как во время развертывания нам необходимо выбрать одну операционную точку. Мы рассматриваем окончательное задание по рекомендации распыления пестицидов как проблему бинарной классификации, когда классы указывают, следует ли распылять пестицид. Следовательно, мы заново определяем ложноположительные и ложноотрицательные факторы ради четкой коммуникации с заинтересованными сторонами, которым, как правило, трудно интерпретировать точность и отзыв, подобные тем, что даны в следующем списке:

  • Пропущенный сигнал тревоги (MAR): в процентах случаев, когда рекомендация должна была касаться опрыскивания, но система предлагала обратное.
  • Скорость ложной тревоги (FAR): в процентах случаев, когда не следует предпринимать никаких действий, но система предложила распыление.

Мы поставили перед собой цель достичь менее 5 % MAR и FAR, поскольку и ложноположительные, и ложноотрицательные результаты вредны для фермера. Мы настроили гиперпараметр и выбор оптимального порога доверия для достижения этой общей цели. В частности, мы обнаружили, что график DataFrame в Weights & Biases крайне полезен для понимания прогнозов во время вывода на уровне изображения.


Сравнение реальных данных и прогноза на изображении с использованием графика dataframe на Weights & Biases

Этот график помогает визуально сравнивать прогнозируемые и реальные рамки в любом изображении, а также позволяет сортировать, группировать и фильтровать данные в соответствии с нашими требованиями. Например, прогнозы могут сортироваться на основе доверительной оценки, чтобы качественно оценивать входные данные с низким доверием. График Parameter Importance в Weights & Biases помогает определить самые значительные в отношении потери валидации гиперпараметры.


График важности параметров Weights & Biases показывает корреляцию различных параметров с заданной метрикой

Сжатие модели


Несмотря на то что основная причина для сжатия модели развертывание на телефоне, оно также выгодно при развертывании в облаке, поскольку работает значительно быстрее. Мы используем итерационную обрезку (как указано в этой работе) и квантование, чтобы уменьшить размер нашей модели с 265 до 5 MB. В статье представлена такая методика для классификационных сетей.

.
Иллюстрация обрезки сверточных слоев

На рисунке выше показано, как обрезается один фильтр. Давайте рассмотрим два последовательных слоя: L и L+1. Количество входных и выходных каналов для слоя L равно K и N соответственно. Поскольку вывод слоя L это вход слоя L+1, то количество входных каналов для слоя L+1 также равно N, а количество выходных каналов равно M. Обрезка фильтра по индексу i из слоя L уменьшила бы количество выходных каналов слоя L, а следовательно, и количество входных каналов в слое L+1 на единицу. Таким образом, для обрезки выбранного фильтра также необходимо обновить все слои с помощью слоя L в качестве входных данных. Мы изменим этот метод для работы с многозадачной архитектурой, сокращая 1024 фильтра на каждой итерации обрезки, для 15 итераций. Смотрите этот пост в блоге, чтобы получить более подробный контекст и другие подробности.


Развертывание модели


Чтобы развернуть модели в производственной среде, мы сначала конвертируем модель PyTorch в модуль TorchScript. TorchScript сериализует модель и делает ее независимой в смысле зависимостей Python. Мы смешиваем сценарии и трассировку для модели, поскольку определенные ее части имеют поток управления, зависимый от данных.

class Model(torch.nn.Module):    def __init__(self, param1, param2):        self.module1 = NeuralNet(param1)        self.module2 = torch.jit.script(FooFunc(param2))        def forward(self, x):        y1 = self.module1(x)        if some_condition:            return self.module2(y1)        else:            return y1traced_model = torch.jit.trace(Model, torch.randn(1,3,300,300))


Иллюстрация рабочего процесса подготовки модели PyTorch, готовой к обслуживанию сервером

Развертывание в облаке


Мы используем TorchServe, работающий как сервер модели. TorchServe имеет встроенный веб-сервер, который принимает модель, запрос вывода делается через REST API. Это позволяет нам легко настроить модель для пакетных прогнозов. Модуль TorchScript упакован в файл архива модели (.mar), необходимый TorchServe. TorchServe содержит точку входа и рабочий процесс для выполнения запроса вывода. Наконец, модуль TorchScript и файл mar загружаются в корзину s3. Мы создали внутреннюю структуру для ведения реестра и обслуживания моделей сервером. Каждая готовая к производству модель добавляется в реестр моделей вместе со всеми связанными метаданными. Платформа использует получившиеся артефакты для последующего обслуживания модели в экземпляре AWS EC2 с помощью TorchServe.

Развертывание на смартфонах


Чтобы развернуть нашу модель на смартфонах, мы используем PyTorch Mobile. Мы создаем вокруг Pytorch Mobile SDK, извлекающий артефакты из реестра моделей и позволяющий нам эффективно развертывать несколько моделей на устройстве. Приложение может делать выводы без подключения к Интернету. Оно ставит в очередь все данные, необходимые для регистрации, и синхронизирует их обратно с сервером в фоновом режиме, когда мобильный телефон подключен к сети.

Лучшие практики


Чтобы такие эксперименты можно было повторить, примите во внимание следующее:=

  • Контейнеризация. Docker широко используется для обеспечения того, чтобы все зависимости, необходимые для репликации эксперимента, были упакованы в единую изолированную среду.
  • Управление версиями данных. Поскольку наборы данных продолжают развиваться с течением времени, разделения на наборы тренировки, валидации и тестирования не могут быть статичными. Явно версионируйте данные, чтобы сделать снимки образцов, используемых для обучения любого конкретного эксперимента. На данный момент мы явно сохраняем разбиения в файле версии данных. Мы с нетерпением ждем перехода на артефакты от Weights & Biases.
  • Управление версиями эксперимента: мы сохраняем все гиперпараметры для каждого эксперимента в файле конфигурации вместе с идентификатором коммита Git и конкретным начальным значением.

В следующем сезоне наше решение используют более 10 000 фермеров по всей Индии, и мы надеемся, что это побудит больше людей творчески применять ИИ, чтобы решать социальные проблемы.

О Wadhwani AI

Институт искусственного интеллекта Wadhwani AI это независимый некоммерческий исследовательский институт и глобальный центр, разрабатывающий решения на основе искусственного интеллекта на благо общества. В институте используют возможности современного искусственного интеллекта, чтобы решать глобальных задачи. Миссия Wadhwani AI разработка и применение инноваций и решений на основе ИИ в широком спектре социальных сфер, включая здравоохранение, сельское хозяйство, образование, инфраструктуру и финансовую доступность.

Искусственный интеллект может решить многие задачи, но задачу экономии на обучении решить можете только вы. А поможет вам в этом специальный промокод HABR, который даст +10% к скидке, указанной на баннере ниже.

image




Рекомендуемые статьи


Подробнее..

Подборка статей о машинном обучении кейсы, гайды и исследования за октябрь 2020

02.11.2020 20:06:40 | Автор: admin


В октябре традиционно в центре внимания вновь GPT-3. С моделью от OpenAI связано сразу несколько новостей хорошая и не очень.

Сделка OpenAI и Microsoft


Начать придется с менее приятной компания Майкрософт завладела эксклюзивными правами на GPT-3. Сделка предсказуемо вызвала негодование Элон Маск, основатель OpenAI, а ныне бывший член совета директоров компании, заявил, что Майкрософт по сути захватили OpenAI.

Дело в том, что OpenAI изначально создавалась как некоммерческая организация с высокой миссией не позволить искусственному интеллекту оказаться в руках отдельного государства или корпорации. Основатели организации призывали к открытости исследований в этой области, чтобы технологии работали на благо всего человечества.

Майкрософт в свое оправдание утверждают, что не собираются ограничивать доступ к API модели. Таким образом, по факту ничего не изменилось до этого OpenAI тоже не публиковала код, но если раньше даже компаниям-партнерам позволяли работать с GPT-3 только через API, то сейчас эксклюзивные права на использование появились у Microsoft.

ruGPT3 от Сбера


Теперь к более приятной новости исследователи из Сбера выложили в открытый доступ модель, которая повторяет архитектуру GPT-3 и основана на коде GPT-2 и, самое главное, обучена на русскоязычном корпусе.

В качестве датасета для обучения использовалась коллекция русской литературы, данные Википедии, снапшоты новостных и вопросно-ответных сайтов, материалы порталов Pikabu, 22century.ru banki.ru, Omnia Russica. Разработчики также включили данные с GitHub и StackOverflow, чтобы научить генерировать и программный код. Общий объем очищенных данных более 600 Гб.

Новость однозначно хорошая, но есть пара оговорок. Эта модель похожа на GPT-3, но не является ей. Сами авторы признают, что она в 230 раз меньше самой большой версии GPT-3, у которой 175 млрд весов, а значит, результаты бенчмарков повторить точно не сможет. То есть, не рассчитывайте, что эта модель будет писать неотличимые от журналистских тексты.

Также стоит учитывать, что описанная архитектура GPT-3 может отличаться от реальной реализации. Сказать точно можно только ознакомившись с тренировочными параметрами, и если раньше веса публиковали с задержкой, то в свете последних событий их можно не ждать.

Дело в том, что от количества тренировочных параметров зависит бюджет проекта, и по оценкам экспертов, обучение GPT-3 обошлось не менее 10 млн. долларов. Таким образом, воспроизвести работу OpenAI могут только крупные компании c сильными ML-специалистами и мощными вычислительными ресурсами.

Отчет State of AI 2020


Все вышесказанное подтверждает выводы третьего ежегодного отчета о текущем состоянии дел в области машинного обучения. Натан Бенайх и Ян Хогарт, инвесторы которые специализируются на ИИ-стартапах, опубликовали подробную презентацию, которая охватывает вопросы технологий, кадров, применения в промышленности и тонкости правового поля.

Из любопытного целых 85% исследований публикуются без исходного кода. Если коммерческие организации можно оправдать тем, что код часто вплетен в инфраструктуру проектов, то что говорить про исследовательские институты и некоммерческие компании вроде DeepMind и OpenAI?

Также сказано, что увеличение датасетов и моделей приводит к увеличению бюджетов, и с учетом того, что область машинного обучения стагнирует, каждый новый прорыв требует несоразмерно больших бюджетов (сравните размер GPT-2 и GPT-3), а значит, позволить это себе смогут только крупные корпорации.

Советуем ознакомиться с этим документом, так как он написан кратко и понятно, хорошо проиллюстрирован. Также четыре прогноза на 2020 год из прошлого отчета уже сбылись.

Не станем и дальше сгущать краски, все-таки встречаются и хорошие истории, иначе бы этой подборки не было.

Открытые мультиязычные модели от Google и Facebook


mT5


Google опубликовали исходный код и датасет семейства мультиязычных моделей T5. Из-за шумихи, связанной с OpenAI, эта новость прошла практически незамеченной, несмотря на внушительных масштаб самая крупная модель насчитывает 13 млрд параметров.

Для обучения использовали датасет из 101 языка, среди которых на втором месте русский. Объяснить это можно тем, что наш великий и могучий занимает второе по популярности место в вебе.

M2M-100


Facebook тоже не отстают и выложили мультиязычную модель, которая по их заявлениям позволяет напрямую, без промежуточного языка, переводить 100x100 языковых пар.

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

Как правило многоязычные системы, которые сразу обрабатывают несколько языков, опираются на английский. Перевод получается опосредованным и неточным. Преодолеть разрыв между исходным и целевым языком сложно из-за недостатка данных, так как найти перевод с китайского на французский и наоборот бывает очень сложно. Для этого создателям пришлось генерировать синтетические данные путем обратного перевода.

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



Достижения в области видеоконференций


В октябре появилось сразу несколько интересных новостей от Nvidia.

StyleGAN2


Во-первых, опубликовали обновления для StyleGAN2. Низкоресурсная архитектура модели теперь дает улучшенные показатели на датасетах с менее чем 30 тысячами изображений. В новой версии появилась поддержка смешанной точности: обучение ускорилось в ~1.6x раз, инференс в ~1.3x раз, потребление GPU снизилось в ~1.5x раза. Также добавили автоматический выбор гиперпараметров модели: готовые решения для датасетов разного разрешения и разного количества доступных графических процессоров.

NeMo


Neural Modules открытый набор инструментов, который помогает быстро создавать, обучать, и файнтюнить разговорные модели. NeMo состоит из ядра, которое обеспечивает единый look and feel для всех моделей и коллекций, состоящих из сгруппированных по области применения модулей.

Maxine


Другой анонсированный продукт, скорее всего внутри использует обе вышеназванные технологии. Платформа для видеозвонков Maxine объединяет в себе целый зоопарк ML-алгоритмов. Сюда входит уже привычные улучшение разрешения, устранение шумов, удаление фона, но также коррекция взгляда и теней, восстановление картинки по ключевым чертам лица (то есть дипфейки), генерация субтитров и перевод речи на другие языки в режиме реального времени. То есть почти все, что ранее встречалось по отдельности, Nvidia объединила в один цифровой продукт. Сейчас можно подать заявку на ранний доступ.

Новые разработки Google


Из-за карантина в этому году идет настоящая гонка за лидерство в области видеоконференций. Google Meet поделились кейсом создания своего алгоритма для качественного удаления фона на основе фреймворка от Mediapipe (который умеет отслеживание движение глаз, головы и рук).

Google также запустил новую функцию для сервиса YouTube Stories на iOS, который позволяет улучшать качество речи. Это интересный кейс, потому что для видео доступно в разы больше энхесеров, чем для аудио. Данный алгоритм отслеживает и фиксирует корреляции между речью и визуальными маркерами, вроде мимики лица, движения губ, которые затем использует, чтобы отделить речь от фоновых звуков, в том числе голосов от остальных говорящих.

Еще компания представила новую попытку в сфере распознования языка жестов.

Говоря о ПО для видеоконференций, стоит также упомянуть о новых дипфейк-алгоритмах.

MakeItTalk


Недавно в открытый доступ выложили код алгоритма, который анимирует фото, опираясь исключительно на аудиопоток. Это примечательно, так как обычно дипфейки алгоритмы принимают на вход видео.



BeyondBelief


Новое поколение дипфейк-алгоритмов ставит перед собой задачу делать подмену не только лица, но и всего тела, включая цвет волосы, тон кожи и фигуру. Данную технологию в первую очередь собираются применять в сфере онлайн-шопинга, чтобы можно было использовать фотографии товаров, представленные самим брендом, без необходимости нанимать отдельных моделей. Больше сфер применения можно посмотреть в видеодемонстрации. Пока что выглядит неубедительно, но в скором времени все может измениться.



Hi-Fi 3D Face


Нейросеть генерирует высококачественную 3D-модель лица человека по фотографиям. На вход модель принимает короткое видео с обычной RGB-D камеры, а на выходе отдает сгенерированную 3D-модель лица. Код проекта и 3DMM-модель находятся в открытом доступе.



SkyAR


Авторы представили открытую технологию для замены неба на видео в режиме реального времени, который также позволяет управлять стилями. На целевом видео можно сгенерировать погодные эффекты вроде молний.

Пайплайн модели поэтапно решает ряд задач: сетка матирует небо, отслеживает движущиеся объекты, оборачивает и перекрашивает изображение под цветовую гамму скайбокса.



Sea-thru


Инструмент решает необычную задачу по восстановлению настоящих цветов на сделанных под водой снимках. То есть, алгоритм учитывает глубину и расстояние до объектов, чтобы восстановить освещение и убрать воду с изображений. Пока доступны только датасеты.

Модель от MIT для диагностики Covid-19


В завершение поделимся любопытным кейсом на актуальную тему исследователи MIT разработали модель, которая отличает бессимптомных больных коронавирусной инфекцией от здоровых людей с помощью записей принудительного кашля.

Модель обучена на десятках тысяч аудиозаписей образцов кашля. Как заявляют в MIT, алгоритм идентифицирует людей, у которых было подтверждено наличие Covid-19 с точностью 98,5%.

Государственные органы уже одобрили создание приложения. Пользователь сможет загрузить аудиозапись своего кашля и на основе результата определить, нужно ли сдавать полноценный анализ в лаборатории.

На этом все, спасибо за внимание!
Подробнее..

Перевод Как экономить память и удваивать размеры моделей PyTorch с новым методом Sharded

07.01.2021 18:21:18 | Автор: admin
Модели глубокого обучения улучшаются с увеличением количества данных и параметров. Даже с последней моделью GPT-3 от Open AI, которая использует 175 миллиардов параметров, нам ещё предстоит увидеть плато роста количества параметров.

Для некоторых областей, таких как NLP, рабочей лошадкой был Transformer, который требует огромных объёмов памяти графического процессора. Реалистичные модели просто не помещаются в памяти. Последний метод под названием Sharded [букв. сегментированный] был представлен в Zero paper Microsoft, в котором они разработали метод, приближающий человечество к 1 триллиону параметров.

Специально к старту нового потока курса по Machine Learning, делюсь с вами статьей о Sharded в которой показывается, как использовать его с PyTorch сегодня для обучения моделей со вдвое большей памятью и всего за несколько минут. Эта возможность в PyTorch теперь доступна благодаря сотрудничеству между командами FairScale Facebook AI Research и PyTorch Lightning.





Для кого эта статья?


Эта статья предназначена для всех, кто использует PyTorch для обучения моделей. Sharded работает на любой модели, независимо от того, какую модель обучать: NLP (трансформатор), зрительную (SIMCL, swav, Resnet) или даже речевые модели. Вот моментальный снимок прироста производительности, который вы можете увидеть с помощью Sharded во всех типах моделей.



SwAV это современный метод контролируемого данными обучения в области компьютерного зрения.
DeepSpeech2 это современный метод для речевых моделей.
Image GPT передовой метод для визуальных моделей.
Трансформер передовой метод обработки естественного языка.

Как использовать Sharded вместе с PyTorch


Для тех, у кого не так много времени, чтобы прочитать интуитивно понятное объяснение о том, как работает Sharded, я сразу объясню, как использовать Sharded с вашим кодом PyTorch. Но призываю прочитать конец статьи, чтобы понять, как работает Sharded.

Sharded предназначен для использования с несколькими графическими процессорами, чтобы задействовать все доступные преимущества. Но обучение на нескольких графических процессорах может быть пугающим и очень болезненным в настройке.

Самый простой способ зарядить ваш код с помощью Sharded это преобразовать вашу модель в PyTorch Lightning (это всего лишь рефакторинг). Вот 4-минутное видео, которое показывает, как преобразовать ваш код PyTorch в Lightning.



Как только вы это сделаете, включить Sharded на 8 графических процессорах будет так же просто, как изменить один флаг: не требуется никаких изменений в вашем коде.



Если ваша модель взята из другой библиотеки глубокого обучения, она всё равно будет работать с Lightning (NVIDIA Nemo, fast.ai, Hugging Face). Всё, что вам нужно сделать, это импортировать модель в LightningModule и начать обучение.

from argparse import ArgumentParserimport torchimport torch.nn as nnimport pytorch_lightning as plfrom pytorch_lightning.metrics.functional import accuracyfrom transformers import BertModelclass LitBertClassifier(pl.LightningModule):    def __init__(self, n_classes, pretrained_model_name='bert-base-uncased'):        super().__init__()        self.save_hyperparameters()        self.bert = BertModel.from_pretrained(pretrained_model_name)        self.drop = nn.Dropout(p=0.3)        self.out = nn.Linear(self.bert.config.hidden_size, n_classes)        self.loss_fn = nn.CrossEntropyLoss()    def forward(self, input_ids, attention_mask):        outputs = self.bert(            input_ids=input_ids,            attention_mask=attention_mask,            return_dict=False        )        pooled_output = outputs[1]        output = self.drop(pooled_output)        return self.out(output)    def training_step(self, batch, batch_idx):        loss, acc = self._shared_step(batch, batch_idx)        self.log("acc", acc)        return loss    def validation_step(self, batch, batch_idx):        _, acc = self._shared_step(batch, batch_idx)        self.log("val_acc", acc)    def _shared_step(self, batch, batch_idx):        input_ids = batch["input_ids"]        attention_mask = batch["attention_mask"]        targets = batch["targets"]        outputs = self.forward(            input_ids=input_ids,            attention_mask=attention_mask        )        _, preds = torch.max(outputs, dim=1)        loss = self.loss_fn(outputs, targets)        acc = accuracy(preds, targets)        return loss, acc    def configure_optimizers(self):        return torch.optim.AdamW(self.parameters(), lr=2e-5)if __name__ == '__main__':    # TODO: add your own dataset    train_dataloader = ...    val_dataloader = ...    bert = LitBertClassifier()    trainer = pl.Trainer(gpus=8, plugins='ddp_sharded')    trainer.fit(bert, train_dataloader)

Интуитивно понятное объяснение работы Sharded


Для эффективного обучения на большом количестве графических процессорах применяются несколько подходов. В одном подходе (DP) каждый пакет делится между графическими процессорами. Вот иллюстрация DP, где каждая часть пакета отправляется на другой графический процессор, а модель многократно копируется на каждый из них.


Обучение DP

Однако этот подход плох, потому что веса модели передаются через устройство. Кроме того, первый графический процессор поддерживает все состояния оптимизатора. Например, Адам хранит дополнительную полную копию весов вашей модели.

В другом методе (параллельное распределение данных, DDP) каждый графический процессор обучается на подмножестве данных и градиенты синхронизируются между графическими процессорами. Этот метод также работает на многих машинах (узлах). На этом рисунке каждый графический процессор получает подмножество данных и инициализирует одинаковые веса модели для всех графических процессоров. Затем, после обратного прохода, все градиенты синхронизируются и обновляются.


Параллельное распределение данных

Однако в этом методе всё ещё есть проблема, которая заключается в том, что каждый графический процессор должен поддерживать копию всех состояний оптимизатора (примерно в 23 раза больше параметров модели), а также всех прямых и обратных активаций.

Sharded устраняет эту избыточность. Он работает так же, как DDP, за исключением того, что все накладные расходы (градиенты, состояние оптимизатора и т. д.) вычисляются только для части полных параметров, и, таким образом, мы устраняем избыточность хранения одного и того же градиента и состояний оптимизатора на всех графических процессорах. Иначе говоря, каждый графический процессор хранит только подмножество активаций, параметров оптимизатора и вычислений градиента.

Использование какого-либо распределённого режима




В PyTorch Lightning переключение режимов распределения тривиально.

Как видите, с помощью любого из этих подходов к оптимизации можно получить много способов добиться максимальной эффективности распределённого обучения.

Хорошая новость в том, что все эти режимы доступны в PyTorch Lightning без необходимости изменять код. Вы можете попробовать любой из них и при необходимости отрегулировать его для вашей конкретной модели.

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

Преимущество использования Lightning в том, что вы никогда не отстанете от последних достижений в области исследований искусственного интеллекта! Команда и сообщество ПО с открытым исходным кодом с помощью Lightning стремятся поделиться с вами последними достижениями.

image



Подробнее..

Подборка статей о машинном обучении кейсы, гайды и исследования за декабрь 2020

11.01.2021 18:14:50 | Автор: admin


Последний месяц года нельзя назвать удачным моментом для масштабных анонсов, так как большинство переходит в режим давайте уже после праздников, но судя по этой насыщенной подборке в области машинного обучения, и в декабре во всю кипела работа. Поэтому с небольшой задержкой встречайте двенадцатый выпуск дайджеста, в котором мы расскажем о самом важном, что произошло в ML в конце 2020 года.

MuZero


DeepMind неожиданно опубликовали статью о MuZero, алгоритме, который способен играть как в популярные логические настольные игры вроде шахмат, Сёги и Го, так и в видеоигры Atari вроде Pac-Man.

MuZero пытается смоделировать не всю среду, а только отдельные аспекты, которые важны для процесса принятия стратегических решений агентом. Алгоритм постоянно собирает информацию о текущем и предыдущем состоянии игры изучая таким образом запреты и награды. Так, например, модель понимает, что в шахматах цель игры поставить мат, а в пакмане съесть желтую точку.

Есть еще одно важное преимущество: MuZero многократно использует изученную модель для улучшения планирования, а не для сбора новых данных о среде. Например, в играх Atari со сложной изменяющейся средой алгоритм использовал изученную модель в 90% случаев чтобы перепланировать то, что должно было быть сделано в прошлых игровых сессиях.

Почему это важно. По сути, MuZero модель общего назначения, которую можно будет применять для решения сложных проблем реального мира, которые трудно свести к простым правилам. DeepMind предлагают такую аналогию новый подход похож на то, как человек в пасмурную погоду решает взять зонтик, чтобы остаться сухим, в то время как прежние подходы пытались бы смоделировать, в каком порядке будут падать капли дождя.



Infinite Nature


Все хоть раз видели захватывающую съемку с дрона, летящего вдоль живописной береговой линии. Обученный на подобных роликах с youtube алгоритм синтезирует видео из одного статичного изображения.

Задача очень непростая, так как нужно генерировать новые изображения, которые могут сильно отличаться от вводных данных на фото часто присутствуют деревья и скалы, которые заслоняют располагающиеся за ними фрагменты ландшафта.

Новизна подхода в том, что он способен синтезировать изображения с учетом геометрии сцены, которая покрывает большие расстояния на сотни кадров. Датасет уже доступен, а вот исходники придется подождать.



Time Travel Rephotography


Нейросеть для реставрации и колоризации старых фото, напоминающая DeOldify. В отличие от обычных фильтров восстановления изображений, которые применяют независимые операции, такие как шумоподавление, раскрашивание и апскейлинг, здесь используется StyleGAN2, чтобы синтезировать близкое к оригиналу лицо. На выходе получаются портретные фотографии сразу в цвете и хорошем разрешении. Код тоже обещают выкатить позже.



pi-GAN


Еще одна GAN-модель, которая генерирует 3D представление объекта из нескольких неразмеченных двухмерных изображений. В демо показано, как модель можно использовать для вращения головы, подобно тому как ранее демонстрировали Nvidia в Maxine.



Neural Scene Flow Fields


Новый NeRf-метод, который строит представление динамической сцены из видео, снятого на обычную камеру. Это позволяет, например, остановить кадр и сдвинуть камеру или наоборот зафиксировать камеру, но будто перемотать время. Алгоритм дорисовывает окружение со сложной структурой, например, с тонкими объектами вроде решеток, и движущимися объектами вроде мыльных пузырей.



YolactEdge


Первый метод мгновенной сегментации изображений, который в режиме реального времени работает на слабых устройствах. Исходный код уже доступен.

ModNet


Технология, которая позволяет не только качественно удалять фон с портретных снимков, но также заменять фон на видео. По факту это может стать неплохой заменой хромакея. В отличие от платного remove.bg, здесь есть и исходный код, коллаб и даже веб-приложение с простеньким интерфейсом, в котором пока можно протестировать только работу с фото.

Svoice


Facebook наконец опубликовала исходники алгоритма, который определяет на аудиозаписи голоса нескольких говорящих людей.


Hypersim


Apple опубликовали датасет с сегментационными масками для искусственно созданных сцен. Почти два терабайта рендеров помещений в сверхвысоком разрешении. Маркировка данных здесь на уровне отдельных пикселей.

ArtLine


Открытая модель, которая превращает фотопортрет в карандашный набросок. Пока что плохо справляется с текстурами одежды и с тенями, но в целом выдает достойные результаты. За основу взята архитектура DeOldify, которая позволяет хорошо распознавать черты лица.

На этом все, вот таким на удивление насыщенным оказался декабрь. Начало года тоже обещает быть интересным. Нам уже не терпится посмотреть, что в январе появится на основе Dall-E от OpenAI. Как говорится, stay tuned!
Подробнее..

Играем с CLIP. Создаем универсальный zero-shot классификатор на Android

13.03.2021 14:14:35 | Автор: admin

TLDR: приложение можно скачать и потестить тут

Ссылка на Google Play

Эта статья является дополненной и сильно расширенной версией моей статьи в TowardsDataScience о создании приложения, использующем новейшую мультимодальную нейросеть от OpenAI

В чем проблема классификаторов?

Многие заметили, что в последние годы все чаще для обработки изображений используется нейросетевой подход. Одной из простейших (по формулировке) задач является задача классификации изображений. В ней необходимо определить, к какому из заданных классов относится изображение. Стандартный подход с использованием сверточных нейросетей предполагает использование большого количества последовательных преобразований - сверток, с добавлением простых нелинейных функций, в результате которых изображение превращается в многомерный набор признаков. Далее эти признаки анализируются полносвязной нейросетью. Для обучения подобной нейросети обычно требуется большое количество обучающих примеров - размеченных изображений и сбор данных для конкретной задачи может являться наиболее трудоемким этапом для решения задачи классификации. Чтобы сократить количество необходимых размеченных данных, обычно используется подход переноса обучения (transfer learning). Для этого в качестве сверточной части используют сеть, предварительно обученную для решения задачи классификации на большом датасете (обычно ImageNet). Использование предобученной части позволяет выделять значимые признаки на любом изображении. Далее используется небольшая полносвязная сеть для решение задачи классификации. Использование такого подхода позволяет снизить размер обучающей выборки до нескольких тысяч или даже сотен примеров каждого класса. Тем не менее у вышеописанного подхода есть два существенных недостатка:

  • Если к сети, обученной определять 1000 классов необходимо добавить еще один класс, нужно существенно менять архитектуру сети и заново переобучать ее с использованием полного датасета, а не только изображений из добавленных классов (есть трюки, чтоб ослабить эту проблему но они не сильно меняют общее положение дел).

  • Для некоторых задач сбор данных может быть очень затруднительным

Поэтому чашей грааля для решения задачи классификации является реализации концепции Zero shot learning - создание классификатора, способного решить произвольную задачу классификации без обучающих примеров. Звучит немного фантастично и малопонятно, как это может быть реализовано. Многие считают, что для создания такого рода классификатора необходимо привлечь "понимание" естественного языка. К счастью, последние пару лет был достигнут большой прогресс в использовании нейросетей типа Transfirmer для обработки естественного языка. Кроме того, сейчас растет популярность и использования трансформеров для обработки изображений.

Почему CLIP?

В январе этого года был сделан прорыв в области обработки изображений - OpenAI представила новый генератор изображений Dall-E, который может генерировать изображение на основе текстового описания. Несмотря на название OpenAI, код Dall-E не является открытым. Тем не менее, меня очень заинтересовала вспомогательная нейронная сеть для обучения Dall-E и отбора лучших примеров. Это сеть CLIP. CLIP, в отличие от Dall-E, проект с открытым исходным кодом, опубликованный под лицензией MIT, поэтому его можно легко использовать в своих целях. Эта нейронная сеть выглядит не столь впечатляющей для демонстраций публике, но меня она очень удивила. В целом, это двухмодульный проект. Первый модуль - эффективная нейронная сеть Image Transformer. Этот модуль использует State-of-Art механизм внимания для кодирования изображения в 512-мерное пространство. Другая часть - нейросеть-трансформер для обработки текста, который преобразует текст в вектор в то же 512-мерное пространство. Сеть обучалась на большом массиве изображений (каком именно я не нашел, но, похоже, что это что-то типа "весь интернет", "вся википедия" или "весь инстаграм", как недавно сделали в Facebook AI). Процедура обучения не раскрывается, но предполагаю, что использовался loss типа Cosface или Arcface и различные параметры обучения для каждого из модулей. При обучении картинка с подходящей подписью должны быть близки, а с неподходящей - максимально далеки в пространстве embedding-ов.

CLIP хорошо работает для решения задачи zero-shot learning. Для этого необходимо создать набор предложений с использованием шаблона. Предложения могут быть типа "This is a photo of OBJECT", где OBJECT - название одного из множества классов. Набор предложений можно превратить при помощи текстового модуля transformer в набор векторов. Далее смотрится на какой из векторов больше всего похоже закодированное при помощи второго модуля изображение. Если нормировать близость векторов при помощи Softmax, то можно интерпретировать результат как вероятность того, что изображение принадлежит к какому-то классу.

Оказалось, что для многих задач Zero-shot learning работает даже лучше, чем натренированные на специально отобранных датасентах state-of-art сверточные нейросети.

Архитектура приложения

С учетом вышесказанного, приложение для классификации может работать следующим образом.

Телефон обладает доступом к изображениям (с камеры или из хранилища). После предварительной обработки (изменения разрешения и перенормировки каналов) изображение при помощи нейросети-трансформера превращается в 512-мерный вектор. Данный вектор сравнивается с каждым из векторов одного из предварительно сгенерированных наборов. Выдается описание 5 наиболее близких векторов.

Кроме того, существует возможность отправить запрос на сервер для генерации собственного классификатора. Отправляется строка - шаблон и набор классов. С сервера возвращается и сохраняется простой torchScript модуль, содержащий набор векторов и необходимые действия с ними. У пользователя появляется собственный классификатор! Работа с CLIP была на python, Android приложение - на JAVA. Серверная часть - Python/FLASK. Архитектура показана на рисунке.

Работаем с CLIP (Python)

Для разработки я использовал дистрибутив Anaconda python. Установим необходимые библиотеки

conda create-name pytorchconda activate pytorchconda install-yes -c pytorch pytorch=1.7.1 torchvision cudatoolkit=11.0conda create-name pytorchpip install ftfy regex tqdmpip install git+https://github.com/openai/CLIP.gitconda install -c conda-forge notebook

После установки необходимых библиотек импортируем их в проект:

И создаем модель CLIP. Работу с ним будем вести на GPU:

Нейросеть для обработки изображений - это модуль clipmodel.visual. Попробуем скомпилировать его и сохранить его отдельно в виде модуля TorchScript. Для этого используется JIT компилятор библиотеки torch (torch.jit). Для JIT компиляции необходимо запустить модуль visual на каком-либо изображении:

Найдем несколько списков названий классов (я использовал 4000 наиболее часто используемых существительных в английском языке, список пород кошек, список 10000 самых известных людей, список названий еды, национальностей и еще несколько других). Функция create_pt_xml создает предложения по шаблону, разбивает их на части (токенизирует), превращает в набор векторов, создает и сохраняет TorchScript модуль для сравнения любого вектора с векторами из набора и нахождения 5 ближайших векторов. create_pt_xml также сохраняет xml файл для чтения названий классов. Сгенерированные файлы будут использованы в приложении:

Создаем приложение для Android (Java)

Так как у меня нет опыта разработки на быстро набирающем популярность языке Kotlin, я использовал Java. В отличии от python, код на Java гораздо более громоздкий и менее выразительный. Поэтому, чтобы не перегружать пост оставлю только 2 наиболее важные части кода. Остальные части - описание работы кнопок/интерфейс - достаточно стандартные для любого приложения.

Первая важная часть - загрузка *.pt модели из папки assets и получение ответа в виде строки в TextView:

В качестве бонуса я добавил модуль, натренированный классическим образом (на большом датасете) для определения вероятности наличия пневмонии (python код не приведен) по флюорографии - сейчас такую задачку очень любят решать.

Вторая важная часть - отправление запроса на сервер и сохранения ответа (модели) в ExternalFilesDir. Список классов и название модели сохраняются в той же директории:

Серверная часть (Python/flask)

Я арендовал VPS на одном из сервисов. О системе - я запустил сервер apache 2.0 с WSGI / Flask под Centos 7 (для меня это была самая сложная часть проекта, поскольку я никогда раньше не работал с развертыванием сервера, на это ушло несколько дней постоянного поиска в Google/StackOverflow). Серверная часть Python очень похожа на функцию cerate_xml. Единственная разница заключается в обработке запросов и отправке сгенерированного файла модели. К сожалению, сервер работает не так быстро, как хотелось бы. Возможно, если приложение будет популярным, мне стоит перенести наиболее тяжелые вычисления (кодирование текста через текстовый трансформер) в AWS Lambda:

Буду смотреть по нагрузке.

Крутая часть! Тестируем!

Приложение работает на удивление хорошо. Сначала давайте повторим удивительные результаты OpenAI, который обнаружил схожие активации нейронов в одной концепции, представленной разными способами:

Общий классификатор NOUN использует 4000 наиболее часто используемых английских существительных и предложений, сгенерированных шаблоном This is an image of NOUN.

Приложение определенно понимает, что все эти понятия относятся к паукам. Интересно, что оно распознает текст, поэтому я решил протестировать его на работах бельгийского художника Рене Магритта, чьи работы в значительной степени наполнены взаимодействием слов и изображений:

Ничего особо интересного. Интересное начинается, когда я пытаюсь описать этот образ, используя национальность или использовать модель, которая может выдать только названия коктейлей или породы собак:

Здесь есть что-то определенно французское) Мне стало особенно любопытно, можно ли описывать любые изображения, используя названия коктейлей:

Список похожих коктейлей меня порадовал. Зеленый гоблин? Кажется у сети все неплохо со знанием вселенной Marvel) Также очевидно, что это изображение чего-то красного и большого, летающего или даже похожего на насекомое. Чтобы протестировать серверную часть, я создал на телефоне классификатор, определяющий профессию по изображению. Я нашел список из 30 профессий в одном из учебников английского языка и добавил их. Модель была успешно сгенерирована на сервере и загружена. К сожалению, на это ушла пара минут (

Проверим, как работает созданная на сервере модель:

Видим, что приложение неплохо определяет профессии.

А кто по профессии Человек-Паук?

Что касается других классификаторов, то они работают хорошо:

Или, как выяснили исследователи OpenAI, мы можем использовать их для выявления некоторых географических ассоциаций:

Думаю ассоциативному мышлению неизбежно сопутствует предвзятость, поэтому от нее невозможно полностью избавиться в сложных системах типа нейросетей:

Посмотрим, понимает ли нейросеть русский язык. Если использовать стандартный классификатор, результаты не очень. Если уточнить, что это животное - уже лучше:

При этом с пониманием английского проблем нет, а с китайским - есть:

А можно ли превратить приложение в переводчик? Ограничимся названием животных. Шаблон: This is NAME in russian. NAME - список из 100 часто встречающихся животных:

/

Загрузка на Google Play market

Основной проблемой стало ограничение на размер загружаемого *.aab файла. Из-за большого размера трансформерной нейросети приложение пришлось разбить на 2 части с использованием механизма Asset Delivery. Оказалось, что оно некорректно доставляет Assets при внутреннем тестировании - отправил запрос в техподдержку, но ответа не получил. Я подключил firebase для сбора аналитики, нарисовал простую страничку с описанием и отправил приложение в Play Market, где оно проверялось в течение 1 недели.

Монетизация

Если приложение будет пользоваться популярностью, я собираюсь добавить пару баннеров из AdMob, чтобы оплатить сервер и заработать немного денег)

Проблемы

В описываемом приложении есть несколько проблем. Во-первых, я обнаружил медленный (5 с) холодный запуск на нескольких устройствах из-за загрузки преобразователя изображений в оперативную память при запуске приложения. Вторая проблема - медленный ответ сервера на запросы новых классификаторов. Эту проблему можно решить, перенеся вычисления в облако (я думаю о сервисе AWS-lambda), но сейчас мне сложно оценить стоимость AWS. Мне, вероятно, следует ограничить ежедневные запросы к серверу для каждого пользователя или взимать плату с пользователей за расширение лимита, чтобы покрыть расходы AWS и обеспечить лучший UX. Третья проблема возникла сегодня - нестабильный доступ к серверу. Похоже связано с "замедлением Твиттера".

Что можно добавить

Еще подумываю добавить режим one-shot (одна фотография используется для создания классификатора). Это улучшение можно легко реализовать в приложении.

Если говорить непосредственно о перспективах CLIP - я много играл с комбинацией CLIP и BERT для генерации описания изображения на естественном языке и уже получил некоторые многообещающие результаты. Но BERT определенно нельзя запускать на телефонах, и даже на моей rtx3080 (успел купить за 80!) есть некоторые проблемы для быстрого прототипирования таких систем. Также пробовал реализовать CLIP Style transfer с описанием обработки фото естественным языком - результат пока не очень, на это следовало бы потратить больше времени. Кроме того подозреваю, что использование CLIP для покадровой обработки видео и анализа потока векторов при помощи трансформеров или LSTM (почему-то кажется, что LSTMы будут лучше работать) может привести к прорыву в нейросетевом описании видео - сейчас результаты в этой области достаточно скромные.

Спасибо!

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

Полезные Ссылки:

О Dall-e:

О CLIP

О мультимодальных нейронах CLIP

Github CLIP

Подробнее..

Виртуальные машины А2 крупнейшие облачные образы с графическими процессорами NVIDIA A100 теперь доступны для всех

20.04.2021 12:16:22 | Автор: admin

Недавно, в нашем Google Cloud блоге, мы анонсировали, что в сервисе Compute Engine появились виртуальные машины A2 на базе графических процессоров NVIDIA Ampere A100 с тензорными ядрами. С их помощью пользователи смогут выполнятьмашинное обучениеивысокопроизводительные вычисленияна базе архитектуры NVIDIA CUDA, увеличивая рабочие нагрузки за меньшее время и цену.

В этой статье, мы хотим рассказать подробнее о том, что представляют из себя виртуальные машины А2, об их производительности и особенностях. И рассказать о том, как мы используют эти машины наши коллеги и партнеры.

Высочайшая производительность

Одна ВМ A2 поддерживает до 16графических процессоров NVIDIA A100. На сегодняшний день это самый производительный экземпляр графического процессора на одном узле среди всех конкурирующих решений от крупнейших поставщиков облачных услуг. В зависимости от масштабов рабочей нагрузкивы также можете выбрать виртуальные машины A2 с меньшим числом графических процессоров (1, 2, 4 и 8).

Конфигурации ВМ A2 доступные в сервисе Compute EngineКонфигурации ВМ A2 доступные в сервисе Compute Engine

Это позволяет исследователям, специалистам по обработке данных и разработчикам значительно увеличивать производительность масштабируемых рабочих нагрузок (например, машинное обучение, логический вывод и высокопроизводительные вычисления) на архитектуре CUDA. Семейство ВМ A2 на платформе Google Cloud Platform способно удовлетворить потребности самых требовательных приложений для высокопроизводительных вычислений, например при моделировании методами вычислительной гидродинамики вAltair ultraFluidX.

Для тех, кому нужны сверхпроизводительные системы, Google Cloud предлагает кластеры из тысяч графических процессоров для распределенного машинного обучения, а также оптимизированные библиотеки NCCL для горизонтального масштабирования. Версия ВМ с 16 графическими процессорами A100, объединенными через шинуNVIDIA NVLink, это уникальное предложение Google Cloud. Если вам нужно масштабировать требовательные рабочие нагрузки по вертикали, можно начать с одного графического процессора A100 и довести их число до 16 без настройки нескольких ВМ для машинного обучения на одном узле.

Новая ВМ A2-MegaGPU: 16 графических процессоров A100 со скоростью передачи данных 9,6 ТБ/с по интерфейсу NVIDIA NVLinkНовая ВМ A2-MegaGPU: 16 графических процессоров A100 со скоростью передачи данных 9,6 ТБ/с по интерфейсу NVIDIA NVLink

Чтобы удовлетворить потребности разных приложений, доступны и менее производительные конфигурации ВМ A2 с встроенным SSD-диском на 3ТБ, который ускоряет доставку данных в графический процессор. Так, графический процессор A100 в Google Cloud более чем в 10раз увеличивает скорость предварительного обучения модели BERT-Large по сравнению с NVIDIA V100 прошлого поколения. При этом в конфигурациях с числом графических процессоров от 8 до 16 наблюдается линейный рост производительности. Кроме того, разработчики могут использовать предварительно настроенное ПО в контейнерах из хранилища NVIDIANGCдля быстрого запуска экземпляров A100 в Compute Engine.

Отзывы пользователей

Мы стали предлагать ВМ A2 с графическими процессорами A100 нашим партнерам в июле 2020 года. Сегодня мы работаем со множеством организаций и помогаем им достигать новых высот в области машинного обучения, визуализации и высокопроизводительных вычислений. Вот что они говорят о виртуальных машинах А2:

КомпаниюDessaнедавно приобрел холдинг Square. Она занимается исследованиями в сфере ИИ и стала использовать ВМ A2 одной из первых. На базе ее экспериментов и инноваций Square разрабатывает персонализированные сервисы и умные инструменты для Cash App, которые с помощью ИИ помогают неспециалистампринимать более взвешенные финансовые решения.

"Благодаря Google Cloud мы получили необходимый контроль над своими процессами, говорит Кайл де Фрейтас, старший разработчик ПО в Dessa. Мы понимали, что предлагаемые в Compute Engine ВМ A2 на базе графических процессоровNVIDIA A100с тензорными ядрами способны радикально сократить время вычислений и значительно ускорить наши эксперименты. Процессоры NVIDIA A100, используемые в Google Cloud AI Platform, позволяют нам эффективно развивать инновации и воплощать в жизнь новые идеи для наших клиентов".

Hyperconnect это международная компания, занимающаяся видеотехнологиями в сфере коммуникаций (WebRTC) и ИИ. Hyperconnect стремится объединять людей во всем мире и для этого создает сервисы на базе различных технологий обработки видео и ИИ.

"Экземпляры A2 с новыми графическими процессорами NVIDIA A100 на платформе Google Cloud поднимают производительность на совершенно новый уровень при настройке моделей глубокого обучения. Мы легко перешли на них с прошлого поколения графических процессоров V100. Благодаря конфигурации ВМ A2-MegaGPU мы не только ускорили обучение более чем в два раза по сравнению с V100, но и получили возможность масштабировать по вертикали рабочие нагрузки с большими нейронными сетями в Google Cloud. Эти инновации помогут нам оптимизировать модели и повышать удобство использования сервисов Hyperconnect", говорит Ким Бемсу, исследователь по машинному обучению в Hyperconnect.

DeepMind(дочерняя компания Alphabet) это команда ученых, инженеров, специалистов по машинному обучению и других экспертов, которые развивают технологии ИИ.

"DeepMind занимается искусственным интеллектом. Наши исследователи проводят различные эксперименты в этой сфере с применением аппаратных ускорителей. Благодаря Google Cloud мы получили доступ к новому поколению графических процессоров NVIDIA, а виртуальная машина A2-MegaGPU-16G позволяет проводить обучение моделей быстрее, чем когда-либо. Мы с радостью продолжаем работать с платформой Google Cloud, которая поможет нам создавать будущую инфраструктуру машинного обучения и ИИ", Корай Кавукчуоглу (Koray Kavukcuoglu), вице-президент DeepMind по исследовательской деятельности.

AI2 это некоммерческий исследовательский институт, занимающийся перспективными исследованиями и разработками в сфере ИИ для общего блага.

"Наша основная миссия расширение возможностей компьютеров. В связи с этим мы сталкиваемся с двумя фундаментальными проблемами. Во-первых, современные алгоритмы ИИ требуют огромных вычислительных мощностей. Во-вторых, специализированное оборудование и ПО в этой области быстро меняются. И с этим нужно что-то делать. Процессоры A100 в GCP в четыре раза производительнее наших нынешних систем, и для их использования не требуется серьезно перерабатывать программный код. По большому счету достаточно минимальных изменений. Графический процессор A100 в Google Cloud позволяет значительно увеличить количество вычислений на доллар. Соответственно, мы можем проводить больше экспериментов и использовать больше данных", говорит Дирк Груневельд, старший разработчик Allen Institute for Artificial Intelligence.

OTOY это компания, которая занимается облачными графическими вычислениями. Она развивает инновационные технологии создания и доставки контента для средств массовой информации и индустрии развлечений.

"Уже около десяти лет мы расширяем границы возможного в сфере графической визуализации и облачных вычислений и стремимся устранить ограничения для художественного творчества. Благодаря процессорам NVIDIA A100 в Google Cloud с большим объемом видеопамяти и самым высоким рейтингом OctaneBench за всю историю мы первыми достигли уровня, когда художникам при реализации своих замыслов больше не нужно задумываться о сложности прорисовки. Система визуализации OctaneRender снизила стоимость спецэффектов. Она позволяет любому разработчику с графическим процессором NVIDIA создавать великолепную картинку кинематографического качества. Виртуальные машины с процессорами NVIDIA A100 в Google Cloud предоставляют пользователям OctaneRender и RNDR доступ к современным графическим процессорам NVIDIA, прежде доступным только для крупнейших голливудских студий", говорит Джулз Урбах, основатель и генеральный директор OTOY.

Цены и доступность графических процессоров

Экземпляры NVIDIA A100 теперь доступны в следующих регионах: us-central1, asia-southeast1 и europe-west4. В течение 2021года к ним добавятся дополнительные регионы. ВМ A2 в Compute Engine доступны по запросу со скидкой за вытесняемые экземпляры и обязательство по использованию, а также полностью поддерживаются в Google Kubernetes Engine (GKE), Cloud AI Platform и других сервисах Google Cloud. A100 предлагаются по цене всего 0,87доллара США за один графический процессор в вытесняемых ВМ A2. С полным прейскурантом можно ознакомитьсяздесь.

Начало работы

Вы можете быстро развернуть работу, приступить к обучению моделей и выполнять рабочие нагрузки с логическим выводом на графических процессорах NVIDIA A100 с помощьюобразов ВМ для глубокого обученияв доступных регионах. В этих образах собрано все необходимое ПО: драйверы, библиотеки NVIDIA CUDA-X AI и популярные фреймворки для ИИ, такие как TensorFlow и PyTorch. Оптимизированныеобразы TensorFlow Enterpriseтакже включают поддержку A100 для текущих и прошлых версий TensorFlow (1.15, 2.1 и 2.3). Вам не нужно беспокоиться об обновлении ПО, совместимости и настройке производительности всё это мы берем на себя. Наэтой страницеприводятся сведения о доступных в Google Cloud графических процессорах.


Напоминаем что при первой регистрации в Google Cloud: вам доступны бонусы на сумму 300 долларов США, а более 20 бесплатных продуктов доступны всегда. Подробнее поспециальной ссылке.

А так же выражаем благодарность за помощь в подготовке материала коллегам: Бхарат Партасарати, Крис Клебан и Звиад Кардава

Подробнее..

Перевод Обманываем нейросети при помощи шума

10.08.2020 14:22:42 | Автор: admin


Меня всегда занимали отказы систем и странности их поведения, в особенности когда те работают в нормальных для себя условиях. Недавно я видел один из слайдов презентации Йена Гудфелло, показавшийся мне очень смешным. Случайный визуальный шум скормили обученной нейросети, и она распознала его как один из известных ей объектов. Тут сразу появляется много вопросов. Будут ли видеть один и тот же объект разные обученные нейросети? Каков максимальный уровень уверенности нейросети в том, что этот случайный шум действительно является распознанным ею объектом? И что на самом деле видит там нейросеть?

Из моего любопытства по этому поводу и родилась данная запись. К счастью, подобные эксперименты очень легко проводить при помощи PyTorch. Для визуализации того, почему нейросеть классифицирует объекты определённым образом, я использую платформу интерпретируемости модели Captum. Код можно скачать с Github.

Важность вопросов


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

Предварительно обученные модели


С предварительно обученными моделями легко начать работать, и им можно быстро отправить данные на классификацию. В таком случае вам не надо определять модели и обучать их всё это сделано уже до вас, и они готовы к использованию сразу после развёртывания. Предварительно обученные модели из библиотеки Torchvision обучены на наборе изображений из базы Imagenet, разбитых на 1000 категорий. Важно помнить, что это обучение предполагало определение единственного объекта на картинке, а не разбор сложных изображений, содержащих различные объекты. Во втором случае тоже можно получать интересные результаты, однако это уже совершенно другая тема. Скачать предварительно обученные модели из библиотеки Torchvision очень легко. Нужно просто импортировать выбранную модель, установив параметр pretrained в True. Я также включил оценочный режим в модели, поскольку во время тестов не предполагается вести обучение.

Сначала у меня идёт сточка кода, выбирающая использование cuda или cpu, в зависимости от того, есть ли в наличии GPU. Для таких простых тестов GPU не обязателен, однако поскольку он у меня есть, я его использую.

device = "cuda" if torch.cuda.is_available() else "cpu"import torchvision.models as modelsvgg16 = models.vgg16(pretrained=True)vgg16.eval()vgg16.to(device)

Со списком предварительно обученных моделей из Torchvision можно ознакомиться по ссылке. Я не хотел использовать все предварительно обученные нейросети, это уже чересчур. Я выбрал пять следующих:

  • vgg16
  • resnet18
  • alexnet
  • densenet
  • inception

Какой-то особой методологией для выбора нейросетей я не пользовался. Например, Vgg16 и Inception часто используются в разных примерах, кроме того, все эти сети разные.

Создаём изображения с шумом


Нам понадобится способ автоматической генерации изображений, содержащих шум, которые можно скармливать нейросети. Для этого я использовал комбинацию библиотек Numpy и PIL, и написал небольшую функцию, возвращающую заполненное случайным шумом изображение.

import numpy as npfrom PIL import Image def gen_image():    image = (np.random.standard_normal([256, 256, 3]) * 255).astype(np.uint8)    im = Image.fromarray(image)         return im  

В итоге получается что-то вроде следующего:



Преобразование изображений


После этого нам нужно преобразовать наши изображения в тензор и нормализовать их. Следующий код можно использовать не только на случайном шуме, но и на любом изображении, которое мы захотим скормить предварительно обученным нейросетям (поэтому в коде и используются значения Resize и CenterCrop).

def xform_image(image):         transform = transforms.Compose([transforms.Resize(256),                                    transforms.CenterCrop(224),                                    transforms.ToTensor(),                                    transforms.Normalize([0.485, 0.456, 0.406],                                                         [0.229, 0.224, 0.225])])         new_image = transform(image).to(device)    new_image = new_image.unsqueeze_(0)         return new_image

Получаем предсказания


Подготовив преобразованные изображения, легко получить предсказания от развёрнутой модели. В данном случае предполагается, что функция xform_image возвращает image_xform. В коде, который я использовал для тестирования, я разделил работу между двумя этими функциями, однако здесь для простоты восприятия я составил их вместе. Нам, по сути, нужно скормить преобразованное изображение сети, запустить функцию softmax, использовать функцию topk для получения оценки и идентификатор предсказанной метки для наилучшего результата.

with torch.no_grad():    vgg16_res = vgg16(image_xform)    vgg16_output = F.softmax(vgg16_res, dim=1)    vgg16score, pred_label_idx = torch.topk(vgg16_output, 1)

Результаты


Ну вот, теперь мы представляем, как генерировать содержащие шум изображения и скармливать их предварительно обученной сети. Так какие получаются результаты? Для этого теста я решил сгенерировать 1000 изображений с шумом, прогнал их через 5 выбранных предварительно обученных сетей и запихнул в датафрейм Pandas для быстрого анализа. Результаты оказались интересными и немного неожиданными.
vgg16 resnet18 alexnet densenet inception
count 1000.000000 1000.000000 1000.000000 1000.000000 1000.000000
mean 0.226978 0.328249 0.147289 0.409413 0.020204
std 0.067972 0.071808 0.038628 0.148315 0.016490
min 0.074922 0.127953 0.061019 0.139161 0.005963
25% 0.178240 0.278830 0.120568 0.291042 0.011641
50% 0.223623 0.324111 0.143090 0.387705 0.015880
75% 0.270547 0.373325 0.171139 0.511357 0.022519
max 0.438011 0.580559 0.328568 0.868025 0.198698

Как видите, некоторые из нейросетей решили, что этот шум на самом деле изображает что-то конкретное с довольно высоким уровнем уверенности. У resnet18 и densenet максимальные значения достигли 50%. Это всё хорошо, однако что именно эти сети видят в шуме? Интересно, что разные сети находили там разные объекты.

Vgg16:
стола 978
медуза 14
коралловый риф 7
пончо 1

Resnet18:
медуза 1000

Alexnet:
пончо 942
тряпка для посуды 58

Densenet:
сетка рабица 893
оконная сетка 37
кольчуга 33
дверной коврик 20
черепичная крыша 16
электрокамин 1

Inception:
переключатель 155
сорока 123
пазл 102
подушка 85
джинсовая ткань 83
американский зяблик 81
скворечник 69
соты 32
пончо 26
картон 25
мышеловка 24
саронг 18
кукуруза 16
кольчуга 16
вакуум 12
оконная сетка 12
кардиган 11
американская цапля 9
брокколи 9
кошелек 8
песочница 7
щурок 5
эскимо 5
посудомоечная машина 5
рыба-молот 5
сетка рабица 4
пасека 4
гвоздь 4
бочка 4
мусорка 3
трикотаж 3
нагрудник 3
малая голубая цапля 3
таракан 3
конверт 2
скат 2
занавеска для душа 2
фартук 2
морская звезда 2
мини юбка 1
варежка 1
итальянская гончая 1
спичка 1
цемент 1
громкоговоритель 1
ведро 1
ухо 1
обувной магазин 1
носовой платок 1
лоток 1
трость 1
толстовка 1
тарелка 1
сороконожка 1
кимоно 1


Каждая из сетей увидела что-то своё. Resnet18 была на 100% уверена, что это медуза, а у Inception наоборот, уверенности в предсказаниях было очень мало, хотя при этом она увидела гораздо больше объектов, чем любая другая сеть.

Просто для интереса я решил посмотреть, какую подпись поставит Microsoft под изображением шума, которое я привёл ближе к началу данной записи. Для теста я решил пойти наипростейшим путём, и использовал PowerPoint из Office 365. Результат получился интересным, поскольку в отличие от моделей, работающих с imagenet, которые пытаются распознать единственный объект, PowerPoint пытается распознать несколько объектов, чтобы создать точное описание изображения.

На изображении представлены слон, люди, большой, мяч.

Результат меня не разочаровал. С моей точки зрения, изображение шума было распознано как цирк.

Перспективы


Это приводит нас к другому вопросу что такого видит нейросеть, что заставляет её думать, будто шум это объект? В поисках ответа мы можем использовать инструмент для интерпретирования модели, который позволит нам примерно понять, что видит сеть. Captum это платформа интерпретации моделей для PyTorch. Тут я не делал ничего особенного, а просто использовал код из обучающих материалов с их веб-сайта. Я только добавил параметр internal_batch_size со значением 50, поскольку без него на моём GPU очень быстро кончалась память.

Для визуализаций я использовал две градиентных атрибуции [gradient based attributions] и заграждающую атрибуцию [occlusion based attribution]. С помощью этих визуализаций мы пытаемся понять, что было важным для классификатора, и, следовательно, увидеть то, что видит сеть. Также я использовал свою предварительно обученную модель resnet, однако вы можете изменить код и использовать любые другие предварительно обученные модели.

Перед тем, как перейти к шуму я взял изображение ромашки в качестве демонстрации процесса визуализации, поскольку её признаки легко распознать.

result = resnet18(image_xform)result = F.softmax(result, dim=1)score, pred_label_idx = torch.topk(result, 1) integrated_gradients = IntegratedGradients(resnet18)attributions_ig = integrated_gradients.attribute(image_xform, target=pred_label_idx,                                                  internal_batch_size=50, n_steps=200) default_cmap = LinearSegmentedColormap.from_list('custom blue',                                                  [(0, '#ffffff'),                                                  (0.25, '#000000'),                                                  (1, '#000000')], N=256) _ = viz.visualize_image_attr(np.transpose(attributions_ig.squeeze().cpu().detach().numpy(), (1,2,0)),                             np.transpose(image_xform.squeeze().cpu().detach().numpy(), (1,2,0)),                             method='heat_map',                             cmap=default_cmap,                             show_colorbar=True,                             sign='positive',                             outlier_perc=1)



noise_tunnel = NoiseTunnel(integrated_gradients) attributions_ig_nt = noise_tunnel.attribute(image_xform, n_samples=10, nt_type='smoothgrad_sq', target=pred_label_idx, internal_batch_size=50)_ = viz.visualize_image_attr_multiple(np.transpose(attributions_ig_nt.squeeze().cpu().detach().numpy(), (1,2,0)),                                      np.transpose(image_xform.squeeze().cpu().detach().numpy(), (1,2,0)),                                      ["original_image", "heat_map"],                                      ["all", "positive"],                                      cmap=default_cmap,                                      show_colorbar=True)



occlusion = Occlusion(resnet18) attributions_occ = occlusion.attribute(image_xform,                                       strides = (3, 8, 8),                                       target=pred_label_idx,                                       sliding_window_shapes=(3,15, 15),                                       baselines=0) _ = viz.visualize_image_attr_multiple(np.transpose(attributions_occ.squeeze().cpu().detach().numpy(), (1,2,0)),                                      np.transpose(image_xform.squeeze().cpu().detach().numpy(), (1,2,0)),                                      ["original_image", "heat_map"],                                      ["all", "positive"],                                      show_colorbar=True,                                      outlier_perc=2,                                     )



Визуализация шума


Мы сгенерировали предыдущие изображения на основе ромашки, и теперь пора посмотреть, как всё работает со случайным шумом.

Я использую предварительно обученную сеть resnet18, и с данным изображением она на 40% уверена, что видит медузу. Код повторять не буду, код для визуализации такой же, как и тот, что приведён выше.









Из визуализаций видно, что нам, людям, так и не удастся понять, почему сеть видит тут медузу. Некоторые области изображения отмечены, как более важные, однако они совсем не такие определённые, как мы видели в примере с ромашкой. В отличие от ромашки, медузы аморфные и отличаются уровнем прозрачности.

Возможно, вам интересно, как будет выглядеть визуализация обработки реального изображения медузы? Мой код выложен на Github, и с его помощью на этот вопрос будет легко получить ответ.

Заключение


На основе этой записи легко видеть, насколько просто можно обмануть нейросети, скармливая им неожиданные входные данные. К их чести скажем, что работу свою они сделали и выдали наилучший результат из тех, на что способны. Также из результатов работы можно увидеть, что в таких случаях недостаточно просто отфильтровать варианты с малой уверенностью, поскольку у некоторых вариантов уверенность была довольно высокой. Нам нужно внимательно следить за ситуациями, в которых системы, работающие в реальных условиях, так легко идут в отказ. Нас не должно заставать врасплох попадание на вход системе неожиданных данных а именно этим уже довольно долгое время занимаются специалисты по безопасности.
Подробнее..

Играемся с Новыми GPU на базе Ampere от Nvidia и пробуем MIG

02.12.2020 16:10:54 | Автор: admin


Каждый раз, когда встает заветный вопрос, апгрейдить ли карточки в серверной или нет, я просматриваю подобные статьи и смотрю такие видосы (нет, маркетинговым материалам от Nvidia конечно верить нельзя, как показал недавний кейс с числом CUDA-ядер).


Канал "Этот Компьютер" очень сильно недооценен, но автор не занимается ML. А в целом при анализе сравнений акселераторов для ML в глаза как правило бросаются несколько вещей:


  • Авторы учитывают как правило только "адекватность" для рынка новых карт в США;
  • Рейтинги далеки от народа и делаются на весьма стандартных сетках (что наверное в целом хорошо) без деталей;
  • Популярная мантра тренировать все более гигантские сетки вносит свои коррективы в сравнения;

Не нужно быть семи пядей во лбу, чтобы знать очевидный ответ на вопрос "а какая карта лучше?": карточки серии 20* в массы не пошли, 1080 Ti с Авито до сих очень привлекательны (и не особо дешевеют как ни странно, вероятно по этой причине).


Все это прекрасно и вряд ли стандартные бенчмарки сильно врут, но недавно я узнал про существование технологии Multi-Intance-GPU для видеокарт А100 и нативную поддержку TF32 и мне пришла идея поделиться своим опытом реального тестирования карточек на архитектуре Ampere (3090 и А100). В этой небольшой заметке я постараюсь ответить на вопросы:


  • Стоит ли свеч обновление на Ampere? (спойлер для нетерпеливых да);
  • Стоят ли своих денег A100 (спойлер в общем случае нет);
  • Есть ли кейсы, когда A100 все-таки интересны (спойлер да);
  • Полезна ли технология MIG (спойлер да, но для инференса и для очень специфичных случаев для обучения);

За деталями прошу под кат.


Простые Вещи


Давайте сразу обратим внимание на слона в комнате. На момент написания этой заметки:


  • 3090 довольно сложно купить и продаются они примерно с 30-40% премией. Причем нехватки новых карт есть не только в СНГ;
  • A100 почти невозможно купить. Партнеры Nvidia говорили что в РФ наличии есть 1 штука, потом приедет еще несколько штук;
  • Я не особо искал, но с наскоку я не нашел информации насколько PCIE версия A100 совместима с обычными ATX платформами (именно на этот вопрос партнеры Nvidia не ответили, но я предполагаю, что в картах нет своего кулера и предполагается установка в серверное шасси с "громким" феном);
  • 3080 и более младшие модели (хотя они очень интересны по цене, а особенно для игр) не тестировали, т.к. у нас их нет, а не рассматривали их из-за размера памяти (я наивно предполагал, что получится крутить несколько сеток на 1 карте, но там все работает несколько иначе);

По понятным причинам холивор кормить или не кормить Амазон выносим за скобки. Уверен, что среди комментаторов найдутся свидетели церкви Амазона, но свой риг как правило "окупается" против облачных цен (особенно если собирать из дешевых компонентов) где-то за полгода-год использования.


Охлаждение


Если верить утилитам от Nvidia, то 3090 и А100 на 15-20 градусов холоднее, чем Maxwell и Pascal. Я не проводил точные замеры, но в среднем ситуация такая:


  • 4 * 1080 Ti (Pascal) с минимальными хаками по охладжению работают в диапазоне 75-80С под 100% нагрузкой;
  • 3 * Titan X (Maxwell) работали в районе 85С под 100% нагрузкой;
  • 3 * 3090 (Ampere) работают в диапазоне 60-70С под 100% нагрузкой;
  • Нигде не применялся ни разгон, ни ограничения по питанию карт или скорости кулеров, все "из коробки";
  • Все карты имеют "турбину", то есть выталкивают тепло из корпуса;

На вопрос "почему" есть 3 гипотезы:


  • Новый техпроцесс;
  • У 3090 немного другая форма самой карты, размер вентилятора заметно больше, размер отверстия на задней панельке сильно больше;
  • 3090 кажется тяжелее (может кто-то знает где найти точные цифры, карт нет под рукой сейчас);


Наглядная иллюстрация отличий карточек, может кто-то из комментариев подскажет диаметр вентилятора?

Наивные Метрики


Сначала, чтобы удостовериться, что драйвера работают верно (а когда они работали неверно, цифры были другие совсем), давайте протестируем все доступные карточки с помощью gpu-burn. Получается такая картина, которая очень сильно коррелирует с тем, что рисуют в обзорах.


Test GPU Gflop/s
./gpu_burn 120 Titan X (Maxwell) 4,300
./gpu_burn 120 1080 Ti (Pascal) 8,500
./gpu_burn 120 3090 (Ampere) 16,500
./gpu_burn 120 A100 (wo MIG) 16,700
./gpu-burn -tc 120 3090 (Ampere) 38,500
./gpu-burn -tc 120 A100 (wo MIG) 81,500

MIG не тестировался тут, дальше в статье увидите почему.


Цена Вопроса


Тут важно отметить, что 1080 Ti и Titan X мы покупали с рук условно "новые" (менее года использования). Не будем останавливаться лишний раз на холиворах про майнеров и политику ценообразования Nvidia, но если бережно использовать даже б/у игровые карты их срок службы где-то 3-4 года. Цены и характеристики указаны примерные. A100 по информации от партнеров Nvidia в России в продаже имеется до нового года одна. Когда 1080Ti были доступны новыми, цены колебались примерно от 50к до 100к рублей.


GPU Mem Цена
Titan X (Maxwell) 12G 10,000 рублей (Авито)
1080 Ti 11G 25,000 рублей (Авито)
3090 (Ampere) 24G 160,000+ рублей (новая)
A100 (wo MIG) 40G US$12,500 (новая)

Думаю очевидные выводы дальше читатели сделают сами.

Пробуем 3090 и A100 c MIG


Пробуем 3090


А теперь переходим к самому интересному к реальным тестам и собираем грабли на реальных задачах. В теории кажется, что если по памяти и вычислительным способностям 3090 или А100 в 2-3 раза превосходят 1080 Ti, то 1 такая карточка может заменить 2-3 1080 Ti и на стандартном сервере с 4 полноценными PCIE портами можно получить аналог сервера с 12 картами? Ну или можно ли взять допустим 3-4 PCIE версии A100 и получить очень мощный сервер, разделив каждую из них на несколько compute instance с использованием MIG?


Короткий ответ нет, более длинный ответ тоже нет, но с многочисленными оговорками.


Зачем спросите? Да просто серверные решения, которые полноценно поддерживают 8 16 видеокарт даже в минимальной разумной конфигурации по цене выходят в 4-5 раз дороже, чем стандартные ATX решения. А DGX Workstation или DGX продаются еще с примерно 50% премией к своей аналогичной сборке, если собирать на базе платформ от Mikrotik или Gigabyte.


Производители карт не торопятся выпускать полноценные однослотовые решения (кроме PNY c серией Quadro, но это отдельная история и скорее для дизайна или инференса). Конечно можно собрать кастомный водяной контур на 7 карточек (было несколько моделей материнских плат с 7 полноценными PCIE портами), но это "сложно" и неясно где такое размещать (да и игра не стоит свеч). С приходом PCIE 4.0 привлекательность таких решений по идее должна вырасти, но я пока не видел ничего интересного на рынке.


Пара огоровок про задачу на которой тестировали:


  • Задача тренировка Spech-to-Text сетки на украинском датасете;
  • Из-за самой задачи экспериментально оптимальный размер батча на один процесс 50 не получается увеличивать без потерь в скорости сходимости;
  • Именно на этой задаче AMP у нас не работает (хотя работает на других при прочих равных, мы пока не поняли почему), но это скорее оптимизация. То есть дело не в железе, а в задаче. На других задачах работает, поэтому вынесем за скобки;
  • Важая оговорка поскольку по сути эта задача это sequence-to-sequence, то в общем случае построение батчей тут не совсем тривиально. Файлы разной длины попадают в батч только с файлами примерно такой же длины (чтобы снизить впустую потраченные ресурсы на обработку падинга), но размер батча статический для упрощения сравнений и более быстрой сходимости;
  • Динамический размер батча и просто его увеличение тестировали, но это особо не влияет на скорость и скорость сходимости (или ухудшает);

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


И тут мы наталкиваемся на первый подводный камень Distributed Data Parallel из PyTorch (DDP, оптимальный способ масштабирования сетей на "много" видеокарт) из коробки по сути настроен только на 1 процесс на 1 карте. То есть 1 процесс может использовать 1+ карту. 2 процесса не могут использовать 1 карту, даже если там есть большой запас по IO / compute / RAM. В старых версиях драйверов явного ограничения нет и на 1080 Ti 2 процесса на 1 карта запускаются (но получется прирост по скорости всего на 5-10% вместо 40-50%). На новых картах туда уже впилили exception.


RuntimeError: NCCL error in: /opt/conda/conda-bld/pytorch_1603729096996/work/torch/lib/c10d/ProcessGroupNCCL.cpp:784, invalid usage, NCCL version 2.7.8

Но не все так печально и плохо. Может из-за какой-то низкоуровневой магии в драйверах, может из-за TF32 (надеюсь тут знатоки подскажут), может из-за наработок в MPS 3090 ведут себя немного иначе на нашем бенчмарке:


  • При прочих равных и неизменных параметрах они используют больше памяти чем Titan X и 1080 Ti (~16 GB вместо 7-8 GB);
  • Скорость примерно в 3 раза выше, чем с Titan X (Maxwell);
  • [Нужно еще точно замерить скорость на 1080 Ti];
  • Утилизация карт на высоком уровне более 90%;

При попытках запускать 2 DDP воркера на 1 карте мы просто получаем ошибку, при попытке тренировать 2 сетки "одновременно" мы получаем кратное замедление, при увеличении батча прирост по скорости незначительный. Тайминги на 2 * 3090 примерно такие:


| Epoch   time, m | Type | Workers | Batch   | Params               ||-----------------|------|---------|---------|----------------------|| exception       | DDP  | 4       | 50 * 4  |                      || 3.8             | DDP  | 2       | 50 * 2  |                      || 3.9             | DDP  | 2       | 50 * 2  | cudnn_benchmark=True || 3.6             | DDP  | 2       | 100 * 2 |                      |

Для полноты рассказа, важно еще отметить что у Nvidia есть MPS который якобы позволяет крутить 2 процесса на картах без переключения контекста а в PyTorch есть встроенный RPC-фреймворк. Но первый я просто не смог адекватно использовать без очень непонятных низкоуровневых ошибок, а второй требует радикального переписывания кода и радикально усложняет код для тренировки моделей (хотя очень интересен долгосрочно).


Так, с 3090 все понятно. Две карточки она не заменит конечно, но сама по себе, даже имея "лишнюю" память (повторюсь, мы тренируем маленькие сети), работает в 2-3 раза быстрее. Эквивалентно ли это наличию 2-3 карт, зависит от задачи.


TLDR:


  • Вы можете просто заменить карты с турбиной в своем риге на 3090 (единственный момент в 3090 2 8-пиновых коннектора для питания, но на рынке есть блоки питания по 2000-Ватт которые точно могут запитать 4-5 таких карт, опять же никто не отменял синхронизацию 2 блоков питания);
  • При это скорее всего температура карт снизится на 10-20 градусов Цельсия;
  • Эти карты сейчас стоят дорого и находятся в дефиците (и, наверное вряд ли пойдут в массы), но если для вас самый дорогой ресурс это время то это интересный вариант;
  • Если большой размер памяти для вас критичен у вас по сути нет выбора;

Пробуем A100 с MIG


Посмотрев на метрики, доступность и цену карт, А100 на первый взгляд вообще не кажется интересным вариантом, разве что в облаке на 3 дня натренировать 1 большую сетку на небольшом не сильно приватном датасете. Также если вашим алгоритмам сильно помогает AMP / FP16, то А100 может существенно добавить скорости.


Но в A100 есть интересная технология MIG (Multi Instance GPU). По сути она позволяет разбить одну "большую и мощную" карточку на набор маленьких "подкарточек" и дальше создать виртуальные Compute Instances, к которым можно обращаться как к отдельным картам.


Там довольно много деталей, за ними откройте документацию, но там доступны такие пресеты:


+--------------------------------------------------------------------------+| GPU instance profiles:                                                   || GPU   Name          ID    Instances   Memory     P2P    SM    DEC   ENC  ||                           Free/Total   GiB              CE    JPEG  OFA  ||==========================================================================||   0  MIG 1g.5gb     19     0/7        4.75       No     14     0     0   ||                                                          1     0     0   |+--------------------------------------------------------------------------+|   0  MIG 2g.10gb    14     0/3        9.75       No     28     1     0   ||                                                          2     0     0   |+--------------------------------------------------------------------------+|   0  MIG 3g.20gb     9     0/2        19.62      No     42     2     0   ||                                                          3     0     0   |+--------------------------------------------------------------------------+|   0  MIG 4g.20gb     5     0/1        19.62      No     56     2     0   ||                                                          4     0     0   |+--------------------------------------------------------------------------+|   0  MIG 7g.40gb     0     0/1        39.50      No     98     5     0   ||                                                          7     1     1   |+--------------------------------------------------------------------------+


Доступные конфигурации

Возникает вопрос, а что если наша сетка маленькая, и A100 в теории (хотя бы на FP16) должна быть в 2 раза мощнее чем 3090? Можно ли взять 4 A100 и сделать из них допустим 12 видеокарт аналогичных по памяти и мощности 1080 Ti? Можно ли на этих многочисленных "микро-картах" тренировать нейросети так же как на нескольких обычных?


Ответим на вопросы по одному. Тут нам поможет как сама документация, так и совсем свежий блог пост от самой Nvidia.


В документации есть такой абзац:


MIG supports running CUDA applications by specifying the CUDA device on which the application should be run. With CUDA 11, only enumeration of a single MIG instance is supported.CUDA applications treat a CI and its parent GI as a single CUDA device. CUDA is limited to use a single CI and will pick the first one available if several of them are visible. To summarize, there are two constraints:- CUDA can only enumerate a single compute instance- CUDA will not enumerate non-MIG GPU if any compute instance is enumerated on any other GPUNote that these constraints may be relaxed in future NVIDIA driver releases for MIG.

Сначала, когда я его прочитал, мне показалось, что он означал только что нельзя распилить 2 карты, а можно использовать только одну. После того, как я попробовал поиграться с реальной картой, оказалось, что фреймворк внутри контейнера видит только 1 "карту" (причем видимо выбирает он только "первую"). Причем если мы внимательно прочитаем те примеры, которые Nvidia приводит в своем блоге, они по сути все относятся к сценарию "1 контейнер 1 кусочек карты" или "тюнинг 7 маленьких моделей параллельно".


Еще там есть вот такой пассаж:


There is no GPU-to-GPU P2P (both PCIe and NVLINK) support in MIG mode, so MIG mode does not support multi-GPU or multi-node training. For large models or models trained with a large batch size, the models may fully utilize a single GPU or even be scaled to multi-GPUs or multi-nodes. In these cases, we still recommend using a full GPU or multi-GPUs, even multi-nodes, to minimize total training time.

Если использовать MIG по прямому назначению, то есть делить карту на физические кусочки (slices), назначать им Compute Instances и прокидывать их в изолированные контейнеры то все работает как надо. It just works.


Итоговые Замеры


Тут не совсем идеальные сравнения (на Титане у меня был DP а не DDP), да и на A100 в итоге я не стал гонять эксперименты на 10, 20, 30 часов и впустую (зачем греть атмосферу), но я замерил время на 1 эпохе.



Когда крутишь 1 сетку на A100 утилизация не достигает даже и половины ну то есть если бы ее можно было распилить на 2-3 карты, все было бы прекрасно

Avg epoch time, m Workers Batch GPUs CER @10 hours CER @20 h CER @30 h Comment
4.7 2, DDP 50 * 2 2 * 3090 14.4 12.3 11.44 Close to 100% utilization
15.3 1, DP 50 2 * Titan X 21.6 17.4 15.7 Close to 100% utilization
11.4 1, DDP 50 * 1 1 * A100 NA NA NA About 35-40% utilization
TBD 2, DDP 50 * 2 2 * 1080 Ti TBD TBD TBD

На 1080 Ti ресурсы были только чтобы прогнать 1 эпоху.


Выводы


Выводы про 3090:


  • Если вынести за скобки вопрос доступности, то апгрейд стоит делать. Вы получите минимум x2 по скорости. Если у вас работает AMP то может даже и все x3-x4;
  • С учетом роста производительности, цена кажется немного завышенной, но не заоблачной. Понижение цены где-то на 30-40% как мне кажется было бы адекватным;
  • Когда выходило новое поколение карт все беспокоились насчет охлаждения. Она на удивление холодная;
  • Единственная беда карточка просит 2 8-пиновых коннектора для питания;

Выводы про A100:


  • Если судить по цене деленной на производительность, карта не очень интересная (наценка в 2-3 раза против 3090);
  • То, что Nvidia сделала технологию для эффективного использования для инференса это круто, а то карты стали уж слишком большими и крутыми;
  • Если вы можете использовать обычные игровые карты (те же 1080 Ti или PNY Quadro) для инференса, то они представляют сильно больший value for money;
  • Есть большой нераскрытый потенциал в развитии технологии MIG;
  • Если вам нужно реально 40 GB памяти и много compute, то альтернатив особо нет;
  • Неясен вопрос с установкой PCIE версии в обычные ATX корпуса без кастома, "колхоза" или воды ;
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru