Quick Draw Doodle Recognition: cumu fà amici cù R, C++ è e rete neurali

Quick Draw Doodle Recognition: cumu fà amici cù R, C++ è e rete neurali

Ehi Habr!

L'ultimu vaghjimu, Kaggle hà ospitatu un cuncorsu per classificà i ritratti disegnati à manu, Quick Draw Doodle Recognition, in quale, frà altri, una squadra di R-scientists hà participatu: Artem Klevtsova, Philippa Manager и Andrey Ogurtsov. Ùn descriveremu micca a cumpetizione in dettagliu; chì hè digià fattu pubblicazione recente.

Questa volta ùn hà micca travagliatu cù l'agricoltura di medaglia, ma assai esperienza preziosa hè stata acquistata, cusì vogliu dì à a cumunità di una quantità di e cose più interessanti è utili nantu à Kagle è in u travagliu di ogni ghjornu. Trà i temi discututi: a vita difficiule senza OpenCV, JSON parsing (questi esempi esaminanu l'integrazione di codice C++ in scripts o pacchetti in R usendu Rcpp), parametrizzazione di scripts è dockerizazione di a suluzione finale. Tuttu u codice da u messagiu in una forma adattata per l'esekzione hè dispunibule in repository.

Contenuti:

  1. Caricà in modu efficiente e dati da CSV in MonetDB
  2. Preparazione di lotte
  3. Iteratori per scaricamentu di batch da a basa di dati
  4. Selezzione di un mudellu di architettura
  5. Parametrizazione di script
  6. Dockerization di scripts
  7. Utilizà parechje GPU in Google Cloud
  8. Inveci di 'na cunchiusioni

1. Caricà in modu efficiente e dati da CSV in a basa di dati MonetDB

I dati in questa cumpetizione ùn sò micca furniti in forma di imaghjini pronti, ma in forma di schedari CSV 340 (un schedariu per ogni classa) chì cuntenenu JSON cù coordenate punti. Cunnettendu questi punti cù e linee, avemu una maghjina finali chì misura 256x256 pixel. Inoltre per ogni registru ci hè una etichetta chì indica se a stampa hè stata ricunnisciuta currettamente da u classificatore utilizatu à u mumentu di a cullizzioni di dati, un codice di duie lettere di u paese di residenza di l'autore di a stampa, un identificatore unicu, un timestamp. è un nome di classi chì currisponde à u nome di u schedariu. Una versione simplificata di i dati originali pesa 7.4 GB in l'archiviu è circa 20 GB dopu à unpacking, i dati cumpleti dopu à unpacking pigghia 240 GB. L'urganizatori anu assicuratu chì e duie versioni riproducevanu i stessi disegni, vale à dì chì a versione completa era redundante. In ogni casu, l'almacenamiento di 50 milioni d'imaghjini in i schedarii grafichi o in forma di arrays hè statu immediatamente cunsideratu senza prufittu, è avemu decisu di unisce tutti i schedari CSV da l'archiviu. train_simplified.zip in a basa di dati cù a generazione successiva di l'imaghjini di a dimensione necessaria "à a mosca" per ogni batch.

Un sistema ben pruvucatu hè statu sceltu cum'è DBMS MonetDB, vale à dì una implementazione per R cum'è un pacchettu MonetDBlite. U pacchettu include una versione integrata di u servitore di basa di dati è vi permette di piglià u servitore direttamente da una sessione R è travaglià cun ellu. A creazione di una basa di dati è a cunnessione à questu sò realizati cù un cumandamentu:

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

Avemu bisognu di creà duie tavule: una per tutte e dati, l'altra per l'infurmazioni di serviziu nantu à i schedarii telecaricati (utile se qualcosa va male è u prucessu deve esse ripigliatu dopu à scaricà parechji schedari):

Creazione di tavule

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

U modu più veloce per carricà e dati in a basa di dati era di cupià direttamente i fugliali CSV cù SQL - cumanda COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTinduve tablename - nome di a tavola è path - a strada di u schedariu. Mentre travaglia cù l'archiviu, hè statu scupertu chì l'implementazione integrata unzip in R ùn funziona micca bè cù una quantità di schedari da l'archiviu, cusì avemu usatu u sistema unzip (aduprendu u paràmetru getOption("unzip")).

Funzione per scrive à a basa di dati

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

Sè avete bisognu di trasfurmà a tavula prima di scrive à a basa di dati, hè abbastanza per passà in l'argumentu preprocess funzione chì trasformerà i dati.

Codice per a carica sequenziale di dati in a basa di dati:

Scrive dati à a basa di dati

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

U tempu di carica di dati pò varià secondu e caratteristiche di velocità di u drive utilizatu. In u nostru casu, a lettura è a scrittura in un SSD o da una unità flash (file fonte) à un SSD (DB) dura menu di 10 minuti.

Ci vole uni pochi di seconde per creà una colonna cù una etichetta di classa intera è una colonna indice (ORDERED INDEX) cù numeri di linea da quale l'osservazioni saranu campionate quandu creanu batch:

Creazione di Culonni supplementari è Indici

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

Per risolve u prublema di creà un batch nantu à a mosca, avemu bisognu di ottene a velocità massima di estrazione di fila aleatoria da a tavula. doodles. Per questu avemu usatu 3 trucchi. U primu era di riduce a dimensione di u tipu chì guarda l'ID d'osservazione. In u settore di dati uriginale, u tipu necessariu per almacenà l'ID hè bigint, ma u nùmeru d'osservazioni permette di mette i so identificatori, uguali à u numeru ordinale, in u tipu. int. A ricerca hè assai più veloce in questu casu. U sicondu truccu era di utilizà ORDERED INDEX - avemu ghjuntu à sta decisione empirically, dopu avè passatu tutti i dispunibuli opzioni. U terzu era di utilizà e dumande parametrizzate. L'essenza di u metudu hè di eseguisce u cumandamentu una volta PREPARE cù l'usu sussegwente di una espressione preparata quandu crea una mansa di dumande di u listessu tipu, ma in fattu ci hè un vantaghju in paragunà cù una simplice. SELECT s'est avéré être dans l'intervalle de l'erreur statistique.

U prucessu di carica di dati ùn cunsuma micca più di 450 MB di RAM. Questu hè, l'approcciu descrittu permette di trasfurmà datasets chì pesanu decine di gigabyte nantu à quasi ogni hardware di bilanciu, cumpresi certi dispositi unicu, chì hè abbastanza cool.

Tuttu ciò chì resta hè di misurà a rapidità di ricuperazione di dati (aleatoriu) è evaluà a scala quandu si campionanu lotti di diverse dimensioni:

Benchmark di basa di dati

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: cumu fà amici cù R, C++ è e rete neurali

2. Preparazione di lotte

Tuttu u prucessu di preparazione di batch hè custituitu da i seguenti passi:

  1. Analisi di parechji JSON chì cuntenenu vettori di stringhe cù coordenate di punti.
  2. Disegnu linee di culore basatu nantu à e coordenate di punti nantu à una maghjina di a dimensione necessaria (per esempiu, 256 × 256 o 128 × 128).
  3. Cunvertisce l'imaghjini resultanti in un tensor.

Comu parte di a cumpetizione trà i kernels di Python, u prublema hè stata risolta principarmenti utilizendu OpenCV. Unu di l'analogi più sèmplice è evidenti in R avaristi cusì:

Implementazione di a cunversione di JSON à Tensor in R

r_process_json_str <- function(json, line.width = 3, 
                               color = TRUE, scale = 1) {
  # Парсинг JSON
  coords <- jsonlite::fromJSON(json, simplifyMatrix = FALSE)
  tmp <- tempfile()
  # Удаляем временный файл по завершению функции
  on.exit(unlink(tmp))
  png(filename = tmp, width = 256 * scale, height = 256 * scale, pointsize = 1)
  # Пустой график
  plot.new()
  # Размер окна графика
  plot.window(xlim = c(256 * scale, 0), ylim = c(256 * scale, 0))
  # Цвета линий
  cols <- if (color) rainbow(length(coords)) else "#000000"
  for (i in seq_along(coords)) {
    lines(x = coords[[i]][[1]] * scale, y = coords[[i]][[2]] * scale, 
          col = cols[i], lwd = line.width)
  }
  dev.off()
  # Преобразование изображения в 3-х мерный массив
  res <- png::readPNG(tmp)
  return(res)
}

r_process_json_vector <- function(x, ...) {
  res <- lapply(x, r_process_json_str, ...)
  # Объединение 3-х мерных массивов картинок в 4-х мерный в тензор
  res <- do.call(abind::abind, c(res, along = 0))
  return(res)
}

U disegnu hè realizatu utilizendu strumenti R standard è salvati in un PNG temporale almacenatu in RAM (in Linux, i repertorii R temporanei sò situati in u cartulare. /tmp, muntatu in RAM). Stu schedariu hè poi lettu cum'è un array tridimensionale cù numeri chì varienu da 0 à 1. Questu hè impurtante perchè un BMP più cunvinziunali seria lettu in un array crudu cù codici di culore hex.

Testemu u risultatu:

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: cumu fà amici cù R, C++ è e rete neurali

U lottu stessu serà furmatu da seguente:

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

Questa implementazione ci pareva subottimali, postu chì a furmazione di grandi batch pigghia un tempu indecentemente longu, è avemu decisu di prufittà di l'esperienza di i nostri culleghi utilizendu una biblioteca putente. OpenCV. À quellu tempu ùn ci era micca un pacchettu prontu per R (ùn ci hè nimu avà), cusì una implementazione minima di a funziunalità necessaria hè stata scritta in C ++ cù integrazione in codice R usendu. Rcpp.

