Quick Draw Doodle Recognition: nola egin lagun R, C++ eta neurona-sareekin

Quick Draw Doodle Recognition: nola egin lagun R, C++ eta neurona-sareekin

Aupa Habr!

Joan den udazkenean, Kagglek eskuz egindako irudiak sailkatzeko lehiaketa bat antolatu zuen, Quick Draw Doodle Recognition, eta bertan, besteak beste, R-zientzialari talde batek parte hartu zuen: Artem Klevtsova, Philippa zuzendaria ΠΈ Andrey Ogurtsov. Ez dugu lehiaketa zehatz-mehatz deskribatuko; hori dagoeneko egin da azken argitalpena.

Oraingo honetan ez zen medailen laborantzarekin funtzionatu, baina esperientzia baliotsu asko lortu zen, beraz, komunitateari Kagleri buruzko eta eguneroko lanean gauza interesgarri eta erabilgarrienen berri eman nahiko nioke. Jorratutako gaien artean: bizitza zaila gabe OpenCV, JSON analizatzea (adibide hauek R-ko script edo paketeetan C++ kodearen integrazioa aztertzen dute erabiliz Rcpp), scripten parametrizazioa eta azken irtenbidearen dockerizazioa. Mezuaren kode guztia exekutatzeko modu egokian dago eskuragarri biltegiak.

Edukia:

  1. Kargatu eraginkortasunez CSVko datuak MonetDB-ra
  2. Loteak prestatzea
  3. Datu-basetik loteak deskargatzeko itertatzaileak
  4. Arkitektura eredu bat hautatzea
  5. Script-en parametrizazioa
  6. Gidoien dockerizazioa
  7. Google Cloud-en hainbat GPU erabiltzea
  8. Horren ordez Ondorio baten

1. Eraginkortasunez kargatu CSVko datuak MonetDB datu-basera

Lehiaketa honetako datuak ez dira prest egindako irudien moduan ematen, 340 CSV fitxategi moduan baizik (fitxategi bat klase bakoitzeko) puntu koordenatuak dituzten JSONak dituztenak. Puntu hauek lerroekin lotuz, 256x256 pixeleko azken irudia lortuko dugu. Era berean, erregistro bakoitzeko etiketa bat dago, datu-multzoa bildu zen unean erabilitako sailkatzaileak irudia behar bezala ezagutu ote zuen adierazten duena, argazkiaren egilearen bizilekuaren bi hizkiko kodea, identifikatzaile esklusiboa, denbora-zigilua. eta fitxategi-izenarekin bat datorren klase-izena. Jatorrizko datuen bertsio sinplifikatuak 7.4 GB pisatzen ditu artxiboan eta 20 GB gutxi gorabehera deskonprimitu ondoren, datu osoak 240 GB hartzen ditu. Antolatzaileek ziurtatu zuten bi bertsioek marrazki berberak erreproduzitzen zituztela, hau da, bertsio osoa soberan zegoen. Nolanahi ere, 50 milioi irudi fitxategi grafikoetan edo array moduan gordetzea berehala errentagarritzat jo zen, eta artxibotik CSV fitxategi guztiak batzea erabaki genuen. train_sinplified.zip datu-basean lote bakoitzerako behar den tamainako irudiak sortuz gero "hegan".

Frogatutako sistema bat aukeratu zen DBMS gisa MonetDB, hots, R-ren inplementazioa pakete gisa MonetDBLite. Paketeak datu-basearen zerbitzariaren bertsio txertatua dakar eta zerbitzaria zuzenean R saio batetik jaso eta bertan lan egiteko aukera ematen du. Datu-base bat sortzea eta harekin konektatzea komando batekin egiten dira:

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

Bi taula sortu beharko ditugu: bata datu guztientzat, bestea, deskargatutako fitxategiei buruzko zerbitzu-informazioari buruzkoa (baliagarria zerbait gaizki gertatzen bada eta prozesuari berriro ekin behar zaio hainbat fitxategi deskargatu ondoren):

