Aitheantas Tapa Tarraingthe Doodle: conas cairdeas a dhéanamh le R, C++ agus líonraí néaracha

Aitheantas Tapa Tarraingthe Doodle: conas cairdeas a dhéanamh le R, C++ agus líonraí néaracha

Hey Habr!

An titim seo caite, d'óstáil Kaggle comórtas chun pictiúir láimhe a rangú, Quick Draw Doodle Recognition, inar ghlac foireann R-eolaithe páirt, i measc daoine eile: Artem Klevtsova, Bainisteoir Philippa и Andrey Ogurtsov. Ní dhéanfaimid cur síos mion ar an gcomórtas; tá sé sin déanta cheana féin i foilsiú le déanaí.

An uair seo níor oibrigh sé amach le feirmeoireacht bonn, ach fuarthas a lán taithí luachmhar, mar sin ba mhaith liom a insint don phobal faoi roinnt de na rudaí is suimiúla agus is úsáidí ar Kagle agus san obair laethúil. I measc na n-ábhar a pléadh: saol deacair gan OpenCV, parsáil JSON (scrúdaíonn na samplaí seo comhtháthú cód C++ i scripteanna nó pacáistí in R ag baint úsáide as Rcpp), paraiméadarú scripteanna agus dockerization an réiteach deiridh. Tá gach cód ón teachtaireacht i bhfoirm atá oiriúnach lena fhorghníomhú ar fáil i stórtha.

Clár ábhair:

  1. Luchtaigh go héifeachtach sonraí ó CSV isteach i MonetDB
  2. Baisceanna a ullmhú
  3. Iterators chun baisceanna a dhíluchtú ón mbunachar sonraí
  4. Roghnú Ailtireachta Múnla
  5. Script paraiméadarú
  6. Dockerization na scripteanna
  7. Ag baint úsáide as GPUanna iolracha ar Google Cloud
  8. In ionad a thabhairt i gcrích

1. Lódáil go héifeachtach sonraí ó CSV isteach i mbunachar sonraí MonetDB

Ní sholáthraítear na sonraí sa chomórtas seo i bhfoirm íomhánna réamhdhéanta, ach i bhfoirm 340 comhad CSV (comhad amháin do gach rang) ina bhfuil JSONanna le comhordanáidí pointe. Trí na pointí seo a nascadh le línte, faigheann muid íomhá deiridh a thomhas 256x256 picteilín. Chomh maith leis sin tá lipéad le haghaidh gach taifid a thugann le fios cé acu ar aithin an t-aicmitheoir a d’úsáid an t-am a bailíodh an tacar sonraí an pictiúr i gceart, cód dhá litir de thír chónaithe údair an phictiúir, aitheantóir uathúil, stampa ama agus ainm ranga a thagann le hainm an chomhaid. Meáchan leagan simplithe de na sonraí bunaidh 7.4 GB sa chartlann agus thart ar 20 GB tar éis díphacáil, tógann na sonraí iomlána tar éis díphacáil suas 240 GB. Chinntigh na heagraithe go ndearna an dá leagan na líníochtaí céanna a atáirgeadh, rud a chiallaíonn go raibh an leagan iomlán iomarcach. Ar aon chuma, measadh láithreach go raibh stóráil 50 milliún íomhá i gcomhaid ghrafacha nó i bhfoirm eagair neamhbhrabúsach, agus shocraigh muid gach comhad CSV ón gcartlann a chumasc train_simplified.zip isteach sa bhunachar sonraí le glúin ina dhiaidh sin d’íomhánna den mhéid riachtanach “ar an eitilt” do gach baisc.

Roghnaíodh córas dea-chruthaithe mar an DBMS MonetDB, eadhon cur chun feidhme do R mar phacáiste MonetDBLite. Cuimsíonn an pacáiste leagan leabaithe den fhreastalaí bunachar sonraí agus ligeann duit an freastalaí a phiocadh suas go díreach ó sheisiún R agus oibriú leis ansin. Déantar bunachar sonraí a chruthú agus nascadh leis le hordú amháin:

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

Beidh orainn dhá thábla a chruthú: ceann amháin le haghaidh na sonraí go léir, an ceann eile le haghaidh faisnéise seirbhíse faoi chomhaid íoslódáilte (úsáideach má théann rud éigin mícheart agus ní mór an próiseas a atosú tar éis roinnt comhad a íoslódáil):

Ag cruthú táblaí

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

