Quick Draw Doodle Recognition: kung paano makipagkaibigan sa R, C++ at neural network

Quick Draw Doodle Recognition: kung paano makipagkaibigan sa R, C++ at neural network

Hoy Habr!

Noong nakaraang taglagas, nag-host si Kaggle ng kumpetisyon upang pag-uri-uriin ang mga larawang iginuhit ng kamay, Quick Draw Doodle Recognition, kung saan, bukod sa iba pa, isang pangkat ng mga R-scientist ang nakibahagi: Artem Klevtsova, Tagapamahala ng Philippa ΠΈ Andrey Ogurtsov. Hindi namin ilalarawan nang detalyado ang kumpetisyon; nagawa na iyon sa kamakailang publikasyon.

Sa pagkakataong ito ay hindi ito gumana sa pagsasaka ng medalya, ngunit maraming mahalagang karanasan ang natamo, kaya gusto kong sabihin sa komunidad ang tungkol sa ilang pinakakawili-wili at kapaki-pakinabang na mga bagay sa Kagle at sa pang-araw-araw na gawain. Kabilang sa mga paksang tinalakay: mahirap na buhay kung wala OpenCV, JSON parsing (sinusuri ng mga halimbawang ito ang pagsasama ng C++ code sa mga script o package sa R ​​gamit ang Rcpp), parameterization ng mga script at dockerization ng panghuling solusyon. Ang lahat ng code mula sa mensahe sa isang form na angkop para sa pagpapatupad ay magagamit sa mga repositoryo.

Nilalaman:

  1. Mahusay na mag-load ng data mula sa CSV papunta sa MonetDB
  2. Paghahanda ng mga batch
  3. Mga iterator para sa pagbabawas ng mga batch mula sa database
  4. Pagpili ng Modelong Arkitektura
  5. Parameterization ng script
  6. Dockerization ng mga script
  7. Paggamit ng maraming GPU sa Google Cloud
  8. Sa halip ng isang konklusyon

1. Mahusay na mag-load ng data mula sa CSV sa MonetDB database

Ang data sa kompetisyong ito ay ibinibigay hindi sa anyo ng mga yari na larawan, ngunit sa anyo ng 340 CSV file (isang file para sa bawat klase) na naglalaman ng mga JSON na may mga point coordinates. Sa pamamagitan ng pagkonekta sa mga puntong ito sa mga linya, nakakakuha kami ng panghuling larawan na may sukat na 256x256 pixels. Para din sa bawat record ay may label na nagsasaad kung ang larawan ay nakilala nang tama ng classifier na ginamit sa oras na nakolekta ang dataset, isang dalawang-titik na code ng bansang tinitirhan ng may-akda ng larawan, isang natatanging identifier, isang timestamp at isang pangalan ng klase na tumutugma sa pangalan ng file. Ang isang pinasimpleng bersyon ng orihinal na data ay tumitimbang ng 7.4 GB sa archive at humigit-kumulang 20 GB pagkatapos i-unpack, ang buong data pagkatapos i-unpack ay tumatagal ng 240 GB. Tiniyak ng mga organizer na ang parehong mga bersyon ay muling ginawa ang parehong mga guhit, ibig sabihin ang buong bersyon ay kalabisan. Sa anumang kaso, ang pag-iimbak ng 50 milyong mga imahe sa mga graphic na file o sa anyo ng mga array ay agad na itinuturing na hindi kumikita, at nagpasya kaming pagsamahin ang lahat ng mga CSV file mula sa archive train_simplified.zip sa database na may kasunod na henerasyon ng mga larawan ng kinakailangang laki "on the fly" para sa bawat batch.

Ang isang mahusay na napatunayang sistema ay napili bilang DBMS MonetDB, ibig sabihin ay isang pagpapatupad para sa R ​​bilang isang pakete MonetDBLite. Kasama sa package ang isang naka-embed na bersyon ng database server at pinapayagan kang kunin ang server nang direkta mula sa isang R session at magtrabaho kasama nito doon. Ang paglikha ng isang database at pagkonekta dito ay isinasagawa gamit ang isang utos:

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

Kakailanganin naming gumawa ng dalawang talahanayan: isa para sa lahat ng data, ang isa para sa impormasyon ng serbisyo tungkol sa mga na-download na file (kapaki-pakinabang kung may mali at kailangang ipagpatuloy ang proseso pagkatapos mag-download ng ilang file):

