Введение
Сейчас практически невозможно представить себе мир без параллельных вычислений. Параллелят все и вся, даже у мобильных телефонов теперь несколько ядер, а значит… ну вы понимаете. Но давайте поговорим не о мобильных приложениях, а о более полезных и интересных вещах. О машинном обучении. Тема тоже модная, разрекламированная, про машинное обучение слышали даже домохозяйки и только ленивый еще не трогал это руками. Для машинного обучения, и если быть более точным, для статистических расчетов есть множество разных фреймворков, на мой вкус лучший из них – R (да простят меня поклонники Octave). И речь пойдет именно о нем.
Disclaimer:
я не претендую на особую строгость изложения, моя задача донести до читателей общую мысль.
R всем хорош и пригож, но у него есть два ограничения, которые почти никак не мешают при работе с небольшим количеством данных и очень сильно портят жизнь при работе с большими корпусами:
- весь код выполняется в одном процессе
- все данные хранятся в памяти
Это почти всегда так, если не очень задумываться об этом. А что делать, если вы задумались об этом, я и расскажу.
Работа без циклов vs foreach
Те, кто писал на R, знают, что в языке вообще-то не принято использовать циклы. Обычно программы используют операции со списками (apply и его собратьев), которые на практике оказываются эффективнее обычного for, потому что внутри может происходить и происходит всякая магия. Да и в философию языка такой подход гораздо лучше вписывается, посмотрите:
a <- c(); for (i in 1:4) { a<- c(a, i^2) } # тупо цикл
a <- 1:4 * 1:4 # прекрасно, не правда ли?
Если нужно сделать что-то нетривиальное, apply можно применить apply. Например, тот же результат можно получить так:
a <- sapply(1:4, function(x) {x^2})
Семейство apply функций позволяет много вкусностей, которые работают мгновенно и очень радуют глаз. Конечно, такой подход требует некоторой сноровки, но тренировка окупается с лихвой.
А где же тут параллельность, спросите вы? Тут-то и кроется самое интересное.
Пришли обычные люди и подумали: «Все эти apply, списки и т.д. – это хорошо. Но я не хочу переучиваться. Дайте мне знакомый инструмент». И такой инструмент появился. Найти его можно в пакете foreach.
Собственно, нас интересует одноименная функция, прелесть которой заключается в том, что она умеет комбинировать результаты, полученные на каждом шаге цикла (чего не умеет оригинальный for, см. извращения с ним выше). Причем комбинировать можно не просто какими-то способами – foreach можно скормить ваш собственный комбайнер или любую подходящую функцию. Самые частые и полезные – c, cbind, rbind.
a <- foreach(i=1:4, .combine='c') %do% {i^2}
Поехали!
Как эффективнее всего запустить параллельный процесс вычисления? Разбить задачу на маленькие независимые кусочки. Именно так и работает параллелизация в R. В качестве основы работает модуль foreach и doSNOW.
Создадим «кластер» для наших вычислений и запустим на нем простенький тест.
# подключим модули
library(foreach)
library(doSNOW)
cl <- makeCluster(4)
registerDoSNOW(cl)
a <- mean(foreach(i=1:10^6, .combine='c') %dopar% {mean(rnorm(i))})
stopCluster(cl)
Для отладки можно использовать опцию %do% а в реальной жизни – %dopar%.
Важно: код, который запускается внутри параллельного цикла должен быть максимально независимым. К примеру, он не сможет увидеть функции, определенные в вашем workspace, поэтому часто их определяют либо прямо внутри %dopar%, либо выносят все необходимые действия в отдельный исходный файл, а затем подключают его через source('trololo.R').
Кластеры doSNOW бывают разные. По сути, кластер – это набор инстансов R, выполняющих код внутри %dopar%. Все они могут находиться на одной машине, но также могут быть разнесены на разные.
cl1 <- makeCluster(c("localhost","remotehost"), type = "SOCK")
Всего доступно четыре типа кластеров: PVM(http://www.csm.ornl.gov/pvm/), MPI(http://cran.r-project.org/web/packages/Rmpi/index.html), NWS (http://nws-r.sourceforge.net/) и SOCK. Для первых трех потребуются дополнительные библиотеки (собственно, реализации), по умолчанию запустится socket-кластер.
Для отладки и всякой разной статистики можно использовать функцию snow.time, оборачивая в нее все обращения к кластеру:
cl <- makeCluster(4)
registerDoSNOW(cl)
tm <- snow.time(a <- mean(foreach(i=1:10^6, .combine='c') %dopar% {mean(rnorm(i))}))
print(tm)
plot(tm)
stopCluster(cl)
Получаются примерно такие картинки:
А нафига козе баян?
Наверное, еще много чего можно написать, но после этих примеров, я думаю, что все разберуться куда копать.
Зачем все это может понадобиться? Да много зачем, как минимум, мы оптимизируем расход времени, а как максимум теперь можно дождаться окончания тяжелых подсчетов. Я использовал подобный подход в основном при проверке каких либо гипотез, когда необходимо было либо
- прогнать расчет на очень большом временном периоде (первоначальная оценка работоспособности биржевой стратегии)
- запустить расчет на множестве похожих dataset-ов (RxQ-кроссвалидация)
Думаю, что у вас получится использовать этот подход в своих целях. Удачи!
Автор: danilchenko