Quick Draw Doodle Recognition: si të bëni miq me R, C++ dhe rrjetet nervore

Quick Draw Doodle Recognition: si të bëni miq me R, C++ dhe rrjetet nervore

Hej Habr!

Vjeshtën e kaluar, Kaggle organizoi një konkurs për klasifikimin e fotografive të vizatuara me dorë, Quick Draw Doodle Recognition, në të cilin, ndër të tjera, mori pjesë një ekip shkencëtarësh R: Artem Klevtsova, Menaxher Philippa и Andrey Ogurtsov. Ne nuk do ta përshkruajmë konkursin në detaje; kjo tashmë është bërë në publikimi i fundit.

Këtë herë nuk funksionoi me kultivimin e medaljeve, por u fitua shumë përvojë e vlefshme, kështu që do të doja t'i tregoja komunitetit për një sërë gjërash më interesante dhe më të dobishme në Kagle dhe në punën e përditshme. Ndër temat e diskutuara: jeta e vështirë pa OpenCV, analiza JSON (këta shembuj shqyrtojnë integrimin e kodit C++ në skriptet ose paketat në R duke përdorur Rcpp), parametrizimi i skripteve dhe dokerizimi i zgjidhjes përfundimtare. I gjithë kodi nga mesazhi në një formë të përshtatshme për ekzekutim është i disponueshëm në depove.

Përmbajtja:

  1. Ngarkoni në mënyrë efikase të dhënat nga CSV në MonetDB
  2. Përgatitja e tufave
  3. Iteratorët për shkarkimin e grupeve nga baza e të dhënave
  4. Zgjedhja e një arkitekture modeli
  5. Parametizimi i skriptit
  6. Dokerizimi i skripteve
  7. Përdorimi i shumë GPU-ve në Google Cloud
  8. Në vend të një përfundimi

1. Ngarkoni në mënyrë efikase të dhënat nga CSV në bazën e të dhënave MonetDB

Të dhënat në këtë konkurs jepen jo në formën e imazheve të gatshme, por në formën e 340 skedarëve CSV (një skedar për secilën klasë) që përmbajnë JSON me koordinata pikash. Duke i lidhur këto pika me vija, marrim një imazh përfundimtar me përmasa 256x256 piksele. Gjithashtu për çdo regjistrim ka një etiketë që tregon nëse fotografia është njohur saktë nga klasifikuesi i përdorur në kohën e grumbullimit të të dhënave, një kod me dy shkronja të vendit të banimit të autorit të figurës, një identifikues unik, një vulë kohore dhe një emër klase që përputhet me emrin e skedarit. Një version i thjeshtuar i të dhënave origjinale peshon 7.4 GB në arkiv dhe afërsisht 20 GB pas shpaketimit, të dhënat e plota pas shpaketimit zënë 240 GB. Organizatorët siguruan që të dy versionet të riprodhonin të njëjtat vizatime, që do të thotë se versioni i plotë ishte i tepërt. Në çdo rast, ruajtja e 50 milion imazheve në skedarë grafikë ose në formën e grupeve u konsiderua menjëherë joprofitabile dhe vendosëm të bashkojmë të gjithë skedarët CSV nga arkivi train_i thjeshtuar.zip në bazën e të dhënave me gjenerimin e mëvonshëm të imazheve të madhësisë së kërkuar "në fluturim" për secilën grumbull.

Një sistem i provuar mirë u zgjodh si DBMS MonetDB, përkatësisht një implementim për R si paketë MonetDBLite. Paketa përfshin një version të integruar të serverit të bazës së të dhënave dhe ju lejon të merrni serverin direkt nga një sesion R dhe të punoni me të atje. Krijimi i një baze të dhënash dhe lidhja me të kryhen me një komandë:

con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))