Ba é an bealach is tapúla chun sonraí a luchtú isteach sa bhunachar sonraí ná comhaid CSV a chóipeáil go díreach ag baint úsáide as SQL - ordú COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTI gcás ina tablename - ainm tábla agus path - an cosán go dtí an comhad. Agus iad ag obair leis an gcartlann, fuarthas amach go bhfuil an-tógtha i bhfeidhm unzip in R ní oibríonn sé i gceart le roinnt comhad ón gcartlann, mar sin d'úsáideamar an córas unzip (ag baint úsáide as an bparaiméadar getOption("unzip")).

Feidhm chun scríobh chuig an mbunachar sonraí

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

Más gá duit an tábla a athrú sula scríobh tú chuig an mbunachar sonraí é, is leor an argóint a chur ar aghaidh preprocess feidhm a athróidh na sonraí.

Cód chun sonraí a lódáil go seicheamhach isteach sa bhunachar sonraí:

Sonraí a scríobh chuig an mbunachar sonraí

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

Féadfaidh am luchtaithe sonraí a bheith éagsúil ag brath ar shaintréithe luais an tiomána a úsáidtear. Inár gcás, tógann léamh agus scríobh laistigh de SSD amháin nó ó thiomáint flash (comhad foinse) go SSD (DB) níos lú ná 10 nóiméad.

Tógann sé cúpla soicind eile chun colún a chruthú le lipéad ranga slánuimhir agus colún innéacs (ORDERED INDEX) le huimhreacha línte trína ndéanfar breathnuithe a shamplaiú agus baisceanna á gcruthú:

Colúin agus Innéacs Breise a Chruthú

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

Chun an fhadhb a réiteach maidir le baisc a chruthú ar an eitilt, bhí orainn an luas uasta a bhaint amach chun sraitheanna randamacha a bhaint as an tábla doodles. Chun seo a úsáid againn 3 cleasanna. Ba é an chéad cheann ná toiseacht an chineáil a stórálann an t-aitheantas breathnadóireachta a laghdú. Sa tacar sonraí bunaidh, is é an cineál atá ag teastáil chun an ID a stóráil bigint, ach de bharr líon na mbreathnuithe is féidir a n-aitheantóirí, comhionann leis an orduimhir, a chur isteach sa chineál int. Tá an cuardach i bhfad níos tapúla sa chás seo. Ba é an dara cleas a úsáid ORDERED INDEX — thángamar ar an gcinneadh seo go heimpíreach, tar éis dul tríd gach rud a bhí ar fáil roghanna. Ba é an tríú ceann ná fiosruithe paraiméadaraithe a úsáid. Is é croílár an mhodha ná an t-ordú a fhorghníomhú uair amháin PREPARE le húsáid slonn ullmhaithe ina dhiaidh sin nuair a chruthaítear líon ceisteanna den chineál céanna, ach i ndáiríre tá buntáiste ann i gcomparáid le ceann simplí SELECT iompaigh amach go raibh sé laistigh de raon na hearráide staidrimh.

Ní ídíonn próiseas uaslódáil sonraí níos mó ná 450 MB de RAM. Is é sin le rá go gceadaíonn an cur chuige a thuairiscítear duit tacair shonraí a mheá na ndeicheanna ghigibheart a bhogadh ar chrua-earraí buiséid ar bith beagnach, lena n-áirítear roinnt gléasanna aonchláir, rud atá deas fionnuar.

Níl fágtha ach luas aisghabhála sonraí (randamach) a thomhas agus an scálú a mheas agus baisceanna de mhéideanna éagsúla á sampláil:

Tagarmharc bunachar sonraí

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)

Aitheantas Tapa Tarraingthe Doodle: conas cairdeas a dhéanamh le R, C++ agus líonraí néaracha

2. Baisceanna a ullmhú

Tá na céimeanna seo a leanas i bpróiseas iomlán ullmhúcháin an bhaisc:

  1. Ag parsáil roinnt JSONanna ina bhfuil veicteoirí teaghrán le comhordanáidí pointí.
  2. Línte daite a tharraingt bunaithe ar chomhordanáidí na bpointí ar íomhá den mhéid riachtanach (mar shampla, 256×256 nó 128×128).
  3. Na híomhánna dá bharr a thiontú ina tensor.

Mar chuid den chomórtas i measc kernels Python, réitíodh an fhadhb go príomha ag baint úsáide as OpenCV. Bheadh ​​cuma mar seo ar cheann de na hanalógacha is simplí agus is soiléire in R:

