Сериал HBO «Кремниевая долина» выпустил настоящее приложение ИИ, которое распознаёт хотдоги и не-хотдоги, как приложение в четвёртом эпизоде четвёртогого сезона (приложение сейчас доступно для Android, а также для iOS!)
Чтобы добиться этого, мы разработали специальную нейронную архитектуру, которая работает непосредственно на вашем телефоне, и обучили её с помощью TensorFlow, Keras и Nvidia GPU.
Хотя практическая польза от него смехотворна, приложение является доступным примером как глубинного обучения, так и краевых вычислений. Вся работа ИИ на 100% обеспечивается пользовательским устройством, а изображения обрабатываются, не покидая телефона. Это даёт мгновенный отклик (не нужно обмениваться данными с облаком), офлайновую доступность и лучшую приватность. Это также позволяет нам поддерживать работу приложения по цене $0, даже с миллионами пользователей, что представляет собой значительную экономию по сравнению с традиционными облачными подходами к ИИ.
Компьютер разработчика с подключенным eGPU для обучения ИИ приложения Not Hotdog
Приложение было разработано киностудией специально для сериала одним разработчиком, на одном ноутбуке с подключенным GPU, используя отобранные вручную данные. В этом отношении оно может служить демонстрацией, чего можно достичь сегодня с ограниченным временем и ресурсами, нетехнической компанией, индивидуальными разработчиками и тому подобными любителями. В таком духе эта статья пытается обеспечить детальный обзор шагов, которые другие могут повторить, чтобы создать свои собственные приложения.
Приложение
Если вы не смотрели сериал и не испытывали приложение (а следовало бы!), оно делает фотографию, а потом высказывает вам своём мнение, изображён на фотографии хотдог или нет. Это прямолинейное использование, которое отдаёт дань последним исследованиям в области ИИ и приложениям, в частности, ImageNet.
Хотя мы, вероятно, выделили больше программистских ресурсов на распознавание хотдогов, чем кто-либо в мире, но приложение всё равно иногда ошибается ужасным и/или утончённым способом.
И наоборот, временами оно способно распознавать хотдоги в сложных ситуациях… Как пишет Engadget, «Это невероятно. За 20 минут я успешно распознал больше еды с этим приложением, чем я пометил и распознал песен с Shazam за последние два года».
От прототипа к продакшну
Когда-нибудь читая Hacker News вы ловили себя на такой мысли: «Они привлекли $10 млн в первом раунде за это? Я могу разработать такое за выходные!» Это приложение, вероятно, вызовет у вас такие же чувства. В конце концов, первоначальный прототип был действительно создан за выходные, при помощи Google Cloud Platform Vision API и React Native. Но для создания окончательной версии приложения, которая в итоге вошла в каталог приложений, потребовались месяцы дополнительной работы (по несколько часов в день), чтобы сделать осмысленные улучшения, которые трудно оценить со стороны. Мы потратили недели на оптимизацию общей точности, времени обучения, времени анализа, мы пробовали разные установки и инструменты, чтобы ускорить разработку, и потратили целые выходные на оптимизацию пользовательского интерфейса с учётом разрешений iOS и Android (даже не начинайте говорить на эту тему).
Слишком часто технические блоги и научные статьи пропускают эту часть, предпочитая сразу показывать окончательную версию. Но чтобы помочь другим извлечь уроки из наших ошибок и действий, мы представим сокращённый список подходов, которые для нас не сработали, прежде чем опишем окончательную архитектуру, к которой пришли в итоге.
V0: Прототип
Пример изображения и соответствующей выдачи API из документации Google Cloud Vision
Для создания прототипа мы выбрали React Native, потому что он предоставляет простую песочницу для экспериментов и помогает быстро обеспечить поддержку многих устройств. Опыт оказался успешным, и мы оставили React Native до конца проекта: он не всегда упрощал процесс, а дизайн приложения пришлось целенаправленно ограничить, но в конце концов React Native сделал свою работу.
От ещё одного основного компонента, который использовали для прототипа — Google Cloud Vision API — мы быстро отказались. Есть три основные причины:
- Первая, и самая главная, состоит в том, что точность распознавания хотдогов была не очень. Хотя он отлично справляется с распознаванием самых разных объектов, но не очень хорошо распознаёт одну конкретную вещь, и были различные примеры довольно общего характера, где сервис плохо проявил себя во время наших экспериментов в 2016 году.
- По своей природе облачный сервис всегда будет медленнее, чем нативное выполнение на устройстве (сетевой лаг больно бьёт!), и не работает в офлайне. Идея передачи изображений за пределы устройства потенциально имеет юридические последствия и последствия для приватности.
- В конце концов, если приложение станет популярным, то работа в Google Cloud может влететь в копеечку.
По этой причине мы начали экспериментировать с тем, что модно называть краевыми вычислениями (“edge computing”). В нашем случае это означает, что после обучения нашей нейронной сети на ноутбуке мы перенесём её напрямую в мобильное приложение, так что фаза исполнения нейросети (или вывода заключения) будет выполняться непосредственно на телефоне пользователя.
V1: TensorFlow, Inception и переобучение
Благодаря счастливой встрече с Питом Варденом из группы разработки TensorFlow мы узнали, что можно запускать TensorFlow напрямую встроив его в устройство iOS, и начали экспериментировать в этом направлении. После React Native, TensorFlow стал второй неотъемлемой частью нашего стека.
Потребовался всего один день работы, чтобы интегрировать пример камеры TensorFlow’s Objective-C++ в нашу оболочку React Native. Гораздо дольше заняло освоить скрипт для обучения, который помогает переобучить архитектуру Inception для работы с более специфичными задачами машинного зрения. Inception — это название семейства нейронных архитектур, созданных Google для распознавания изображений. Inception доступна как предобученная система, то есть этап обучения завершён и веса установлены. Чаще всего нейросети распознавания изображений обучаются на ImageNet, ежегодном конкурсе на поиск лучшей нейронной архитектуры, в котором распознаётся более 20 000 различных типов объектов (и хотдоги среди них). Однако, также как и Google Cloud Vision API, конкурс поощряет максимально большое количество распознавааемых объектов, а изначальная точность для одного конкретного объекта из более чем 20 000 не очень высока. По этой причине переобучение (его также называют «передачей обучения», “transfer learning”) нацелено на то, чтобы взять полностью обученную нейросеть и переобучить её для лучшего выполнения конкретной задачи, с которой вы работаете. Это обычно включает в себя некоторую степень «забывания» либо через иссечение целых слоёв из стека, либо медленно стирая способность нейросети различать отдельные типы объектов (например, стулья) ради большей точности в распознавании того объекта, который нужен вам (например, хотдоги).
Хотя нейросеть (в данном случае Inception) могла обучаться на 14 миллионах изображений ImageNet, мы смогли переобучить её всего на нескольких тысячах фотографий хотдогов, чтобы кардинально улучшить распознавание хотдогов.
Большое преимущество передачи обучения состоит в том, что вы получаете лучшие результаты гораздо быстрее и с меньшим количеством данных, чем если бы вы обучали нейросеть с нуля. Полное обучение могло занять несколько месяцев на многочисленных GPU и потребовало бы миллионы изображений, в то время как переобучение предположительно можно провести за несколько часов на ноутбуке с парой тысяч фотографий.
Одной из самых сложных задач, с которой мы столкнулись, было точное определение, что считать хотдогом, а что нет. Определение «хотдога» оказалось на удивление сложным (считаются ли разрезанные сосиски, а если да, то каких видов?) и подверженным культурной интерпретации.
Аналогично, характер «открытого мира» нашей проблемы означал, что нам придётся иметь дело с практически бесконечным количество входных данных. Некоторые задачи компьютерного зрения имеют дело с относительно ограниченным набором входных данных (например, рентгеновские снимки болтов с механическими дефектами или без дефектов), но нам пришлось готовить приложение к обработке селфи, снимков природы и широкого разнообразия блюд.
Достаточно сказать, что такой подход был многообещающим и привёл к некоторому улучшению результатов, но от него пришлось отказаться по ряду причин.
Во-первых, природа нашей задачи означала сильную несбалансированность данных для обучения: существует намного больше примеров того, что не является хотдогами, чем самих хотдогов. На практике это означает, что если вы обучаете ваш алгоритм на трёх изображениях хотдогов и 97 изображениях, которые не являются хотдогами, и он распознаёт 0% первых и 100% вторых, то вы получаете номинальную точность 97%! Эта проблема не решается напрямую инструментом переобучения TensorFlow и по существу вынуждает установить модель глубинного обучения с нуля, импортировать веса и проводить обучение более контролируемым образом.
На этом этапе мы решили стиснуть зубы и начать работу с Keras, библиотекой глубинного обучения, которая предоставляет лучшие, более простые в использовании абстракции поверх TensorFlow, в том числе довольно крутые инструменты обучения, а также опцию class_weights, которая идеально подходит для решения такого рода задач с несбалансированным набором данных, как у нас.
Мы использовали эту возможность для испытания других нейронных архитектуур, таких как VGG, но осталась одна проблема. Никакие из них не обеспечивали комфортную работу на iPhone. Они потребляли слишком много памяти, что вело к сбоям приложения, а иногда требовало до 10 секунд для выдачи результата, что неидеально с точки зрения UX. Мы многое пробовали, чтобы решить проблему, но в итоге признали, что эти архитектуры слишком громоздкие, чтобы работать на мобильном устройстве.
V2: Keras и SqueezeNet
SqueezeNet против AlexNet, дедушки архитектур компьютерного зрения. Источник: научная статья по SqueezeNet
Чтобы дать вам контекст, где мы находимся, это примерно полпути в истории разработки проекта. К этому времени UI был готов более чем на 90%, осталось изменить очень немногое. Но сейчас видно, что нейросеть была готова в лучшем случае на 20%. У нас было хорошее понимание проблем и хороший набор данных, но написано 0 строчек кода готовой нейронной архитектуры, никакой наш код не мог надёжно работать на мобильном телефоне и даже точность впоследствии будет кардинально улучшена.
Стоящая непосредственно перед нами проблема была простой: если Inception и VGG слишком громоздкие, существует ли более простая, предварительно обученная нейросеть, которую мы можем переобучить? По наводке всегда великолепного Джереми Говарда (где этот парень был всю нашу жизнь?) мы попробовали Xception, Enet и SqueezeNet. Очень быстро мы остановили свой выбор на SqueezeNet в связи с явным позиционированием этой системы как решения для встроенных систем глубинного обучения и в связи с доступностью предварительно обученной модели Keras на GitHub (ура, open-source).
Так насколько большая получается разница? Архитектура вроде VGG использует около 138 млн параметров (по существу, это количество чисел, необходимых для моделирования нейронов и значений между ними). Inception представляет собой значительный прогресс, требуя всего 23 млн параметров. Для сравнения, SqueezeNet работает с 1,25 млн параметров.
Это даёт два преимущества:
- На этапе обучения гораздо быстрее обучается меньшая нейросеть. Там меньше параметров для размещения в памяти, так что вы можете чуть лучше распараллелить обучение (большие размеры пакетов), а нейросеть будет быстрее сходиться (то есть приближаться к идеализированной математической функции).
- В продакшне модель гораздо меньше и гораздо быстрее. SqueezeNet потребляет менее 10 МБ RAM, в то время как архитектурам вроде Inception требуется 100 МБ или больше. Разница гигантская, и она особенно важна при работе на мобильных устройствах, у которых может быть менее 100 МБ доступной памяти для вашего приложения. Меньшие нейросети также вычисляют итоговый результат гораздо быстрее, чем большие.
Конечно же, кое-чем пришлось пожертвовать:
- Архитектура нейросети меньшего размера имеет меньше доступной «памяти»: она не будет настолько же эффективна в сложных ситуациях (таких как распознавание 20 000 различных объектов) или даже в обработке сложных ситуаций в узком классе задач (например, понимать разницу между хотдогами в нью-йоркском стиле и чикагском стиле). Как следствие, нейросети меньшего размера обычно демонстрируют меньшую точность, чем большие сети. При попытке распознать 20 000 объектов ImageNet, SqueezeNet показывает точность распознавания всего 58%, в то время как у VGG этот показатель 72%.
- Маленькую нейросеть труднее переобучить. Технически, ничего не мешает нам использовать тот же подход, что и в случае с Inception и VGG, заставить SqueezeNet кое-что «забыть» — и переобучить её конкретно для различия хотдогов и не-хотдогов. На практике мы столкнулись с трудностями настройки темпа обучения, а результаты всегда оказывались менее удовлетворительными, чем обучение SqueezeNet с нуля. Это также может быть частично вызвано природой «открытого мира» нашей задачи.
- Теоретически, нейросети меньшего размера редко когда должны перерасширяться, но мы сталкивались с этим на нескольких «маленьких» архитектурах. Перерасширение (overfit) означает, что ваша сеть слишком сильно специализируется, а вместо обучения распознавать хотдоги в целом она обучается распознавать в точности и только конкретные фотографии хотдогов, на которых вы её обучали. Человеческой аналогией было бы запоминание конкретных фотографий с хотдогами, которые вам показывают, вместо абстракции, что хотдог обычно состоит из сосиски в булочке, возможно с приправами и т. д. Если вам покажут полностью новое изображение хотдога, а не то что вы запомнили, то вы будете склоняться к тому, чтобы сказать, что это не хотдог. Из-за того, что в маленьких сетях обычно меньше «памяти», легко понять, почему им сложнее специализироваться. Но в некоторых случаях точность наших маленьких сетей подпрыгивала до 99% и неожиданно она переставала распознавать изображения, которые не видела на этапе обучения. Эффект обычно исчезал, когда мы добавляли аугментированные данные: полуслучайные растянутые/искажённые изображения на входе, и вместо 1,00 раза на каждом из 1000 изображений обучение на определённым образом изменённых вариациях тысячи изображений, чтобы снизить вероятность запоминания нейросетью конкретно этой 1000 изображений. Вместо этого она должна распознавать «признаки» хотдога (булочка, сосиска, приправы и т. д.), при этом оставаясь гибкой и достаточно общей, чтобы не слишком привязываться к конкретным значениям пикселей конкретных изображений в обучающем наборе.
Пример аугментированных данных из блога Keras
На этом этапе мы начали экспериментировать с настройкой архитектуры нейросети. В частности, мы стали использовать Batch Normalization и пробовать разные функции активации.
- Batch Normalization помогает нейросети обучаться быстрее, «сглаживая» значения на различных этапах в стеке. В точности почему это работает, ещё до конца не понятно, но эффект известный: нейросеть сходится гораздо быстрее, а значит достигает большей точности с меньшим обучением или большей точности при том же объёме обучения, часто кардинально большей точности.
- Функции активации — это внутренние математические функции, которые определяют, активировать ваши «нейроны» или нет. Во многих научных статьях по-прежнему упоминается ReLU (Rectified Linear Unit), но у нас лучшие результаты получались на ELU.
После добавления Batch Normalization и ELU в SqueezeNet, мы смогли обучить нейронную сеть, которая достигла точности выше 90% при обучении с нуля, но она была довольно хрупкой, то есть та же самая нейросеть в некоторых случаях могла перерасширяться или недорасширяться в других ситуациях, когда сталкивалась с тестированием в реальных условиях. Даже добавление дополнительных примеров в набор данных и эксперименты с аугментацией данных не помогали настроить сеть, которая показывает нормальный результат.
Так что хотя этот этап был многообещающим и впервые дал нам функционирующее приложение, которое целиком работало на iPhone и вычисляло результат менее чем за секунду, но в итоге мы перешли к нашей четвёртой и окончательной архитектуре.
3. Архитектура DeepDog
from keras.applications.imagenet_utils import _obtain_input_shape
from keras import backend as K
from keras.layers import Input, Convolution2D, SeparableConvolution2D,
GlobalAveragePooling2d
Dense, Activation, BatchNormalization
from keras.models import Model
from keras.engine.topology import get_source_inputs
from keras.utils import get_file
from keras.utils import layer_utils
def DeepDog(input_tensor=None, input_shape=None, alpha=1, classes=1000):
input_shape = _obtain_input_shape(input_shape,
default_size=224,
min_size=48,
data_format=K.image_data_format(),
include_top=True)
if input_tensor is None:
img_input = Input(shape=input_shape)
else:
if not K.is_keras_tensor(input_tensor):
img_input = Input(tensor=input_tensor, shape=input_shape)
else:
img_input = input_tensor
x = Convolution2D(int(32*alpha), (3, 3), strides=(2, 2), padding='same')(img_input)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = SeparableConvolution2D(int(32*alpha), (3, 3), strides=(1, 1), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = SeparableConvolution2D(int(64 * alpha), (3, 3), strides=(2, 2), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = SeparableConvolution2D(int(128 * alpha), (3, 3), strides=(1, 1), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = SeparableConvolution2D(int(128 * alpha), (3, 3), strides=(2, 2), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = SeparableConvolution2D(int(256 * alpha), (3, 3), strides=(1, 1), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = SeparableConvolution2D(int(256 * alpha), (3, 3), strides=(2, 2), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
for _ in range(5):
x = SeparableConvolution2D(int(512 * alpha), (3, 3), strides=(1, 1), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = SeparableConvolution2D(int(512 * alpha), (3, 3), strides=(2, 2), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = SeparableConvolution2D(int(1024 * alpha), (3, 3), strides=(1, 1), padding='same')(x)
x = BatchNormalization()(x)
x = Activation('elu')(x)
x = GlobalAveragePooling2D()(x)
out = Dense(1, activation='sigmoid')(x)
if input_tensor is not None:
inputs = get_source_inputs(input_tensor)
else:
inputs = img_input
model = Model(inputs, out, name='deepdog')
return model
Дизайн
Наша окончательная архитектура во многом создана под влиянием опубликованной 17 апреля 2017 года научной статьи от Google по сетям MobileNets, которая описывала новую архитектуру нейросетей с точностью как у Inception на простых задачах вроде нашей и с использованием всего 4 млн параметров или около того. Это означает, что она занимает выгодное положение между SqueezeNet, которая возможно была слишком простой для нашей задачи, и слишком перегруженными архитектурами Inception и VGG, излишне тяжеловесными для мобильного использования. Эта статья описывает некоторые возможности для настройки размера и сложности нейросети, конкретно для выбора баланса между потреблением памяти/CPU и точностью, а это именно то, о чём мы думали в то время.
Менее чем за месяц до дедлайна мы попытались воспроизвести результаты из научной статьи. Было абсолютным разочарованием, когда в течение суток после публикации статьи реализация Keras уже стала общедоступной на GitHub благодаря Рефику Кан Малли, студенту Стамбульского технического университета, плодами работы которого мы уже пользовались, когда взяли его великолепную реализацию Keras SqueezeNet. Численность, квалификация и открытость сообщества глубинного обучения, а также присутствие таких талантов как Рефик — это то, что делает глубинное обучение подходящим для использования в современных приложениях, но это также делает работу в данной отрасли более волнительной, чем в какой-либо другой технической отрасли, в которую я был вовлечён в своей жизни.
В нашей окончательной архитектуре мы значительно отошли от изначальной архитектуры MobileNets и от общепринятых правил, в частности:
- Мы не используем Batch Normalization & Activation между свёртываниями точечно и по глубине, потому что научная статья по XCeption (где подробно обсуждается свёртывание по глубине) наводит на мысль, что это в реальности ведёт к снижению точности на архитектуре такого типа (как услужливо заметил на Reddit автор научной статьи по QuickNet). Наш метод также помогает уменьшить размер нейросети.
- Мы использовали ELU вместо ReLU. Также как в экспериментах с SqueezeNet, здесь ELU обеспечивает лучшую скорость сходимости и итоговую точность, по сравнению с ReLU.
- Мы не использовали PELU. Хотя и перспективная, но эта функция активации стремиться выпасть в бинарное состояние, как бы мы её ни использовали. Вместо постепенного улучшения точность нашей нейросети чередовалась между примерно 0% и примерно 100% от одного пакета к другому. Неясно, почему так происходит, и это может быть связано с какой-то ошибкой реализации или ошибкой пользователя. Слияние осей ширины и высоты наших изображений не изменило ситуацию.
- Мы не использовали SELU. Небольшое расследование, проведённое между выпуском версий для iOS и Android, показало результаты, очень похожие на PELU. Мы подозреваем, что SELU нельзя использовать в изоляции саму по себе как некую серебряную пулю функций активации, а её следует применять — на что указывает названием научной статьи — как часть узко определённой архитектуры SNN.
- Мы продолжили использовать Batch Normalization вместе с ELU. Есть много индикаторов, что это необязательно, но в каждом эксперименте, который мы проводили без Batch Normalization, нейросеть полностью отказывалась сходиться. Причиной может быть малый размер нашей архитектуры.
- Мы применяли Batch Normalization перед активацией. Хотя это предмет некоторых споров в наши дни, но при экспериментах размещения BN после активации сходимости тоже не было.
- Для оптимизации нейросети мы использовали Cyclical Learning Rates и великолепную реализацию Keras от (однокурсника) Брэда Кенстлера. CLR играет в игру, пытаясь угадать оптимальный темп обучения для нейросети. Что ещё более важно, повышая и понижая темп обучения, CLR помогает достичь точности распознавания, которая по нашему опыту выше, чем у традиционного оптимизатора. По обеим этим причинам мы не можем понять, зачем использовать что-то кроме CLR для обучения нейросетей в будущем.
- Для нашей задачи мы не видели необходимости настраивать значения или из архитектуры MobileNets. Наша модель достаточно маленькая для наших задач при , а вычисления происходят достаточно быстро при , так что мы предпочли сконцентрироваться на достижении максимальной точности. Однако эти изменения могут быть полезными при попытке запустить приложение на более старых мобильных телефонах или встроенных платформах.
Так как в точности работает этот стек? Глубинное обучение часто имеет плохую репутацию некоего «чёрного ящика», и хотя многие компоненты в самом деле могут быть загадочными, наши нейросети часто проявляют информацию о том, как работают некоторые из их магических трюков. Мы можем взять отдельные слои из этого стека и посмотреть, как они активируются на конкретных входных изображениях, что даёт нам понимание того, какие способности у каждого слоя к распознаванию сосиски, булочек или других наиболее заметных признаков хотдога.
Обучение
Качество исходных данных было самым важным. Нейросеть может быть только настолько хороша, насколько хороши исходные данные, а улучшение качества обучающего набора, вероятно, стало одной из трёх вещей, на которые мы потратили больше всего времени во время работы над этим проектом. Для его улучшения мы предприняли следующие ключевые шаги:
- Поиск большего количества изображений и более разнообразных изображений (высота/ширина, фон, условия освещения, культурные особенности, перспектива, композиция и т. д.).
- Соответствие типов изображения ожидаемым фотографиям в продакшне. Мы предполагали, что люди будут фотографировать, в основном, настоящие хотдоги, другую еду или будут пытаться всячески обхитрить систему со случайными объектами, так что наш набор данных отражал это предположение.
- Дать как можно больше примеров похожих объектов. Некоторые блюда больше похожи на хотдоги, чем другие (например, гамбургеры и сэндвичи или, в случапе с голыми хотдогами, это молодая морковь или даже приготовленные помидоры черри). Наш набор данных отразил это.
- Ожидаемые искажения: большинство фотографий с мобильного телефона будут хуже, чем «средняя» фотография с цифровой зеркалки или в идеальных условиях освещения. Мобильные фотографии тусклые, зашумлённые, сняты под углом. Агрессивная аугментация данных была ключевым инструментом для решения этой проблемы.
- Вдобавок мы выяснили, что у пользователей может не быть доступа к настоящим хотдогам, так что они попытаются смфотографировать хотдоги из результатов поиска Google, что ведёт к особым типам искажений (угловое смещение, если фотография сделана под углом, отражение вспышки от экрана, эффект муара от ЖК-экрана, снятого мобильной камерой). Эти специфические отражения обладали практически сверхъестественной способностью обманывать нашу нейросеть, во многом как описано в недавно опубликованных научных работах об (отсутствии) сопротивления шуму со стороны свёрточных нейросетей. Применение функции сдвига канала в Keras решило большинство этих проблем.
Пример искажений из-за муара и вспышки. Оригинальная фотография: Wikimedia Commons
- Некоторые граничные случаи было сложно уловить. Например, изображения хотдогов со сферическими аберрациями (мягкий фокус) или большое количество размытостей в фоне иногда вводило в заблуждение нашу нейросеть. Против такого трудно защититься, потому что а) существует не так много фотографий хотдогов с мягким фокусом (мы испытываем голод от одной мысли об этом) и б) опасно тратить слишком большую часть ёмкости нашей нейросети на мягкий фокус, когда в реальности большинство фотографий с мобильного телефона не будут иметь такого признака. В итоге мы решили оставить эту проблему, по большому счёту, нерешённой.
В окончательном виде наш набор данных состоял из 150 тыс. изображений, из которых только 3000 были хотдогами. Несбалансированное соотношение 49:1 было указано в настройках веса класса Keras 49:1 в пользу хотдогов. Бóльшую часть остальных 147 тыс. фотографий представляли собой разные блюда, и только 3000 были не едой, чтобы помочь нейросети сделать чуть лучшие обобщения и не принимать за хотдог изображение человека в красной одежде.
Наши правила аугментации данных следующие:
- Мы применили вращение в пределах ±135° — значительно сильнее среднего, потому что запрограммировали приложение не обращать внимание на ориентацию телефона.
- Высота и ширина искажались на 20%.
- Обрезание в диапазоне 30%.
- Зуммирование в диапазоне 10%.
- Сдвиги канала на 20%.
- Случайные горизонтальные перевороты, чтобы помочь нейросети делать обобщения.
Эти параметры выведены интуитивно, на основании экспериментов и нашего понимания, как будет использоваться приложение в реальных условиях, в отличие от аккуратных экспериментов.
На последнем этапе нашего конвейера по обработке данных мы применили многопроцессный генератор изображений для Keras от Патрика Родригеса. Хотя в Keras есть встроенная реализация многопоточности и многопроцессности, библиотека Патрика работала последовательно быстрее в наших экспериментах по причинам, которые у нас не было времени выяснять. Эта библиотека сократила на треть время обучения.
Сеть обучалась на ноутбуке 2015 MacBook Pro с подключенным внешним GPU (eGPU), а именно Nvidia GTX 980 Ti (вероятно, мы бы купили 1080 Ti, если бы начали работу сегодня). Мы смогли обучать нейросеть на пакетах по 128 изображений. Сеть обучалась в общей сложности 240 эпох. Это значит, что мы пропустили через неё все 150 тыс. изображений 240 раз. Это заняло примерно 80 часов.
Мы обучали нейросеть в три этапа:
- Первый этап продолжался 112 эпох (7 полных циклов CLR с размером шага в 8 эпох), с темпом обучения между 0,005 и 0,03, с правилом треугольника 2 (это значит, что максимальный темп обучения сокращался вдвое каждые 16 эпох).
- Второй этап продолжался ещё 64 эпохи (4 цикла CLR с размером шага в 8 эпох), с темпом обучения между 0,0004 и 0,0045, с правилом треугольника 2.
- Третий этап продолжался ещё 64 эпохи (4 цикла CLR с размером шага в 8 эпох), с темпом обучения между 0,000015 и 0,0002, с правилом треугольника 2.
Хотя темпы обучения были определены путём проведения линейного эксперимента, рекомендованного авторами научной статьи по CLR, они как будто интуитивно понятны, здесь максимальный показатель на каждом этапе примерно в два раза меньше предыдущего минимума, что соответствует отраслевому стандарту уменьшения вдвое темпа обучения, если точность распознавания в процессе обучения перестала расти.
Для экономии времени мы провели часть обучения на инстансе Paperspace P5000 под Ubuntu. В некоторых случаях получалось удвоить размер пакетов, и оптимальные темпы обучения на каждом этапе тоже удвоились.
Запуск нейронных сетей на мобильных телефонах
Даже спроектировав относительно компактную нейронную архитектуру и обучив её справляться со специфическими ситуациями в мобильном контексте, оставалось проделать ещё много работы, чтобы приложение правильно работало. Если запустить первоклассную архитектуру нейросети без изменений, то она быстро израсходует сотни мегабайт оперативной памяти, что выдержат немногие из современных мобильных устройств. Кроме оптимизации самой сети, выяснилось, что способ обработки изображений и даже способ загрузки самой TensorFlow оказывают огромный эффект на скорость работы нейросети, объём потребляемой оперативной памяти и количество сбоев.
Наверное, это самая таинственная часть проекта. Довольно сложно найти информацию по этой теме, возможно, из-за малого количества приложений глубинного обучения, которые сегодня работают на мобильных устройствах. Однако мы признательны группе разработки TensorFlow, а особенно Питу Вардену, Эндрю Харпу и Чаду Уипки за существующую документацию и их доброжелательность в ответах на наши вопросы.
- Округления весов в нашей нейросети помогло сжать её примерно на 25% по размеру. В сущности, вместо использования произвольных стоковых значений, полученных в ходе обучения, эта оптимизация выбирает N наиболее распространённых значений и приводит все параметры нейросети к этим значениям, что кардинально снижает размер нейросети в zip-архиве. Однако это никак не влияет на размер после разархивирования или на потребление памяти. Мы не внедрили это улучшение в продакшн, потому что нейросеть и так была достаточно маленькой для наших задач, и у нас не было времени исследовать, как округление повлияет на точность распознавания в приложении.
- Оптимизация библиотеки TensorFlow путём её компиляции для продакшна с ключом
-Os
. - Удаление ненужных фрагментов из TensorFlow: библиотека изначально поддерживает виртуальные машины, способна интерпретировать большое количество произвольных операций TensorFlow, в том числе сложение, умножение, конкатенации и др. Вы можете значительно сэкономить в размере (и потреблении памяти), удалив ненужные фрагменты из библиотеки TensorFlow перед компиляцией для iOS.
- Возможны и другие улучшения. Например, в другом проекте автору удалось добиться сокращения бинарника под Android на 1 МБ относительно простым приёмом, так что могут быть и другие области, где код TensorFlow для iOS может быть оптимизирован под ваши задачи.
Вместо использования TensorFlow на iOS мы изучили встроенные библиотеки глубинного обучения Apple (BNNS, MPSCNN, а позже CoreML). Мы бы спроектировали нейросеть на Keras, обучили её с помощью TensorFlow, экспортировали все значения весов, повторно реализовали нейросеть на BNNS или MPSCNN (или импортировали через CoreML) и загрузили бы параметры в новую реализацию. Однако самым большим препятствием стало то, что новые библиотеки Apple доступны только под iOS 10+, а мы хотели поддерживать и старые версии iOS. По мере распространения iOS 10+ и улучшения этих фреймворков, может в будущем и не понадобиться запускать TensorFlow на мобильном устройстве.
Изменение поведения приложения путём внедрения нейросети на лету
Если вы думаете, что внедрить JavaScript в ваше приложение на лету это круто, то попробуйте внедрить на лету нейросеть! Последним трюком, который мы использовали в продакшне, было применение CodePush и относительно либеральных условий использования от Apple, чтобы вживую внедрять новые версии наших нейросетей после их публикации в каталоге приложений. Это делается в основном для повышения точности распознавания после релиза, но этот метод, теоретически, можно использовать для кардинального улучшения функциональности вашего приложения без необходимости повторной рецензии в AppStore.
#import <CodePush/CodePush.h>
…
NSString* FilePathForResourceName(NSString* name, NSString* extension) {
// NSString* file_path = [[NSBundle mainBundle] pathForResource:name ofType:extension];
NSString* file_path = [[[[CodePush.bundleURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"assets"] URLByAppendingPathComponent:name] URLByAppendingPathExtension:extension] path];
if (file_path == NULL) {
LOG(FATAL) << "Couldn't find '" << [name UTF8String] << "."
<< [extension UTF8String] << "' in bundle.";
}
return file_path;
}
…
import React, { Component } from 'react';
import { AppRegistry } from 'react-native';
import CodePush from "react-native-code-push";
import App from './App';
class nothotdog extends Component {
render() {
return (
<App />
)
}
}
require('./deepdog.pdf')
const codePushOptions = { checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME };
AppRegistry.registerComponent('nothotdog', () => CodePush(codePushOptions)(nothotdog));
Что бы мы сделали иначе
Есть много вещей, которые не заработали или не было времени их испытывать, и вот какие идеи мы бы изучили в будущем:
- Более тщательная настройка наших параметров аугментации.
- Измерение точности от начала до конца, в том числе окончательное измерение уровня абстракций вроде того, различает ли наше приложение две или более категории объектов, каков финальный предел распознавания хотдогов (мы пришли к тому, что приложение говорит «хотдог», если предел распознавания выше 0,90, по сравнению с показателем по умолчанию 0,5), после округления весов и т. д.
- Встраивание в приложение механизма обратной связи — чтобы пользователи могли выразить своё разочарование ошибочными результатами работы программы, или для активного улучшения нейросети.
- Использовать большее разрешение при распознавании изображений, чем нынешние 224×224 пикселя — конкретно, путём повышения значения
UX/DX, предвзятость и эффект «зловещей долины» ИИ
В конце концов, было бы непростительно не упомянуть очевидное и важное влияние взаимодействия с пользователем (UX), с разработчиком (DX) и встроенной предвзятости в разработке приложения ИИ. Возможно, каждая из этих тем заслуживает отдельной статьи (или отдельной книги), но вот каким было очень конкретное влияние этих трёх факторов на нашу работу.
UX (взимодействие с пользователем), возможно, имеет более важное значение на каждом этапе разработки приложения ИИ, чем обычного приложения. Прямо сейчас нет алгоритмов глубинного обучения, которые дадут вам идеальные результаты, но существует много ситуаций, где правильное сочетание глубинного обучения и UX дадут результат, неотличимый от идеального. Правильные ожидания в отношении UX неоценимы, когда речь идёт о выработке корректного направления проектирования нейросети и изящной обработки случаев неизбежных сбоев ИИ. Создание приложений ИИ без мысли о взаимодействии с пользователем — это как обучение нейросети без стохастического градиентного спуска: вы застрянете в локальном минимуме зловещей долины на вашем пути к созданию идеально работающего ИИ.
Источник: New Scientist
DX (взимодействие с разработчиком) тоже чрезвычайно важно, потому что время обучения нейросети — это новый геморрой, вместе с ожиданием компиляции программы. Мы полагаем, что вы обязательно поставите DX на первое место в списке приоритетов (следовательно, выберете Keras), потому что всегда есть возможность оптимизировать среду для последующего выполнения (ручное распараллеливание GPU, аугментация данных для многопроцессности, конвейер TensorFlow, даже повторная реализация для caffe2 / pyTorch).
Даже проекты с относительно бестолковой документацией вроде TensorFlow значительно облегчают взаимодействие с разработчиком, предоставляя отлично протестированное, широко используемое и великолепно поддерживаемое окружение для обучения и запуска нейросетей.
По той же причине трудно найти нечто более дешёвое и удобное, чем свой собственный GPU для разработки. Возможность локально просматривать и редактировать изображения, редактировать код в вашем любимом редакторе без задержек — это значительно повышает качество и скорость разработки проектов ИИ.
Большинство приложений ИИ столкнутся с бóльшим количеством культурной предвзятости, чем наше приложение. Но для примера, даже в нашем простейшем случае, изначально думая о культурных особенностях, мы обучили нейросеть распознавать хотдоги по-французски, азиатские хотдоги и ещё больше странностей, о которых раньше не имели понятия. Важно помнить, что ИИ не принимает «лучшие» решения, чем человек — они поражены теми же предвзятостями, что и люди, а инфицирование происходит во время обучения человеком.
Автор: m1rko