
Përshëndetje, Habr!
Në vjeshtën e vitit të kaluar, në Kaggle u zhvillua një garë për klasifikimin e vizatimeve të bëra me dorë, Quick Draw Doodle Recognition, në të cilën mori pjesë ekipi i R-sistemeve me përbërje , dhe . Nuk do ta përshkruajmë garën në detaje, kjo tashmë është bërë në .
Me farmën e medaljeve kësaj here nuk pasqyrohej, por u fitua shumë përvojë të vlefshme, ndaj do të dëshironim të ndanim me komunitetin disa nga temat më interesante dhe të dobishme në Kaggle dhe në punën përditshme. Nga temat e shqyrtuara: jeta e vështirë pa OpenCV, analizimi i JSON-ve (në këto shembuj shqyrtohet integrimi i kodit në C++ në skriptet ose paketat në R përmes Rcpp), parametrizimi i skriptëve dhe dockerizimi i zgjidhjes përfundimtare. Të gjithë kodin nga mesazhi në formatin e tij të përdorshëm mund ta gjeni në .
Përmbajtja:
1. Ngarko të dhëna me efikasitet nga CSV në bazën e të dhënave MonetDB
Të dhënat në këtë garë ofrohen jo në formën e imazheve të gatshme, por si 340 skedarë CSV (një për çdo klasë), që përmbajnë JSON me koordinatat e pikave. Duke lidhur këto pika me linea, ne marrim imazhin përfundimtar me përmasa 256x256 piksel. Po ashtu, për çdo regjistrim jepet një etiketë, nëse imazhi është njohur si i saktë nga klasifikuesi në përdorim në momentin e grumbullimit të dataset-it, një kod dytësh për vendin e banuar të autorit të vizatimit, një identifikues unik, një etiketë kohe dhe emri i klasës, që korrespondon me emrin e skedarit. Një version i thjeshtuar i të dhënave zë 7.4 GB në arkiv dhe rreth 20 GB pas shkarkimit, ndërsa të dhënat e plota pas shkarkimit zënë 240 GB. Organizatorët garantuan se të dy versionet prodhojnë të njëjtat vizatime, që do të thotë se versioni i plotë është i tepërt. Në çdo rast, ruajtja e 50 milionëve imazheve në skedarë grafikë ose si tipare menjëherë u njoh si jo fitimprurëse, dhe ne vendosëm që t'i bashkojmë të gjitha skedarët CSV nga arkivi train_simplified.zip në një bazë të dhënash me gjenerimin e imazheve të nevojshme "në flakë" për çdo grup.
Si DBMS u zgjodh një MonetDB, konkretisht implementimi për R në formën e paketës . Paketa përfshin versionin embedded të serverit të bazës së të dhënave dhe lejon ngritjen e serverit direkt nga sesioni R dhe punën me të aty. Krijimi i bazës së të dhënave dhe lidhja me të kryhen me një komandë:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))Na nevojitet të krijojmë dy tabela: një për të gjitha të dhënat, një tjetër për informacionin e shërbimit në lidhje me skedarët e ngarkuar (do të jetë e dobishme nëse diçka shkon keq dhe procesi duhet të riniset pas ngarkimit të disa skedarëve):
Krijimi i tabelave
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"
)
)
}Mënyra më e shpejtë për të ngarkuar të dhënat në DB u vërtetua të jetë kopjimi direkt i skedarëve CSV përmes SQL — komanda COPY OFFSET 2 INTO emri_i_tabelës FROM rruga duke përdorur DELIMITERS ',','n','"' NULL AS '' BEST EFFORT, ku emri_tabelës — emri i tabelës dhe path — rruga e skedarit. Gjatë punës me arkiv, u zbulua se implementimi i ndërtuar unzip në R nuk punon saktë me disa skedarë nga arkivi, prandaj ne përdorëm sistemin unzip (me parametrin getOption("unzip")).
Funksioni për të shkruar në bazë
#' @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))
}Nëse është e nevojshme të keni një transformim të tabelës para se të shkruhet në DB, mjafton të kaloni në argument parapërpunim një funksion, i cili do të transformojë të dhënat.
Kodi për ngarkimin e të dhënave në bazë në mënyrë sekondare:
Shkruaj të dhënat në bazë
# Список файлов для записи
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->SSDKoha e ngarkimit të të dhënave mund të variojë në varësi të karakteristikave të shpejtësisë së ruajtësit të përdorur. Në rastin tonë, lexim dhe shkruar brenda një SSD ose nga një USB (skedari burim) në SSD (DB) zërë më pak se 10 minuta.
Për disa sekonda të tjera nevojiten për krijimin e kolonës me etiketë të plotë numerike të klasës dhe kolonës-indeks (ORDERED INDEX) me numrat e rreshtave, sipas të cilit do të bëhet ndarja e vëzhgimeve gjatë krijimit të grupeve:
Krijimi i kolonave shtesë dhe indeksit
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)"))Për zgjidhjen e problemit të formimit të grupit "në flakë" na duhej të arrinim shpejtësinë maksimale të nxjerrjes së rreshtave të rastit nga tabela doodles. Për këtë ne përdorëm 3 truke. E para ishte të shkurtosh dimensionin e tipit, ku ruhet ID e vëzhgimit. Në grupin e dhënave origjinale, për të ruajtur ID na nevojitet një tip bigint, por numri i vëzhgimeve lejon të përfshihen identifikuesit e tyre, të barabarta me numrin rendor, në një tip int. Kërkimi bëhet dukshëm më shpejt. Truku i dytë ishte përdorimi i ORDERED INDEX — këtë zgjidhje arritëm empiricisht, duke kontrolluar të gjitha . Triku i tretë ishte përdorimi i kërkesave të parametrizuara. Thelbi i metodës është ekzekutimi i komandës vetëm një herë PREPARE me përdorimin e shprehjes së përgatitur gjatë krijimit të grupeve të kërkesave të ngjashme, por në të vërtetë fitimi krahasuar me thjeshtin SELECT u gjet në rrethin e gabimit statistik.
Procesi i ngarkimit të të dhënave konsumon jo më shumë se 450 MB RAM. Pra, qasja e përshkruar lejon të menaxhohen datasetet me pPesha në dhjetëra gigabajt pothuajse në çdo pajisje buxhetore, duke përfshirë disa karta të vetme, që është mjaft e këndshme.
Mbetej për të kryer matjet e shpejtësisë së nxjerrjes (sfidave) të të dhënave dhe të vlerësohej shkallëzimi gjatë zgjedhjes së grupeve të madhësive të ndryshme:
Benchmark i bazës së të dhënave
library(ggplot2)
set.seed(0)
# Lidhja me bazën e të dhënave
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
# Funksioni për të përgatitur kërkesën në server
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)
}
# Funksioni për nxjerrjen e të dhënave
fetch_data <- function(rs, batch_size) {
ids <- sample(seq_len(n), batch_size)
res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
return(res)
}
# Kryerja e matjes
res_bench <- bench::press(
batch_size = 2^(4:10),
{
rs <- prep_sql(batch_size)
bench::mark(
fetch_data(rs, batch_size),
min_iterations = 50L
)
}
)
# Parametrat e benchmarkut
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
#
# 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("koha mesatare, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE) 
2. Përgatitja e grupeve
I gjithë procesi i përgatitjes së grupeve përbëhet nga etapet e mëposhtme:
- Parsing disa JSON-ve, që përmbajnë vektorë string me koordinatat e pikave.
- Vizualizimi i linjave të ngjyrave sipas koordinatave të pikave në një imazh të kërkuar (për shembull, 256×256 ose 128×128).
- Transformimi i imazheve të marra në tensor.
Brenda garës së kernel-eve në Python, kjo detyrë u zgjidh kryesisht me mjetet OpenCV. Një nga ekuivalentët më të thjeshtë dhe më të dukshëm në R do të dukej si më poshtë:
Implementimi i transformimit të JSON në tensor në R
r_process_json_str <- function(json, line.width = 3,
color = TRUE, scale = 1) {
# Parsing JSON
coords <- jsonlite::fromJSON(json, simplifyMatrix = FALSE)
tmp <- tempfile()
# Delete temporary file upon function completion
on.exit(unlink(tmp))
png(filename = tmp, width = 256 * scale, height = 256 * scale, pointsize = 1)
# Empty plot
plot.new()
# Plot window size
plot.window(xlim = c(256 * scale, 0), ylim = c(256 * scale, 0))
# Line colors
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()
# Transform image into a 3D array
res <- png::readPNG(tmp)
return(res)
}
r_process_json_vector <- function(x, ...) {
res <- lapply(x, r_process_json_str, ...)
# Combine 3D image arrays into a 4D tensor
res <- do.call(abind::abind, c(res, along = 0))
return(res)
}Drawing is performed using standard R tools with saving to a temporary PNG stored in RAM (on Linux, R temporary directories are located in a directory /tmp, mounted in RAM). Then this file is read as a three-dimensional array with numbers ranging from 0 to 1. This is important since the more common BMP would have been read into a raw array with hex color codes.
Let's test the result:
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)) 
The batch will be formed as follows:
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
# ..$ : NULLThis implementation seems suboptimal to us as forming large batches takes an unreasonably long time, and we decided to draw on the experience of colleagues by utilizing a powerful library OpenCV. At that time, there was no ready-made package for R (and there isn't one now), so a minimal implementation of the necessary functionality was written in C++ with integration into the R code using Rcpp.
To solve the task, the following packages and libraries were used:
OpenCV for working with images and drawing lines. We used pre-installed system libraries and header files, as well as dynamic linking.
xtensor for working with multidimensional arrays and tensors. Header files included in the eponymous R package were used. The library allows working with multidimensional arrays, both in row-major and column-major order.
ndjson for parsing JSON. This library is used in xtensor automatically when available in the project.
RcppThread for organizing multi-threaded processing of JSON vectors. Header files provided by this package were used. From the more popular RcppParallel package, among other things, features a built-in mechanism for loop interruption (interrupt).
Duhet të theksohet se xtensor u dokazua se si një zgjidhje e shkëlqyer: përveç se ka funksionalitet të gjerë dhe performancë të lartë, zhvilluesit e tij ishin mjaft të përgjegjshëm dhe përgjigjen e tyre ndaj pyetjeve ishte të shpejtë dhe të detajuar. Me ndihmën e tyre, arritëm të realizojmë transformime të matricave OpenCV në tenzorë xtensor, si dhe mënyrën e bashkimit të tenzorëve 3-dimensionale të imazheve në një tenzor 4-dimensionale me dimensionet e sakta (në të vërtetë një grup).
Materialet për studimin e Rcpp, xtensor dhe RcppThread
Për të kompiluara skedarët që përdorin skedarët sistematikë dhe lidhjen dinamike me biblioteka të instaluara në sistem, ne përdorëm mekanizmin e pluginëve të realizuar në paketë Rcpp. Për gjetjen automatike të rrugëve dhe flamujve, përdorëm utilitarin popullor të linux pkg-config.
Realizimi i plugin-it Rcpp për përdorimin e bibliotekës OpenCV
Rcpp::registerPlugin("opencv", function() {
# Emrat e mundshëm të paketave
pkg_config_name <- c("opencv", "opencv4")
# Skedari binar i utilitarit pkg-config
pkg_config_bin <- Sys.which("pkg-config")
# Kontrollimi i pranishmërisë së utilitarit në sistem
checkmate::assert_file_exists(pkg_config_bin, access = "x")
# Kontrollimi i pranishmërisë së skedarit të konfigurimit OpenCV për pkg-config
check <- sapply(pkg_config_name,
function(pkg) system(paste(pkg_config_bin, pkg)))
if (all(check != 0)) {
stop("Konfigurimi OpenCV për pkg-config nuk u gjet", 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)
))
})Si rezultat i punës së plugin-it, në procesin e kompilimit do të zëvendësohen këto vlera:
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"Kodi për zbatimin e parsing-ut të JSON dhe formimin e grupit për ta dërguar në model është paraqitur nën spojlerin. Fillimisht, shtojmë direktorinë lokale të projektit për të kërkuar skedarë titujsh (kjo është e nevojshme për ndjson):
Sys.setenv("PKG_CXXFLAGS" = paste0("-I", normalizePath(file.path("src"))))Realizimi i transformimit nga JSON në tenzor në 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;
}Ky kod duhet të vendoset në skedarin src/cv_xt.cpp dhe të kompilohet me komandën Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); gjithashtu për funksionimin e tij do të nevojitet nlohmann/json.hpp nga . Kodi është ndarë në disa funksione:
to_xt— funksioni i templatuar për transformimin e matricës së imazhit (cv::Mat) në tenzorxt::xtensor;parse_json— funksioni parse JSON-në, nxjerr koordinatat e pikave duke i paketuar në një vektor;ocv_draw_lines— nga vektori i pikave të marrë çizë linja me ngjyra të ndryshme;process— bashkëngjit funksionet e përmendura më sipër, si dhe shton mundësinë për të shkallëzuar imazhin e fituar;cpp_process_json_str— mbështjellës për funksioninprocess, i cili eksporton rezultatin në një objekt R (masë shumëdimensionale);cpp_process_json_vector— mbështjellës për funksionincpp_process_json_str, e cila lejon të procesohen vektorët e vargjeve në mënyrë shumë-funksionale.
Për vizatimin e linjave me ngjyra të ndryshme u përdor modeli i ngjyrave HSV me konvertim në RGB. Le të testojmë rezultatin:
arr <- cpp_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256 3
plot(magick::image_read(arr)) 
Krahasimi i shpejtësisë së zbatimeve në R dhe 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
)
# Parametrat e benches
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
#
# 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)
# Kryerja e matjes
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
# & & & &
# 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("koha mesatare, s") +
theme_minimal() +
scale_color_discrete(name = "", labels = c("cpp", "r")) +
theme(legend.position = "bottom") 
Siç e shohim, rritja e shpejtësisë ishte shumë e konsiderueshme, dhe nuk është e mundur të arrihet kodi në C++ duke përdorur paralelizimin e kodit në R.
3. Iteratorët për eksporte të grupeve nga DB
R ka një reputacion të merituar si një gjuhë për përpunimin e të dhënave që mund të vendosen në RAM, ndërsa Python karakterizohet më shumë nga përpunimi iterativ i të dhënave, i cili lejon realizimin e lehtë dhe pa mundim të llogaritjeve out-of-core (llogaritje që përdorin memorie të jashtme). Një shembull klasik dhe i rëndësishëm për ne në kontekstin e detyrës që po përshkruajmë janë rrjetet neuronale të thella, të cilat janë të trajnuara me metodën e zbritjes gradient, duke aproksimuar gradientin në çdo hap me një sasi të vogël vëzhgimesh, ose mini-batch.
Kornizat për mësimin e thellë, të shkruara në Python, kanë klasa të posaçme që realizojnë iteratorë mbi të dhëna: tabela, imazhe në dosje, formate binarë dhe të tjera. Mund të përdoren variante të gatshme ose të shkruhen ato tona për detyra specifike. Në R ne mund të shfrytëzojmë të gjitha mundësitë e bibliotekës python keras me backend-et e saj duke përdorur paketën me të njëjtin emër, e cila punon mbi paketën reticulate. Ky i fundit meriton një artikull të gjerë të veçantë; ai jo vetëm që lejon ekzekutimin e kodit në Python nga R, por gjithashtu siguron transferimin e objekteve midis sesioneve R dhe Python, duke kryer automatikisht të gjitha konvertimet e nevojshme të tipeve.
Ne e kemi eliminuar nevojën për të ruajtur të gjitha të dhënat në RAM duke përdorur MonetDBLite, gjithë punën "neuronale" do ta kryejë kodi origjinal në Python, ne na mbetet vetëm të shkruajmë një iterator për të dhënat, pasi nuk ka asnjë të gatshëm për këtë situatë as në R dhe as në Python. Kërkesat për të janë në të vërtetë vetëm dy: ai duhet të kthejë batche në një cikël të pafund dhe të ruajë gjendjen e tij midis iteracioneve (kjo në R realizohet në mënyrë të thjeshtë me mjaftueshmëri). Më parë, brenda iteratorit ishte e nevojshme të shndërrohen shpejtësitë R në numpy arrays, por versioni aktual i paketës keras e bën këtë vetë.
Iteratori për të dhënat për trajnim dhe validim doli si në vijim:
Iteratori për të dhënat për trajnim dhe validim
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)
}
}Funksioni merr si argument një variabël me lidhjen me DB, numrat e rreshtave të përdorur, numrin e klasave, madhësinë e batch-it, masën (scale = 1 që korrespondon me vizatimin e imazheve 256x256 pikselë, scale = 0.5 — 128x128 pikselë), indikatorin e ngjyrave (color = FALSE cila c akton vizatimin në nuanca gri, duke përdorur color = TRUE çdo goditje vizatohet me një ngjyrë të re) dhe indikatorin e përpunimit për rrjetet e paraformuara në imagenet. Ky i fundit nevojitet për të skaluar vlerat e pikselëve nga intervali [0, 1] në intervalin [-1, 1], i cili është përdorur gjatë trajnimit të modeleve të ofruara me keras modele.
Funksioni i jashtëm përmban një verifikim të tipeve të argumenteve, një tabelë data.table me numrat e rreshtave të marrë në mënyrë të rastësishme nga samples_index dhe numrat e grupeve, numëruesi dhe numri maksimal i grupeve, si dhe shprehja SQL për shkarkimin e të dhënave nga DB. Shtesë, ne kemi përcaktuar brenda një ekuivalenti të shpejtë të funksionit keras::to_categorical(). Kemi përdorur thuajse të gjitha të dhënat për trajnim, duke lënë gjysmë përqind për validim, kështu që madhësia e epokës ishte e kufizuar nga parametri steps_per_epoch kur thërrisni keras::fit_generator(), dhe kushti if (i > max_i) saktë niste vetëm për iteratorin e validimit.
Në funksionin e brendshëm ndodh përzgjedhja e indekseve të rreshtave për grupin tjetër, shkarkimi i regjistrimeve nga DB me rritjen e numëruesit të grupeve, analizimi i JSON-ve (funksioni cpp_process_json_vector(), i shkruar në C++) dhe krijimi i masivave që përparojnë imazhet. Më pas krijohen vektorë one-hot me etiketa klasash, masivët me vlera pikseli dhe etiketat bashkohen në një listë, e cila është vlera e rikthyer. Për të përshpejtuar punën është përdorur krijimi i indekseve në tabela data.table dhe modifikimi në menyrë referencë - pa këto ‘truke’ të paketës data.table është mjaft e vështirë të imagjinosh një funksionim efikas me volume të konsiderueshme të dhënash në R.
Rezultatet e matjeve të shpejtësisë së punës në një laptop Core i5 duken si më poshtë:
Benchmark-i i iteratorit
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]]
# Indekset për grupin e të dhënave për trajnimin
train_ind <- sample(ind, floor(length(ind) * 0.995))
# Indekset për grupin e verifikimit
val_ind <- ind[-train_ind]
rm(ind)
# Faktorin e shkallës
scale <- 0.5
# Kryerja e matjeve
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
)
}
)
# Parametrat e benchmark-ut
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
#
# 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("koha mesatare, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE) 
Nëse ka një volum të mjaftueshëm RAM-i, mund të përshpejtohet ndjeshëm funksionimi i bazës së të dhënave duke e transferuar atë në këtë RAM (për detyrën tonë mjafton 32 GB). Në Linux, pjesa mementohet në mënyrë të paracaktuar /dev/shm, duke zënë deri në gjysmën e volumit të RAM-it. Mund të alokosh edhe më shumë, duke redaktuar /etc/fstab, në mënyrë që të rezultojë një shënim si tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Sigurohu që ribashkohu dhe kontrollo rezultatin duke ekzekutuar komandën df -h.
Iteratori për të dhënat testuese duket shumë më i thjeshtë, pasi dataset-i i testit vendoset tërësisht në RAM.
Iteratori për të dhënat testuese
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. Zgjedhja e arkitekturës së modelit
Arkitektura e parë që u përdor ishte , karakteristikat e së cilës janë analizuar në mesazhin. Ajo është e pranishme në paketën standarde keras dhe, për rrjedhojë, është e disponueshme në paketën me të njëjtin emër për R. Por, kur përpiqeshim ta përdorim atë me imazhe me një kanal, u zbulua diçka e çuditshme: tensor-i hyrës duhet të ketë gjithmonë dimensionin (batch, height, width, 3), dmth numri i kanaleve nuk mund të ndryshohet. Në Python nuk ka një kufizim të tillë, prandaj ne u nxituam dhe shkruam implementimin tonë të kësaj arkitekture, duke e ndjekur artikullin origjinal (pa dropout, që ekziston në versionin e keras):
Arkitektura mobilenet v1
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 %
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 %
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 % compile(
optimizer = optimizer,
loss = loss,
metrics = metrics
)
return(model)
}Disavantazhet e këtij qasje janë të dukshme. Dëshirojmë të testojmë shumë modele, por nuk dëshirojmë të rishkruajmë çdo arhitekture manualisht. Gjithashtu, ishim të privuar nga mundësia për të përdorur peshat e modeleve që ishin paraprakisht të trajnuara në imagenet. Si zakonisht, studimi i dokumentacionit ndihmoi. Funksioni get_config() mund të marrësh përshkrimin e modelit në një format të redaktueshëm (base_model_conf$layers — një listë standarde R), dhe funksioni from_config() kthen në një objekt model:
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)Tani është e lehtë të shkruash një funksion universale për të marrë çfarëdo nga modelet e ofruara si pjesë e keras modeleve me pesha të trajnuara në imagenet ose pa to:
Funksioni për ngarkimin e arkitekturave të gatshme
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) {
# Kontrollimi i argumenteve
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)
# Marrim objektin nga paketa keras
model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
# Kontrolli i disponueshmërisë së objektit në paketë
if (is.null(model_fun)) {
stop("Modeli ", shQuote(name), " nuk u gjet.", call. = FALSE)
}
base_model <- model_fun(
input_shape = input_shape,
include_top = FALSE,
weights = weights,
pooling = pooling
)
# Nëse imazhi nuk është me ngjyra, ndryshojmë dimensionalitetin e inputit
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)
}Kur përdoren imazhe njëkanalëshe, pesha të trajnuara nuk përdoren. Kjo mund të rregullohej: duke përdorur funksionin get_weights() për të marrë peshat e modelit në një listë nga arrat R, për të ndryshuar dimensionalitetin e elementit të parë të kësaj liste (duke marrë ndonjë kanal ngjyrash ose me mesataren e të treve), dhe pastaj për të ngarkuar peshat përsëri në model me funksionin set_weights(). Ne nuk e shtuam këtë funksionalitet, pasi në këtë fazë ishte e qartë se ishte më produktive të punosh me imazhe me ngjyra.
Masën kryesore të eksperimenteve e kemi bërë duke përdorur mobilenet version 1 dhe 2, si dhe resnet34. Në këtë garë, arkitekturat më moderne si SE-ResNeXt performuan mirë. Fatkeqësisht, nuk kishim realizime të gatshme në dispozicion, dhe nuk kemi shkruar të tonat (por patjetër do të shkruajmë).
5. Parametrizimi i skripteve
Për lehtësi, i gjithë kodi për nisjen e trajnimit u përgatit si një skript i vetëm, i parametrizuar me si nga:
doc <- '
Përdorimi:
train_nn.R --help
train_nn.R --list-models
train_nn.R [options]
Mundësitë:
-h --help Trego këtë mesazh.
-l --list-models Lista e modeleve të disponueshme.
-m --model= Emri i modelit të rrjetit nervor [default: mobilenet_v2].
-b --batch-size= Madhësia e grupit [default: 32].
-s --scale-factor= Faktori i shkallës [default: 0.5].
-c --color Përdor linja me ngjyrë [default: FALSE].
-d --db-dir= Rruga drejt dosjes së bazës së dhënash [default: Sys.getenv("db_dir")].
-r --validate-ratio= Raporti i verifikimit të mostrës [default: 0.995].
-n --n-gpu= Numri i GPU-ve [default: 1].
'
args <- docopt::docopt(doc)Paketa docopt është një implementim për R. Me të, skriptet ekzekutohen me komanda të thjeshta si Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db ose ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, nëse skedari train_nn.R është ekzekutues (kjo komandë do të fillojë trajnimin e modelit resnet50 në imazhe me tre ngjyra me madhësi 128x128 piksel, baza e të dhënave duhet të jetë në dosjen /home/andrey/doodle_db). Në listë mund të shtohet norma e të mësuarit, lloji i optimizuesit dhe çdo parameter tjetër i konfigurueshëm. Në procesin e përgatitjes së publikimit u zbulua se arkitektura mobilenet_v2 nga versioni aktual keras në R përdoret për shkak të ndryshimeve që nuk janë marrë parasysh në paketën R — po presim që të korrigjohet.
Ky qasje lejon që eksperimentet me modele të ndryshme të përshpejtohen ndjeshëm krahasuar me ekzekutimin më tradicional të skripteve në RStudio (si një alternativë të mundshme mund të përmendim paketën ). Por avantazhi kryesor qëndron në mundësinë për të menaxhuar lehtësisht ekzekutimin e skripteve në Docker ose thjesht në server, pa instaluar për këtë RStudio.
6. Dockerizimi i skripteve
Ne përdorëm Docker me qëllim të sigurimit të portabilitetit të mjedisit për trajnimin e modeleve midis anëtarëve të ekipit dhe për shpërndarjen e shpejtë në cloud. Të nisim njohjen me këtë vegël relativisht të pazakontë për programuesit R mund të fillojmë me serinë e publikimeve ose me .
Docker lejon si krijimin e imazheve të veta "nga fillimi", ashtu edhe përdorimin e imazheve të tjera si bazë për të krijuar të veta. Duke analizuar variantet e disponueshme, arritëm në përfundimin se instalimi i drejtuesve NVIDIA, CUDA+cuDNN dhe biblioteka të Python-it është një pjesë mjaft e voluminshme e imazhit, dhe vendosëm të marrim si bazë imazhin zyrtar tensorflow/tensorflow:1.12.0-gpu, duke shtuar aty paketat e nevojshme të R.
Shkalla përfundimtare e Dockerfile-it doli kështu:
Dockerfile
FROM tensorflow/tensorflow:1.12.0-gpu
MAINTAINER Artem Klevtsov
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
Për lehtësinë e përdoruesve, paketat e përdorura janë të vendosura në variabla; pjesa kryesore e skripteve të shkruara kopjohet brenda kontejnerëve gjatë ndërtimit. Ne gjithashtu e ndërrojmë shell-in në /bin/bash për lehtësinë e përdorimit të përmbajtjes /etc/os-release. Kjo ndihmon në shmangien e nevojës për të specifikuar versionin e OS në kod.
Përveç kësaj, është shkruar një skript i vogël bash, i cili lejon ekzekutimin e kontejnerit me komanda të ndryshme. Për shembull, këto mund të jenë skripte për trajnim të rrjeteve nervore, të vendosura më parë brenda kontejnerit, ose një shell për debugging dhe monitorimin e funksionimit të kontejnerit:
Skripti për ekzekutimin e kontejnerit
#!/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}Nëse ky skript bash ekzekutohet pa parametra, brenda kontejnerit do të thirret skripti train_nn.R me vlera të paracaktuara; nëse argumenti i parë pozicional është "bash", atëherë kontejneri do të nisë në modin interaktiv me shell-in. Në të gjitha rastet e tjera, ndodhi zëvendësimi i vlerave të argumenteve pozicionale: CMD="Rscript /app/train_nn.R $@".
Duhet të vihet re se drejtoritë me të dhënat e origjinës dhe bazën e të dhënave, si dhe drejtoritë për ruajtjen e modeleve të trajnuara, montohen brenda kontejnerit nga sistemi host, çka lejon qasjen në rezultatet e punës së skripteve pa manovra të panevojshme.
7. Përdorimi i shumë GPU në Google Cloud
Një nga karakteristikat e garës ishin të dhënat mjaft të zhurmshme (shihni imazhin e titullit, e marrë nga @Leigh.plt nga ODS-slack). Për t'u përballur me këtë, ndihmuan grupet e mëdha, dhe pas eksperimenteve në PC me 1 GPU, vendosëm të mësojmë trajnimin e modeleve në disa GPU në re. Përdorëm GoogleCloud () për shkak të zgjedhjes së madhe të konfiguracioneve të disponueshme, çmimeve të arsyeshme dhe bonusit prej $300. Për shkak të lakmisë, u porosit një instancë me 4xV100 me SSD dhe shumë RAM, dhe kjo ishte një gabim i madh. Një makinë e tillë konsumon shpejt para, duke eksperimentuar pa një pipeline të punuar mund të shpenzosh shumë. Për qëllime mësimore, është më mirë të marrësh K80. Sidoqoftë, sasia e madhe e RAM ishte e dobishme — SSD-në cloud nuk na impresionoi me shpejtësinë, kështu që na duhej të transferonim bazën e të dhënave në çdo fillim instancës në dev/shm.
Fragmenti i kodit që është më i interesuar tregon për përdorimin e disa GPU-ve. Fillimisht, modeli krijohet në CPU duke përdorur menaxherin e kontekstit, krejt siç bëhet në 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
)
})Pastaj, modeli i papërfunduar (këtu është e rëndësishme) kopjohet në numrin e caktuar të GPU-ve të disponueshëm, dhe vetëm atëherë kompilohesh:
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)
)Nuk arritëm ta realizojmë në mënyrë klasike teknikën e ngrirjes së të gjithë niveleve, përveç atij të fundit, trajnimin e nivelit të fundit, ngrirjen dhe stërvitjen e modelit si një tërësi për disa GPU.
E ndjekur trajnimin pa përdorimin e tensorboard, duke u kufizuar në regjistrimin e logeve dhe ruajtjen e modeleve me emra informativë pas çdo epoke:
Kolbë
# Шаблон имени файла лога
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. Në vend të përfundimit
Një sërë problemesh me të cilat u përballëm nuk arritëm t'i zgjidhim ende:
- në keras nuk ka një funksion të gatshëm për të kërkuar automatikisht shpejtësinë optimale të mësimit (analogun e
lr_findernë bibliotekë fast.ai); me disa përpjekje, mund të portosh implementimet e jashtme në R, për shembull, ; - si pasojë e pikës së mëparshme, nuk arritëm të përcaktonim shpejtësinë e duhur të mësimit në përdorimin e disa GPU-ve;
- mungojnë arkitekturat moderne të rrjeteve nervorë, veçanërisht ato të trajnuara më parë në imagenet;
- nuk ka politikën one cycle dhe shkallët diskriminuese të mësimit (kosi annealing sipas kërkesës sonë ishte , faleminderit ).
Çfarë dobi të dobishme arritëm të nxjerrim nga kjo garë:
- Në rrethana të mjaftueshme, mund të punosh pa dhimbje me sasi të konsiderueshme të dhënash (duke e kaluar ndjeshëm madhësinë e RAM). Paketa data.table kurse ruajtjen e memories përmes modifikimit in-place të tabelave, duke shmangur kopjimin e tyre, dhe me përdorimin e duhur të mundësive të tij, tregon pothuajse gjithmonë shpejtësinë më të lartë nga të gjitha mjetet e njohura për gjuhët scripting. Ruajtja e të dhënave në DB në shumë raste lejon që të mos mendoni fare për nevojën për të shtypur të gjithë datasetin në RAM.
- Funksionet e ngadalta në R mund të zëvendësohen me ato të shpejta në C++ përmes paketës Rcpp. Nëse përveç kësaj përdorim RcppThread ose RcppParallel, marrim realizime të shumëfishta dhe të platformës kryesore, prandaj nuk është e nevojshme të paralelizoni kodin në nivelin R.
- Paketat Rcpp mund të përdoren pa njohuri të thella në C++, minimumi i nevojshëm është i paraqitur . Skedarët e titullit për disa biblioteka të shkëlqyera në C si xtensor janë të disponueshme në CRAN, që do të thotë se formohet infrastruktura për realizimin e projekteve që integrojnë në R kodin me performancë të lartë në C++. Një lehtësi shtesë — ndihma për sintaksën dhe analisti statik i kodit në C++ në RStudio.
- docopt lejon ekzekutimin e skripteve autonome me parametra. Kjo është e përshtatshme për përdorim në serverë të largët, përfshirë nën Docker. Të kryesh eksperimente me trajnimin e rrjeteve nervore në RStudio për shumë orë është e papërshtatshme, dhe gjithashtu instalimi i IDE në server nuk është gjithmonë i justifikuar.
- Docker ofron portabilitetin e kodit dhe riprodhueshmërinë e rezultateve midis zhvilluesve me versione të ndryshme të OS dhe bibliotekave, si dhe thjeshtësinë e ekzekutimit në serverë. Të ekzekutosh të gjithë pipeline-n për trajnimin mund të bëhet me vetëm një komandë.
- Google Cloud është një mënyrë me kosto të përballueshme për të eksperimentuar në pajisje të shtrenjta, por është e nevojshme të bëhet një zgjedhje e mençur e konfigurimeve.
- Maturimi i shpejtësisë së punës së segmenteve të veçanta të kodit është shumë e dobishme, sidomos me kombinimin e R-së dhe C++, dhe me paketën bench — është gjithashtu shumë e lehtë.
Në përgjithësi, kjo eksperiencë ka qenë shumë e dobishme dhe ne vazhdojmë të punojmë mbi zgjidhjen e disa prej problemeve të përmendura.
Burimi: habr.com
