Quick Draw Doodle Recognition: kā sadraudzēties ar R, C++ un neironu tīkliem

Quick Draw Doodle Recognition: kā sadraudzēties ar R, C++ un neironu tīkliem

Sveiki, Habr!

PagājuÅ”ajā gada rudenÄ« Kaggle norisinājās zÄ«mēto attēlu klasifikācijas konkurss Quick Draw Doodle Recognition, kurā piedalÄ«jās arÄ« R komandas dalÄ«bnieki, Artem Klevcov, Filips Upravitelev un Andrej Ogurcov. Neplānojam detalizēti aprakstÄ«t sacensÄ«bu, tas jau tika izdarÄ«ts nesenajā publikācijā.

Å oreiz medaļu laimēŔana nesanāca, bet tika iegÅ«ta liela vērtÄ«ga pieredze, tāpēc vēlamies dalÄ«ties ar dažu no interesantākajām un noderÄ«gākajām tēmām Kaggle un ikdienas darbā. ApskatÄ«tās tēmas ietver: grÅ«to dzÄ«vi bez OpenCV, JSON parsēŔanu (Å”ajos piemēros tiek apsvērta C++ koda integrācija R skriptos vai paketēs, izmantojot Rcpp), skriptu parametrizāciju un gala risinājuma dockerizāciju. Visi kodu piemēri ir pieejami izpildei repozitorijā.

Saturs:

  1. Efektīva datu ielāde no CSV uz MonetDB datu bāzi
  2. SagatavoŔanas partijas
  3. Iteratori partiju izgūŔanai no DB
  4. Modeļa arhitektūras izvēle
  5. Skriptu parametrizācija
  6. Skriptu dockerizācija
  7. Vairāku GPU izmantoŔana Google Cloud
  8. Noslēgumā

1. Efektīva datu ielāde no CSV uz MonetDB datu bāzi

Dati Å”ajā sacensÄ«bā tiek nodroÅ”ināti nevis kā gatavi attēli, bet kā 340 CSV faili (pa vienam failam uz katru klasi), kas satur JSON ar punktu koordinātēm. Savienojot Å”os punktus ar lÄ«nijām, mēs iegÅ«stam gala attēlu izmērā 256x256 pikseļi. ArÄ« katram ierakstam ir norādÄ«ta birka, vai attēls tika pareizi atpazÄ«ts ar tobrÄ«d izmantoto klasifikatoru, divu burtu kods valsts, kurā dzÄ«vo zÄ«mētāja autors, unikāls identifikators, laikspiedols un klases nosaukums, kas atbilst faila nosaukumam. VienkārÅ”otā versija sver 7.4 GB arhivētā formātā un apmēram 20 GB pēc atpakotÅ”anas, pilni dati pēc atpakotÅ”anas aizņem 240 GB. Organizatori garantēja, ka abas versijas reproducē tos paÅ”us zÄ«mējumus, tātad pilna versija ir lieka. Jebkurā gadÄ«jumā ir atzÄ«ts, ka 50 miljons zÄ«mējumu glabāŔana grafiskajos failos vai masÄ«vos nav rentabla, un mēs izlemām apvienot visus CSV failus no arhÄ«va train_simplified.zip datu bāzē ar sekojoÅ”u attēlu Ä£enerēŔanu vajadzÄ«gajos izmēros ā€žuz vietasā€ katrai partijai.

Kā DBMS tika izvēlēta labi sevi pierādÄ«jusi MonetDB, proti tam ir realizācija R kā pakotne MonetDBLite. Pakotne ietver iebÅ«vēto datu bāzes servera versiju un ļauj palaist serveri tieÅ”i no R sesijas un strādāt ar to. Datu bāzes izveidoÅ”ana un pieslēgÅ”anās notiek ar vienu komandu:

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

Mums bÅ«s nepiecieÅ”ams izveidot divas tabulas: vienu visiem datiem, otru - pakalpojuma informācijai par ielādētajiem failiem (tas noderēs, ja kaut kas noies greizi un procesa turpināŔana bÅ«s nepiecieÅ”ama pēc vairāku failu ielādes):

Tabulu izveide

if (!DBI::dbExistsTable(con, "doodles")) {
  DBI::dbCreateTable(
    con = con,
    name = "doodles",
    fields = c(
      "countrycode" = "char(2)",
      "drawing" = "text",
      "key_id" = "bigint",
      "recognized" = "bool",
      "timestamp" = "timestamp",
      "word" = "text"
    )
  )
}

if (!DBI::dbExistsTable(con, "upload_log")) {
  DBI::dbCreateTable(
    con = con,
    name = "upload_log",
    fields = c(
      "id" = "serial",
      "file_name" = "text UNIQUE",
      "uploaded" = "bool DEFAULT false"
    )
  )
}

Ātrākais veids, kā ielādēt datus datu bāzē, izrādÄ«jās CSV failu tieÅ”a kopēŔana, izmantojot SQL komandu COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',', 'n', '"' NULL AS '' BEST EFFORT, kur tablename — tabulas nosaukums un ceļŔ — ceļŔ uz failu. Strādājot ar arhÄ«vu, tika atklāts, ka iebÅ«vētā realizācija unzip R nepareizi strādā ar vairākiem arhÄ«va failiem, tāpēc mēs izmantojām sistēmas unzip (izmantojot parametru getOption("unzip")).

Funkcija datu ierakstīŔanai bāzē

#' @title Š˜Š·Š²Š»ŠµŃ‡ŠµŠ½ŠøŠµ Šø Š·Š°Š³Ń€ŃƒŠ·ŠŗŠ° файлов
#'
#' @description
#' Š˜Š·Š²Š»ŠµŃ‡ŠµŠ½ŠøŠµ CSV-файлов ŠøŠ· ZIP-архива Šø Š·Š°Š³Ń€ŃƒŠ·ŠŗŠ° ŠøŃ… в базу Ганных
#'
#' @param con ŠžŠ±ŃŠŠµŠŗŃ‚ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ Šŗ базе Ганных (класс `MonetDBEmbeddedConnection`).
#' @param tablename ŠŠ°Š·Š²Š°Š½ŠøŠµ таблицы в базе Ганных.
#' @oaram zipfile ŠŸŃƒŃ‚ŃŒ Šŗ ZIP-Š°Ń€Ń…ŠøŠ²Ńƒ.
#' @oaram filename Š˜Š¼Ń файла Š²Š½ŃƒŃ€Šø ZIP-архива.
#' @param preprocess Š¤ŃƒŠ½ŠŗŃ†ŠøŃ преГобработки, ŠŗŠ¾Ń‚Š¾Ń€Š°Ń Š±ŃƒŠ“ŠµŃ‚ применена ŠøŠ·Š²Š»ŠµŃ‡Ń‘Š½Š½Š¾Š¼Ńƒ Ń„Š°Š¹Š»Ńƒ.
#'   Должна ŠæŃ€ŠøŠ½ŠøŠ¼Š°Ń‚ŃŒ оГин Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ `data` (Š¾Š±ŃŠŠµŠŗŃ‚ `data.table`).
#'
#' @return `TRUE`.
#'
upload_file <- function(con, tablename, zipfile, filename, preprocess = NULL) {
  # ŠŸŃ€Š¾Š²ŠµŃ€ŠŗŠ° Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Š¾Š²
  checkmate::assert_class(con, "MonetDBEmbeddedConnection")
  checkmate::assert_string(tablename)
  checkmate::assert_string(filename)
  checkmate::assert_true(DBI::dbExistsTable(con, tablename))
  checkmate::assert_file_exists(zipfile, access = "r", extension = "zip")
  checkmate::assert_function(preprocess, args = c("data"), null.ok = TRUE)

  # Š˜Š·Š²Š»ŠµŃ‡ŠµŠ½ŠøŠµ файла
  path <- file.path(tempdir(), filename)
  unzip(zipfile, files = filename, exdir = tempdir(), 
        junkpaths = TRUE, unzip = getOption("unzip"))
  on.exit(unlink(file.path(path)))

  # ŠŸŃ€ŠøŠ¼ŠµŠ½ŃŠµŠ¼ Ń„ŃƒŠ½ŠŗŃ†ŠøŃ преГобработки
  if (!is.null(preprocess)) {
    .data <- data.table::fread(file = path)
    .data <- preprocess(data = .data)
    data.table::fwrite(x = .data, file = path, append = FALSE)
    rm(.data)
  }

  # Запрос Šŗ БД на импорт CSV
  sql <- sprintf(
    "COPY OFFSET 2 INTO %s FROM '%s' USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORT",
    tablename, path
  )
  # Выполнение запроса Šŗ БД
  DBI::dbExecute(con, sql)

  # Добавление записи об успешной Š·Š°Š³Ń€ŃƒŠ·ŠŗŠµ в ŃŠ»ŃƒŠ¶ŠµŠ±Š½ŃƒŃŽ Ń‚Š°Š±Š»ŠøŃ†Ńƒ
  DBI::dbExecute(con, sprintf("INSERT INTO upload_log(file_name, uploaded) VALUES('%s', true)",
                              filename))

  return(invisible(TRUE))
}