Taulak sortzea

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

Datu-basean datuak kargatzeko modurik azkarrena CSV fitxategiak zuzenean kopiatzea zen SQL - komandoa erabiliz COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTNon tablename - taularen izena eta path - fitxategiaren bidea. Artxiboarekin lan egiten ari zen bitartean, integratutako ezarpena aurkitu zen unzip R-n ez dabil behar bezala artxiboko hainbat fitxategirekin, beraz, sistema erabili dugu unzip (parametroa erabiliz getOption("unzip")).

Datu-basean idazteko funtzioa

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

Datu-basean idatzi aurretik taula eraldatu behar baduzu, nahikoa da argumentua pasatzea preprocess datuak eraldatuko dituen funtzioa.

Datu-basean sekuentzialki kargatzeko kodea:

Datu-basean datuak idaztea

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

Datuak kargatzeko denbora alda daiteke erabilitako unitatearen abiadura-ezaugarrien arabera. Gure kasuan, SSD batean edo flash drive batetik (iturburu-fitxategi) SSD batera (DB) irakurtzea eta idaztea 10 minutu baino gutxiago behar dira.

Segundo batzuk gehiago behar dira klase osoko etiketa eta indize-zutabea dituen zutabe bat sortzeko (ORDERED INDEX) loteak sortzerakoan behaketak laginduko diren lerro-zenbakiekin:

Zutabe eta Indize gehigarriak sortzea

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

Lote bat hegan sortzeko arazoa konpontzeko, mahaitik ausazko errenkadak ateratzeko abiadura maximoa lortu behar genuen. doodles. Horretarako 3 trikimailu erabili ditugu. Lehenengoa behaketa IDa gordetzen duen motaren dimentsioa murriztea izan zen. Jatorrizko datu multzoan, IDa gordetzeko behar den mota da bigint, baina behaketa kopuruari esker, haien identifikatzaileak, zenbaki ordinalaren berdinak, motara egokitzea ahalbidetzen du. int. Bilaketa askoz azkarragoa da kasu honetan. Bigarren trikimailua erabiltzea zen ORDERED INDEX β€” Erabaki hau enpirikoki heldu ginen, eskuragarri dauden guztiak aztertuta aukera. Hirugarrena parametrizatutako kontsultak erabiltzea zen. Metodoaren funtsa komandoa behin exekutatzen da PREPARE Mota bereko kontsulta sorta bat sortzerakoan prestatutako esamolde bat erabili ondoren, baina, egia esan, abantaila bat dago sinple batekin alderatuta SELECT akats estatistikoaren barnean egon da.

Datuak kargatzeko prozesuak ez du 450 MB RAM baino gehiago kontsumitzen. Hau da, deskribatutako ikuspegiari esker, hamar gigabyte pisatzen dituzten datu multzoak mugi ditzakezu aurrekontuko ia edozein hardwaretan, plaka bakarreko gailu batzuetan barne, eta hori nahiko polita da.

Datuak (ausazkoak) berreskuratzeko abiadura neurtzea eta tamaina ezberdinetako loteak lagintzerakoan eskalatzea ebaluatzea besterik ez da geratzen:

Datu-basearen erreferentzia

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: nola egin lagun R, C++ eta neurona-sareekin

2. Loteak prestatzea

Loteak prestatzeko prozesu osoa urrats hauek ditu:

  1. Puntuen koordenatuak dituzten kate bektoreak dituzten hainbat JSON analizatzea.
  2. Beharrezko tamainako irudi batean (adibidez, 256Γ—256 edo 128Γ—128) puntuen koordenatuetan oinarritutako koloretako lerroak marraztea.
  3. Lortutako irudiak tentsore bihurtzea.

Python nukleoen arteko lehiaren barruan, arazoa batez ere erabiliz konpondu zen OpenCV. R-ko analogo sinple eta agerikoenetako bat honela izango litzateke:

JSON tentsore bihurketa inplementatzea R-n

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

Marrazketa R tresna estandarrak erabiliz egiten da eta RAM-n gordetako aldi baterako PNG batean gordetzen da (Linux-en, aldi baterako R direktorioak direktorioa daude /tmp, RAM batean muntatua). Ondoren, fitxategi hau hiru dimentsioko array gisa irakurtzen da 0tik 1era bitarteko zenbakiak dituena. Hau garrantzitsua da, BMP konbentzionalagoa kolore-kode hexadeimaleko array gordina batean irakurriko litzatekeelako.

Proba dezagun emaitza:

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: nola egin lagun R, C++ eta neurona-sareekin

Lotea bera honela osatuko da:

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

Inplementazio hau ezin hobea iruditu zitzaigun, sorta handien eraketak oso denbora luzea hartzen baitu, eta gure lankideen esperientzia aprobetxatzea erabaki genuen liburutegi indartsu bat erabiliz. OpenCV. Garai hartan ez zegoen R-rako prestaturiko paketerik (orain ez dago), beraz, beharrezko funtzionalitatearen inplementazio minimo bat idatzi zen C++-n R kodean integratuz. Rcpp.

Arazoa konpontzeko, pakete eta liburutegi hauek erabili dira:

  1. OpenCV irudiak lantzeko eta lerroak marrazteko. Aurrez instalatutako sistemaren liburutegiak eta goiburuko fitxategiak erabili ditu, baita estekatze dinamikoak ere.

  2. xtentsorea dimentsio anitzeko matrizeekin eta tentsoreekin lan egiteko. Izen bereko R paketean sartutako goiburuko fitxategiak erabili ditugu. Liburutegiak dimentsio anitzeko matrizeekin lan egiteko aukera ematen du, bai errenkada nagusietan, bai zutabe nagusietan.

  3. ndjson JSON analizatzeko. Liburutegi hau erabiltzen da xtentsorea automatikoki proiektuan badago.

  4. RcppThread JSON-ren bektore baten hari anitzeko prozesamendua antolatzeko. Pakete honek emandako goiburuko fitxategiak erabili ditu. Ezagunagotik RcppParaleloa Paketeak, besteak beste, begizta eten mekanismo bat dauka.

Kontuan hartu behar da xtentsorea Jainkoaren opari bat izan zen: funtzionaltasun zabala eta errendimendu handia izateaz gain, bere garatzaileek nahiko erantzuna eman zuten eta berehala eta zehatz-mehatz erantzun zituzten galderei. Haien laguntzarekin, OpenCV matrizeen tentsoreak xtentsoreetan eraldaketak ezartzea posible izan zen, baita 3 dimentsioko irudi tentsoreak dimentsio zuzeneko 4 dimentsioko tentsore batean konbinatzeko modu bat ere (lotea bera).

Rcpp, xtensor eta RcppThread ikasteko materialak

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

Sistemako fitxategiak eta sisteman instalatutako liburutegiekin lotura dinamikoa erabiltzen duten fitxategiak biltzeko, paketean inplementatutako plugin-mekanismoa erabili dugu. Rcpp. Bideak eta banderak automatikoki aurkitzeko, Linux utilitate ezagun bat erabili dugu pkg-config.

OpenCV liburutegia erabiltzeko Rcpp pluginaren ezarpena

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

Pluginaren funtzionamenduaren ondorioz, balio hauek ordezkatuko dira konpilazio prozesuan:

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 analizatzeko eta eredura igortzeko lote bat sortzeko inplementazio kodea spoiler azpian ematen da. Lehenik eta behin, gehitu proiektu lokaleko direktorioa goiburuko fitxategiak bilatzeko (beharrezkoa ndjson-erako):

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

JSON tentsore bihurketa C++-n inplementatzea

// [[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;
}

