Quick Draw Doodle Recognition: hvernig á að eignast vini með R, C++ og taugakerfi

Quick Draw Doodle Recognition: hvernig á að eignast vini með R, C++ og taugakerfi

Hæ Habr!

Síðasta haust stóð Kaggle fyrir keppni til að flokka handteiknaðar myndir, Quick Draw Doodle Recognition, þar sem meðal annars teymi R-vísindamanna tók þátt: Artem Klevtsova, Philippa framkvæmdastjóri и Andrey Ogurtsov. Við munum ekki lýsa keppninni í smáatriðum; það hefur þegar verið gert í nýleg útgáfa.

Að þessu sinni gekk það ekki upp með verðlaunabúskap, en mikil og dýrmæt reynsla var fengin og því langar mig að segja samfélaginu frá ýmsum áhugaverðustu og gagnlegustu hlutunum á Kagle og í daglegu starfi. Meðal umræðuefna: erfitt líf án OpenCV, JSON þáttun (þessi dæmi skoða samþættingu C++ kóða í forskriftir eða pakka í R með Rcpp), breytugreiningu á forskriftum og tengingu endanlegrar lausnar. Allur kóði úr skilaboðunum á formi sem hentar til framkvæmdar er fáanlegur í geymslum.

Efnisyfirlit:

  1. Hladdu gögnum frá CSV á skilvirkan hátt í MonetDB
  2. Undirbúningur lotur
  3. Ítrekanir til að losa runur úr gagnagrunninum
  4. Að velja fyrirmyndararkitektúr
  5. Forskriftarstillingu
  6. Dockerization handrita
  7. Notkun margra GPU á Google Cloud
  8. Í stað þess að niðurstöðu

1. Hladdu gögnum frá CSV á skilvirkan hátt inn í MonetDB gagnagrunninn

Gögnin í þessari keppni eru ekki veitt í formi tilbúinna mynda, heldur í formi 340 CSV skráa (ein skrá fyrir hvern flokk) sem innihalda JSON með punkthnit. Með því að tengja þessa punkta með línum fáum við endanlega mynd sem mælist 256x256 dílar. Einnig fyrir hverja skrá er merkimiði sem gefur til kynna hvort myndin hafi verið rétt viðurkennd af flokkunaraðilanum sem notaður var á þeim tíma sem gagnasafninu var safnað, tveggja stafa kóði búsetulands höfundar myndarinnar, einstakt auðkenni, tímastimpill og flokksheiti sem passar við skráarnafnið. Einföld útgáfa af upprunalegu gögnunum vegur 7.4 GB í skjalasafninu og um það bil 20 GB eftir upptöku, öll gögnin eftir upptöku taka upp 240 GB. Skipuleggjendur tryggðu að báðar útgáfurnar endurgerðu sömu teikningarnar, sem þýðir að heildarútgáfan var óþörf. Hvað sem því líður var það strax talið óarðbært að geyma 50 milljónir mynda í grafískum skrám eða í formi fylkinga og við ákváðum að sameina allar CSV skrár úr skjalasafninu train_simplified.zip inn í gagnagrunninn með síðari myndum af tilskildri stærð „á flugi“ fyrir hverja lotu.

Vel sannað kerfi var valið sem DBMS MonetDB, nefnilega útfærsla fyrir R sem pakka MonetDBLite. Pakkinn inniheldur innbyggða útgáfu af gagnagrunnsþjóninum og gerir þér kleift að taka upp þjóninn beint úr R lotu og vinna með hann þar. Að búa til gagnagrunn og tengjast honum eru framkvæmdar með einni skipun:

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

Við þurfum að búa til tvær töflur: eina fyrir öll gögn, hin fyrir þjónustuupplýsingar um niðurhalaðar skrár (gagnlegt ef eitthvað fer úrskeiðis og ferlið þarf að halda áfram eftir að hafa hlaðið niður nokkrum skrám):

Að búa til töflur

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

Fljótlegasta leiðin til að hlaða gögnum inn í gagnagrunninn var að afrita CSV skrár beint með því að nota SQL - skipun COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORThvar tablename - nafn töflu og path - slóðin að skránni. Þegar unnið var með skjalasafnið kom í ljós að innbyggða útfærslan unzip í R virkar ekki rétt með fjölda skráa úr skjalasafninu, svo við notuðum kerfið unzip (með því að nota færibreytuna getOption("unzip")).

