Quick Draw Doodle Rekonesans: ki jan fè zanmi ak R, C++ ak rezo neral

Quick Draw Doodle Rekonesans: ki jan fè zanmi ak R, C++ ak rezo neral

Hey Habr!

Otòn pase a, Kaggle te òganize yon konpetisyon pou klasifye foto trase men yo, Quick Draw Doodle Recognition, nan ki, pami lòt moun, yon ekip R-syantifik te patisipe: Artem Klevtsova, Manadjè Philippa и Andrey Ogurtsov. Nou pa pral dekri konpetisyon an an detay; sa te deja fè nan dènye piblikasyon.

Fwa sa a, li pa t 'travay soti ak agrikilti meday, men yo te gen anpil eksperyans ki gen anpil valè, kidonk mwen ta renmen di kominote a sou yon kantite bagay ki pi enteresan ak itil sou Kagle ak nan travay chak jou. Pami sijè yo diskite: lavi difisil san yo pa OpenCV, analiz JSON (egzanp sa yo egzamine entegrasyon kòd C++ nan scripts oswa pakè nan R lè l sèvi avèk Rcpp), paramètrizasyon nan scripts ak dockerization nan solisyon final la. Tout kòd ki soti nan mesaj la nan yon fòm apwopriye pou ekzekisyon ki disponib nan depo.

Table of Contents

  1. Efikas chaje done ki soti nan CSV nan MonetDB
  2. Prepare pakèt
  3. Iteratè pou dechaje lo nan baz done a
  4. Chwazi yon achitekti modèl
  5. Paramètrizasyon script
  6. Dockerization nan scripts
  7. Sèvi ak plizyè GPU sou Google Cloud
  8. Olye pou yo yon konklizyon

1. Efikas chaje done ki soti nan CSV nan baz done MonetDB

Done yo nan konpetisyon sa a pa bay imaj yo pare, men nan fòm 340 dosye CSV (yon fichye pou chak klas) ki gen JSON ak kowòdone pwen. Lè nou konekte pwen sa yo ak liy, nou jwenn yon imaj final ki mezire 256x256 piksèl. Epitou pou chak dosye gen yon etikèt ki endike si klasifikasyon yo te itilize a te rekonèt foto a kòrèkteman nan moman yo te kolekte done yo, yon kòd de lèt ki endike peyi kote otè foto a te rete a, yon idantifyan inik, yon horodatage. ak yon non klas ki koresponn ak non fichye a. Yon vèsyon senplifye nan done orijinal yo peze 7.4 GB nan achiv la ak apeprè 20 GB apre debake, done yo konplè apre debake pran 240 GB. Òganizatè yo te asire ke tou de vèsyon repwodui desen yo menm, sa vle di vèsyon konplè a te redondants. Nan nenpòt ka, estoke 50 milyon imaj nan dosye grafik oswa nan fòm lan nan etalaj te imedyatman konsidere kòm rantabilite, epi nou deside rantre tout fichye CSV nan achiv la. train_simplified.zip antre nan baz done a ak jenerasyon ki vin apre nan imaj ki gen gwosè obligatwa "sou vole a" pou chak pakèt.

Yo te chwazi yon sistèm ki byen pwouve kòm DBMS la MonetDB, sètadi yon aplikasyon pou R kòm yon pake MonetDBLite. Pake a gen ladan yon vèsyon entegre nan sèvè baz done a epi li pèmèt ou ranmase sèvè a dirèkteman nan yon sesyon R epi travay avèk li la. Kreye yon baz done ak konekte li yo fèt ak yon sèl lòd:

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

Nou pral bezwen kreye de tab: youn pou tout done, lòt la pou enfòmasyon sèvis sou dosye telechaje (itil si yon bagay ale mal epi pwosesis la dwe rekòmanse apre telechaje plizyè fichye):

Kreye tab

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

Fason ki pi rapid pou chaje done nan baz done a se te dirèkteman kopi dosye CSV lè l sèvi avèk SQL - kòmand COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTkote tablename - non tab la ak path - chemen an nan dosye a. Pandan w ap travay ak achiv la, li te dekouvri ke aplikasyon an bati-an unzip nan R pa travay kòrèkteman ak yon kantite fichye ki soti nan achiv la, kidonk nou te itilize sistèm nan unzip (itilize paramèt la getOption("unzip")).