JSON go Tiontú Tensor a chur i bhfeidhm in 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)
}

Baintear úsáid as uirlisí caighdeánacha an líníocht agus sábhálfar go PNG sealadach é atá stóráilte i RAM (ar Linux, tá eolairí sealadacha R lonnaithe san eolaire /tmp, suite i RAM). Ansin léitear an comhad seo mar eagar tríthoiseach le huimhreacha ó 0 go 1. Tá sé seo tábhachtach mar go léifí BMP níos traidisiúnta ina eagar amh le cóid datha heicsidheachúlach.

Déanaimis an toradh a thástáil:

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

Aitheantas Tapa Tarraingthe Doodle: conas cairdeas a dhéanamh le R, C++ agus líonraí néaracha

Déanfar an bhaisc féin a fhoirmiú mar seo a leanas:

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

Bhí an chuma air nach raibh an cur i bhfeidhm seo ró-optamach dúinne, ós rud é go dtógann foirmiú baisceanna móra tamall fada mígheanasach, agus shocraigh muid leas a bhaint as taithí ár gcomhghleacaithe trí úsáid a bhaint as leabharlann chumhachtach. OpenCV. Ag an am sin ní raibh aon phacáiste réidh le haghaidh R (níl aon cheann ann anois), agus mar sin scríobhadh cur i bhfeidhm íosta na feidhme riachtanach i C++ le comhtháthú isteach i gcód R ag baint úsáide as Rcpp.

Chun an fhadhb a réiteach, úsáideadh na pacáistí agus na leabharlanna seo a leanas:

  1. OpenCV chun oibriú le híomhánna agus línte a tharraingt. Úsáideadh leabharlanna córais réamhshuiteáilte agus comhaid ceanntásca, chomh maith le nascadh dinimiciúil.

  2. xtensor chun oibriú le eagair agus teanntóirí iltoiseacha. D'úsáideamar comhaid ceanntásca a bhí sa phacáiste R den ainm céanna. Ligeann an leabharlann duit oibriú le eagair iltoiseacha, i mór-ord agus in ord colúin.

  3. ndjson chun JSON a pharsáil. Úsáidtear an leabharlann seo i xtensor go huathoibríoch má tá sé i láthair sa tionscadal.

  4. RcppThread chun próiseáil il-snáithe veicteora ó JSON a eagrú. Úsáideadh na comhaid ceanntásca a sholáthraíonn an pacáiste seo. Ó níos mó tóir Rcpp Comhthreomhar Tá meicníocht idirbhriste lúb ionsuite ag an bpacáiste, i measc rudaí eile.

Ba chóir a thabhairt faoi deara go xtensor iompaigh amach a bheith ina godsend: chomh maith leis an bhfíric go bhfuil feidhmiúlacht fairsing agus ardfheidhmíocht, d'éirigh a fhorbróirí amach a bheith sách sofhreagrach agus d'fhreagair ceisteanna go pras agus go mion. Le cabhair uathu, bhíothas in ann claochluithe maitrísí OpenCV a chur i bhfeidhm i tensor xtensor, chomh maith le bealach chun tensor íomhá 3thoiseach a chomhcheangal i tensor 4-tríthoiseach den toise ceart (an bhaisc féin).

Ábhair foghlama Rcpp, xtensor agus 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

Chun comhaid a thiomsú a úsáideann comhaid chórais agus naisc dhinimiciúla le leabharlanna suiteáilte ar an gcóras, d'úsáideamar an mheicníocht breiseán a cuireadh i bhfeidhm sa phacáiste Rcpp. Chun cosáin agus bratacha a aimsiú go huathoibríoch, d'úsáideamar áirgiúlacht mhóréilimh Linux pkg-config.

Breiseán Rcpp a chur i bhfeidhm chun leabharlann OpenCV a úsáid

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

Mar thoradh ar oibriú an bhreiseáin, cuirfear na luachanna seo a leanas in ionad an phróisis tiomsaithe:

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"

Tugtar an cód cur chun feidhme chun JSON a pharsáil agus chun baisc a ghiniúint lena tharchur chuig an tsamhail faoin spoiler. Ar dtús, cuir eolaire tionscadail áitiúil leis chun comhaid ceanntásc a chuardach (ag teastáil le haghaidh ndjson):

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

Cur i bhfeidhm JSON go tiontú tensor in 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;
}

