Quick Draw Doodle Unerkennung: wéi Dir Frënn mat R, C++ an neurale Netzwierker mécht

Quick Draw Doodle Unerkennung: wéi Dir Frënn mat R, C++ an neurale Netzwierker mécht

Hey Habr!

De leschte Hierscht huet de Kaggle e Concours organiséiert fir handgezeechente Biller ze klassifizéieren, Quick Draw Doodle Recognition, un deem ënner anerem eng Equipe vu R-Wëssenschaftler deelgeholl huet: Artem Klevtsova, Philippa Manager и Andrey Ogurtsov. Mir wäerten d'Konkurrenz net am Detail beschreiwen; dat ass scho gemaach an rezent Verëffentlechung.

Dës Kéier huet et net mat Medaillelandwirtschaft geklappt, awer vill wäertvoll Erfarunge goufe gesammelt, also wëll ech der Gemeng iwwer eng Rei vun den interessantsten an nëtzlechsten Saachen op Kagle an am Alldag erzielen. Ënnert den Themen diskutéiert: schwéier Liewen ouni OpenCV, JSON Parsing (dës Beispiller ënnersicht d'Integratioun vum C++ Code a Skripte oder Packagen am R mat Rcpp), Parameteriséierung vu Skripte an Dockeriséierung vun der definitiver Léisung. All Code aus dem Message an enger Form gëeegent fir Ausféierung ass verfügbar an Repositories.

Inhalt:

  1. Lued d'Donnéeën vun der CSV effizient an MonetDB
  2. Chargen virbereeden
  3. Iteratoren fir Chargen aus der Datebank ze entluede
  4. Auswiel vun engem Model Architektur
  5. Skript Parameteriséierung
  6. Dockeriséierung vu Scripten
  7. Benotzt verschidde GPUs op Google Cloud
  8. Amplaz vun enger Konklusioun

1. Effizient lued Daten aus CSV an d'MonetDB Datebank

D'Donnéeën an dësem Concours ginn net a Form vu fäerdege Biller geliwwert, awer a Form vun 340 CSV Dateien (eng Datei fir all Klass) mat JSONs mat Punktkoordinaten. Andeems Dir dës Punkte mat Linnen verbënnt, kréie mir en endgültegt Bild mat 256x256 Pixelen. Och fir all Rekord gëtt et e Label deen uginn ob d'Bild richteg erkannt gouf vum Klassifizéierer, deen an der Zäit wou d'Datetaz gesammelt gouf, en zwee-Bréifcode vum Land wou den Auteur vum Bild wunnt, en eenzegaartegen Identifizéierer, en Zäitstempel. an e Klassennumm deen dem Dateinumm entsprécht. Eng vereinfacht Versioun vun den ursprénglechen Donnéeën waacht 7.4 GB am Archiv an ongeféier 20 GB no der Auspackung, déi voll Donnéeën no der Auspackung hëlt 240 GB. D'Organisateuren hunn dofir gesuergt datt béid Versiounen déiselwecht Zeechnungen reproduzéiert hunn, dat heescht datt déi voll Versioun iwwerflësseg war. Op jidde Fall, d'Späichere vu 50 Millioune Biller a Grafikdateien oder a Form vun Arrays gouf direkt als onrentabel ugesinn, a mir hu beschloss all CSV Dateien aus dem Archiv ze fusionéieren train_simplified.zip an d'Datebank mat der spéider Generatioun vu Biller vun der erfuerderter Gréisst "on the fly" fir all Batch.

E gutt bewährte System gouf als DBMS gewielt MonetDB, nämlech d'Ëmsetzung fir R als Package MonetDBLite. De Package enthält eng embedded Versioun vum Datebankserver an erlaabt Iech de Server direkt vun enger R Sessioun opzehuelen an domat ze schaffen. Eng Datebank erstellen an domat verbannen ginn mat engem Kommando ausgefouert:

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

Mir mussen zwou Dëscher erstellen: eng fir all Daten, déi aner fir Serviceinformatioun iwwer erofgeluede Dateien (nëtzlech wann eppes falsch geet an de Prozess muss erëmfonnt ginn nodeems Dir verschidde Dateien erofgelueden hutt):

Schafen Dëscher

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

De schnellste Wee fir Daten an d'Datebank ze lueden war direkt CSV Dateien ze kopéieren mat SQL - Kommando COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTwou tablename - Dësch Numm an path - de Wee op d'Datei. Wärend der Aarbecht mam Archiv gouf entdeckt datt déi agebaute Implementatioun unzip am R funktionnéiert net korrekt mat enger Zuel vu Dateien aus dem Archiv, also hu mir de System benotzt unzip (benotzt de Parameter getOption("unzip")).

Funktioun fir d'Datebank ze schreiwen

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

Wann Dir den Dësch transforméiere musst ier Dir se an d'Datebank schreift, ass et genuch fir am Argument ze passéieren preprocess Funktioun déi d'Donnéeën transforméiert.

Code fir sequenziell Daten an d'Datebank ze lueden:

Schreiwen Daten an der Datebank

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

D'Datelaaschtzäit ka variéieren jee no der Geschwindegkeetscharakteristike vum benotzte Drive. An eisem Fall, liesen a schreiwen bannent enger SSD oder vun engem Flash Drive (Quelldatei) op ​​eng SSD (DB) dauert manner wéi 10 Minutten.

Et dauert e puer Sekonnen méi fir eng Kolonn mat engem ganztall Klassenlabel an enger Indexkolonn (ORDERED INDEX) mat Zeilnummeren, mat deenen d'Observatioune gesammelt ginn wann Dir Chargen erstellt:

Zousätzlech Kolonnen an Index erstellen

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

Fir de Problem vun der Schafung vun enger Batch op der Flucht ze léisen, brauche mir déi maximal Geschwindegkeet z'erreechen fir zoufälleg Reihen aus der Tabell ze extrahieren doodles. Fir dëst hu mir 3 Tricken benotzt. Déi éischt war d'Dimensioun vun der Aart ze reduzéieren déi d'Observatioun ID späichert. Am ursprénglechen Dateset ass den Typ néideg fir d'ID ze späicheren bigint, awer d'Zuel vun den Observatioune mécht et méiglech hir Identifizéierer, gläich wéi d'Ordinale Zuel, an den Typ ze passen int. D'Sich ass an dësem Fall vill méi séier. Den zweeten Trick war ze benotzen ORDERED INDEX - Mir sinn empiresch zu dëser Entscheedung komm, nodeems mir all verfügbar sinn Méiglechkeeten. Déi drëtt war parametriséiert Ufroen ze benotzen. D'Essenz vun der Method ass de Kommando eemol auszeféieren PREPARE mat der spéider Notzung vun engem preparéierten Ausdrock wann Dir eng Rëtsch Ufroe vum selwechten Typ erstellt, awer tatsächlech ass et e Virdeel am Verglach mat engem einfachen SELECT huet sech am Beräich vum statistesche Feeler erausgestallt.

De Prozess vun der Eroplueden vun Donnéeën verbraucht net méi wéi 450 MB RAM. Dat ass, déi beschriwwe Approche erlaabt Iech Datesätz ze réckelen, déi Zénger vu Gigabytes op bal all Budget Hardware weegt, och e puer Eenbordgeräter, wat zimmlech cool ass.

Alles wat bleift ass d'Geschwindegkeet ze moossen fir (zoufälleg) Daten z'erhalen an d'Skaléierung ze evaluéieren wann Dir Chargen vu verschiddene Gréissten probéieren:

Datebank Benchmark

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 Unerkennung: wéi Dir Frënn mat R, C++ an neurale Netzwierker mécht

2. Chargen virbereeden

De ganze Batch Virbereedungsprozess besteet aus de folgende Schrëtt:

  1. Parsing e puer JSONs mat Vektore vu Saiten mat Koordinaten vu Punkten.
  2. Zeechnen faarweg Linnen baséiert op de Koordinate vun Punkten op engem Bild vun der néideg Gréisst (zum Beispill, 256 × 256 oder 128 × 128).
  3. Konvertéieren déi resultéierend Biller an en Tensor.

Als Deel vun der Konkurrenz tëscht Python Kärelen gouf de Problem haaptsächlech geléist mat OpenCV. Ee vun den einfachsten an offensichtlechsten Analoga am R géif esou ausgesinn:

JSON zu Tensor Konversioun an R

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

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

Zeechnen gëtt mat Standard R Tools ausgefouert an an engem temporäre PNG gespäichert am RAM gespäichert (op Linux, temporär R Verzeichnisser sinn am Verzeichnis /tmp, am RAM montéiert). Dëse Fichier gëtt dann als dräi-zweedimensional Array gelies mat Zuelen rangéiert vun 0 bis 1. Dëst ass wichteg well e méi konventionnell BMP an e raw Array mat Hex Faarfcoden gelies gëtt.

Loosst eis d'Resultat testen:

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 Unerkennung: wéi Dir Frënn mat R, C++ an neurale Netzwierker mécht

De Batch selwer gëtt wéi follegt geformt:

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

Dës Ëmsetzung schéngt eis suboptimal ze sinn, well d'Bildung vu grousse Chargen eng onschëlleg laang Zäit dauert, a mir hu beschloss d'Erfahrung vun eise Kollegen ze profitéieren andeems mir eng mächteg Bibliothéik benotzen OpenCV. Zu där Zäit gouf et kee fäerdege Paket fir R (et gëtt elo keen), sou datt eng minimal Ëmsetzung vun der erfuerderter Funktionalitéit an C++ geschriwwe gouf mat Integratioun an R Code benotzt Rcpp.

Fir de Problem ze léisen, goufen déi folgend Packagen a Bibliothéike benotzt:

  1. OpenCV fir mat Biller ze schaffen an Zeilen ze zéien. Benotzt virinstalléiert Systembibliothéiken an Headerdateien, souwéi dynamesch Verknëppung.

  2. xtensor fir mat multidimensionalen Arrays an Tensoren ze schaffen. Mir hunn Headerdateien benotzt, déi am R Package mam selwechten Numm abegraff sinn. D'Bibliothéik erlaabt Iech mat multidimensionalen Arrays ze schaffen, souwuel an der Rei Major a Kolonn Major Uerdnung.

  3. ndjson fir JSON ze analyséieren. Dës Bibliothéik gëtt benotzt an xtensor automatesch wann et am Projet präsent ass.

  4. RcppThread fir Multi-threaded Veraarbechtung vun engem Vektor aus JSON ze organiséieren. Benotzt d'Headerdateien, déi vun dësem Package geliwwert ginn. Vun méi populär RcppParallel De Package huet ënner anerem en agebaute Loop-Ënnerbriechungsmechanismus.

Et ass derwäert ze wëssen datt xtensor huet sech als Gottesdéngscht erausgestallt: Zousätzlech zu der Tatsaach datt et extensiv Funktionalitéit an héich Leeschtung huet, hunn hir Entwéckler sech ganz reaktiounsfäeger gemaach an d'Froen prompt an am Detail beäntwert. Mat hirer Hëllef war et méiglech Transformatiounen vun OpenCV Matrizen an xtensor Tensoren ëmzesetzen, souwéi e Wee fir 3-zweedimensional Bildtensoren an e 4-zweedimensionalen Tensor vun der korrekter Dimensioun (de Batch selwer) ze kombinéieren.

Material fir Rcpp, xtensor a RcppThread ze léieren

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

Fir Dateien ze kompiléieren déi Systemdateien benotzen an dynamesche Verknëppung mat Bibliothéiken déi um System installéiert sinn, hu mir de Plugin Mechanismus benotzt deen am Package implementéiert ass Rcpp. Fir automatesch Weeër a Fändelen ze fannen, hu mir e populäre Linux Utility benotzt pkg-config.

Ëmsetzung vum Rcpp Plugin fir d'OpenCV Bibliothéik ze benotzen

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

Als Resultat vun der Operatioun vum Plugin ginn déi folgend Wäerter wärend dem Kompiléierungsprozess ersat:

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"

Den Implementatiounscode fir JSON ze analyséieren an e Batch ze generéieren fir d'Transmissioun op de Modell gëtt ënner dem Spoiler uginn. Als éischt, füügt e lokale Projetsverzeechnes fir no Headerdateien ze sichen (braucht fir ndjson):

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

Ëmsetzung vun JSON zu Tensor Konversioun an C ++

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

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

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

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

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

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

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

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

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

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

Dëse Code soll an der Datei gesat ginn src/cv_xt.cpp a kompiléiere mam Kommando Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); och néideg fir Aarbecht nlohmann/json.hpp aus repository. De Code ass an e puer Funktiounen opgedeelt:

  • to_xt - eng Schabloun Funktioun fir eng Bildmatrix ze transforméieren (cv::Mat) zu engem Tensor xt::xtensor;

  • parse_json - d'Funktioun parséiert e JSON String, extrahéiert d'Koordinate vu Punkten, packt se an e Vektor;

  • ocv_draw_lines - aus dem resultéierende Vektor vu Punkten zitt multifaarweg Linnen;

  • process - kombinéiert déi uewe genannte Funktiounen a füügt och d'Fäegkeet fir dat resultéierend Bild ze skaléieren;

  • cpp_process_json_str - wrapper iwwer d'Funktioun process, wat d'Resultat op en R-Objet exportéiert (multidimensional Array);

  • cpp_process_json_vector - wrapper iwwer d'Funktioun cpp_process_json_str, wat Iech erlaabt e Stringvektor am Multi-threaded Modus ze veraarbechten.