Kode hau fitxategian jarri behar da src/cv_xt.cpp eta konpilatu komandoarekin Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); lanerako ere beharrezkoak nlohmann/json.hpp - biltegia. Kodea hainbat funtziotan banatzen da:

  • to_xt β€” irudi-matrize bat eraldatzeko txantiloidun funtzio bat (cv::Mat) tentsore bati xt::xtensor;

  • parse_json β€” funtzioak JSON kate bat analizatzen du, puntuen koordenatuak ateratzen ditu, bektore batean bilduz;

  • ocv_draw_lines β€” ondoriozko puntuen bektoretik, kolore anitzeko lerroak marrazten ditu;

  • process β€” goiko funtzioak konbinatzen ditu eta ondoriozko irudia eskalatzeko gaitasuna ere gehitzen du;

  • cpp_process_json_str - funtzioaren gainean bilgarria process, emaitza R-objektu batera esportatzen duena (dimentsio anitzeko matrizea);

  • cpp_process_json_vector - funtzioaren gainean bilgarria cpp_process_json_str, kate-bektore bat hari anitzeko moduan prozesatzeko aukera ematen duena.

Kolore anitzeko lerroak marrazteko, HSV kolore-eredua erabili zen, eta ondoren RGB bihurtu zen. Proba dezagun emaitza:

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

Quick Draw Doodle Recognition: nola egin lagun R, C++ eta neurona-sareekin
R eta C++-n inplementazioen abiaduraren alderaketa

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: nola egin lagun R, C++ eta neurona-sareekin

Ikusten duzunez, abiaduraren igoera oso esanguratsua izan zen, eta ezin da C++ kodea atzematea R kodea paralelizatuz.

3. Datu-basetik loteak deskargatzeko itertatzaileak

R-k merezitako ospea du RAMan sartzen diren datuak prozesatzeko, eta Python-ek datuen prozesamendu iteratiboaren ezaugarriak dituen bitartean, nukleotik kanpoko kalkuluak (kalkuluak kanpoko memoria erabiliz) erraz eta naturaltasunez ezartzeko aukera ematen du. Guretzat adibide klasiko eta garrantzitsu bat deskribatutako arazoaren testuinguruan, gradienteen jaitsieraren metodoaren bidez trebatutako neurona-sare sakonak dira urrats bakoitzean gradientearen hurbilketarekin, behaketen zati txiki bat edo mini-lote bat erabiliz.

Python-en idatzitako deep learning framework-ek datuetan oinarritutako iteratzaileak inplementatzen dituzten klase bereziak dituzte: taulak, karpetetako irudiak, formatu bitarrak, etab. Prestatutako aukerak erabil ditzakezu edo zurea idatzi zeregin zehatzetarako. R-n Python liburutegiaren ezaugarri guztiak aprobetxa ditzakegu keras bere backend ezberdinekin izen bereko paketea erabiliz, eta horrek paketearen gainean funtzionatzen du erretikulatu. Azken honek aparteko artikulu luze bat merezi du; R-tik Python kodea exekutatzeaz gain, objektuak R eta Python saioen artean transferitzeko aukera ematen du, automatikoki beharrezko mota bihurketa guztiak eginez.

MonetDBlite erabiliz datu guztiak RAM memorian gordetzeko beharra kendu dugu, "sare neuronal" lan guztia Python-en jatorrizko kodearen bidez egingo da, datuen gainean iterador bat idatzi besterik ez dugu egin behar, ez baitago ezer prest. R edo Python-en halako egoera baterako. Funtsean, bi baldintza baino ez daude horretarako: loteak itzuli behar ditu amaigabeko begizta batean eta bere egoera gorde behar du iterazioen artean (azken hau R-n inplementatzen da itxierak erabiliz modurik errazenean). Aurretik, R matrizeak esplizituki bihurtzea eskatzen zen iteragailuaren barruan numpy matrizeetan, baina paketearen egungo bertsioa keras berak egiten du.

Prestakuntza- eta baliozkotze-datuen iteratzailea honako hau izan da:

Prestakuntza- eta baliozkotze-datuen itertatzailea

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

Funtzioak datu-basearekin konexioa duen aldagai bat hartzen du sarrera gisa, erabilitako lerro kopurua, klase kopurua, lotearen tamaina, eskala (scale = 1 256x256 pixeleko irudiak errendatzeari dagokio, scale = 0.5 β€” 128x128 pixel), kolore-adierazlea (color = FALSE Grisen eskalan errendatzea zehazten du erabiltzen denean color = TRUE trazu bakoitza kolore berri batez marraztuta dago) eta aldez aurretik entrenatuta dauden sareen aurreprozesatzeko adierazle bat, imagenet-en. Azken hau behar da pixel balioak [0, 1] tartetik [-1, 1] tartera eskalatzeko, hornitutakoa entrenatzerakoan erabili zena. keras ereduak.

Kanpoko funtzioak argumentu mota egiaztatzea dauka, taula bat data.table ausaz nahastutako lerro-zenbakiekin samples_index eta lote-zenbakiak, kontagailua eta loteen gehienezko kopurua, baita datu-basetik datuak deskargatzeko SQL adierazpena ere. Gainera, barruan funtzioaren analogo azkar bat definitu dugu keras::to_categorical(). Entrenatzeko datu ia guztiak erabili ditugu, ehuneko erdia baliozkotzeko utziz, beraz, garaiaren tamaina parametroak mugatzen zuen. steps_per_epoch deituta keras::fit_generator(), eta egoera if (i > max_i) baliozkotze iteradorerako bakarrik funtzionatu zuen.

Barne-funtzioan, errenkada-indizeak hurrengo loterako berreskuratzen dira, erregistroak datu-basetik deskargatzen dira lote-kontagailua handituz, JSON analizatzea (funtzioa cpp_process_json_vector(), C++-n idatzita) eta irudiei dagozkien arrayak sortuz. Ondoren, klase etiketak dituzten bektore beroak sortzen dira, pixel balioak eta etiketak dituzten matrizeak zerrenda batean konbinatzen dira, hau da, itzuleraren balioa. Lana bizkortzeko, tauletan indizeak sortzea erabili dugu data.table eta estekaren bidez aldatzea - ​​pakete "txip" hauek gabe datuak.taula Nahiko zaila da R-ko edozein datu-kopuru esanguratsurekin modu eraginkorrean lan egitea imajinatzea.

Core i5 ordenagailu eramangarri batean abiadura neurketen emaitzak hauek dira:

Iteratzaileen erreferentea

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: nola egin lagun R, C++ eta neurona-sareekin

RAM kopuru nahikoa baduzu, datu-basearen funtzionamendua serioski azkartu dezakezu RAM honetara transferituz (32 GB nahikoa da gure zereginerako). Linux-en, partizioa lehenespenez muntatuta dago /dev/shm, RAM ahalmenaren erdia okupatuz. Gehiago nabarmendu dezakezu editatuz /etc/fstabbezalako disko bat lortzeko tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Ziurtatu berrabiarazi eta egiaztatu emaitza komandoa exekutatuz df -h.

Proba-datuen iteratzaileak askoz sinpleagoa dirudi, proba-datu-multzoa RAM-an sartzen baita guztiz:

Proba datuetarako itertatzailea

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. Eredu arkitektura hautatzea

Erabilitako lehen arkitektura izan zen mobilenet v1, zeinaren ezaugarriak atalean eztabaidatzen diren hau mezua. Estandar gisa sartzen da keras eta, horrenbestez, R-ren izen bereko paketean dago eskuragarri. Baina kanal bakarreko irudiekin erabiltzen saiatzean, gauza arraro bat gertatu da: sarrerako tentsoreak beti izan behar du dimentsioa. (batch, height, width, 3), hau da, kanal kopurua ezin da aldatu. Python-en ez dago halako mugarik, beraz, arkitektura honen inplementazio propioa idatzi genuen, jatorrizko artikuluari jarraituz (keras bertsioan dagoen uztea gabe):