Ba cheart an cód seo a chur sa chomhad src/cv_xt.cpp agus a thiomsú leis an ordú Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); ag teastáil freisin le haghaidh oibre nlohmann/json.hpp de repositoria. Tá an cód roinnte i roinnt feidhmeanna:

  • to_xt — feidhm theimpléadaithe chun íomhá mhaitrís a athrú (cv::Mat) a tensor xt::xtensor;

  • parse_json — déanann an fheidhm teaghrán JSON a pharsáil, baintear amach comhordanáidí na bpointí agus pacáil isteach i veicteoir iad;

  • ocv_draw_lines — ón veicteoir pointí a thagann as, tarraingíonn sé línte ildaite;

  • process — comhcheanglaíonn sé na feidhmeanna thuas agus cuireann sé leis freisin an cumas an íomhá a thagann as a scála a scála;

  • cpp_process_json_str - wrapper thar an fheidhm process, a onnmhairíonn an toradh chuig R-réad (eagar iltoiseach);

  • cpp_process_json_vector - wrapper thar an fheidhm cpp_process_json_str, a ligeann duit veicteoir teaghrán a phróiseáil i mód il-snáithithe.

Chun línte il-daite a tharraingt, baineadh úsáid as samhail dath HSV, agus ina dhiaidh sin tiontú go RGB. Déanaimis an toradh a thástáil:

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

Aitheantas Tapa Tarraingthe Doodle: conas cairdeas a dhéanamh le R, C++ agus líonraí néaracha
Comparáid idir luas na bhfeidhmiúchán in R agus 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") 

Aitheantas Tapa Tarraingthe Doodle: conas cairdeas a dhéanamh le R, C++ agus líonraí néaracha

Mar a fheiceann tú, d'éirigh leis an méadú luais a bheith an-suntasach, agus ní féidir teacht suas le cód C++ trí chóid R a chomhthreomharú.

3. Iterators chun baisceanna a dhíluchtú ón mbunachar sonraí

Tá cáil tuillte go maith ag R as próiseáil sonraí a oireann do RAM, agus tá próiseáil sonraí atriallach níos saintréithe ag Python, rud a ligeann duit ríomhaireachtaí lasmuigh den chroí a chur i bhfeidhm go héasca agus go nádúrtha (ríomhanna ag baint úsáide as cuimhne sheachtrach). Sampla clasaiceach agus ábhartha dúinn i gcomhthéacs na faidhbe a bhfuil cur síos déanta uirthi is ea líonraí néaracha doimhne atá oilte ag modh shliocht an ghrádáin le comhfhogasú an ghrádáin ag gach céim ag baint úsáide as cuid bheag de na breathnuithe, nó mionbhaisc.

Tá ranganna speisialta ag creataí domhainfhoghlama atá scríofa i Python a chuireann iterators i bhfeidhm bunaithe ar shonraí: táblaí, pictiúir i bhfillteáin, formáidí dénártha, etc. Is féidir leat roghanna réamhdhéanta a úsáid nó do chuid féin a scríobh le haghaidh tascanna sonracha. In R is féidir linn leas a bhaint as gnéithe uile leabharlann Python keras lena hais éagsúla ag baint úsáide as an bpacáiste den ainm céanna, a oibríonn ar a seal ar bharr an phacáiste reticulate. Tá alt fada ar leith tuillte ag an dara ceann; ní hamháin go gceadaíonn sé duit cód Python a rith ó R, ach ligeann sé duit rudaí a aistriú idir seisiúin R agus Python, ag feidhmiú go huathoibríoch na tiontaithe cineál riachtanacha go léir.

Fuaireamar réidh leis an ngá na sonraí go léir a stóráil i RAM trí úsáid a bhaint as MonetDBLite, déanfar an obair “líonra néaraíoch” ar fad leis an gcód bunaidh i Python, níl le déanamh againn ach atriaradóir a scríobh thar na sonraí, ós rud é nach bhfuil aon rud réidh do chás den sórt sin i ceachtar R nó Python. Go bunúsach níl ach dhá cheanglas ann: caithfidh sé baisceanna a thabhairt ar ais i lúb gan teorainn agus a staid a shábháil idir atriall (cuirtear an dara ceann in R i bhfeidhm ar an mbealach is simplí ag baint úsáide as dúnta). Roimhe seo, b'éigean eagair R a thiontú go sainráite ina n-eagair numpy taobh istigh den iterator, ach leagan reatha an phacáiste keras a dhéanann sé í féin.