Fonksyon pou ekri nan baz done a

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

Si ou bezwen transfòme tab la anvan ou ekri li nan baz done a, li ase yo pase nan agiman an preprocess fonksyon ki pral transfòme done yo.

Kòd pou chaje done sekans nan baz done a:

Ekri done nan baz done a

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

Tan loading done yo ka varye selon karakteristik vitès kondwi a itilize. Nan ka nou an, li ak ekri nan yon sèl SSD oswa soti nan yon kondwi flash (fichye sous) nan yon SSD (DB) pran mwens pase 10 minit.

Li pran kèk segonn plis pou kreye yon kolòn ak yon etikèt klas nonb antye relatif ak yon kolòn endèks (ORDERED INDEX) ak nimewo liy kote yo pral pran echantiyon obsèvasyon yo lè w ap kreye pakèt:

Kreye lòt kolòn ak endèks

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

Pou rezoud pwoblèm nan kreye yon pakèt sou vole a, nou te bezwen reyalize vitès maksimòm nan ekstrè ranje o aza nan tab la. doodles. Pou sa nou te itilize 3 ke trik nouvèl. Premye a te diminye dimansyon nan kalite ki estoke ID obsèvasyon an. Nan seri done orijinal la, kalite ki nesesè pou konsève ID a se bigint, men kantite obsèvasyon yo fè li posib pou anfòm idantifyan yo, ki egal ak nimewo ordinal la, nan kalite a. int. Rechèch la pi vit nan ka sa a. Trick dezyèm lan te itilize ORDERED INDEX — nou te pran desizyon sa a anpirik, nou te pase nan tout sa ki disponib opsyon. Twazyèm lan se te itilize paramèt demann. Sans nan metòd la se egzekite lòd la yon fwa PREPARE ak itilizasyon ki vin apre nan yon ekspresyon prepare lè w ap kreye yon pakèt demann nan menm kalite a, men an reyalite gen yon avantaj an konparezon ak yon sèl senp. SELECT te tounen soti nan seri a nan erè estatistik.

Pwosesis la nan telechaje done konsome pa plis pase 450 MB nan RAM. Sa vle di, apwòch ki dekri a pèmèt ou deplase seri done ki peze plizyè dizèn jigokte sou prèske nenpòt pyès ki nan konpitè bidjè, ki gen ladan kèk aparèy yon sèl tablo, ki se trè fre.

Tout sa ki rete se mezire vitès la nan rekipere done (o aza) epi evalye dekale a lè pran echantiyon pakèt ki gen diferan gwosè:

Baz done referans

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 Rekonesans: ki jan fè zanmi ak R, C++ ak rezo neral

2. Prepare pakèt

Tout pwosesis preparasyon pakèt la konsiste de etap sa yo:

  1. Analize plizyè JSON ki gen vektè fisèl ak kowòdone pwen.
  2. Desen liy ki gen koulè ki baze sou kowòdone pwen yo sou yon imaj ki gen gwosè obligatwa (pa egzanp, 256×256 oswa 128×128).
  3. Konvèti imaj ki kapab lakòz yo nan yon tensor.

Kòm yon pati nan konpetisyon an nan mitan nwayo Python, pwoblèm nan te rezoud prensipalman lè l sèvi avèk OpenCV. Youn nan analogue ki pi senp ak pi evidan nan R ta sanble sa a:

Aplike JSON pou konvèsyon Tensor nan 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)
}

Desen fèt lè l sèvi avèk zouti R estanda ak sove nan yon PNG tanporè ki estoke nan RAM (sou Linux, anyè R tanporè yo sitiye nan anyè a. /tmp, monte nan RAM). Lè sa a, yo li dosye sa a kòm yon etalaj ki genyen twa dimansyon ak nimewo ki sòti nan 0 a 1. Sa a enpòtan paske yo ta li yon BMP plis konvansyonèl nan yon etalaj anvan tout koreksyon ak kòd koulè hex.

Ann teste rezilta a:

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 Rekonesans: ki jan fè zanmi ak R, C++ ak rezo neral

Pakèt la tèt li pral fòme jan sa a:

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

