Aithneachadh Quick Draw Doodle: mar a nì thu caraidean le R, C ++ agus lìonraidhean neural

Aithneachadh Quick Draw Doodle: mar a nì thu caraidean le R, C ++ agus lìonraidhean neural

Hi Habr!

An tuiteam mu dheireadh, chùm Kaggle farpais gus dealbhan air an tarraing le làimh a sheòrsachadh, Quick Draw Doodle Recognition, anns an do ghabh sgioba de luchd-saidheans R pàirt, am measg feadhainn eile: Artem Klevtsova, Manaidsear Philippa и Andrey Ogurtsov. Cha toir sinn cunntas mionaideach air a’ cho-fharpais; chaidh sin a dhèanamh mu thràth ann an fhoillseachadh o chionn ghoirid.

An turas seo cha do dh’obraich e a-mach le tuathanachas buinn, ach chaidh tòrr eòlas luachmhor fhaighinn, agus mar sin bu mhath leam innse don choimhearsnachd mu ghrunn de na rudan as inntinniche agus as fheumaile air Kagle agus ann an obair làitheil. Am measg nan cuspairean air an deach beachdachadh: beatha dhoirbh às aonais OpenCV, parsadh JSON (tha na h-eisimpleirean seo a’ sgrùdadh amalachadh còd C ++ ann an sgriobtaichean no pacaidean ann an R a’ cleachdadh Rcpp), parameterization de sgriobtaichean agus dockerization den fhuasgladh mu dheireadh. Tha a h-uile còd bhon teachdaireachd ann an cruth a tha freagarrach airson a chuir gu bàs ri fhaighinn ann an tasgaidh.

Clàr-innse:

  1. Luchdaich dàta gu h-èifeachdach bho CSV gu MonetDB
  2. Ag ullachadh batches
  3. Iterators airson batches a luchdachadh bhon stòr-dàta
  4. A 'taghadh ailtireachd modail
  5. Parameterization sgriobt
  6. Dockerization de sgriobtaichean
  7. A’ cleachdadh grunn GPUs air Google Cloud
  8. An àite a bhith co-dhùnadh

1. Thoir dàta gu h-èifeachdach bho CSV gu stòr-dàta MonetDB

Tha an dàta sa cho-fharpais seo air a thoirt seachad chan ann ann an cruth ìomhaighean deiseil, ach ann an cruth 340 faidhle CSV (aon fhaidhle airson gach clas) anns a bheil JSONs le co-chomharran puing. Le bhith a’ ceangal na puingean sin le loidhnichean, gheibh sinn dealbh mu dheireadh le tomhas 256x256 piogsail. Cuideachd airson gach clàr tha leubail a’ nochdadh an deach an dealbh aithneachadh gu ceart leis an neach-seòrsachaidh a chaidh a chleachdadh aig an àm a chaidh an dàta a chruinneachadh, còd dà-litir de dhùthaich còmhnaidh ùghdar an deilbh, aithnichear sònraichte, clàr-ama agus ainm clas a tha a rèir ainm an fhaidhle. Tha dreach nas sìmplidhe den dàta tùsail le cuideam 7.4 GB anns an tasglann agus timcheall air 20 GB às deidh a bhith air a dhì-phapadh, bidh an dàta slàn às deidh dì-phacadh a ’toirt suas 240 GB. Rinn an luchd-eagrachaidh cinnteach gun robh an dà dhreach ag ath-riochdachadh na h-aon dealbhan, a’ ciallachadh nach robh feum air an dreach slàn. Co-dhiù, bha stòradh 50 millean ìomhaigh ann am faidhlichean grafaigeach no ann an cruth arrays air a mheas sa bhad neo-phrothaideach, agus chuir sinn romhainn a h-uile faidhle CSV bhon tasglann a thoirt còmhla. train_simplified.zip a-steach don stòr-dàta le ginealach às deidh sin de dhealbhan den mheud riatanach “air an iteig” airson gach baidse.

Chaidh siostam dearbhte a thaghadh mar an DBMS MonetDB, is e sin buileachadh airson R mar phacaid MonetDBLite. Tha am pasgan a’ toirt a-steach dreach freumhaichte de fhrithealaiche an stòr-dàta agus leigidh e leat an frithealaiche a thogail gu dìreach bho sheisean R agus obrachadh leis an sin. Bithear a’ cruthachadh stòr-dàta agus a’ ceangal ris le aon àithne:

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

Feumaidh sinn dà chlàr a chruthachadh: aon airson a h-uile dàta, am fear eile airson fiosrachadh seirbheis mu fhaidhlichean a chaidh a luchdachadh sìos (feumail ma thèid rudeigin ceàrr agus feumar am pròiseas ath-thòiseachadh às deidh grunn fhaidhlichean a luchdachadh sìos):

A 'cruthachadh chlàran

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

