
Tere, Habr!
Eelmisel sügisel toimus Kaggle'is joonistatud piltide klassifitseerimise konkurss Quick Draw Doodle Recognition, kus osales R-i meeskond koosseisus , ja . Me ei hakka võistlust detailsemalt kirjeldama, sellest on juba kirjutatud .
Medalite kogumist seekord ei õnnestunud, kuid saavutati palju väärtuslikku kogemust, seega sooviksin rääkida kogukonnale mõningatest kõige huvitavamatest ja kasulikematest asjadest, mis on seotud Kaggle'i ja igapäevase tööga. Arutatud teemade hulgas: mitte lihtne elu ilma OpenCV, JSON'i parsimine (neid näiteid käsitletakse C++ koodi integreerimise kaudu R-i skriptidesse või pakettidesse kasutades Rcpp), skriptide parameetriseerimine ja lõpliku lahenduse konteinerimine. Kõik postituses sisalduv kood on käivitatavas vormis saadaval .
Sisu:
1. Tõhus andmete laadimine CSV-failist MonetDB andmebaasi
Käesolevas võistluses esitatud andmed ei ole valmis piltidena, vaid 340 CSV-faili (iga klassi kohta üks fail), mis sisaldavad JSON-e punktide koordinaatidega. Need punktid ühendades joontega, saame lõpuks pildi suuruses 256x256 pikslit. Iga kirje jaoks tuuakse välja ka silt, kas pilt oli õigesti tuvastatud andmekogumise hetkel kasutatud klassifikaatori poolt, autori elukoha kahe tähega riigikood, unikaalne identifikaator, ajatempli silt ja klassinimi, mis vastab faili nimele. Lihtsustatud versioon algandmetest kaalub 7,4 GB arhiivis ja umbes 20 GB pärast dearkiveerimist, samas kui täielikud andmed dearkiveerimise järel võtavad 240 GB. Korraldajad garanteerisid, et mõlemad versioonid toovad välja samad joonised, mistõttu on täisversioon üleliigne. Igatahes, 50 miljoni pildi säilitamine graafilistes failides või massiivides on kohe tunnistatud ebaotstarbekaks ning otsustasime kõik CSV-failid arhiivist kokku sulatada. train_simplified.zip andmete baasi, mille tulemusena genereeritakse igas mõõdus pildid "otse" iga partii jaoks.
Andmebaasiks valiti hästi tuntud ja usaldusväärne MonetDB, nimelt R-i rakendus paketina . Pakett sisaldab sisseehitatud andmebaasiserveri versiooni ja võimaldab käivitada serverit otse R-seansist ning selle juures töötada. Andmebaasi loomine ja sellega ühendamine toimub ühe käsuga:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))Peame looma kaks tabelit: ühe kõigi andmete jaoks ja teise laadimise ajal üles laaditud failide teenuseteabe jaoks (kasulik, kui midagi läheb valesti ja protsessi tuleb jätkata pärast mitme faili üleslaadimist):
Tabelite loomine
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"
)
)
}Andmete kiireim üleslaadimise meetod osutus CSV-failide otseseks kopeerimiseks SQL-i vahenditega — käsk KOPIY OFFSET 2 TABELLISE PATHIST ALLDEMADEGADEL, KASUTADES PIIRDEID ',','n','"' NULL KUI '' PARIM TÖÖ, kus tabelinimi — tabeli nimi ja failitee — tee faili. Arhiiviga töötamisel leiti, et sisseehitatud rakendus unzip R-is ei toimi õigesti mitmete failidega arhiivist, seetõttu kasutasime süsteemi unzip (parameetri kaudu getOption("unzip")).
Funktsioon andmete salvestamiseks andmebaasi
#' @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))
}Kui on vaja tabelit enne andmebaasi salvestamist töödelda, piisab, kui edastada argumendiks eeltöötlus funktsioon, mis töötleb andmed.
Kood andmete järkjärguliseks laadimiseks andmebaasi:
Andmete salvestamine andmebaasi
# Список файлов для записи
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->SSDAndmete laadimise aeg võib varieeruda sõltuvalt kasutatud salvestusseadmest. Meie juhul toimub lugemine ja kirjutamine SSD-lt või USB-st (algfail) SSD-le (andmebaasi) alla 10 minuti.
Veel mõned sekundid kuluvad klassi täisarvu märgise ja indeksiveergu loomise (KORRALDATUD INDEKS) rea numbritega, mille alusel valitakse tähelepanekud partii loomisel:
Lisa veergude ja indeksi loomine
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)"))Ette ülesande lahendamiseks, et luua partii "režiimis", oli meil vaja saavutada maksimaalne kiirus juhuslike ridade väljavõtmiseks tabelist. doodles. Selle saavutamiseks kasutasime kolme nippi. Esimene seisnes vaatluse ID hoidmise tüübi mõõtme vähendamises. Algsetes andmekogudes on ID hoidmiseks vajalik tüüp bigint, kuid vaatlemise arv võimaldab nende identifikaatoreid, mis on järjestusnumbrid, mahutada tüüpi int. Otsing toimub sel juhul märgatavalt kiiremini. Teine nipp oli kasutada KORRALDATUD INDEKS — sellele lahendusele jõudsime empiiriliselt, testides kõiki võimalikke Kolmas oli parameetriliste päringute kasutamine. Meetodi essents seisneb käsu PREPARE ühekordses täitmisel, liigse taoline päringute loomisel, kuid tegelikus elus saame võrreldes tavalisega kasu. SELECT languste statistilise vea piiresse.
Andmete üleslaadimise protsess kasutab mitte rohkem kui 450 MB RAM-i. See tähendab, et kirjeldatud lähenemine võimaldab töötada kümnete gigabaitide suuruste andmestikega praktiliselt igasugustes odavates seadmetes, sealhulgas mõnedel ühesilmaemulatoritel, mis on üsna muljetavaldav.
On vaja teha juhuslike andmete väljavõtmise kiirusmõõtmised ja hinnata skaleeritavust erineva suurusega parteide valimisel:
Andmebaasi tõhususe test
library(ggplot2)
set.seed(0)
# Andmebaasi ühendamine
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
# Funktsioon päringu ettevalmistamiseks serveris
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)
}
# Funktsioon andmete väljavõtmiseks
fetch_data <- function(rs, batch_size) {
ids <- sample(seq_len(n), batch_size)
res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
return(res)
}
# Testi läbiviimine
res_bench <- bench::press(
batch_size = 2^(4:10),
{
rs <- prep_sql(batch_size)
bench::mark(
fetch_data(rs, batch_size),
min_iterations = 50L
)
}
)
# Bänkimise parameetrid
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("mediaan aeg, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE) 
2. Batšite ettevalmistamine
Kogu partii ettevalmistamise protsess koosneb järgmistest etappidest:
- Mitme JSON-i parsimine, mis sisaldavad punktide koordinaatidega stringide vektoreid.
- Värviliste joonte joonistamine punktide koordinaatide järgi soovitud suurusega pildil (näiteks 256×256 või 128×128).
- Saadud piltide teisendamine tensori vormi.
Python'i kernelite võistluse raames lahendati ülesanne peamiselt vahenditega OpenCV. Üks lihtsamaid ja ilmsemaid analooge R-is näeks välja selline:
JSON-i teisendamise rakendamine tensori vormi R-is
r_process_json_str <- function(json, line.width = 3,
color = TRUE, scale = 1) {
# JSON parse
coords <- jsonlite::fromJSON(json, simplifyMatrix = FALSE)
tmp <- tempfile()
# Remove temporary file after function completes
on.exit(unlink(tmp))
png(filename = tmp, width = 256 * scale, height = 256 * scale, pointsize = 1)
# Empty plot
plot.new()
# Plot window dimensions
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()
# Convert image to 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)
}Joonistamine toimub R-i standardsete tööriistadega, salvestades ajutisse PNG-faili, mis asub RAM-is (Linuxi puhul asuvad ajutised R-i kataloogid) /tmp, mis on monteeritud RAM-i). Seejärel loetakse see fail kolmemõõtmeliseks massiiviks, mille numbrid on vahemikus 0 kuni 1. See on oluline, kuna tavaline BMP loetaks hex-koodide raw-massiiviks.
Katsume tulemust:
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)) 
Batch koostatakse järgmiselt:
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
# ..$ : NULLSee rakendus tundus meile mitteoptimaalne, kuna suurte partii(de) moodustamine võtab ebanormaalselt kaua aega, ja me otsustasime kasutada kolleegide kogemusi, kaasates võimsat teeki OpenCV. Sellel ajal ei olnud R jaoks valmis paketti (ja ei ole ka praegu), seetõttu kirjutati C++-s minimaalne rakendus vajaliku funktsionaalsuse integreerimiseks R-i koodi. Rcpp.
Ülesande lahendamiseks kasutati järgmisi pakette ja teeke:
OpenCV piltidega töötamiseks ja jooniste tegemiseks. Kasutati eelnevalt installitud süsteemiteeke ja päise faile ning ka dünaamilist linkimist.
xtensor mõõtmete massiivide ja tensoritega töötamiseks. Kasutasime päiseid, mis on kaasatud samanimelisse R-paketti. Raamatukogu võimaldab töötada mõõtmete massiividega, nii row major kui column major järjekorras.
ndjson JSON-i parsimiseks. Seda raamatukogu kasutatakse xtensor automaatsete olemasolu korral projektis.
RcppThread JSON-idest vektori mitme töötluse korraldamiseks. Kasutasime selle paketi poolt pakutavaid päiseid. Rohkem tuntud RcppParallel pakett erineb muu hulgas sisseehitatud katkestusmehhanismiga (interrupt).
Oluline on märkida, et xtensor oli lihtsalt leid: lisaks laiale funktsionaalsusele ja kõrgele jõudlusele olid tema arendajad üsna reageerivad, vastates kiiresti ja põhjalikult tekkivatele küsimustele. Nende abiga õnnestus realiseerida OpenCV-matriiside teisendamine xtensor-tensoriteks, samuti meetod kolme mõõtmeliste piltide tensorite ühendamiseks neljamõõtmeliseks tensoriks õige mõõtmega (praktiliselt partii).
Materjalid Rcpp, xtensor ja RcppThread uurimiseks
Süsteemifailide ja süsteemis installitud teekidega dünaamiliseks linkimiseks vajalike failide kompileerimiseks kasutasime pistikprogrammide mehhanismi, mis on rakendatud paketis Rcpp. Tee ja lipu automaatseks leidmiseks kasutasime populaarset Linuxi utiliiti pkg-config.
Rcpp-pistikprogrammi rakendamine OpenCV teegi kasutamiseks
Rcpp::registerPlugin("opencv", function() {
# Võimalikud paketi nimed
pkg_config_name <- c("opencv", "opencv4")
# pkg-config utiliidi binaarfail
pkg_config_bin <- Sys.which("pkg-config")
# Kontroll utiliidi olemasolu süsteemis
checkmate::assert_file_exists(pkg_config_bin, access = "x")
# Kontroll OpenCV seadete faili olemasolu pkg-config jaoks
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)
))
})Pistikprogrammi töö tulemusena kompileerimise käigus asendatakse järgmised väärtused:
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"JSON-i parsimise ja mudelile edastamiseks mõeldud partii loomise kood on esitatud peidetud osa all. Eeltingimusena lisame projekti kohalikku katalooge pealkirjade failide leidmiseks (vajalik ndjson-i jaoks):
Sys.setenv("PKG_CXXFLAGS" = paste0("-I", normalizePath(file.path("src"))))JSON-i teisendamise C++ kood
// [[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;
}See kood tuleks salvestada faili src/cv_xt.cpp ja kompileerida käsuga Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); töötamiseks on vajalik ka nlohmann/json.hpp kohast . Kood on jaotatud mitmeks funktsiooniks:
to_xt— mallifunktsioon pildimatrixi (cv::Mat) tensoriksxt::xtensor;parse_json— funktsioon analüüsib JSON-stringi, eraldades punktide koordinaadid ja pakkides need vektorisse;ocv_draw_lines— joonistab saadud punkti vektorist erinevat värvi jooni;process— ühendab ülalkirjeldatud funktsioonid ning lisab võimaluse saadud pilti skaleerida;cpp_process_json_str— funktsiooni ümbritsev wrapper,processmis eksportib tulemuse R-objekti (mitmemõõtmelise massiivi);cpp_process_json_vector— funktsiooni ümbritsev wrapper,cpp_process_json_str, mis võimaldab töödelda stringivektorit mitme lõime režiimis.
Erinevate värvijoonte joonistamiseks kasutati HSV värvimudelit, millele järgnes RGB-ks konverteerimine. Testime tulemust:
arr <- cpp_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256 3
plot(magick::image_read(arr)) 
R ja C++ rakenduste kiirusvõrdlus
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
)
# Benchmarki parameetrid
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)
# Mõõtmise teostamine
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
# <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("mediaan aeg, s") +
theme_minimal() +
scale_color_discrete(name = "", labels = c("cpp", "r")) +
theme(legend.position = "bottom") 
Kuidas näha, oli kiirusetõus märkimisväärne, ja R keeles koodi paraliseerimisega C++ koodi järele jõuda ei õnnestu.
3. Iteraatorid andmebaasi gruppide väljundiks
R-l on õigustatud maine mäluandmete töötlemise keele osas, samas kui Pythonile on iseloomulik andmete iteratiivne töötlemine, mis võimaldab kenasti ja lihtsalt läbi viia out-of-core arvutusi (arvutused, mis kasutavad välist mälu). Klassikaline ja aktuaalne näide, mis on meie kontekstis asjakohane, on sügavad närvivõrgud, mida õpetatakse gradientide langetamise meetodil, gradientide lähendamisega igal sammul väikese andmeosa või mini-grupi põhjal.
Sügava õppe raamistikud, mis on kirjutatud Pythonis, sisaldavad eriklasse, mis töötlevad andmeid: tabeleid, pilte kaustades, binaarformaatides jne. Saame kasutada valmis variante või kirjutada oma, et täita spetsiifilisi ülesandeid. R-is saame kasutada kõiki Python'i raamatukogu võimalusi. keras oma erinevate tagaplaanidega sama nimetusega paketi abil, mis omakorda töötab paketi peal reticulate. Viimane väärib eraldi suurt artiklit; see mitte ainult ei võimalda Pythonis koodi käitada R-st, vaid tagab ka objektide edastamise R- ja Python-seansside vahel, automaatselt teostades kõik vajalikud tüüpide konversioonid.
Me oleme loobunud vajadusest hoida kõik andmed RAM-is, kasutades MonetDBLite-d; kogu "neuraalvõrgu" töö teeb originaalkood Pythonis, meie ülesanne on vaid kirjutada andmete iteratoor, kuna sellist valmis lahendust ei ole ei R- ega Pythonis. Sellele on põhimõtteliselt vaid kaks nõuet: see peab tagastama partieid lõpmatus tsüklis ja säilitama oma oleku iteratsioonide vahel (viimane R-is on lihtsaim viis saavutada sulgemiste abil). Varasemalt tuli iteratooris selgelt konverteerida R-massiivid numpy-massiivideks, kuid paketi praegune versioon keras teeb seda ise.
Koolitus- ja valideerimisandmete iteratoor on järgmine:
Koolitus- ja valideerimisandmete iteratoor
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)
}
}Funktsioon võtab sisendiks andmebaasiühenduse, kasutatavate ridade numbrid, klasside arvu, partii suuruse, skaala (skaala = 1 vastab piltide joonistamisele suuruses 256x256 pikslit, skaala = 0.5 — 128x128 pikslit), värvuse indikaator (värv = FALSE määrab joonistamise halltoonis, kui kasutatakse värv = TRUE igal joonistusel on uus värv) ja indikaator eeltöötluseks mudelile, mis on eelnevalt koolitatud imagenet-il. Viimane on vajalik, et skaleerida pikslite väärtused vahemikust [0, 1] vahemikku [-1, 1], mida kasutati juba välja antud keras mudelite koolitamisel.
Väline funktsioon sisaldab argumentide tüüpide kontrollimist, tabelit data.table juhuslikult segatud ridade numbritega samples_index ja partii numbritega, loendur ja maksimaalne partii arv, ning SQL-lause andmete väljavõtmiseks andmebaasist. Täiendavalt oleme määratlenud kiirversiooni funktsioonist keras::to_categorical().Koolitamiseks oleme kasutanud peaaegu kogu andmestikku, jättes poole protsendi valideerimiseks, seega oli epohhi suurus piiratud parameetriga steps_per_epoch kui kutsutakse keras::fit_generator(), ja tingimus if (i > max_i) sattus ainult valideerimise iteratsioonile.
Sisemises funktsioonis valitakse järgmise partii rea indeksid, laaditakse andmed andmebaasist, suurendades partii loendurit, parsitakse JSON-e (funktsioon cpp_process_json_vector(), kirjutatud C++ keeles) ja luuakse massiivid, mis vastavad piltidele. Seejärel luuakse one-hot vektoreid klassimärkidega, massiivid piksliväärtustega ja sildid ühendatakse nimekirjaks, mis on tagastatav väärtus. Töö kiirus on parendatud tabelite indeksite loomisega data.table ja viite kaudu modifitseerimisega — ilma nende "nippideta" on üsna raske ette kujutada tõhusat töötlemist märkimisväärsete andmehulkadega R-is. data.table Noote järgi mõõdetud kiirusetulemused lahtiselt Core i5-l näevad välja järgmised:
Iteraatori tulemuslikkuse test
Iteraatori benchmark
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]]
# Treeningvaliku indeksid
train_ind <- sample(ind, floor(length(ind) * 0.995))
# Validatsiooni valiku indeksid
val_ind <- ind[-train_ind]
rm(ind)
# Skaleerimise koefitsient
scale <- 0.5
# Mõõtmise teostamine
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
)
}
)
# Bänkmärkide parameetrid
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("mediaan aega, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE) 
Kui piisavalt RAM-i on olemas, saab andmebaasi tööd märkimisväärselt kiirendada, viies selle mälu (32 GB on meie ülesande jaoks piisav). Linuxis on vaikimisi ühendatud osa, /dev/shm, mis kasutab kuni poole mälu mahust. Saame määrata ka rohkem, muutes /etc/fstab, et saaksime kirjutise kujul tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Peame kindlasti taaskäivitama ja kontrollima tulemust, käivitades käsu df -h.
Testandmete iteratsioon näeb palju lihtsam välja, kuna testandmed mahutatakse täielikult mällu:
Testandmete iteratsioon
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. Mudeli arhitektuuri valik
Esimene kasutatud arhitektuur oli , mille omadused on lahti seletatud postituses. See on standardsetes tarnetes olemas keras ja seega saadaval samanimelises paketis R jaoks. Kuid ühekordsete piltidega kasutamisel selgus imelik asi: sisenditensor peab alati olema mõõtmetega (batch, kõrgus, laius, 3), st, see number of channels cannot be changed. In Python, there is no such restriction, so we rushed and wrote our implementation of this architecture, following the original article (without dropout, which is present in the Keras version):
mobilenet v1 arhitektuur
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)
}Selle lähenemise puudused on ilmne. Soovitakse testida palju mudeleid, kuid iga arhitektuuri käsitsi ümber kirjutamine ei ole soovitav. Samuti ei saanud me kasutada mudeleid, millele on eelnevalt õpetatud Imagenet-is. Nagu alati, aitas dokumentatsiooni uurimine. Funktsioon get_config() võimaldab saada mudeli kirjelduse redigeerimiseks sobivas vormingus (base_model_conf$layers — tavaline R-i loend), samas funktsioon from_config() teeb vastupidise teisenduse mudeli objekti:
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)Nüüd on lihtne kirjutada universaalset funktsiooni, et saada ükskõik milline mudel, mis on varustatud Imagenet-is õpetatud kaaludega või ilma nendeta: keras Funktsioon valmis arhitektuuride laadimiseks
Valmis arhitektuuride laadimise funktsioon
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) {
# Kontrollige argumendid
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)
# Saame objekti keras paketist
model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
# Kontrollime objekti olemasolu paketis
if (is.null(model_fun)) {
stop("Mudelit ", shQuote(name), " ei leitud.", call. = FALSE)
}
base_model <- model_fun(
input_shape = input_shape,
include_top = FALSE,
weights = weights,
pooling = pooling
)
# Kui pilt ei ole värviline, muudame sisendi mõõtmeid
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)
}Ühekanaliste piltide kasutamisel ei rakendata eeltreenitud kaalusid. Seda oleks saanud parandada funktsiooni abil get_weights() saamiseks, et saada mudeli kaalusid R-sarnaste massiivide loendina, muutes selle loendi esimese elemendi mõõtmeid (valides ühe värvikanali või keskmistades kõik kolm), seejärel laadides kaalud mudelisse tagasi funktsiooniga set_weights(). Me ei lisanud seda funktsionaalsust, kuna sellel etapil oli juba selge, et efektiivsem on töötada värviliste piltidega.
Peamised eksperimendid viisime läbi mobilenet versioonidega 1 ja 2 ning samuti resnet34-ga. Sellel võistlusel näitasid paremaid tulemusi kaasaegsemad arhitektuurid, nagu SE-ResNeXt. Kahjuks ei olnud meil valmis rakendusi, kuid oma omad me ei kirjutanud (aga kindlasti kirjutame).
5. Skriptide parametriseerimine
Meeldivuse huvides oli kogu treeningu käivitamiseks mõeldud kood vormindatud üheks skriptiks, mida parametriseeriti järgmiselt:
doc <- '
Kasutus:
train_nn.R --help
train_nn.R --list-models
train_nn.R [valikud]
Valikud:
-h --help Näita seda sõnumit.
-l --list-models Loetle saadaval olevad mudelid.
-m --model= Sünaptilise võrgu mudeli nimi [vaikimisi: mobilenet_v2].
-b --batch-size= Partii suurus [vaikimisi: 32].
-s --scale-factor= Skaala tegur [vaikimisi: 0.5].
-c --color Kasutada värvilisi jooni [vaikimisi: FALSE].
-d --db-dir= Teed andmebaasi kataloogini [vaikimisi: Sys.getenv("db_dir")].
-r --validate-ratio= Kontrolli proovi suhet [vaikimisi: 0.995].
-n --n-gpu= GPU-de arv [vaikimisi: 1].
'
args <- docopt::docopt(doc)Pakett docopt esindab rakendust R-ile. Selle abil käivitatakse skripte lihtsate käskudega, nagu Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db või ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, kui fail train_nn.R on teostatav (see käsk alustab resnet50 mudeli koolitamist värvilistes piltides, mille suurus on 128x128 pikslit; andmebaas peab olema kaustas /home/andrey/doodle_db). Loendisse saab lisada õppimise kiirus, optimeerija tüüp ja kõik muud kohandatavad parameetrid. Valmistamise protsessis selgus, et arhitektuuri mobilenet_v2 aktuaalses versioonis keras R-is kasutada kuna R-paketi muudatusi ei ole arvestatud — ootame, kuni nad parandavad.
See lähenemine võimaldas oluliselt kiirendada katsetusi erinevate mudelitega võrreldes traditsioonilisemate skriptide käivitamisega RStudios (võimaliku alternatiivina märkime paketi ). Kuid peamine eelis seisneb lihtsas skriptide käivitamise haldamises Dockeris või lihtsalt serveris, ilma et oleks vaja RStudio'd installida.
6. Skriptide dockeriseerimine
Kasutasime Dockerit, et tagada mudelite õppe keskkonna ühilduvus meeskonna liikmete vahel ning kiirete paiknemiste tegemise jaoks pilves. Selle R-programmeerija jaoks suhteliselt ebatavalise tööriista tundmaõppimist saab alustada artiklite sarjast või .
Docker võimaldab luua nii täiesti uusi pilte kui ka kasutada teisi pilte aluseks oma piltide loomiseks. Olemasolevate variantide analüüsimisel jõudsime järeldusele, et NVIDIA juhtmete, CUDA+cuDNN ja Python'i teekide installimine on üsna mahukas osa pildist, ning otsustasime võtta aluseks ametliku pildi tensorflow/tensorflow:1.12.0-gpu, lisades sinna vajalikud R-paketid.
Lõplik Dockerfile näeb välja selline:
Dockerfile
FROM tensorflow/tensorflow:1.12.0-gpu
MAINTAINER Artem Klevtsov
SHELL ["/bin/bash", "-c"]
ARG LOCALE="et_EE.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
Mugavuse huvides on kasutatavad paketid viidud muutujatesse; peamine osa kirjutatud skripte kopeeritakse konteinerite sisse kogumise ajal. Samuti oleme muutnud käskluste kestaks /bin/bash sisu mugavamaks kasutamiseks /etc/os-release. See võimaldas vältida vajadust näidata OS-i versiooni koodis.
Lisaks on kirjutatud väike bash-skript, mis võimaldab konteinerit käivitada erinevate käsklustega. Näiteks võivad need olla skriptid, mis on seotud närvivõrkude õppimisega, mis on varem konteineri sisse pandud, või käskluskest debugimiseks ja konteineri töö jälgimiseks:
Konteineri käivitamise skript
#!/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}Kui seda bash-skripti käivitada ilma parameetrita, kutsutakse konteineris välja skript train_nn.R vaikeväärtustega; kui esimene positsiooniline argument on «bash», käivitub konteiner interaktiivses režiimis käskluskesega. Kõigil muudel juhtudel toimub positsiooniliste argumentide väärtuste asendamine: CMD="Rscript /app/train_nn.R $@".
Oluline on märkida, et allikafaile ja andmebaasi sisaldavad kataloogid, samuti treenitud mudelite salvestamiseks mõeldud kataloog mountitakse konteinerisse host-süsteemist, võimaldades pääseda skriptide tulemuste juurde ilma täiendavate toiminguteta.
7. Mitme GPU kasutamine Google Cloudis
Üks võistluse eripära oli väga mürarikkad andmed (vt pealkujutist, mis on saadud @Leigh.plt ODS Slackist). Suurte partiidena töötamine aitab sellega toime tulla ja pärast katsetusi 1 GPU-ga arvutis otsustasime õppida mudelite treenimist mitme GPU-ga pilves. Kasutasime GoogleCloudi () suure konfiguratsioonide valiku, mõistlike hindade ja $300 boonuse tõttu. Ahne olek käskis tellida 4xV100 SSD-ga instantsi koos suure hulga RAM-iga, ja see oli suur viga. Selline masin kulutab kiiresti raha, eksperimentide käigus ilma välja töötatud torustikuta võib kiiresti bankrotti minna. Õppe eesmärkidega tasub pigem valida K80. Suur RAM-i maht osutus siiski kasulikuks — pilve SSD ei olnud oma kiiruselt eriline, seetõttu kandsime andmebaasi iga instantsi käivitamise korral üle dev/shm.
Kõige huvitavam on koodilõik, mis vastutab mitu GPU kasutamise eest. Esiteks luuakse mudel CPU-l, kasutades konteksti haldurit, just nagu Pythoni puhul:
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
)
})Seejärel kopeeritakse kompilatsioonita (see on oluline) mudel määratud arvu saadaval olevaid GPU-sid, ja alles seejärel kompileeritakse:
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)
)Klassikalist lähenemist, kus külmutatakse kõik kihid peale viimase, õpetatakse viimane kiht, seejärel vabastatakse ja täiustatakse mudelit täielikult mitme GPU jaoks, ei õnnestunud ellu viia.
Koolitust jälgiti ilma kasutamata tensorboard, piirdudes logide salvestamise ja mudelite salvestamisega informatiivsete nimedega pärast iga epohhi:
Callback'id
# Шаблон имени файла лога
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. Kokkuvõtte asemel
Oleme kokku puutunud mitmete probleemidega, millega praegu toime ei tulnud:
- ühes keras puudub valmis funktsioon optimalse õppe kiirus (kogus,
lr_finderfast.ai raamatukogus )); tehes olles on võimalik R-le portida kolmandate osapoolte teostusi, näiteks, ; - nagu eelneva punkti tõttu ei õnnestunud leida õiget õpikoguse kiirus mitme GPU kasutamisel;
- puuduvad kaasaegsed närvivõrgustruktuurid, eriti need, mis on ette koolitatud imagenet-l;
- puudub one cycle policy ja diskrimineerivad õppimispeed (cosine annealing meie palve järgi oli , aitäh ).
Mis kasulikku saadi sellest võistlusest:
- Suhteliselt väikese jõudlusega riistvaraga on võimalik valutult töötada mõistlikult (korduvalt suuremate) andmehulkadega. Pakett data.table säästab mälu in-place tabelite muutmise kaudu, vältides nende kopeerimist, ja õigesti kasutades demonstreerib see enamasti kõige kiiremat kiirust kõigi tuntud tööriistade seas skriptikeelte jaoks. Andmete salvestamine andmebaasi võimaldab paljusid kordi üldse mitte mõelda vajadusele suruda kogu andmestik mälu.
- Aeglased funktsioonid R-is saab asendada kiirete C++ funktsioonidega paketi Rcpp. Kui lisaks kasutada RcppThread või RcppParallel, saame platvormideüleseid mitme eri rakenduse versioone, seega R-tasandi koodi paralleelne töötlemine ei ole vajalik.
- Paketti Rcpp võib kasutada ilma tõsiste teadmistega C++-st, vajalik minimaalne teave on esitatud . Pealkirjafailid mitmete tipptasemel C keele teekide jaoks, näiteks xtensor on saadaval CRAN-is, seega luuakse infrastruktuur projektide teostamiseks, mis integreerivad R-i valmis kõrge jõudlusega C++ koodi. Lisamugavus — C++ süntaksivalgustus ja staatiline koodianalüsaator RStudios.
- docopt võimaldab käivitada iseseisvaid skripte parameetritega. See on mugav kasutamiseks eemalserveris, sealhulgas Dockeris. RStudios pole pikka aega kestvate närvivõrgu koolituskatsed mugavad, samuti ei ole IDE installimine serveris alati põhjendatud.
- Docker tagab koodi kaasaskantavuse ja tulemuste reproduktsiooni arendajate vahel, kellel on erinevad operatsioonisüsteemide ja teekide versioonid, samuti muudab serverites käivitamise lihtsaks. Kogu koolituse torujuhtme saab käivitada ainult ühe käsu abil.
- Google Cloud on eelarvesõbralik viis katsetamiseks kallil riistvaral, kuid konfiguratsioonide valimisel tuleb hoolikalt kaaluda.
- Koodi fragmentide töötamise kiirusest mõõdmine on väga kasulik, eriti R-i ja C++ kombinatsioonis, ja koos paketiga bench — on samuti väga lihtne.
Kokkuvõttes oli see kogemus väga väärtuslik ning me jätkame mõningate mainitud probleemide lahendamisega.
Allikas: habr.com