Aplikasyon sa a te sanble pa pi bon pou nou, depi fòmasyon nan gwo pakèt pran yon tan endesan, epi nou deside pwofite eksperyans kòlèg nou yo lè nou itilize yon bibliyotèk pwisan. OpenCV. Lè sa a, pa te gen okenn pake ki pare pou R (pa gen okenn kounye a), se konsa yon aplikasyon minim nan fonksyonalite ki nesesè yo te ekri nan C ++ ak entegrasyon nan kòd R lè l sèvi avèk. Rcpp.

Pou rezoud pwoblèm nan, yo te itilize pakè ak bibliyotèk sa yo:

  1. OpenCV pou travay ak imaj ak trase liy. Itilize bibliyotèk sistèm pre-enstale ak dosye header, osi byen ke lyen dinamik.

  2. xtensor pou travay ak etalaj miltidimansyonèl ak tensè. Nou itilize dosye header ki enkli nan pake R ki gen menm non an. Bibliyotèk la pèmèt ou travay ak etalaj miltidimansyon, tou de nan ranje pi gwo ak kolòn pi gwo lòd.

  3. ndjson pou analiz JSON. Bibliyotèk sa a itilize nan xtensor otomatikman si li prezan nan pwojè a.

  4. RcppThread pou òganize pwosesis milti-threaded nan yon vektè soti nan JSON. Itilize dosye header yo bay pakè sa a. Soti nan pi popilè RcppParallel Pake a, pami lòt bagay, gen yon mekanis entèwonp bouk entegre.

Li se vo anyen sa xtensor te tounen yon obsève: Anplis de sa nan lefèt ke li gen anpil fonksyonalite ak pèfòmans segondè, devlopè li yo te vin byen reponn epi reponn kesyon san pèdi tan ak an detay. Avèk èd yo, li te posib aplike transfòmasyon nan matris OpenCV nan tenseur xtensor, osi byen ke yon fason yo konbine tenseur imaj 3 dimansyon nan yon tenseur 4 dimansyon nan dimansyon kòrèk la (pakèt nan tèt li).

Materyèl pou aprann Rcpp, xtensor ak 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

Pou konpile fichye ki sèvi ak fichye sistèm ak lyen dinamik ak bibliyotèk ki enstale sou sistèm nan, nou te itilize mekanis plugin ki aplike nan pake a. Rcpp. Pou jwenn otomatikman chemen ak drapo, nou te itilize yon itilite Linux popilè pkg-config.

Aplikasyon Plugin Rcpp pou itilize bibliyotèk 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)
  ))
})

Kòm rezilta operasyon plugin a, valè sa yo pral ranplase pandan pwosesis konpilasyon an:

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"

Kòd aplikasyon pou analize JSON ak jenere yon pakèt pou transmisyon nan modèl la bay anba spoiler la. Premyèman, ajoute yon anyè pwojè lokal pou chèche dosye header (ki nesesè pou ndjson):

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

Aplikasyon JSON pou konvèsyon tensor nan 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;
}

Kòd sa a ta dwe mete nan dosye a src/cv_xt.cpp epi konpile ak kòmandman an Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); obligatwa tou pou travay nlohmann/json.hpp nan depo. Kòd la divize an plizyè fonksyon:

  • to_xt — yon fonksyon modèl pou transfòme yon matris imaj (cv::Mat) nan yon tensor xt::xtensor;

  • parse_json — fonksyon an analize yon fisèl JSON, ekstrè kowòdone pwen yo, anbalaj yo nan yon vektè;

  • ocv_draw_lines — soti nan vektè a ki kapab lakòz nan pwen, trase liy ki gen plizyè koulè;

  • process — konbine fonksyon ki anwo yo epi tou li ajoute kapasite nan echèl imaj la ki kapab lakòz;

  • cpp_process_json_str - wrapper sou fonksyon an process, ki ekspòte rezilta a nan yon R-objè (etalaj miltidimansyonèl);

  • cpp_process_json_vector - wrapper sou fonksyon an cpp_process_json_str, ki pèmèt ou trete yon vektè fisèl nan mòd milti-threaded.

Pou trase liy ki gen plizyè koulè, yo te itilize modèl koulè HSV, ki te swiv pa konvèsyon an RGB. Ann teste rezilta a:

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

Quick Draw Doodle Rekonesans: ki jan fè zanmi ak R, C++ ak rezo neral
Konparezon vitès aplikasyon yo nan R ak 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 Rekonesans: ki jan fè zanmi ak R, C++ ak rezo neral

Kòm ou ka wè, ogmantasyon vitès la te vin trè enpòtan, epi li pa posib pou ratrape kòd C++ lè w paralelize kòd R.

