Microsoft Cognitive Toolkit (CNTK): руководство для начала работы с библиотекой

Microsoft Cognitive Toolkit

Навигация

  • Вступление, несколько слов о библиотеке:
    • Кому полезна;
    • Какие задачи решает;
    • Необходимое оборудование;
  • Гайд по конфигурации:
    • Список всех дистрибутивов/библиотек и т.д.;
    • Установка библиотеки;
    • Проверка работоспособности библиотеки;
  • Описание проблемы XOR:
    • Создание сети, объяснение того, как работает InputVariable (Placeholder) в CNTK;
    • Конфигурация слоя, описание CNTK.Function;
    • Запуск и обучение сети, графическая визуализация процесса обучения.

Вступление и несколько слов о библиотеке

За последние 5–7 лет Python стал самым популярным языком для решения задач машинного обучения. Глаза разбегаются, когда пытаешься выбрать инструментарий для решения своей задачи. Тем не менее, если вы занимаетесь глубоким обучением, 3–4 года назад трудно было найти что-то сложнее, чем модель вида Multi-Layered-Perceptron. Настоящим прорывом была TensorFlow: библиотека поставила все на более «функциональные» рейки, позволила более тонкую настройку модели, а также более сложные архитектуры моделей. В начале 2016 Microsoft нанесла ответный удар, и вышел Microsoft Cognitive Toolkit, он же CNTK.

Библиотека делает упор именно на Deep Learning, если быть еще более точным, — на нейронные сети с рекуррентной архитектурой. То есть вы не найдете здесь привычные вам SVM, Decision Tree, NBC и т.д. Только нейронные сети и ничего больше.

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

Вы можете задать вопрос: «а почему я должен использовать CNTK, а не тот же Tensorflow или MXNet?» Около года назад началась «гонка вычислений», которую явно вел CNTK: он делал вычисления на одной/нескольких GPU, Tensorflow же на тот момент предлагал только 1 GPU. Производительность на CIFAR-10, MNIST у CNTK также немного выше. Сейчас же библиотеки идут практически «ноздря к ноздре», и главным критерием выбора я бы назвал инфраструктуру вашего проекта. CNTK в этом плане более гибкий, так как вы можете использовать .Net совместно с Python. Также стоит отметить введение формата ONNX, который делает модели совместимыми с Caffe2, MXNet и т.д.

Итого:

  • Простота эксплуатации модели в продакшене;
  • Возможность экспортирования и создания модели на различные платформы, в том числе .Net;
  • Возможность тонкой настройки модели;
  • Возможность еще более тонкой настройки процесса обучения;
  • Возможность использования GPU, а лучше GPU-кластера;
  • Ясный график и активное развитие библиотеки.

Если хотя бы 3 пункта из 5 вам подходят  — CNTK будет хорошим выбором. Хотя решающим в большинстве случаев будет второй пункт.

Для начала работы хватит даже CPU, операционная система — Windows, Linux. Если же у вас есть видеокарта, то это должна быть NVidia с поддержкой CUDA-ядер. Приступим к установке библиотеки на Windows. Это будет Python GPU, гайд по конфигурации для .Net сильно отличается, поэтому его вы найдете в другой части.

Конфигурация

Заходим на GitHub CNTK. Выбираем версию в соответствии с конфигурацией вашей машины.

Убеждаемся в том, что у нас есть Microsoft Visual C++ 14.0 или старше. При необходимости качаем отсюда.
Если вы собираетесь проводить вычисления на видеокарте, нужно поставить cuDNN v5.1 для CUDA v8.0 (в ближайшее время планируется поддержка CUDA v9.0), после прохождения быстрой регистрации здесь.
Ставим CUDA v8.0.
Добавляем в PATH System variable следующие папки:

  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\bin;
  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v8.0\libnvvp;
  • C:\Program Files\cuDNN\bin.

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

Мы объявили 2 константы — a и b — и присвоили им значения, а их сумму записали в переменную c. Давайте выведем ее значение.

Видим, что переменная c — это Function. В CNTK 70% всего, с чем вы будете работать, представляет собой Function. А у каждой функции есть метод eval, при вызове которого функция вернет значение, что и произошло ниже:

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

Постановка задачи

Раз мы знаем устройство элементарной компоненты, давайте попробуем решить какую-либо задачу с её помощью. Поскольку это Neural Hello World, задача будет простой — обучение операции XOR.

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

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

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

  • EpochsCount — проще говоря, кол-во раз, которые мы покажем датасет нейронной сети. Как говорится, повторение — мать учения;
  • LearningRate — шаг обучения, то, насколько быстро мы учимся;
  • Input_Dim — размерность наших входных данных. Исходя из того, что операция бинарная, размерность наших данных — 2;
  • Output_Dim — размерность результата операции. Имеет размерность 1.

Как было написано выше, у библиотеки несколько функциональный подход. Есть свои особенности задания размерности данных. Это делается при помощи создания некого плейсхолдера — input_variable, который и «подвязывает» мир к сети (input_) и результат работы сети к миру (ouput_) во время обучения.

import cntk as C
import numpy as np
import matplotlib.pyplot as plt