Do të na duhet të krijojmë dy tabela: njëra për të gjitha të dhënat, tjetra për informacionin e shërbimit në lidhje me skedarët e shkarkuar (e dobishme nëse diçka shkon keq dhe procesi duhet të rifillojë pas shkarkimit 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ë bazën e të dhënave ishte kopjimi i drejtpërdrejtë i skedarëve CSV duke përdorur komandën SQL COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTKu tablename - emri i tabelës dhe path - rruga për në skedar. Gjatë punës me arkivin, u zbulua se zbatimi i integruar unzip në R nuk funksionon siç duhet me një numër skedarësh nga arkivi, kështu që ne përdorëm sistemin unzip (duke përdorur parametrin getOption("unzip")).

Funksioni për të shkruar në bazën e të dhënave

#' @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 duhet të transformoni tabelën përpara se ta shkruani në bazën e të dhënave, mjafton të kaloni në argument preprocess funksioni që do të transformojë të dhënat.

Kodi për ngarkimin sekuencial të të dhënave në bazën e të dhënave:

Shkrimi i të dhënave në bazën e të dhënave

# Список файлов для записи
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

Koha e ngarkimit të të dhënave mund të ndryshojë në varësi të karakteristikave të shpejtësisë së diskut të përdorur. Në rastin tonë, leximi dhe shkrimi brenda një SSD ose nga një flash drive (skedar burim) në një SSD (DB) zgjat më pak se 10 minuta.

Duhen edhe disa sekonda për të krijuar një kolonë me një etiketë të klasës së plotë dhe një kolonë indeksi (ORDERED INDEX) me numrat e rreshtave me të cilët do të merren mostra vëzhgimet kur krijohen grupe:

Krijimi i kolonave dhe indeksit shtesë

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 të zgjidhur problemin e krijimit të një grupi në fluturim, na duhej të arrinim shpejtësinë maksimale të nxjerrjes së rreshtave të rastësishëm nga tabela doodles. Për këtë kemi përdorur 3 truke. E para ishte zvogëlimi i dimensionalitetit të llojit që ruan ID-në e vëzhgimit. Në grupin origjinal të të dhënave, lloji i kërkuar për të ruajtur ID-në është bigint, por numri i vëzhgimeve bën të mundur përshtatjen e identifikuesve të tyre, të barabartë me numrin rendor, në llojin int. Kërkimi është shumë më i shpejtë në këtë rast. Truku i dytë ishte përdorimi ORDERED INDEX — Ne erdhëm në këtë vendim në mënyrë empirike, pasi kemi kaluar të gjitha në dispozicion РІР ° СЂРёР ° РЅС‚С. E treta ishte përdorimi i pyetjeve të parametrizuara. Thelbi i metodës është ekzekutimi i komandës një herë PREPARE me përdorimin e mëvonshëm të një shprehjeje të përgatitur kur krijoni një grup pyetjesh të të njëjtit lloj, por në fakt ka një avantazh në krahasim me një të thjeshtë SELECT rezultoi të jetë brenda kufirit të gabimit statistikor.

Procesi i ngarkimit të të dhënave konsumon jo më shumë se 450 MB RAM. Kjo do të thotë, qasja e përshkruar ju lejon të zhvendosni grupet e të dhënave që peshojnë dhjetëra gigabajt në pothuajse çdo harduer buxhetor, duke përfshirë disa pajisje me një tabelë, gjë që është shumë interesante.

E tëra që mbetet është të matet shpejtësia e marrjes së të dhënave (të rastësishme) dhe të vlerësohet shkallëzimi kur kampionohen grupe të madhësive të ndryshme:

Standardi i bazës së të dhënave

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)

Quick Draw Doodle Recognition: si të bëni miq me R, C++ dhe rrjetet nervore

2. Përgatitja e tufave

I gjithë procesi i përgatitjes së serisë përbëhet nga hapat e mëposhtëm:

  1. Parimi i disa JSON-ve që përmbajnë vektorë vargjesh me koordinata pikash.
  2. Vizatimi i vijave me ngjyra bazuar në koordinatat e pikave në një imazh të madhësisë së kërkuar (për shembull, 256×256 ose 128×128).
  3. Shndërrimi i imazheve që rezultojnë në një tensor.

Si pjesë e konkurrencës midis bërthamave Python, problemi u zgjidh kryesisht duke përdorur OpenCV. Një nga analogët më të thjeshtë dhe më të dukshëm në R do të dukej kështu:

Zbatimi i konvertimit JSON në tensor në 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)
}

Vizatimi kryhet duke përdorur mjete standarde R dhe ruhet në një PNG të përkohshëm të ruajtur në RAM (në Linux, drejtoritë e përkohshme R ndodhen në drejtori /tmp, montuar në RAM). Ky skedar më pas lexohet si një grup tre-dimensionale me numra që variojnë nga 0 në 1. Kjo është e rëndësishme sepse një BMP më konvencionale do të lexohej në një grup të papërpunuar me kode hex ngjyrash.

Le të testojmë rezultatin:

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))

Quick Draw Doodle Recognition: si të bëni miq me R, C++ dhe rrjetet nervore

Vetë grupi do të formohet si më poshtë:

res <- r_process_json_vector(tmp_data[1:4, drawing], scale = 0.5)
str(res)
 # num [1:4, 1:128, 1:128, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
 # - attr(*, "dimnames")=List of 4
 #  ..$ : NULL
 #  ..$ : NULL
 #  ..$ : NULL
 #  ..$ : NULL

Ky zbatim na dukej jo optimal, pasi formimi i grupeve të mëdha kërkon një kohë të gjatë dhe vendosëm të përfitonim nga përvoja e kolegëve tanë duke përdorur një bibliotekë të fuqishme. OpenCV. Në atë kohë nuk kishte një paketë të gatshme për R (nuk ka asnjë tani), kështu që një zbatim minimal i funksionalitetit të kërkuar u shkrua në C++ me integrim në kodin R duke përdorur Rcpp.