Fir multi-faarweg Linnen ze zéien, gouf den HSV Faarfmodell benotzt, gefollegt vun der Konversioun op RGB. Loosst eis d'Resultat testen:

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

Quick Draw Doodle Unerkennung: wéi Dir Frënn mat R, C++ an neurale Netzwierker mécht
Verglach vun der Vitesse vun Implementatiounen an R an 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 Unerkennung: wéi Dir Frënn mat R, C++ an neurale Netzwierker mécht

Wéi Dir gesitt, ass d'Geschwindegkeetserhéijung ganz bedeitend erausgestallt, an et ass net méiglech mam C ++ Code z'erreechen andeems de R Code paralleliséiert.

3. Iteratoren fir Chargen aus der Datebank ze entlaaschten

R huet e gutt-verdéngt Ruff fir Donnéeën ze Veraarbechtung, datt am RAM passt, iwwerdeems Python méi duerch iterative Daten Veraarbechtung charakteriséiert ass, erlaabt Iech einfach an natierlech aus-Kär Berechnungen ëmsetzen (Berechnungen mat externen Erënnerung). E klassescht a relevant Beispill fir eis am Kontext vum beschriwwenen Problem ass déif neural Netzwierker, déi vun der Gradient Ofstamungsmethod trainéiert ginn mat der Approximatioun vum Gradient op all Schrëtt mat engem klengen Deel vun Observatiounen, oder Mini-Batch.