Seo a leanas an t-athróir ar shonraí oiliúna agus bailíochtaithe:

Athsheoltóir le haghaidh sonraí oiliúna agus bailíochtaithe

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

Glacann an fheidhm mar ionchur athróg le ceangal leis an mbunachar sonraí, líon na línte a úsáidtear, líon na ranganna, méid baisce, scála (scale = 1 fhreagraíonn do íomhánna rindreála 256x256 picteilín, scale = 0.5 - 128x128 picteilín), táscaire datha (color = FALSE sonraíonn sé rindreáil i liathscála nuair a úsáidtear é color = TRUE tarraingítear gach stróc i ndath nua) agus táscaire réamhphróiseála do líonraí réamhoilte ar imagenet. Tá an dara ceann ag teastáil chun luachanna picteilín a scála ón eatramh [0, 1] go dtí an t-eatramh [-1, 1], a úsáideadh agus an t-eatramh á thraenáil keras samhlacha.

Tá seiceáil cineál argóinte, tábla san fheidhm sheachtrach data.table le huimhreacha randamacha measctha ó samples_index agus baiscuimhreacha, cuntar agus uaslíon na mbaisceanna, chomh maith le slonn SQL chun sonraí a dhíluchtú ón mbunachar sonraí. Ina theannta sin, shainmhíomar analóg tapa den fheidhm taobh istigh keras::to_categorical(). D'úsáideamar beagnach na sonraí go léir le haghaidh oiliúna, rud a d'fhág leath faoin gcéad le haghaidh bailíochtaithe, agus mar sin bhí an méid ré teoranta ag an bparaiméadar steps_per_epoch nuair a ghlaoitear air keras::fit_generator(), agus an riocht if (i > max_i) níor oibrigh sé ach don atrialltóir bailíochtaithe.

San fheidhm inmheánach, aisghabhtar innéacsanna rónna don chéad bhaisc eile, díluchtaítear taifid ón mbunachar sonraí agus méadaítear an t-áiritheoir baisc, parsáil JSON (feidhm cpp_process_json_vector(), scríofa i C++) agus ag cruthú eagair a fhreagraíonn do phictiúir. Ansin cruthaítear veicteoirí aon-te le lipéid ranga, cuirtear eagair le luachanna picteilín agus lipéid le chéile i liosta, arb é an luach tuairisceáin é. Chun an obair a bhrostú, d’úsáideamar cruthú innéacsanna i dtáblaí data.table agus modhnú tríd an nasc - gan na “sceallóga” pacáiste seo sonraí.tábla Tá sé deacair a shamhlú oibriú go héifeachtach le haon mhéid suntasach sonraí in R.

Seo a leanas torthaí na dtomhas luais ar ríomhaire glúine Core i5:

Iterator tagarmharc

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)

Aitheantas Tapa Tarraingthe Doodle: conas cairdeas a dhéanamh le R, C++ agus líonraí néaracha

Má tá méid leordhóthanach RAM agat, is féidir leat oibriú an bhunachair shonraí a bhrostú go mór trína aistriú chuig an RAM céanna seo (is leor 32 GB dár tasc). I Linux, tá an deighilt gléasta de réir réamhshocraithe /dev/shm, ag áitiú suas le leath an toilleadh RAM. Is féidir leat níos mó béime a chur air trí eagarthóireacht a dhéanamh /etc/fstabchun taifead a fháil mar tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Bí cinnte a atosú agus an toradh a sheiceáil tríd an ordú a rith df -h.

Tá cuma i bhfad níos simplí ar an atrialltóir sonraí tástála, toisc go luíonn an tacar sonraí tástála go hiomlán le RAM:

Atriator le haghaidh sonraí tástála

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. Roghnú na hailtireachta samhlacha

Ba é an chéad ailtireacht a úsáideadh mobilenet v1, a phléitear na gnéithe de i seo teachtaireacht. Tá sé san áireamh mar chaighdeán keras agus, dá réir sin, ar fáil sa phacáiste den ainm céanna do R. Ach nuair a iarraidh é a úsáid le híomhánna aon-chainéil, d'éirigh rud aisteach amach: ní mór go mbeadh an toise i gcónaí ag an tensor ionchuir. (batch, height, width, 3), is é sin, ní féidir líon na gcainéal a athrú. Níl aon teorannú den sórt sin i Python, agus mar sin rinneamar rushed agus scríobhamar ár gcur i bhfeidhm féin ar an ailtireacht seo, ag leanúint leis an alt bunaidh (gan an titim amach atá sa leagan keras):

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