Për të zgjidhur problemin, u përdorën paketat dhe bibliotekat e mëposhtme:

  1. OpenCV për të punuar me imazhe dhe për të vizatuar linja. Përdoren bibliotekat e para-instaluara të sistemit dhe skedarët e kokës, si dhe lidhjet dinamike.

  2. xtensor për punën me vargje dhe tensorë shumëdimensionale. Ne përdorëm skedarë kokë të përfshirë në paketën R me të njëjtin emër. Biblioteka ju lejon të punoni me vargje shumëdimensionale, si në radhë të mëdha ashtu edhe në kolonë.

  3. ndjson për analizimin e JSON. Kjo bibliotekë përdoret në xtensor automatikisht nëse është i pranishëm në projekt.

  4. RcppThread për organizimin e përpunimit me shumë fije të një vektori nga JSON. Përdori skedarët e kokës të ofruara nga kjo paketë. Nga më të njohurit RcppParalele Paketa, ndër të tjera, ka një mekanizëm të integruar të ndërprerjes së ciklit.

Ajo duhet të theksohet se xtensor doli të ishte një dhuratë nga perëndia: përveç faktit se ka funksionalitet të gjerë dhe performancë të lartë, zhvilluesit e tij rezultuan të ishin mjaft të përgjegjshëm dhe iu përgjigjën pyetjeve menjëherë dhe në detaje. Me ndihmën e tyre, ishte e mundur të zbatoheshin transformimet e matricave OpenCV në tensorë xtensor, si dhe një mënyrë për të kombinuar tensorët e imazhit 3-dimensionale në një tensor 4-dimensional të dimensionit të saktë (vetë grupi).

Materiale për të mësuar Rcpp, xtensor dhe RcppThread

https://thecoatlessprofessor.com/programming/unofficial-rcpp-api-documentation

https://docs.opencv.org/4.0.1/d7/dbd/group__imgproc.html

https://xtensor.readthedocs.io/en/latest/

https://xtensor.readthedocs.io/en/latest/file_loading.html#loading-json-data-into-xtensor

https://cran.r-project.org/web/packages/RcppThread/vignettes/RcppThread-vignette.pdf

Për të përpiluar skedarë që përdorin skedarë të sistemit dhe lidhje dinamike me bibliotekat e instaluara në sistem, ne përdorëm mekanizmin e shtojcës të implementuar në paketë Rcpp. Për të gjetur automatikisht shtigjet dhe flamujt, ne përdorëm një mjet të njohur Linux konfigurim pkg.

Zbatimi i shtojcës Rcpp për përdorimin e bibliotekës OpenCV

Rcpp::registerPlugin("opencv", function() {
  # Возможные названия пакета
  pkg_config_name <- c("opencv", "opencv4")
  # Бинарный файл утилиты pkg-config
  pkg_config_bin <- Sys.which("pkg-config")
  # Проврека наличия утилиты в системе
  checkmate::assert_file_exists(pkg_config_bin, access = "x")
  # Проверка наличия файла настроек OpenCV для pkg-config
  check <- sapply(pkg_config_name, 
                  function(pkg) system(paste(pkg_config_bin, pkg)))
  if (all(check != 0)) {
    stop("OpenCV config for the pkg-config not found", call. = FALSE)
  }

  pkg_config_name <- pkg_config_name[check == 0]
  list(env = list(
    PKG_CXXFLAGS = system(paste(pkg_config_bin, "--cflags", pkg_config_name), 
                          intern = TRUE),
    PKG_LIBS = system(paste(pkg_config_bin, "--libs", pkg_config_name), 
                      intern = TRUE)
  ))
})

Si rezultat i funksionimit të shtojcës, vlerat e mëposhtme do të zëvendësohen gjatë procesit të përpilimit:

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 i zbatimit për analizimin e JSON dhe gjenerimin e një grupi për transmetim në model jepet nën spoiler. Së pari, shtoni një direktori lokale të projektit për të kërkuar skedarët e kokës (të nevojshme për ndjson):

Sys.setenv("PKG_CXXFLAGS" = paste0("-I", normalizePath(file.path("src"))))

Zbatimi i konvertimit JSON në tensor 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ë skedar src/cv_xt.cpp dhe përpiloni me komandën Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); kërkohet edhe për punë nlohmann/json.hpp nga repozitoriя. Kodi është i ndarë në disa funksione:

  • to_xt - një funksion shabllon për transformimin e një matrice imazhi (cv::Mat) në një tensor xt::xtensor;

  • parse_json — funksioni analizon një varg JSON, nxjerr koordinatat e pikave, duke i paketuar ato në një vektor;

  • ocv_draw_lines - nga vektori që rezulton i pikave, vizaton vija me shumë ngjyra;

  • process — kombinon funksionet e mësipërme dhe gjithashtu shton aftësinë për të shkallëzuar imazhin që rezulton;

  • cpp_process_json_str - mbështjellës mbi funksionin process, i cili eksporton rezultatin në një objekt R (array shumëdimensional);

  • cpp_process_json_vector - mbështjellës mbi funksionin cpp_process_json_str, i cili ju lejon të përpunoni një vektor vargu në modalitetin me shumë fije.

Për të vizatuar linja me shumë ngjyra, u përdor modeli i ngjyrave HSV, i ndjekur nga konvertimi 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))