Per risolve u prublema, i seguenti pacchetti è biblioteche sò stati utilizati:

  1. OpenCV per travaglià cù l'imaghjini è disegnu linee. Aduprate biblioteche di sistema preinstallate è fugliali d'intestazione, è ancu ligami dinamichi.

  2. xtensor per travaglià cù arrays multidimensionali è tensori. Avemu usatu i fugliali di header inclusi in u pacchettu R di u stessu nome. A biblioteca permette di travaglià cù arrays multidimensionali, sia in fila maiò sia in ordine di colonna maiò.

  3. ndjson per analizà JSON. Questa biblioteca hè aduprata in xtensor automaticamente s'ellu hè presente in u prugettu.

  4. RcppThread per urganizà u prucessu multi-threaded di un vettore da JSON. Adupratu i fugliali d'intestazione furniti da stu pacchettu. Da più populari RcppParallel U pacchettu, frà altre cose, hà un mecanismu di interruzzione di loop integratu.

Ci hè da nutà chì xtensor hè diventatu una manna divina: in più di u fattu chì hà una funziunalità estensiva è un altu rendiment, i so sviluppatori sò diventati abbastanza responsive è risponde à e dumande prontamente è in dettagliu. Cù u so aiutu, era pussibule di implementà e trasfurmazioni di e matrici OpenCV in tensori xtensor, è ancu una manera di cumminà i tensori di l'imaghjini tridimensionali in un tensor tridimensionale di a dimensione curretta (u batch stessu).

Materiali per amparà Rcpp, xtensor è 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

Per cumpilà i fugliali chì utilizanu schedarii di sistema è ligami dinamichi cù biblioteche installate in u sistema, avemu usatu u mecanismu di plugin implementatu in u pacchettu. Rcpp. Per truvà automaticamente percorsi è bandiere, avemu usatu una utilità Linux populari pkg-config.

Implementazione di u plugin Rcpp per aduprà a libreria OpenCV

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

In u risultatu di l'operazione di u plugin, i seguenti valori seranu sustituiti durante u prucessu di compilazione:

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"

U codice di implementazione per analizà JSON è generà un batch per a trasmissione à u mudellu hè datu sottu u spoiler. Prima, aghjunghje un repertoriu di prughjettu lucale per circà i fugliali di header (necessariu per ndjson):

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

Implementazione di a cunversione di JSON à tensor in C++

// [[Rcpp::plugins(cpp14)]]
// [[Rcpp::plugins(opencv)]]
// [[Rcpp::depends(xtensor)]]
// [[Rcpp::depends(RcppThread)]]

#include <xtensor/xjson.hpp>
#include <xtensor/xadapt.hpp>
#include <xtensor/xview.hpp>
#include <xtensor-r/rtensor.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <Rcpp.h>
#include <RcppThread.h>

// Синонимы для типов
using RcppThread::parallelFor;
using json = nlohmann::json;
using points = xt::xtensor<double,2>;     // Извлечённые из JSON координаты точек
using strokes = std::vector<points>;      // Извлечённые из JSON координаты точек
using xtensor3d = xt::xtensor<double, 3>; // Тензор для хранения матрицы изоображения
using xtensor4d = xt::xtensor<double, 4>; // Тензор для хранения множества изображений
using rtensor3d = xt::rtensor<double, 3>; // Обёртка для экспорта в R
using rtensor4d = xt::rtensor<double, 4>; // Обёртка для экспорта в R

// Статические константы
// Размер изображения в пикселях
const static int SIZE = 256;
// Тип линии
// См. https://en.wikipedia.org/wiki/Pixel_connectivity#2-dimensional
const static int LINE_TYPE = cv::LINE_4;
// Толщина линии в пикселях
const static int LINE_WIDTH = 3;
// Алгоритм ресайза
// https://docs.opencv.org/3.1.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121
const static int RESIZE_TYPE = cv::INTER_LINEAR;

// Шаблон для конвертирования OpenCV-матрицы в тензор
template <typename T, int NCH, typename XT=xt::xtensor<T,3,xt::layout_type::column_major>>
XT to_xt(const cv::Mat_<cv::Vec<T, NCH>>& src) {
  // Размерность целевого тензора
  std::vector<int> shape = {src.rows, src.cols, NCH};
  // Общее количество элементов в массиве
  size_t size = src.total() * NCH;
  // Преобразование cv::Mat в xt::xtensor
  XT res = xt::adapt((T*) src.data, size, xt::no_ownership(), shape);
  return res;
}

// Преобразование JSON в список координат точек
strokes parse_json(const std::string& x) {
  auto j = json::parse(x);
  // Результат парсинга должен быть массивом
  if (!j.is_array()) {
    throw std::runtime_error("'x' must be JSON array.");
  }
  strokes res;
  res.reserve(j.size());
  for (const auto& a: j) {
    // Каждый элемент массива должен быть 2-мерным массивом
    if (!a.is_array() || a.size() != 2) {
      throw std::runtime_error("'x' must include only 2d arrays.");
    }
    // Извлечение вектора точек
    auto p = a.get<points>();
    res.push_back(p);
  }
  return res;
}