Virkni til að skrifa í gagnagrunninn

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

Ef þú þarft að umbreyta töflunni áður en þú skrifar hana í gagnagrunninn er nóg að fara í rökfærsluna preprocess aðgerð sem mun umbreyta gögnunum.

Kóði til að hlaða gögnum í röð í gagnagrunninn:

Að skrifa gögn í gagnagrunninn

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

Gagnahleðslutími getur verið breytilegur eftir hraðaeiginleikum drifsins sem notað er. Í okkar tilviki tekur lestur og ritun innan einnar SSD eða frá flash-drifi (frumskrá) yfir í SSD (DB) innan við 10 mínútur.

Það tekur nokkrar sekúndur í viðbót að búa til dálk með heiltöluflokksmerki og vísidálki (ORDERED INDEX) með línunúmerum sem athuganir verða teknar eftir þegar runur eru búnar til:

Að búa til viðbótardálka og vísitölu

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

Til að leysa vandamálið við að búa til lotu á flugu þurftum við að ná hámarkshraða við að draga handahófskenndar raðir úr töflunni doodles. Til þess notuðum við 3 brellur. Í fyrsta lagi var að draga úr vídd tegundarinnar sem geymir athugunarauðkennið. Í upprunalegu gagnasettinu er gerðin sem þarf til að geyma auðkennið bigint, en fjöldi athugana gerir það mögulegt að passa auðkenni þeirra, jafnt og raðtölu, inn í gerðina int. Leitin er miklu hraðari í þessu tilfelli. Annað bragðið var að nota ORDERED INDEX — Við komumst að þessari ákvörðun með reynslu, eftir að hafa farið í gegnum allt tiltækt valkostir. Þriðja var að nota færibreytur fyrirspurnir. Kjarni aðferðarinnar er að framkvæma skipunina einu sinni PREPARE með síðari notkun á tilbúinni tjáningu þegar búið er til fullt af fyrirspurnum af sömu gerð, en í raun er það kostur í samanburði við einfalda SELECT reyndist vera innan marka tölfræðilegra skekkju.

Ferlið við að hlaða upp gögnum eyðir ekki meira en 450 MB af vinnsluminni. Það er að segja, aðferðin sem lýst er gerir þér kleift að færa gagnasöfn sem vega tugi gígabæta á næstum hvaða fjárhagsáætlun vélbúnaði sem er, þar á meðal sum eins borðs tæki, sem er frekar flott.

Allt sem er eftir er að mæla hraðann við að sækja (tilviljanakenndar) gögn og meta mælikvarðana þegar sýnatökur eru teknar af mismunandi stærðum:

Viðmið gagnagrunns

library(ggplot2)

set.seed(0)
# Подключение к базе данных
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))

# Функция для подготовки запроса на стороне сервера
prep_sql <- function(batch_size) {
  sql <- sprintf("PREPARE SELECT id FROM doodles WHERE id IN (%s)",
                 paste(rep("?", batch_size), collapse = ","))
  res <- DBI::dbSendQuery(con, sql)
  return(res)
}

# Функция для извлечения данных
fetch_data <- function(rs, batch_size) {
  ids <- sample(seq_len(n), batch_size)
  res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
  return(res)
}

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

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: hvernig á að eignast vini með R, C++ og taugakerfi

2. Undirbúningur lotur

Allt framleiðsluferlið fyrir lotu samanstendur af eftirfarandi skrefum:

  1. Að þátta nokkur JSON sem innihalda vektora af strengjum með hnitum punkta.
  2. Teiknaðu litaðar línur byggðar á hnitum punkta á mynd af nauðsynlegri stærð (til dæmis 256×256 eða 128×128).
  3. Breytir myndunum sem myndast í tensor.

Sem hluti af samkeppninni meðal Python kjarna var vandamálið leyst fyrst og fremst með því að nota OpenCV. Ein einfaldasta og augljósasta hliðstæðan í R myndi líta svona út:

Innleiðing JSON í Tensor umbreytingu í 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)
}

