Привет, Хабр! Параллельно с публикациями статей открытого курса по машинному обучению мы решили запустить ещё одну серию — о работе с популярными фреймворками для нейронных сетей и глубокого обучения. Я открою этот цикл статьёй о Theano — библиотеке, которая используется для разработки систем машинного обучения как сама по себе, так и в качестве вычислительного бекэнда для более высокоуровневых библиотек, например, Lasagne, Keras или Blocks. Theano разрабатывается с 2007 года главным образом группой MILA из Университета Монреаля и названа в честь древнегреческой женщины-философа и математика Феано (предположительно изображена на картинке). Основными принципами являются: интеграция с numpy, прозрачное использование различных вычислительных устройств (главным образом GPU), динамическая генерация оптимизированного С-кода.  Код с примерами из этого поста можно найти тут. В настоящее время разработаны десятки библиотек для работы с нейронными сетями, все они, подчас существенно, различаются в реализации, но можно выявить два основных подхода: императивный и символьный. 1 Давайте посмотрим на примере, чем они различаются. Предположим, что мы хотим вычислить простое выражение  Вот так оно выглядело бы в императивном изложении на языке python: Интерпретатор исполняет код построчно, сохраняя результаты в переменных a, b, c и d.
Та же программа в символьной парадигме выглядела бы так: Существенное различие заключается в том, что когда мы объявляем D, исполнения не происходит, мы лишь задали граф вычислений, который затем скомпилировали и наконец выполнили.  Оба подхода имеют свои достоинства и недостатки. В первую очередь, императивные программы гибче, нагляднее и проще в отладке. Мы можем использовать все богатства используемого языка программирования, например, циклы и ветвления, выводить промежуточные результаты в отладочных целях. Такая гибкость достигается, в первую очередь, малыми ограничениями, накладываемыми на интерпретатор, который должен быть готов к любому последующему использованию переменных. С другой стороны, символьная парадигма накладывает больше ограничений, но вычисления получаются более эффективными, как по памяти, так и по скорости исполнения: на этапе компиляции можно применить ряд оптимизаций, выявить неиспользуемые переменные, выполнить часть вычислений, переиспользуя память и так далее. Отличительная черта символьных программ — отдельные этапы объявления графа, компиляции и выполнения. Мы останавливаемся на этом так подробно, потому что императивная парадигма знакома большинству программистов, в то время как символьная может показаться непривычной, и Theano, как раз, явный пример символьного фреймворка. Тем, кому хочется разобраться в этом вопросе подробнее, рекомендую почитать соответствующий раздел документации к MXNet (об этой библиотеке мы еще напишем отдельный пост), но ключевой момент для понимания дальнейшего текста заключается в том, что программируя на Theano, мы пишем на python программу, которую потом скомпилируем и выполним.
Но довольно теории, давайте разбираться с Theano на примерах. Для установки нам понадобятся: python версии старше 2.6 или 3.3 (лучше dev-версию), компилятор С++ (g++ для Linux или Windows, clang для MacOS), библиотека примитивов линейной алгебры (например ATLAS, OpenBLAS, Intel MKL), NumPy и SciPy. Для выполнения вычислений на GPU понадобится CUDA, а ряд операций, встречающихся в нейронных сетях, можно ускорить с помощью сuDNN. Начиная с версии 0.8.0, разработчики Theano рекомендуют использовать libgpuarray, что также даёт возможность использовать несколько GPU. Когда все зависимости установлены, можно установить Theano через pip: Theano можно настроить тремя способами: Я обычно использую примерно такой конфигурационный файл: Подробнее о конфигурации можно узнать в документации. Теперь, когда всё установлено и настроено, давайте попробуем написать немного кода, например, вычислим значение многочлена  в точке 10: Здесь мы совершили 4 вещи: определили скалярную переменную а типа long, создали выражение, содержащее наш многочлен, определили и скомпилировали функцию f, а также выполнили её, передав на вход число 10. Обратим внимание на тот факт, что переменные в Theano — типизированные, причем тип переменной содержит информацию как о типе данных, так и о их размерности, т.е. чтобы посчитать наш многочлен сразу в нескольких точках, потребуется определить а как вектор: В данном случае нам нужно только указать количество измерений переменной при инициализации: размер каждого измерения вычисляется автоматически на этапе вызова функции. UPD: Не секрет, что аппарат линейной алгебры повсеместно используется в машинном обучении: примеры описываются векторами признаков, параметры модели записывают в виде матриц, изображения представляют в виде 3х-мерных тензоров. Скалярные величины, векторы и матрицы можно рассматривать как частный случай тензоров, поэтому именно так мы в дальнейшем будем называть эти объекты линейной алгебры. Под тензором будем понимать N-мерные массивы чисел.
Пакет theano.tensor содержит наиболее часто употребляемые типы тензоров, однако, нетрудно определить и свой тип. При несовпадении типов Theano выбросит исключение. Исправить это, кстати как и поменять многое другое в работе функций, можно, передав конструктору аргумент allow_input_downcast=True: Мы также можем вычислять несколько выражений сразу, оптимизатор в этом случае может переиспользовать пересекающиеся части, в данном случае сумму : Для обмена состояниями между функциями используются специальные shared переменные: Значения таких переменных, в отличие от тензорных, можно получать и модифицировать вне Theano-функций из обычного python-кода: Значения в shared переменных можно «подставлять» в тензорные переменные: Theano предоставляет ряд средств для отображения графа вычислений и отладки. Тем не менее, отладка символьных выражений по-прежнему остаётся задачей не из лёгких. Мы кратко перечислим тут наиболее употребимые подходы, подробнее об отладке можно прочитать в документации: http://deeplearning.net/software/theano/tutorial/printing_drawing.html Мы можем распечатать граф вычислений для каждой функции: Заметьте, что сумма вычисляется только один раз: Выражения можно выводить и в более лаконичной форме: Или в виде графа: 
К сожалению, читаемость таких графов резко падает с ростом сложности выражения. Фактически что-то понять можно только на игрушечных примерах. Давайте посмотрим на примере логистической регрессии, как с помощью Theano можно разрабатывать алгоритмы машинного обучения. Намеренно не будем вдаваться в подробности того, как устроена эта модель (оставим это до соответствующей статьи открытого курса), но напомним, что апостериорная вероятность класса  имеет вид  Давайте определим параметры модели, для удобства введём отдельный параметр для смещения: И заведём символьные переменные для признаков и меток класса: Давайте теперь определим выражения для апостериорной вероятности и предсказаний модели: И определим функцию потерь вида:  Мы не стали выписывать выражения для сигмоиды и кросс-энтропии в явном виде, а воспользовались функциями из пакета theano.tensor.nnet, который предоставляет оптимизированные реализации ряда популярных в машинном обучении функций. Кроме того, функции из этого пакета обычно включают в себя дополнительные трюки для численной устойчивости. Для оптимизации функции потерь давайте воспользуемся методом градиентного спуска, каждый шаг которого задаётся выражением:   Давайте воплотим его в коде: Здесь мы воспользовались замечательной возможностью Theano — автоматическим2 дифференцированием. Вызов T.grad вернул нам выражение, которое будет содержать градиент первого аргумента по второму. Это может показаться излишним для столь простого случая, но очень выручает при построении больших, многослойных моделей. Когда градиенты получены, нам остаётся лишь скомпилировать Theano-функции: И запустить итеративный процесс: Для сгенерированных мною данных процесс сходится к такой разделяющей прямой:
 Выглядит неплохо, но кажется, что для такой простой задачи 4000 итераций — это как-то многовато… Давайте попробуем ускорить оптимизацию и воспользуемся методом Ньютона. Этот метод использует вторые производные функции потерь и представляет собой последовательность таких шагов:   где  — матрица Гессе. Чтобы посчитать матрицу Гессе, создадим одномерные версии параметров нашей модели: И определим шаг оптимизатора: Хоть мы и пришли к тем же результатам,