Paglikha ng mga talahanayan

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

Ang pinakamabilis na paraan upang mai-load ang data sa database ay ang direktang pagkopya ng mga CSV file gamit ang SQL - command COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTSaan tablename - pangalan ng talahanayan at path - ang landas patungo sa file. Habang nagtatrabaho sa archive, natuklasan na ang built-in na pagpapatupad unzip sa R ay hindi gumagana nang tama sa isang bilang ng mga file mula sa archive, kaya ginamit namin ang system unzip (gamit ang parameter getOption("unzip")).

Function para sa pagsulat sa database

#' @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))
}

Kung kailangan mong ibahin ang anyo ng talahanayan bago isulat ito sa database, ito ay sapat na upang pumasa sa argumento preprocess function na magbabago ng data.

Code para sa sunud-sunod na paglo-load ng data sa database:

Pagsusulat ng data sa database

# Бписок Ρ„Π°ΠΉΠ»ΠΎΠ² для записи
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

Ang oras ng paglo-load ng data ay maaaring mag-iba depende sa mga katangian ng bilis ng drive na ginamit. Sa aming kaso, ang pagbabasa at pagsusulat sa loob ng isang SSD o mula sa isang flash drive (source file) patungo sa isang SSD (DB) ay tumatagal ng wala pang 10 minuto.

Tumatagal pa ng ilang segundo para gumawa ng column na may integer class label at index column (ORDERED INDEX) na may mga numero ng linya kung saan isasampol ang mga obserbasyon kapag gumagawa ng mga batch:

Paglikha ng Mga Karagdagang Column at Index

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

Upang malutas ang problema ng paglikha ng isang batch sa mabilisang, kailangan naming makamit ang pinakamataas na bilis ng pagkuha ng mga random na hilera mula sa talahanayan doodles. Para dito gumamit kami ng 3 trick. Ang una ay upang bawasan ang dimensionality ng uri na nag-iimbak ng observation ID. Sa orihinal na set ng data, ang uri na kinakailangan upang maiimbak ang ID ay bigint, ngunit ginagawang posible ng bilang ng mga obserbasyon na magkasya ang kanilang mga identifier, katumbas ng ordinal na numero, sa uri int. Ang paghahanap ay mas mabilis sa kasong ito. Ang pangalawang trick ay ang paggamit ORDERED INDEX β€” empirically nakarating kami sa desisyong ito, na napagdaanan ang lahat ng magagamit mga pagpipilian. Ang pangatlo ay gumamit ng mga parameterized na query. Ang kakanyahan ng pamamaraan ay upang maisagawa ang utos nang isang beses PREPARE na may kasunod na paggamit ng isang inihandang expression kapag lumilikha ng isang grupo ng mga query ng parehong uri, ngunit sa katunayan mayroong isang kalamangan kumpara sa isang simple SELECT lumabas na nasa saklaw ng statistical error.

Ang proseso ng pag-upload ng data ay gumagamit ng hindi hihigit sa 450 MB ng RAM. Iyon ay, binibigyang-daan ka ng inilarawang diskarte na ilipat ang mga dataset na tumitimbang ng sampu-sampung gigabytes sa halos anumang hardware na badyet, kabilang ang ilang mga single-board device, na medyo cool.

Ang natitira na lang ay sukatin ang bilis ng pagkuha (random) na data at suriin ang pag-scale kapag nagsa-sample ng mga batch na may iba't ibang laki:

Benchmark ng database

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: kung paano makipagkaibigan sa R, C++ at neural network

2. Paghahanda ng mga batch

Ang buong proseso ng paghahanda ng batch ay binubuo ng mga sumusunod na hakbang:

  1. Pag-parse ng ilang JSON na naglalaman ng mga vector ng mga string na may mga coordinate ng mga puntos.
  2. Pagguhit ng mga linyang may kulay batay sa mga coordinate ng mga punto sa isang imahe ng kinakailangang laki (halimbawa, 256Γ—256 o 128Γ—128).
  3. Pag-convert ng mga nagresultang larawan sa isang tensor.

Bilang bahagi ng kumpetisyon sa mga kernel ng Python, ang problema ay nalutas pangunahin gamit OpenCV. Ang isa sa pinakasimpleng at pinaka-halatang analogues sa R ​​ay magiging ganito:

