Известно, что библиотеки для глубокого обучения (Deep learning) очень хорошо дружат с языком Python: например, библиотека Keras написана на Python и может использовать в качестве бекенда Theano (тоже на Python) или Tensorflow (C++/Python).
Но пользователи R тоже не обделены возможностью обучать глубокие нейронные сети. Помимо недавно появившего интерфейса для Tensorflow, существует не столь известная, но набирающая популярность библиотека mxnet, написанная на C++ и укомплектованная интерфейсами и для R, и для Python (а также для Julia, Matlab, Scala и Javascript!). Данная библиотека обладает высокой производительностью и умеренным расходом памяти, умеет работать как на CPU, так и на GPU Nvidia, используя CUDA/cuDNN (причем можно обучаться сразу на нескольких видеокартах).
Основным препятствием для ее освоения является очень своеобразно написанная и не всегда поддерживаемая в актуальном состоянии документация: если для Python она более-менее полная и последовательная, то в случае с R все плохо. MXNet R Reference Manual является лишь списком функций и аргументов, подавляющее большинство примеров написаны с использованием Python, так что в R приходится действовать по аналогии и искать ответы в issues на Гитхабе. Это сообщение является попыткой слегка исправить ситуацию и создать руководство по установке и использованию mxnet в R. Возможно, будет и продолжение про более продвинутые темы.
Используется Linux; предполагается, что R и RStudio уже установлены. Полезные руководства: раз, два, три, четыре.
Обучать нейросети на CPU долго и скучно, поэтому прежде всего нужна видеокарта от Nvidia. Хорошо, когда она на архитектуре последнего поколения - сейчас это Pascal. Еще лучше, если это GTX 1080 c 8 Гб памяти. Но и на GTX 1050Ti у меня все достаточно бодро работает.
Устанавливаем драйвер по инструкции:
sudo apt-get purge nvidia* # удаляем старый драйвер, если он был
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt update
# 370 заменить на актуальную версию
sudo apt-get install nvidia-370 nvidia-settings
sudo nvidia-xconfig
Устанавливаем CUDA: скачиваем runfile с https://developer.nvidia.com/cuda-downloads, запускаем установку командой sudo sh cuda_8.0.44_linux.run
(в моем случае использовалась версия 8.0.44, у вас может быть более новая, а для старых видеокарт - наоборот, нужна более старая).
Устанавливаем cuDNN: регистриуемся и скачиваем с https://developer.nvidia.com/rdp/cudnn-download, распаковываем, копируем нужные файлы в положенные места:
tar -zxf cudnn-8.0-linux-x64-v5.1.tgz
cd cuda # имя папки после распаковки
sudo cp -P include/cudnn.h /usr/include
sudo cp -P lib64/libcudnn* /usr/lib/x86_64-linux-gnu/
sudo chmod a+r /usr/lib/x86_64-linux-gnu/libcudnn*
За работой видеокарты поможет следить консольная утилита nvidia-smi
.
Ставим нужные зависимости:
sudo apt-get update
sudo apt-get install -y build-essential git libatlas-base-dev libopencv-dev
sudo apt-get install libcurl4-openssl-dev libssl-dev
sudo apt-get install libxml2-dev
При дальнейшей установке, возможно, будет не хватать чего-то еще - ставим аналогичным образом.
Установка самой библиотеки mxnet происходит путем скачивания с Гитхаба и сборки из исходников:
git clone --recursive https://github.com/dmlc/mxnet
cd mxnet
nano make/config.mk
В файл make/config.mk нужно внести следующие изменения, чтобы библиотека собралась с поддержкой GPU:
USE_CUDA = 1
USE_CUDA_PATH = /usr/local/cuda
USE_CUDNN = 1
USE_BLAS = atlas
Вместо atlas
можно указать blas
или openblas
. Также файл make/config.mk можно скопировать на уровень выше (в папку mxnet) и отредактировать копию. Сама сборка запускается командой make -j4
, где 4 - количество используемых ядер. Лучше указать все имеющиеся, потому что процесс компиляции небыстрый. При повторной сборке нужно будет предварительной выполнить команду make clean_all
.
Запускаем из папки mxnet
:
cd R-package
Rscript -e "library(devtools); library(methods); options(repos=c(CRAN='https://cran.rstudio.com')); install_deps(dependencies = TRUE)"
cd ..
make rpkg
R CMD INSTALL mxnet_current_r.tar.gz
После чего вносим изменения в /usr/lib/R/etc/ldpaths
(команда sudo nano /usr/lib/R/etc/ldpaths
):
export CUDA_HOME=/usr/local/cuda
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:${LD_LIBRARY_PATH}
cd ~/mxnet/setup-utils
bash install-mxnet-ubuntu-python.sh
Таким образом будет выполнена установка для системной версии Python, у меня это 2.7 - с Python 3 могут быть проблемы. Если у вас установлен дистрибутив Anaconda, то в ~/.bashrc
нужно закомментировать строку вида export PATH="/home/andrey/anaconda3/bin:$PATH"
. Впрочем, установка с использованием виртуальных окружений Anaconda тоже не представляет сложностей, по ссылке есть описание.
Для корректной работы библиотеки мне пришлось отредактировать файл ~/.bashrc
(команда sudo nano ~/.bashrc
), добавив следующие строки:
export CUDA_HOME=/usr/local/cuda-8.0
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64
PATH=${CUDA_HOME}/bin:${PATH}
export PATH
После чего можно перейти в папку mxnet
и запустить тесты на CPU и GPU:
python example/image-classification/train_mnist.py
python example/image-classification/train_mnist.py --network lenet --gpus 0
Описан процесс подготовки данных, начиная с “сырых” картинок. Для некоторых классических наборов данных уже есть готовые бинарные файлы: http://data.dmlc.ml/mxnet/data/.
Будем использовать набор cifar10, архив train.7z: картинки в формате .png 32х32х3, то есть 32x32 пикселя с тремя цветовыми каналами (RGB). Всего 50000 картинок в 10 классах, по 5000 в каждом. После распаковки назовем папку с картинками data
.
Используемый далее скрипт im2rec.py должен работать с файлами разных форматов, включая .png, но у меня он работал только с .jpg. Мне подказали, что дело может быть в сборке opencv без поддержки формата .png, но сборка opencv из исходников тоже ничего не дала, как и установка командой sudo apt-get install python-opencv
. Так что будем конвертировать в .jpg.
Устанавливаем утилиту imagemagick: sudo apt-get install imagemagick
. Конвертируем .png в .jpg и удаляем исходные файлы в формате .png:
cd ~/R/cifar10/data
for img in *.png
do
convert "$img" "$img.jpg"
done
rm *.png
Теперь файлы имеют имена вида 1.png.jpg. Их нужно переименовать таким образом, чтобы они всегда шли в соответствии с порядком номеров, то есть добавить в начале имени каждого файла нужное количество нулей:
# Исходные имена файлов
files <- list.files("data")
# Создаем вектор из нужного количества 0, которые добавляются к номеру картинки
max_length <- max(sapply(files, nchar))
zeros <- max_length - sapply(files, nchar)
zeros <- paste0(sapply(zeros, function(x) paste(rep(0, x), collapse = "")))
# Новые имена файлов
newnames <- paste0("./data/", zeros, files)
# Полные имена файлов (с папкой, где они лежат)
files <- paste0("./data/", files)
# Переименовываем файлы в формате 00001.png.jpg
Map(function(x, y) file.rename(from = x, to = y), files, newnames)
Осталось разложить файлы по папкам таким образом, чтобы в каждой папке были изображения одного класса:
# Исходные имена файлов
files <- list.files("data")
# Метки классов
labels <- read.table("trainLabels.csv", header = TRUE, sep = ",")
# Создаем папки для каждого класса
lapply(as.character(unique(labels$label)),
function(x) dir.create(path = paste0("./data/", x)))
# Новые имена файлов
newnames <- paste0("./data/", labels$label, "/", files)
# Полные имена файлов (с папкой, где они лежат)
files <- paste0("./data/", files)
# Раскладываем файлы по папкам
Map(function(x, y) file.rename(from = x, to = y), files, newnames)
Библиотека mxnet работает с бинарными файлами формата RecordIO. Создать их можно с помощью идущего в комплекте скрипта на Python, для которого нужно разложить файлы как раз так, как мы только что сделали - каждый класс в отдельной папке. Вначал создается два списка: 80% файлов - обучающая выборка (cifar_train.lst), 20% - проверочная (cifar_val.lst). Затем из файлов в этих списках создаются бинарные файлы cifar_train.rec и cifar_val.rec. Отдельный тестовый набор для финальной оценка качества создавать не будем, ограничимся проверочным.
cd ~/R/cifar10/
python ~/mxnet/tools/im2rec.py --list=1 --recursive=1 --train-ratio=0.8 cifar data
python ~/mxnet/tools/im2rec.py --num-thread=4 --pass-through=1 cifar_train.lst data
python ~/mxnet/tools/im2rec.py --num-thread=4 --pass-through=1 cifar_val.lst data
setwd("~/R/cifar10/")
library(mxnet)
Для обучения нейросети нужны три компонента: итераторы для данных, символьное описание модели (архитектура сети) и собственно вызов функции для обучения. См. примеры, а также basic_model.R.
Создадим функцию, которая будет возвращать итераторы для обучающей и проверочной выборки. Эта функция принимает следующие аргументы: размерность данных, пути к файлам с обучающей и проверочной выборками, размер мини-выборки (batch size):
get_iterator <- function(data_shape,
train_data,
val_data,
batch_size = 128) {
train <- mx.io.ImageRecordIter(
path.imgrec = train_data,
batch.size = batch_size,
data.shape = data_shape,
rand.crop = TRUE,
rand.mirror = TRUE)
val <- mx.io.ImageRecordIter(
path.imgrec = val_data,
batch.size = batch_size,
data.shape = data_shape,
rand.crop = FALSE,
rand.mirror = FALSE
)
return(list(train = train, val = val))
}
Таким образом можно работать со сколь угодно большими наборами данных. Оперативная память и память видеокарты ограничивают только размер мини-выборки, которая загружается вся сразу. Маленькие картинки можно загружать десятками-сотнями, большие - меньшими порциями.
Создаем итераторы, указав размерность (28, 28, 3)
, то есть картинки будут обрезаться до размера 28х28:
data <- get_iterator(data_shape = c(28, 28, 3),
train_data = "/home/andrey/R/cifar10/cifar_train.rec",
val_data = "/home/andrey/R/cifar10/cifar_val.rec",
batch_size = 100)
train <- data$train
val <- data$val
Выше мы указали rand.crop = TRUE
, то есть обрезка до нужного размера будет происходить случайным образом (насколько я понимаю, для каждой эпохи обучения), и это увеличит разнообразие предъявляемых обучающих примеров. rand.mirror = TRUE
с той же целью создает зеркальные варианты изображений. Для проверочных данных никакие трансформации не задаем.
Используем один из вариантов архитектуры Resnet:
conv_factory <- function(data, num_filter, kernel, stride,
pad, act_type = 'relu', conv_type = 0) {
if (conv_type == 0) {
conv = mx.symbol.Convolution(data = data, num_filter = num_filter,
kernel = kernel, stride = stride, pad = pad)
bn = mx.symbol.BatchNorm(data = conv)
act = mx.symbol.Activation(data = bn, act_type = act_type)
return(act)
} else if (conv_type == 1) {
conv = mx.symbol.Convolution(data = data, num_filter = num_filter,
kernel = kernel, stride = stride, pad = pad)
bn = mx.symbol.BatchNorm(data = conv)
return(bn)
}
}
residual_factory <- function(data, num_filter, dim_match) {
if (dim_match) {
identity_data = data
conv1 = conv_factory(data = data, num_filter = num_filter, kernel = c(3, 3),
stride = c(1, 1), pad = c(1, 1), act_type = 'relu', conv_type = 0)
conv2 = conv_factory(data = conv1, num_filter = num_filter, kernel = c(3, 3),
stride = c(1, 1), pad = c(1, 1), conv_type = 1)
new_data = identity_data + conv2
act = mx.symbol.Activation(data = new_data, act_type = 'relu')
return(act)
} else {
conv1 = conv_factory(data = data, num_filter = num_filter, kernel = c(3, 3),
stride = c(2, 2), pad = c(1, 1), act_type = 'relu', conv_type = 0)
conv2 = conv_factory(data = conv1, num_filter = num_filter, kernel = c(3, 3),
stride = c(1, 1), pad = c(1, 1), conv_type = 1)
# adopt project method in the paper when dimension increased
project_data = conv_factory(data = data, num_filter = num_filter, kernel = c(1, 1),
stride = c(2, 2), pad = c(0, 0), conv_type = 1)
new_data = project_data + conv2
act = mx.symbol.Activation(data = new_data, act_type = 'relu')
return(act)
}
}
residual_net <- function(data, n) {
#fisrt 2n layers
for (i in 1:n) {
data = residual_factory(data = data, num_filter = 16, dim_match = TRUE)
}
#second 2n layers
for (i in 1:n) {
if (i == 1) {
data = residual_factory(data = data, num_filter = 32, dim_match = FALSE)
} else {
data = residual_factory(data = data, num_filter = 32, dim_match = TRUE)
}
}
#third 2n layers
for (i in 1:n) {
if (i == 1) {
data = residual_factory(data = data, num_filter = 64, dim_match = FALSE)
} else {
data = residual_factory(data = data, num_filter = 64, dim_match = TRUE)
}
}
return(data)
}
get_symbol <- function(num_classes = 10) {
conv <- conv_factory(data = mx.symbol.Variable(name = 'data'), num_filter = 16,
kernel = c(3, 3), stride = c(1, 1), pad = c(1, 1),
act_type = 'relu', conv_type = 0)
n <- 3 # set n = 3 means get a model with 3*6+2=20 layers, set n = 9 means 9*6+2=56 layers
resnet <- residual_net(conv, n) #
pool <- mx.symbol.Pooling(data = resnet, kernel = c(7, 7), pool_type = 'avg')
flatten <- mx.symbol.Flatten(data = pool, name = 'flatten')
fc <- mx.symbol.FullyConnected(data = flatten, num_hidden = num_classes, name = 'fc1')
softmax <- mx.symbol.SoftmaxOutput(data = fc, name = 'softmax')
return(softmax)
}
# Сеть для 10 классов
resnet <- get_symbol(10)
Поскольку сеть очень глубокая, ее неудобно описывать слой за слоем. Здесь применяются дополнительные функции для создания нужного количества слоев; это не очень интуитивно, но пользоваться готовыми решениями и адаптировать их под свои нужды не так уж сложно. А создать свою сеть из умеренного количества слоев вообще не проблема: объект, содержащий описание n-1 слоев, передается функции-конструктору n-ого слоя. Чтобы узнать, какие бывают слои, воспользуйтесь командой apropos("mx.symbol.")
. Можно даже создавать свои собственные слои, но это уже сложнее, и описания для R я не нашел.
Начинаем обучение модели с использованием GPU (ctx = mx.gpu(0)
) в течение трех эпох, сохраняя модель после каждой эпохи (epoch.end.callback = mx.callback.save.checkpoint("resnet")
):
model <- mx.model.FeedForward.create(
symbol = resnet,
X = train,
eval.data = val,
ctx = mx.gpu(0),
eval.metric = mx.metric.accuracy,
num.round = 3,
learning.rate = 0.05,
momentum = 0.9,
wd = 0.00001,
kvstore = "local",
array.batch.size = 100,
epoch.end.callback = mx.callback.save.checkpoint("resnet"),
batch.end.callback = mx.callback.log.train.metric(150),
initializer = mx.init.Xavier(factor_type = "in", magnitude = 2.34),
optimizer = "sgd"
)
## Start training with 1 devices
## Batch [150] Train-accuracy=0.302533333333333
## Batch [300] Train-accuracy=0.363466666666667
## [1] Train-accuracy=0.397518796992481
## [1] Validation-accuracy=0.4561
## Model checkpoint saved to resnet-0001.params
## Batch [150] Train-accuracy=0.538733333333333
## Batch [300] Train-accuracy=0.565000000000001
## [2] Train-accuracy=0.581825
## [2] Validation-accuracy=0.6296
## Model checkpoint saved to resnet-0002.params
## Batch [150] Train-accuracy=0.6462
## Batch [300] Train-accuracy=0.660499999999999
## [3] Train-accuracy=0.671699999999999
## [3] Validation-accuracy=0.6096
## Model checkpoint saved to resnet-0003.params
В рабочей папке сохранились файлы resnet-0001.params, resnet-0002.params, resnet-0002.params и resnet-symbol.json. В них есть все необходимое, чтобы продолжить обучать модель. Загружаем модель после 3 эпох:
model <- mx.model.load("resnet", 3)
Продолжаем обучение, уменьшив скорость (learning.rate = 0.03
):
model <- mx.model.FeedForward.create(
symbol = model$symbol,
X = train,
eval.data = val,
ctx = mx.gpu(0),
eval.metric = mx.metric.accuracy,
num.round = 12,
learning.rate = 0.03,
momentum = 0.9,
wd = 0.00001,
kvstore = "local",
array.batch.size = 100,
# epoch.end.callback = mx.callback.save.checkpoint("resnet"),
batch.end.callback = mx.callback.log.train.metric(150),
initializer = mx.init.Xavier(factor_type = "in", magnitude = 2.34),
optimizer = "sgd",
arg.params = model$arg.params,
aux.params = model$aux.params
)
## Start training with 1 devices
## Batch [150] Train-accuracy=0.722133333333333
## Batch [300] Train-accuracy=0.727466666666666
## [1] Train-accuracy=0.733508771929825
## [1] Validation-accuracy=0.6953
## Batch [150] Train-accuracy=0.744866666666666
## Batch [300] Train-accuracy=0.750466666666667
## [2] Train-accuracy=0.75545
## [2] Validation-accuracy=0.7529
## Batch [150] Train-accuracy=0.766133333333334
## Batch [300] Train-accuracy=0.769800000000001
## [3] Train-accuracy=0.77345
## [3] Validation-accuracy=0.7685
## Batch [150] Train-accuracy=0.7774
## Batch [300] Train-accuracy=0.7809
## [4] Train-accuracy=0.78505
## [4] Validation-accuracy=0.772
## Batch [150] Train-accuracy=0.795533333333333
## Batch [300] Train-accuracy=0.797933333333334
## [5] Train-accuracy=0.8005
## [5] Validation-accuracy=0.7801
## Batch [150] Train-accuracy=0.807466666666667
## Batch [300] Train-accuracy=0.809100000000001
## [6] Train-accuracy=0.811400000000001
## [6] Validation-accuracy=0.7899
## Batch [150] Train-accuracy=0.8132
## Batch [300] Train-accuracy=0.814500000000001
## [7] Train-accuracy=0.816875
## [7] Validation-accuracy=0.7833
## Batch [150] Train-accuracy=0.8214
## Batch [300] Train-accuracy=0.823466666666668
## [8] Train-accuracy=0.824075000000001
## [8] Validation-accuracy=0.8008
## Batch [150] Train-accuracy=0.829466666666667
## Batch [300] Train-accuracy=0.829433333333335
## [9] Train-accuracy=0.831825000000001
## [9] Validation-accuracy=0.7973
## Batch [150] Train-accuracy=0.836733333333333
## Batch [300] Train-accuracy=0.837
## [10] Train-accuracy=0.83925
## [10] Validation-accuracy=0.7949
## Batch [150] Train-accuracy=0.8382
## Batch [300] Train-accuracy=0.840033333333334
## [11] Train-accuracy=0.8414
## [11] Validation-accuracy=0.7961
## Batch [150] Train-accuracy=0.8482
## Batch [300] Train-accuracy=0.846933333333334
## [12] Train-accuracy=0.8481
## [12] Validation-accuracy=0.8098
Видим, что модель начинает переобучаться, потому что качество на обучающих данных растет, а на проверочных - нет. Хотя было бы полезно продолжить обучение и посмотреть, действительно ли это плато, или будет еще небольшой рост. Очень радует скорость работы, на обучение ушли считанные минуты.
Архитектуру сети можно визуализировать (примеры):
graph.viz(model$symbol$as.json(), graph.width.px = 1200, graph.height.px = 3000)
На этом рассмотрение основ заканчивается, продолжение следует.