Ja ir nepiecieÅ”ams pārvērst tabulu pirms ierakstīŔanas datu bāzē, pietiek nodot argumentā preprocess funkciju, kas pārvērÅ” datus.

Kods sekvenču datu ielādei bāzē:

Datu ierakstīŔana bāzē

# Дписок файлов Š“Š»Ń записи
files <- unzip(zipfile, list = TRUE)$Name

# Дписок ŠøŃŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠ¹, если Ń‡Š°ŃŃ‚ŃŒ файлов уже была Š·Š°Š³Ń€ŃƒŠ¶ŠµŠ½Š°
to_skip <- DBI::dbGetQuery(con, "SELECT file_name FROM upload_log")[[1L]]
files <- setdiff(files, to_skip)

if (length(files) > 0L) {
  # Š—Š°ŠæŃƒŃŠŗŠ°ŠµŠ¼ таймер
  tictoc::tic()
  # ŠŸŃ€Š¾Š³Ń€ŠµŃŃ бар
  pb <- txtProgressBar(min = 0L, max = length(files), style = 3)
  for (i in seq_along(files)) {
    upload_file(con = con, tablename = "doodles", 
                zipfile = zipfile, filename = files[i])
    setTxtProgressBar(pb, i)
  }
  close(pb)
  # ŠžŃŃ‚Š°Š½Š°Š²Š»ŠøŠ²Š°ŠµŠ¼ таймер
  tictoc::toc()
}

# 526.141 sec elapsed - копирование SSD->SSD
# 558.879 sec elapsed - копирование USB->SSD

Datu ielādes laiks var atŔķirties atkarÄ«bā no izmantotā krātuves ātruma raksturlielumiem. MÅ«su gadÄ«jumā lasīŔana un rakstīŔana no viena SSD vai no zibatmiņas (sākotnējais fails) uz SSD (datu bāzi) aizņem mazāk par 10 minÅ«tēm.

Vēl dažas sekundes ir nepiecieÅ”amas, lai izveidotu kolonnu ar veselu skaitļu klases marÄ·i un indeksa kolonnu (ORDERED INDEX) ar rindu numuriem, pēc kuriem tiks veikta novērojumu izvēle, veidojot partijas:

Papildu kolonnu un indeksa izveide

message("Generate labels")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD label_int int"))
invisible(DBI::dbExecute(con, "UPDATE doodles SET label_int = dense_rank() OVER (ORDER BY word) - 1"))

message("Generate row numbers")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD id serial"))
invisible(DBI::dbExecute(con, "CREATE ORDERED INDEX doodles_id_ord_idx ON doodles(id)"))

Lai izpildÄ«tu uzdevumu izveidot partiju "uz vietas", mums bija jāsasniedz maksimāla ātruma nejauÅ”o rindu izgūŔanai no tabulas. doodles. Tādēļ mēs izmantojām 3 trikus. Pirmais bija samazināt datu tipa izmēru, kurā tiek glabāts novērojumu ID. Sākotnējā datu kopā ID glabāŔanai nepiecieÅ”ams tips bigint, bet novērojumu skaits ļauj ietilpināt to identifikatorus, kas ir vienādi ar secÄ«bas numuru, tipā int. Å ajā gadÄ«jumā meklēŔana notiek ievērojami ātrāk. Otrais triks bija izmantot ORDERED INDEX — Å”o risinājumu mēs atradām empÄ«riski, izmēģinot visus pieejamos variantus. TreÅ”ais sastāvēja no parametrizētiem pieprasÄ«jumiem. Metodes bÅ«tÄ«ba ir vienreizēji izpildÄ«t komandu PREPARE , sekojoÅ”i izmantojot sagatavoto izteiksmi, veidojot virkni viendabÄ«gu pieprasÄ«jumu, taču praksē ieguvums salÄ«dzinājumā ar vienkārÅ”u SELECT izrādÄ«jās statistiski nenozÄ«mÄ«gs.

Datu ievadīŔanas process patērē ne vairāk kā 450 MB RAM. Tātad, aprakstÄ«tais pieejas ļauj apstrādāt datu kopas, kuru svars ir desmitiem gigabaitu, praktiski uz jebkura budžeta dzelžiem, ieskaitot dažus viensaldus, kas ir diezgan forÅ”i.

Paliek veikt ātruma mērÄ«jumus (nejauÅ”o) datu izgūŔanai un novērtēt mērogojamÄ«bu, izvēloties partijas dažāda lieluma:

Datu bāzes tests

library(ggplot2)

set.seed(0)
# PieslēgÅ”anās pie datu bāzes
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))

# Funkcija, lai sagatavotu vaicājumu servera pusē
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)
}

# Funkcija datu iegūŔanai
fetch_data <- function(rs, batch_size) {
  ids <- sample(seq_len(n), batch_size)
  res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
  return(res)
}

# Mēģinājumu veikÅ”ana
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    rs <- prep_sql(batch_size)
    bench::mark(
      fetch_data(rs, batch_size),
      min_iterations = 50L
    )
  }
)
# Benchmarks parametri
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   batch_size      min   median      max `itr/sec` total_time n_itr
#                    
# 1         16   23.6ms  54.02ms  93.43ms     18.8        2.6s    49
# 2         32     38ms  84.83ms 151.55ms     11.4       4.29s    49
# 3         64   63.3ms 175.54ms 248.94ms     5.85       8.54s    50
# 4        128   83.2ms 341.52ms 496.24ms     3.00      16.69s    50
# 5        256  232.8ms 653.21ms 847.44ms     1.58      31.66s    50
# 6        512  784.6ms    1.41s    1.98s     0.740       1.1m    49
# 7       1024  681.7ms    2.72s    4.06s     0.377      2.16m    49

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: kā sadraudzēties ar R, C++ un neironu tīkliem

2. Batču sagatavoŔana

Visu batču sagatavoŔanas process sastāv no Ŕādiem posmiem:

  1. Vairāku JSON parsēŔana, kas satur virknes vektorus ar punktu koordinātēm.
  2. Krāsainu lÄ«niju uzzÄ«mēŔana pēc punktu koordinātēm attēlā ar nepiecieÅ”amo izmēru (piemēram, 256Ɨ256 vai 128Ɨ128).
  3. IegÅ«to attēlu pārvērÅ”ana tensorā.