Pagpapatupad ng JSON sa Tensor Conversion sa 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)
}

Ang pagguhit ay ginagawa gamit ang karaniwang R tool at nai-save sa isang pansamantalang PNG na nakaimbak sa RAM (sa Linux, ang mga pansamantalang R na direktoryo ay matatagpuan sa direktoryo /tmp, naka-mount sa RAM). Ang file na ito ay babasahin bilang isang three-dimensional array na may mga numerong mula 0 hanggang 1. Mahalaga ito dahil ang isang mas kumbensyonal na BMP ay mababasa sa isang raw array na may mga hex na color code.

Subukan natin ang resulta:

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: kung paano makipagkaibigan sa R, C++ at neural network

Ang batch mismo ay mabubuo tulad ng sumusunod:

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

Ang pagpapatupad na ito ay tila suboptimal sa amin, dahil ang pagbuo ng malalaking batch ay tumatagal ng napakatagal na panahon, at nagpasya kaming samantalahin ang karanasan ng aming mga kasamahan sa pamamagitan ng paggamit ng isang malakas na library OpenCV. Sa oras na iyon ay walang handa na pakete para sa R ​​(wala na ngayon), kaya isang minimal na pagpapatupad ng kinakailangang pag-andar ay isinulat sa C++ na may pagsasama sa R ​​code gamit ang Rcpp.

Upang malutas ang problema, ginamit ang mga sumusunod na pakete at aklatan:

  1. OpenCV para sa pagtatrabaho sa mga larawan at pagguhit ng mga linya. Gumamit ng mga paunang naka-install na library ng system at mga file ng header, pati na rin ang dynamic na pag-link.

  2. xtensor para sa pagtatrabaho sa mga multidimensional na array at tensor. Gumamit kami ng mga file ng header na kasama sa R ​​package na may parehong pangalan. Binibigyang-daan ka ng library na magtrabaho kasama ang mga multidimensional na array, pareho sa row major at column major order.

  3. ndjson para sa pag-parse ng JSON. Ang aklatan na ito ay ginagamit sa xtensor awtomatikong kung ito ay naroroon sa proyekto.

  4. RcppThread para sa pag-aayos ng multi-threaded na pagproseso ng isang vector mula sa JSON. Ginamit ang mga file ng header na ibinigay ng package na ito. Mula sa mas sikat RcppParallel Ang package, bukod sa iba pang mga bagay, ay may built-in na loop interrupt mechanism.

Dapat ito ay nabanggit na xtensor naging isang kaloob ng diyos: bilang karagdagan sa katotohanan na mayroon itong malawak na pag-andar at mataas na pagganap, ang mga developer nito ay naging medyo tumutugon at sumagot ng mga tanong kaagad at detalyado. Sa kanilang tulong, naging posible na ipatupad ang mga pagbabagong-anyo ng OpenCV matrice sa mga xtensor tensor, pati na rin ang isang paraan upang pagsamahin ang 3-dimensional na mga tensor ng imahe sa isang 4-dimensional na tensor ng tamang dimensyon (ang batch mismo).

Mga materyales para sa pag-aaral ng Rcpp, xtensor at 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

Upang mag-compile ng mga file na gumagamit ng mga system file at dynamic na pag-link sa mga library na naka-install sa system, ginamit namin ang mekanismo ng plugin na ipinatupad sa package Rcpp. Upang awtomatikong mahanap ang mga landas at flag, gumamit kami ng isang sikat na utility ng Linux pkg-config.

Pagpapatupad ng Rcpp plugin para sa paggamit ng OpenCV library

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

Bilang resulta ng pagpapatakbo ng plugin, ang mga sumusunod na halaga ay papalitan sa panahon ng proseso ng compilation:

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"

Ang code ng pagpapatupad para sa pag-parse ng JSON at pagbuo ng isang batch para sa paghahatid sa modelo ay ibinibigay sa ilalim ng spoiler. Una, magdagdag ng lokal na direktoryo ng proyekto upang maghanap ng mga file ng header (kinakailangan para sa ndjson):

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

Pagpapatupad ng JSON sa tensor conversion sa 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;
}