INPUT_DIM = 2
OUTPUT_DIM = 1
EPOCHS_COUNT = 200
LEARNING_RATE = 0.15

input_ = C.input_variable(INPUT_DIM, 'float32')
output_ = C.input_variable(OUTPUT_DIM, 'float32')

Теперь самое интересное — объявление сети. Пока сделаем версию с тангенсом. Что происходит ниже: мы создали модель (функцию), которая содержит в себе 3 слоя (C.layers.Dense(N) — этой функцией можно объявить полносвязный слой, где N — кол-во нейронов в слое). В первом слое у нас 2 нейрона, в следующем — 1, а последний — это выходной слой. Дело в том, что мы еще не подключили входной слой. Это происходит в return’е функции, где мы параметром передаем «входной слой», а вернее некий плейсхолдер для него, всем остальным слоям, которые представлены композицией функций. Параметр init отвечает за случайное распределение, которое инициализирует веса сети.


def create_model(input_):
   with C.layers.default_options(activation=C.sigmoid, init=C.glorot_uniform()):
	 //Модель
       model = C.layers.Sequential([
		//Первый скрытый слой
           C.layers.Dense(2),
		//Второй скрытый слой
           C.layers.Dense(1),
		//Выходной слой
           C.layers.Dense(OUTPUT_DIM)
       ])

   //”Привязывание” входного слоя к модели
   return model(input_)

z = create_model(input_)

Объявим наши датасеты, здесь все просто и понятно:

X = np.array([                  
   [0, 0],
   [0, 1],
   [1, 0],
   [1, 1]
])

Y = np.array([
   [0],
   [1],
   [1],
   [0]
])

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

Расписание

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

lr_schedule = C.learning_rate_schedule(LEARNING_RATE, C.UnitType.minibatch)

Алгоритм оптимизации

Я решил выбрать один из самых универсальных методов градиентного спуска — ADAM. Если не знаете, какой метод использовать, всегда берите ADAM с ускорением 0.9. Его преимущество в том, что он сам знает, когда ему нужно ускориться, а когда замедлиться в процессе обучения, хотя это вновь достаточно высокая материя. Далее идет функция потерь — всем знакомый Squared Error.

learner = C.adam(z.parameters, lr_schedule, momentum=0.9)
loss = C.squared_error(z, output_)

Тренер

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

trainer = C.Trainer(z, loss, [learner])

Визуально процесс обучения будет выглядеть следующим образом:

Финишная прямая

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

datamap = {
   input_: X,
   output_: Y
}

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

losses = []

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

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

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

for i in range(EPOCHS_COUNT):
   trainer.train_minibatch(datamap)
   loss = trainer.previous_minibatch_loss_average
   losses.append(loss)

   if i % 10 == 0:
       print('Loss: {val}'.format(val=loss))

Нарисуем график функции потерь в зависимости от эпох:

plt.plot(losses)
plt.title('Loss function')
plt.ylabel('Loss')
plt.xlabel('Epochs')
plt.show()

У меня вышла следующая картина:

Чем ниже значение функции потерь (ошибок) — тем лучше. Неформально, это можно понимать как то, что начиная с 75-й эпохи нейронная сеть обучилась операции XOR полностью, а ошибка приравнялась к нулю.

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

print(z.eval({input_: X}))

У меня вышел следующий ответ:

[[ 5.00380935e-04]
[ 9.75124002e-01]
[ 9.75660741e-01]
[ 9.76070471e-04]]

Что эквивалентно [[ 0 ][ 1 ][ 1 ][ 0 ]] — результату операции при всех возможных аргументах.

Давайте теперь поэксперементируем, поменяем функцию активации на сигмоид.

def create_model(input_):
   with C.layers.default_options(activation=C.sigmoid, init=C.glorot_uniform()):
       model = C.layers.Sequential([
           C.layers.Dense(2),
           C.layers.Dense(1),
           C.layers.Dense(OUTPUT_DIM)
       ])
   return model(input_)

Запускаем скрипт и смотрим на график функции потерь.

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

[[ 0.07686842]
[ 0.9390229 ]
[ 0.49739015]
[ 0.5020082 ]]

Полная ерунда. Правильны только первые 2 ответа. Давайте увеличим кол-во эпох с 200 до 2000.

Не помогло. Давайте поступим более радикально и в корне поменяем архитектуру сети для сигмоида: сделаем 1 скрытый слой в 4 нейрона, а кол-во эпох — 500:

def create_model(input_):
   with C.layers.default_options(activation=C.sigmoid, init=C.glorot_uniform()):
       model = C.layers.Sequential([
           C.layers.Dense(4),
           C.layers.Dense(OUTPUT_DIM)
       ])
   return model(input_)

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

Смотрим на результаты:

[[ 0.01297866]
[ 0.96145022]
[ 0.94457209]
[ 0.05540414]]

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

Продолжение следует…

Авторы: 

Александр Ганджа, CTO DataTrading

Богдан Домненко, Data Scientist DataTrading

Ещё интересное для вас:
— Тест «Насколько хорошо вы разбираетесь в C#?»
— Блиц-тест «Настоящий ли ты фронтендер?»
— Меньше готовить, больше кодить: обзор питания с доставкой на дом.