// Отрисовка линий
// Цвета HSV
cv::Mat ocv_draw_lines(const strokes& x, bool color = true) {
  // Исходный тип матрицы
  auto stype = color ? CV_8UC3 : CV_8UC1;
  // Итоговый тип матрицы
  auto dtype = color ? CV_32FC3 : CV_32FC1;
  auto bg = color ? cv::Scalar(0, 0, 255) : cv::Scalar(255);
  auto col = color ? cv::Scalar(0, 255, 220) : cv::Scalar(0);
  cv::Mat img = cv::Mat(SIZE, SIZE, stype, bg);
  // Количество линий
  size_t n = x.size();
  for (const auto& s: x) {
    // Количество точек в линии
    size_t n_points = s.shape()[1];
    for (size_t i = 0; i < n_points - 1; ++i) {
      // Точка начала штриха
      cv::Point from(s(0, i), s(1, i));
      // Точка окончания штриха
      cv::Point to(s(0, i + 1), s(1, i + 1));
      // Отрисовка линии
      cv::line(img, from, to, col, LINE_WIDTH, LINE_TYPE);
    }
    if (color) {
      // Меняем цвет линии
      col[0] += 180 / n;
    }
  }
  if (color) {
    // Меняем цветовое представление на RGB
    cv::cvtColor(img, img, cv::COLOR_HSV2RGB);
  }
  // Меняем формат представления на float32 с диапазоном [0, 1]
  img.convertTo(img, dtype, 1 / 255.0);
  return img;
}

// Обработка JSON и получение тензора с данными изображения
xtensor3d process(const std::string& x, double scale = 1.0, bool color = true) {
  auto p = parse_json(x);
  auto img = ocv_draw_lines(p, color);
  if (scale != 1) {
    cv::Mat out;
    cv::resize(img, out, cv::Size(), scale, scale, RESIZE_TYPE);
    cv::swap(img, out);
    out.release();
  }
  xtensor3d arr = color ? to_xt<double,3>(img) : to_xt<double,1>(img);
  return arr;
}

// [[Rcpp::export]]
rtensor3d cpp_process_json_str(const std::string& x, 
                               double scale = 1.0, 
                               bool color = true) {
  xtensor3d res = process(x, scale, color);
  return res;
}

// [[Rcpp::export]]
rtensor4d cpp_process_json_vector(const std::vector<std::string>& x, 
                                  double scale = 1.0, 
                                  bool color = false) {
  size_t n = x.size();
  size_t dim = floor(SIZE * scale);
  size_t channels = color ? 3 : 1;
  xtensor4d res({n, dim, dim, channels});
  parallelFor(0, n, [&x, &res, scale, color](int i) {
    xtensor3d tmp = process(x[i], scale, color);
    auto view = xt::view(res, i, xt::all(), xt::all(), xt::all());
    view = tmp;
  });
  return res;
}

Stu codice deve esse piazzatu in u schedariu src/cv_xt.cpp è compilà cù u cumandimu Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); ancu necessariu per u travagliu nlohmann/json.hpp из repository. U codice hè divisu in parechje funzioni:

  • to_xt - una funzione mudellu per trasfurmà una matrice di l'imaghjini (cv::Mat) à un tensor xt::xtensor;

  • parse_json - a funzione analizza una stringa JSON, estrae e coordenate di i punti, imballendu in un vettore;

  • ocv_draw_lines - da u vettore resultanti di punti, disegna linee multicolore;

  • process - combina e funzioni sopra è aghjunghje ancu a capacità di scala l'imaghjini resultanti;

  • cpp_process_json_str - wrapper sopra a funzione process, chì esporta u risultatu à un R-object (array multidimensionale);

  • cpp_process_json_vector - wrapper sopra a funzione cpp_process_json_str, chì permette di processà un vettore di stringa in modu multi-threaded.

Per disegnà linee multicolore, u mudellu di culore HSV hè stata utilizata, seguita da a cunversione à RGB. Testemu u risultatu:

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

Quick Draw Doodle Recognition: cumu fà amici cù R, C++ è e rete neurali
Comparazione di a velocità di implementazioni in R è 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: cumu fà amici cù R, C++ è e rete neurali

Comu pudete vede, l'aumentu di a velocità hè diventatu assai significativu, è ùn hè micca pussibule di chjappà cù u codice C ++ parallelisendu u codice R.

3. Iteratori per scaricamentu di batch da a basa di dati

R hà una reputazione ben meritata per u processu di dati chì si mette in RAM, mentre chì Python hè più carattarizatu da u processu di dati iterativu, chì permette di implementà facilmente è naturalmente calculi fora di core (calculazioni cù memoria esterna). Un esempiu classicu è pertinenti per noi in u cuntestu di u prublema discritta hè e rete neurali prufonda furmatu da u metudu di discendenza di gradiente cù l'approssimazione di u gradiente à ogni passu cù una piccula parte di osservazioni, o mini-batch.

I quadri d'apprendimentu prufonda scritti in Python anu classi speciale chì implementanu iteratori basati nantu à dati: tavule, stampi in caratteri, formati binari, etc. Pudete utilizà l'opzioni pronti o scrive u vostru propiu per compiti specifichi. In R pudemu prufittà di tutte e funziunalità di a biblioteca Python keras cù i so diversi backends chì utilizanu u pacchettu di u stessu nome, chì à u turnu travaglia nantu à u pacchettu reticulà. L'ultime meriteghja un articulu longu separatu; ùn solu permette à voi à curriri codice Python da R, ma permette dinù voi à trasfiriri ogetti trà R è sessioni Python, automaticamente eseguisce tutte e cunversione tippu nicissariu.