Ang code na ito ay dapat ilagay sa file src/cv_xt.cpp at isama ang utos Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); kailangan din para sa trabaho nlohmann/json.hpp ng imbakan. Ang code ay nahahati sa ilang mga function:

  • to_xt β€” isang templated function para sa pagbabago ng isang image matrix (cv::Mat) sa isang tensor xt::xtensor;

  • parse_json β€” ang function ay nag-parse ng isang string ng JSON, kinukuha ang mga coordinate ng mga puntos, i-pack ang mga ito sa isang vector;

  • ocv_draw_lines β€” mula sa nagresultang vector ng mga puntos, gumuhit ng maraming kulay na mga linya;

  • process β€” pinagsasama ang mga pag-andar sa itaas at nagdaragdag din ng kakayahang sukatin ang resultang imahe;

  • cpp_process_json_str - wrapper sa ibabaw ng function process, na nag-e-export ng resulta sa isang R-object (multidimensional array);

  • cpp_process_json_vector - wrapper sa ibabaw ng function cpp_process_json_str, na nagbibigay-daan sa iyong iproseso ang isang string vector sa multi-threaded mode.

Upang gumuhit ng maraming kulay na mga linya, ginamit ang modelo ng kulay ng HSV, na sinusundan ng conversion sa RGB. Subukan natin ang resulta:

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

Quick Draw Doodle Recognition: kung paano makipagkaibigan sa R, C++ at neural network
Paghahambing ng bilis ng mga pagpapatupad sa R ​​at 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: kung paano makipagkaibigan sa R, C++ at neural network

Tulad ng nakikita mo, ang pagtaas ng bilis ay naging napaka makabuluhan, at hindi posible na abutin ang C++ code sa pamamagitan ng pagpaparis ng R code.

3. Mga iterator para sa pagbabawas ng mga batch mula sa database

Ang R ay may mahusay na karapat-dapat na reputasyon para sa pagproseso ng data na akma sa RAM, habang ang Python ay higit na nailalarawan sa pamamagitan ng umuulit na pagproseso ng data, na nagbibigay-daan sa iyong madali at natural na magpatupad ng mga out-of-core na kalkulasyon (mga kalkulasyon gamit ang panlabas na memorya). Ang isang klasiko at may-katuturang halimbawa para sa amin sa konteksto ng inilarawang problema ay ang mga malalim na neural network na sinanay ng gradient descent method na may approximation ng gradient sa bawat hakbang gamit ang isang maliit na bahagi ng mga obserbasyon, o mini-batch.

Ang mga deep learning framework na nakasulat sa Python ay may mga espesyal na klase na nagpapatupad ng mga iterator batay sa data: mga talahanayan, mga larawan sa mga folder, binary na format, atbp. Maaari kang gumamit ng mga handa na opsyon o magsulat ng sarili mo para sa mga partikular na gawain. Sa R maaari nating samantalahin ang lahat ng mga tampok ng library ng Python matigas kasama ang iba't ibang backend nito gamit ang package na may parehong pangalan, na gumagana naman sa ibabaw ng package bigkasin. Ang huli ay nararapat sa isang hiwalay na mahabang artikulo; ito ay hindi lamang nagbibigay-daan sa iyo upang patakbuhin ang Python code mula sa R, ngunit pinapayagan ka ring maglipat ng mga bagay sa pagitan ng R at Python session, awtomatikong gumaganap ng lahat ng kinakailangang uri ng mga conversion.

Inalis namin ang pangangailangan na mag-imbak ng lahat ng data sa RAM sa pamamagitan ng paggamit ng MonetDBlite, lahat ng gawaing "neural network" ay isasagawa ng orihinal na code sa Python, kailangan lang naming magsulat ng isang iterator sa data, dahil walang handa para sa ganoong sitwasyon sa alinman sa R ​​o Python. Mayroong dalawang mga kinakailangan lamang para dito: dapat itong ibalik ang mga batch sa isang walang katapusang loop at i-save ang estado nito sa pagitan ng mga pag-ulit (ang huli sa R ​​ay ipinatupad sa pinakasimpleng paraan gamit ang mga pagsasara). Noong nakaraan, kinakailangan na tahasang i-convert ang mga R arrays sa mga numpy array sa loob ng iterator, ngunit ang kasalukuyang bersyon ng package matigas ginagawa niya mismo.

Ang iterator para sa data ng pagsasanay at pagpapatunay ay naging ang mga sumusunod:

Iterator para sa data ng pagsasanay at pagpapatunay

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

Kinukuha ng function bilang input ang isang variable na may koneksyon sa database, ang mga bilang ng mga linyang ginamit, ang bilang ng mga klase, laki ng batch, sukat (scale = 1 tumutugma sa pag-render ng mga larawan ng 256x256 pixels, scale = 0.5 β€” 128x128 pixels), tagapagpahiwatig ng kulay (color = FALSE tumutukoy sa pag-render sa grayscale kapag ginamit color = TRUE bawat stroke ay iginuhit sa isang bagong kulay) at isang preprocessing indicator para sa mga network na pre-trained sa imagenet. Ang huli ay kinakailangan upang masukat ang mga halaga ng pixel mula sa pagitan [0, 1] hanggang sa pagitan [-1, 1], na ginamit kapag sinasanay ang ibinigay matigas mga modelo.

Ang panlabas na function ay naglalaman ng pagsusuri ng uri ng argumento, isang talahanayan data.table na may random na pinaghalong mga numero ng linya mula sa samples_index at mga batch number, counter at maximum na bilang ng mga batch, pati na rin ang SQL expression para sa pag-unload ng data mula sa database. Bilang karagdagan, tinukoy namin ang isang mabilis na analogue ng function sa loob keras::to_categorical(). Ginamit namin ang halos lahat ng data para sa pagsasanay, na nag-iiwan ng kalahating porsyento para sa pagpapatunay, kaya ang sukat ng panahon ay limitado ng parameter steps_per_epoch kapag tinawag keras::fit_generator(), at ang kondisyon if (i > max_i) gumana lang para sa validation iterator.

Sa panloob na function, kinukuha ang mga row index para sa susunod na batch, ang mga record ay dini-load mula sa database na may pagtaas ng batch counter, JSON parsing (function cpp_process_json_vector(), nakasulat sa C++) at paglikha ng mga arrays na naaayon sa mga larawan. Pagkatapos ay nilikha ang mga one-hot vector na may mga label ng klase, ang mga array na may mga halaga ng pixel at mga label ay pinagsama sa isang listahan, na siyang halaga ng pagbabalik. Upang mapabilis ang trabaho, ginamit namin ang paglikha ng mga index sa mga talahanayan data.table at pagbabago sa pamamagitan ng link - nang wala itong mga package na "chips" talaan ng mga impormasyon Medyo mahirap isipin na gumagana nang epektibo sa anumang makabuluhang halaga ng data sa R.

Ang mga resulta ng mga pagsukat ng bilis sa isang Core i5 laptop ay ang mga sumusunod:

Ang benchmark ng 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: kung paano makipagkaibigan sa R, C++ at neural network

Kung mayroon kang sapat na dami ng RAM, maaari mong seryosong pabilisin ang pagpapatakbo ng database sa pamamagitan ng paglilipat nito sa parehong RAM (sapat na ang 32 GB para sa aming gawain). Sa Linux, ang partition ay naka-mount bilang default /dev/shm, na sumasakop ng hanggang kalahati ng kapasidad ng RAM. Maaari kang mag-highlight ng higit pa sa pamamagitan ng pag-edit /etc/fstabpara makakuha ng record like tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Tiyaking i-reboot at suriin ang resulta sa pamamagitan ng pagpapatakbo ng command df -h.

Ang iterator para sa data ng pagsubok ay mukhang mas simple, dahil ang test dataset ay ganap na umaangkop sa RAM:

Iterator para sa data ng pagsubok

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. Pagpili ng arkitektura ng modelo

Ang unang arkitektura na ginamit ay mobilenet v1, ang mga tampok nito ay tinalakay sa Ito mensahe. Ito ay kasama bilang pamantayan matigas at, nang naaayon, ay magagamit sa pakete ng parehong pangalan para sa R. Ngunit kapag sinusubukang gamitin ito sa mga single-channel na imahe, isang kakaibang bagay ang lumabas: ang input tensor ay dapat palaging may sukat (batch, height, width, 3), ibig sabihin, hindi mababago ang bilang ng mga channel. Walang ganoong limitasyon sa Python, kaya nagmadali kami at nagsulat ng sarili naming pagpapatupad ng arkitektura na ito, kasunod ng orihinal na artikulo (nang walang dropout na nasa hard version):