Deep Learning Kaderen, déi am Python geschriwwe sinn, hunn speziell Klassen, déi Iteratoren op Basis vun Daten implementéieren: Dëscher, Biller an Ordner, binär Formater, etc.. Dir kënnt fäerdeg Optiounen benotzen oder Är eege fir spezifesch Aufgaben schreiwen. Am R kënne mir all d'Features vun der Python-Bibliothéik profitéieren keras mat senge verschiddene Backends de Package mam selwechten Numm benotzt, deen am Tour uewen um Package funktionnéiert retikuléieren. Déi lescht verdéngt en separaten laangen Artikel; et erlaabt Iech net nëmmen Python Code aus R ze lafen, awer erlaabt Iech och Objeten tëscht R a Python Sessiounen ze transferéieren, automatesch all déi néideg Typkonversiounen auszeféieren.

Mir hunn d'Noutwennegkeet entlooss fir all d'Donnéeën am RAM ze späicheren andeems Dir MonetDBLite benotzt, all "neural Netzwierk" Aarbecht gëtt vum ursprénglechen Code am Python gemaach, mir mussen just en Iterator iwwer d'Donnéeën schreiwen, well et ass näischt prett fir esou eng Situatioun an entweder R oder Python. Et gi wesentlech nëmmen zwou Ufuerderunge fir et: et muss Chargen an enger endloser Loop zréckginn a säin Zoustand tëscht Iteratiounen späicheren (déi lescht am R gëtt op déi einfachst Manéier mat Zoumaache implementéiert). Virdrun war et erfuerderlech R-Arrays explizit an numpy Arrays am Iterator ze konvertéieren, awer déi aktuell Versioun vum Package keras mécht et selwer.