3. Iteratè pou dechaje lo nan baz done a

R gen yon repitasyon byen merite kòm yon lang pou trete done ki anfòm nan RAM, pandan y ap Python se plis karakterize pa pwosesis done iteratif, ki pèmèt ou fasil epi natirèlman aplike kalkil ki pa debaz yo (kalkil lè l sèvi avèk memwa ekstèn). Yon egzanp klasik ak enpòtan pou nou nan yon kontèks pwoblèm ki dekri a se rezo neral gwo twou san fon ki fòme pa metòd la desandan gradyan ak apwoksimasyon nan gradyan an nan chak etap lè l sèvi avèk yon ti pòsyon nan obsèvasyon, oswa mini-pakèt.

Kad aprantisaj pwofon ekri nan Python gen klas espesyal ki aplike iteratè ki baze sou done: tab, foto nan dosye, fòma binè, elatriye Ou ka itilize opsyon ki pare oswa ekri pwòp ou a pou travay espesifik. Nan R nou ka pran avantaj de tout karakteristik bibliyotèk Python la keras ak divès kalite backend li yo lè l sèvi avèk pake a ki gen menm non, ki an vire travay sou tèt pake a retikule. Lèt la merite yon atik long separe; li pa sèlman pèmèt ou kouri kòd Python soti nan R, men tou, pèmèt ou transfere objè ant sesyon R ak Python, otomatikman fè tout konvèsyon kalite ki nesesè yo.

Nou te debarase m de nesesite pou estoke tout done yo nan RAM lè l sèvi avèk MonetDBlite, tout travay la "rezo neral" pral fèt pa kòd orijinal la nan Python, nou jis bezwen ekri yon iteratè sou done yo, paske pa gen anyen ki pare. pou yon sitiyasyon konsa nan swa R oswa Python. Gen esansyèlman sèlman de kondisyon pou li: li dwe retounen lo nan yon bouk kontinuèl epi sove eta li ant iterasyon (lèt la nan R aplike nan fason ki pi senp lè l sèvi avèk fèmen). Anvan sa, li te oblije konvèti klèman R etalaj nan etalaj numpy andedan iteratè a, men vèsyon aktyèl la nan pake a. keras fè li tèt li.

Iteratè a pou fòmasyon ak done validation yo te vin jan sa a:

Iteratè pou fòmasyon ak done validation

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

Fonksyon an pran kòm antre yon varyab ak yon koneksyon ak baz done a, kantite liy yo itilize, kantite klas, gwosè pakèt, echèl (scale = 1 koresponn ak rann imaj 256x256 piksèl, scale = 0.5 — 128x128 piksèl), endikatè koulè (color = FALSE presize rann nan gri lè yo itilize color = TRUE chak konjesyon serebral trase nan yon nouvo koulè) ak yon endikatè pre-pwosesis pou rezo pre-antre sou imagenet. Lèt la nesesè yo nan lòd yo echèl valè pixel soti nan entèval la [0, 1] nan entèval la [-1, 1], ki te itilize lè fòmasyon apwovizyone a. keras modèl.

Fonksyon ekstèn la gen yon seri kontwòl kalite agiman, yon tab data.table ak nimewo liy owaza melanje soti nan samples_index ak nimewo pakèt, kontwa ak kantite maksimòm pakèt, osi byen ke yon ekspresyon SQL pou dechaje done ki sòti nan baz done a. Anplis de sa, nou defini yon analòg rapid nan fonksyon anndan an keras::to_categorical(). Nou te itilize prèske tout done yo pou fòmasyon, kite mwatye yon pousan pou validation, kidonk gwosè epòk la te limite pa paramèt la. steps_per_epoch lè yo rele keras::fit_generator(), ak kondisyon an if (i > max_i) te travay sèlman pou iteratè validation la.

Nan fonksyon entèn la, endis ranje yo rekipere pou pwochen pakèt la, dosye yo dechaje nan baz done a ak kontwa pakèt la ogmante, analiz JSON (fonksyon cpp_process_json_vector(), ekri an C++) epi kreye etalaj ki koresponn ak foto yo. Lè sa a, vektè yon sèl-cho ak etikèt klas yo kreye, etalaj ak valè pixel ak etikèt yo konbine nan yon lis, ki se valè a retounen. Pou pi vit travay, nou te itilize kreyasyon endèks nan tab data.table ak modifikasyon atravè lyen an - san yo pa pake sa yo "chips" done.tab Li trè difisil pou imajine travay efektivman ak nenpòt kantite done enpòtan nan R.