Arkitektura ng 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)
}

Ang mga disadvantages ng diskarteng ito ay halata. Gusto kong subukan ang maraming mga modelo, ngunit sa kabaligtaran, hindi ko nais na muling isulat nang manu-mano ang bawat arkitektura. Pinagkaitan din kami ng pagkakataong gamitin ang mga timbang ng mga modelong pre-trained sa imagenet. Gaya ng dati, nakatulong ang pag-aaral sa dokumentasyon. Function get_config() nagbibigay-daan sa iyo na makakuha ng paglalarawan ng modelo sa isang form na angkop para sa pag-edit (base_model_conf$layers - isang regular na listahan ng R), at ang function from_config() nagsasagawa ng reverse conversion sa isang modelong object:

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)

Ngayon hindi mahirap magsulat ng isang unibersal na function upang makuha ang alinman sa mga ibinigay matigas mga modelong mayroon o walang mga timbang na sinanay sa imagenet:

Function para sa pag-load ng mga yari na arkitektura

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

Kapag gumagamit ng mga single-channel na imahe, walang paunang sinanay na timbang ang ginagamit. Ito ay maaaring maayos: gamit ang function get_weights() kunin ang mga timbang ng modelo sa anyo ng isang listahan ng mga R array, baguhin ang dimensyon ng unang elemento ng listahang ito (sa pamamagitan ng pagkuha ng isang color channel o pag-average sa lahat ng tatlo), at pagkatapos ay i-load ang mga timbang pabalik sa modelo gamit ang function set_weights(). Hindi namin idinagdag ang pagpapaandar na ito, dahil sa yugtong ito ay malinaw na na mas produktibo ang pagtatrabaho sa mga larawang may kulay.

Isinagawa namin ang karamihan sa mga eksperimento gamit ang mobilenet na bersyon 1 at 2, pati na rin ang resnet34. Mas mahusay na gumanap ang mas modernong arkitektura gaya ng SE-ResNeXt sa kompetisyong ito. Sa kasamaang palad, wala kaming handa na mga pagpapatupad sa aming pagtatapon, at hindi kami sumulat ng aming sarili (ngunit tiyak na magsusulat kami).

5. Parameterization ng mga script

Para sa kaginhawahan, ang lahat ng code para sa pagsisimula ng pagsasanay ay idinisenyo bilang isang solong script, na naka-parameter gamit docopt tulad ng sumusunod:

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)

Package docopt kumakatawan sa pagpapatupad http://docopt.org/ para sa R. Sa tulong nito, ang mga script ay inilunsad gamit ang mga simpleng utos tulad ng Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db o ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, kung file train_nn.R ay maipapatupad (ang utos na ito ay magsisimulang magsanay sa modelo resnet50 sa tatlong-kulay na mga imahe na may sukat na 128x128 pixels, ang database ay dapat na matatagpuan sa folder /home/andrey/doodle_db). Maaari kang magdagdag ng bilis ng pag-aaral, uri ng optimizer, at anumang iba pang nako-customize na parameter sa listahan. Sa proseso ng paghahanda ng publikasyon, lumabas na ang arkitektura mobilenet_v2 mula sa kasalukuyang bersyon matigas sa paggamit ng R hindi pwede dahil sa mga pagbabagong hindi isinasaalang-alang sa R ​​package, hinihintay namin silang ayusin ito.

Ang diskarte na ito ay naging posible upang makabuluhang mapabilis ang mga eksperimento na may iba't ibang mga modelo kumpara sa mas tradisyonal na paglulunsad ng mga script sa RStudio (napansin namin ang package bilang isang posibleng alternatibo tfruns). Ngunit ang pangunahing bentahe ay ang kakayahang madaling pamahalaan ang paglulunsad ng mga script sa Docker o sa server lamang, nang hindi nag-i-install ng RStudio para dito.

6. Dockerization ng mga script

Ginamit namin ang Docker para matiyak ang portability ng environment para sa mga modelo ng pagsasanay sa pagitan ng mga miyembro ng team at para sa mabilis na pag-deploy sa cloud. Maaari kang magsimulang maging pamilyar sa tool na ito, na medyo hindi karaniwan para sa isang R programmer, na may ito serye ng mga publikasyon o kursong video.