Den Iterator fir Trainings- a Validatiounsdaten huet sech als folgend erausgestallt:

Iterator fir Training a Validatiounsdaten

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

D'Funktioun hëlt als Input eng Variabel mat enger Verbindung mat der Datebank, d'Zuel vun de benotzte Linnen, d'Zuel vun de Klassen, d'Batchgréisst, d'Skala (scale = 1 entsprécht der Rendering Biller vun 256x256 Pixel, scale = 0.5 - 128x128 Pixel), Faarfindikator (color = FALSE spezifizéiert Rendering a Grauskala wann se benotzt ginn color = TRUE all Strich gëtt an enger neier Faarf gezeechent) an e Virveraarbechtungsindikator fir Netzwierker, déi op Imagenet virgeschriwwe sinn. Déi lescht ass gebraucht fir Pixelwäerter vum Intervall [0, 1] bis zum Intervall [-1, 1] ze skaléieren, wat benotzt gouf beim Training vun der geliwwert keras Modeller.

Déi extern Funktioun enthält Argumenttyp Iwwerpréiwung, en Dësch data.table mat zoufälleg gemëscht Linn Zuelen aus samples_index a Batchnummeren, Konter a maximal Zuel vu Chargen, souwéi e SQL Ausdrock fir d'Daten aus der Datebank z'entluede. Zousätzlech hu mir e séieren Analog vun der Funktioun dobannen definéiert keras::to_categorical(). Mir hunn bal all d'Donnéeën fir Training benotzt, en halleft Prozent fir d'Validatioun verlooss, sou datt d'Epochgréisst vum Parameter limitéiert war steps_per_epoch wann genannt keras::fit_generator(), an d'Konditioun if (i > max_i) huet nëmme fir de Validatioun Iterator geschafft.

