
Čau Habr!
Pagājušā gada rudenī Kaggle rīkoja ar roku zīmētu attēlu klasifikācijas konkursu Quick Draw Doodle Recognition, kurā cita starpā piedalījās R-zinātnieku komanda: , и . Mēs neaprakstīsim konkursu sīkāk, tas jau ir izdarīts .
Šoreiz neveicās ar medaļu audzēšanu, taču tika iegūta liela vērtīga pieredze, tāpēc vēlos pastāstīt sabiedrībai par vairākām interesantākajām un noderīgākajām lietām Kaglē un ikdienas darbā. Starp apspriestajām tēmām: grūta dzīve bez OpenCV, JSON parsēšana (šajos piemēros apskatīta C++ koda integrācija skriptos vai paketēs R, izmantojot Rcpp), skriptu parametrizēšana un gala risinājuma dokerizācija. Viss ziņojuma kods izpildei piemērotā formā ir pieejams .
Saturs:
1. Efektīvi ielādējiet datus no CSV datu bāzē MonetDB
Dati šajā konkursā tiek sniegti nevis gatavu attēlu veidā, bet gan 340 CSV failu veidā (katrai klasei viens fails), kas satur JSON ar punktu koordinātām. Savienojot šos punktus ar līnijām, mēs iegūstam galīgo attēlu, kura izmērs ir 256x256 pikseļi. Katram ierakstam ir arī etiķete, kas norāda, vai attēlu pareizi atpazina datu kopas vākšanas laikā izmantotais klasifikators, attēla autora dzīvesvietas valsts divu burtu kods, unikālais identifikators, laika zīmogs. un klases nosaukums, kas atbilst faila nosaukumam. Sākotnējo datu vienkāršotā versija arhīvā sver 7.4 GB un pēc izpakošanas aptuveni 20 GB, pilnie dati pēc izpakošanas aizņem 240 GB. Organizatori nodrošināja, ka abās versijās tika reproducēti vieni un tie paši zīmējumi, kas nozīmē, ka pilna versija bija lieka. Jebkurā gadījumā 50 miljonu attēlu glabāšana grafiskajos failos vai masīvu veidā nekavējoties tika uzskatīta par nerentablu, un mēs nolēmām apvienot visus CSV failus no arhīva. train_simplified.zip datubāzē ar sekojošu vajadzīgā izmēra attēlu ģenerēšanu “lidojumā” katrai partijai.
Par DBVS tika izvēlēta labi pārbaudīta sistēma MonetDB, proti, R kā pakotnes ieviešana . Paketē ir iekļauta datu bāzes servera iegultā versija, un tā ļauj paņemt serveri tieši no R sesijas un strādāt ar to tur. Datu bāzes izveide un pieslēgšana tai tiek veikta ar vienu komandu:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))Mums būs jāizveido divas tabulas: viena visiem datiem, otra pakalpojuma informācijai par lejupielādētajiem failiem (noder, ja kaut kas noiet greizi un process ir jāatsāk pēc vairāku failu lejupielādes):
Tabulu veidošana
if (!DBI::dbExistsTable(con, "doodles")) {
DBI::dbCreateTable(
con = con,
name = "doodles",
fields = c(
"countrycode" = "char(2)",
"drawing" = "text",
"key_id" = "bigint",
"recognized" = "bool",
"timestamp" = "timestamp",
"word" = "text"
)
)
}
if (!DBI::dbExistsTable(con, "upload_log")) {
DBI::dbCreateTable(
con = con,
name = "upload_log",
fields = c(
"id" = "serial",
"file_name" = "text UNIQUE",
"uploaded" = "bool DEFAULT false"
)
)
}Ātrākais veids, kā ielādēt datus datu bāzē, bija tieši kopēt CSV failus, izmantojot komandu SQL - COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTKur tablename - tabulas nosaukums un path - ceļš uz failu. Strādājot ar arhīvu, tika atklāts, ka iebūvētā ieviešana unzip in R nedarbojas pareizi ar vairākiem failiem no arhīva, tāpēc mēs izmantojām sistēmu unzip (izmantojot parametru getOption("unzip")).
Funkcija rakstīšanai datu bāzē
#' @title Извлечение и загрузка файлов
#'
#' @description
#' Извлечение CSV-файлов из ZIP-архива и загрузка их в базу данных
#'
#' @param con Объект подключения к базе данных (класс `MonetDBEmbeddedConnection`).
#' @param tablename Название таблицы в базе данных.
#' @oaram zipfile Путь к ZIP-архиву.
#' @oaram filename Имя файла внури ZIP-архива.
#' @param preprocess Функция предобработки, которая будет применена извлечённому файлу.
#' Должна принимать один аргумент `data` (объект `data.table`).
#'
#' @return `TRUE`.
#'
upload_file <- function(con, tablename, zipfile, filename, preprocess = NULL) {
# Проверка аргументов
checkmate::assert_class(con, "MonetDBEmbeddedConnection")
checkmate::assert_string(tablename)
checkmate::assert_string(filename)
checkmate::assert_true(DBI::dbExistsTable(con, tablename))
checkmate::assert_file_exists(zipfile, access = "r", extension = "zip")
checkmate::assert_function(preprocess, args = c("data"), null.ok = TRUE)
# Извлечение файла
path <- file.path(tempdir(), filename)
unzip(zipfile, files = filename, exdir = tempdir(),
junkpaths = TRUE, unzip = getOption("unzip"))
on.exit(unlink(file.path(path)))
# Применяем функция предобработки
if (!is.null(preprocess)) {
.data <- data.table::fread(file = path)
.data <- preprocess(data = .data)
data.table::fwrite(x = .data, file = path, append = FALSE)
rm(.data)
}
# Запрос к БД на импорт CSV
sql <- sprintf(
"COPY OFFSET 2 INTO %s FROM '%s' USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORT",
tablename, path
)
# Выполнение запроса к БД
DBI::dbExecute(con, sql)
# Добавление записи об успешной загрузке в служебную таблицу
DBI::dbExecute(con, sprintf("INSERT INTO upload_log(file_name, uploaded) VALUES('%s', true)",
filename))
return(invisible(TRUE))
}Ja pirms rakstīšanas datu bāzē tabula ir jāpārveido, pietiek ar argumenta ievadīšanu preprocess funkcija, kas pārveidos datus.
Kods datu secīgai ielādei datu bāzē:
Datu ierakstīšana datu bāzē
# Список файлов для записи
files <- unzip(zipfile, list = TRUE)$Name
# Список исключений, если часть файлов уже была загружена
to_skip <- DBI::dbGetQuery(con, "SELECT file_name FROM upload_log")[[1L]]
files <- setdiff(files, to_skip)
if (length(files) > 0L) {
# Запускаем таймер
tictoc::tic()
# Прогресс бар
pb <- txtProgressBar(min = 0L, max = length(files), style = 3)
for (i in seq_along(files)) {
upload_file(con = con, tablename = "doodles",
zipfile = zipfile, filename = files[i])
setTxtProgressBar(pb, i)
}
close(pb)
# Останавливаем таймер
tictoc::toc()
}
# 526.141 sec elapsed - копирование SSD->SSD
# 558.879 sec elapsed - копирование USB->SSDDatu ielādes laiks var atšķirties atkarībā no izmantotā diskdziņa ātruma īpašībām. Mūsu gadījumā lasīšana un rakstīšana vienā SSD vai no zibatmiņas diska (avota faila) uz SSD (DB) aizņem mazāk nekā 10 minūtes.
Ir vajadzīgas vēl dažas sekundes, lai izveidotu kolonnu ar veselu skaitļu klases apzīmējumu un indeksa kolonnu (ORDERED INDEX) ar rindu numuriem, pēc kuriem tiks ņemti novērojumi, veidojot partijas:
Papildu kolonnu un indeksa izveide
message("Generate lables")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD label_int int"))
invisible(DBI::dbExecute(con, "UPDATE doodles SET label_int = dense_rank() OVER (ORDER BY word) - 1"))
message("Generate row numbers")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD id serial"))
invisible(DBI::dbExecute(con, "CREATE ORDERED INDEX doodles_id_ord_idx ON doodles(id)"))Lai atrisinātu partijas izveides problēmu lidojumā, mums bija jāsasniedz maksimālais ātrums nejaušu rindu izvilkšanai no tabulas doodles. Šim nolūkam mēs izmantojām 3 trikus. Pirmais bija samazināt tā veida izmēru, kurā tiek saglabāts novērojuma ID. Sākotnējā datu kopā ID glabāšanai nepieciešamais veids ir bigint, bet novērojumu skaits ļauj ievietot to identifikatorus, kas vienādi ar kārtas numuru int. Šajā gadījumā meklēšana ir daudz ātrāka. Otrs triks bija izmantot ORDERED INDEX — pie šāda lēmuma nonācām empīriski, izpētot visu pieejamo . Trešais bija parametrizētu vaicājumu izmantošana. Metodes būtība ir vienreiz izpildīt komandu PREPARE ar vēlāku sagatavotas izteiksmes izmantošanu, veidojot tāda paša veida vaicājumu kopumu, taču patiesībā ir priekšrocība salīdzinājumā ar vienkāršu SELECT izrādījās statistiskās kļūdas robežās.
Datu augšupielādes process patērē ne vairāk kā 450 MB RAM. Tas nozīmē, ka aprakstītā pieeja ļauj pārvietot datu kopas, kas sver desmitiem gigabaitu, gandrīz uz jebkura budžeta aparatūras, ieskaitot dažas vienas plates ierīces, kas ir diezgan forši.
Atliek tikai izmērīt (nejauši) datu izguves ātrumu un novērtēt mērogošanu, ņemot dažāda lieluma partijas:
Datu bāzes etalons
library(ggplot2)
set.seed(0)
# Подключение к базе данных
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
# Функция для подготовки запроса на стороне сервера
prep_sql <- function(batch_size) {
sql <- sprintf("PREPARE SELECT id FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ","))
res <- DBI::dbSendQuery(con, sql)
return(res)
}
# Функция для извлечения данных
fetch_data <- function(rs, batch_size) {
ids <- sample(seq_len(n), batch_size)
res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
return(res)
}
# Проведение замера
res_bench <- bench::press(
batch_size = 2^(4:10),
{
rs <- prep_sql(batch_size)
bench::mark(
fetch_data(rs, batch_size),
min_iterations = 50L
)
}
)
# Параметры бенчмарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]
# batch_size min median max `itr/sec` total_time n_itr
# <dbl> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 16 23.6ms 54.02ms 93.43ms 18.8 2.6s 49
# 2 32 38ms 84.83ms 151.55ms 11.4 4.29s 49
# 3 64 63.3ms 175.54ms 248.94ms 5.85 8.54s 50
# 4 128 83.2ms 341.52ms 496.24ms 3.00 16.69s 50
# 5 256 232.8ms 653.21ms 847.44ms 1.58 31.66s 50
# 6 512 784.6ms 1.41s 1.98s 0.740 1.1m 49
# 7 1024 681.7ms 2.72s 4.06s 0.377 2.16m 49
ggplot(res_bench, aes(x = factor(batch_size), y = median, group = 1)) +
geom_point() +
geom_line() +
ylab("median time, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE) 
2. Partiju sagatavošana
Viss partijas sagatavošanas process sastāv no šādām darbībām:
- Vairāku JSON parsēšana, kas satur virkņu vektorus ar punktu koordinātām.
- Krāsainu līniju zīmēšana, pamatojoties uz punktu koordinātām uz vajadzīgā izmēra attēla (piemēram, 256×256 vai 128×128).
- Iegūto attēlu pārvēršana tensorā.
Python kodolu konkurences ietvaros problēma tika atrisināta galvenokārt, izmantojot OpenCV. Viens no vienkāršākajiem un acīmredzamākajiem R analogiem izskatītos šādi:
JSON konvertēšana uz Tensoru programmā R
r_process_json_str <- function(json, line.width = 3,
color = TRUE, scale = 1) {
# Парсинг JSON
coords <- jsonlite::fromJSON(json, simplifyMatrix = FALSE)
tmp <- tempfile()
# Удаляем временный файл по завершению функции
on.exit(unlink(tmp))
png(filename = tmp, width = 256 * scale, height = 256 * scale, pointsize = 1)
# Пустой график
plot.new()
# Размер окна графика
plot.window(xlim = c(256 * scale, 0), ylim = c(256 * scale, 0))
# Цвета линий
cols <- if (color) rainbow(length(coords)) else "#000000"
for (i in seq_along(coords)) {
lines(x = coords[[i]][[1]] * scale, y = coords[[i]][[2]] * scale,
col = cols[i], lwd = line.width)
}
dev.off()
# Преобразование изображения в 3-х мерный массив
res <- png::readPNG(tmp)
return(res)
}
r_process_json_vector <- function(x, ...) {
res <- lapply(x, r_process_json_str, ...)
# Объединение 3-х мерных массивов картинок в 4-х мерный в тензор
res <- do.call(abind::abind, c(res, along = 0))
return(res)
}Zīmēšana tiek veikta, izmantojot standarta R rīkus, un saglabāta pagaidu PNG failā, kas tiek glabāts RAM atmiņā ( Linux R pagaidu direktoriji atrodas direktorijā /tmp, uzstādīts RAM). Pēc tam šis fails tiek nolasīts kā trīsdimensiju masīvs ar skaitļiem no 0 līdz 1. Tas ir svarīgi, jo tradicionālāks BMP tiktu nolasīts neapstrādātā masīvā ar heksadecimālo krāsu kodiem.
Pārbaudīsim rezultātu:
zip_file <- file.path("data", "train_simplified.zip")
csv_file <- "cat.csv"
unzip(zip_file, files = csv_file, exdir = tempdir(),
junkpaths = TRUE, unzip = getOption("unzip"))
tmp_data <- data.table::fread(file.path(tempdir(), csv_file), sep = ",",
select = "drawing", nrows = 10000)
arr <- r_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256 3
plot(magick::image_read(arr)) 
Pati partija tiks veidota šādi:
res <- r_process_json_vector(tmp_data[1:4, drawing], scale = 0.5)
str(res)
# num [1:4, 1:128, 1:128, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
# - attr(*, "dimnames")=List of 4
# ..$ : NULL
# ..$ : NULL
# ..$ : NULL
# ..$ : NULLŠī realizācija mums šķita neoptimāla, jo lielu partiju veidošana aizņem nepieklājīgi ilgu laiku, un mēs nolēmām izmantot kolēģu pieredzi, izmantojot jaudīgu bibliotēku. OpenCV. Toreiz nebija gatavas pakotnes priekš R (tagad tādas nav), tāpēc minimāla nepieciešamās funkcionalitātes realizācija tika uzrakstīta C++ ar integrāciju R kodā, izmantojot Rcpp.
Lai atrisinātu problēmu, tika izmantotas šādas pakotnes un bibliotēkas:
OpenCV darbam ar attēliem un līniju zīmēšanai. Izmantotas iepriekš instalētas sistēmas bibliotēkas un galvenes faili, kā arī dinamiskā saite.
xtensor darbam ar daudzdimensiju masīviem un tenzoriem. Mēs izmantojām galvenes failus, kas iekļauti tāda paša nosaukuma R pakotnē. Bibliotēka ļauj strādāt ar daudzdimensiju masīviem gan rindu galvenajā, gan kolonnu galvenajā secībā.
ndjson JSON parsēšanai. Šī bibliotēka tiek izmantota xtensor automātiski, ja tas ir projektā.
RcppPavediens vektora daudzpavedienu apstrādes organizēšanai no JSON. Izmantoti šīs pakotnes nodrošinātie galvenes faili. No populārākiem RcppParallel Paketē, cita starpā, ir iebūvēts cilpas pārtraukšanas mehānisms.
Jāatzīmē, ka xtensor izrādījās Dieva dāvana: papildus tam, ka tam ir plaša funkcionalitāte un augsta veiktspēja, tā izstrādātāji izrādījās diezgan atsaucīgi un ātri un detalizēti atbildēja uz jautājumiem. Ar to palīdzību bija iespējams realizēt OpenCV matricu transformācijas xtensoros, kā arī veids, kā apvienot 3-dimensiju attēla tensorus pareizās dimensijas 4-dimensiju tensorā (pašā partija).
Materiāli Rcpp, xtensor un RcppThread apguvei
Lai apkopotu failus, kuros tiek izmantoti sistēmas faili un dinamiska saite ar sistēmā instalētajām bibliotēkām, mēs izmantojām pakotnē ieviesto spraudņa mehānismu Rcpp. Lai automātiski atrastu ceļus un karogus, mēs izmantojām populāru Linux utilītu pkg-config.
Rcpp spraudņa ieviešana OpenCV bibliotēkas lietošanai
Rcpp::registerPlugin("opencv", function() {
# Возможные названия пакета
pkg_config_name <- c("opencv", "opencv4")
# Бинарный файл утилиты pkg-config
pkg_config_bin <- Sys.which("pkg-config")
# Проврека наличия утилиты в системе
checkmate::assert_file_exists(pkg_config_bin, access = "x")
# Проверка наличия файла настроек OpenCV для pkg-config
check <- sapply(pkg_config_name,
function(pkg) system(paste(pkg_config_bin, pkg)))
if (all(check != 0)) {
stop("OpenCV config for the pkg-config not found", call. = FALSE)
}
pkg_config_name <- pkg_config_name[check == 0]
list(env = list(
PKG_CXXFLAGS = system(paste(pkg_config_bin, "--cflags", pkg_config_name),
intern = TRUE),
PKG_LIBS = system(paste(pkg_config_bin, "--libs", pkg_config_name),
intern = TRUE)
))
})Spraudņa darbības rezultātā kompilācijas procesā tiks aizstātas šādas vērtības:
Rcpp:::.plugins$opencv()$env
# $PKG_CXXFLAGS
# [1] "-I/usr/include/opencv"
#
# $PKG_LIBS
# [1] "-lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_datasets -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_line_descriptor -lopencv_optflow -lopencv_video -lopencv_plot -lopencv_reg -lopencv_saliency -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_rgbd -lopencv_viz -lopencv_surface_matching -lopencv_text -lopencv_ximgproc -lopencv_calib3d -lopencv_features2d -lopencv_flann -lopencv_xobjdetect -lopencv_objdetect -lopencv_ml -lopencv_xphoto -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -lopencv_photo -lopencv_imgproc -lopencv_core"Īstenošanas kods JSON parsēšanai un partijas ģenerēšanai pārsūtīšanai uz modeli ir norādīts zem spoilera. Vispirms pievienojiet vietējo projektu direktoriju, lai meklētu galvenes failus (nepieciešams ndjson):
Sys.setenv("PKG_CXXFLAGS" = paste0("-I", normalizePath(file.path("src"))))JSON konvertēšana uz tenzoru programmā C++
// [[Rcpp::plugins(cpp14)]]
// [[Rcpp::plugins(opencv)]]
// [[Rcpp::depends(xtensor)]]
// [[Rcpp::depends(RcppThread)]]
#include <xtensor/xjson.hpp>
#include <xtensor/xadapt.hpp>
#include <xtensor/xview.hpp>
#include <xtensor-r/rtensor.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <Rcpp.h>
#include <RcppThread.h>
// Синонимы для типов
using RcppThread::parallelFor;
using json = nlohmann::json;
using points = xt::xtensor<double,2>; // Извлечённые из JSON координаты точек
using strokes = std::vector<points>; // Извлечённые из JSON координаты точек
using xtensor3d = xt::xtensor<double, 3>; // Тензор для хранения матрицы изоображения
using xtensor4d = xt::xtensor<double, 4>; // Тензор для хранения множества изображений
using rtensor3d = xt::rtensor<double, 3>; // Обёртка для экспорта в R
using rtensor4d = xt::rtensor<double, 4>; // Обёртка для экспорта в R
// Статические константы
// Размер изображения в пикселях
const static int SIZE = 256;
// Тип линии
// См. https://en.wikipedia.org/wiki/Pixel_connectivity#2-dimensional
const static int LINE_TYPE = cv::LINE_4;
// Толщина линии в пикселях
const static int LINE_WIDTH = 3;
// Алгоритм ресайза
// https://docs.opencv.org/3.1.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121
const static int RESIZE_TYPE = cv::INTER_LINEAR;
// Шаблон для конвертирования OpenCV-матрицы в тензор
template <typename T, int NCH, typename XT=xt::xtensor<T,3,xt::layout_type::column_major>>
XT to_xt(const cv::Mat_<cv::Vec<T, NCH>>& src) {
// Размерность целевого тензора
std::vector<int> shape = {src.rows, src.cols, NCH};
// Общее количество элементов в массиве
size_t size = src.total() * NCH;
// Преобразование cv::Mat в xt::xtensor
XT res = xt::adapt((T*) src.data, size, xt::no_ownership(), shape);
return res;
}
// Преобразование JSON в список координат точек
strokes parse_json(const std::string& x) {
auto j = json::parse(x);
// Результат парсинга должен быть массивом
if (!j.is_array()) {
throw std::runtime_error("'x' must be JSON array.");
}
strokes res;
res.reserve(j.size());
for (const auto& a: j) {
// Каждый элемент массива должен быть 2-мерным массивом
if (!a.is_array() || a.size() != 2) {
throw std::runtime_error("'x' must include only 2d arrays.");
}
// Извлечение вектора точек
auto p = a.get<points>();
res.push_back(p);
}
return res;
}
// Отрисовка линий
// Цвета HSV
cv::Mat ocv_draw_lines(const strokes& x, bool color = true) {
// Исходный тип матрицы
auto stype = color ? CV_8UC3 : CV_8UC1;
// Итоговый тип матрицы
auto dtype = color ? CV_32FC3 : CV_32FC1;
auto bg = color ? cv::Scalar(0, 0, 255) : cv::Scalar(255);
auto col = color ? cv::Scalar(0, 255, 220) : cv::Scalar(0);
cv::Mat img = cv::Mat(SIZE, SIZE, stype, bg);
// Количество линий
size_t n = x.size();
for (const auto& s: x) {
// Количество точек в линии
size_t n_points = s.shape()[1];
for (size_t i = 0; i < n_points - 1; ++i) {
// Точка начала штриха
cv::Point from(s(0, i), s(1, i));
// Точка окончания штриха
cv::Point to(s(0, i + 1), s(1, i + 1));
// Отрисовка линии
cv::line(img, from, to, col, LINE_WIDTH, LINE_TYPE);
}
if (color) {
// Меняем цвет линии
col[0] += 180 / n;
}
}
if (color) {
// Меняем цветовое представление на RGB
cv::cvtColor(img, img, cv::COLOR_HSV2RGB);
}
// Меняем формат представления на float32 с диапазоном [0, 1]
img.convertTo(img, dtype, 1 / 255.0);
return img;
}
// Обработка JSON и получение тензора с данными изображения
xtensor3d process(const std::string& x, double scale = 1.0, bool color = true) {
auto p = parse_json(x);
auto img = ocv_draw_lines(p, color);
if (scale != 1) {
cv::Mat out;
cv::resize(img, out, cv::Size(), scale, scale, RESIZE_TYPE);
cv::swap(img, out);
out.release();
}
xtensor3d arr = color ? to_xt<double,3>(img) : to_xt<double,1>(img);
return arr;
}
// [[Rcpp::export]]
rtensor3d cpp_process_json_str(const std::string& x,
double scale = 1.0,
bool color = true) {
xtensor3d res = process(x, scale, color);
return res;
}
// [[Rcpp::export]]
rtensor4d cpp_process_json_vector(const std::vector<std::string>& x,
double scale = 1.0,
bool color = false) {
size_t n = x.size();
size_t dim = floor(SIZE * scale);
size_t channels = color ? 3 : 1;
xtensor4d res({n, dim, dim, channels});
parallelFor(0, n, [&x, &res, scale, color](int i) {
xtensor3d tmp = process(x[i], scale, color);
auto view = xt::view(res, i, xt::all(), xt::all(), xt::all());
view = tmp;
});
return res;
}Šis kods ir jāievieto failā src/cv_xt.cpp un kompilējiet ar komandu Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); nepieciešami arī darbam nlohmann/json.hpp no . Kods ir sadalīts vairākās funkcijās:
to_xt— veidnes funkcija attēla matricas pārveidošanai (cv::Mat) uz tensoruxt::xtensor;parse_json— funkcija parsē JSON virkni, izvelk punktu koordinātas, iepakojot tās vektorā;ocv_draw_lines— no iegūtā punktu vektora zīmē daudzkrāsainas līnijas;process— apvieno iepriekš minētās funkcijas un arī pievieno iespēju mērogot iegūto attēlu;cpp_process_json_str- funkciju aptinumsprocess, kas eksportē rezultātu uz R-objektu (daudzdimensiju masīvs);cpp_process_json_vector- funkciju aptinumscpp_process_json_str, kas ļauj apstrādāt virknes vektoru vairāku vītņu režīmā.
Lai zīmētu daudzkrāsainas līnijas, tika izmantots HSV krāsu modelis, kam sekoja pārveidošana uz RGB. Pārbaudīsim rezultātu:
arr <- cpp_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256 3
plot(magick::image_read(arr)) 
Īstenošanas ātruma salīdzinājums R un C++
res_bench <- bench::mark(
r_process_json_str(tmp_data[4, drawing], scale = 0.5),
cpp_process_json_str(tmp_data[4, drawing], scale = 0.5),
check = FALSE,
min_iterations = 100
)
# Параметры бенчмарка
cols <- c("expression", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]
# expression min median max `itr/sec` total_time n_itr
# <chr> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 r_process_json_str 3.49ms 3.55ms 4.47ms 273. 490ms 134
# 2 cpp_process_json_str 1.94ms 2.02ms 5.32ms 489. 497ms 243
library(ggplot2)
# Проведение замера
res_bench <- bench::press(
batch_size = 2^(4:10),
{
.data <- tmp_data[sample(seq_len(.N), batch_size), drawing]
bench::mark(
r_process_json_vector(.data, scale = 0.5),
cpp_process_json_vector(.data, scale = 0.5),
min_iterations = 50,
check = FALSE
)
}
)
res_bench[, cols]
# expression batch_size min median max `itr/sec` total_time n_itr
# <chr> <dbl> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 r 16 50.61ms 53.34ms 54.82ms 19.1 471.13ms 9
# 2 cpp 16 4.46ms 5.39ms 7.78ms 192. 474.09ms 91
# 3 r 32 105.7ms 109.74ms 212.26ms 7.69 6.5s 50
# 4 cpp 32 7.76ms 10.97ms 15.23ms 95.6 522.78ms 50
# 5 r 64 211.41ms 226.18ms 332.65ms 3.85 12.99s 50
# 6 cpp 64 25.09ms 27.34ms 32.04ms 36.0 1.39s 50
# 7 r 128 534.5ms 627.92ms 659.08ms 1.61 31.03s 50
# 8 cpp 128 56.37ms 58.46ms 66.03ms 16.9 2.95s 50
# 9 r 256 1.15s 1.18s 1.29s 0.851 58.78s 50
# 10 cpp 256 114.97ms 117.39ms 130.09ms 8.45 5.92s 50
# 11 r 512 2.09s 2.15s 2.32s 0.463 1.8m 50
# 12 cpp 512 230.81ms 235.6ms 261.99ms 4.18 11.97s 50
# 13 r 1024 4s 4.22s 4.4s 0.238 3.5m 50
# 14 cpp 1024 410.48ms 431.43ms 462.44ms 2.33 21.45s 50
ggplot(res_bench, aes(x = factor(batch_size), y = median,
group = expression, color = expression)) +
geom_point() +
geom_line() +
ylab("median time, s") +
theme_minimal() +
scale_color_discrete(name = "", labels = c("cpp", "r")) +
theme(legend.position = "bottom") 
Kā redzat, ātruma pieaugums izrādījās ļoti ievērojams, un, paralēli R kodu, nav iespējams panākt C++ kodu.
3. Iteratori partiju izkraušanai no datu bāzes
R ir pelnīta reputācija attiecībā uz datu apstrādi, kas iekļaujas RAM, savukārt Python vairāk raksturīga iteratīva datu apstrāde, kas ļauj viegli un dabiski īstenot ārpus kodola aprēķinus (aprēķinus, izmantojot ārējo atmiņu). Klasisks un mums atbilstošs piemērs aprakstītās problēmas kontekstā ir dziļi neironu tīkli, kas apmācīti ar gradienta nolaišanās metodi ar gradienta tuvināšanu katrā solī, izmantojot nelielu novērojumu daļu vai mini-partiju.
Python rakstītajās dziļās mācīšanās ietvaros ir īpašas klases, kas ievieš iteratorus, pamatojoties uz datiem: tabulas, attēli mapēs, bināri formāti utt. Varat izmantot gatavas opcijas vai rakstīt savu konkrētu uzdevumu veikšanai. Programmā R mēs varam izmantot visas Python bibliotēkas iespējas keras ar dažādām aizmugursistēmām, izmantojot tāda paša nosaukuma pakotni, kas savukārt darbojas uz pakotnes tīklveida. Pēdējais ir pelnījis atsevišķu garu rakstu; tas ne tikai ļauj palaist Python kodu no R, bet arī ļauj pārsūtīt objektus starp R un Python sesijām, automātiski veicot visas nepieciešamās tipa konvertācijas.
Mēs atbrīvojāmies no nepieciešamības glabāt visus datus RAM, izmantojot MonetDBLite, visu “neironu tīkla” darbu veiks sākotnējais kods Python, mums tikai jāraksta iterators pār datiem, jo nekas nav gatavs šādai situācijai vai nu R, vai Python. Būtībā tam ir tikai divas prasības: tai ir jāatgriež partijas bezgalīgā cilpā un jāsaglabā savs stāvoklis starp iterācijām (pēdējais R tiek īstenots visvienkāršākajā veidā, izmantojot slēgšanu). Iepriekš iteratora iekšienē R masīvi bija nepārprotami jāpārvērš par masīviem, taču pašreizējā pakotnes versija keras dara pati.
Apmācības un validācijas datu iterators izrādījās šāds:
Iterators apmācībai un datu apstiprināšanai
train_generator <- function(db_connection = con,
samples_index,
num_classes = 340,
batch_size = 32,
scale = 1,
color = FALSE,
imagenet_preproc = FALSE) {
# Проверка аргументов
checkmate::assert_class(con, "DBIConnection")
checkmate::assert_integerish(samples_index)
checkmate::assert_count(num_classes)
checkmate::assert_count(batch_size)
checkmate::assert_number(scale, lower = 0.001, upper = 5)
checkmate::assert_flag(color)
checkmate::assert_flag(imagenet_preproc)
# Перемешиваем, чтобы брать и удалять использованные индексы батчей по порядку
dt <- data.table::data.table(id = sample(samples_index))
# Проставляем номера батчей
dt[, batch := (.I - 1L) %/% batch_size + 1L]
# Оставляем только полные батчи и индексируем
dt <- dt[, if (.N == batch_size) .SD, keyby = batch]
# Устанавливаем счётчик
i <- 1
# Количество батчей
max_i <- dt[, max(batch)]
# Подготовка выражения для выгрузки
sql <- sprintf(
"PREPARE SELECT drawing, label_int FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ",")
)
res <- DBI::dbSendQuery(con, sql)
# Аналог keras::to_categorical
to_categorical <- function(x, num) {
n <- length(x)
m <- numeric(n * num)
m[x * n + seq_len(n)] <- 1
dim(m) <- c(n, num)
return(m)
}
# Замыкание
function() {
# Начинаем новую эпоху
if (i > max_i) {
dt[, id := sample(id)]
data.table::setkey(dt, batch)
# Сбрасываем счётчик
i <<- 1
max_i <<- dt[, max(batch)]
}
# ID для выгрузки данных
batch_ind <- dt[batch == i, id]
# Выгрузка данных
batch <- DBI::dbFetch(DBI::dbBind(res, as.list(batch_ind)), n = -1)
# Увеличиваем счётчик
i <<- i + 1
# Парсинг JSON и подготовка массива
batch_x <- cpp_process_json_vector(batch$drawing, scale = scale, color = color)
if (imagenet_preproc) {
# Шкалирование c интервала [0, 1] на интервал [-1, 1]
batch_x <- (batch_x - 0.5) * 2
}
batch_y <- to_categorical(batch$label_int, num_classes)
result <- list(batch_x, batch_y)
return(result)
}
}Funkcija kā ievadi ņem mainīgo ar savienojumu ar datu bāzi, izmantoto rindu skaitu, klašu skaitu, partijas lielumu, mērogu (scale = 1 atbilst 256x256 pikseļu attēlu renderēšanai, scale = 0.5 — 128x128 pikseļi), krāsu indikators (color = FALSE norāda renderēšanu pelēktoņos, kad to lieto color = TRUE katrs triepiens tiek uzzīmēts jaunā krāsā) un priekšapstrādes indikators tīkliem, kas iepriekš apmācīti, izmantojot imagenet. Pēdējais ir nepieciešams, lai mērogotu pikseļu vērtības no intervāla [0, 1] līdz intervālam [-1, 1], kas tika izmantots, apmācot piegādāto. keras modeļiem.
Ārējā funkcija satur argumentu tipa pārbaudi, tabulu data.table ar nejauši sajauktiem rindu numuriem no samples_index un partiju numuri, skaitītājs un maksimālais partiju skaits, kā arī SQL izteiksme datu izkraušanai no datu bāzes. Turklāt mēs definējām ātru funkcijas analogu iekšpusē keras::to_categorical(). Mēs izmantojām gandrīz visus datus apmācībai, atstājot pusprocentu apstiprināšanai, tāpēc laikmeta lielumu ierobežoja parametrs steps_per_epoch kad sauc keras::fit_generator(), un nosacījums if (i > max_i) strādāja tikai validācijas iteratoram.
Iekšējā funkcijā tiek izgūti rindu indeksi nākamajai partijai, ieraksti tiek izlādēti no datu bāzes, palielinot partiju skaitītāju, JSON parsēšana (funkcija cpp_process_json_vector(), rakstīts C++) un izveidojot attēliem atbilstošus masīvus. Pēc tam tiek izveidoti one-hot vektori ar klases etiķetēm, masīvi ar pikseļu vērtībām un etiķetēm tiek apvienoti sarakstā, kas ir atgriešanas vērtība. Lai paātrinātu darbu, mēs izmantojām indeksu izveidi tabulās data.table un modifikācijas, izmantojot saiti - bez šīm pakotnes "mikroshēmām" dati.tabula Ir diezgan grūti iedomāties efektīvu darbu ar ievērojamu datu apjomu R.
Core i5 klēpjdatora ātruma mērījumu rezultāti ir šādi:
Iteratora etalons
library(Rcpp)
library(keras)
library(ggplot2)
source("utils/rcpp.R")
source("utils/keras_iterator.R")
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
ind <- seq_len(DBI::dbGetQuery(con, "SELECT count(*) FROM doodles")[[1L]])
num_classes <- DBI::dbGetQuery(con, "SELECT max(label_int) + 1 FROM doodles")[[1L]]
# Индексы для обучающей выборки
train_ind <- sample(ind, floor(length(ind) * 0.995))
# Индексы для проверочной выборки
val_ind <- ind[-train_ind]
rm(ind)
# Коэффициент масштаба
scale <- 0.5
# Проведение замера
res_bench <- bench::press(
batch_size = 2^(4:10),
{
it1 <- train_generator(
db_connection = con,
samples_index = train_ind,
num_classes = num_classes,
batch_size = batch_size,
scale = scale
)
bench::mark(
it1(),
min_iterations = 50L
)
}
)
# Параметры бенчмарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]
# batch_size min median max `itr/sec` total_time n_itr
# <dbl> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 16 25ms 64.36ms 92.2ms 15.9 3.09s 49
# 2 32 48.4ms 118.13ms 197.24ms 8.17 5.88s 48
# 3 64 69.3ms 117.93ms 181.14ms 8.57 5.83s 50
# 4 128 157.2ms 240.74ms 503.87ms 3.85 12.71s 49
# 5 256 359.3ms 613.52ms 988.73ms 1.54 30.5s 47
# 6 512 884.7ms 1.53s 2.07s 0.674 1.11m 45
# 7 1024 2.7s 3.83s 5.47s 0.261 2.81m 44
ggplot(res_bench, aes(x = factor(batch_size), y = median, group = 1)) +
geom_point() +
geom_line() +
ylab("median time, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE) 
Ja jums ir pietiekami daudz RAM, varat nopietni paātrināt datu bāzes darbību, pārsūtot to uz šo pašu RAM (mūsu uzdevumam pietiek ar 32 GB). Operētājsistēmā Linux nodalījums ir uzstādīts pēc noklusējuma /dev/shm, kas aizņem līdz pusei RAM ietilpības. Rediģējot, varat izcelt vairāk /etc/fstablai iegūtu ierakstu patīk tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Noteikti restartējiet un pārbaudiet rezultātu, izpildot komandu df -h.
Testa datu iterators izskatās daudz vienkāršāks, jo testa datu kopa pilnībā iekļaujas RAM:
Iterators testa datiem
test_generator <- function(dt,
batch_size = 32,
scale = 1,
color = FALSE,
imagenet_preproc = FALSE) {
# Проверка аргументов
checkmate::assert_data_table(dt)
checkmate::assert_count(batch_size)
checkmate::assert_number(scale, lower = 0.001, upper = 5)
checkmate::assert_flag(color)
checkmate::assert_flag(imagenet_preproc)
# Проставляем номера батчей
dt[, batch := (.I - 1L) %/% batch_size + 1L]
data.table::setkey(dt, batch)
i <- 1
max_i <- dt[, max(batch)]
# Замыкание
function() {
batch_x <- cpp_process_json_vector(dt[batch == i, drawing],
scale = scale, color = color)
if (imagenet_preproc) {
# Шкалирование c интервала [0, 1] на интервал [-1, 1]
batch_x <- (batch_x - 0.5) * 2
}
result <- list(batch_x)
i <<- i + 1
return(result)
}
}4. Modeļa arhitektūras izvēle
Pirmā izmantotā arhitektūra bija , kuras īpašības ir apskatītas ziņa. Tas ir iekļauts standarta komplektācijā keras un attiecīgi ir pieejams iepakojumā ar tādu pašu nosaukumu R. Bet, mēģinot to izmantot ar viena kanāla attēliem, izrādījās dīvaina lieta: ievades tensoram vienmēr jābūt izmēram (batch, height, width, 3), tas ir, kanālu skaitu nevar mainīt. Python nav šāda ierobežojuma, tāpēc mēs steidzāmies un uzrakstījām paši savu šīs arhitektūras ieviešanu, sekojot oriģinālajam rakstam (bez atbiruma, kas ir keras versijā):
Mobilenet v1 arhitektūra
library(keras)
top_3_categorical_accuracy <- custom_metric(
name = "top_3_categorical_accuracy",
metric_fn = function(y_true, y_pred) {
metric_top_k_categorical_accuracy(y_true, y_pred, k = 3)
}
)
layer_sep_conv_bn <- function(object,
filters,
alpha = 1,
depth_multiplier = 1,
strides = c(2, 2)) {
# NB! depth_multiplier != resolution multiplier
# https://github.com/keras-team/keras/issues/10349
layer_depthwise_conv_2d(
object = object,
kernel_size = c(3, 3),
strides = strides,
padding = "same",
depth_multiplier = depth_multiplier
) %>%
layer_batch_normalization() %>%
layer_activation_relu() %>%
layer_conv_2d(
filters = filters * alpha,
kernel_size = c(1, 1),
strides = c(1, 1)
) %>%
layer_batch_normalization() %>%
layer_activation_relu()
}
get_mobilenet_v1 <- function(input_shape = c(224, 224, 1),
num_classes = 340,
alpha = 1,
depth_multiplier = 1,
optimizer = optimizer_adam(lr = 0.002),
loss = "categorical_crossentropy",
metrics = c("categorical_crossentropy",
top_3_categorical_accuracy)) {
inputs <- layer_input(shape = input_shape)
outputs <- inputs %>%
layer_conv_2d(filters = 32, kernel_size = c(3, 3), strides = c(2, 2), padding = "same") %>%
layer_batch_normalization() %>%
layer_activation_relu() %>%
layer_sep_conv_bn(filters = 64, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 128, strides = c(2, 2)) %>%
layer_sep_conv_bn(filters = 128, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 256, strides = c(2, 2)) %>%
layer_sep_conv_bn(filters = 256, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(2, 2)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 1024, strides = c(2, 2)) %>%
layer_sep_conv_bn(filters = 1024, strides = c(1, 1)) %>%
layer_global_average_pooling_2d() %>%
layer_dense(units = num_classes) %>%
layer_activation_softmax()
model <- keras_model(
inputs = inputs,
outputs = outputs
)
model %>% compile(
optimizer = optimizer,
loss = loss,
metrics = metrics
)
return(model)
}Šīs pieejas trūkumi ir acīmredzami. Es vēlos pārbaudīt daudzus modeļus, bet gluži pretēji, es nevēlos pārrakstīt katru arhitektūru manuāli. Mums arī tika liegta iespēja izmantot modeļu atsvarus, kas iepriekš apmācīti imagenet. Kā parasti, palīdzēja dokumentācijas izpēte. Funkcija get_config() ļauj iegūt modeļa aprakstu rediģēšanai piemērotā formā (base_model_conf$layers - parastais R saraksts) un funkcija from_config() veic apgriezto konvertēšanu uz modeļa objektu:
base_model_conf <- get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- from_config(base_model_conf)Tagad nav grūti uzrakstīt universālu funkciju, lai iegūtu kādu no piegādātajiem keras modeļi ar vai bez svariem, kas apmācīti, izmantojot imagenet:
Funkcija gatavu arhitektūru ielādei
get_model <- function(name = "mobilenet_v2",
input_shape = NULL,
weights = "imagenet",
pooling = "avg",
num_classes = NULL,
optimizer = keras::optimizer_adam(lr = 0.002),
loss = "categorical_crossentropy",
metrics = NULL,
color = TRUE,
compile = FALSE) {
# Проверка аргументов
checkmate::assert_string(name)
checkmate::assert_integerish(input_shape, lower = 1, upper = 256, len = 3)
checkmate::assert_count(num_classes)
checkmate::assert_flag(color)
checkmate::assert_flag(compile)
# Получаем объект из пакета keras
model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
# Проверка наличия объекта в пакете
if (is.null(model_fun)) {
stop("Model ", shQuote(name), " not found.", call. = FALSE)
}
base_model <- model_fun(
input_shape = input_shape,
include_top = FALSE,
weights = weights,
pooling = pooling
)
# Если изображение не цветное, меняем размерность входа
if (!color) {
base_model_conf <- keras::get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- keras::from_config(base_model_conf)
}
predictions <- keras::get_layer(base_model, "global_average_pooling2d_1")$output
predictions <- keras::layer_dense(predictions, units = num_classes, activation = "softmax")
model <- keras::keras_model(
inputs = base_model$input,
outputs = predictions
)
if (compile) {
keras::compile(
object = model,
optimizer = optimizer,
loss = loss,
metrics = metrics
)
}
return(model)
}Izmantojot viena kanāla attēlus, netiek izmantoti iepriekš sagatavoti svari. To var labot: izmantojot funkciju get_weights() iegūstiet modeļa svarus R masīvu saraksta veidā, mainiet šī saraksta pirmā elementa izmēru (izņemot vienu krāsu kanālu vai aprēķinot vidējos visus trīs) un pēc tam ielādējiet svarus atpakaļ modelī ar funkciju set_weights(). Mēs nekad nepievienojām šo funkcionalitāti, jo šajā posmā jau bija skaidrs, ka produktīvāk ir strādāt ar krāsainiem attēliem.
Lielāko daļu eksperimentu veicām, izmantojot mobilā tīkla 1. un 2. versiju, kā arī resnet34. Mūsdienīgākas arhitektūras, piemēram, SE-ResNeXt, šajās sacensībās veica labus rezultātus. Diemžēl mūsu rīcībā nebija gatavu implementāciju, un arī paši nerakstījām (bet noteikti rakstīsim).
5. Skriptu parametrizācija
Ērtības labad viss apmācības sākšanas kods tika izveidots kā viens skripts, kas tika parametrizēts, izmantojot šādi:
doc <- '
Usage:
train_nn.R --help
train_nn.R --list-models
train_nn.R [options]
Options:
-h --help Show this message.
-l --list-models List available models.
-m --model=<model> Neural network model name [default: mobilenet_v2].
-b --batch-size=<size> Batch size [default: 32].
-s --scale-factor=<ratio> Scale factor [default: 0.5].
-c --color Use color lines [default: FALSE].
-d --db-dir=<path> Path to database directory [default: Sys.getenv("db_dir")].
-r --validate-ratio=<ratio> Validate sample ratio [default: 0.995].
-n --n-gpu=<number> Number of GPUs [default: 1].
'
args <- docopt::docopt(doc)Paka docpt atspoguļo ieviešanu par R. Ar tās palīdzību tiek palaisti skripti ar vienkāršām komandām, piemēram Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db vai ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, ja fails train_nn.R ir izpildāma (šī komanda sāks modeļa apmācību resnet50 uz trīskrāsu attēliem, kuru izmērs ir 128x128 pikseļi, datu bāzei jāatrodas mapē /home/andrey/doodle_db). Sarakstam varat pievienot mācīšanās ātrumu, optimizētāja veidu un citus pielāgojamus parametrus. Publikācijas sagatavošanas procesā atklājās, ka arhitektūra mobilenet_v2 no pašreizējās versijas keras R lietošanā sakarā ar izmaiņām, kas nav ņemtas vērā R paketē, mēs gaidām, kad viņi to izlabos.
Šī pieeja ļāva ievērojami paātrināt eksperimentus ar dažādiem modeļiem, salīdzinot ar tradicionālāku skriptu palaišanu programmā RStudio (mēs atzīmējam pakotni kā iespējamu alternatīvu ). Bet galvenā priekšrocība ir iespēja viegli pārvaldīt skriptu palaišanu programmā Docker vai vienkārši serverī, šim nolūkam neinstalējot RStudio.
6. Skriptu dokerizācija
Mēs izmantojām Docker, lai nodrošinātu vides pārnesamību apmācības modeļiem starp komandas locekļiem un ātrai izvietošanai mākonī. Ar šo R programmētājam salīdzinoši neparasto rīku var sākt iepazīties ar publikāciju sērija vai .
Docker ļauj gan izveidot savus attēlus no nulles, gan izmantot citus attēlus kā pamatu savu izveidei. Analizējot pieejamās iespējas, nonācām pie secinājuma, ka NVIDIA, CUDA+cuDNN draiveru un Python bibliotēku instalēšana ir diezgan apjomīga attēla daļa, un mēs nolēmām par pamatu ņemt oficiālo attēlu. tensorflow/tensorflow:1.12.0-gpu, pievienojot tur nepieciešamās R pakotnes.
Galīgais docker fails izskatījās šādi:
Dockerfile
FROM tensorflow/tensorflow:1.12.0-gpu
MAINTAINER Artem Klevtsov <a.a.klevtsov@gmail.com>
SHELL ["/bin/bash", "-c"]
ARG LOCALE="en_US.UTF-8"
ARG APT_PKG="libopencv-dev r-base r-base-dev littler"
ARG R_BIN_PKG="futile.logger checkmate data.table rcpp rapidjsonr dbi keras jsonlite curl digest remotes"
ARG R_SRC_PKG="xtensor RcppThread docopt MonetDBLite"
ARG PY_PIP_PKG="keras"
ARG DIRS="/db /app /app/data /app/models /app/logs"
RUN source /etc/os-release &&
echo "deb https://cloud.r-project.org/bin/linux/ubuntu ${UBUNTU_CODENAME}-cran35/" > /etc/apt/sources.list.d/cran35.list &&
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E084DAB9 &&
add-apt-repository -y ppa:marutter/c2d4u3.5 &&
add-apt-repository -y ppa:timsc/opencv-3.4 &&
apt-get update &&
apt-get install -y locales &&
locale-gen ${LOCALE} &&
apt-get install -y --no-install-recommends ${APT_PKG} &&
ln -s /usr/lib/R/site-library/littler/examples/install.r /usr/local/bin/install.r &&
ln -s /usr/lib/R/site-library/littler/examples/install2.r /usr/local/bin/install2.r &&
ln -s /usr/lib/R/site-library/littler/examples/installGithub.r /usr/local/bin/installGithub.r &&
echo 'options(Ncpus = parallel::detectCores())' >> /etc/R/Rprofile.site &&
echo 'options(repos = c(CRAN = "https://cloud.r-project.org"))' >> /etc/R/Rprofile.site &&
apt-get install -y $(printf "r-cran-%s " ${R_BIN_PKG}) &&
install.r ${R_SRC_PKG} &&
pip install ${PY_PIP_PKG} &&
mkdir -p ${DIRS} &&
chmod 777 ${DIRS} &&
rm -rf /tmp/downloaded_packages/ /tmp/*.rds &&
rm -rf /var/lib/apt/lists/*
COPY utils /app/utils
COPY src /app/src
COPY tests /app/tests
COPY bin/*.R /app/
ENV DBDIR="/db"
ENV CUDA_HOME="/usr/local/cuda"
ENV PATH="/app:${PATH}"
WORKDIR /app
VOLUME /db
VOLUME /app
CMD bash
Ērtības labad izmantotie iepakojumi tika ievietoti mainīgajos; lielākā daļa rakstīto skriptu montāžas laikā tiek kopēti konteineros. Mēs arī mainījām komandu apvalku uz /bin/bash satura lietošanas ērtībai /etc/os-release. Tādējādi tika novērsta nepieciešamība kodā norādīt OS versiju.
Turklāt tika uzrakstīts neliels bash skripts, kas ļauj palaist konteineru ar dažādām komandām. Piemēram, tie varētu būt skripti neironu tīklu apmācībai, kas iepriekš tika ievietoti konteinerā, vai komandu apvalks atkļūdošanai un konteinera darbības uzraudzībai:
Skripts konteinera palaišanai
#!/bin/sh
DBDIR=${PWD}/db
LOGSDIR=${PWD}/logs
MODELDIR=${PWD}/models
DATADIR=${PWD}/data
ARGS="--runtime=nvidia --rm -v ${DBDIR}:/db -v ${LOGSDIR}:/app/logs -v ${MODELDIR}:/app/models -v ${DATADIR}:/app/data"
if [ -z "$1" ]; then
CMD="Rscript /app/train_nn.R"
elif [ "$1" = "bash" ]; then
ARGS="${ARGS} -ti"
else
CMD="Rscript /app/train_nn.R $@"
fi
docker run ${ARGS} doodles-tf ${CMD}Ja šis bash skripts tiek palaists bez parametriem, skripts tiks izsaukts konteinerā train_nn.R ar noklusējuma vērtībām; ja pirmais pozicionālais arguments ir "bash", tad konteiners sāksies interaktīvi ar komandas apvalku. Visos citos gadījumos pozicionālo argumentu vērtības tiek aizstātas: CMD="Rscript /app/train_nn.R $@".
Ir vērts atzīmēt, ka direktoriji ar avota datiem un datu bāzi, kā arī direktorijs apmācīto modeļu saglabāšanai ir uzstādīti konteinera iekšpusē no resursdatora sistēmas, kas ļauj piekļūt skriptu rezultātiem bez nevajadzīgām manipulācijām.
7. Vairāku GPU izmantošana pakalpojumā Google Cloud
Viena no konkursa iezīmēm bija ļoti trokšņainie dati (skat. titulbildi, aizgūts no @Leigh.plt no ODS slack). Lielas partijas palīdz ar to cīnīties, un pēc eksperimentiem datorā ar 1 GPU mēs nolēmām apgūt apmācību modeļus vairākos mākonī esošajos GPU. Lietots GoogleCloud (), pateicoties lielajai pieejamo konfigurāciju izvēlei, saprātīgām cenām un 300 USD bonusam. Mantkārības dēļ pasūtīju 4xV100 eksemplāru ar SSD un tonnu RAM, un tā bija liela kļūda. Šāda iekārta ātri patērē naudu, jūs varat izgāzties, eksperimentējot bez pārbaudīta cauruļvada. Izglītības nolūkos labāk ņemt K80. Taču lielais operatīvās atmiņas apjoms noderēja – mākoņa SSD ar savu veiktspēju nepārsteidza, tāpēc datubāze tika pārsūtīta uz dev/shm.
Vislielāko interesi rada koda fragments, kas ir atbildīgs par vairāku GPU izmantošanu. Pirmkārt, modelis tiek izveidots CPU, izmantojot konteksta pārvaldnieku, tāpat kā Python:
with(tensorflow::tf$device("/cpu:0"), {
model_cpu <- get_model(
name = model_name,
input_shape = input_shape,
weights = weights,
metrics =(top_3_categorical_accuracy,
compile = FALSE
)
})Pēc tam nekompilētais (tas ir svarīgi) modelis tiek kopēts uz noteiktu skaitu pieejamo GPU un tikai pēc tam tiek kompilēts:
model <- keras::multi_gpu_model(model_cpu, gpus = n_gpu)
keras::compile(
object = model,
optimizer = keras::optimizer_adam(lr = 0.0004),
loss = "categorical_crossentropy",
metrics = c(top_3_categorical_accuracy)
)Nevarēja ieviest klasisko paņēmienu visu slāņu iesaldēšanai, izņemot pēdējo, pēdējā slāņa apmācību, visa modeļa atsaldēšanu un pārkvalificēšanu vairākiem GPU.
Apmācība tika uzraudzīta bez lietošanas. tensorboard, aprobežojoties ar žurnālu ierakstīšanu un modeļu saglabāšanu ar informatīviem nosaukumiem pēc katra laikmeta:
Atzvani
# Шаблон имени файла лога
log_file_tmpl <- file.path("logs", sprintf(
"%s_%d_%dch_%s.csv",
model_name,
dim_size,
channels,
format(Sys.time(), "%Y%m%d%H%M%OS")
))
# Шаблон имени файла модели
model_file_tmpl <- file.path("models", sprintf(
"%s_%d_%dch_{epoch:02d}_{val_loss:.2f}.h5",
model_name,
dim_size,
channels
))
callbacks_list <- list(
keras::callback_csv_logger(
filename = log_file_tmpl
),
keras::callback_early_stopping(
monitor = "val_loss",
min_delta = 1e-4,
patience = 8,
verbose = 1,
mode = "min"
),
keras::callback_reduce_lr_on_plateau(
monitor = "val_loss",
factor = 0.5, # уменьшаем lr в 2 раза
patience = 4,
verbose = 1,
min_delta = 1e-4,
mode = "min"
),
keras::callback_model_checkpoint(
filepath = model_file_tmpl,
monitor = "val_loss",
save_best_only = FALSE,
save_weights_only = FALSE,
mode = "min"
)
)8. Secinājuma vietā
Vairākas problēmas, ar kurām esam saskārušies, vēl nav atrisinātas:
- в keras nav gatavas funkcijas, lai automātiski meklētu optimālo mācīšanās ātrumu (analogs
lr_finderbibliotēkā ātri.ai); Ar zināmām pūlēm ir iespējams pārnest trešo pušu implementācijas uz R, piemēram, ; - iepriekšējā punkta rezultātā, izmantojot vairākus GPU, nebija iespējams izvēlēties pareizo treniņu ātrumu;
- trūkst modernu neironu tīklu arhitektūru, jo īpaši to, kas ir iepriekš apmācītas uz imagenet;
- neviena cikla politika un diskriminējoši mācību rādītāji (kosinusa atkausēšana tika veikta pēc mūsu pieprasījuma , Paldies ).
Ko noderīgu uzzināja šajā konkursā:
- Izmantojot salīdzinoši mazjaudas aparatūru, bez sāpēm varat strādāt ar pienācīgu (daudzkārt lielāku par RAM) datu apjomu. Plastmasas maisiņš dati.tabula ietaupa atmiņu tabulu uz vietas veikto modifikāciju dēļ, kas ļauj izvairīties no to kopēšanas, un, pareizi lietojot, tās iespējas gandrīz vienmēr demonstrē lielāko ātrumu starp visiem mums zināmajiem skriptu valodu rīkiem. Datu saglabāšana datu bāzē ļauj daudzos gadījumos vispār nedomāt par nepieciešamību iespiest visu datu kopu RAM.
- Lēnās funkcijas R var aizstāt ar ātrajām C++, izmantojot pakotni Rcpp. Ja papildus lietošanai RcppPavediens vai RcppParallel, mēs iegūstam starpplatformu daudzpavedienu implementācijas, tāpēc nav nepieciešams paralēli kodēt R līmenī.
- Iepakojums Rcpp var lietot bez nopietnām C++ zināšanām, ir iezīmēts nepieciešamais minimums . Galvenes faili vairākām lieliskām C bibliotēkām, piemēram xtensor pieejams CRAN, tas ir, tiek veidota infrastruktūra projektu īstenošanai, kas integrē gatavu augstas veiktspējas C++ kodu R. Papildu ērtības ir sintakses izcelšana un statisks C++ koda analizators programmā RStudio.
- docpt ļauj palaist autonomus skriptus ar parametriem. Tas ir ērti lietošanai attālā serverī, t.sk. zem doka. Programmā RStudio ir neērti veikt daudzu stundu eksperimentus ar neironu tīklu apmācību, un IDE instalēšana pašā serverī ne vienmēr ir pamatota.
- Docker nodrošina koda pārnesamību un rezultātu reproducējamību starp izstrādātājiem ar dažādām OS versijām un bibliotēkām, kā arī vieglu izpildi serveros. Jūs varat palaist visu apmācību cauruļvadu tikai ar vienu komandu.
- Google Cloud ir budžetam draudzīgs veids, kā eksperimentēt ar dārgu aparatūru, taču jums rūpīgi jāizvēlas konfigurācijas.
- Atsevišķu koda fragmentu ātruma mērīšana ir ļoti noderīga, īpaši kombinējot R un C++, kā arī ar pakotni sols - arī ļoti viegli.
Kopumā šī pieredze bija ļoti vērtīga, un mēs turpinām strādāt, lai atrisinātu dažas no izvirzītajām problēmām.
Avots: www.habr.com