Mobilenet v1 arkitektura

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

Ikuspegi honen desabantailak agerikoak dira. Eredu asko probatu nahi ditut, baina aitzitik, ez dut arkitektura bakoitza eskuz berridatzi nahi. Imageneten aurrez prestatutako modeloen pisuak erabiltzeko aukera ere kendu ziguten. Ohi bezala, dokumentazioa aztertzeak lagundu zuen. Funtzioa get_config() ereduaren deskribapena editatzeko egokia den forma batean lortzeko aukera ematen du (base_model_conf$layers - R zerrenda arrunta), eta funtzioa from_config() alderantzizko bihurketa egiten du eredu-objektu batera:

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)

Orain ez da zaila funtzio unibertsal bat idaztea hornitutako edozein lortzeko keras imagenet-en trebatutako pisuak dituzten edo gabe modeloak:

Prestatutako arkitekturak kargatzeko funtzioa

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

Kanal bakarreko irudiak erabiltzean, ez da aldez aurretik trebatutako pisurik erabiltzen. Hau konpondu liteke: funtzioa erabiliz get_weights() lortu ereduaren pisuak R matrizeen zerrenda moduan, aldatu zerrenda honetako lehen elementuaren dimentsioa (kolore-kanal bat hartuz edo hiruren batez bestekoa eginez), eta gero kargatu pisuak ereduan funtzioarekin. set_weights(). Ez dugu inoiz funtzionaltasun hori gehitu, fase honetan jada argi zegoelako koloretako irudiekin lan egitea produktiboagoa zela.

Esperimentu gehienak mobilenet 1 eta 2 bertsioak erabiliz egin ditugu, baita resnet34 ere. SE-ResNeXt bezalako arkitektura modernoagoak ondo aritu ziren lehiaketa honetan. Zoritxarrez, ez genuen prest egindako inplementaziorik eskura, eta ez genuen gurea idatzi (baina idatziko dugu zalantzarik gabe).

5. Gidoien parametrizazioa

Erosotasunerako, prestakuntza hasteko kode guztiak script bakar gisa diseinatu ziren, erabiliz parametrizatuta dokopt honela:

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)

pakete dokopt ezarpena adierazten du http://docopt.org/ R-rentzat. Bere laguntzarekin, script-ak komando sinpleekin abiarazten dira Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db edo ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, fitxategia bada train_nn.R exekutagarria da (komando hau eredua entrenatzen hasiko da resnet50 128x128 pixeleko hiru koloretako irudietan, datu-basea karpetan kokatu behar da /home/andrey/doodle_db). Ikasteko abiadura, optimizatzaile mota eta pertsonaliza daitezkeen beste edozein parametro gehi ditzakezu zerrendara. Argitalpena prestatzeko prozesuan, arkitekturak mobilenet_v2 egungo bertsiotik keras R erabileran ezin da R paketean kontuan hartu ez diren aldaketak direla eta, noiz konponduko zain gaude.

Ikuspegi honi esker, eredu ezberdinekin egindako esperimentuak nabarmen bizkortu ziren RStudio-ko scripten abiarazte tradizionalarekin alderatuta (paketea alternatiba posible gisa nabarmentzen dugu. tfruns). Baina abantaila nagusia Docker-en edo, besterik gabe, zerbitzarian scripten abiarazte erraz kudeatzeko gaitasuna da, horretarako RStudio instalatu gabe.

6. Gidoien dockerizazioa

Docker erabili dugu ingurunearen eramangarritasuna bermatzeko taldekideen artean trebatzeko ereduak egiteko eta hodeian azkar hedatzeko. R programatzaile batentzat nahiko ezohikoa den tresna hau ezagutzen has zaitezke hau argitalpen sorta edo bideo ikastaroa.