Rezilta mezi vitès sou yon laptop Core i5 se jan sa a:

Iteratè referans

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 Rekonesans: ki jan fè zanmi ak R, C++ ak rezo neral

Si ou gen yon kantite lajan ase nan RAM, ou ka seryezman akselere operasyon an nan baz done a pa transfere li nan RAM sa a menm (32 GB se ase pou travay nou an). Nan Linux, patisyon an monte pa default /dev/shm, okipe jiska mwatye kapasite RAM la. Ou ka mete aksan sou plis pa koreksyon /etc/fstabpou jwenn yon dosye like tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Asire w ke w rekòmanse epi tcheke rezilta a lè w ap kouri lòd la df -h.

Iteratè a pou done tès yo sanble pi senp, paske done tès la anfòm nèt nan RAM:

Iteratè pou done tès yo

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. Seleksyon modèl achitekti

Premye achitekti yo te itilize te mobilenet v1, karakteristik yo ki diskite nan sa a mesaj. Li enkli kòm estanda keras epi, kòmsadwa, ki disponib nan pake a ki gen menm non pou R. Men, lè w ap eseye sèvi ak li ak imaj yon sèl-chanèl, yon bagay etranj te tounen soti: tenseur a opinyon dwe toujou gen dimansyon an. (batch, height, width, 3), se sa ki, kantite chanèl pa ka chanje. Pa gen okenn limit sa yo nan Python, kidonk nou te kouri epi ekri pwòp aplikasyon nou an nan achitekti sa a, apre atik orijinal la (san yo pa abandone a ki nan vèsyon an keras):

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

Dezavantaj yo nan apwòch sa a se evidan. Mwen vle teste yon anpil nan modèl, men okontrè, mwen pa vle reekri chak achitekti manyèlman. Nou te tou prive de opòtinite pou sèvi ak pwa yo nan modèl pre-antre sou imagenet. Kòm dabitid, etidye dokiman an te ede. Fonksyon get_config() pèmèt ou jwenn yon deskripsyon modèl la nan yon fòm apwopriye pou koreksyon (base_model_conf$layers - yon lis R regilye), ak fonksyon an from_config() fè konvèsyon ranvèse nan yon objè modèl:

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)

Koulye a, li pa difisil yo ekri yon fonksyon inivèsèl jwenn nenpòt nan apwovizyone a keras modèl avèk oswa san pwa ki fòme sou imagenet:

Fonksyon pou chaje achitekti pare yo

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

Lè w ap itilize imaj yon sèl-chanèl, pa gen okenn pwa ki te antrene anvan yo itilize. Sa a ta ka fiks: lè l sèvi avèk fonksyon an get_weights() jwenn pwa modèl yo nan fòm yon lis etalaj R, chanje dimansyon premye eleman nan lis sa a (pa pran yon sèl chanèl koulè oswa fè yon mwayèn tout twa), ak Lè sa a, chaje pwa yo tounen nan modèl la ak fonksyon an. set_weights(). Nou pa janm ajoute fonksyonalite sa a, paske nan etap sa a li te deja klè ke li te pi pwodiktif pou travay ak foto koulè.

Nou te fè pi fò nan eksperyans yo lè l sèvi avèk vèsyon mobilenet 1 ak 2, osi byen ke resnet34. Plis achitekti modèn tankou SE-ResNeXt te fè byen nan konpetisyon sa a. Malerezman, nou pa t 'gen aplikasyon pare-fè a dispozisyon nou, epi nou pa t' ekri pwòp pa nou (men nou pral definitivman ekri).

5. Parametrizasyon scripts

Pou konvenyans, tout kòd pou kòmanse fòmasyon yo te fèt kòm yon script sèl, paramèt lè l sèvi avèk dokopt jan sa a:

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)