Teikning er framkvæmd með venjulegum R verkfærum og vistuð í tímabundið PNG sem er geymt í vinnsluminni (á Linux eru tímabundnar R möppur staðsettar í möppunni /tmp, fest í vinnsluminni). Þessi skrá er síðan lesin sem þrívítt fylki með tölum á bilinu 0 til 1. Þetta er mikilvægt vegna þess að hefðbundnari BMP væri lesið inn í hrátt fylki með hex litakóðum.

Prófum niðurstöðuna:

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

Quick Draw Doodle Recognition: hvernig á að eignast vini með R, C++ og taugakerfi

Lotan sjálf verður mynduð sem hér segir:

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

Þessi útfærsla þótti okkur ekki ákjósanleg þar sem myndun stórra lota tekur ósæmilega langan tíma og við ákváðum að nýta reynslu samstarfsmanna okkar með því að nota öflugt bókasafn OpenCV. Á þeim tíma var enginn tilbúinn pakki fyrir R (það er enginn núna), þannig að lágmarks útfærsla á nauðsynlegri virkni var skrifuð í C++ með samþættingu í R kóða með Rcpp.

Til að leysa vandamálið voru eftirfarandi pakkar og bókasöfn notuð:

  1. OpenCV til að vinna með myndir og teikna línur. Notaði fyrirfram uppsett kerfissöfn og hausaskrár, svo og kraftmikla tengingu.

  2. xtensor til að vinna með fjölvíddar fylki og tensora. Við notuðum hausskrár sem eru innifalin í R pakkanum með sama nafni. Bókasafnið gerir þér kleift að vinna með fjölvíddar fylki, bæði í stórum röðum og dálkum.

  3. ndjson fyrir þáttun JSON. Þetta bókasafn er notað í xtensor sjálfkrafa ef það er til staðar í verkefninu.

  4. RcppÞráður til að skipuleggja fjölþráða vinnslu á vektor frá JSON. Notaði hausskrárnar sem þessi pakki býður upp á. Frá vinsælli RcppParallel Í pakkanum er meðal annars innbyggt lykkjurofkerfi.

Það skal tekið fram að xtensor reyndist vera guðsgjöf: auk þess að það hefur mikla virkni og mikla afköst, reyndust verktaki þess vera mjög móttækilegur og svöruðu spurningum tafarlaust og ítarlega. Með hjálp þeirra var hægt að útfæra umbreytingar á OpenCV fylki í xtensor tensora, sem og leið til að sameina þrívíddar myndtensora í 3-víddar tensor af réttri vídd (lotan sjálf).

Efni til að læra Rcpp, xtensor og 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

Til að setja saman skrár sem nota kerfisskrár og kraftmikla tengingu við bókasöfn sem eru uppsett á kerfinu, notuðum við viðbótina sem er útfærð í pakkanum Rcpp. Til að finna slóðir og fána sjálfkrafa notuðum við vinsælt Linux tól pkg-config.

Innleiðing á Rcpp viðbótinni til að nota OpenCV bókasafnið

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

Sem afleiðing af notkun viðbótarinnar verður eftirfarandi gildi skipt út í söfnunarferlinu:

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"

Útfærslukóðinn fyrir að flokka JSON og búa til lotu fyrir sendingu á líkanið er gefinn undir spilli. Bættu fyrst við staðbundinni verkefnaskrá til að leita að hausskrám (þarf fyrir ndjson):

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

Útfærsla á JSON til tensor umbreytingu í 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;
}

Þessi kóða ætti að vera settur í skrána src/cv_xt.cpp og settu saman með skipuninni Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); einnig krafist fyrir vinnu nlohmann/json.hpp á geymsla. Kóðanum er skipt í nokkrar aðgerðir:

  • to_xt — sniðmát aðgerð til að umbreyta myndfylki (cv::Mat) til tensor xt::xtensor;

  • parse_json — fallið greinir JSON streng, dregur út hnit punkta, pakkar þeim í vektor;

  • ocv_draw_lines — dregur marglitar línur úr punktvigrunni sem myndast;

  • process — sameinar ofangreindar aðgerðir og bætir einnig við getu til að skala myndina sem myndast;

  • cpp_process_json_str - vefja yfir aðgerðina process, sem flytur niðurstöðuna út í R-hlut (fjölvíddar fylki);

  • cpp_process_json_vector - vefja yfir aðgerðina cpp_process_json_str, sem gerir þér kleift að vinna strengvigur í fjölþráða ham.