Sacensību ietvaros starp Python kernel-iem uzdevums galvenokārt tika risināts ar līdzekļiem OpenCV. Viens no acīmredzamākajiem līdzīgiem risinājumiem R būtu Ŕāds:

JSON pārvērÅ”anas Ä«stenoÅ”ana tensorā R

r_process_json_str <- function(json, line.width = 3, 
                               color = TRUE, scale = 1) {
  # JSON parsēŔana
  coords <- jsonlite::fromJSON(json, simplifyMatrix = FALSE)
  tmp <- tempfile()
  # DzēŔam pagaidu failu funkcijas beigās
  on.exit(unlink(tmp))
  png(filename = tmp, width = 256 * scale, height = 256 * scale, pointsize = 1)
  # TukŔa diagramma
  plot.new()
  # Diagrammas loga izmērs
  plot.window(xlim = c(256 * scale, 0), ylim = c(256 * scale, 0))
  # Līniju krāsas
  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()
  # Attēla pārvērÅ”ana 3D masÄ«vā
  res <- png::readPNG(tmp)
  return(res)
}

r_process_json_vector <- function(x, ...) {
  res <- lapply(x, r_process_json_str, ...)
  # Apvienojam 3D attēlu masīvus 4D tenzorā
  res <- do.call(abind::abind, c(res, along = 0))
  return(res)
}

ZÄ«mēŔana tiek veikta, izmantojot standarta R rÄ«kus, saglabājot pagaidu PNG failā, kas tiek glabāts RAM (Linux pagaidu direktorijas R atrodas katalogā /tmp, kas piesprausts RAM). Pēc tam Å”is fails tiek nolasÄ«ts kā trÄ«sdimensionāls masÄ«vs ar skaitļiem diapazonā no 0 lÄ«dz 1. Tas ir svarÄ«gi, jo vispārpieņemtais BMP tiktu nolasÄ«ts kā raw-masÄ«vs ar hex krāsu kodiem.

Pārbaudīsim rezultātu:

zip_file <- file.path("data", "train_simplified.zip")
csv_file <- "cat.csv"
unzip(zip_file, files = csv_file, exdir = tempdir(), 
      junkpaths = TRUE, unzip = getOption("unzip"))
tmp_data <- data.table::fread(file.path(tempdir(), csv_file), sep = ",", 
                              select = "drawing", nrows = 10000)
arr <- r_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256   3
plot(magick::image_read(arr))

Quick Draw Doodle Recognition: kā sadraudzēties ar R, C++ un neironu tīkliem

PaÅ”nÄ«kt vēlas veidot partiju Ŕādi:

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

Å Ä« Ä«stenoÅ”ana mums Ŕķita neefektÄ«va, jo liela partiju veidoÅ”ana prasa nenormāli daudz laika, un mēs nolēmām izmantot kolēģu pieredzi, izmantojot jaudÄ«gu bibliotēku OpenCV. Tajā laikā gatava pakete R nebija (nav arÄ« tagad), tāpēc tika uzrakstÄ«ta minimāla nepiecieÅ”amā funkcionalitāte C++ ar integrāciju R kodā, izmantojot Rcpp.

Lai atrisinātu uzdevumu, tika izmantotas Ŕādas paketes un bibliotēkas:

  1. OpenCV attēlu apstrādei un lÄ«niju zÄ«mēŔanai. Tika izmantotas iepriekÅ” instalētas sistēmas bibliotēkas un galvenes, kā arÄ« dinamisā saistīŔana.

  2. xtensor darbam ar daudzdimensiju masīviem un tenzoriem. Mēs izmantojām galvenes failus, kas iekļauti attiecīgajā R paketē. Bibliotēka ļauj strādāt ar daudzdimensiju masīviem, gan row major, gan column major secībā.

  3. ndjson JSON parsēŔanai. Å Ä« bibliotēka tiek izmantota xtensor automātiski, ja tā ir pieejama projektā.

  4. RcppThread daudzvÄ«du apstrādei JSON vektoriem. Mēs izmantojām galvenes failus, ko nodroÅ”ina Ŕī pakete. SalÄ«dzinoÅ”i ar populārāko RcppParallel paketi, starp citu, ir iebÅ«vēts cikla pārtraukÅ”anas mehānisms (interrupt).

JāatzÄ«mē, ka xtensor izrādÄ«jās kā Ä«sts atklājums: ne tikai tam ir plaÅ”s funkcionalitātes klāsts un augsta veiktspēja, bet arÄ« tā izstrādātāji izrādÄ«jās diezgan atsaucÄ«gi un operatÄ«vi un detalizēti atbildēja uz raduŔām jautājumiem. Ar viņu palÄ«dzÄ«bu izdevās realizēt OpenCV matricas pārvērÅ”anu xtensor tenzoros, kā arÄ« trÄ«s dimensiju attēlu tenzoru apvienoÅ”anu četru dimensiju tenzorā ar pareizu dimensiju (proti, partiju).

Materiāli Rcpp, xtensor un RcppThread apguvei

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

Lai kompilētu failus, kas izmanto sistēmas failus un dinamiski saistÄ«tās bibliotēkas, kas instalētas sistēmā, mēs izmantojām spraudņa mehānisma, kas Ä«stenots paketē, Rcpp. Automātiskai ceļu un karogu atraÅ”anai izmantojām populāro linux rÄ«ku pkg-config.

Rcpp spraudņa Ä«stenoÅ”ana, lai izmantotu OpenCV bibliotēku

Rcpp::registerPlugin("opencv", function() {
  # Iespējamie paketes nosaukumi
  pkg_config_name <- c("opencv", "opencv4")
  # pkg-config rīka binārais fails
  pkg_config_bin <- Sys.which("pkg-config")
  # Pārbaude, vai rīks ir pieejams sistēmā
  checkmate::assert_file_exists(pkg_config_bin, access = "x")
  # Pārbaude, vai OpenCV konfigurācijas fails ir pieejams 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)
  ))
})

Kompilācijas procesā spraudņa darbÄ«bas rezultātā tiks aizvietoti Ŕādi vērtÄ«bas:

Rcpp:::.plugins$opencv()$env

# $PKG_CXXFLAGS
# [1] "-I/usr/include/opencv"
#
# $PKG_LIBS
# [1] "-lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_datasets -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_line_descriptor -lopencv_optflow -lopencv_video -lopencv_plot -lopencv_reg -lopencv_saliency -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_rgbd -lopencv_viz -lopencv_surface_matching -lopencv_text -lopencv_ximgproc -lopencv_calib3d -lopencv_features2d -lopencv_flann -lopencv_xobjdetect -lopencv_objdetect -lopencv_ml -lopencv_xphoto -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -lopencv_photo -lopencv_imgproc -lopencv_core"

JSON parsēŔanas un partijas veidoÅ”anas kods modelim ir sniegts zem spoileris. IepriekÅ” pievienojam lokālo projekta direktoriju galvene failu meklēŔanai (nepiecieÅ”a ndjson):

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

JSON pārveidoŔanas implementācija uz C++

// [[Rcpp::plugins(cpp14)]]
// [[Rcpp::plugins(opencv)]]
// [[Rcpp::depends(xtensor)]]
// [[Rcpp::depends(RcppThread)]]

#include <xtensor/xjson.hpp>
#include <xtensor/xadapt.hpp>
#include <xtensor/xview.hpp>
#include <xtensor-r/rtensor.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <Rcpp.h>
#include <RcppThread.h>