B ’e an dòigh as luaithe air dàta a luchdachadh a-steach don stòr-dàta faidhlichean CSV a chopaigeadh gu dìreach a’ cleachdadh SQL - àithne COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTcàite tablename - ainm clàr agus path - an t-slighe chun an fhaidhle. Fhad 'sa bha e ag obair leis an tasglann, chaidh a lorg gun robh am buileachadh a-staigh unzip chan eil ann an R ag obair gu ceart le grunn fhaidhlichean bhon tasglann, agus mar sin chleachd sinn an siostam unzip (a’ cleachdadh am paramadair getOption("unzip")).

Gnìomh airson sgrìobhadh chun an stòr-dàta

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

Ma dh’ fheumas tu an clàr atharrachadh mus sgrìobh thu e chun stòr-dàta, tha e gu leòr airson an argamaid a chuir a-steach preprocess gnìomh a dh’ atharraicheas an dàta.

Còd airson dàta a luchdachadh a-steach don stòr-dàta ann an òrdugh:

A 'sgrìobhadh dàta gu stòr-dàta

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

Faodaidh ùine luchdachadh dàta atharrachadh a rèir feartan astair an draibhidh a thathar a’ cleachdadh. Anns a’ chùis againn, bheir leughadh agus sgrìobhadh taobh a-staigh aon SSD no bho dhràibhear flash (faidhle stòr) gu SSD (DB) nas lugha na 10 mionaidean.

Bheir e beagan dhiog a bharrachd colbh a chruthachadh le leubail clas slànaighear agus colbh clàr-amais (ORDERED INDEX) le àireamhan loidhne leis an tèid beachdan a shampallachadh nuair a thathar a’ cruthachadh batches:

A’ cruthachadh cholbhan agus clàr-amais a bharrachd

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

Gus fuasgladh fhaighinn air an duilgheadas a thaobh a bhith a 'cruthachadh baidse air a' chuileag, dh'fheumadh sinn an astar as àirde a choileanadh gus sreathan air thuaiream a thoirt a-mach às a 'bhòrd doodles. Airson seo chleachd sinn 3 cleasan. B’ e a’ chiad fhear meudachd an t-seòrsa a bhios a’ stòradh an ID amharc a lughdachadh. Anns an t-seata dàta tùsail, is e an seòrsa a tha a dhìth gus an ID a stòradh bigint, ach tha an àireamh de bheachdan ga dhèanamh comasach an aithnichearan, co-ionann ris an àireamh òrdail, a chur a-steach don t-seòrsa int. Tha an rannsachadh fada nas luaithe sa chùis seo. B 'e an dàrna cleas a chleachdadh ORDERED INDEX - thàinig sinn chun cho-dhùnadh seo gu empirigeach, às deidh dhuinn a dhol tro na bha ri fhaighinn варианты. B’ e an treas fear ceistean parameterized a chleachdadh. Is e brìgh an dòigh an òrdugh a chuir an gnìomh aon uair PREPARE le cleachdadh às deidh sin de abairt ullaichte nuair a bhios tu a’ cruthachadh dòrlach de cheistean den aon sheòrsa, ach gu dearbh tha buannachd ann an coimeas ri fear sìmplidh SELECT thionndaidh e a-mach gu robh e taobh a-staigh raon mearachd staitistigeil.

Cha bhith am pròiseas luchdachadh suas dàta a’ caitheamh barrachd air 450 MB de RAM. Is e sin, tha an dòigh-obrach a chaidh a mhìneachadh a’ toirt cothrom dhut dàta le cuideam deichean de gigabytes a ghluasad air cha mhòr bathar-cruaidh buidseit sam bith, a’ toirt a-steach cuid de dh’ innealan aon-bhòrd, rud a tha gu math fionnar.

Chan eil air fhàgail ach astar faighinn air ais dàta (air thuaiream) agus measadh a dhèanamh air an sgèile nuair a thathar a’ samplachadh batches de dhiofar mheudan:

Stòr-dàta comharran

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)

Aithneachadh Quick Draw Doodle: mar a nì thu caraidean le R, C ++ agus lìonraidhean neural

2. Ag ullachadh batches

Tha am pròiseas ullachaidh baidse gu lèir air a dhèanamh suas de na ceumannan a leanas:

  1. A’ parsadh grunn JSONn anns a bheil vectaran sreang le co-chomharran phuingean.
  2. A’ tarraing loidhnichean dathte stèidhichte air co-chomharran phuingean air ìomhaigh den mheud a tha a dhìth (mar eisimpleir, 256 × 256 no 128 × 128).
  3. Ag atharrachadh na h-ìomhaighean mar thoradh air gu tensor.

Mar phàirt den cho-fharpais am measg kernels Python, chaidh an duilgheadas fhuasgladh gu sònraichte le bhith a’ cleachdadh OpenCV. Bhiodh aon de na analogues as sìmplidh agus as follaisiche ann an R a’ coimhead mar seo:

A’ buileachadh JSON gu tionndadh Tensor ann an 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)
}

Thathas a’ dèanamh dealbh le bhith a’ cleachdadh innealan àbhaisteach R agus air a shàbhaladh gu PNG sealach air a stòradh ann an RAM (air Linux, tha clàran sealach R suidhichte san eòlaire /tmp, air a chuir suas ann an RAM). Tha am faidhle seo an uairsin air a leughadh mar raon trì-thaobhach le àireamhan eadar 0 is 1. Tha seo cudromach oir bhiodh BMP nas àbhaistich air a leughadh ann an sreath amh le còdan dath heics.

Feuch an dèan sinn deuchainn air an toradh:

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

Aithneachadh Quick Draw Doodle: mar a nì thu caraidean le R, C ++ agus lìonraidhean neural

Thèid am baidse fhèin a chruthachadh mar 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

Bha coltas nach robh am buileachadh seo cho math dhuinne, leis gu bheil cruthachadh batches mòra a’ toirt ùine gu h-iongantach fada, agus chuir sinn romhainn brath a ghabhail air eòlas ar co-obraichean le bhith a’ cleachdadh leabharlann cumhachdach. OpenCV. Aig an àm sin cha robh pasgan deiseil ann airson R (chan eil gin ann a-nis), agus mar sin chaidh glè bheag de bhuileachadh den ghnìomhachd riatanach a sgrìobhadh ann an C ++ le amalachadh a-steach do chòd R a’ cleachdadh Rcpp.

Gus an duilgheadas fhuasgladh, chaidh na pasganan agus na leabharlannan a leanas a chleachdadh:

  1. OpenCV airson a bhith ag obair le dealbhan agus loidhnichean tarraing. Chleachd sinn leabharlannan siostam ro-stàlaichte agus faidhlichean cinn, a bharrachd air ceangal fiùghantach.

  2. xtensor airson a bhith ag obair le arrays ioma-thaobhach agus tensors. Chleachd sinn faidhlichean cinn a bha sa phacaid R den aon ainm. Leigidh an leabharlann leat obrachadh le arrays ioma-thaobhach, an dà chuid ann an òrdugh prìomh shreath agus prìomh cholbh.

  3. ndjson airson JSON a pharsadh. Tha an leabharlann seo air a chleachdadh ann an xtensor gu fèin-ghluasadach ma tha e an làthair sa phròiseact.

  4. RcppThread airson a bhith ag eagrachadh giollachd ioma-snàthainn de vectar bho JSON. Chleachd sinn na faidhlichean cinn a thug a’ phacaid seo seachad. Bho nas mòr-chòrdte Rcpp Co-shìnte Anns a ’phacaid, am measg rudan eile, tha uidheamachd brisidh lùb togte.

Is fhiach a bhith mothachail sin xtensor thionndaidh e a-mach gu bhith na dhiadhachd: a bharrachd air gu bheil comas-gnìomh farsaing agus àrd-choileanadh aige, thionndaidh an luchd-leasachaidh gu bhith gu math freagairteach agus fhreagair iad ceistean gu sgiobalta agus gu mionaideach. Le an cuideachadh, bha e comasach cruth-atharrachaidhean matrices OpenCV a chuir an gnìomh gu tensor xtensor, a bharrachd air dòigh air tensor ìomhaighean 3-mheudach a chur còmhla ann an tensor 4-mheudach den mheud cheart (am baidse fhèin).

Stuthan airson ionnsachadh 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

Gus faidhlichean a chur ri chèile a chleachdas faidhlichean siostaim agus ceangal fiùghantach ri leabharlannan a chaidh a chuir a-steach air an t-siostam, chleachd sinn an uidheamachd plugan a chaidh a chuir an gnìomh sa phacaid Rcpp. Gus slighean agus brataichean a lorg gu fèin-ghluasadach, chleachd sinn goireas Linux mòr-chòrdte pkg-config.

Cur an gnìomh am plugan Rcpp airson an leabharlann OpenCV a chleachdadh

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 air gnìomhachd a ’phlug, thèid na luachan a leanas a chuir nan àite tron ​​​​phròiseas cruinneachaidh:

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"

Tha an còd buileachaidh airson JSON a pharsadh agus a’ gineadh baidse airson a chuir chun mhodail air a thoirt seachad fon spoiler. An toiseach, cuir a-steach eòlaire pròiseict ionadail gus faidhlichean cinn a lorg (feumar ndjson):

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

Cur an gnìomh JSON gu tionndadh tensor ann an 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;
}