Pinapayagan ka ng Docker na parehong lumikha ng iyong sariling mga imahe mula sa simula at gumamit ng iba pang mga imahe bilang batayan para sa paglikha ng iyong sarili. Kapag pinag-aaralan ang mga magagamit na opsyon, napagpasyahan namin na ang pag-install ng NVIDIA, CUDA+cuDNN drivers at Python library ay isang medyo malaking bahagi ng imahe, at nagpasya kaming kunin ang opisyal na imahe bilang batayan. tensorflow/tensorflow:1.12.0-gpu, pagdaragdag ng mga kinakailangang R package doon.

Ang huling docker file ay ganito ang hitsura:

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

Para sa kaginhawahan, ang mga pakete na ginamit ay inilagay sa mga variable; ang karamihan sa mga nakasulat na script ay kinopya sa loob ng mga lalagyan sa panahon ng pagpupulong. Binago din namin ang command shell sa /bin/bash para sa kadalian ng paggamit ng nilalaman /etc/os-release. Iniiwasan nito ang pangangailangang tukuyin ang bersyon ng OS sa code.

Bilang karagdagan, isang maliit na script ng bash ang isinulat na nagbibigay-daan sa iyo upang maglunsad ng isang lalagyan na may iba't ibang mga utos. Halimbawa, ang mga ito ay maaaring mga script para sa pagsasanay ng mga neural network na dating inilagay sa loob ng container, o isang command shell para sa pag-debug at pagsubaybay sa pagpapatakbo ng container:

Script upang ilunsad ang lalagyan

#!/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}

Kung ang bash script na ito ay tatakbo nang walang mga parameter, ang script ay tatawagin sa loob ng container train_nn.R na may mga default na halaga; kung ang unang positional na argumento ay "bash", kung gayon ang lalagyan ay magsisimulang interactive sa isang command shell. Sa lahat ng iba pang mga kaso, ang mga halaga ng mga positional na argumento ay pinapalitan: CMD="Rscript /app/train_nn.R $@".

Kapansin-pansin na ang mga direktoryo na may mapagkukunan ng data at database, pati na rin ang direktoryo para sa pag-save ng mga sinanay na modelo, ay naka-mount sa loob ng lalagyan mula sa host system, na nagbibigay-daan sa iyo upang ma-access ang mga resulta ng mga script nang walang mga hindi kinakailangang manipulasyon.

7. Paggamit ng maraming GPU sa Google Cloud

Isa sa mga tampok ng kumpetisyon ay ang napakaingay na data (tingnan ang pamagat na larawan, na hiniram mula kay @Leigh.plt mula sa ODS slack). Nakakatulong ang malalaking batch na labanan ito, at pagkatapos ng mga eksperimento sa isang PC na may 1 GPU, nagpasya kaming mag-master ng mga modelo ng pagsasanay sa ilang GPU sa cloud. Gumamit ng GoogleCloud (magandang gabay sa mga pangunahing kaalaman) dahil sa malaking seleksyon ng mga available na configuration, makatwirang presyo at $300 na bonus. Dahil sa kasakiman, nag-order ako ng 4xV100 instance na may SSD at isang toneladang RAM, at iyon ay isang malaking pagkakamali. Ang ganitong makina ay kumakain ng pera nang mabilis; maaari kang masira ang pag-eksperimento nang walang isang napatunayang pipeline. Para sa mga layuning pang-edukasyon, mas mainam na kunin ang K80. Ngunit ang malaking halaga ng RAM ay madaling gamitin - ang cloud SSD ay hindi humanga sa pagganap nito, kaya ang database ay inilipat sa dev/shm.

Ang pinaka-interesante ay ang code fragment na responsable para sa paggamit ng maraming GPU. Una, ang modelo ay nilikha sa CPU gamit ang isang tagapamahala ng konteksto, tulad ng sa 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
  )
})

Pagkatapos ay ang hindi pinagsama-sama (ito ay mahalaga) na modelo ay kinopya sa isang naibigay na bilang ng mga magagamit na GPU, at pagkatapos lamang na ito ay pinagsama-sama:

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

Ang klasikong pamamaraan ng pagyeyelo ng lahat ng mga layer maliban sa huling isa, pagsasanay sa huling layer, pag-unfreeze at muling pagsasanay sa buong modelo para sa ilang mga GPU ay hindi maipatupad.