// Динонимы Š“Š»Ń типов
using RcppThread::parallelFor;
using json = nlohmann::json;
using points = xt::xtensor<double,2>;     // Š˜Š·Š²Š»ŠµŃ‡Ń‘Š½Š½Ń‹Šµ ŠøŠ· JSON коорГинаты точек
using strokes = std::vector<points>;      // Š˜Š·Š²Š»ŠµŃ‡Ń‘Š½Š½Ń‹Šµ ŠøŠ· JSON коорГинаты точек
using xtensor3d = xt::xtensor<double, 3>; // Тензор Š“Š»Ń Ń…Ń€Š°Š½ŠµŠ½ŠøŃ матрицы ŠøŠ·Š¾Š¾Š±Ń€Š°Š¶ŠµŠ½ŠøŃ
using xtensor4d = xt::xtensor<double, 4>; // Тензор Š“Š»Ń Ń…Ń€Š°Š½ŠµŠ½ŠøŃ множества изображений
using rtensor3d = xt::rtensor<double, 3>; // ŠžŠ±Ń‘Ń€Ń‚ŠŗŠ° Š“Š»Ń ŃŠŗŃŠæŠ¾Ń€Ń‚Š° в R
using rtensor4d = xt::rtensor<double, 4>; // ŠžŠ±Ń‘Ń€Ń‚ŠŗŠ° Š“Š»Ń ŃŠŗŃŠæŠ¾Ń€Ń‚Š° в R

// Дтатические константы
// Размер ŠøŠ·Š¾Š±Ń€Š°Š¶ŠµŠ½ŠøŃ в ŠæŠøŠŗŃŠµŠ»ŃŃ…
const static int SIZE = 256;
// Тип линии
// Дм. https://en.wikipedia.org/wiki/Pixel_connectivity#2-dimensional
const static int LINE_TYPE = cv::LINE_4;
// Толщина линии в ŠæŠøŠŗŃŠµŠ»ŃŃ…
const static int LINE_WIDTH = 3;
// Алгоритм ресайза
// https://docs.opencv.org/3.1.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121
const static int RESIZE_TYPE = cv::INTER_LINEAR;

// Шаблон Š“Š»Ń ŠŗŠ¾Š½Š²ŠµŃ€Ń‚ŠøŃ€Š¾Š²Š°Š½ŠøŃ OpenCV-матрицы в тензор
template <typename T, int NCH, typename XT=xt::xtensor<T,3,xt::layout_type::column_major>>
XT to_xt(const cv::Mat_<cv::Vec<T, NCH>>& src) {
  // Š Š°Š·Š¼ŠµŃ€Š½Š¾ŃŃ‚ŃŒ целевого тензора
  std::vector<int> shape = {src.rows, src.cols, NCH};
  // ŠžŠ±Ń‰ŠµŠµ количество ŃŠ»ŠµŠ¼ŠµŠ½Ń‚Š¾Š² в массиве
  size_t size = src.total() * NCH;
  // ŠŸŃ€ŠµŠ¾Š±Ń€Š°Š·Š¾Š²Š°Š½ŠøŠµ cv::Mat в xt::xtensor
  XT res = xt::adapt((T*) src.data, size, xt::no_ownership(), shape);
  return res;
}

// ŠŸŃ€ŠµŠ¾Š±Ń€Š°Š·Š¾Š²Š°Š½ŠøŠµ JSON в список коорГинат точек
strokes parse_json(const std::string& x) {
  auto j = json::parse(x);
  // Š ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ парсинга Голжен Š±Ń‹Ń‚ŃŒ массивом
  if (!j.is_array()) {
    throw std::runtime_error("'x' must be JSON array.");
  }
  strokes res;
  res.reserve(j.size());
  for (const auto& a: j) {
    // ŠšŠ°Š¶Š“Ń‹Š¹ ŃŠ»ŠµŠ¼ŠµŠ½Ń‚ массива Голжен Š±Ń‹Ń‚ŃŒ 2-мерным массивом
    if (!a.is_array() || a.size() != 2) {
      throw std::runtime_error("'x' must include only 2d arrays.");
    }
    // Š˜Š·Š²Š»ŠµŃ‡ŠµŠ½ŠøŠµ вектора точек
    auto p = a.get<points>();
    res.push_back(p);
  }
  return res;
}

// ŠžŃ‚Ń€ŠøŃŠ¾Š²ŠŗŠ° линий
// Цвета HSV
cv::Mat ocv_draw_lines(const strokes& x, bool color = true) {
  // Š˜ŃŃ…Š¾Š“Š½Ń‹Š¹ тип матрицы
  auto stype = color ? CV_8UC3 : CV_8UC1;
  // Š˜Ń‚Š¾Š³Š¾Š²Ń‹Š¹ тип матрицы
  auto dtype = color ? CV_32FC3 : CV_32FC1;
  auto bg = color ? cv::Scalar(0, 0, 255) : cv::Scalar(255);
  auto col = color ? cv::Scalar(0, 255, 220) : cv::Scalar(0);
  cv::Mat img = cv::Mat(SIZE, SIZE, stype, bg);
  // ŠšŠ¾Š»ŠøŃ‡ŠµŃŃ‚Š²Š¾ линий
  size_t n = x.size();
  for (const auto& s: x) {
    // ŠšŠ¾Š»ŠøŃ‡ŠµŃŃ‚Š²Š¾ точек в линии
    size_t n_points = s.shape()[1];
    for (size_t i = 0; i < n_points - 1; ++i) {
      // Точка начала ŃˆŃ‚Ń€ŠøŃ…Š°
      cv::Point from(s(0, i), s(1, i));
      // Точка Š¾ŠŗŠ¾Š½Ń‡Š°Š½ŠøŃ ŃˆŃ‚Ń€ŠøŃ…Š°
      cv::Point to(s(0, i + 1), s(1, i + 1));
      // ŠžŃ‚Ń€ŠøŃŠ¾Š²ŠŗŠ° линии
      cv::line(img, from, to, col, LINE_WIDTH, LINE_TYPE);
    }
    if (color) {
      // ŠœŠµŠ½ŃŠµŠ¼ цвет линии
      col[0] += 180 / n;
    }
  }
  if (color) {
    // ŠœŠµŠ½ŃŠµŠ¼ цветовое преГставление на RGB
    cv::cvtColor(img, img, cv::COLOR_HSV2RGB);
  }
  // ŠœŠµŠ½ŃŠµŠ¼ формат ŠæŃ€ŠµŠ“ŃŃ‚Š°Š²Š»ŠµŠ½ŠøŃ на float32 с Гиапазоном [0, 1]
  img.convertTo(img, dtype, 1 / 255.0);
  return img;
}

// ŠžŠ±Ń€Š°Š±Š¾Ń‚ŠŗŠ° JSON Šø ŠæŠ¾Š»ŃƒŃ‡ŠµŠ½ŠøŠµ тензора с Ганными ŠøŠ·Š¾Š±Ń€Š°Š¶ŠµŠ½ŠøŃ
xtensor3d process(const std::string& x, double scale = 1.0, bool color = true) {
  auto p = parse_json(x);
  auto img = ocv_draw_lines(p, color);
  if (scale != 1) {
    cv::Mat out;
    cv::resize(img, out, cv::Size(), scale, scale, RESIZE_TYPE);
    cv::swap(img, out);
    out.release();
  }
  xtensor3d arr = color ? to_xt<double,3>(img) : to_xt<double,1>(img);
  return arr;
}

// [[Rcpp::export]]
rtensor3d cpp_process_json_str(const std::string& x, 
                               double scale = 1.0, 
                               bool color = true) {
  xtensor3d res = process(x, scale, color);
  return res;
}

// [[Rcpp::export]]
rtensor4d cpp_process_json_vector(const std::vector<std::string>& x, 
                                  double scale = 1.0, 
                                  bool color = false) {
  size_t n = x.size();
  size_t dim = floor(SIZE * scale);
  size_t channels = color ? 3 : 1;
  xtensor4d res({n, dim, dim, channels});
  parallelFor(0, n, [&x, &res, scale, color](int i) {
    xtensor3d tmp = process(x[i], scale, color);
    auto view = xt::view(res, i, xt::all(), xt::all(), xt::all());
    view = tmp;
  });
  return res;
}