Til að teikna marglitar línur var HSV litalíkanið notað og síðan breytt í RGB. Prófum niðurstöðuna:

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

Quick Draw Doodle Recognition: hvernig á að eignast vini með R, C++ og taugakerfi
Samanburður á hraða útfærslu í R og C++

res_bench <- bench::mark(
  r_process_json_str(tmp_data[4, drawing], scale = 0.5),
  cpp_process_json_str(tmp_data[4, drawing], scale = 0.5),
  check = FALSE,
  min_iterations = 100
)
# Параметры бенчмарка
cols <- c("expression", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   expression                min     median       max `itr/sec` total_time  n_itr
#   <chr>                <bch:tm>   <bch:tm>  <bch:tm>     <dbl>   <bch:tm>  <int>
# 1 r_process_json_str     3.49ms     3.55ms    4.47ms      273.      490ms    134
# 2 cpp_process_json_str   1.94ms     2.02ms    5.32ms      489.      497ms    243

library(ggplot2)
# Проведение замера
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    .data <- tmp_data[sample(seq_len(.N), batch_size), drawing]
    bench::mark(
      r_process_json_vector(.data, scale = 0.5),
      cpp_process_json_vector(.data,  scale = 0.5),
      min_iterations = 50,
      check = FALSE
    )
  }
)

res_bench[, cols]

#    expression   batch_size      min   median      max `itr/sec` total_time n_itr
#    <chr>             <dbl> <bch:tm> <bch:tm> <bch:tm>     <dbl>   <bch:tm> <int>
#  1 r                   16   50.61ms  53.34ms  54.82ms    19.1     471.13ms     9
#  2 cpp                 16    4.46ms   5.39ms   7.78ms   192.      474.09ms    91
#  3 r                   32   105.7ms 109.74ms 212.26ms     7.69        6.5s    50
#  4 cpp                 32    7.76ms  10.97ms  15.23ms    95.6     522.78ms    50
#  5 r                   64  211.41ms 226.18ms 332.65ms     3.85      12.99s    50
#  6 cpp                 64   25.09ms  27.34ms  32.04ms    36.0        1.39s    50
#  7 r                  128   534.5ms 627.92ms 659.08ms     1.61      31.03s    50
#  8 cpp                128   56.37ms  58.46ms  66.03ms    16.9        2.95s    50
#  9 r                  256     1.15s    1.18s    1.29s     0.851     58.78s    50
# 10 cpp                256  114.97ms 117.39ms 130.09ms     8.45       5.92s    50
# 11 r                  512     2.09s    2.15s    2.32s     0.463       1.8m    50
# 12 cpp                512  230.81ms  235.6ms 261.99ms     4.18      11.97s    50
# 13 r                 1024        4s    4.22s     4.4s     0.238       3.5m    50
# 14 cpp               1024  410.48ms 431.43ms 462.44ms     2.33      21.45s    50

ggplot(res_bench, aes(x = factor(batch_size), y = median, 
                      group =  expression, color = expression)) +
  geom_point() +
  geom_line() +
  ylab("median time, s") +
  theme_minimal() +
  scale_color_discrete(name = "", labels = c("cpp", "r")) +
  theme(legend.position = "bottom") 

Quick Draw Doodle Recognition: hvernig á að eignast vini með R, C++ og taugakerfi

Eins og þú sérð reyndist hraðaaukningin vera mjög veruleg og það er ekki hægt að ná C++ kóða með samhliða R kóða.

3. Ítrekanir til að losa lotur úr gagnagrunninum

R hefur verðskuldað orðspor fyrir að vinna úr gögnum sem passa í vinnsluminni, en Python einkennist frekar af endurtekinni gagnavinnslu, sem gerir þér kleift að útfæra útreikninga utan kjarna (útreikningar með ytra minni) á auðveldan og eðlilegan hátt. Klassískt og viðeigandi dæmi fyrir okkur í samhengi við vandamálið sem lýst er eru djúp tauganet sem eru þjálfuð með hallafallsaðferðinni með nálgun hallans í hverju skrefi með því að nota lítinn hluta athugana, eða smálotu.

