Или почему нейронные сети больше не очень нейронные?

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

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

Практики в последние годы стали свидетелями дальнейшего ребрендинга, который начался с распространения очень гибких программных библиотек, таких как PyTorch, Chainer и других. Несмотря на свои различия, все эти библиотеки имеют две основные характеристики: (i) обратное распространение полностью автоматическое и прозрачное для пользователя (за счет использования автоматического дифференцирования); и (ii) код для сетей и логика моделей легко интегрируются с другими конструкциями программирования, такими как условные ветвления и циклы.

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

Этот сдвиг в перспективе имеет множество названий, в первую очередь дифференцируемое программирование (старая идея, популяризированная Янном ЛеКуном год назад) и программное обеспечение 2.0. Дифференцируемая программа (DP) - это фрагмент кода, реализованный с использованием дифференцируемых операторов (также называемых нейронными сетями), внутренняя логика которых адаптируется одной или несколькими процедурами оптимизации на основе данных. Концептуальное отличие от стандартных нейронных сетей заключается в разнообразии дифференцируемых операторов, которые можно использовать (например, дифференцируемые операции чтения / записи, нейронные арифметические устройства и т. Д.), И в том, что эти операторы смешаны. с большим разнообразием стандартных конструкций и частей программного обеспечения 1.0.

Дифференцируемое программирование - это не что иное, как ребрендинг [современных] методов глубокого обучения […] Но важным моментом является то, что сейчас люди создают новый вид программного обеспечения, собирая сети параметризованных функциональных блоков и обучая их на примерах с использованием некоторой формы градиентная оптимизация. (источник: Ян Лекун в Facebook).

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

Цель этой статьи

DP можно рассматривать не только как ребрендинг, но и как изменение мышления: глубокие сети больше не являются биологически вдохновленными инструментами, которые каким-то образом «учатся», как наш мозг, - они представляют собой программные конструкции . В этой статье я хочу показать, сколько концепций машинного обучения / глубокого обучения может возникнуть естественным образом, если начать с чистого программирования, то есть думать о модульном тестировании, инкапсуляции, модульности и тому подобном. Это не очередная статья о «глубоком обучении, объясненном программистам» - по крайней мере, я на это надеюсь! Это скорее набросок статьи «Глубокое обучение, объясненное ОТ программистов»: естественный шаг на пути к дифференцируемому программированию.

Примечание: эта работа находится в стадии разработки. Любые комментарии или отзывы приветствуются.

Модульное тестирование и «мягкие» утверждения

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

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

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

Представьте, например, что программист банка пишет функцию, чтобы определить, следует ли данному клиенту x давать ссуду (y = 1) или нет (y = 0 ). Полное тело этой функции будет (вероятно) беспорядочным, полным инсайдерских знаний с его / ее части, вложенных ветвей if-else, оценочных оценок и тому подобного. Однако тестирование функции тривиально: можно просто выбрать ряд известных клиентов, для которых известен желаемый результат, и оценить функцию на них.

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

Мягкое модульное тестирование позволяет оптимизировать?

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

Примечание: мы могли бы решить случайным образом изменить каждую отдельную инструкцию внутри нашей функции (например, изменить предложение if a› 0 на предложение if a ≤ 0 ) и протестировать новый набор функций, также известный как генетическое программирование . Однако у нас нет гарантии, что даже одна из этих, возможно, тысяч измененных функций будет лучше нашей исходной!

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

  1. Для непрерывного изменения их входных параметров;
  2. Чтобы разрешить производные по тем же параметрам.

Давайте посмотрим на пример дифференцируемой функции с одним входным параметром, b:

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

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

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

«Утечка информации» и переоснащение

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

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

Разделение обучения / тестирования также имеет еще одно преимущество: хотя критерий обучения должен быть дифференцируемым, критерий теста не имеет такого требования. Снова рассмотрим проблему классификации пользователей: даже очень простую цель, такую ​​как f (x1) == y1 (что-то, называемое потеря 0/1 в терминологии ML), нельзя дифференцировать, не говоря уже о прямом оптимизирован. Однако вместо этого мы можем оптимизировать соответствующий суррогат (например, перекрестная энтропия) и по-прежнему иметь возможность тестировать наше приложение с точки зрения точности.

Роль дифференцируемого программиста