Šis kods jāievieto failā src/cv_xt.cpp un jākompilē ar komandu Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); turklāt darbosies arī nlohmann/json.hpp no repositorijs. Kods sadalīts vairākās funkcijās:

  • to_xt — pielāgojuma funkcija, lai attēla matricas (cv::Mat) pārveidotu tensorā xt::xtensor;

  • parse_json — funkcija analizē JSON virkni, iegÅ«st koordinātas un iepako tās vektorā;

  • ocv_draw_lines — no iegÅ«tā punktu vektora zÄ«mē daudzkrāsainas lÄ«nijas;

  • process — apvieno iepriekÅ” aprakstÄ«tās funkcijas un pievieno iespēju iegÅ«tā attēla mērogoÅ”anai;

  • cpp_process_json_str — apvalks pār funkciju process, kas eksportē rezultātu R objektā (daudzdimensiju masÄ«vā);

  • cpp_process_json_vector — apvalks pār funkciju cpp_process_json_str, kas ļauj apstrādāt virkņu vektoru daudzprotokolā.

Daudzkrāsaino lÄ«niju zÄ«mēŔanai tika izmantots HSV krāsu modelis ar sekojoÅ”u pārvērÅ”anu RGB. PārbaudÄ«sim rezultātu:

arr <- cpp_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256   3
plot(magick::image_read(arr))

Quick Draw Doodle Recognition: kā sadraudzēties ar R, C++ un neironu tīkliem
SalīdzinoŔā ātruma izpilde uz R un C++

res_bench <- bench::mark(
  r_process_json_str(tmp_data[4, drawing], scale = 0.5),
  cpp_process_json_str(tmp_data[4, drawing], scale = 0.5),
  check = FALSE,
  min_iterations = 100
)
# Benchmark parameters
cols <- c("expression", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   expression                min     median       max `itr/sec` total_time  n_itr
#                                  
# 1 r_process_json_str     3.49ms     3.55ms    4.47ms      273.      490ms    134
# 2 cpp_process_json_str   1.94ms     2.02ms    5.32ms      489.      497ms    243

library(ggplot2)
# Measurement execution
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    .data <- tmp_data[sample(seq_len(.N), batch_size), drawing]
    bench::mark(
      r_process_json_vector(.data, scale = 0.5),
      cpp_process_json_vector(.data,  scale = 0.5),
      min_iterations = 50,
      check = FALSE
    )
  }
)

res_bench[, cols]

#    expression   batch_size      min   median      max `itr/sec` total_time n_itr
#                  <bch:tm> <bch:tm> <bch:tm>         <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: kā sadraudzēties ar R, C++ un neironu tīkliem

Kā redzam, ātruma pieaugums ir patieŔām ievērojams, un nav iespējams panākt C++ kodu, izmantojot R koda paralelizāciju.

3. Iteratori partiju izsaukÅ”anai no datubāzēm

R ir pelnÄ«jusi cieņu kā valoda datu apstrādei, kas ietilpst RAM, bet Python ir raksturÄ«gāka iteratÄ«vā datu apstrāde, kas ļauj viegli un dabiski Ä«stenot out-of-core aprēķinus (aprēķinus, kas izmanto ārējo atmiņu). Klasisks un mums aktuāls piemērs Ŕādiem aprēķiniem ir dziļās neironu tÄ«klu apmācÄ«ba, izmantojot gradienta nolaiÅ”anās metodi, apejot gradienta tuvinājumu katrā solÄ« uz nelielām novērojumu porcijām jeb mini-batch.

Dziļā mācīŔanās ietvari, kas uzrakstÄ«ti Python, satur Ä«paÅ”as klases, kas realizē datu iterātus: tabulas, attēlus mapēs, bināros formātos utt. Var izmantot gatavas variants vai arÄ« rakstÄ«t savus specifiskām uzdevumiem. R mēs varam izmantot visas Python bibliotēkas iespējas keras ar tās dažādajiem backend, izmantojot vienādas nosaukuma pakotni, kas darbojas virs pakotnes reticulate. Pēdējais pelnÄ«jis atseviŔķu lielu rakstu; tas ne tikai ļauj izpildÄ«t Python kodu no R, bet arÄ« nodroÅ”ina objektu pārsÅ«tīŔanu starp R un Python sesijām, automātiski veicot visus nepiecieÅ”amos tipa pārveidojumus.

Mums vairs nav jāglabā visi dati RAM, pateicoties MonetDBLite izmantoÅ”anai, visa "neironu tÄ«kla" darbÄ«ba tiks veikta ar oriÄ£inālo Python kodu, mums atliek tikai uzrakstÄ«t datu iterātoru, jo gatavu variantu Ŕādai situācijai nav ne R, ne Python. PrasÄ«bas tam faktiski ir divas: tam jāatgriež batch bezgalÄ«gā ciklā un jāsaglabā savs stāvoklis starp iterācijām (pēdējā R tiek vienkārÅ”i realizēta, izmantojot closures). Agrāk bija nepiecieÅ”ams iekŔējā iterātorā skaidri pārvērst R masÄ«vus numpy masÄ«vos, bet aktuālā pakotnes versija keras to dara pati.

Mēs iegÅ«stam Ŕādu iterātoru mācÄ«bu un validācijas datiem:

Mācību un validācijas datu iterātors

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 pieņem mainÄ«go ar savienojumu ar DB, izmantoto rindu numurus, klaÅ”u skaitu, batch izmēru, mērogu (scale = 1 atbilst attēlu zÄ«mēŔanai ar izmēru 256x256 pikseļi, scale = 0.5 - 128x128 pikseļi), krāsu indikators (color = FALSE nosaka zÄ«mēŔanu pelēkajos toņos, izmantojot krāsa = TRUE katrs vilciena posms tiek uzzÄ«mēts ar jaunu krāsu) un priekÅ”apstrādes indikators tÄ«klam, kas iepriekÅ” apmācÄ«ts ar imagenet. Pēdējais nepiecieÅ”ams, lai pārskalotu pikseļu vērtÄ«bas no intervāla [0, 1] uz intervālu [-1, 1], kas tika izmantots apmācÄ«bā. keras modeļiem.

Ārējā funkcija satur argumentu tipu pārbaudi, tabulu data.table ar nejauÅ”i sajauktiem rindu numuriem no samples_index un partiju numuriem, skaitÄ«tāju un maksimālo partiju skaitu, kā arÄ« SQL izteiksmi datu izgūŔanai no DB. Papildus mēs definējām ātru analogu funkcijai keras::to_categorical(). Mēs izmantojām gandrÄ«z visus datus apmācÄ«bai, atstājot pusprocentu validācijai, tāpēc epohas lielums tika ierobežots ar parametru steps_per_epoch aicinājuma laikā, tādēļ visi keras::fit_generator(), un nosacÄ«jums if (i > max_i) tika aktivizēts tikai validācijas iteratoram.

IekŔējā funkcijā notiek rindu indeksu atlasīŔana nākamajai partijai, ierakstu izgūŔana no DB ar partiju skaitÄ«tāja palielināŔanu, JSON parsēŔana (funkcija cpp_process_json_vector(), uzrakstÄ«ta C++) un attēlu atbilstoÅ”u masÄ«vu izveide. Tad tiek izveidoti one-hot vektori ar klases atzÄ«mēm, masÄ«vi ar pikseļu vērtÄ«bām un atzÄ«mes tiek apvienotas sarakstā, kas ir atgriezamā vērtÄ«ba. Lai paātrinātu darbu, tika izmantota indeksu izveide tabulās data.table un atsauces modificēŔana — bez Ŕīm "fīŔkām" pakete data.table uz vietas diezgan grÅ«ti iedomāties efektÄ«vu darbu ar ievērojamiem datu apjomiem R.