Quick Draw Doodle Recognition: si të bëni miq me R, C++ dhe rrjetet nervore
Krahasimi i shpejtësisë së implementimeve 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
)
# Параметры бенчмарка
cols <- c("expression", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   expression                min     median       max `itr/sec` total_time  n_itr
#   <chr>                <bch:tm>   <bch:tm>  <bch:tm>     <dbl>   <bch:tm>  <int>
# 1 r_process_json_str     3.49ms     3.55ms    4.47ms      273.      490ms    134
# 2 cpp_process_json_str   1.94ms     2.02ms    5.32ms      489.      497ms    243

library(ggplot2)
# Проведение замера
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    .data <- tmp_data[sample(seq_len(.N), batch_size), drawing]
    bench::mark(
      r_process_json_vector(.data, scale = 0.5),
      cpp_process_json_vector(.data,  scale = 0.5),
      min_iterations = 50,
      check = FALSE
    )
  }
)

res_bench[, cols]

#    expression   batch_size      min   median      max `itr/sec` total_time n_itr
#    <chr>             <dbl> <bch:tm> <bch:tm> <bch:tm>     <dbl>   <bch:tm> <int>
#  1 r                   16   50.61ms  53.34ms  54.82ms    19.1     471.13ms     9
#  2 cpp                 16    4.46ms   5.39ms   7.78ms   192.      474.09ms    91
#  3 r                   32   105.7ms 109.74ms 212.26ms     7.69        6.5s    50
#  4 cpp                 32    7.76ms  10.97ms  15.23ms    95.6     522.78ms    50
#  5 r                   64  211.41ms 226.18ms 332.65ms     3.85      12.99s    50
#  6 cpp                 64   25.09ms  27.34ms  32.04ms    36.0        1.39s    50
#  7 r                  128   534.5ms 627.92ms 659.08ms     1.61      31.03s    50
#  8 cpp                128   56.37ms  58.46ms  66.03ms    16.9        2.95s    50
#  9 r                  256     1.15s    1.18s    1.29s     0.851     58.78s    50
# 10 cpp                256  114.97ms 117.39ms 130.09ms     8.45       5.92s    50
# 11 r                  512     2.09s    2.15s    2.32s     0.463       1.8m    50
# 12 cpp                512  230.81ms  235.6ms 261.99ms     4.18      11.97s    50
# 13 r                 1024        4s    4.22s     4.4s     0.238       3.5m    50
# 14 cpp               1024  410.48ms 431.43ms 462.44ms     2.33      21.45s    50

ggplot(res_bench, aes(x = factor(batch_size), y = median, 
                      group =  expression, color = expression)) +
  geom_point() +
  geom_line() +
  ylab("median time, s") +
  theme_minimal() +
  scale_color_discrete(name = "", labels = c("cpp", "r")) +
  theme(legend.position = "bottom") 

Quick Draw Doodle Recognition: si të bëni miq me R, C++ dhe rrjetet nervore

Siç mund ta shihni, rritja e shpejtësisë doli të jetë shumë domethënëse dhe nuk është e mundur të kapni kodin C++ duke paralelizuar kodin R.

3. Iteratorët për shkarkimin e grupeve nga baza e të dhënave

R ka një reputacion të merituar për përpunimin e të dhënave që përshtaten në RAM, ndërsa Python karakterizohet më shumë nga përpunimi i përsëritur i të dhënave, duke ju lejuar të zbatoni lehtësisht dhe natyrshëm llogaritjet jashtë bërthamës (llogaritjet duke përdorur memorie të jashtme). Një shembull klasik dhe përkatës për ne në kontekstin e problemit të përshkruar janë rrjetet nervore të thella të trajnuara me metodën e zbritjes së gradientit me përafrim të gradientit në çdo hap duke përdorur një pjesë të vogël vëzhgimesh, ose mini-batch.

Kornizat e mësimit të thellë të shkruara në Python kanë klasa të veçanta që implementojnë përsëritës të bazuar në të dhëna: tabela, fotografi në dosje, formate binare, etj. Mund të përdorni opsione të gatshme ose të shkruani tuajat për detyra specifike. Në R ne mund të përfitojmë nga të gjitha veçoritë e bibliotekës Python keras me backend-et e tij të ndryshme duke përdorur paketën me të njëjtin emër, e cila nga ana tjetër funksionon në krye të paketës rrjetëzoj. Ky i fundit meriton një artikull të gjatë më vete; jo vetëm që ju lejon të ekzekutoni kodin Python nga R, por gjithashtu ju lejon të transferoni objekte midis sesioneve R dhe Python, duke kryer automatikisht të gjitha konvertimet e nevojshme të tipit.