An der interner Funktioun ginn Zeilindexe fir déi nächst Batch zréckgezunn, Opzeechnunge ginn aus der Datebank ofgelueden mam Batchzähler erop, JSON Parsing (Funktioun cpp_process_json_vector(), geschriwwen an C++) a erstellen Arrays entspriechend Biller. Da ginn een-waarm Vektore mat Klasseetiketten erstallt, Arrays mat Pixelwäerter an Etiketten ginn an eng Lëscht kombinéiert, wat de Retourwäert ass. Fir d'Aarbecht ze beschleunegen, hu mir d'Schafung vun Indexen an Tabellen benotzt data.table an Ännerung iwwer de Link - ouni dëse Package "Chips" data.tabell Et ass zimmlech schwéier virzestellen effektiv mat all bedeitende Quantitéit un Daten am R ze schaffen.

D'Resultater vu Geschwindegkeetsmessungen op engem Core i5 Laptop sinn wéi follegt:

Iterator Benchmark

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 Unerkennung: wéi Dir Frënn mat R, C++ an neurale Netzwierker mécht

Wann Dir genuch RAM hutt, kënnt Dir d'Operatioun vun der Datebank eescht beschleunegen andeems Dir se op dee selwechte RAM transferéiert (32 GB ass genuch fir eis Aufgab). Am Linux ass d'Partition par défaut montéiert /dev/shm, besetzt bis zu der Halschent vun der RAM Kapazitéit. Dir kënnt méi Highlight andeems Dir ännert /etc/fstabe Rekord ze kréien wéi tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Vergewëssert Iech nei opzemaachen an d'Resultat ze kontrolléieren andeems Dir de Kommando ausféiert df -h.

Den Iterator fir Testdaten gesäit vill méi einfach aus, well d'Testdataset ganz an de RAM passt:

Iterator fir Testdaten

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. Auswiel vun Modell Architektur

Déi éischt Architektur benotzt gouf mobilnet v1, d'Features vun deenen diskutéiert ginn an dat Message. Et ass als Standard abegraff keras an deementspriechend ass et am Pak mam selwechten Numm fir R. Awer wann Dir probéiert et mat Single-Channel Biller ze benotzen, huet sech eng komesch Saach erausgestallt: den Input Tensor muss ëmmer d'Dimensioun hunn (batch, height, width, 3), dat ass, d'Zuel vun de Channels kann net geännert ginn. Et gëtt keng sou Limitatioun am Python, also hu mir eis gerannt an eis eegen Implementatioun vun dëser Architektur geschriwwen, no dem ursprénglechen Artikel (ouni den Ausfall deen an der keras Versioun ass):