Veiktspējas mērÄ«jumu rezultāti uz klēpjdatora ar Core i5 izskatās Ŕādi:

Iteratora salīdzinoŔais tests

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

# Indeksi mācību paraugam
train_ind <- sample(ind, floor(length(ind) * 0.995))
# Indeksi pārbaudes paraugam
val_ind <- ind[-train_ind]
rm(ind)
# MērogoÅ”anas koeficients
scale <- 0.5

# MērīŔanas veikÅ”ana
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
    )
  }
)
# Benchmarks parametri
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: kā sadraudzēties ar R, C++ un neironu tīkliem

Ja ir pietiekami daudz RAM, datubāzes darbu var ievērojami paātrināt, pārvietojot to uz Å”o paÅ”u RAM (mÅ«su uzdevumam pietiek ar 32 GB). Linuxā pēc noklusējuma tiek montēts nodalÄ«jums /dev/shm, kas aizņem lÄ«dz puscieto disku apjoma. Var norādÄ«t arÄ« vairāk, rediģējot /etc/fstab, lai iegÅ«tu ierakstu formātā tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Obligāti restartējiet un pārbaudiet rezultātu, izpildot komandu df -h.

Iterators testēŔanas datiem izskatās daudz vienkārŔāk, jo testēŔanas datu kopums pilnÄ«bā ietilpst RAM:

Iterators testēŔanas datiem

test_generator <- function(dt,
                           batch_size = 32,
                           scale = 1,
                           color = FALSE,
                           imagenet_preproc = FALSE) {

  # ŠŸŃ€Š¾Š²ŠµŃ€ŠŗŠ° Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Š¾Š²
  checkmate::assert_data_table(dt)
  checkmate::assert_count(batch_size)
  checkmate::assert_number(scale, lower = 0.001, upper = 5)
  checkmate::assert_flag(color)
  checkmate::assert_flag(imagenet_preproc)

  # ŠŸŃ€Š¾ŃŃ‚Š°Š²Š»ŃŠµŠ¼ номера батчей
  dt[, batch := (.I - 1L) %/% batch_size + 1L]
  data.table::setkey(dt, batch)
  i <- 1
  max_i <- dt[, max(batch)]

  # Замыкание
  function() {
    batch_x <- cpp_process_json_vector(dt[batch == i, drawing], 
                                       scale = scale, color = color)
    if (imagenet_preproc) {
      # Шкалирование c интервала [0, 1] на интервал [-1, 1]
      batch_x <- (batch_x - 0.5) * 2
    }
    result <- list(batch_x)
    i <<- i + 1
    return(result)
  }
}

4. Modeļa arhitektūras izvēle

Pirmā no izmantotajām arhitektÅ«rām bija mobilenet v1, kuras Ä«patnÄ«bas ir izskatÄ«tas this ziņojumā. Tā klātesoÅ”ie standarta komplektā keras un, attiecÄ«gi, pieejams vienā un tajā paŔā paketē R. Taču, mēģinot to izmantot ar vienkanālu attēliem, izrādÄ«jās dÄ«vaini: ieejas tenzora izmēram vienmēr jābÅ«t formātā (batch, height, width, 3), tas nozÄ«mē, ka kanālu skaitu nevar mainÄ«t. Python Ŕādu ierobežojumu neuzliek, tāpēc mēs steigā uzrakstÄ«jām savu Ŕīs arhitektÅ«ras realizāciju, sekojot oriÄ£inālajam rakstam (bez izslēgÅ”anas, kas ir keras versijā):

Mobilenet v1 arhitektūra

library(keras)

top_3_categorical_accuracy <- custom_metric(
    name = "top_3_categorical_accuracy",
    metric_fn = function(y_true, y_pred) {
         metric_top_k_categorical_accuracy(y_true, y_pred, k = 3)
    }
)

layer_sep_conv_bn %
  layer_batch_normalization() %>% 
  layer_activation_relu() %>%
  layer_conv_2d(
    filters = filters * alpha,
    kernel_size = c(1, 1), 
    strides = c(1, 1)
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() 
}

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

  inputs <- layer_input(shape = input_shape)

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

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

    return(model)
}

Å Ä« pieeja acÄ«mredzami ir nepilnÄ«ga. Gribas pārbaudÄ«t daudz modeļu, bet negribas katru arhitektÅ«ru pārrakstÄ«t manuāli. Mēs arÄ« bijām izslēgti no iespējas izmantot modeļu svarus, kas jau iepriekÅ” apmācÄ«ti uz imagenet. Parasti palÄ«dz dokumentācijas izpēte. Funkcija get_config() ļauj iegÅ«t modeļa aprakstu rediģējamā formātā (base_model_conf$layers — parasts saraksts), bet funkcija from_config() veic atpakaļ pārveidoÅ”anu modelējamā objektā:

base_model_conf <- get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- from_config(base_model_conf)

Tagad nav grūti uzrakstīt universālu funkciju jebkuras no piegādātajām keras modeļiem ar apmācītiem imagenet svaru vai bez tiem:

Funkcija gatavo arhitektūru ielādei

get_model <- function(name = "mobilenet_v2",
                      input_shape = NULL,
                      weights = "imagenet",
                      pooling = "avg",
                      num_classes = NULL,
                      optimizer = keras::optimizer_adam(lr = 0.002),
                      loss = "categorical_crossentropy",
                      metrics = NULL,
                      color = TRUE,
                      compile = FALSE) {
  # Argumentu pārbaude
  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)

  # Iegūstam objektu no keras paketes
  model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
  # Pārbaudām objekta esamību paketē
  if (is.null(model_fun)) {
    stop("Model ", shQuote(name), " netika atrasts.", call. = FALSE)
  }

  base_model <- model_fun(
    input_shape = input_shape,
    include_top = FALSE,
    weights = weights,
    pooling = pooling
  )

  # Ja attēls nav krāsains, mainām ieejas dimensiju
  if (!color) {
    base_model_conf <- keras::get_config(base_model)
    base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
    base_model <- keras::from_config(base_model_conf)
  }

  predictions <- keras::get_layer(base_model, "global_average_pooling2d_1")$output
  predictions <- keras::layer_dense(predictions, units = num_classes, activation = "softmax")
  model <- keras::keras_model(
    inputs = base_model$input,
    outputs = predictions
  )

  if (compile) {
    keras::compile(
      object = model,
      optimizer = optimizer,
      loss = loss,
      metrics = metrics
    )
  }

  return(model)
}

Izmantojot vienkanāla attēlus, iepriekÅ” apmācÄ«tie svari netiek izmantoti. To varētu labot: izmantojot funkciju get_weights() saņemt modeļa svarus saraksta veidā no R masÄ«viem, mainÄ«t pirmā elementa dimensiju (ņemot kādu vienu krāsu kanālu vai vidējot visus trÄ«s), un pēc tam ielādēt svarus atpakaļ modelÄ« ar funkciju set_weights(). Mēs Å”o funkcionalitāti tā arÄ« neieviesām, jo Å”ajā posmā bija skaidrs, ka produktÄ«vāk ir strādāt ar krāsainiem attēliem.

Lielāko daļu eksperimentu mēs veicām, izmantojot mobilenet versijas 1 un 2, kā arÄ« resnet34. Å ajā sacensÄ«bā labi parādÄ«jās modernākas arhitektÅ«ras, piemēram, SE-ResNeXt. Diemžēl mÅ«su rÄ«cÄ«bā nebija gatavu realizāciju, bet mēs paÅ”reizējo izstrādāsim (bet noteikti izstrādāsim).