Ne hoqëm nevojën për të ruajtur të gjitha të dhënat në RAM duke përdorur MonetDBLite, e gjithë puna e "rrjetit nervor" do të kryhet nga kodi origjinal në Python, thjesht duhet të shkruajmë një përsëritës mbi të dhënat, pasi nuk ka asgjë gati. për një situatë të tillë ose në R ose në Python. Në thelb ka vetëm dy kërkesa për të: duhet të kthejë tufa në një lak të pafund dhe të ruajë gjendjen e tij midis përsëritjeve (kjo e fundit në R zbatohet në mënyrën më të thjeshtë duke përdorur mbylljet). Më parë, kërkohej që në mënyrë eksplicite të konvertoheshin vargjet R në grupe numpy brenda iteratorit, por versioni aktual i paketës keras e bën vetë.

Përsëritësi për të dhënat e trajnimit dhe vërtetimit doli të ishte si më poshtë:

Iterator për trajnimin dhe të dhënat e vlefshmërisë

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 hyrje një variabël me një lidhje me bazën e të dhënave, numrin e linjave të përdorura, numrin e klasave, madhësinë e grupit, shkallën (scale = 1 korrespondon me paraqitjen e imazheve prej 256x256 piksele, scale = 0.5 — 128x128 piksele), tregues me ngjyra (color = FALSE specifikon paraqitjen në shkallë gri kur përdoret color = TRUE çdo goditje vizatohet me një ngjyrë të re) dhe një tregues parapërpunimi për rrjetet e trajnuara paraprakisht në imagenet. Kjo e fundit është e nevojshme për të shkallëzuar vlerat e pikselit nga intervali [0, 1] në intervalin [-1, 1], i cili është përdorur gjatë trajnimit të furnizuar. keras modele.

Funksioni i jashtëm përmban kontrollin e llojit të argumentit, një tabelë data.table me numra rreshtash të përzier rastësisht nga samples_index dhe numrat e grupeve, numëruesi dhe numri maksimal i grupeve, si dhe një shprehje SQL për shkarkimin e të dhënave nga baza e të dhënave. Për më tepër, ne përcaktuam një analog të shpejtë të funksionit brenda keras::to_categorical(). Ne përdorëm pothuajse të gjitha të dhënat për trajnim, duke lënë gjysmë për qind për vërtetim, kështu që madhësia e epokës ishte e kufizuar nga parametri steps_per_epoch kur thirret keras::fit_generator(), dhe gjendjen if (i > max_i) funksionoi vetëm për përsëritësin e vlefshmërisë.

Në funksionin e brendshëm, indekset e rreshtave merren për grupin tjetër, të dhënat shkarkohen nga baza e të dhënave me numëruesin e grupit në rritje, analiza JSON (funksioni cpp_process_json_vector(), i shkruar në C++) dhe krijimi i vargjeve që korrespondojnë me fotografitë. Më pas krijohen vektorë një-hot me etiketa klasash, vargje me vlera pikselësh dhe etiketa kombinohen në një listë, e cila është vlera e kthimit. Për të përshpejtuar punën, ne përdorëm krijimin e indekseve në tabela data.table dhe modifikimi përmes lidhjes - pa këto paketë "patate të skuqura" të dhëna.tabela Është mjaft e vështirë të imagjinohet të punosh në mënyrë efektive me ndonjë sasi të konsiderueshme të të dhënave në R.

Rezultatet e matjeve të shpejtësisë në një laptop Core i5 janë si më poshtë:

Standardi Iterator

library(Rcpp)
library(keras)
library(ggplot2)

source("utils/rcpp.R")
source("utils/keras_iterator.R")

con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))

ind <- seq_len(DBI::dbGetQuery(con, "SELECT count(*) FROM doodles")[[1L]])
num_classes <- DBI::dbGetQuery(con, "SELECT max(label_int) + 1 FROM doodles")[[1L]]

# Индексы для обучающей выборки
train_ind <- sample(ind, floor(length(ind) * 0.995))
# Индексы для проверочной выборки
val_ind <- ind[-train_ind]
rm(ind)
# Коэффициент масштаба
scale <- 0.5

# Проведение замера
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    it1 <- train_generator(
      db_connection = con,
      samples_index = train_ind,
      num_classes = num_classes,
      batch_size = batch_size,
      scale = scale
    )
    bench::mark(
      it1(),
      min_iterations = 50L
    )
  }
)
# Параметры бенчмарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   batch_size      min   median      max `itr/sec` total_time n_itr
#        <dbl> <bch:tm> <bch:tm> <bch:tm>     <dbl>   <bch:tm> <int>
# 1         16     25ms  64.36ms   92.2ms     15.9       3.09s    49
# 2         32   48.4ms 118.13ms 197.24ms     8.17       5.88s    48
# 3         64   69.3ms 117.93ms 181.14ms     8.57       5.83s    50
# 4        128  157.2ms 240.74ms 503.87ms     3.85      12.71s    49
# 5        256  359.3ms 613.52ms 988.73ms     1.54       30.5s    47
# 6        512  884.7ms    1.53s    2.07s     0.674      1.11m    45
# 7       1024     2.7s    3.83s    5.47s     0.261      2.81m    44