Bu chòir an còd seo a chuir san fhaidhle src/cv_xt.cpp agus cuir ri chèile leis an àithne Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); cuideachd a dhìth airson obair nlohmann/json.hpp bho repozitoria. Tha an còd air a roinn ann an grunn ghnìomhan:

  • to_xt - gnìomh teamplaid airson atharrachadh matrix ìomhaigh (cv::Mat) gu tensor xt::xtensor;

  • parse_json - bidh an gnìomh a’ parsadh sreang JSON, a’ toirt a-mach co-chomharran phuingean, gan pacadh a-steach do vectar;

  • ocv_draw_lines - bhon vectar puingean a thig às, tarraing loidhnichean ioma-dathte;

  • process - a ’cothlamadh na gnìomhan gu h-àrd agus cuideachd a’ cur ris a ’chomas an ìomhaigh a thig às a sgèile;

  • cpp_process_json_str - wrapper thairis air a 'ghnìomh process, a tha a 'toirt a-mach an toradh gu rud R (sreath ioma-thaobhach);

  • cpp_process_json_vector - wrapper thairis air a 'ghnìomh cpp_process_json_str, a leigeas leat vectar sreang a phròiseasadh ann am modh ioma-snàithlean.

Gus loidhnichean ioma-dathte a tharraing, chaidh am modail dath HSV a chleachdadh, agus an uairsin tionndadh gu RGB. Feuch an dèan sinn deuchainn air an toradh:

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

Aithneachadh Quick Draw Doodle: mar a nì thu caraidean le R, C ++ agus lìonraidhean neural
Coimeas air astar buileachadh ann an 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") 

Aithneachadh Quick Draw Doodle: mar a nì thu caraidean le R, C ++ agus lìonraidhean neural

Mar a chì thu, bha an àrdachadh astair gu math cudromach, agus chan eil e comasach faighinn suas le còd C ++ le bhith a ’co-thaobhadh còd R.

3. Iterators airson batches a luchdachadh bhon stòr-dàta

Tha cliù airidh air R airson a bhith a’ giullachd dàta a tha a’ freagairt air RAM, fhad ‘s a tha Python nas comharraichte le giullachd dàta ath-aithriseach, a’ toirt cothrom dhut àireamhachadh taobh a-muigh bunaiteach a chuir an gnìomh gu furasta agus gu nàdarra (àireamhachadh a’ cleachdadh cuimhne bhon taobh a-muigh). Is e eisimpleir clasaigeach agus buntainneach dhuinn ann an co-theacsa na trioblaid a chaidh a mhìneachadh lìonraidhean neural domhainn air an trèanadh leis an dòigh teàrnadh caisead le tuairmse air an caisead aig gach ceum a’ cleachdadh cuibhreann bheag de bheachdan, no baidse beag.

Tha clasaichean sònraichte aig frèaman ionnsachaidh domhainn sgrìobhte ann am Python a bhios a’ cur an gnìomh luchd-aithris stèidhichte air dàta: clàran, dealbhan ann am pasganan, cruthan binary, msaa. Faodaidh tu roghainnean deiseil a chleachdadh no do chuid fhèin a sgrìobhadh airson gnìomhan sònraichte. Ann an R is urrainn dhuinn brath a ghabhail air na feartan uile aig leabharlann Python keras le na diofar backends aige a’ cleachdadh a’ phacaid den aon ainm, a tha e fhèin ag obair air mullach a’ phacaid ath-aithris. Tha an tè mu dheireadh airidh air artaigil fhada air leth; chan e a-mhàin gu bheil e a’ leigeil leat còd Python a ruith bho R, ach leigidh e leat cuideachd nithean a ghluasad eadar seiseanan R agus Python, a’ coileanadh a h-uile tionndadh seòrsa riatanach gu fèin-obrachail.

Fhuair sinn cuidhteas an fheum air an dàta gu lèir a stòradh ann an RAM le bhith a’ cleachdadh MonetDBLite, thèid an obair “lìonra neural” gu lèir a dhèanamh leis a’ chòd tùsail ann am Python, feumaidh sinn iterator a sgrìobhadh thairis air an dàta, leis nach eil dad deiseil airson suidheachadh mar sin ann an R no Python. Gu bunaiteach chan eil ann ach dà riatanas air a shon: feumaidh e batches a thilleadh ann an lùb gun chrìoch agus a staid a shàbhaladh eadar ath-aithrisean (tha an tè mu dheireadh ann an R air a chuir an gnìomh san dòigh as sìmplidh le bhith a ’cleachdadh dùnadh). Roimhe sin, bha feum air arrays R a thionndadh gu soilleir gu arrays numpy taobh a-staigh an iterator, ach an dreach làithreach den phacaid keras ga dhèanamh fhèin.

B’ e an iterator airson dàta trèanaidh is dearbhaidh mar a leanas:

Iterator airson trèanadh agus dàta dearbhaidh

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