5. Skriptu parametru iestatīŔana

Lai atvieglotu darbu, viss kods apmācÄ«bas uzsākÅ”anai tika sakārtots vienā skriptā, ko parametrizē ar docopt Å”eit:

doc <- '
Usage:
  train_nn.R --help
  train_nn.R --list-models
  train_nn.R [options]

Options:
  -h --help                   RādÄ«t Å”o ziņojumu.
  -l --list-models            Saraksts ar pieejamajiem modeļiem.
  -m --model=          Neironu tīkla modeļa nosaukums [noklusējums: mobilenet_v2].
  -b --batch-size=      Partijas izmērs [noklusējums: 32].
  -s --scale-factor=   MērogoÅ”anas faktors [noklusējums: 0.5].
  -c --color                  Izmantot krāsainas līnijas [noklusējums: FALSE].
  -d --db-dir=          CeļŔ uz datu bāzes katalogu [noklusējums: Sys.getenv("db_dir")].
  -r --validate-ratio= ApstiprināŔanas paraugu attiecÄ«ba [noklusējums: 0.995].
  -n --n-gpu=         GPU skaits [noklusējums: 1].
'
args <- docopt::docopt(doc)

Pakete docopt ir realizācija http://docopt.org/ R valodā. Tā palÄ«dz palaist skriptus ar vienkārŔām komandām, piemēram, Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db vai ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, ja fails train_nn.R ir izpildāms (Ŕī komanda uzsāks modeļa apmācÄ«bu resnet50 trÄ«s kanālu attēlos ar izmēriem 128x128 pikseļu, datu bāze jānovieto mapē /home/andrey/doodle_db). Sarakstā var pievienot mācīŔanās ātrumu, optimizatora veidu un jebkurus citus konfigurējamos parametrus. SagatavoÅ”anās publikācijai noskaidrojās, ka arhitektÅ«ra mobilenet_v2 no aktuālās versijas keras R izmantoÅ”ana nav iespējams ir saistÄ«ta ar R pakotnē neiekļautām izmaiņām — gaidām, kamēr tās novērsÄ«s.

Å Ä« pieeja ļāva bÅ«tiski paātrināt eksperimentus ar dažādiem modeļiem salÄ«dzinājumā ar tradicionālo skriptu palaiÅ”anu RStudio (kā iespējamu alternatÄ«vu var minēt pakotni tfruns). Bet galvenā priekÅ”rocÄ«ba ir iespēja viegli pārvaldÄ«t skriptu izpildi Docker vai vienkārÅ”i uz servera, neinstalējot RStudio.

6. Skriptu Dockerizācija

Mēs izmantojām Docker, lai nodroÅ”inātu vides pārnesamÄ«bu modeļu apmācÄ«bai starp komandas locekļiem un ātru izvietoÅ”anu mākoņos. Sākt iepazīŔanos ar Å”o relativi neierasto instrumentu R programmētājam var ar Ŕīs publikāciju sēriju vai ar video kursu.

Docker ļauj gan izveidot savus attēlus "no nulles", gan izmantot citus attēlus kā pamatu saviem. Analizējot pieejamos variantus, mēs secinājām, ka NVIDIA, CUDA+cuDNN un Python bibliotēku instalēŔana ir diezgan apjomÄ«ga daļa no attēla, tāpēc nolēmām izmantot oficiālo attēlu tensorflow/tensorflow:1.12.0-gpu, pievienojot nepiecieÅ”amos R paketus.

Galīgais Docker fails izskatījās Ŕādi:

Dockerfile

FROM tensorflow/tensorflow:1.12.0-gpu

MAINTAINER Artem Klevtsov 

SHELL ["/bin/bash", "-c"]

ARG LOCALE="en_US.UTF-8"
ARG APT_PKG="libopencv-dev r-base r-base-dev littler"
ARG R_BIN_PKG="futile.logger checkmate data.table rcpp rapidjsonr dbi keras jsonlite curl digest remotes"
ARG R_SRC_PKG="xtensor RcppThread docopt MonetDBLite"
ARG PY_PIP_PKG="keras"
ARG DIRS="/db /app /app/data /app/models /app/logs"