ggplot(res_bench, aes(x = factor(batch_size), y = median, group = 1)) +
    geom_point() +
    geom_line() +
    ylab("median time, s") +
    theme_minimal()

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: si të bëni miq me R, C++ dhe rrjetet nervore

Nëse keni një sasi të mjaftueshme RAM, mund të shpejtoni seriozisht funksionimin e bazës së të dhënave duke e transferuar atë në të njëjtën RAM (32 GB janë të mjaftueshme për detyrën tonë). Në Linux, ndarja është montuar si parazgjedhje /dev/shm, duke zënë deri në gjysmën e kapacitetit RAM. Mund të nënvizoni më shumë duke redaktuar /etc/fstabpër të marrë një rekord si tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Sigurohuni që të rindizni dhe kontrolloni rezultatin duke ekzekutuar komandën df -h.

Përsëritësi për të dhënat e testimit duket shumë më i thjeshtë, pasi grupi i të dhënave testuese përshtatet plotësisht në RAM:

Iterator për të dhënat e provës

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. Përzgjedhja e arkitekturës model

Arkitektura e parë e përdorur ishte mobilenet v1, veçoritë e të cilave diskutohen në kjo mesazh. Është i përfshirë si standard keras dhe, në përputhje me rrethanat, është i disponueshëm në paketën me të njëjtin emër për R. Por kur përpiqeni ta përdorni me imazhe me një kanal, doli një gjë e çuditshme: tensori i hyrjes duhet të ketë gjithmonë dimensionin (batch, height, width, 3), domethënë, numri i kanaleve nuk mund të ndryshohet. Nuk ka një kufizim të tillë në Python, kështu që ne nxituam dhe shkruam zbatimin tonë të kësaj arkitekture, duke ndjekur artikullin origjinal (pa braktisjen që është në versionin 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 <- function(object, 
                              filters,
                              alpha = 1,
                              depth_multiplier = 1,
                              strides = c(2, 2)) {

  # NB! depth_multiplier !=  resolution multiplier
  # https://github.com/keras-team/keras/issues/10349

  layer_depthwise_conv_2d(
    object = object,
    kernel_size = c(3, 3), 
    strides = strides,
    padding = "same",
    depth_multiplier = depth_multiplier
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() %>%
  layer_conv_2d(
    filters = filters * alpha,
    kernel_size = c(1, 1), 
    strides = c(1, 1)
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() 
}

get_mobilenet_v1 <- function(input_shape = c(224, 224, 1),
                             num_classes = 340,
                             alpha = 1,
                             depth_multiplier = 1,
                             optimizer = optimizer_adam(lr = 0.002),
                             loss = "categorical_crossentropy",
                             metrics = c("categorical_crossentropy",
                                         top_3_categorical_accuracy)) {

  inputs <- layer_input(shape = input_shape)

  outputs <- inputs %>%
    layer_conv_2d(filters = 32, kernel_size = c(3, 3), strides = c(2, 2), padding = "same") %>%
    layer_batch_normalization() %>% 
    layer_activation_relu() %>%
    layer_sep_conv_bn(filters = 64, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 128, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 128, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 256, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 256, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 1024, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 1024, strides = c(1, 1)) %>%
    layer_global_average_pooling_2d() %>%
    layer_dense(units = num_classes) %>%
    layer_activation_softmax()

    model <- keras_model(
      inputs = inputs,
      outputs = outputs
    )

    model %>% compile(
      optimizer = optimizer,
      loss = loss,
      metrics = metrics
    )

    return(model)
}

Disavantazhet e kësaj qasjeje janë të dukshme. Unë dua të testoj shumë modele, por përkundrazi, nuk dua të rishkruaj çdo arkitekturë me dorë. Na u hoq edhe mundësia për të përdorur peshat e modeleve të trajnuara paraprakisht në imagenet. Si zakonisht, studimi i dokumentacionit ndihmoi. Funksioni get_config() ju lejon të merrni një përshkrim të modelit në një formë të përshtatshme për redaktim (base_model_conf$layers - një listë e rregullt R), dhe funksioni from_config() kryen konvertimin e kundërt 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 nuk është e vështirë të shkruash një funksion universal për të marrë ndonjë nga ato të ofruara keras modele me ose pa pesha të trajnuara në imagenet:

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) {
  # Проверка аргументов
  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)
}

Kur përdorni imazhe me një kanal, nuk përdoren pesha të paratrajnuara. Kjo mund të rregullohet: duke përdorur funksionin get_weights() merrni peshat e modelit në formën e një liste të vargjeve R, ndryshoni dimensionin e elementit të parë të kësaj liste (duke marrë një kanal me ngjyra ose duke mesatarizuar të tre) dhe më pas ngarkoni përsëri peshat në model me funksionin set_weights(). Ne kurrë nuk e shtuam këtë funksionalitet, sepse në këtë fazë ishte tashmë e qartë se ishte më produktive të punohej me foto me ngjyra.

Ne kryem shumicën e eksperimenteve duke përdorur versionet mobilenet 1 dhe 2, si dhe resnet34. Arkitekturat më moderne si SE-ResNeXt performuan mirë në këtë konkurs. Për fat të keq, ne nuk kishim në dispozicion implementime të gatshme dhe nuk i shkruanim tona (por do të shkruajmë patjetër).

5. Parametizimi i skripteve

Për lehtësi, i gjithë kodi për fillimin e trajnimit u krijua si një skenar i vetëm, i parametrizuar duke përdorur dokpt si më poshtë:

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)