Djúpnámsrammar skrifaðar í Python eru með sérstaka flokka sem útfæra ítrekanir byggðar á gögnum: töflur, myndir í möppum, tvöfaldur snið osfrv. Þú getur notað tilbúna valkosti eða skrifað þína eigin fyrir ákveðin verkefni. Í R getum við nýtt okkur alla eiginleika Python bókasafnsins erfitt með ýmsum bakendum sínum með því að nota samnefndan pakka, sem aftur virkar ofan á pakkann seinka. Sú síðarnefnda á skilið sérstaka langa grein; það gerir þér ekki aðeins kleift að keyra Python kóða frá R, heldur gerir það þér einnig kleift að flytja hluti á milli R og Python lota og framkvæma sjálfkrafa allar nauðsynlegar tegundabreytingar.

Við losnuðum við þörfina á að geyma öll gögnin í vinnsluminni með því að nota MonetDBLite, öll „tauganet“ vinnan verður unnin af upprunalega kóðanum í Python, við verðum bara að skrifa endurtekningu yfir gögnin, þar sem ekkert er tilbúið fyrir slíkar aðstæður í annað hvort R eða Python. Það eru í rauninni aðeins tvær kröfur fyrir það: það verður að skila lotum í endalausri lykkju og vista ástand þess á milli endurtekningar (síðarnefnda í R er útfært á einfaldasta hátt með lokun). Áður var nauðsynlegt að umbreyta R fylki beinlínis í numpy fylki inni í iterator, en núverandi útgáfa af pakkanum erfitt gerir það sjálf.

Endurtekningin fyrir þjálfunar- og staðfestingargögn reyndist vera sem hér segir:

Iterator fyrir þjálfun og staðfestingargögn

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

Fallið tekur sem inntak breytu með tengingu við gagnagrunninn, fjölda lína sem notaðar eru, fjölda flokka, lotustærð, mælikvarða (scale = 1 samsvarar flutningi mynda af 256x256 pixlum, scale = 0.5 — 128x128 pixlar), litavísir (color = FALSE tilgreinir flutning í grátóna þegar það er notað color = TRUE hvert slag er teiknað í nýjum lit) og forvinnsluvísir fyrir net sem eru forþjálfuð á imagenet. Hið síðarnefnda er nauðsynlegt til að skala pixlagildi frá bilinu [0, 1] til bilsins [-1, 1], sem notað var við þjálfun meðfylgjandi erfitt fyrirmyndir.

Ytri aðgerðin inniheldur athugun á tegund röksemda, töflu data.table með handahófsblanduðum línutölum frá samples_index og lotunúmer, teljara og hámarksfjölda lota, auk SQL tjáningar til að losa gögn úr gagnagrunninum. Að auki skilgreindum við hraðvirka hliðstæðu aðgerðarinnar inni keras::to_categorical(). Við notuðum næstum öll gögn til þjálfunar og skildum eftir hálft prósent til staðfestingar, þannig að tímabilsstærðin var takmörkuð af færibreytunni steps_per_epoch þegar hringt er í keras::fit_generator(), og ástandið if (i > max_i) virkaði aðeins fyrir staðfestingarendurtekninguna.

Í innri aðgerðinni eru línuvísitölur sóttar fyrir næstu lotu, færslur eru losaðar úr gagnagrunninum með lotuteljaranum hækkandi, JSON þáttun (aðgerð cpp_process_json_vector(), skrifað í C++) og búa til fylki sem samsvara myndum. Þá eru einheitir vektorar með flokkamerkjum búnir til, fylki með pixlagildum og merki eru sameinuð í lista, sem er skilagildið. Til að flýta fyrir vinnu notuðum við gerð vísitölu í töflum data.table og breyting í gegnum hlekkinn - án þessara „flaga“ pakka gögn.tafla Það er frekar erfitt að ímynda sér að vinna á áhrifaríkan hátt með verulegt magn af gögnum í R.

Niðurstöður hraðamælinga á Core i5 fartölvu eru sem hér segir:

Iterator viðmið

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

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

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

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

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

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

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: hvernig á að eignast vini með R, C++ og taugakerfi