RUN source /etc/os-release && 
    echo "deb https://cloud.r-project.org/bin/linux/ubuntu ${UBUNTU_CODENAME}-cran35/" > /etc/apt/sources.list.d/cran35.list && 
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E084DAB9 && 
    add-apt-repository -y ppa:marutter/c2d4u3.5 && 
    add-apt-repository -y ppa:timsc/opencv-3.4 && 
    apt-get update && 
    apt-get install -y locales && 
    locale-gen ${LOCALE} && 
    apt-get install -y --no-install-recommends ${APT_PKG} && 
    ln -s /usr/lib/R/site-library/littler/examples/install.r /usr/local/bin/install.r && 
    ln -s /usr/lib/R/site-library/littler/examples/install2.r /usr/local/bin/install2.r && 
    ln -s /usr/lib/R/site-library/littler/examples/installGithub.r /usr/local/bin/installGithub.r && 
    echo 'options(Ncpus = parallel::detectCores())' >> /etc/R/Rprofile.site && 
    echo 'options(repos = c(CRAN = "https://cloud.r-project.org"))' >> /etc/R/Rprofile.site && 
    apt-get install -y $(printf "r-cran-%s " ${R_BIN_PKG}) && 
    install.r ${R_SRC_PKG} && 
    pip install ${PY_PIP_PKG} && 
    mkdir -p ${DIRS} && 
    chmod 777 ${DIRS} && 
    rm -rf /tmp/downloaded_packages/ /tmp/*.rds && 
    rm -rf /var/lib/apt/lists/*

COPY utils /app/utils
COPY src /app/src
COPY tests /app/tests
COPY bin/*.R /app/

ENV DBDIR="/db"
ENV CUDA_HOME="/usr/local/cuda"
ENV PATH="/app:${PATH}"

WORKDIR /app

VOLUME /db
VOLUME /app

CMD bash

Lai atvieglotu lietoto pakotņu pārvaldÄ«bu, tās tika izdalÄ«tas mainÄ«gajos; galvenā daļa uzrakstÄ«tās skriptu tiek kopēta konteineru iekÅ”ienē to izveides laikā. Mēs arÄ« mainÄ«jām komandrindas vidi uz /bin/bash lai atvieglotu satura izmantoÅ”anu /etc/os-release. Tas novērsa nepiecieÅ”amÄ«bu norādÄ«t operētājsistēmas versiju kodā.

Papildu tika uzrakstÄ«ts neliels bash skripts, kas ļauj palaist konteineru ar dažādām komandām. Piemēram, tas varētu bÅ«t skripti neironu tÄ«klu apmācÄ«bai, kas iepriekÅ” tika ievietoti konteinerā, vai komandrindas vide, lai debugotu un uzraudzÄ«tu konteineru darbu:

Skripts konteineram palaiŔanai

#!/bin/sh

DBDIR=${PWD}/db
LOGSDIR=${PWD}/logs
MODELDIR=${PWD}/models
DATADIR=${PWD}/data
ARGS="--runtime=nvidia --rm -v ${DBDIR}:/db -v ${LOGSDIR}:/app/logs -v ${MODELDIR}:/app/models -v ${DATADIR}:/app/data"

if [ -z "$1" ]; then
    CMD="Rscript /app/train_nn.R"
elif [ "$1" = "bash" ]; then
    ARGS="${ARGS} -ti"
else
    CMD="Rscript /app/train_nn.R $@"
fi

docker run ${ARGS} doodles-tf ${CMD}

Ja, ja palaidzot Å”o bash skriptu bez parametriem, tiks izsaukts skripts ar noklusējuma vērtÄ«bām iekÅ” konteitera; ja pirmais pozicionālais arguments ir "bash", tad konteineris tiks palaists interaktÄ«vajā režīmā ar komandu apstrādātāju. Visos citos gadÄ«jumos notiek pozicionālo argumentu vērtÄ«bu aizvietoÅ”ana: train_nn.R CMD="Rscript /app/train_nn.R $@" Ir vērts pievērst uzmanÄ«bu, ka direktorijas ar izejas datiem un datu bāzi, kā arÄ« direktorija, kurā tiek glabātas apmācÄ«tās modeļi, tiek montētas iekÅ” konteitera no viesmaŔīnas, kas ļauj piekļūt skriptu darba rezultātiem bez liekām manipulācijām..

7. Daudzu GPU izmantoŔana Google Cloud

Viena no konkursa Ä«paŔībām bija diezgan trokŔņainie dati (skat. virsrakstu attēlu, ko aizguvusi no @Leigh.plt no ODS Slack). Ar lielu partiju palÄ«dzÄ«bu mēs izdevÄ«gi risinājām Å”o problēmu, un pēc eksperimentiem uz datora ar 1 GPU, nolēmām apgÅ«t modeļu apmācÄ«bu uz vairākiem GPU mākonÄ«. Izmantojām Google Cloud (

labs ceļvedis par pamatiem) sakarā ar plaÅ”o pieejamo konfigurāciju izvēli, pieņemamām cenām un bonusiem $300. No mantkārÄ«bas tika pasÅ«tÄ«ts instances ar 4xV100 ar SSD un daudz RAM, un tā bija liela kļūda. Nauda Ŕādas maŔīnas ātri iztērē, un eksperimentiem, bez labi izstrādāta cauruļu lÄ«nijas, var saprātÄ«gi tikt iztērēti. IzglÄ«tÄ«bas mērÄ·iem labāk ņemt K80. Bet liels RAM apjoms noderēja — mākoņa SSD neapvainoja ar ātrdarbÄ«bu, tādēļ datu bāzi pie katras instances palaiÅ”anas pārvietojām uzdev/shm Vislielāko interesi rada koda fragments, kas atbild par vairāku GPU izmantoÅ”anu. Vispirms modelis tiek izveidots CPU uz CPU, izmantojot konteksta menedžeri, tāpat kā Pythonā:.

with(tensorflow::tf$device("/cpu:0"), { model_cpu <- get_model( name = model_name, input_shape = input_shape, weights = weights, metrics =(top_3_categorical_accuracy, compile = FALSE ) })

Tad nekompilētais (tas ir svarīgi) modelis tiek kopēts uz norādīto pieejamo GPU skaitu, un tikai pēc tam tiek kompilēts:

model <- keras::multi_gpu_model(model_cpu, gpus = n_gpu) keras::compile( object = model, optimizer = keras::optimizer_adam(lr = 0.0004), loss = "categorical_crossentropy", metrics = c(top_3_categorical_accuracy) )

Klasisko pieeju ar visu slāņu sasaldēŔanu, izņemot pēdējo, pēdējā slāņa apmācÄ«bu, atsaldēŔanu un modeļa apmācīŔanu kopumā vairākiem GPU, realizēt neizdevās.

Apmācības procesu uzraudzījām bez

tensorboard tensorboard, ierobežojoties ar žurnālu ierakstiem un modeļu saglabāŔanu ar informatÄ«vām nosaukumiem pēc katras epochas:

Atgriezeniskās saites

# Шаблон имени файла лога
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. Aizvērums

Vairāku problēmu, ar kurām saskārāmies, risināŔana vēl nav izdevusies:

  • par keras nav gatavas funkcijas automātiskai optimālās mācÄ«bu ātruma meklēŔanai (analogs lr_finder bibliotēkā fast.ai); nedaudz piepÅ«loties, var pārvietot uz R treÅ”o puÅ”u realizācijas, piemēram, Å”o;
  • kā iepriekÅ” minētā punkta sekas, neizdevās atrast pareizo mācÄ«bu ātrumu, izmantojot vairākus GPU;
  • trÅ«kst mÅ«sdienu neironu tÄ«klu arhitektÅ«ru, it Ä«paÅ”i priekÅ”apmācÄ«tas uz imagenet;
  • nav one cycle policy un diskriminatÄ«vie mācÄ«bu ātrumi (cosine annealing pēc mÅ«su lÅ«guma bija realizēts, paldies skeydan).

Ko noderīgu izdevās gūt no Ŕī konkursa:

  • Uz salÄ«dzinoÅ”i vājām iekārtām var bez grÅ«tÄ«bām strādāt ar pietiekami ievērojamām (daudz lielākām par RAM izmēru) datu apjomiem. Pakete data.table ietaupa atmiņu, veicot in-place modifikāciju tabulām, kas ļauj izvairÄ«ties no to kopēŔanas, un, pareizi izmantojot tās iespējas, gandrÄ«z vienmēr demonstrē visaugstāko ātrumu starp visiem zināmajiem rÄ«kiem skriptu valodām. Datu saglabāŔana datu bāzē ļauj daudzās situācijās vispār nedomāt par nepiecieÅ”amÄ«bu iekļaut visu datu kopu RAM.
  • Lēnas funkcijas R var aizstāt ar ātrām C++ izmantojot paketi Rcpp. Ja papildus izmantosim RcppThread vai RcppParallel, iegÅ«stam multiplatformiskas daudzplÅ«smu realizācijas, tāpēc R lÄ«meņa kods nav jāparalē.
  • Paketē Rcpp var lietot bez nopietnām zināŔanām par C++, nepiecieÅ”amais minimums ir izklāstÄ«ts Å”eit. Virsrakstu faili vairākiem kruta C bibliotēkām, piemēram, xtensor ir pieejami CRAN, tas nozÄ«mē, ka tiek veidota infrastruktÅ«ra projektu Ä«stenoÅ”anai, kas integrē R gatavo augstas veiktspējas C++ kodu. Papildu ērtÄ«ba — sintakse krāsoÅ”ana un statiskais koda analizators C++ RStudio.
  • docopt ļauj izpildÄ«t patstāvÄ«gus skriptus ar parametriem. Tas ir ērti izmantot attālinātajos serveros, tostarp Docker. RStudio veikt daudzu stundu eksperimentus ar neironu tÄ«klu apmācÄ«bu nav ērti, un pati IDE uzstādīŔana serverÄ« ne vienmēr ir pamatota.
  • Docker nodroÅ”ina koda pārnesamÄ«bu un rezultātu reproducējamÄ«bu starp izstrādātājiem ar dažādām OS un bibliotēru versijām, kā arÄ« vienkārŔību palaiÅ”anai uz serveriem. Visa mācīŔanās procesa palaiÅ”ana iespējama ar vienu komandu.
  • Google Cloud ir budžeta veids, kā eksperimentēt ar dārgu aparatÅ«ru, bet ir jāizvēlas konfigurācijas pārdomāti.
  • Ä»oti noderÄ«gi ir izmērÄ«t atseviŔķu koda fragmentu veiktspēju, Ä«paÅ”i apvienojot R un C++, bet ar pakotni bench — vēl jo vairāk, tas ir ļoti viegli.

Kopumā Ŕī pieredze bija ļoti noderÄ«ga, un mēs turpinām strādāt pie dažām no izteiktajām problēmām.

Avots: habr.com

Iegādājieties uzticamu tÄ«mekļa hostingu ar DDoS aizsardzÄ«bu, VPS VDS serveriem šŸ”„ Iegādājieties uzticamu tÄ«mekļa hostingu ar DDoS aizsardzÄ«bu, VPS VDS serveriem | ProHoster