Is léir na míbhuntáistí a bhaineann leis an gcur chuige seo. Ba mhaith liom a lán samhlacha a thástáil, ach ar a mhalairt, níl mé ag iarraidh gach ailtireacht a athscríobh de láimh. Níor mhór dúinn freisin an deis úsáid a bhaint as meáchain na múnlaí réamhoilte ar imagenet. Mar is gnách, chabhraigh staidéar a dhéanamh ar na doiciméid. Feidhm get_config() ligeann sé duit cur síos a fháil ar an tsamhail i bhfoirm atá oiriúnach le haghaidh eagarthóireacht (base_model_conf$layers - liosta R rialta), agus an fheidhm from_config() déanann sé an tiontú droim ar ais go réad samhalta:

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)

Anois níl sé deacair feidhm uilíoch a scríobh chun aon cheann de na soláthraithe a fháil keras samhlacha le meáchain nó gan meáchain oilte ar imagenet:

Feidhm chun ailtireachtaí réamhdhéanta a lódáil

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

Agus íomhánna aon chainéil á n-úsáid, ní úsáidtear aon mheáchain réamhoilte. D'fhéadfaí é seo a shocrú: ag baint úsáide as an fheidhm get_weights() faigh na meáchain mhúnla i bhfoirm liosta d'eagair R, athraigh toise na chéad eiliminte den liosta seo (trí chainéal datha amháin a ghlacadh nó trí mheán na dtrí cinn a mheán), agus ansin luchtaigh na meáchain ar ais isteach sa mhúnla leis an bhfeidhm set_weights(). Níor chuireamar an fheidhmiúlacht seo leis riamh, mar ag an gcéim seo bhí sé soiléir cheana féin go raibh sé níos táirgiúla oibriú le pictiúir datha.

Rinneamar an chuid is mó de na turgnaimh ag baint úsáide as leagan mobilenet 1 agus 2, chomh maith le resnet34. D’fheidhmigh ailtireachtaí níos nua-aimseartha ar nós SE-ResNeXt go maith sa chomórtas seo. Ar an drochuair, ní raibh feidhmiúcháin réidh againn ar fáil dúinn, agus níor scríobhamar ár gcuid féin (ach scríobhfaimid cinnte).

5. Parameterization na scripteanna

Ar mhaithe le caoithiúlacht, dearadh an cód ar fad chun tús a chur le hoiliúint mar script amháin, paraiméadar ag baint úsáide as docopt ar an mbealach seo a leanas:

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)

Pacáiste docopt is ionann an cur i bhfeidhm http://docopt.org/ le haghaidh R. Lena chabhair, seoltar scripteanna le horduithe simplí mar Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, más comhad train_nn.R is inrite (tosóidh an t-ordú seo ag traenáil an mhúnla resnet50 ar íomhánna trí-dath a thomhas 128x128 picteilín, ní mór an bunachar sonraí a bheith suite san fhillteán /home/andrey/doodle_db). Is féidir leat luas foghlama, cineál optimizer, agus aon pharaiméadair inoiriúnaithe eile a chur leis an liosta. Sa phróiseas ullmhú an fhoilseacháin, d'éirigh sé amach go bhfuil an ailtireacht mobilenet_v2 ón leagan reatha keras in úsáid R Ní mór mar gheall ar athruithe nach gcuirtear san áireamh sa phacáiste R, táimid ag fanacht leo é a shocrú.