paketë dokpt paraqet zbatimin http://docopt.org/ për R. Me ndihmën e tij lansohen skriptet me komanda të thjeshta si p.sh 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ë e ekzekutueshme (kjo komandë do të fillojë trajnimin e modelit resnet50 në imazhet me tre ngjyra me përmasa 128x128 piksele, baza e të dhënave duhet të jetë e vendosur në dosje /home/andrey/doodle_db). Mund të shtoni shpejtësinë e të mësuarit, llojin e optimizuesit dhe çdo parametër tjetër të personalizueshëm në listë. Në procesin e përgatitjes së botimit, rezultoi se arkitektura mobilenet_v2 nga versioni aktual keras në përdorim R nuk mund për shkak të ndryshimeve që nuk janë marrë parasysh në paketën R, presim që ta rregullojnë.

Kjo qasje bëri të mundur përshpejtimin e konsiderueshëm të eksperimenteve me modele të ndryshme në krahasim me nisjen më tradicionale të skripteve në RStudio (ne vërejmë paketën si një alternativë të mundshme tfruns). Por avantazhi kryesor është aftësia për të menaxhuar me lehtësi nisjen e skripteve në Docker ose thjesht në server, pa instaluar RStudio për këtë.

6. Dokerizimi i skripteve

Ne përdorëm Docker për të siguruar transportueshmëri të mjedisit për modelet e trajnimit midis anëtarëve të ekipit dhe për vendosjen e shpejtë në cloud. Ju mund të filloni të njiheni me këtë mjet, i cili është relativisht i pazakontë për një programues R, me kjo seria e botimeve ose kurs video.

Docker ju lejon të krijoni imazhet tuaja nga e para dhe të përdorni imazhe të tjera si bazë për të krijuar imazhet tuaja. Kur analizuam opsionet e disponueshme, arritëm në përfundimin se instalimi i drejtuesve NVIDIA, CUDA+cuDNN dhe bibliotekave Python është një pjesë mjaft voluminoze e imazhit dhe vendosëm të marrim imazhin zyrtar si bazë tensorflow/tensorflow:1.12.0-gpu, duke shtuar aty paketat e nevojshme R.

Skedari përfundimtar docker dukej kështu:

dockerfile

FROM tensorflow/tensorflow:1.12.0-gpu

MAINTAINER Artem Klevtsov <[email protected]>

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ësi, paketat e përdorura u vendosën në variabla; pjesa më e madhe e skripteve të shkruara kopjohen brenda kontejnerëve gjatë montimit. Ne gjithashtu ndryshuam guaskën e komandës në /bin/bash për lehtësinë e përdorimit të përmbajtjes /etc/os-release. Kjo shmangi nevojën për të specifikuar versionin e OS në kod.

Për më tepër, u shkrua një skenar i vogël bash që ju lejon të nisni një kontejner me komanda të ndryshme. Për shembull, këto mund të jenë skriptet për trajnimin e rrjeteve nervore që ishin vendosur më parë brenda kontejnerit, ose një guaskë komandimi për korrigjimin dhe monitorimin e funksionimit të kontejnerit:

Skript për të nisur kontejnerin

#!/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, skripti do të thirret brenda kontejnerit train_nn.R me vlerat e paracaktuara; nëse argumenti i parë pozicional është "bash", atëherë kontejneri do të fillojë në mënyrë interaktive me një guaskë komandimi. Në të gjitha rastet e tjera, vlerat e argumenteve të pozicionit zëvendësohen: CMD="Rscript /app/train_nn.R $@".

Vlen të përmendet se direktoritë me të dhënat burimore dhe bazën e të dhënave, si dhe drejtoria për ruajtjen e modeleve të trajnuara, janë montuar brenda kontejnerit nga sistemi pritës, i cili ju lejon të përdorni rezultatet e skripteve pa manipulime të panevojshme.

7. Përdorimi i GPU-ve të shumta në Google Cloud

Një nga veçoritë e konkursit ishin të dhënat shumë të zhurmshme (shih foton e titullit, huazuar nga @Leigh.plt nga ODS slack). Grupe të mëdha ndihmojnë në luftimin e kësaj, dhe pas eksperimenteve në një PC me 1 GPU, vendosëm të zotërojmë modelet e trajnimit në disa GPU në re. Përdorur GoogleCloud (udhëzues i mirë për bazat) për shkak të përzgjedhjes së madhe të konfigurimeve të disponueshme, çmimeve të arsyeshme dhe bonusit 300$. Nga lakmia, porosita një shembull 4xV100 me një SSD dhe një ton RAM, dhe ky ishte një gabim i madh. Një makinë e tillë i ha paratë shpejt; ju mund të eksperimentoni pa një tubacion të provuar. Për qëllime edukative, është më mirë të merrni K80. Por sasia e madhe e RAM-it erdhi në ndihmë - SSD i cloud nuk bëri përshtypje me performancën e tij, kështu që baza e të dhënave u transferua në dev/shm.