Ang pagsasanay ay sinusubaybayan nang walang paggamit. tensorboard, nililimitahan ang ating sarili sa pag-record ng mga log at pag-save ng mga modelo na may mga pangalang nagbibigay-kaalaman pagkatapos ng bawat panahon:

Mga callback

# Π¨Π°Π±Π»ΠΎΠ½ ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π° Π»ΠΎΠ³Π°
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. Sa halip na isang konklusyon

Ang ilang mga problema na nakatagpo namin ay hindi pa nagtagumpay:

  • Π² matigas walang handa na function para sa awtomatikong paghahanap para sa pinakamainam na rate ng pag-aaral (analogue lr_finder sa library mabilis.ai); Sa ilang pagsisikap, posibleng i-port ang mga pagpapatupad ng third-party sa R, halimbawa, ito;
  • bilang resulta ng nakaraang punto, hindi posible na piliin ang tamang bilis ng pagsasanay kapag gumagamit ng ilang mga GPU;
  • may kakulangan ng mga modernong arkitektura ng neural network, lalo na ang mga pre-trained sa imagenet;
  • walang one cycle policy at discriminative learning rate (cosine annealing ay sa aming kahilingan ipinatupad, salamat skeydan).

Anong mga kapaki-pakinabang na bagay ang natutunan mula sa kompetisyong ito:

  • Sa medyo mababang lakas na hardware, maaari kang magtrabaho nang may disenteng (maraming beses ang laki ng RAM) na dami ng data nang walang sakit. Plastik na bag talaan ng mga impormasyon nakakatipid ng memorya dahil sa in-place na pagbabago ng mga talahanayan, na umiiwas sa pagkopya sa mga ito, at kapag ginamit nang tama, ang mga kakayahan nito ay halos palaging nagpapakita ng pinakamataas na bilis sa lahat ng mga tool na kilala sa amin para sa mga wika ng script. Ang pag-save ng data sa isang database ay nagbibigay-daan sa iyo, sa maraming mga kaso, na huwag isipin ang lahat tungkol sa pangangailangan na i-squeeze ang buong dataset sa RAM.
  • Ang mga mabagal na function sa R ​​ay maaaring mapalitan ng mabilis sa C++ gamit ang package Rcpp. Kung bukod sa paggamit RcppThread o RcppParallel, nakakakuha kami ng mga cross-platform na multi-threaded na pagpapatupad, kaya hindi na kailangang iparallelize ang code sa R ​​level.
  • Package Rcpp maaaring gamitin nang walang seryosong kaalaman sa C++, ang kinakailangang minimum ay nakabalangkas dito. Header file para sa isang bilang ng mga cool na C-library tulad ng xtensor magagamit sa CRAN, iyon ay, isang imprastraktura ay nabuo para sa pagpapatupad ng mga proyekto na nagsasama ng handa na mataas na pagganap na C++ code sa R. Ang karagdagang kaginhawahan ay ang pag-highlight ng syntax at isang static na C++ code analyzer sa RStudio.
  • docopt nagbibigay-daan sa iyong magpatakbo ng mga self-contained na script na may mga parameter. Ito ay maginhawa para sa paggamit sa isang malayong server, kasama. sa ilalim ng pantalan. Sa RStudio, hindi maginhawang magsagawa ng maraming oras ng mga eksperimento sa pagsasanay ng mga neural network, at ang pag-install ng IDE sa server mismo ay hindi palaging makatwiran.
  • Tinitiyak ng Docker ang code portability at reproducibility ng mga resulta sa pagitan ng mga developer na may iba't ibang bersyon ng OS at mga library, pati na rin ang kadalian ng pagpapatupad sa mga server. Maaari mong ilunsad ang buong pipeline ng pagsasanay sa isang utos lamang.
  • Ang Google Cloud ay isang paraan ng badyet para mag-eksperimento sa mamahaling hardware, ngunit kailangan mong maingat na pumili ng mga configuration.
  • Ang pagsukat ng bilis ng mga indibidwal na fragment ng code ay lubhang kapaki-pakinabang, lalo na kapag pinagsama ang R at C++, at kasama ang package hukuman - napakadali din.

Sa pangkalahatan, napakahusay ng karanasang ito at patuloy kaming nagsusumikap upang malutas ang ilan sa mga isyung iniharap.

Pinagmulan: www.habr.com

Magdagdag ng komento