Mar gheall ar an gcur chuige seo bhíothas in ann turgnaimh le múnlaí éagsúla a bhrostú go mór i gcomparáid le seoladh níos traidisiúnta scripteanna in RStudio (tugaimid faoi deara an pacáiste mar rogha eile a d'fhéadfadh a bheith ann tfrúin). Ach is é an buntáiste is mó an cumas a bhainistiú go héasca ar an seoladh scripteanna i Docker nó go simplí ar an bhfreastalaí, gan a shuiteáil RStudio le haghaidh seo.

6. Dockerization na scripteanna

D’úsáideamar Docker chun inaistritheacht an chomhshaoil ​​a chinntiú do mhúnlaí oiliúna idir baill foirne agus d’imscaradh tapa sa scamall. Is féidir leat dul i dtaithí ar an uirlis seo, rud atá measartha neamhghnách do ríomhchláraitheoir R, le seo sraith foilseachán nó cúrsa físe.

Ligeann Docker duit d’íomhánna féin a chruthú ón tús agus íomhánna eile a úsáid mar bhonn chun do chuid féin a chruthú. Agus anailís á dhéanamh againn ar na roghanna atá ar fáil, thángamar ar an tátal gur cuid measartha toirtiúil den íomhá é tiománaithe NVIDIA, CUDA + cuDNN agus leabharlanna Python a shuiteáil, agus shocraigh muid an íomhá oifigiúil a ghlacadh mar bhonn tensorflow/tensorflow:1.12.0-gpu, ag cur na pacáistí R riachtanacha ann.

Bhí cuma mar seo ar an gcomhad docker deiridh:

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

Ar mhaithe le caoithiúlacht, cuireadh na pacáistí a úsáideadh in athróga; déantar formhór na scripteanna scríofa a chóipeáil laistigh de na coimeádáin le linn an tionóil. D'athraigh muid an bhlaosc ordú go /bin/bash ar mhaithe le héascaíocht úsáide an ábhair /etc/os-release. Sheachain sé seo an gá leis an leagan OS a shonrú sa chód.

Ina theannta sin, scríobhadh script bash beag a ligeann duit coimeádán a sheoladh le horduithe éagsúla. Mar shampla, d’fhéadfadh gur scripteanna iad seo chun líonraí néaracha a oiliúint a cuireadh sa choimeádán roimhe seo, nó blaosc ordaithe le haghaidh dífhabhtaithe agus monatóireacht a dhéanamh ar oibriú an choimeádáin:

Script chun an coimeádán a sheoladh

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

Má reáchtáiltear an script bash seo gan paraiméadair, tabharfar an script taobh istigh den choimeádán train_nn.R le luachanna réamhshocraithe; más é "bash" an chéad argóint suímh, tosóidh an coimeádán go hidirghníomhach le blaosc ordaithe. I ngach cás eile, cuirtear luachanna na n-argóintí seasaimh in ionad: CMD="Rscript /app/train_nn.R $@".

Is fiú a thabhairt faoi deara go bhfuil na heolairí le sonraí foinse agus bunachar sonraí, chomh maith leis an eolaire chun samhlacha oilte a shábháil, suite taobh istigh den choimeádán ón gcóras óstach, rud a ligeann duit rochtain a fháil ar thorthaí na scripteanna gan ionramhálacha neamhriachtanach.

7. Ag baint úsáide as GPUs il ar Google Cloud

Ceann de ghnéithe an chomórtais ab ea na sonraí an-fhuaimneach (féach an pictiúr teidil, a fuarthas ar iasacht ó @Leigh.plt ó ODS slack). Cuidíonn baisceanna móra leis seo a chomhrac, agus tar éis turgnaimh ar ríomhaire le 1 GPU, shocraigh muid máistreacht a fháil ar mhúnlaí oiliúna ar roinnt GPUanna sa scamall. Úsáidte GoogleCloud (treoir mhaith ar na bunghnéithe) mar gheall ar an rogha mór cumraíochtaí atá ar fáil, praghsanna réasúnta agus bónas $300. As saint, d'ordaigh mé mar shampla 4xV100 le SSD agus tonna RAM, agus ba botún mór é sin. Itheann meaisín den sórt sin airgead go tapa; is féidir leat triail a bhaint as briste gan píblíne cruthaithe. Chun críocha oideachais, is fearr an K80 a ghlacadh. Ach tháinig an méid mór RAM áisiúil - níor chuir an scamall SSD isteach ar a fheidhmíocht, agus mar sin aistríodh an bunachar sonraí chuig dev/shm.

Is díol spéise is mó an blúire cód atá freagrach as il-GPUanna a úsáid. Ar dtús, cruthaítear an tsamhail ar an LAP ag baint úsáide as bainisteoir comhthéacs, díreach mar atá i 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
  )
})

Ansin déantar an tsamhail neamh-thiomsaithe (tá sé seo tábhachtach) a chóipeáil chuig líon áirithe GPUanna atá ar fáil, agus ní dhéantar é a thiomsú ach ina dhiaidh sin:

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

Níorbh fhéidir an teicníc clasaiceach de reo na sraitheanna go léir ach amháin an ceann deireanach, an ciseal deireanach a oiliúint, an múnla iomlán a dhíreoite agus a athoiliúint do roinnt GPUanna a chur i bhfeidhm.