Ef þú ert með nægilegt magn af vinnsluminni geturðu hraðað gagnagrunninum verulega með því að flytja það yfir í þetta sama vinnsluminni (32 GB er nóg fyrir verkefni okkar). Í Linux er skiptingin sjálfgefið uppsett /dev/shm, sem tekur allt að helming af vinnsluminni. Þú getur auðkennt meira með því að breyta /etc/fstabað fá plötu eins og tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Vertu viss um að endurræsa og athuga niðurstöðuna með því að keyra skipunina df -h.

Endurtekningin fyrir prófunargögn lítur miklu einfaldari út, þar sem prófunargagnagrunnurinn passar algjörlega í vinnsluminni:

Iterator fyrir prófunargögn

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. Val á módelarkitektúr

Fyrsti arkitektúrinn sem notaður var var farsímanet v1, sem fjallað er um í þetta skilaboð. Það er innifalið sem staðalbúnaður erfitt og, í samræmi við það, er fáanlegur í pakkanum með sama nafni fyrir R. En þegar reynt var að nota það með einrása myndum kom undarlegt í ljós: inntakstensorinn verður alltaf að hafa víddina (batch, height, width, 3), það er að segja að ekki er hægt að breyta fjölda rása. Það er engin slík takmörkun í Python, svo við flýttum okkur og skrifuðum okkar eigin útfærslu á þessum arkitektúr, eftir upprunalegu greininni (án brottfallsins sem er í keras útgáfunni):

Mobilenet v1 arkitektúr

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

Ókostirnir við þessa aðferð eru augljósir. Ég vil prófa margar gerðir, en þvert á móti, ég vil ekki endurskrifa hverja arkitektúr handvirkt. Við vorum líka sviptir möguleikanum á að nota lóð fyrirsæta sem voru forþjálfaðar á imagenet. Eins og venjulega hjálpaði það að kynna sér skjölin. Virka get_config() gerir þér kleift að fá lýsingu á líkaninu á formi sem hentar til að breyta (base_model_conf$layers - venjulegur R listi), og aðgerðin from_config() framkvæmir öfuga umbreytingu í líkanhlut:

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)

Nú er ekki erfitt að skrifa alhliða aðgerð til að fá eitthvað af því sem fylgir erfitt módel með eða án lóða sem eru þjálfaðar á imagenet:

Virkni til að hlaða tilbúnum arkitektúr

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

Þegar notaðar eru einrásar myndir eru engar forþjálfaðar lóðir notaðar. Þetta gæti verið lagað: með því að nota aðgerðina get_weights() fáðu líkanþyngdina í formi lista yfir R fylki, breyttu vídd fyrsta þáttar þessa lista (með því að taka eina litarás eða miða alla þrjá) og hlaða svo lóðunum aftur inn í líkanið með fallinu set_weights(). Við bættum aldrei þessari virkni við, því á þessu stigi var þegar ljóst að það var afkastameira að vinna með litmyndir.

Við framkvæmdum flestar tilraunirnar með því að nota farsímanet útgáfur 1 og 2, sem og resnet34. Nútímalegri arkitektúr eins og SE-ResNeXt stóð sig vel í þessari keppni. Því miður höfðum við ekki tilbúnar útfærslur til umráða, og við skrifuðum ekki okkar eigin (en við munum örugglega skrifa).

5. Breyting á skriftum

Til þæginda var allur kóði til að hefja þjálfun hannaður sem eitt handrit, stillt með breytum doktor sem hér segir:

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)

Pakkinn doktor stendur fyrir framkvæmdina http://docopt.org/ fyrir R. Með hjálp þess eru forskriftir ræstar með einföldum skipunum eins og Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db eða ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, ef skrá train_nn.R er keyranleg (þessi skipun mun byrja að þjálfa líkanið resnet50 á þriggja lita myndum sem mæla 128x128 pixla þarf gagnagrunnurinn að vera staðsettur í möppunni /home/andrey/doodle_db). Þú getur bætt námshraða, gerð fínstillingar og öðrum sérhannaðar breytum við listann. Við undirbúning útgáfunnar kom í ljós að arkitektúrinn mobilenet_v2 frá núverandi útgáfu erfitt í R notkun getur ekki vegna breytinga sem ekki er tekið tillit til í R pakkanum, bíðum við eftir því að þeir laga það.