Docker-ek zure irudiak hutsetik sortzeko eta beste irudi batzuk erabiltzeko aukera ematen dizu zurea sortzeko oinarri gisa. Eskuragarri dauden aukerak aztertzean, NVIDIA, CUDA+cuDNN kontrolatzaileak eta Python liburutegiak instalatzea irudiaren zati nahiko handi bat dela ondorioztatu genuen, eta irudi ofiziala oinarritzat hartzea erabaki genuen. tensorflow/tensorflow:1.12.0-gpu, beharrezko R paketeak bertan gehituz.

Azken docker fitxategiak itxura hau zuen:

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

Erosotasunerako, erabilitako paketeak aldagaietan jarri ziren; idatzizko gidoien zatirik handiena edukiontzien barruan kopiatzen da muntatzean. Komando shell-a ere aldatu dugu /bin/bash edukiak erabiltzeko erraztasunagatik /etc/os-release. Honek sistema eragilearen bertsioa kodean zehaztu beharra saihestu zuen.

Gainera, bash script txiki bat idatzi zen, hainbat komando dituen edukiontzi bat abiarazteko aukera ematen duena. Esaterako, hauek izan litezke aldez aurretik edukiontzi barruan jartzen ziren neurona-sareak entrenatzeko scriptak edo edukiontziaren funtzionamendua arakatzeko eta kontrolatzeko komando-shell bat:

Edukiontzia abiarazteko scripta

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

Bash script hau parametrorik gabe exekutatzen bada, script-a edukiontzi barruan deituko da train_nn.R balio lehenetsiekin; lehen posizio-argumentua "bash" bada, edukiontzia interaktiboki hasiko da komando shell batekin. Gainerako kasuetan, posizio-argumentuen balioak ordezkatzen dira: CMD="Rscript /app/train_nn.R $@".

Azpimarratzekoa da iturburu-datuak eta datu-baseak dituzten direktorioak, baita trebatutako ereduak gordetzeko direktorioa edukiontzi barruan muntatzen direla ostalari-sistematik, eta horri esker, scripten emaitzak alferrikako manipulaziorik gabe atzitu ditzakezu.

7. Google Cloud-en hainbat GPU erabiltzea

Lehiaketaren ezaugarrietako bat oso datu zaratatsuak izan ziren (ikus izenburuko argazkia, @Leigh.plt-tik ODS slack-etik hartutakoa). Sorte handiek horri aurre egiten laguntzen dute, eta 1 GPUdun ordenagailu batean esperimentu ondoren, hodeiko hainbat GPUtan trebatzeko ereduak menperatzea erabaki genuen. GoogleCloud erabilia (oinarrietarako gida ona) eskuragarri dauden konfigurazioen aukeraketa handia dela eta, arrazoizko prezioak eta $ 300 hobaria. Gutiziaz, 4xV100 instantzia bat eskatu nuen SSD eta RAM tona batekin, eta hori akats handia izan zen. Horrelako makina batek dirua azkar jaten du; huts egin dezakezu esperimentatzen frogatu gabe. Hezkuntza helburuetarako, hobe da K80 hartzea. Baina RAM kopuru handia ondo etorri zen - hodeiko SSDak ez zuen bere errendimenduarekin harritu, beraz datu-basea transferitu zen. dev/shm.

Interes handiena da GPU anitz erabiltzeaz arduratzen den kode zatia. Lehenik eta behin, eredua CPUan sortzen da testuinguru-kudeatzailea erabiliz, Python-en bezala:

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

Ondoren, konpilatu gabeko (hau garrantzitsua da) eredua eskuragarri dauden GPU kopuru jakin batera kopiatzen da, eta ondoren bakarrik konpilatzen da:

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

Geruza guztiak izozteko teknika klasikoa azkena izan ezik, azken geruza entrenatzea, desizoztea eta eredu osoa hainbat GPUtarako entrenatzea ezin izan da inplementatu.

Prestakuntza erabili gabe kontrolatu zen. tentsore-taula, erregistroak grabatzera eta garai bakoitzaren ondoren izen informatiboak dituzten ereduak gordetzera mugatuz:

Deiak

# Π¨Π°Π±Π»ΠΎΠ½ ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π° Π»ΠΎΠ³Π°
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. Ondorio baten ordez

Topatu ditugun hainbat arazo ez dira oraindik gainditu:

  • Π² keras ez dago prestatutako funtziorik automatikoki ikasteko tasa optimoa bilatzeko (analogikoa lr_finder liburutegian azkar.ai); Ahalegin batekin, hirugarrenen inplementazioak R-ra eraman daitezke, adibidez, hau;
  • aurreko puntuaren ondorioz, ezin izan da entrenamendu-abiadura zuzena hautatu hainbat GPU erabiltzean;
  • sare neuronalaren arkitektura modernoak falta dira, batez ere imagenet-en aurrez prestatutakoak;
  • ziklo bakarreko politika eta ikasketa-tasa diskriminatzaileak (kosinua erretzea gure eskaera egin zen ezarrita, eskerrik asko skeydan).

Lehiaketa honetatik zer gauza erabilgarriak ikasi ziren:

  • Potentzia nahiko baxuko hardwarean, datu-bolumen duinekin (RAMaren tamaina askotan) lan egin dezakezu minik gabe. Plastikozko poltsa datuak.taula memoria aurrezten du taulak lekuan aldatzeagatik, eta horrek kopiatzea saihesten du, eta behar bezala erabiltzen denean, bere gaitasunek ia beti erakusten dute script-lengoaietarako ezagutzen ditugun tresna guztien artean abiadura handiena. Datu-base batean datuak gordetzeak aukera ematen du, kasu askotan, datu-multzo osoa RAM batean estutu beharraz batere ez pentsatzea.
  • R-ko funtzio geldoak C++-ko bizkorrekin ordezka daitezke paketea erabiliz Rcpp. Erabiltzeaz gain bada RcppThread edo RcppParaleloa, plataforma anitzeko hari anitzeko inplementazioak lortzen ditugu, beraz, ez dago kodea R mailan paralelizatu beharrik.
  • Paketea Rcpp C++-ren ezagutza seriorik gabe erabil daiteke, eskatzen den gutxienekoa zehazten da Hemen. Goiburuko fitxategiak bezalako C-liburutegi polit batzuentzat xtentsorea CRAN-en eskuragarri, hau da, prest dauden errendimendu handiko C++ kodea R-n integratzen duten proiektuak ezartzeko azpiegitura bat osatzen ari da. Erosotasun gehigarria sintaxia nabarmentzea eta RStudioko C++ kode analizatzaile estatikoa da.
  • dokopt parametrodun script autonomoak exekutatzeko aukera ematen du. Hau erosoa da urruneko zerbitzari batean erabiltzeko, barne. docker azpian. RStudio-n, deserosoa da sare neuronalak entrenatzeko ordu asko esperimentuak egitea, eta IDEa zerbitzarian bertan instalatzea ez da beti justifikatzen.
  • Docker-ek kodearen eramangarritasuna eta emaitzen erreproduzigarritasuna bermatzen du sistema eragilearen eta liburutegien bertsio desberdinak dituzten garatzaileen artean, baita zerbitzarietan exekutatzeko erraztasuna ere. Prestakuntza kanal osoa abiarazi dezakezu komando bakarrarekin.
  • Google Cloud hardware garestietan esperimentatzeko aurrekontua errespetatzen duen modu bat da, baina konfigurazioak arretaz aukeratu behar dituzu.
  • Kode zati indibidualen abiadura neurtzea oso erabilgarria da, batez ere R eta C++ konbinatzean eta paketearekin. AGORREGI - oso erraza ere.

Orokorrean esperientzia hau oso aberasgarria izan zen eta lanean jarraitzen dugu planteatutako arazo batzuk konpontzeko.

Iturria: www.habr.com

Gehitu iruzkin berria