Pake dokopt reprezante aplikasyon an http://docopt.org/ pou R. Avèk èd li yo, scripts yo te lanse ak kòmandman senp tankou Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db oswa ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, si dosye train_nn.R se ègzekutabl (kòmand sa a pral kòmanse fòme modèl la resnet50 sou imaj twa koulè ki mezire 128x128 piksèl, baz done a dwe lokalize nan katab la /home/andrey/doodle_db). Ou ka ajoute vitès aprantisaj, kalite optimisateur, ak nenpòt lòt paramèt customizable nan lis la. Nan pwosesis la nan prepare piblikasyon an, li te tounen soti ke achitekti la mobilenet_v2 soti nan vèsyon aktyèl la keras nan R itilize pa dwe akòz chanjman ki pa pran an kont nan pake R la, nou ap tann pou yo ranje li.

Apwòch sa a te fè li posib siyifikativman akselere eksperyans ak modèl diferan konpare ak lansman an plis tradisyonèl nan scripts nan RStudio (nou sonje pake a kòm yon altènatif posib tfruns). Men, avantaj prensipal la se kapasite nan jere fasilman lansman scripts nan Docker oswa tou senpleman sou sèvè a, san yo pa enstale RStudio pou sa.

6. Dockerization nan scripts

Nou te itilize Docker pou asire portabilite anviwònman an pou modèl fòmasyon ant manm ekip yo ak pou deplwaman rapid nan nwaj la. Ou ka kòmanse fè konesans ak zouti sa a, ki se relativman etranj pou yon pwogramè R, ak sa a seri piblikasyon oswa kou videyo.

Docker pèmèt ou tou de kreye pwòp imaj ou nan grate epi sèvi ak lòt imaj kòm yon baz pou kreye pwòp ou a. Lè nou analize opsyon ki disponib yo, nou te rive nan konklizyon ke enstale NVIDIA, chofè CUDA + cuDNN ak bibliyotèk Python se yon pati jistis volumineuz nan imaj la, epi nou deside pran imaj ofisyèl la kòm yon baz. tensorflow/tensorflow:1.12.0-gpu, ajoute pakè R ki nesesè yo la.

Docker final la te sanble ak sa a:

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

Pou konvenyans, pakè yo itilize yo te mete nan varyab; èstime nan ekriti yo ekri yo kopye andedan resipyan yo pandan asanble. Nou menm tou nou chanje kokiy lòd la /bin/bash pou fasilite itilizasyon kontni /etc/os-release. Sa a te evite bezwen presize vèsyon an OS nan kòd la.

Anplis de sa, yo te ekri yon ti script bash ki pèmèt ou lanse yon veso ki gen kòmandman divès kalite. Pou egzanp, sa yo ta ka scripts pou fòmasyon rezo neral ki te deja mete andedan veso a, oswa yon kokiy lòd pou debogaj ak kontwole operasyon an nan veso a:

Script pou lanse veso a

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

Si script sa a kouri san paramèt, yo pral rele script la andedan veso a train_nn.R ak valè default; si premye agiman pozisyon an se "bash", Lè sa a, veso a ap kòmanse entèaktif ak yon kokiy lòd. Nan tout lòt ka yo, valè agiman pozisyon yo ranplase: CMD="Rscript /app/train_nn.R $@".

Li se vo anyen ke anyè yo ak done sous ak baz done, osi byen ke anyè a pou ekonomize modèl ki resevwa fòmasyon, yo monte andedan veso a soti nan sistèm lame a, ki pèmèt ou jwenn aksè nan rezilta yo nan scripts yo san yo pa manipilasyon nesesè.

7. Sèvi ak plizyè GPU sou Google Cloud

Youn nan karakteristik yo nan konpetisyon an se te done yo trè fè bwi (gade foto tit la, prete nan @Leigh.plt nan ODS slack). Gwo pakèt ede konbat sa a, epi apre eksperyans sou yon PC ak 1 GPU, nou deside metrize modèl fòmasyon sou plizyè GPU nan nwaj la. Itilize GoogleCloud (bon gid pou de baz yo) akòz seleksyon an gwo nan konfigirasyon ki disponib, pri rezonab ak $ 300 bonis. Soti nan Evaris, mwen te bay lòd pou yon egzanp 4xV100 ak yon SSD ak yon tòn RAM, e se te yon gwo erè. Yon machin konsa manje lajan byen vit; ou ka fè eksperyans san yon tiyo pwouve. Pou rezon edikasyon, li pi bon pou w pran K80 la. Men, gwo kantite RAM te vin an sou la men - SSD nwaj la pa t 'enpresyone ak pèfòmans li yo, se konsa baz done a te transfere nan dev/shm.

