Эта статья является первой частью руководства по приготовления нейронных сетей с использованием библиотеки mxnet на языке R. Источником вдохновения послужила онлайн-книга Deep Learning — The Straight Dope, объема которой достаточно для осознанного использования mxnet на Питоне. Примеры оттуда будут воспроизводиться с поправкой на отсутствие реализации интерфейса Gluon для R. В первой части рассмотрим установку библиотеки и общие принципы работы, а также реализуем простую линейную модель для решения задачи регрессии.
1. Установка библиотеки mxnet
Понятное руководство по установке разных версий (CPU/GPU) для разных языков и под разные устройства появилось относительно недавно. Подготовительные этапы типа установки драйверов и CUDA/cuDNN были рассмотрены в этом сообщении, повторяться не будем. Для Python, кстати, можно ничего не собирать из исходников, а просто ставить командой pip install mxnet --pre. В ситуации, когда ядер CPU относительно много, а ОЗУ относительно мало, при сборке из исходников можно столкнуться с нехваткой памяти. В таком случае следует запустить сборку в однопоточном режиме: make -j1.
Библиотеку можно использовать также в коде на языках Julia, Scala и, внезапно, Perl. А еще заявлена поддержка работы на Raspberry Pi 3.
2. Используемый набор данных
Создадим искусственный набор данных из 10000 наблюдений (num_examples) с двумя признаками (num_inputs) и одной целевой переменной (num_outputs). Истинная форма зависимости между предикторами и целевой переменной задана функцией real_fn(), к сгенерированных при помощи этой функции значениям добавлен небольшой гауссовский шум 0.01 * rnorm(num_examples):
За обучение нейросетей прямого распространения отвечает функция mx.model.FeedForward.create(). Список принимаемых ею параметров выглядит следующим образом:
symbol — архитектура сети в виде символьного описания. Это не совсем граф вычислений, поскольку в нем не заданы размерности тензоров и функция потерь, но что-то похожее. Архитектуру можно нарисовать при помощи функции graph.viz(), пример будет ниже;
X — матрица/массив или итератор данных, используемых для обучения. Массивы используем, когда данные целиком помещаются в память, а итераторы — когда не помещаются. Есть несколько готовых итераторов (по массивам, по картинкам в бинарном формате RecordIO, по csv-файлам) и возможность создавать свои собственные;
y — значения целевой переменной. Задается только в случае, когда X является массивом, в противном случае итератор должен возвращать все необходимое для обучения, включая правильные ответы;
ctx — устройство или список устройств, используемых для обучения (CPU/GPU). Объект класса MXContext, создаваемый при помощи mx.cpu() или mx.gpu();
begin.round — начальное значение счетчика итераций (эпох) обучения. Менять значение по умолчанию (1) нужно только при дообучении модели;
num.round — количество эпох;
optimizer — используемый оптимизатор, заданный по имени (в виде строки). По умолчанию — стохастический градиентный спуск ("sgd"). Пример настройки параметров оптимизатора представлен ниже;
eval.data — список вида list(data = R.array, label = R.array) или итератор с валидационными данными;
eval.metric — функция для оценка качества модели. Помимо доступных вариантов можно также использовать собственноручно написанную;
epoch.end.callback — функция, запускаемая после каждой эпохи обучения;
batch.end.callback — функция, запускаемая после каждого батча;
array.batch.size — размер батча при использовании данных в виде массива. При использовании итераторов размер мбатча в них же и задается;
array.layout — "auto", "colmajor" или "rowmajor" (по умолчанию — "auto"). Формат массивов: для матрицы "rowmajor" означает, что dim(X) = c(nexample, nfeatures), а "colmajor" — что dim(X) = c(nfeatures, nexample). Формат "rowmajor" допустим только для матриц. Функция mx.io.arrayiter() в явном виде потребует преобразовать данные к формату "colmajor", т.е. матрица будет транспонированной по отношению к привычному виду с наблюдениями в строках и признаками в столбцах;
kvstore — задается в виде строки и отвечает за схему синхронизации при обучении на нескольких устройства. По умолчанию равен "local";
verbose — отвечает за вывод информационных сообщений в процессе обучения. По умолчанию равен TRUE;
aux.params — аналогичный список дополнительных параметров;
input.names — имена символов, подаваемых на вход;
output.names — имена символов, получаемых на выходе;
fixed.param — параметры, которые остаются фиксированными в ходе обучения (не обучаются);
allow.extra.params — разрешает передавать дополнительные параметры, которые не требуются согласно символьному описанию модели. Если задать равным TRUE, при наличии таких лишних параметров в списках arg.params и aux.params ошибка появляться не будет.
Функция принимает на вход массив или матрицу признаков (в данном случае, как уже было сказано, матрицу нужно транспонировать), массив со значениями целевой переменной и размер батча. Также мы включили перемешивание наблюдений опцией shuffle = TRUE. Список всех доступных итераторов выглядит так:
Сейчас мы не будем рассматривать ни остальные варианты, ни написание собственных итераторов.
5. Архитектура сети
Архитектура сети описывается путем последовательных вызовов функций семейства mx.symbol.*, каждая из которых добавляет к модели абстрактные представления слоев: полносвязного, сверточного, пулингового и других. Слоев доступно очень много:
Инициализатор определяет, с каких начальных значений стартует обучение нейросети. Поскольку наша сеть очень простая и неглубокая, достаточно инициализировать веса случайными значениями, имеющими нормальное распределение:
initializer <- mx.init.normal(sd = 0.1)
Единственным принимаемым параметров в данном случае является стандартное отклонение. Также имеется инициализатор mx.init.uniform(), единственным параметром которого является граница диапазона, из которого генерируются значения.
Для глубоких сетей правильная инициализация весов имеет большое значение, поэтому мы бы воспользовались вариантом mx.init.Xavier().
Эта схема инициализации весов была придумана в 2010 году Йошуа Бенджио и Ксавье Глоро (Xavier Gloro), в честь которого метод и получил свое название. В настоящий момент используется повсеместно под разными именами, например, в Keras можно найти glorot_normalglorot_uniform. Библиотека для Python содержит также множество других инициализаторов, недоступных в варианте для R. Например, отсутствует возможность использовать предпочтительный вариант инициалиализции весов нейронов с функцией активации ReLU — инициализацию Хе.
Параметры функции mx.init.Xavier():
rnd_type — строка, задающая вид распределения ("uniform" или "gaussian"), из которого будут генерироваться веса;
factor_type — "avg", "in" или "out" (см. ниже);
magnitude — не совсем понятный числовой параметр, задающий масштаб получаемых весов.
Если rnd_type = "uniform" и factor_type = "avg" (по умолчанию), веса будут инициализированы случайными значениями из диапазона , где , — число нейронов на входе (т.е. в предыдущем слое), — число нейронов на выходе (т.е. в следующем слое).
Если rnd_type = "uniform" и factor_type = "in, то . Аналогично, при rnd_type = "uniform" и factor_type = "out" получим .
При rnd_type = "gaussian" и factor_type = "avg" веса будут извлекаться из нормального распределения со стандартным отклонением .
7. Оптимизатор
Оптимизатор определяет способ обновления весов сети. Доступные варианты: sgd, rmsprop, adam, adagrad и adadelta. Создать оптимизатор с нужными настройками можно при помощи общей функции mx.opt.create():
Сами эти функции привычным способом вызвать нельзя (но к ним можно получить доступ: mxnet:::mx.opt.adagrad).
Параметры mx.opt.sgd()
learning.rate — скорость обучения;
momentum — момент;
wd — коэффициент l2-регуляризации (добавляет штраф за большие веса);
rescale.grad — значение, на которое умножается полученный градиент перед обновлением весов. Часто берется равным 1 / batch_size;
clip_gradient — ограничение величин градиентов путем их проекции на интервал ;
lr_scheduler — планировщик изменения скорость обучения.
Параметры mx.opt.rmsprop()
learning.rate — скорость обучения;
gamma1 — коэффициент затухания для скользящего среднего квадратов градиентов;
gamma2 — момент;
wd — коэффициент l2-регуляризации (добавляет штраф за большие веса);
rescale.grad — значение, на которое умножается полученный градиент перед обновлением весов. Часто берется равным 1 / batch_size;
clip_gradient — ограничение величин градиентов путем их проекции на интервал ;
lr_scheduler — планировщик изменения скорость обучения.
Параметры mx.opt.adadelta():
rho — коэффициент затухания для квадратов градиентов и квадратов обновлений параметров;
epsilon — маленькая константа (1e-05), чтобы избежать деления на 0;
wd — коэффициент l2-регуляризации (добавляет штраф за большие веса);
rescale.grad — значение, на которое умножается полученный градиент перед обновлением весов. Часто берется равным 1 / batch_size;
clip_gradient — ограничение величин градиентов путем их проекции на интервал .
Обратите внимание: для скорости обучения параметр не предусмотрен.
Оптимизатор Adadelta похож на RMSprop, но Adadelta делает вторую поправку с изменением единиц и хранением истории обновлений, а RMSprop просто использует корень из среднего от квадратов градиентов. Следующий алгоритм — Adagrad — использует сглаженные версии среднего и среднеквадратичного градиентов. Подробнее обо всем этом можно прочитать в книге Глубокое обучение, которая рекомендуется к прочтению целиком.
Параметры mx.opt.adagrad():
learning.rate — скорость обучения;
epsilon — маленькая константа (1e-08), чтобы избежать деления на 0;
wd — коэффициент l2-регуляризации (добавляет штраф за большие веса);
rescale.grad — значение, на которое умножается полученный градиент перед обновлением весов. Часто берется равным 1 / batch_size;
clip_gradient — ограничение величин градиентов путем их проекции на интервал ;
lr_scheduler — планировщик изменения скорость обучения.
Параметры mx.opt.adam():
learning.rate — скорость обучения;
beta1 — коэффициент затухания для первой оценки момента;
beta2 — коэффициент затухания для второй оценки момента;
epsilon — маленькая константа (1e-08), чтобы избежать деления на 0;
wd — коэффициент l2-регуляризации (добавляет штраф за большие веса);
rescale.grad — значение, на которое умножается полученный градиент перед обновлением весов. Часто берется равным 1 / batch_size;
clip_gradient — ограничение величин градиентов путем их проекции на интервал ;
lr_scheduler — планировщик изменения скорость обучения.
8. Функции обратного вызова (колбэки)
Сохранять историю обучения будем при помощи соответствующей callback-функции:
logger <- mx.metric.logger() epoch.end.callback <- mx.callback.log.train.metric( period = 1, # число батчей, после которого оценивается метрика logger = logger)
После обучения объект logger будет содержать информацию вида
Другие колбэки: mx.callback.early.stop() отвечает за раннюю остановку, mx.callback.log.speedometer() выводит скорость обработки с заданной частотой, mx.callback.save.checkpoint() сохраняет модель через заданные промежутки в файл с заданным префиксом.
9. Обучение модели
Обучение запускается вызовом описанной выше функции mx.model.FeedForward.create():
model <- mx.model.FeedForward.create( symbol = linreg, X = train_data, ctx = mx.cpu(), num.round = 5, initializer = initializer, optimizer = optimizer, eval.metric = mx.metric.rmse, epoch.end.callback = epoch.end.callback) ## Start training with 1 devices ## [1] Train-rmse=2.39517188021255 ## [2] Train-rmse=0.34100598193831 ## [3] Train-rmse=0.0498822148288494 ## [4] Train-rmse=0.0120600163293274 ## [5] Train-rmse=0.00946668211065784
Все работает, можно переходить к более серьезному примеру!
10. Решение задачи регрессии на реальных данных
Рассмотрим решение задачи регрессии на примере прогнозирования степени проникаемости (по сути — скорости пассивной диффузии) вещества через монослой клеток исходя из известных свойств этого вещества (подробнее см. здесь).
Зачем это нужно
Существует особая процедура регистрации генерических лекарственных препаратов — так называемый биовейвер. Она подразумевает оценку биоэквивалентности путем проведения тестов на растворимость и проникаемость (in vitro) вместо сравнительных фармакокинетических/фармакодинамических/клинических испытаний (in vivo). В качестве стандартной модели для оценки проникаемости веществ используется монослой клеток линии Caco2. Если научиться заранее предсказывать степень проникаемости, появится возможность более осознанно подходить к выбору веществ-кандидатов, проверяемых в ходе экспериментов.
Описанной задаче посвящена работа ADME evaluation in drug discovery. 5. Correlation of Caco-2 permeation with simple molecular properties. В ней приводится таблица, содержащая характеристики 77 различных по структуре веществ, а также экспериментальные данные по проникаемости этих веществ. Следует отметить, что данные, полученные для одного и того же вещества в ходе разных экспериментов, могут значительно варьировать. Но мы не будем углубляться в этот аспект проблемы, а просто возьмем те данные, с которыми работали авторы публикации.
Загрузим данные при помощи кода, созданного полезной функцией dump():
Переменные rgyr и rgyr_d, а также HCPSA и TPSA предсказуемо сильно коррелируют, поскольку в обоих случаях пары переменных представляют собой разные способы расчета одной и той же физической величины.
Обучим такую же нейронную сеть, как и в предыдущем примере. На этот раз разобъем выборку на обучающую и проверочную, и в этот раз обойдемся без итераторов.