ProHoster > Quick Draw Doodle Recognition: kaip susidraugauti su R, C++ ir neuroniniais tinklais
Quick Draw Doodle Recognition: kaip susidraugauti su R, C++ ir neuroniniais tinklais
Sveiki, Habr!
Praėjusį rudenį Kaggle surengė ranka pieštų paveikslėlių klasifikavimo konkursą „Quick Draw Doodle Recognition“, kuriame, be kitų, dalyvavo ir R mokslininkų komanda: Artemas Klevcova, Philippa vadovas и Andrejus Ogurcovas. Detaliau konkurso neaprašysime, tai jau buvo padaryta naujausia publikacija.
Šį kartą su medaliu ūkininkauti nepavyko, tačiau buvo sukaupta daug vertingos patirties, todėl noriu papasakoti bendruomenei apie įdomiausius ir naudingiausius dalykus Kagle ir kasdieniame darbe. Tarp aptartų temų: sunkus gyvenimas be OpenCV, JSON analizė (šiuose pavyzdžiuose nagrinėjamas C++ kodo integravimas į scenarijus arba paketus R programoje, naudojant Rcpp), scenarijų parametrizavimas ir galutinio sprendimo dokerizavimas. Visas pranešimo kodas tinkama vykdyti forma yra prieinamas saugyklos.
1. Efektyviai įkelkite duomenis iš CSV į MonetDB duomenų bazę
Duomenys šiame konkurse pateikiami ne paruoštų vaizdų, o 340 CSV failų (po vieną failą kiekvienai klasei) pavidalu, kuriuose yra JSON su taškų koordinatėmis. Šiuos taškus sujungę linijomis, gauname galutinį 256x256 pikselių dydžio vaizdą. Taip pat prie kiekvieno įrašo yra etiketė, nurodanti, ar paveikslėlį teisingai atpažino klasifikatorius, naudotas renkant duomenų rinkinį, dviejų raidžių nuotraukos autoriaus gyvenamosios šalies kodas, unikalus identifikatorius, laiko žyma. ir klasės pavadinimą, atitinkantį failo pavadinimą. Supaprastinta originalių duomenų versija archyve sveria 7.4 GB, o išpakavus – maždaug 20 GB, visi duomenys po išpakavimo užima 240 GB. Organizatoriai užtikrino, kad abiejose versijose būtų atkartoti tie patys brėžiniai, o tai reiškia, kad visa versija buvo perteklinė. Bet kokiu atveju 50 milijonų vaizdų saugojimas grafiniuose failuose arba masyvų pavidalu buvo nedelsiant laikomas nepelningu, todėl nusprendėme sujungti visus CSV failus iš archyvo. traukinys_supaprastintas.zip į duomenų bazę, vėliau kiekvienai partijai generuojant reikiamo dydžio vaizdus.
DBVS buvo pasirinkta gerai patikrinta sistema MonetDB, būtent R kaip paketo įgyvendinimas MonetDBLite. Į paketą įtraukta įterptoji duomenų bazės serverio versija ir leidžia pasiimti serverį tiesiai iš R seanso ir dirbti su juo. Duomenų bazės sukūrimas ir prisijungimas prie jos atliekami viena komanda:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
Turėsime sukurti dvi lenteles: vieną – visiems duomenims, kitą – paslaugų informacijai apie atsisiųstus failus (naudinga, jei kas nors nepavyksta ir procesas turi būti atnaujintas atsisiuntus kelis failus):
Greičiausias būdas įkelti duomenis į duomenų bazę buvo tiesiogiai nukopijuoti CSV failus naudojant SQL komandą COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTKur tablename - lentelės pavadinimas ir path - kelias į failą. Dirbant su archyvu buvo nustatyta, kad įmontuotas įgyvendinimas unzip R neveikia tinkamai su daugybe failų iš archyvo, todėl naudojome sistemą unzip (naudojant parametrą getOption("unzip")).
Funkcija rašyti į duomenų 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))
}
Jei lentelę reikia transformuoti prieš rašant ją į duomenų bazę, pakanka pateikti argumentą preprocess funkcija, kuri pakeis duomenis.
Kodas, skirtas nuosekliai įkelti duomenis į duomenų bazę:
Duomenų įrašymas į duomenų 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->SSD
Duomenų įkėlimo laikas gali skirtis priklausomai nuo naudojamo disko greičio charakteristikų. Mūsų atveju skaitymas ir rašymas viename SSD arba iš „flash drive“ (šaltinio failo) į SSD (DB) trunka mažiau nei 10 minučių.
Stulpeliui su sveikųjų skaičių klasės etikete ir indekso stulpeliu sukurti prireikia dar kelių sekundžių (ORDERED INDEX) su eilučių numeriais, pagal kuriuos bus imami stebėjimai kuriant paketus:
Papildomų stulpelių ir indekso kūrimas
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)"))
Kad išspręstume partijos sukūrimo problemą, turėjome pasiekti maksimalų atsitiktinių eilučių iš lentelės greitį. doodles. Tam panaudojome 3 triukus. Pirmasis buvo sumažinti tipo, kuriame saugomas stebėjimo ID, matmenis. Pradiniame duomenų rinkinyje ID saugojimui reikalingas tipas yra bigint, tačiau stebėjimų skaičius leidžia sutalpinti jų identifikatorius, lygius eilės skaičiui, į tipą int. Šiuo atveju paieška vyksta daug greičiau. Antrasis triukas buvo naudoti ORDERED INDEX — tokį sprendimą priėjome empiriškai, išnagrinėję visus turimus dalykus parinktys. Trečia buvo naudoti parametrizuotas užklausas. Metodo esmė – komandą vykdyti vieną kartą PREPARE vėliau naudojant paruoštą išraišką kuriant krūvą to paties tipo užklausų, tačiau iš tikrųjų yra pranašumas, palyginti su paprasta SELECT pasirodė esanti statistinės paklaidos ribose.
Duomenų įkėlimo procesas sunaudoja ne daugiau kaip 450 MB RAM. Tai yra, aprašytas metodas leidžia perkelti dešimtis gigabaitų sveriančius duomenų rinkinius beveik bet kurioje biudžetinėje aparatinėje įrangoje, įskaitant kai kuriuos vienos plokštės įrenginius, o tai yra gana puiku.
Belieka išmatuoti (atsitiktinių) duomenų gavimo greitį ir įvertinti mastelį imant įvairaus dydžio partijas:
Duomenų bazės etalonas
library(ggplot2)
set.seed(0)
# Подключение к базе данных
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
# Функция для подготовки запроса на стороне сервера
prep_sql <- function(batch_size) {
sql <- sprintf("PREPARE SELECT id FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ","))
res <- DBI::dbSendQuery(con, sql)
return(res)
}
# Функция для извлечения данных
fetch_data <- function(rs, batch_size) {
ids <- sample(seq_len(n), batch_size)
res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
return(res)
}
# Проведение замера
res_bench <- bench::press(
batch_size = 2^(4:10),
{
rs <- prep_sql(batch_size)
bench::mark(
fetch_data(rs, batch_size),
min_iterations = 50L
)
}
)
# Параметры бенчмарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]
# batch_size min median max `itr/sec` total_time n_itr
# <dbl> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 16 23.6ms 54.02ms 93.43ms 18.8 2.6s 49
# 2 32 38ms 84.83ms 151.55ms 11.4 4.29s 49
# 3 64 63.3ms 175.54ms 248.94ms 5.85 8.54s 50
# 4 128 83.2ms 341.52ms 496.24ms 3.00 16.69s 50
# 5 256 232.8ms 653.21ms 847.44ms 1.58 31.66s 50
# 6 512 784.6ms 1.41s 1.98s 0.740 1.1m 49
# 7 1024 681.7ms 2.72s 4.06s 0.377 2.16m 49
ggplot(res_bench, aes(x = factor(batch_size), y = median, group = 1)) +
geom_point() +
geom_line() +
ylab("median time, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE)
2. Partijų ruošimas
Visas partijos paruošimo procesas susideda iš šių etapų:
Kelių JSON, turinčių eilučių vektorius su taškų koordinatėmis, analizė.
Spalvotų linijų brėžimas pagal taškų koordinates reikiamo dydžio paveikslėlyje (pvz., 256×256 arba 128×128).
Gautų vaizdų pavertimas tenzoriumi.
Vykstant konkurencijai tarp Python branduolių, problema buvo išspręsta pirmiausia naudojant OpenCV. Vienas iš paprasčiausių ir akivaizdžiausių R analogų atrodytų taip:
JSON konvertavimo į Tensor diegimas R
r_process_json_str <- function(json, line.width = 3,
color = TRUE, scale = 1) {
# Парсинг JSON
coords <- jsonlite::fromJSON(json, simplifyMatrix = FALSE)
tmp <- tempfile()
# Удаляем временный файл по завершению функции
on.exit(unlink(tmp))
png(filename = tmp, width = 256 * scale, height = 256 * scale, pointsize = 1)
# Пустой график
plot.new()
# Размер окна графика
plot.window(xlim = c(256 * scale, 0), ylim = c(256 * scale, 0))
# Цвета линий
cols <- if (color) rainbow(length(coords)) else "#000000"
for (i in seq_along(coords)) {
lines(x = coords[[i]][[1]] * scale, y = coords[[i]][[2]] * scale,
col = cols[i], lwd = line.width)
}
dev.off()
# Преобразование изображения в 3-х мерный массив
res <- png::readPNG(tmp)
return(res)
}
r_process_json_vector <- function(x, ...) {
res <- lapply(x, r_process_json_str, ...)
# Объединение 3-х мерных массивов картинок в 4-х мерный в тензор
res <- do.call(abind::abind, c(res, along = 0))
return(res)
}
Piešimas atliekamas naudojant standartinius R įrankius ir išsaugomas į laikiną PNG, saugomą RAM („Linux“ laikinieji R katalogai yra kataloge /tmp, sumontuotas RAM). Tada šis failas nuskaitomas kaip trimatis masyvas su skaičiais nuo 0 iki 1. Tai svarbu, nes įprastesnis BMP būtų nuskaitomas į neapdorotą masyvą su šešioliktainiais spalvų kodais.
Šis įgyvendinimas mums atrodė neoptimalus, nes didelių partijų formavimas užtrunka nepadoriai ilgai, todėl nusprendėme pasinaudoti kolegų patirtimi, pasitelkę galingą biblioteką. OpenCV. Tuo metu nebuvo paruošto R paketo (dabar jo nėra), todėl C++ buvo parašytas minimalus reikiamų funkcijų įgyvendinimas su integravimu į R kodą naudojant Rcpp.
Norėdami išspręsti problemą, buvo naudojami šie paketai ir bibliotekos:
OpenCV darbui su vaizdais ir linijų piešimui. Naudojamos iš anksto įdiegtos sistemos bibliotekos ir antraščių failai, taip pat dinaminis susiejimas.
xtensor darbui su daugiamačiais masyvais ir tenzoriais. Naudojome antraštės failus, įtrauktus į to paties pavadinimo R paketą. Biblioteka leidžia dirbti su daugiamačiais masyvais, tiek pagrindinės eilutės, tiek stulpelio pagrindinės eilės tvarka.
ndjson JSON analizei. Ši biblioteka naudojama xtensor automatiškai, jei jis yra projekte.
RcppThread organizuojant kelių gijų vektoriaus apdorojimą iš JSON. Naudojo šio paketo pateiktus antraščių failus. Iš populiaresnių RcppParallel Paketas, be kita ko, turi įmontuotą kilpos pertraukimo mechanizmą.
Reikėtų pažymėti, kad xtensor pasirodė esanti nelaimė: be to, kad jis turi platų funkcionalumą ir didelį našumą, jo kūrėjai pasirodė gana jautrūs ir greitai bei išsamiai atsakė į klausimus. Jų pagalba buvo galima įgyvendinti OpenCV matricų transformacijas į xtensor tenzorius, taip pat būdą sujungti 3 dimensijų vaizdo tenzorius į tinkamo matmens 4 dimensijos tenzorių (pačią partiją).
Norėdami sudaryti failus, kuriuose naudojami sistemos failai ir dinaminis susiejimas su sistemoje įdiegtomis bibliotekomis, naudojome pakete įdiegtą papildinio mechanizmą Rcpp. Norėdami automatiškai rasti kelius ir vėliavėles, naudojome populiarią „Linux“ programą pkg-config.
Rcpp įskiepio, skirto naudoti OpenCV biblioteką, įdiegimas
Diegimo kodas, skirtas JSON analizei ir paketo generavimui, skirtas perdavimui į modelį, pateiktas po spoileriu. Pirmiausia pridėkite vietinį projekto katalogą, kad galėtumėte ieškoti antraštės failų (reikia „ndjson“):
// [[Rcpp::plugins(cpp14)]]
// [[Rcpp::plugins(opencv)]]
// [[Rcpp::depends(xtensor)]]
// [[Rcpp::depends(RcppThread)]]
#include <xtensor/xjson.hpp>
#include <xtensor/xadapt.hpp>
#include <xtensor/xview.hpp>
#include <xtensor-r/rtensor.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <Rcpp.h>
#include <RcppThread.h>
// Синонимы для типов
using RcppThread::parallelFor;
using json = nlohmann::json;
using points = xt::xtensor<double,2>; // Извлечённые из JSON координаты точек
using strokes = std::vector<points>; // Извлечённые из JSON координаты точек
using xtensor3d = xt::xtensor<double, 3>; // Тензор для хранения матрицы изоображения
using xtensor4d = xt::xtensor<double, 4>; // Тензор для хранения множества изображений
using rtensor3d = xt::rtensor<double, 3>; // Обёртка для экспорта в R
using rtensor4d = xt::rtensor<double, 4>; // Обёртка для экспорта в R
// Статические константы
// Размер изображения в пикселях
const static int SIZE = 256;
// Тип линии
// См. https://en.wikipedia.org/wiki/Pixel_connectivity#2-dimensional
const static int LINE_TYPE = cv::LINE_4;
// Толщина линии в пикселях
const static int LINE_WIDTH = 3;
// Алгоритм ресайза
// https://docs.opencv.org/3.1.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121
const static int RESIZE_TYPE = cv::INTER_LINEAR;
// Шаблон для конвертирования OpenCV-матрицы в тензор
template <typename T, int NCH, typename XT=xt::xtensor<T,3,xt::layout_type::column_major>>
XT to_xt(const cv::Mat_<cv::Vec<T, NCH>>& src) {
// Размерность целевого тензора
std::vector<int> shape = {src.rows, src.cols, NCH};
// Общее количество элементов в массиве
size_t size = src.total() * NCH;
// Преобразование cv::Mat в xt::xtensor
XT res = xt::adapt((T*) src.data, size, xt::no_ownership(), shape);
return res;
}
// Преобразование JSON в список координат точек
strokes parse_json(const std::string& x) {
auto j = json::parse(x);
// Результат парсинга должен быть массивом
if (!j.is_array()) {
throw std::runtime_error("'x' must be JSON array.");
}
strokes res;
res.reserve(j.size());
for (const auto& a: j) {
// Каждый элемент массива должен быть 2-мерным массивом
if (!a.is_array() || a.size() != 2) {
throw std::runtime_error("'x' must include only 2d arrays.");
}
// Извлечение вектора точек
auto p = a.get<points>();
res.push_back(p);
}
return res;
}
// Отрисовка линий
// Цвета HSV
cv::Mat ocv_draw_lines(const strokes& x, bool color = true) {
// Исходный тип матрицы
auto stype = color ? CV_8UC3 : CV_8UC1;
// Итоговый тип матрицы
auto dtype = color ? CV_32FC3 : CV_32FC1;
auto bg = color ? cv::Scalar(0, 0, 255) : cv::Scalar(255);
auto col = color ? cv::Scalar(0, 255, 220) : cv::Scalar(0);
cv::Mat img = cv::Mat(SIZE, SIZE, stype, bg);
// Количество линий
size_t n = x.size();
for (const auto& s: x) {
// Количество точек в линии
size_t n_points = s.shape()[1];
for (size_t i = 0; i < n_points - 1; ++i) {
// Точка начала штриха
cv::Point from(s(0, i), s(1, i));
// Точка окончания штриха
cv::Point to(s(0, i + 1), s(1, i + 1));
// Отрисовка линии
cv::line(img, from, to, col, LINE_WIDTH, LINE_TYPE);
}
if (color) {
// Меняем цвет линии
col[0] += 180 / n;
}
}
if (color) {
// Меняем цветовое представление на RGB
cv::cvtColor(img, img, cv::COLOR_HSV2RGB);
}
// Меняем формат представления на float32 с диапазоном [0, 1]
img.convertTo(img, dtype, 1 / 255.0);
return img;
}
// Обработка JSON и получение тензора с данными изображения
xtensor3d process(const std::string& x, double scale = 1.0, bool color = true) {
auto p = parse_json(x);
auto img = ocv_draw_lines(p, color);
if (scale != 1) {
cv::Mat out;
cv::resize(img, out, cv::Size(), scale, scale, RESIZE_TYPE);
cv::swap(img, out);
out.release();
}
xtensor3d arr = color ? to_xt<double,3>(img) : to_xt<double,1>(img);
return arr;
}
// [[Rcpp::export]]
rtensor3d cpp_process_json_str(const std::string& x,
double scale = 1.0,
bool color = true) {
xtensor3d res = process(x, scale, color);
return res;
}
// [[Rcpp::export]]
rtensor4d cpp_process_json_vector(const std::vector<std::string>& x,
double scale = 1.0,
bool color = false) {
size_t n = x.size();
size_t dim = floor(SIZE * scale);
size_t channels = color ? 3 : 1;
xtensor4d res({n, dim, dim, channels});
parallelFor(0, n, [&x, &res, scale, color](int i) {
xtensor3d tmp = process(x[i], scale, color);
auto view = xt::view(res, i, xt::all(), xt::all(), xt::all());
view = tmp;
});
return res;
}
Šis kodas turi būti įdėtas į failą src/cv_xt.cpp ir sukompiliuokite su komanda Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); taip pat reikalingas darbui nlohmann/json.hpp iš saugykla. Kodas yra padalintas į keletą funkcijų:
to_xt — šabloninė funkcija vaizdo matricai transformuoti (cv::Mat) į tenzorių xt::xtensor;
parse_json — funkcija analizuoja JSON eilutę, išskiria taškų koordinates, supakuoja jas į vektorių;
ocv_draw_lines — iš gauto taškų vektoriaus nubrėžia įvairiaspalves linijas;
process — sujungia aukščiau nurodytas funkcijas ir taip pat prideda galimybę keisti gauto vaizdo mastelį;
cpp_process_json_str - apvyniokite funkciją process, kuris eksportuoja rezultatą į R objektą (daugiamatis masyvas);
cpp_process_json_vector - apvyniokite funkciją cpp_process_json_str, kuri leidžia apdoroti eilutės vektorių kelių gijų režimu.
Daugiaspalvėms linijoms piešti buvo naudojamas HSV spalvų modelis, po kurio buvo konvertuojamas į RGB. Išbandykime rezultatą:
Kaip matote, greičio padidėjimas pasirodė labai reikšmingas, o lygiagrečiuojant R kodą C++ kodo pasivyti neįmanoma.
3. Iteratoriai partijų iškrovimui iš duomenų bazės
R turi pelnytą reputaciją apdoroti duomenis, kurie telpa į RAM, o Python labiau būdingas pasikartojantis duomenų apdorojimas, leidžiantis lengvai ir natūraliai įgyvendinti ne pagrindinius skaičiavimus (skaičiavimus naudojant išorinę atmintį). Klasikinis ir mums aktualus pavyzdys aprašytos problemos kontekste yra gilieji neuroniniai tinklai, treniruojami gradiento nusileidimo metodu su gradiento aproksimavimu kiekviename žingsnyje naudojant nedidelę stebėjimų dalį arba mini partiją.
„Python“ parašytose giluminio mokymosi sistemose yra specialios klasės, kurios įdiegia iteratorius pagal duomenis: lenteles, paveikslėlius aplankuose, dvejetainius formatus ir kt. Galite naudoti paruoštas parinktis arba parašyti savo konkrečias užduotis. R programoje galime pasinaudoti visomis Python bibliotekos funkcijomis sunku su įvairiomis galinėmis programomis, naudojant to paties pavadinimo paketą, kuris savo ruožtu veikia paketo viršuje tinklinis. Pastarasis nusipelno atskiro ilgo straipsnio; tai ne tik leidžia paleisti Python kodą iš R, bet ir perkelti objektus tarp R ir Python seansų, automatiškai atliekant visas reikalingas tipo konvertacijas.
Atsikratėme poreikio saugoti visus duomenis operatyviojoje atmintyje naudodami MonetDBLite, visas „neuronų tinklo“ darbas bus atliktas originaliu Python kodu, tereikia ant duomenų parašyti iteratorių, nes nėra nieko paruošto tokiai situacijai R arba Python. Jam iš esmės keliami tik du reikalavimai: jis turi grąžinti partijas begaliniu ciklu ir išsaugoti savo būseną tarp iteracijų (pastarasis R yra įgyvendinamas paprasčiausiu būdu naudojant uždarymus). Anksčiau buvo reikalaujama aiškiai konvertuoti R masyvus į numpy masyvus iteratoriaus viduje, tačiau dabartinė paketo versija sunku daro pati.
Mokymo ir patvirtinimo duomenų iteratorius pasirodė toks:
Iteratorius, skirtas mokymo ir patvirtinimo duomenims
train_generator <- function(db_connection = con,
samples_index,
num_classes = 340,
batch_size = 32,
scale = 1,
color = FALSE,
imagenet_preproc = FALSE) {
# Проверка аргументов
checkmate::assert_class(con, "DBIConnection")
checkmate::assert_integerish(samples_index)
checkmate::assert_count(num_classes)
checkmate::assert_count(batch_size)
checkmate::assert_number(scale, lower = 0.001, upper = 5)
checkmate::assert_flag(color)
checkmate::assert_flag(imagenet_preproc)
# Перемешиваем, чтобы брать и удалять использованные индексы батчей по порядку
dt <- data.table::data.table(id = sample(samples_index))
# Проставляем номера батчей
dt[, batch := (.I - 1L) %/% batch_size + 1L]
# Оставляем только полные батчи и индексируем
dt <- dt[, if (.N == batch_size) .SD, keyby = batch]
# Устанавливаем счётчик
i <- 1
# Количество батчей
max_i <- dt[, max(batch)]
# Подготовка выражения для выгрузки
sql <- sprintf(
"PREPARE SELECT drawing, label_int FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ",")
)
res <- DBI::dbSendQuery(con, sql)
# Аналог keras::to_categorical
to_categorical <- function(x, num) {
n <- length(x)
m <- numeric(n * num)
m[x * n + seq_len(n)] <- 1
dim(m) <- c(n, num)
return(m)
}
# Замыкание
function() {
# Начинаем новую эпоху
if (i > max_i) {
dt[, id := sample(id)]
data.table::setkey(dt, batch)
# Сбрасываем счётчик
i <<- 1
max_i <<- dt[, max(batch)]
}
# ID для выгрузки данных
batch_ind <- dt[batch == i, id]
# Выгрузка данных
batch <- DBI::dbFetch(DBI::dbBind(res, as.list(batch_ind)), n = -1)
# Увеличиваем счётчик
i <<- i + 1
# Парсинг JSON и подготовка массива
batch_x <- cpp_process_json_vector(batch$drawing, scale = scale, color = color)
if (imagenet_preproc) {
# Шкалирование c интервала [0, 1] на интервал [-1, 1]
batch_x <- (batch_x - 0.5) * 2
}
batch_y <- to_categorical(batch$label_int, num_classes)
result <- list(batch_x, batch_y)
return(result)
}
}
Funkcija kaip įvestį paima kintamąjį su ryšiu su duomenų baze, naudojamų eilučių skaičių, klasių skaičių, partijos dydį, mastelį (scale = 1 atitinka 256 x 256 pikselių vaizdų atvaizdavimą, scale = 0.5 - 128x128 pikseliai), spalvų indikatorius (color = FALSE nurodomas pilkos spalvos atvaizdavimas, kai naudojamas color = TRUE kiekvienas potėpis nupieštas nauja spalva) ir išankstinio apdorojimo indikatorius tinklams, iš anksto apmokytiems „imagenet“. Pastarasis reikalingas pikselių reikšmių masteliui nuo intervalo [0, 1] iki intervalo [-1, 1], kuris buvo naudojamas treniruojant pateiktą sunku modeliai.
Išorinėje funkcijoje yra argumentų tipo tikrinimas, lentelė data.table su atsitiktinai sumaišytais eilučių numeriais iš samples_index ir partijų numeriai, skaitiklis ir maksimalus partijų skaičius, taip pat SQL išraiška duomenims iš duomenų bazės iškrauti. Be to, apibrėžėme greitą funkcijos analogą viduje keras::to_categorical(). Treniruotėms panaudojome beveik visus duomenis, patvirtinimui palikome pusę procento, todėl epochos dydį ribojo parametras steps_per_epoch kai skambina keras::fit_generator(), ir sąlyga if (i > max_i) veikė tik patvirtinimo iteratoriuje.
Vidinėje funkcijoje nuskaitomi eilučių indeksai kitai paketai, įrašai iškraunami iš duomenų bazės didinant partijos skaitiklį, JSON analizavimas (funkcija cpp_process_json_vector(), parašytas C++) ir sukurti paveikslėlius atitinkančius masyvus. Tada sukuriami vienkartiniai vektoriai su klasių etiketėmis, masyvai su pikselių reikšmėmis ir etiketėmis sujungiami į sąrašą, kuris yra grąžinama reikšmė. Norėdami pagreitinti darbą, mes panaudojome indeksų kūrimą lentelėse data.table ir modifikavimas per nuorodą - be šių paketo „lustų“ duomenys. lentelė Gana sunku įsivaizduoti efektyvų darbą su dideliu duomenų kiekiu R.
„Core i5“ nešiojamojo kompiuterio greičio matavimų rezultatai yra tokie:
Jei turite pakankamai RAM, galite rimtai pagreitinti duomenų bazės veikimą, perkeldami ją į tą pačią RAM (mūsų užduočiai pakanka 32 GB). Linux sistemoje skaidinys yra prijungtas pagal numatytuosius nustatymus /dev/shm, užimantis iki pusės RAM talpos. Redaguodami galite paryškinti daugiau /etc/fstabgauti tokį įrašą kaip tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Būtinai paleiskite iš naujo ir patikrinkite rezultatą paleisdami komandą df -h.
Bandymų duomenų iteratorius atrodo daug paprastesnis, nes bandymo duomenų rinkinys visiškai telpa į RAM:
Bandymo duomenų iteratorius
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. Modelio architektūros parinkimas
Pirmoji panaudota architektūra buvo Mobilenet v1, kurios ypatybės aptariamos tai žinutę. Jis įtrauktas kaip standartinis sunku ir, atitinkamai, yra to paties pavadinimo pakuotėje, skirta R. Tačiau bandant jį naudoti su vieno kanalo vaizdais, pasirodė keistas dalykas: įvesties tenzorius visada turi turėti matmenis (batch, height, width, 3)ty kanalų skaičiaus keisti negalima. Python nėra tokio apribojimo, todėl suskubome ir parašėme savo šios architektūros įgyvendinimą, vadovaudamiesi originaliu straipsniu (be iškritimo, kuris yra keras versijoje):
Šio metodo trūkumai yra akivaizdūs. Noriu išbandyti daugybę modelių, bet, priešingai, nenoriu perrašyti kiekvienos architektūros rankiniu būdu. Mums taip pat buvo atimta galimybė naudoti modelių, iš anksto apmokytų „imagenet“ svoriais. Kaip įprasta, dokumentų studijavimas padėjo. Funkcija get_config() leidžia gauti modelio aprašymą redaguoti tinkama forma (base_model_conf$layers - įprastas R sąrašas) ir funkcija from_config() atlieka atvirkštinį konvertavimą į modelio objektą:
Dabar nėra sunku parašyti universalią funkciją, kad gautumėte bet kurią iš pateiktų sunku modeliai su svoriais arba be jų, treniruojami „imagenet“ tinkle:
Paruoštų architektūrų įkėlimo funkcija
get_model <- function(name = "mobilenet_v2",
input_shape = NULL,
weights = "imagenet",
pooling = "avg",
num_classes = NULL,
optimizer = keras::optimizer_adam(lr = 0.002),
loss = "categorical_crossentropy",
metrics = NULL,
color = TRUE,
compile = FALSE) {
# Проверка аргументов
checkmate::assert_string(name)
checkmate::assert_integerish(input_shape, lower = 1, upper = 256, len = 3)
checkmate::assert_count(num_classes)
checkmate::assert_flag(color)
checkmate::assert_flag(compile)
# Получаем объект из пакета keras
model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
# Проверка наличия объекта в пакете
if (is.null(model_fun)) {
stop("Model ", shQuote(name), " not found.", call. = FALSE)
}
base_model <- model_fun(
input_shape = input_shape,
include_top = FALSE,
weights = weights,
pooling = pooling
)
# Если изображение не цветное, меняем размерность входа
if (!color) {
base_model_conf <- keras::get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- keras::from_config(base_model_conf)
}
predictions <- keras::get_layer(base_model, "global_average_pooling2d_1")$output
predictions <- keras::layer_dense(predictions, units = num_classes, activation = "softmax")
model <- keras::keras_model(
inputs = base_model$input,
outputs = predictions
)
if (compile) {
keras::compile(
object = model,
optimizer = optimizer,
loss = loss,
metrics = metrics
)
}
return(model)
}
Naudojant vieno kanalo vaizdus, nenaudojami jokie iš anksto paruošti svoriai. Tai galima ištaisyti: naudojant funkciją get_weights() gaukite modelio svorius kaip R masyvų sąrašą, pakeiskite pirmojo šio sąrašo elemento matmenį (paimdami vieną spalvų kanalą arba apskaičiuodami visų trijų vidurkį), tada įkelkite svorius atgal į modelį naudodami funkciją set_weights(). Niekada nepridėjome šios funkcijos, nes jau šiame etape buvo aišku, kad produktyviau dirbti su spalvotomis nuotraukomis.
Daugumą eksperimentų atlikome naudodami 1 ir 2 mobiliojo tinklo versijas, taip pat resnet34. Šiame konkurse gerai pasirodė modernesnės architektūros, tokios kaip SE-ResNeXt. Deja, mes neturėjome paruoštų diegimų ir savo neparašėme (bet būtinai parašysime).
5. Scenarijų parametrizavimas
Patogumui visas mokymo pradžios kodas buvo sukurtas kaip vienas scenarijus, parametrizuotas naudojant docpt taip:
doc <- '
Usage:
train_nn.R --help
train_nn.R --list-models
train_nn.R [options]
Options:
-h --help Show this message.
-l --list-models List available models.
-m --model=<model> Neural network model name [default: mobilenet_v2].
-b --batch-size=<size> Batch size [default: 32].
-s --scale-factor=<ratio> Scale factor [default: 0.5].
-c --color Use color lines [default: FALSE].
-d --db-dir=<path> Path to database directory [default: Sys.getenv("db_dir")].
-r --validate-ratio=<ratio> Validate sample ratio [default: 0.995].
-n --n-gpu=<number> Number of GPUs [default: 1].
'
args <- docopt::docopt(doc)
Pakuotė docpt reprezentuoja įgyvendinimą http://docopt.org/ už R. Su jo pagalba paleidžiami scenarijai su paprastomis komandomis, pvz Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db arba ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, jei failas train_nn.R yra vykdomas (ši komanda pradės treniruoti modelį resnet50 ant trijų spalvų 128x128 pikselių vaizdų duomenų bazė turi būti aplanke /home/andrey/doodle_db). Į sąrašą galite įtraukti mokymosi greitį, optimizatoriaus tipą ir bet kokius kitus tinkinamus parametrus. Rengiant leidinį paaiškėjo, kad architektūra mobilenet_v2 nuo dabartinės versijos sunku naudojant R negaliu dėl pakeitimų, į kuriuos neatsižvelgta R pakete, laukiame, kol juos pataisys.
Šis metodas leido žymiai paspartinti eksperimentus su skirtingais modeliais, palyginti su tradiciniu scenarijų paleidimu RStudio (atkreipiame dėmesį į paketą kaip į galimą alternatyvą tfruns). Tačiau pagrindinis privalumas yra galimybė lengvai valdyti scenarijų paleidimą „Docker“ arba tiesiog serveryje, neįdiegus RStudio.
6. Scenarijų dokerizavimas
Naudojome „Docker“, kad užtikrintume aplinkos perkeliamumą mokymo modeliams tarp komandos narių ir greitam diegimui debesyje. Susipažinti su šiuo R programuotojui gana neįprastu įrankiu galite pradėti su tai publikacijų serijos arba vaizdo kursas.
„Docker“ leidžia kurti savo vaizdus nuo nulio ir naudoti kitus vaizdus kaip pagrindą kuriant savo. Analizuodami galimas parinktis padarėme išvadą, kad NVIDIA, CUDA+cuDNN tvarkyklių ir Python bibliotekų įdiegimas yra gana didelė vaizdo dalis, todėl nusprendėme remtis oficialiu vaizdu. tensorflow/tensorflow:1.12.0-gpu, pridedant ten reikiamus R paketus.
Patogumui panaudotos pakuotės buvo suskirstytos į kintamuosius; Didžioji dalis parašytų scenarijų surinkimo metu nukopijuojama į konteinerius. Taip pat pakeitėme komandų apvalkalą į /bin/bash kad būtų patogu naudotis turiniu /etc/os-release. Taip išvengta būtinybės kode nurodyti OS versiją.
Be to, buvo parašytas mažas bash scenarijus, leidžiantis paleisti konteinerį su įvairiomis komandomis. Pavyzdžiui, tai gali būti scenarijai, skirti apmokyti neuroninius tinklus, kurie anksčiau buvo sudėti konteinerio viduje, arba komandų apvalkalas, skirtas derinti ir stebėti konteinerio veikimą:
Scenarijus konteineriui paleisti
#!/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}
Jei šis bash scenarijus vykdomas be parametrų, scenarijus bus iškviestas konteinerio viduje train_nn.R su numatytosiomis reikšmėmis; jei pirmasis pozicijos argumentas yra "bash", konteineris prasidės interaktyviai su komandos apvalkalu. Visais kitais atvejais pozicijos argumentų reikšmės pakeičiamos: CMD="Rscript /app/train_nn.R $@".
Verta paminėti, kad katalogai su šaltinio duomenimis ir duomenų baze, taip pat apmokytų modelių išsaugojimo katalogas yra sumontuoti talpyklos viduje iš pagrindinės sistemos, o tai leidžia pasiekti scenarijų rezultatus be nereikalingų manipuliacijų.
7. Kelių GPU naudojimas „Google Cloud“.
Viena iš konkurso ypatybių buvo labai triukšmingi duomenys (žr. titulinį paveikslėlį, pasiskolintas iš @Leigh.plt iš ODS slack). Didelės partijos padeda su tuo kovoti, ir po eksperimentų su kompiuteriu su 1 GPU nusprendėme įvaldyti mokymo modelius keliuose GPU debesyje. Naudojamas GoogleCloud (geras pagrindų vadovas) dėl didelio galimų konfigūracijų pasirinkimo, priimtinų kainų ir 300 USD premijos. Iš godumo užsisakiau 4xV100 egzempliorių su SSD ir daugybe RAM, ir tai buvo didelė klaida. Toks aparatas greitai suvalgo pinigus; galite žlugti eksperimentuodami be patikrinto vamzdyno. Švietimo tikslais geriau pasiimti K80. Tačiau didelis RAM kiekis pravertė – debesies SSD savo našumu nesužavėjo, todėl duomenų bazė buvo perkelta į dev/shm.
Didžiausią susidomėjimą kelia kodo fragmentas, atsakingas už kelių GPU naudojimą. Pirma, modelis sukuriamas CPU naudojant konteksto tvarkyklę, kaip ir Python:
Klasikinės visų sluoksnių, išskyrus paskutinį, užšaldymo, paskutinio sluoksnio mokymo, viso modelio atšaldymo ir perkvalifikavimo keliems GPU technikos nepavyko įgyvendinti.
Treniruotės buvo stebimos nenaudojant. tenzoro lenta, apsiribodami žurnalų įrašymu ir modelių išsaugojimu informatyviais pavadinimais po kiekvienos epochos:
Kai kurios problemos, su kuriomis susidūrėme, dar nebuvo įveiktos:
в sunku nėra paruoštos funkcijos, leidžiančios automatiškai ieškoti optimalaus mokymosi greičio (analoginis lr_finder bibliotekoje greitai.ai); Su tam tikromis pastangomis galima perkelti trečiųjų šalių diegimus į R, pavyzdžiui, tai;
dėl ankstesnio punkto, naudojant kelis GPU, nebuvo įmanoma pasirinkti tinkamo treniruočių greičio;
trūksta modernių neuroninių tinklų architektūrų, ypač tų, kurios iš anksto apmokytos „imagenet“;
jokios ciklo politikos ir diskriminacinių mokymosi tempų (mūsų prašymu buvo atliktas kosinuso atkaitinimas įgyvendinta, dėkoju skeydan).
Ko naudingo išmokote iš šio konkurso:
Naudodami santykinai mažos galios aparatinę įrangą galite be skausmo dirbti su pakankamais (daug kartų didesniais už RAM) kiekiais duomenų. Plastikinis maišelis duomenys. lentelė taupo atmintį dėl vietoje modifikuotų lentelių, kurios išvengia jų kopijavimo, o teisingai naudojant jos galimybės beveik visada demonstruoja didžiausią greitį tarp visų mums žinomų skriptų kalbų įrankių. Duomenų išsaugojimas duomenų bazėje leidžia daugeliu atvejų visai negalvoti apie būtinybę išspausti visą duomenų rinkinį į RAM.
Lėtas funkcijas R galima pakeisti greitosiomis C++ naudojant paketą Rcpp. Jei be naudojimo RcppThread arba RcppParallel, gauname kelių platformų kelių gijų diegimus, todėl nereikia lygiagretinti kodo R lygiu.
Paketas Rcpp gali būti naudojamas be rimtų C++ žinių, nurodytas reikalingas minimumas čia. Antraštės failai daugeliui puikių C bibliotekų, pvz xtensor prieinama CRAN, tai yra, formuojama infrastruktūra projektams, integruojantiems paruoštą didelio našumo C++ kodą į R, įgyvendinti. Papildomas patogumas yra sintaksės paryškinimas ir statinis C++ kodo analizatorius RStudio.
docpt leidžia paleisti savarankiškus scenarijus su parametrais. Tai patogu naudoti nuotoliniame serveryje, įskaitant. pagal dokerį. RStudio yra nepatogu atlikti daugybę valandų eksperimentų su neuroniniais tinklais, o IDE diegimas pačiame serveryje ne visada pagrįstas.
„Docker“ užtikrina kodo perkeliamumą ir rezultatų atkuriamumą tarp kūrėjų, turinčių skirtingas OS versijas ir bibliotekas, taip pat lengvą vykdymą serveriuose. Galite paleisti visą mokymo vamzdyną tik viena komanda.
„Google Cloud“ yra ekonomiškas būdas eksperimentuoti su brangia technine įranga, tačiau turite atidžiai pasirinkti konfigūracijas.
Atskirų kodo fragmentų greičio matavimas yra labai naudingas, ypač derinant R ir C++ bei su paketu suolas - taip pat labai lengva.
Apskritai ši patirtis buvo labai naudinga, todėl toliau dirbame, kad išspręstume kai kurias iškeltas problemas.