Avemu liberatu da a necessità di almacenà tutte e dati in RAM usendu MonetDBlite, tuttu u travagliu di a "rete neurale" serà realizatu da u codice originale in Python, avemu solu à scrive un iteratore nantu à e dati, postu chì ùn ci hè nunda prontu. per una tale situazione in R o Python. Ci hè essenzialmente solu duie esigenze per questu: deve rinvià batch in un ciclu infinitu è ​​salvà u so statu trà iterazioni (l'ultimu in R hè implementatu in u modu più simplice cù chjusi). In precedenza, era necessariu di cunvertisce esplicitamente arrays R in numpy arrays in l'iteratore, ma a versione attuale di u pacchettu keras face ella stessa.

L'iteratore per i dati di furmazione è di validazione hè statu cusì:

Iteratore per i dati di furmazione è validazione

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

A funzione piglia cum'è input una variabile cù una cunnessione à a basa di dati, u numeru di linii utilizati, u numeru di classi, a dimensione di batch, a scala (scale = 1 currisponde à l'imaghjini di rendering di 256x256 pixel, scale = 0.5 - 128x128 pixels), indicatore di culore (color = FALSE specifica u rendering in scala di grisgiu quandu hè utilizatu color = TRUE ogni colpu hè disegnatu in un novu culore) è un indicatore di preprocessing per e rete pre-addestrate nantu à imagenet. L'ultime hè necessariu per scalà i valori di pixel da l'intervallu [0, 1] à l'intervallu [-1, 1], chì hè stata utilizata per a furmazione di u fornitu. keras mudelli.

A funzione esterna cuntene un cuntrollu di tipu d'argumentu, una tabella data.table cù numeri di linea mischiati aleatoriamente da samples_index è numeri di batch, contatore è numeru massimu di batch, è ancu una espressione SQL per scaricamentu di dati da a basa di dati. Inoltre, avemu definitu un analogu veloce di a funzione in l'internu keras::to_categorical(). Avemu usatu quasi tutti i dati per a furmazione, lascendu a mità per centu per a validazione, cusì a dimensione di l'epica era limitata da u paràmetru steps_per_epoch quandu chjamatu keras::fit_generator(), è a cundizione if (i > max_i) hà travagliatu solu per l'iteratore di validazione.

In a funzione interna, l'indici di fila sò recuperati per u prossimu batch, i registri sò scaricati da a basa di dati cù u batch counter crescente, JSON parsing (funzione cpp_process_json_vector(), scrittu in C++) è creanu array currispondenti à l'imaghjini. Allora sò creati vettori unicu cù etichette di classi, arrays cù valori di pixel è etichette sò cumminati in una lista, chì hè u valore di ritornu. Per accelerà u travagliu, avemu usatu a creazione d'indici in tavule data.table è mudificazione via u ligame - senza questi pacchetti "chips" dati.tavula Hè abbastanza difficiuli di imaginà travaglià in modu efficace cù qualsiasi quantità significativa di dati in R.

I risultati di e misurazioni di velocità nantu à un laptop Core i5 sò i seguenti:

Benchmark di iteratore

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: cumu fà amici cù R, C++ è e rete neurali

Se tenete una quantità di RAM sufficiente, pudete accelerà seriamente u funziunamentu di a basa di dati trasferendu in questa stessa RAM (32 GB hè abbastanza per u nostru compitu). In Linux, a partizione hè muntata per automaticamente /dev/shm, occupendu finu à a mità di a capacità RAM. Pudete mette in risaltu più editendu /etc/fstabpè ottene un record like tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Assicuratevi di reboot è verificate u risultatu eseguendu u cumandamentu df -h.

L'iteratore per i dati di prova pare assai più simplice, postu chì u set di dati di prova si mette interamente in RAM:

Iteratore per i dati di prova

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. Selezzione di architettura mudellu