Rinneadh monatóireacht ar oiliúint gan úsáid. tensorboard, sinn féin a theorannú chun logaí a thaifeadadh agus samhlacha a shábháil le hainmneacha faisnéiseacha tar éis gach aga:

Glaonna ar ais

# Шаблон имени файла лога
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. In ionad conclúid

Níor sáraíodh roinnt fadhbanna go fóill:

  • в keras níl aon fheidhm réamhdhéanta chun an ráta foghlama is fearr a chuardach go huathoibríoch (analóg lr_finder sa leabharlann tapa.ai); Le roinnt iarracht, is féidir feidhmiúcháin tríú páirtí a phortáil chuig R, mar shampla, seo;
  • mar thoradh ar an bpointe roimhe seo, níorbh fhéidir an luas oiliúna ceart a roghnú agus roinnt GPUanna á n-úsáid;
  • tá easpa ailtireachta líonraí néaracha nua-aimseartha ann, go háirithe iad siúd atá réamhoilte ar imagenet;
  • gan aon bheartas timthrialla amháin agus rátaí foghlama idirdhealaitheacha (bhí an t-annealú cosine ar iarratas uainne curtha i bhfeidhm, go raibh maith agat skeydan).

Cad iad na rudaí úsáideacha a foghlaimíodh ón gcomórtas seo:

  • Ar chrua-earraí ísealchumhachta, is féidir leat oibriú le méideanna réasúnta (go leor uaireanta an méid RAM) sonraí gan phian. Mála plaisteach sonraí.tábla sábhálann sé cuimhne mar gheall ar mhodhnú intí ar tháblaí, rud a sheachnaíonn iad a chóipeáil, agus nuair a úsáidtear iad i gceart, léiríonn a chumais beagnach i gcónaí an luas is airde i measc na n-uirlisí go léir atá ar eolas againn maidir le teangacha scriptithe. Má shábhálann tú sonraí i mbunachar sonraí is féidir leat, i go leor cásanna, gan smaoineamh ar an ngá atá leis an tacar sonraí iomlán a bhrú isteach i RAM.
  • Is féidir feidhmeanna malla in R a chur in ionad feidhmeanna gasta i C++ ag baint úsáide as an bpacáiste Rcpp. Más rud é chomh maith le húsáid RcppThreadRcpp Comhthreomhar, faigheann muid feidhmiúcháin il-snáithithe tras-ardán, agus mar sin ní gá an cód a chomhthreomharú ag an leibhéal R.
  • Pacáiste Rcpp is féidir é a úsáid gan eolas dáiríre ar C++, leagtar amach an t-íosmhéid is gá anseo. Comhaid ceanntásca do roinnt leabharlanna fionnuara C mar xtensor ar fáil ar CRAN, is é sin, tá bonneagar á fhoirmiú chun tionscadail a chur chun feidhme a chomhtháthaíonn cód C++ ardfheidhmíochta réamhdhéanta in R. Is áis bhreise é béim ar chomhréir agus anailísí cód statach C++ in RStudio.
  • docopt ligeann duit scripteanna féinchuimsitheacha a rith le paraiméadair. Tá sé seo áisiúil le húsáid ar fhreastalaí cianda, lena n-áirítear. faoi ​​docker. I RStudio, tá sé deacair go leor uaireanta turgnaimh a dhéanamh le líonraí néarúla oiliúna, agus ní bhíonn údar maith i gcónaí leis an IDE a shuiteáil ar an bhfreastalaí féin.
  • Cinntíonn Docker inaistritheacht cód agus in-atáirgtheacht na dtorthaí idir fhorbróirí le leaganacha éagsúla den OS agus leabharlanna, chomh maith le héascaíocht forghníomhaithe ar fhreastalaithe. Is féidir leat an píblíne oiliúna iomlán a sheoladh le hordú amháin.
  • Is bealach atá neamhdhíobhálach don bhuiséad é Google Cloud chun triail a bhaint as crua-earraí costasacha, ach ní mór duit cumraíochtaí a roghnú go cúramach.
  • Tá sé an-úsáideach luas blúirí cód aonair a thomhas, go háirithe agus R agus C++ le chéile, agus leis an bpacáiste forma - freisin an-éasca.

Tríd is tríd bhí an taithí seo an-sásúil agus leanaimid ag obair chun cuid de na saincheisteanna a ardaíodh a réiteach.

Foinse: will.com

Add a comment