Þessi nálgun gerði það mögulegt að flýta verulega fyrir tilraunum með mismunandi gerðir samanborið við hefðbundnari ræsingu forskrifta í RStudio (við tökum eftir pakkanum sem mögulegan valkost tfruns). En helsti kosturinn er hæfileikinn til að stjórna ræsingu forskrifta auðveldlega í Docker eða einfaldlega á þjóninum, án þess að setja upp RStudio fyrir þetta.

6. Dockerization forskrifta

Við notuðum Docker til að tryggja færanleika umhverfisins fyrir þjálfunarlíkön milli liðsmanna og fyrir hraða dreifingu í skýinu. Þú getur byrjað að kynna þér þetta tól, sem er tiltölulega óvenjulegt fyrir R forritara, með þetta ritröð eða myndbandsnámskeið.

Docker gerir þér kleift að búa til þínar eigin myndir frá grunni og nota aðrar myndir sem grunn til að búa til þínar eigin. Við greiningu á tiltækum valkostum komumst við að þeirri niðurstöðu að uppsetning NVIDIA, CUDA+cuDNN rekla og Python bókasöfn er nokkuð fyrirferðarmikill hluti af myndinni og við ákváðum að taka opinberu myndina til grundvallar tensorflow/tensorflow:1.12.0-gpu, bæta við nauðsynlegum R pakka þar.

Síðasta docker skráin leit svona út:

Dockerfil

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

Til hægðarauka voru pakkarnir sem notaðir voru settir í breytur; Megnið af skrifuðu handritunum er afritað inni í gámunum við samsetningu. Við breyttum líka skipanaskelinni í /bin/bash til að auðvelda notkun á efni /etc/os-release. Þetta kom í veg fyrir þörfina á að tilgreina stýrikerfisútgáfuna í kóðanum.

Að auki var lítið bash handrit skrifað sem gerir þér kleift að ræsa gám með ýmsum skipunum. Til dæmis gætu þetta verið forskriftir til að þjálfa taugakerfi sem áður voru sett inni í ílátinu, eða skipanaskel til að kemba og fylgjast með rekstri ílátsins:

Forskrift til að ræsa ílátið

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

Ef þetta bash forskrift er keyrt án breytu, verður handritið kallað inni í gámnum train_nn.R með sjálfgefnum gildum; ef fyrsta staðsetningarröksemdin er "bash", þá mun ílátið byrja gagnvirkt með skipanaskel. Í öllum öðrum tilvikum er skipt út fyrir gildi stöðuröksemda: CMD="Rscript /app/train_nn.R $@".

Það er athyglisvert að möppurnar með upprunagögnum og gagnagrunni, svo og möppu til að vista þjálfaðar gerðir, eru settar upp í gámnum frá hýsingarkerfinu, sem gerir þér kleift að fá aðgang að niðurstöðum forskriftanna án óþarfa meðhöndlunar.

7. Notkun margra GPU á Google Cloud

Einn af eiginleikum keppninnar var mjög hávær gögn (sjá titilmyndina, fengin að láni frá @Leigh.plt frá ODS slack). Stórar lotur hjálpa til við að berjast gegn þessu og eftir tilraunir á tölvu með 1 GPU ákváðum við að ná tökum á þjálfunarlíkönum á nokkrum GPU í skýinu. Notaði GoogleCloud (góð leiðarvísir um grunnatriði) vegna mikils úrvals af tiltækum stillingum, sanngjörnu verði og $300 bónus. Af græðgi pantaði ég 4xV100 tilvik með SSD og tonn af vinnsluminni og það voru mikil mistök. Slík vél eyðir peningum fljótt; þú getur gert tilraunir án sannaðrar leiðslu. Í fræðsluskyni er betra að taka K80. En mikið vinnsluminni kom að góðum notum - skýja SSD-inn var ekki hrifinn af frammistöðu sinni, svo gagnagrunnurinn var fluttur til dev/shm.

Mest áhugavert er kóðabrotið sem ber ábyrgð á notkun margra GPU. Í fyrsta lagi er líkanið búið til á CPU með samhengisstjóra, alveg eins og í 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
  )
})