A prima architettura utilizata hè stata mobilenet v1, e caratteristiche di quale sò discututi in questu missaghju. Hè inclusu cum'è standard keras è, per quessa, hè dispunibule in u pacchettu di u listessu nome per R. Ma quandu pruvate d'utilizà cù l'imaghjini di un canale, una cosa strana hè stata: u tensor di input deve sempre avè a dimensione. (batch, height, width, 3), vale à dì, u numeru di canali ùn pò esse cambiatu. Ùn ci hè micca una tale limitazione in Python, cusì avemu precipitatu è scrivite a nostra propria implementazione di sta architettura, dopu à l'articulu originale (senza l'abbandunamentu chì hè in a versione keras):

Architettura Mobilenet v1

library(keras)

top_3_categorical_accuracy <- custom_metric(
    name = "top_3_categorical_accuracy",
    metric_fn = function(y_true, y_pred) {
         metric_top_k_categorical_accuracy(y_true, y_pred, k = 3)
    }
)

layer_sep_conv_bn <- function(object, 
                              filters,
                              alpha = 1,
                              depth_multiplier = 1,
                              strides = c(2, 2)) {

  # NB! depth_multiplier !=  resolution multiplier
  # https://github.com/keras-team/keras/issues/10349

  layer_depthwise_conv_2d(
    object = object,
    kernel_size = c(3, 3), 
    strides = strides,
    padding = "same",
    depth_multiplier = depth_multiplier
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() %>%
  layer_conv_2d(
    filters = filters * alpha,
    kernel_size = c(1, 1), 
    strides = c(1, 1)
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() 
}

get_mobilenet_v1 <- function(input_shape = c(224, 224, 1),
                             num_classes = 340,
                             alpha = 1,
                             depth_multiplier = 1,
                             optimizer = optimizer_adam(lr = 0.002),
                             loss = "categorical_crossentropy",
                             metrics = c("categorical_crossentropy",
                                         top_3_categorical_accuracy)) {

  inputs <- layer_input(shape = input_shape)

  outputs <- inputs %>%
    layer_conv_2d(filters = 32, kernel_size = c(3, 3), strides = c(2, 2), padding = "same") %>%
    layer_batch_normalization() %>% 
    layer_activation_relu() %>%
    layer_sep_conv_bn(filters = 64, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 128, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 128, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 256, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 256, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 1024, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 1024, strides = c(1, 1)) %>%
    layer_global_average_pooling_2d() %>%
    layer_dense(units = num_classes) %>%
    layer_activation_softmax()

    model <- keras_model(
      inputs = inputs,
      outputs = outputs
    )

    model %>% compile(
      optimizer = optimizer,
      loss = loss,
      metrics = metrics
    )

    return(model)
}

I disadvantages di stu approcciu sò evidenti. Vogliu pruvà assai mudelli, ma à u cuntrariu, ùn vogliu micca riscrive ogni architettura manualmente. Avemu statu ancu privatu di l'uppurtunità di utilizà i pesi di mudelli pre-furmati nantu à imagenet. Comu solitu, studià a documentazione aiutau. Funzione get_config() permette di ottene una descrizzione di u mudellu in una forma adattata per edità (base_model_conf$layers - una lista R regular), è a funzione from_config() esegue a cunversione inversa à un oggettu mudellu:

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)

Avà ùn hè micca difficiule di scrive una funzione universale per ottene qualcunu di i forniti keras mudelli cù o senza pesi addestrati nantu à imagenet:

Funzione per caricare architetture pronte

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

Quandu si usa l'imaghjini à un canale, ùn sò micca usati pesi pretrained. Questu puderia esse riparatu: utilizendu a funzione get_weights() uttene i pesi di u mudellu in a forma di una lista di arrays R, cambiate a dimensione di u primu elementu di sta lista (pigliendu un canale di culore o una media di tutti i trè), è poi caricate i pesi in u mudellu cù a funzione. set_weights(). Ùn avemu mai aghjustatu sta funziunalità, perchè in questu stadiu era digià chjaru chì era più pruduttivu di travaglià cù stampi di culore.

Avemu fattu a maiò parte di l'esperimenti cù e versioni mobilenet 1 è 2, è ancu resnet34. L'architetture più muderne cum'è SE-ResNeXt anu fattu bè in questa cumpetizione. Sfurtunatamente, ùn avemu micca implementazioni pronti à a nostra dispusizione, è ùn avemu micca scrittu u nostru propiu (ma avemu da scrivite definitivamente).

5. Parametrizazione di script

Per comodità, tuttu u codice per inizià a furmazione hè statu cuncepitu cum'è un script unicu, parametrizzatu cù l'usu docopt a siguenti:

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)

Poney docopt rapprisenta l'implementazione http://docopt.org/ per R. Cù u so aiutu, i script sò lanciati cù cumandamenti simplici cum'è Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db o ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, se u schedariu train_nn.R hè eseguibile (questu cumandamentu principià a furmazione di u mudellu resnet50 nantu à l'imaghjini di trè culori chì misuranu 128x128 pixel, a basa di dati deve esse situata in u cartulare /home/andrey/doodle_db). Pudete aghjunghje a velocità di apprendimentu, u tippu di ottimizzatore è qualsiasi altri paràmetri persunalizabili à a lista. In u prucessu di a preparazione di a publicazione, hè risultatu chì l'architettura mobilenet_v2 da a versione attuale keras in R usu pò micca per via di cambiamenti chì ùn sò micca tenuti in contu in u pacchettu R, aspittemu per risolve.

Stu approcciu hà permessu di accelerà significativamente l'esperimenti cù mudelli diffirenti paragunatu à u lanciamentu più tradiziunale di script in RStudio (notemu u pacchettu cum'è una pussibuli alternativa tfruns). Ma u vantaghju principali hè a capacità di gestisce facilmente u lanciamentu di scripts in Docker o solu in u servitore, senza installà RStudio per questu.

6. Dockerization di scripts

Avemu usatu Docker per assicurà a portabilità di l'ambiente per i mudelli di furmazione trà i membri di a squadra è per una implementazione rapida in u nuvulu. Pudete principià à cunnosce stu strumentu, chì hè relativamente inusual per un programatore R, cù questu seria di publicazioni o corsu video.

Docker vi permette di creà e vostre propiu imagine da zero è di utilizà altre imagine cum'è una basa per creà u vostru propiu. Quandu analizà l'opzioni dispunibuli, simu ghjunti à a cunclusione chì l'installazione di NVIDIA, CUDA + cuDNN drivers è biblioteche Python hè una parte abbastanza voluminosa di l'imaghjini, è avemu decisu di piglià l'imagine ufficiale cum'è una basa. tensorflow/tensorflow:1.12.0-gpu, aghjunghjendu i pacchetti R necessarii quì.

U schedariu docker finali pareva cusì:

dockerfile

FROM tensorflow/tensorflow:1.12.0-gpu

MAINTAINER Artem Klevtsov <[email protected]>

SHELL ["/bin/bash", "-c"]

ARG LOCALE="en_US.UTF-8"
ARG APT_PKG="libopencv-dev r-base r-base-dev littler"
ARG R_BIN_PKG="futile.logger checkmate data.table rcpp rapidjsonr dbi keras jsonlite curl digest remotes"
ARG R_SRC_PKG="xtensor RcppThread docopt MonetDBLite"
ARG PY_PIP_PKG="keras"
ARG DIRS="/db /app /app/data /app/models /app/logs"

RUN source /etc/os-release && 
    echo "deb https://cloud.r-project.org/bin/linux/ubuntu ${UBUNTU_CODENAME}-cran35/" > /etc/apt/sources.list.d/cran35.list && 
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E084DAB9 && 
    add-apt-repository -y ppa:marutter/c2d4u3.5 && 
    add-apt-repository -y ppa:timsc/opencv-3.4 && 
    apt-get update && 
    apt-get install -y locales && 
    locale-gen ${LOCALE} && 
    apt-get install -y --no-install-recommends ${APT_PKG} && 
    ln -s /usr/lib/R/site-library/littler/examples/install.r /usr/local/bin/install.r && 
    ln -s /usr/lib/R/site-library/littler/examples/install2.r /usr/local/bin/install2.r && 
    ln -s /usr/lib/R/site-library/littler/examples/installGithub.r /usr/local/bin/installGithub.r && 
    echo 'options(Ncpus = parallel::detectCores())' >> /etc/R/Rprofile.site && 
    echo 'options(repos = c(CRAN = "https://cloud.r-project.org"))' >> /etc/R/Rprofile.site && 
    apt-get install -y $(printf "r-cran-%s " ${R_BIN_PKG}) && 
    install.r ${R_SRC_PKG} && 
    pip install ${PY_PIP_PKG} && 
    mkdir -p ${DIRS} && 
    chmod 777 ${DIRS} && 
    rm -rf /tmp/downloaded_packages/ /tmp/*.rds && 
    rm -rf /var/lib/apt/lists/*

COPY utils /app/utils
COPY src /app/src
COPY tests /app/tests
COPY bin/*.R /app/

ENV DBDIR="/db"
ENV CUDA_HOME="/usr/local/cuda"
ENV PATH="/app:${PATH}"

WORKDIR /app

VOLUME /db
VOLUME /app

CMD bash

Per comodità, i pacchetti utilizati sò stati messi in variàbili; a maiò parte di i script scritti sò copiati in i cuntenituri durante l'assemblea. Avemu ancu cambiatu a shell di cumanda in /bin/bash per facilità di usu di cuntenutu /etc/os-release. Questu hà evitatu a necessità di specificà a versione OS in u codice.

Inoltre, un picculu script bash hè statu scrittu chì vi permette di lancià un containeru cù diversi cumandamenti. Per esempiu, questi puderianu esse scripts per a furmazione di e rete neurali chì sò stati posti prima in u cuntinuu, o una shell di cumanda per debugging è monitorizà u funziunamentu di u cuntinuu:

Script per lancià u containeru

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

Se stu script bash hè eseguitu senza parametri, u script serà chjamatu in u cuntinuu train_nn.R cù i valori predeterminati; se u primu argumentu pusitivu hè "bash", allora u cuntinuu principiarà in modu interattivu cù una shell di cumanda. In tutti l'altri casi, i valori di l'argumenti posizionali sò sustituiti: CMD="Rscript /app/train_nn.R $@".

Hè nutate chì i cartulari cù dati di fonte è basa di dati, è ancu u repertoriu per salvà mudelli furmati, sò muntati in u cuntinuu da u sistema d'ospiti, chì permette di accede à i risultati di i scripts senza manipulazioni innecessarii.

7. Utilizendu parechje GPU in Google Cloud

Una di e caratteristiche di a cumpetizione era a dati assai rumorosi (vede a stampa di titulu, presa in prestito da @Leigh.plt da ODS slack). Grandi batchs aiutanu à cumbatte questu, è dopu l'esperimenti in un PC cù 1 GPU, avemu decisu di maestru di mudelli di furmazione nantu à parechje GPU in u nuvulu. Adupratu GoogleCloud (bona guida à i principii) per via di a grande selezzione di cunfigurazioni dispunibuli, prezzi ragiunate è $ 300 bonus. Per avidità, aghju urdinatu una istanza 4xV100 cù un SSD è una tonna di RAM, è questu era un grande sbagliu. Una tale macchina manghja soldi rapidamente; pudete andà à spirimintà senza una pipeline pruvata. Per scopi educativi, hè megliu piglià u K80. Ma a grande quantità di RAM hè stata utile - u cloud SSD ùn hà micca impressuatu cù u so rendimentu, cusì a basa di dati hè stata trasferita à dev/shm.

Di più grande interessu hè u frammentu di codice rispunsevuli di utilizà parechje GPU. Prima, u mudellu hè creatu nantu à u CPU cù un gestore di cuntestu, cum'è in 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
  )
})

Allora u mudellu micca compilatu (questu hè impurtante) hè copiatu à un certu numaru di GPU dispunibili, è solu dopu hè compilatu:

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

A tecnica classica di freezing all layers except the last one, furmà l'ultima capa, unfreezing è retraining u mudellu sanu per parechje GPU ùn pudia esse implementatu.

A furmazione hè stata monitorata senza usu. tensorboard, limitendu à registrà logs è salvà mudelli cù nomi informativi dopu ogni epoca:

Callbacks

# Шаблон имени файла лога
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. Invece di una cunclusione

Una quantità di prublemi chì avemu scontru ùn sò ancu stati superati:

  • в keras ùn ci hè micca una funzione pronta per a ricerca automatica di u ritmu di apprendimentu ottimale (analogicu lr_finder in biblioteca viloci.ai); Cù qualchì sforzu, hè pussibule di portà implementazioni di terze parti à R, per esempiu, questu;
  • in cunseguenza di u puntu precedente, ùn era micca pussibule di selezziunà a veloce di furmazione curretta quandu si usanu parechji GPU;
  • ci hè una mancanza di architetture di rete neurale muderni, in particulare quelli chì sò pre-furmati in imagenet;
  • nisuna pulitica di ciclu è tassi di apprendimentu discriminatori (l'annealing di cosenu era à a nostra dumanda implementatu, Grazie skeydan).

Chì cose utili sò state amparate da sta cumpetizione:

  • In un hardware relativamente bassu, pudete travaglià cù volumi di dati decentu (assai volte a dimensione di RAM) senza dolore. Saccu di plastica dati.tavula salva a memoria per via di a mudificazione in situ di e tavule, chì evita di cupià, è quandu s'utilice currettamente, e so capacità dimustranu quasi sempre a più alta velocità trà tutti l'arnesi cunnisciuti da noi per i linguaggi di scrittura. A salvezza di dati in una basa di dati vi permette, in parechji casi, di ùn pensà à tuttu à a necessità di strincà tuttu u dataset in RAM.
  • E funzioni lenti in R ponu esse rimpiazzate cù quelle veloci in C++ cù u pacchettu Rcpp. Sè in più di usu RcppThread o RcppParallel, avemu avutu implementazioni multi-piattaforma multi-threaded, cusì ùn ci hè bisognu di parallelizà u codice à u livellu R.
  • Pacchettu Rcpp pò esse usatu senza cunniscenza seria di C ++, u minimu necessariu hè delineatu ccà. I fugliali d'intestazione per una quantità di biblioteche C cool cum'è xtensor dispunibule nantu à CRAN, vale à dì, una infrastruttura hè in forma per l'implementazione di prughjetti chì integranu codice C++ d'alta prestazione pronta in R. A cunvenzione supplementaria hè l'evidenziazione di sintassi è un analizzatore di codice C++ staticu in RStudio.
  • docopt permette di eseguisce script autònumi cù paràmetri. Questu hè cunvenutu per aduprà in un servitore remoto, incl. sottu docker. In RStudio, hè inconveniente per fà parechje ore di esperimenti cù a furmazione di rete neurali, è l'installazione di l'IDE in u servitore stessu ùn hè micca sempre ghjustificatu.
  • Docker assicura a portabilità di u codice è a riproducibilità di i risultati trà i sviluppatori cù diverse versioni di u SO è di e librerie, è ancu a facilità d'esekzione in i servitori. Pudete lancià tuttu u pipeline di furmazione cù un solu cumandamentu.
  • Google Cloud hè un modu di budget-friendly per sperimentà nantu à hardware caru, ma avete bisognu di sceglie cunfigurazioni cun cura.
  • A misurazione di a velocità di i frammenti di codice individuale hè assai utile, soprattuttu quandu si combina R è C ++, è cù u pacchettu. bancu - ancu assai faciule.

In generale, sta sperienza hè stata assai gratificante è cuntinuemu à travaglià per risolve alcune di e prublemi suscitati.

Source: www.habr.com

Add a comment