Pi gwo enterè se fragman kòd ki responsab pou itilize plizyè GPU. Premyèman, modèl la kreye sou CPU a lè l sèvi avèk yon manadjè kontèks, jis tankou nan 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
  )
})

Lè sa a, modèl ki pa konpile (sa a enpòtan) kopye nan yon kantite GPU ki disponib, epi sèlman apre sa li konpile:

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

Teknik klasik konjelasyon tout kouch eksepte dènye a, fòmasyon dènye kouch la, dekonjelasyon ak refòmasyon tout modèl la pou plizyè GPU pa t 'kapab aplike.

Fòmasyon yo te kontwole san yo pa itilize. tensorboard, limite tèt nou nan anrejistreman mòso bwa ak ekonomize modèl ak non enfòmatif apre chak epòk:

Rappels

# Шаблон имени файла лога
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. Olye de yon konklizyon

Gen plizyè pwoblèm ke nou rankontre yo poko simonte:

  • в keras pa gen okenn fonksyon ki pare pou chèche otomatikman to aprantisaj optimal (analòg lr_finder nan bibliyotèk la vit.ai); Avèk kèk efò, li posib pou pote aplikasyon twazyèm pati yo nan R, pou egzanp, sa a;
  • kòm yon konsekans pwen anvan an, li pa t posib yo chwazi vitès fòmasyon kòrèk la lè w ap itilize plizyè GPU;
  • gen yon mank de achitekti modèn rezo neral, espesyalman moun ki pre-fòme sou imagenet;
  • pa gen okenn politik sik ak pousantaj aprantisaj diskriminatwa (rekwir kosinen te sou demann nou an aplike, mèsi skeydan).

Ki bagay itil yo te aprann nan konpetisyon sa a:

  • Sou pyès ki nan konpitè relativman ba-pouvwa, ou ka travay ak desan (anpil fwa gwosè RAM) volim done san doulè. Sache plastik done.tab sove memwa akòz modifikasyon an plas nan tab yo, ki evite kopye yo, epi lè yo itilize kòrèkteman, kapasite li yo prèske toujou demontre vitès ki pi wo nan mitan tout zouti nou konnen pou lang scripting. Ekonomize done nan yon baz done pèmèt ou, nan anpil ka, pa panse ditou sou nesesite pou peze dataset la tout antye nan RAM.
  • Fonksyon ralanti nan R ka ranplase ak sa ki rapid nan C++ lè l sèvi avèk pake a Rcpp. Si anplis itilize RcppThread oswa RcppParallel, nou jwenn aplikasyon milti-threaded kwa-platfòm, kidonk pa gen okenn bezwen paralelize kòd la nan nivo R la.
  • Pake Rcpp ka itilize san konesans grav nan C++, se minimòm ki nesesè yo dekri isit la. Dosye header pou yon kantite fre C-bibliotèk tankou xtensor disponib sou CRAN, se sa ki, yon enfrastrikti ap fòme pou aplikasyon an nan pwojè ki entegre kòd C++ pèfòmans pare yo nan R. Konvenyans adisyonèl se en sentaks ak yon analizè kòd estatik C++ nan RStudio.
  • dokopt pèmèt ou kouri scripts endepandan ak paramèt. Sa a se pratik pou itilize sou yon sèvè aleka, enkli. anba docker. Nan RStudio, li pa konvenyan pou fè anpil èdtan nan eksperyans ak fòmasyon rezo neral, epi enstale IDE a sou sèvè a li menm pa toujou jistifye.
  • Docker asire transparans kòd ak repwodibilite rezilta ant devlopè ak diferan vèsyon eksplwatasyon an ak bibliyotèk, osi byen ke fasilite nan ekzekisyon sou sèvè. Ou ka lanse tout tiyo fòmasyon an ak yon sèl kòmand.
  • Google Cloud se yon fason ki zanmitay bidjè pou fè eksperyans sou pyès ki nan konpitè chè, men ou bezwen chwazi konfigirasyon ak anpil atansyon.
  • Mezire vitès fragman kòd endividyèl yo trè itil, espesyalman lè w konbine R ak C++, ak pake a. ban - tou trè fasil.

An jeneral, eksperyans sa a te trè rekonpanse e nou kontinye travay pou rezoud kèk nan pwoblèm ki te soulve yo.

Sous: www.habr.com

Add nouvo kòmantè