В «программном обеспечении 1.0» программист выбирает последовательность операций для функции, а затем выбирает серию сценариев использования для модульного теста. В DP тестовые случаи более правильно собираются, чем выбираются, поскольку они представляют собой историческую информацию, из которой мы хотим извлечь некоторую информацию. Поскольку DP также нуждаются в данных на этапе оптимизации, и поскольку эти данные обычно должны быть по крайней мере на один порядок больше, чем тестовые примеры, дифференцируемое программирование - это дисциплина голодных данных: поиск данных, маркировка Это, обеспечение правильного описания нашей проблемной области, устранение предвзятости - все это предпосылки для решения проблемы программного обеспечения в версии «2.0».

Затем программист должен выбрать правильную метрику для оптимизации (потери) и конкретную форму для f (x). Так же, как все программы, выполняемые в настоящее время по всему миру, основаны на относительно небольшом количестве примитивов, небольшое количество дифференцируемых операторов позволяет нам определить экспоненциальное количество возможных «программных структур» для f (x), и способность делать это интуитивно - вот что отличает инженера глубокого обучения с многолетним опытом от новичка.

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

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

Дифференцируемые процедуры

Типичная дифференцируемая операция - это линейная комбинация. Например, линейная комбинация двух входов a и b:

И a, и b соглашаются при выборе значения c, каждое из которых имеет определенный вес, который может быть найден алгоритмом. Линейные комбинации являются строительными блоками всех DP, потому что они представляют собой простейшие возможные способы вставки параметрических отношений между объектами в нашей программе. c может быть промежуточным этапом наших вычислений или даже конечным выходным значением.

Математически линейные комбинации нельзя складывать: линейная комбинация линейных комбинаций аналогична одиночной линейной комбинации с разными параметрами (вам, вероятно, придется прочитать это дважды). Если мы хотим иметь последовательности линейных операций, нам нужно чередовать каждую из них с одной или несколькими нелинейностями. В терминологии глубоких сетей они называются функциями активации. Линейные комбинации и функции активации - это основы классических нейронных сетей, в общих чертах представляющие «синапсы» (соединительные линии на диаграмме выше) и пороговые значения.

Однако DP не ограничиваются операциями, имеющими биологическое сходство. Сегодняшний «инструментарий DP» очень широк, и мы можем упомянуть лишь несколько характерных примеров.

Отличное разветвление и внимание

Рассмотрим общую условную инструкцию вида «если условие a выполнено, то выполните f (x), иначе выполните g (x)». Пока условие a не является частью дифференцируемого компонента нашей программы, у нас нет проблем. Однако все сложнее, когда мы также хотим изучить (оптимизировать) предложение, потому что, как указано выше, мы не можем различать в отношении двоичного решения.

Дифференцируемая версия ветвления состоит из двух компонентов. Первый - предварительно обработать условие a с помощью так называемой сигмоидальной функции:

Как видно из графика, независимо от значения a, значение σ (a) (выход сигмоида) будет числом от 0 до 1, вероятно, ближе к одной из двух крайностей. Тогда вместо простого выбора одной из двух ветвей, «дифференцируемое, если» принимает комбинацию обеих ветвей, взвешенных по самому условию:

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

Отличное чтение и письмо

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

Дифференцируемая арифметика

Классическая арифметика также выходит за рамки DP, потому что у нас нет тривиального способа заставить веса нашей программы быть точными целыми числами. Тем не менее, мы можем попытаться изменить нашу архитектуру так, чтобы вычисления были арифметическими. Это случай недавнего устройства нейронной арифметической логики, которое использует умную комбинацию нелинейностей и механизмов внимания, чтобы заставить веса быть близкими к -1, 0 или +1.

Дифференцируемые петли

Теперь рассмотрим общий механизм цикла, в котором мы перебираем элементы массива a и собираем результаты с помощью функции f:

Этот вид операции не требует каких-либо изменений в дифференцируемом мире! Фактически, если f является DP, мы получаем известный объект, называемый рекуррентной нейронной сетью, сетью с небольшой памятью (в виде промежуточных значений z), который может научиться обрабатывать последовательности элементов:

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

Другие дифференцируемые операции

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

В заключение, наконец

Если кто-то действительно хочет продвинуть глубокое обучение в сторону дифференцируемого программирования, недостаточно иметь учебники по математике для программистов: нам нужно перефразировать / переосмыслить всю область в терминах программирования. Этот пост - мой «мысленный набросок» того, как может происходить такая перефразировка.

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