Þá er ósamsetta (þetta er mikilvægt) líkan afritað á ákveðinn fjölda tiltækra GPUs og aðeins eftir það er það sett saman:

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

Ekki var hægt að útfæra klassíska tæknina að frysta öll lög nema það síðasta, þjálfa síðasta lagið, affrysta og endurþjálfa allt líkanið fyrir nokkrar GPU.

Fylgst var með þjálfun án notkunar. tensorboard, takmarka okkur við að taka upp annála og vista líkön með upplýsandi nöfnum eftir hvert tímabil:

Hringingar

# Шаблон имени файла лога
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. Í stað niðurstöðu

Ýmis vandamál sem við höfum lent í hefur ekki enn verið sigrast á:

  • в erfitt það er engin tilbúin aðgerð til að leita sjálfkrafa að ákjósanlegu námshraða (hliðstæða lr_finder á bókasafni fast.ai); Með nokkurri fyrirhöfn er hægt að flytja útfærslur þriðja aðila yfir á R, til dæmis, þetta;
  • sem afleiðing af fyrri liðnum var ekki hægt að velja réttan þjálfunarhraða þegar notaðar voru nokkrar GPU;
  • það er skortur á nútíma taugakerfisarkitektúr, sérstaklega þeim sem eru fyrirfram þjálfaðir á myndneti;
  • engin ein hringstefna og mismunandi námshlutfall (kósínuglæðing var að beiðni okkar komið til framkvæmda, takk skeydan).

Hvaða gagnlegu hlutir lærðust af þessari keppni:

  • Á tiltölulega litlum vélbúnaði geturðu unnið með ágætis (margfalt stærra vinnsluminni) gagnamagn án sársauka. Plastpoki gögn.tafla sparar minni vegna breytinga á töflum á staðnum, sem kemur í veg fyrir að afrita þær og þegar þær eru notaðar á réttan hátt sýna hæfileikar þess næstum alltaf mesta hraða meðal allra tækja sem við vitum fyrir forskriftarmál. Að vista gögn í gagnagrunni gerir þér í mörgum tilfellum kleift að hugsa alls ekki um þörfina á að kreista allt gagnasafnið í vinnsluminni.
  • Hægt er að skipta út hægum aðgerðum í R fyrir hraðar í C++ með því að nota pakkann Rcpp. Ef auk þess að nota RcppÞráður eða RcppParallel, við fáum margþráðar útfærslur á milli vettvanga, svo það er engin þörf á að samhliða kóðanum á R-stigi.
  • Pakki Rcpp er hægt að nota án alvarlegrar vitneskju um C++, tilskilið lágmark er lýst hér. Hausskrár fyrir fjölda flottra C-bókasafna eins og xtensor í boði á CRAN, það er að segja að verið er að mynda innviði fyrir framkvæmd verkefna sem samþætta tilbúinn afkastamikinn C++ kóða inn í R. Viðbótarþægindi eru setningafræði auðkenning og kyrrstæður C++ kóða greiningartæki í RStudio.
  • doktor gerir þér kleift að keyra sjálfstætt forskriftir með breytum. Þetta er þægilegt til notkunar á ytri netþjóni, þ.m.t. undir bryggju. Í RStudio er óþægilegt að gera margar klukkustundir af tilraunum með að þjálfa taugakerfi og það er ekki alltaf réttlætanlegt að setja upp IDE á þjóninum sjálfum.
  • Docker tryggir kóða flytjanleika og endurgeranleika niðurstaðna milli þróunaraðila með mismunandi útgáfur af stýrikerfinu og bókasöfnum, sem og auðvelda framkvæmd á netþjónum. Þú getur ræst alla þjálfunarleiðsluna með aðeins einni skipun.
  • Google Cloud er kostnaðarvæn leið til að gera tilraunir með dýran vélbúnað, en þú þarft að velja stillingar vandlega.
  • Að mæla hraða einstakra kóðabúta er mjög gagnlegt, sérstaklega þegar R og C++ eru sameinuð og með pakkanum bekkur - líka mjög auðvelt.

Á heildina litið var þessi reynsla mjög gefandi og við höldum áfram að vinna að því að leysa sum vandamálin sem komu upp.

Heimild: www.habr.com

Bæta við athugasemd