Tha an gnìomh a’ gabhail a-steach caochladair le ceangal ris an stòr-dàta, na h-àireamhan de loidhnichean a chaidh a chleachdadh, an àireamh de chlasaichean, meud baidse, sgèile (scale = 1 a’ freagairt ri dealbhan 256x256 piogsail, scale = 0.5 - 128x128 piogsail), comharra dath (color = FALSE a’ sònrachadh cuibhreann ann an sgèile-gràn nuair a thèid a chleachdadh color = TRUE tha gach stròc air a tharraing ann an dath ùr) agus comharra ro-phròiseas airson lìonraidhean air an trèanadh ro-làimh air imagenet. Tha feum air an fhear mu dheireadh gus luachan piogsail a sgèileadh bhon eadar-ama [0, 1] chun an eadar-ama [-1, 1], a chaidh a chleachdadh nuair a bha e a’ trèanadh na chaidh a thoirt seachad. keras modailean.

Anns a’ ghnìomh taobh a-muigh tha sgrùdadh seòrsa argamaid, clàr data.table le àireamhan loidhne measgaichte air thuaiream bho samples_index agus àireamhan baidse, cuntair agus an àireamh as motha de batches, a bharrachd air abairt SQL airson dàta a luchdachadh bhon stòr-dàta. A bharrachd air an sin, mhìnich sinn analogue luath den ghnìomh a-staigh keras::to_categorical(). Chleachd sinn cha mhòr a h-uile dàta airson trèanadh, a’ fàgail leth sa cheud airson dearbhadh, agus mar sin bha meud na h-ùine air a chuingealachadh leis a’ pharamadair steps_per_epoch nuair a chaidh a ghairm keras::fit_generator(), agus an staid if (i > max_i) ag obair airson an neach-aithris dearbhaidh a-mhàin.

Anns a’ ghnìomh a-staigh, thathas a’ faighinn clàran-amais sreath air ais airson an ath bhaidse, tha clàran gan luchdachadh bhon stòr-dàta leis a’ chunntair baidse a’ dol am meud, JSON parsing (gnìomh cpp_process_json_vector(), sgrìobhte ann an C ++) agus a’ cruthachadh arrays a fhreagras air dealbhan. An uairsin thèid vectaran aon-teth le bileagan clas a chruthachadh, tha arrays le luachan piogsail agus bileagan air an cur còmhla ann an liosta, is e sin an luach tilleadh. Gus obair a luathachadh, chleachd sinn cruthachadh chlàran-amais ann an clàran data.table agus atharrachadh tron ​​​​cheangal - às aonais na “chips” pacaid sin dàta.clàr Tha e gu math duilich smaoineachadh air obrachadh gu h-èifeachdach le tomhas mòr de dhàta ann an R.

Tha toraidhean tomhais astair air laptop Core i5 mar a leanas:

Slat-tomhais airson iterator

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

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

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

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

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

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

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Aithneachadh Quick Draw Doodle: mar a nì thu caraidean le R, C ++ agus lìonraidhean neural

Ma tha gu leòr de RAM agad, faodaidh tu obrachadh an stòr-dàta a luathachadh gu mòr le bhith ga ghluasad chun aon RAM seo (tha 32 GB gu leòr airson ar gnìomh). Ann an Linux, tha an sgaradh air a chuir suas gu bunaiteach /dev/shm, a’ gabhail thairis suas ri leth comas RAM. Faodaidh tu barrachd a chomharrachadh le bhith a’ deasachadh /etc/fstabgus clàr fhaighinn mar tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Dèan cinnteach gun ath-thòisich thu agus thoir sùil air an toradh le bhith a’ ruith an àithne df -h.

Tha an iterator airson dàta deuchainn a’ coimhead tòrr nas sìmplidhe, leis gu bheil an dàta deuchainn a’ freagairt gu tur ri RAM:

Iterator airson dàta deuchainn

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. Taghadh de ailtireachd modail

B 'e a' chiad ailtireachd a chaidh a chleachdadh mobilenet v1, air a bheilear a’ beachdachadh air na feartan aca ann an seo teachdaireachd. Tha e air a thoirt a-steach mar ìre àbhaisteach keras agus, a rèir sin, ri fhaighinn anns a 'phacaid den aon ainm airson R. Ach nuair a thathar a' feuchainn ri a chleachdadh le ìomhaighean aon-seanail, thionndaidh rud neònach a-mach: feumaidh an tomhas a bhith aig an tensor cuir a-steach an-còmhnaidh (batch, height, width, 3), is e sin, chan urrainnear an àireamh de shianalan atharrachadh. Chan eil a leithid de chuingealachadh ann am Python, agus mar sin rinn sinn cabhag agus sgrìobh sinn ar buileachadh fhèin den ailtireachd seo, a’ leantainn an artaigil thùsail (às aonais an dropout a tha san dreach keras):