,
Методу Ньютона для этого понадобилось всего 30 (против 4000 у градиентного спуска) шагов. Пути обоих методов можно посмотреть на этом графике:
 Так же мы без труда можем реализовать метод опорных векторов, для этого достаточно представить функцию потерь в следующем виде:  В терминах Theano это можно написать с помощью замены нескольких строк в предыдущем примере: Для выбранного С классификатор разделит пространство так:  Циклы являются одной из наиболее употребимых конструкций в программировании. Поддержка циклов в Theano представлена функцией scan. Давайте познакомимся с тем, как она работает. Думаю, читателям уже очевидно, что линейная функция от признаков — не лучший кандидат на разделение сгенерированных данных. Этот недостаток можно исправить, добавив полиномиальные признаки к исходным (этот приём подробно описан в другой статье нашего блога). Итак, хочется получить преобразование вида . В python мы могли бы реализовать его, например, так: В Theano это выглядит следующим образом: Первой в scan передается функция, которая будет вызываться на каждой итерации, её первый аргумент — результат на предыдущей итерации, последующие — все non_sequences; outputs_info инициализирует выходной тензор такой же размерности и типа, как и x, и заполняет его единицами; n_steps указывает на требуемое количество итераций.  scan вернет результат в виде тензора размера (n_steps, ) + outputs_info.shape, поэтому мы преобразуем его в матрицу, чтобы получить нужные признаки. Проиллюстрируем работу полученного выражения простым примером: Чтобы воспользоваться плодами своих усилий, достаточно поменять определение модели и добавить параметров (ведь признаков стало больше):  Новые признаки позволяют значительно лучше разделить классы:
 К этому моменту мы уже обсудили основные этапы создания систем машинного обучения на Theano: инициализация входных переменных, определение модели, компиляция Theano-функций, цикл с шагами оптимизатора. На этом можно было бы и закончить, но очень уж хочется познакомить читателей с Lasagne — замечательной библиотекой для нейронных сетей, работающей поверх Theano. Lasagne предоставляет набор готовых компонентов: слоёв, алгоритмов оптимизации, функций потерь, инициализаций параметров и т.д., при этом не скрывает Theano за многочисленными слоями абстракций. Рассмотрим, как может выглядеть типичный код на Theano/Lasagne на примере классификации MNIST'a.  Сконструируем многослойный перцептрон с двумя скрытыми слоями по 800 нейронов, для регуляризации будем использовать dropout и разместим этот код в отдельной функции: Получим вот такую простую полносвязную сеть: 
Инициализируем тензорные переменные и скомпилируем Theano-функции для обучения и валидации: Теперь создадим цикл обучения: Получившиеся кривые обучения: 
Нашей модели удаётся достичь точности более 98 %, что, несомненно, можно улучшить, используя, например, свёрточные нейронные сети, но эта тема уже выходит за рамки данной статьи. Сохранять и загружать веса удобно с помощью хелперов: Документация к Lasagne доступна тут, масса примеров и предобученные модели находятся в отдельном репозитории. В этом посте мы довольно поверхностно ознакомились с возможностями Theano, узнать больше можно: Большая благодарность bauchgefuehl за помощь в подготовке поста.