Mobilenet v1 Architektur

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

D'Nodeeler vun dëser Approche sinn evident. Ech wëll vill Modeller testen, awer am Géigendeel, ech wëll net all Architektur manuell iwwerschreiwe. Mir waren och vun der Geleeënheet entzu kréien d'Gewiichter vun Modeller pre-trained op imagenet ze benotzen. Wéi gewinnt huet d'Dokumentatioun gehollef. Funktioun get_config() erlaabt Iech eng Beschreiwung vum Modell an enger Form ze kréien, déi gëeegent ass fir z'änneren (base_model_conf$layers - eng regulär R Lëscht), an d'Funktioun from_config() mécht déi ëmgedréint Konversioun op e Modellobjekt:

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)

Elo ass et net schwéier eng universell Funktioun ze schreiwen fir eng vun de geliwwert ze kréien keras Modeller mat oder ouni Gewiichter trainéiert op Imagenet:

Funktioun fir lued fäerdeg Architekturen

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

Wann Dir Single-Channel Biller benotzt, gi keng pretrained Gewiichter benotzt. Dëst kéint fixéiert ginn: mat der Funktioun get_weights() kritt d'Modellgewiichter a Form vun enger Lëscht vu R-Arrays, ännert d'Dimensioun vum éischten Element vun dëser Lëscht (duerch e Faarfkanal ze huelen oder all dräi duerchschnëttlech), a lued dann d'Gewichte zréck an de Modell mat der Funktioun set_weights(). Mir hunn dës Funktionalitéit ni bäigefüügt, well op dëser Etapp war et scho kloer datt et méi produktiv wier mat Faarfbiller ze schaffen.

Mir hunn déi meescht vun den Experimenter mat Mobilenet Versiounen 1 an 2 duerchgefouert, souwéi resnet34. Méi modern Architekturen wéi SE-ResNeXt hunn an dësem Concours gutt geschafft. Leider hu mir keng fäerdeg Implementatiounen zur Verfügung gestallt, a mir hunn eis net selwer geschriwwen (mee mir wäerten definitiv schreiwen).

5. Parameteriséierung vun Scripten

Fir Kamoudheet, all Code fir Start Training war als eenzege Schrëft entworf, parameterized benotzt docopt wéi folgend:

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)

Package docopt representéiert d'Ëmsetzung http://docopt.org/ fir R. Mat senger Hëllef ginn Scripte mat einfache Kommandoen lancéiert wéi Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db oder ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, wann Datei train_nn.R ass ausführbar (dëse Kommando fänkt un de Modell ze trainéieren resnet50 op dräi-faarweg Biller Mooss 128x128 Pixel, der Datebank muss am Dossier läit /home/andrey/doodle_db). Dir kënnt Léiergeschwindegkeet, Optimizertyp an all aner personaliséierbar Parameteren op d'Lëscht addéieren. Am Prozess vun der Virbereedung vun der Publikatioun huet sech erausgestallt, datt d'Architektur mobilenet_v2 vun der aktueller Versioun keras an R benotzen net kann wéinst Ännerungen déi net am R Package berücksichtegt ginn, waarden mir op si fir et ze fixéieren.

Dës Approche huet et méiglech gemaach Experimenter mat verschiddene Modeller wesentlech ze beschleunegen am Verglach zum méi traditionelle Start vu Scripten am RStudio (mir bemierken de Package als eng méiglech Alternativ tfruns). Awer den Haaptvirdeel ass d'Fäegkeet fir de Start vu Skripten am Docker oder einfach um Server einfach ze managen, ouni RStudio fir dëst z'installéieren.

6. Dockerization vun Scripten

Mir hunn Docker benotzt fir Portabilitéit vun der Ëmwelt ze garantéieren fir Trainingsmodeller tëscht Teammemberen a fir séier Deployment an der Wollek. Dir kënnt mat dësem Outil Gewunnecht ufänken, déi fir e R Programméierer relativ ongewéinlech ass, mat dat Serie vu Publikatiounen oder Video Course.