Mobilenet v1 ailtireachd

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

Tha eas-bhuannachdan an dòigh-obrach seo follaiseach. Tha mi airson tòrr mhodalan a dhearbhadh, ach air an làimh eile, chan eil mi airson gach ailtireachd ath-sgrìobhadh le làimh. Cha robh cothrom againn cuideachd cuideaman mhodalan a chaidh an trèanadh ro-làimh air imagenet a chleachdadh. Mar as àbhaist, chuidich sgrùdadh nan sgrìobhainnean. Gnìomh get_config() a’ leigeil leat tuairisgeul fhaighinn air a’ mhodail ann an cruth a tha freagarrach airson deasachadh (base_model_conf$layers - liosta R cunbhalach), agus an gnìomh from_config() a 'dèanamh an tionndadh cùil gu nì modail:

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)

A-nis chan eil e duilich gnìomh uile-choitcheann a sgrìobhadh gus gin de na chaidh a thoirt seachad fhaighinn keras modalan le no às aonais cuideaman air an trèanadh air imagenet:

Gnìomh airson ailtireachd deiseil a luchdachadh

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

Nuair a bhios tu a’ cleachdadh ìomhaighean aon-seanail, chan eilear a’ cleachdadh cuideaman ro-thrèanadh. Dh’ fhaodadh seo a bhith air a shuidheachadh: a’ cleachdadh a’ ghnìomh get_weights() faigh na cuideaman modail ann an cruth liosta de chlàran R, atharraich meud a 'chiad eileamaid den liosta seo (le bhith a' gabhail aon sianal dath no a 'toirt a-mach na trì gu cuibheasach), agus an uairsin luchdaich na cuideaman air ais dhan mhodail leis a' ghnìomh set_weights(). Cha do chuir sinn a-steach an gnìomh seo a-riamh, oir aig an ìre seo bha e soilleir mar-thà gu robh e na bu buannachdail a bhith ag obair le dealbhan dathte.

Rinn sinn a’ mhòr-chuid de na deuchainnean a’ cleachdadh dreachan mobilenet 1 agus 2, a bharrachd air resnet34. Rinn ailtireachd nas ùire leithid SE-ResNeXt gu math san fharpais seo. Gu mì-fhortanach, cha robh buileachadh deiseil againn, agus cha do sgrìobh sinn ar cuid fhèin (ach sgrìobhaidh sinn gu cinnteach).

5. Parameterization de sgriobtaichean

Airson goireasachd, chaidh a h-uile còd airson tòiseachadh air trèanadh a dhealbhadh mar aon sgriobt, le paramadair a’ cleachdadh docopt mar 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)

Pasgan docopt a’ riochdachadh buileachadh http://docopt.org/ airson R. Le a chuideachadh, tha sgriobtaichean air an cur air bhog le òrdughan sìmplidh mar Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db no ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, ma tha faidhle train_nn.R a ghabhas coileanadh (tòisichidh an àithne seo a’ trèanadh a’ mhodail resnet50 air ìomhaighean trì-dath le tomhas 128x128 piogsail, feumaidh an stòr-dàta a bhith suidhichte sa phasgan /home/andrey/doodle_db). Faodaidh tu astar ionnsachaidh, seòrsa optimizer, agus paramadairean gnàthaichte sam bith eile a chuir ris an liosta. Anns a 'phròiseas ag ullachadh an fhoillseachaidh, thionndaidh e a-mach gun robh an ailtireachd mobilenet_v2 bhon tionndadh làithreach keras ann an cleachdadh R chan urrainn mar thoradh air atharrachaidhean nach deach a ghabhail a-steach sa phacaid R, tha sinn a’ feitheamh riutha a chàradh.

Rinn an dòigh-obrach seo comasach air deuchainnean a luathachadh gu mòr le diofar mhodalan an taca ri cur air bhog nas traidiseanta de sgriobtaichean ann an RStudio (tha sinn a’ toirt fa-near don phacaid mar roghainn eile a dh’ fhaodadh a bhith ann). tfruns). Ach is e am prìomh bhuannachd an comas foillseachadh sgriobtaichean ann an Docker a riaghladh gu furasta no dìreach air an fhrithealaiche, gun a bhith a’ stàladh RStudio airson seo.

6. Dockerization de sgriobtaichean

Chleachd sinn Docker gus dèanamh cinnteach à comas giùlain na h-àrainneachd airson modalan trèanaidh eadar buill sgioba agus airson cleachdadh luath san sgòth. Faodaidh tu tòiseachadh air eòlas fhaighinn air an inneal seo, a tha gu math neo-àbhaisteach do phrògramadair R, le seo sreath de fhoillseachaidhean no cùrsa bhidio.