Me interes më të madh është fragmenti i kodit përgjegjës për përdorimin e shumë GPU-ve. Së pari, modeli krijohet në CPU duke përdorur një menaxher konteksti, ashtu si 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 pakompiluar (kjo është e rëndësishme) kopjohet në një numër të caktuar të GPU-ve të disponueshme, dhe vetëm pas kësaj përpilohet:

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)
)

Teknika klasike e ngrirjes së të gjitha shtresave përveç asaj të fundit, trajnimit të shtresës së fundit, shkrirjes dhe ritrajnimit të të gjithë modelit për disa GPU nuk mund të zbatohej.

Trajnimi u monitorua pa përdorim. tensorboard, duke u kufizuar në regjistrimin e regjistrave dhe ruajtjen e modeleve me emra informues pas çdo epoke:

kthimet e telefonatave

# Шаблон имени файла лога
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ë një përfundimi

Një sërë problemesh që kemi hasur ende nuk janë tejkaluar:

  • в keras nuk ka asnjë funksion të gatshëm për kërkimin automatik të shkallës optimale të të mësuarit (analog lr_finder në bibliotekë shpejt.ai); Me disa përpjekje, është e mundur të transferohen implementimet e palëve të treta në R, për shembull, kjo;
  • si pasojë e pikës së mëparshme, nuk ishte e mundur të zgjidhej shpejtësia e duhur e trajnimit kur përdorni disa GPU;
  • ka mungesë të arkitekturave moderne të rrjeteve nervore, veçanërisht ato të trajnuara paraprakisht në imagenet;
  • Askush nuk cikli politika dhe normat diskriminuese të të mësuarit (pjekja kosinus ishte me kërkesën tonë zbatuar, Faleminderit skeydan).

Cilat gjëra të dobishme u mësuan nga ky konkurs:

  • Në pajisje relativisht me fuqi të ulët, mund të punoni me vëllime të përshtatshme (shumë herë më të mëdha se RAM-i) të të dhënave pa dhimbje. Qese plastike të dhëna.tabela kursen kujtesën për shkak të modifikimit në vend të tabelave, i cili shmang kopjimin e tyre dhe kur përdoret si duhet, aftësitë e tij pothuajse gjithmonë demonstrojnë shpejtësinë më të lartë midis të gjitha mjeteve të njohura për ne për gjuhët e skriptimit. Ruajtja e të dhënave në një bazë të dhënash ju lejon, në shumë raste, të mos mendoni fare për nevojën për të shtrydhur të gjithë grupin e të dhënave në RAM.
  • Funksionet e ngadalta në R mund të zëvendësohen me ato të shpejta në C++ duke përdorur paketën Rcpp. Nëse përveç përdorimit RcppThread ose RcppParalele, marrim zbatime me shumë fije ndër-platformash, kështu që nuk ka nevojë të paralelizojmë kodin në nivelin R.
  • Paketa Rcpp mund të përdoret pa njohuri serioze të C++, është përshkruar minimumi i kërkuar këtu. Skedarët e kokës për një numër bibliotekash të lezetshme C si xtensor në dispozicion në CRAN, domethënë po formohet një infrastrukturë për zbatimin e projekteve që integrojnë kodin e gatshëm të performancës së lartë C++ në R. Komoditet shtesë është theksimi i sintaksës dhe një analizues statik i kodit C++ në RStudio.
  • dokpt ju lejon të ekzekutoni skriptet e pavarura me parametra. Kjo është e përshtatshme për t'u përdorur në një server të largët, përfshirë. nën doker. Në RStudio, është e papërshtatshme të kryhen shumë orë eksperimente me trajnimin e rrjeteve nervore, dhe instalimi i IDE në vetë serverin nuk është gjithmonë i justifikuar.
  • Docker siguron transportueshmëri kodi dhe riprodhueshmëri të rezultateve midis zhvilluesve me versione të ndryshme të OS dhe bibliotekave, si dhe lehtësinë e ekzekutimit në serverë. Ju mund të nisni të gjithë tubacionin e trajnimit me vetëm një komandë.
  • Google Cloud është një mënyrë buxhetore për të eksperimentuar në pajisje të shtrenjta, por duhet të zgjidhni konfigurimet me kujdes.
  • Matja e shpejtësisë së fragmenteve individuale të kodit është shumë e dobishme, veçanërisht kur kombinohen R dhe C++, dhe me paketën stol - gjithashtu shumë e lehtë.

Në përgjithësi kjo përvojë ishte shumë shpërblyese dhe ne vazhdojmë të punojmë për të zgjidhur disa nga çështjet e ngritura.

Burimi: www.habr.com

Shto një koment