Docker erlaabt Iech souwuel Är eege Biller vun Null ze kreéieren an aner Biller als Basis ze benotzen fir Äert eegent ze kreéieren. Wann Dir déi verfügbar Optiounen analyséiert, si mir zur Conclusioun komm datt d'Installatioun vun NVIDIA, CUDA + cuDNN Treiber a Python Bibliothéiken e relativ voluminösen Deel vum Bild ass, a mir hunn decidéiert den offiziellen Bild als Basis ze huelen tensorflow/tensorflow:1.12.0-gpu, dobäi déi néideg R Packagen do.

Déi lescht Docker Datei huet esou ausgesinn:

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

Fir d'Bequemlechkeet goufen déi benotzte Packagen a Variabelen gesat; de gréissten Deel vun de schrëftleche Skripte ginn an de Container während der Montage kopéiert. Mir hunn och d'Kommando Shell geännert op /bin/bash fir einfacher Benotzung vum Inhalt /etc/os-release. Dëst huet de Besoin vermeit fir d'OS Versioun am Code ze spezifizéieren.

Zousätzlech gouf e klenge Bash-Skript geschriwwen, deen Iech erlaabt e Container mat verschiddene Kommandoen ze starten. Zum Beispill kënnen dës Skripte sinn fir neural Netzwierker ze trainéieren, déi virdru am Container plazéiert goufen, oder eng Kommandoshell fir d'Debugging an d'Iwwerwaachung vun der Operatioun vum Container:

Skript fir de Container ze starten

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

Wann dëst Bash Skript ouni Parameteren leeft, gëtt de Skript am Container genannt train_nn.R mat Standardwäerter; wann déi éischt positional Argument "bash" ass, da fänkt de Container interaktiv mat enger Kommando Shell. An all anere Fäll ginn d'Wäerter vun de positionalen Argumenter ersat: CMD="Rscript /app/train_nn.R $@".

Et ass derwäert ze bemierken datt d'Verzeichnisser mat Quelldaten an Datebank, wéi och den Verzeechnes fir trainéiert Modeller ze späicheren, am Container vum Hostsystem montéiert sinn, wat Iech erlaabt d'Resultater vun de Skripte ouni onnéideg Manipulatiounen ze kréien.

7. Benotzt MÉI GPUs op Google Cloud