Leigidh Docker leat an dà chuid na h-ìomhaighean agad fhèin a chruthachadh bhon fhìor thoiseach agus ìomhaighean eile a chleachdadh mar bhunait airson do chuid fhèin a chruthachadh. Nuair a bha sinn a’ dèanamh anailis air na roghainnean a bha rim faighinn, thàinig sinn chun cho-dhùnadh gu bheil stàladh draibhearan NVIDIA, CUDA + cuDNN agus leabharlannan Python na phàirt meadhanach mòr den ìomhaigh, agus chuir sinn romhainn an ìomhaigh oifigeil a ghabhail mar bhunait. tensorflow/tensorflow:1.12.0-gpu, a 'cur ris na pasganan R riatanach an sin.

Bha am faidhle docker mu dheireadh a’ coimhead mar seo:

Faidhle docker

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

Airson goireasachd, chaidh na pacaidean a chaidh a chleachdadh a chuir ann an caochladairean; thèid a’ mhòr-chuid de na sgriobtaichean sgrìobhte a chopaigeadh am broinn na soithichean aig àm co-chruinneachadh. Dh'atharraich sinn cuideachd an t-slige àithne gu /bin/bash airson susbaint a chleachdadh gu furasta /etc/os-release. Sheachain seo an fheum air an tionndadh OS a shònrachadh sa chòd.

A bharrachd air an sin, chaidh sgriobt bash beag a sgrìobhadh a leigeas leat soitheach a chuir air bhog le diofar òrdughan. Mar eisimpleir, dh’ fhaodadh iad seo a bhith nan sgriobtaichean airson a bhith a’ trèanadh lìonraidhean neural a chaidh a chuir am broinn an t-soithich roimhe seo, no slige òrduigh airson dì-bhugachadh agus sùil a chumail air obrachadh a’ bhogsa:

Sgriobt gus an soitheach a chuir air bhog

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

Ma thèid an sgriobt bash seo a ruith gun pharamadairean, thèid an sgriobt a ghairm taobh a-staigh an t-soithich train_nn.R le luachan bunaiteach; mas e “bash” a’ chiad argamaid suidheachaidh, tòisichidh an soitheach gu h-eadar-ghnìomhach le slige àithne. Anns a h-uile cùis eile, thèid luachan argamaidean suidheachaidh a chuir an àite: CMD="Rscript /app/train_nn.R $@".

Is fhiach a bhith mothachail gu bheil na clàran le dàta stòr agus stòr-dàta, a bharrachd air an eòlaire airson modalan trèanaidh a shàbhaladh, air an cur suas taobh a-staigh an t-soithich bhon t-siostam aoigheachd, a leigeas leat faighinn gu toraidhean nan sgriobtaichean gun làimhseachadh neo-riatanach.

7. A 'cleachdadh ioma GPUs air Google Cloud

B’ e aon de fheartan na farpais an dàta fìor fhuaimneach (faic an dealbh tiotal, air iasad bho @Leigh.plt bho ODS slack). Bidh baidsean mòra a’ cuideachadh le bhith a’ sabaid seo, agus às deidh deuchainnean air PC le 1 GPU, chuir sinn romhainn maighstireachd a dhèanamh air modalan trèanaidh air grunn GPUs san sgòth. Chleachdadh GoogleCloud (stiùireadh math air bunaitean) mar thoradh air an taghadh mòr de rèiteachaidhean a tha rim faighinn, prìsean reusanta agus bònas $ 300. A-mach à sannt, dh ’òrduich mi eisimpleir 4xV100 le SSD agus tunna de RAM, agus b’ e mearachd mòr a bha sin. Bidh inneal mar seo ag ithe airgead gu sgiobalta; faodaidh tu a dhol a-mach a ’feuchainn às aonais loidhne-phìoban dearbhte. Airson adhbharan foghlaim, tha e nas fheàrr an K80 a ghabhail. Ach thàinig an ìre mhòr de RAM a-steach - cha do chòrd an sgòth SSD ris a choileanadh, agus mar sin chaidh an stòr-dàta a ghluasad gu dev/shm.

Is e an ùidh as motha am pìos còd le uallach airson a bhith a’ cleachdadh ioma GPUs. An toiseach, tha am modail air a chruthachadh air an CPU a’ cleachdadh manaidsear co-theacsa, dìreach mar ann am 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
  )
})

An uairsin tha am modail neo-ullaichte (tha seo cudromach) air a chopaigeadh gu àireamh sònraichte de GPUs a tha rim faighinn, agus dìreach às deidh sin tha e air a chur ri chèile:

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

Cha b 'urrainnear an dòigh clasaigeach de reothadh a h-uile sreathan ach a-mhàin an tè mu dheireadh, a' trèanadh an ìre mu dheireadh, gun reothadh agus ath-thrèanadh a 'mhodail gu lèir airson grunn GPUs.

Chaidh sùil a chumail air trèanadh gun fheum. tensorboard, gan cuingealachadh fhèin gu bhith a’ clàradh logaichean agus a’ sàbhaladh mhodalan le ainmean fiosrachail às deidh gach àm:

Glaodh air 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. An àite co-dhùnadh

Tha grunn dhuilgheadasan air an do choinnich sinn nach deach faighinn seachad orra fhathast:

  • в keras chan eil gnìomh deiseil ann airson a bhith a’ lorg gu fèin-ghluasadach airson an ìre ionnsachaidh as fheàrr (analog lr_finder anns an leabharlann luath.ai.); Le beagan oidhirp, tha e comasach buileachadh treas-phàrtaidhean a ghluasad gu R, mar eisimpleir, seo;
  • mar thoradh air a’ phuing roimhe, cha robh e comasach an astar trèanaidh ceart a thaghadh nuair a bha thu a’ cleachdadh grunn GPUs;
  • tha gainnead ann an ailtireachd lìonra neural an latha an-diugh, gu h-àraidh an fheadhainn a fhuair trèanadh ro-làimh air imagenet;
  • cha robh poileasaidh cearcall sam bith agus ìrean ionnsachaidh lethbhreith (bha anailachadh cosine air an iarrtas againn air a chur an gnìomh, Tapadh leat sgadan).

Dè na rudan feumail a chaidh ionnsachadh bhon cho-fharpais seo:

  • Air bathar-cruaidh le cumhachd an ìre mhath ìosal, faodaidh tu obrachadh le meudan dàta iomchaidh (iomadh uair nas motha na RAM) gun phian. Poca plastaig dàta.clàr a’ sàbhaladh cuimhne mar thoradh air mion-atharrachadh chlàran a-staigh, a sheachnas a bhith gan lethbhreacadh, agus nuair a thèid an cleachdadh gu ceart, bidh na comasan aige cha mhòr an-còmhnaidh a’ nochdadh an astar as àirde am measg nan innealan air fad as aithne dhuinn airson cànanan sgrìobhaidh. Le bhith a’ sàbhaladh dàta ann an stòr-dàta leigidh sin leat, ann an iomadh cùis, gun a bhith a’ smaoineachadh idir mun fheum air an dàta gu lèir a bhrùthadh gu RAM.
  • Faodar gnìomhan slaodach ann an R a chuir an àite feadhainn luath ann an C ++ a’ cleachdadh a’ phacaid Rcpp. Ma tha e a bharrachd air a chleachdadh RcppThread no Rcpp Co-shìnte, bidh sinn a’ faighinn buileachadh ioma-snàthainn tar-àrd-ùrlar, agus mar sin chan fheumar an còd a cho-thaobhadh aig ìre R.
  • Pacaid Rcpp faodar a chleachdadh gun eòlas mòr air C ++, tha an ìre as ìsle air a mhìneachadh an seo. Faidhlichean cinn airson grunn leabharlannan C fionnar mar xtensor ri fhaighinn air CRAN, is e sin, thathas a’ cruthachadh bun-structar airson pròiseactan a chuir an gnìomh a bhios ag amalachadh còd C ++ àrd-choileanadh deiseil ann an R. Is e goireasachd a bharrachd a bhith a’ soilleireachadh co-chòrdadh agus anailis còd C ++ statach ann an RStudio.
  • docopt a’ leigeil leat sgriobtaichean fèin-chumanta a ruith le paramadairean. Tha seo goireasach airson a chleachdadh air frithealaiche iomallach, incl. fo docker. Ann an RStudio, tha e mì-ghoireasach mòran uairean a thìde de dheuchainnean a dhèanamh le bhith a’ trèanadh lìonraidhean neural, agus chan eil e an-còmhnaidh reusanta an IDE a chuir a-steach air an fhrithealaiche fhèin.
  • Bidh Docker a’ dèanamh cinnteach à comas giùlain còd agus ath-riochdachadh thoraidhean eadar luchd-leasachaidh le dreachan eadar-dhealaichte den OS agus leabharlannan, a bharrachd air a bhith furasta an coileanadh air frithealaichean. Faodaidh tu an loidhne-phìoban trèanaidh gu lèir a chuir air bhog le dìreach aon àithne.
  • Tha Google Cloud na dhòigh buidseit airson deuchainn a dhèanamh air bathar-cruaidh daor, ach feumaidh tu rèiteachadh a thaghadh gu faiceallach.
  • Tha e glè fheumail astar criomagan còd fa leth a thomhas, gu sònraichte nuair a thathar a’ cothlamadh R agus C ++, agus leis a’ phacaid beinn - cuideachd gu math furasta.

Uile gu lèir bha an t-eòlas seo air leth buannachdail agus tha sinn a’ leantainn oirnn ag obair gus fuasgladh fhaighinn air cuid de na cùisean a chaidh a thogail.

Source: www.habr.com

Cuir beachd ann