Ee vun de Fonctiounen vun der Konkurrenz waren déi ganz laut Daten (kuckt den Titelbild, geléint vum @Leigh.plt vun ODS Slack). Grouss Chargen hëllefen dëst ze bekämpfen, an no Experimenter op engem PC mat 1 GPU hu mir décidéiert Trainingsmodeller op verschidde GPUs an der Wollek ze beherrschen. Benotzt GoogleCloud (gudde Guide fir d'Grondlage) wéinst der grousser Auswiel u verfügbare Konfiguratiounen, raisonnabel Präisser an $ 300 Bonus. Aus Gier hunn ech eng 4xV100 Instanz mat enger SSD an enger Tonn RAM bestallt, an dat war e grousse Feeler. Sou eng Maschinn ësst séier Suen; Dir kënnt ouni eng bewährte Pipeline briechen experimentéieren. Fir pädagogesch Zwecker ass et besser de K80 ze huelen. Awer déi grouss Quantitéit vum RAM ass nëtzlech - d'Cloud SSD huet net beandrockt mat senger Leeschtung, sou datt d'Datebank op transferéiert gouf dev/shm.

Vu gréissten Interessi ass de Codefragment verantwortlech fir verschidde GPUs ze benotzen. Als éischt gëtt de Modell op der CPU erstallt mat engem Kontextmanager, sou wéi am Python:

with(tensorflow::tf$device("/cpu:0"), {
  model_cpu <- get_model(
    name = model_name,
    input_shape = input_shape,
    weights = weights,
    metrics =(top_3_categorical_accuracy,
    compile = FALSE
  )
})

Dann gëtt den onkompiléierten (dëst ass wichteg) Modell op eng bestëmmte Zuel vu verfügbare GPUs kopéiert, an eréischt duerno gëtt et kompiléiert:

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

Déi klassesch Technik fir all Schichten ausser déi lescht ze afréieren, déi lescht Schicht ze trainéieren, de ganze Modell fir e puer GPUs z'entfréieren an ze retrainéieren konnt net ëmgesat ginn.

Training gouf ouni Gebrauch iwwerwaacht. tensorboard, limitéiert eis fir Logbicher opzehuelen a Modeller mat informativen Nimm no all Epoch ze späicheren:

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. Amplaz vun enger Conclusioun

Eng Zuel vu Problemer, déi mir begéint hunn, sinn nach net iwwerwonne ginn:

  • в keras et gëtt keng fäerdeg Funktioun fir automatesch no der optimaler Léierrate ze sichen (analog lr_finder an der Bibliothéik séier.ai); Mat e puer Effort ass et méiglech Drëtt Partei Implementatiounen op R ze portéieren, zum Beispill, dat;
  • als Konsequenz vum virege Punkt war et net méiglech déi richteg Trainingsgeschwindegkeet ze wielen wann Dir verschidde GPUs benotzt;
  • et gëtt e Manktem u modernen neuralen Netzwierkarchitekturen, besonnesch déi op Imagenet viraus trainéiert;
  • keen Zyklus Politik an diskriminativ Léieren Tariffer (Cosinus annealing war op eis Ufro ëmgesat, Merci schei).

Wat nëtzlech Saachen goufen aus dësem Concours geléiert:

  • Op relativ niddereg-Muecht Hardware, kënnt Dir mat uerdentlech (vill mol der Gréisst vun RAM) Volumen vun Daten ouni Péng Aarbecht. Plastikstut data.tabell spuert Erënnerung wéinst der Plazverännerung vun den Dëscher, wat vermeit se ze kopéieren, a wa se richteg benotzt ginn, weisen seng Fäegkeeten bal ëmmer déi héchst Geschwindegkeet ënner allen Tools, déi eis bekannt sinn fir Skriptsproochen. D'Späichere vun Daten an enger Datebank erlaabt Iech a ville Fäll guer net iwwer d'Noutwennegkeet ze denken, de ganzen Dataset an de RAM ze pressen.
  • Luesen Funktiounen am R kënne mat schnellen am C ++ ersat ginn andeems Dir de Package benotzt Rcpp. Wann zousätzlech ze benotzen RcppThread oder RcppParallel, mir kréien Cross-Plattform Multi-threaded Implementatiounen, sou datt et net néideg ass de Code um R-Niveau ze paralleliséieren.
  • Package Rcpp kann ouni sérieux Wësse vu C ++ benotzt ginn, ass den erfuerderleche Minimum duergestallt hei. Header Dateien fir eng Rei cool C-Bibliothéiken wéi xtensor verfügbar op CRAN, dat heescht, eng Infrastruktur gëtt geformt fir d'Ëmsetzung vu Projeten déi fäerdeg gemaachte High-Performance C++ Code an R integréieren. Zousätzlech Komfort ass Syntax Highlight an e statesche C++ Code Analyser am RStudio.
  • docopt erlaabt Iech selbstänneg Scripte mat Parameteren ze lafen. Dëst ass bequem fir op engem Fernserver ze benotzen, inkl. ënner docker. Am RStudio ass et onbequem fir vill Stonnen Experimenter mat der Ausbildung vun neurale Netzwierker ze maachen, an d'Installatioun vun der IDE um Server selwer ass net ëmmer gerechtfäerdegt.
  • Docker garantéiert d'Codeportabilitéit an d'Reproduzibilitéit vu Resultater tëscht Entwéckler mat verschiddene Versioune vum OS a Bibliothéiken, souwéi d'Liichtegkeet vun der Ausféierung op Serveren. Dir kënnt déi ganz Trainingspipeline mat just engem Kommando starten.
  • Google Cloud ass e budgetfrëndleche Wee fir op deier Hardware ze experimentéieren, awer Dir musst Konfiguratiounen suergfälteg wielen.
  • D'Messung vun der Geschwindegkeet vun eenzelne Codefragmenter ass ganz nëtzlech, besonnesch wann Dir R an C++ kombinéiert, a mam Package ausernee geholl - och ganz einfach.

Insgesamt war dës Erfahrung ganz belountend a mir schaffen weider fir e puer vun de Problemer ze léisen.

Source: will.com

Setzt e Commentaire