Quick Draw Doodle Recognition: hoe freonen te meitsjen mei R, C ++ en neurale netwurken

Quick Draw Doodle Recognition: hoe freonen te meitsjen mei R, C ++ en neurale netwurken

Hoi Habr!

Ferline hjerst organisearre Kaggle in kompetysje om mei de hân tekene foto's te klassifisearjen, Quick Draw Doodle Recognition, wêryn ûnder oaren in team fan R-wittenskippers meidie: Artem Klevtsova, Behearder Philippa и Andrey Ogurtsov. Wy sille de konkurrinsje net yn detail beskriuwe; dat is al dien yn resinte publikaasje.

Dit kear slagge it net mei medaljebuorkerij, mar der is in protte weardefolle ûnderfining opdien, dat ik wol graach fertelle de mienskip oer in oantal nijsgjirrige en brûkbere dingen op Kagle en yn it deistich wurk. Under de ûnderwerpen besprutsen: dreech libben sûnder OpenCV, JSON-parsing (dizze foarbylden ûndersykje de yntegraasje fan C++-koade yn skripts of pakketten yn R mei Rcpp), parameterisaasje fan skripts en dockerisaasje fan 'e definitive oplossing. Alle koade út it berjocht yn in foarm geskikt foar útfiering is beskikber yn repositories.

Ynhâld:

  1. Laad gegevens effisjint fan CSV yn MonetDB
  2. It tarieden fan batches
  3. Iterators foar it lossen fan batches út de databank
  4. Selektearje in model arsjitektuer
  5. Skriptparameterisaasje
  6. Dockerisaasje fan skripts
  7. It brûken fan meardere GPU's op Google Cloud
  8. Yn stee fan in konklúzje

1. Laad gegevens effisjint fan CSV yn 'e MonetDB-database

De gegevens yn dizze kompetysje wurde net levere yn 'e foarm fan klearmakke ôfbyldings, mar yn' e foarm fan 340 CSV-bestannen (ien triem foar elke klasse) mei JSON's mei puntkoordinaten. Troch dizze punten te ferbinen mei rigels, krije wy in definitive ôfbylding fan 256x256 piksels. Ek foar elk record is d'r in kaartsje dat oanjout oft de foto goed waard erkend troch de klassifikaasje dy't brûkt waard op it momint dat de dataset waard sammele, in twa-letter koade fan it lân fan ferbliuw fan de skriuwer fan de foto, in unike identifier, in tiidstempel en in klasse namme dy't oerienkomt mei de triemnamme. In ferienfâldige ferzje fan 'e orizjinele gegevens waacht 7.4 GB yn it argyf en sawat 20 GB nei it útpakken, de folsleine gegevens nei it útpakken nimt 240 GB. De organisatoaren soarge derfoar dat beide ferzjes deselde tekeningen reprodusearren, wat betsjuttet dat de folsleine ferzje oerstallich wie. Yn alle gefallen waard it opslaan fan 50 miljoen ôfbyldings yn grafyske bestannen as yn 'e foarm fan arrays fuortendaliks beskôge as net rendabel, en wy besletten om alle CSV-bestannen út it argyf te fusearjen train_simplified.zip yn 'e database mei folgjende generaasje fan ôfbyldings fan' e fereaske grutte "on the fly" foar elke batch.

In goed bewezen systeem waard keazen as de DBMS MonetDB, nammentlik in ymplemintaasje foar R as in pakket MonetDBLite. It pakket befettet in ynbêde ferzje fan 'e databanktsjinner en kinne jo de tsjinner direkt fan in R-sesje ophelje en dêr mei wurkje. It oanmeitsjen fan in databank en it ferbinen mei it wurde útfierd mei ien kommando:

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

Wy sille twa tabellen moatte oanmeitsje: ien foar alle gegevens, de oare foar tsjinstynformaasje oer ynladen bestannen (nuttich as der wat mis giet en it proses moat wurde hervat nei it ynladen fan ferskate bestannen):

It meitsjen fan tabellen

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 fluchste manier om gegevens yn 'e databank te laden wie CSV-bestannen direkt te kopiearjen mei SQL - kommando COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTwêr tablename - tabel namme en path - it paad nei it bestân. By it wurkjen mei it argyf waard ûntdutsen dat de ynboude ymplemintaasje unzip yn R wurket net goed mei in oantal triemmen út it argyf, dus wy hawwe it systeem brûkt unzip (mei de parameter getOption("unzip")).

Funksje foar it skriuwen nei de databank

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

As jo ​​​​de tabel moatte transformearje foardat jo it skriuwe nei de databank, is it genôch om it argumint troch te jaan preprocess funksje dy't de gegevens sil transformearje.

Koade foar it opfolgjend laden fan gegevens yn 'e databank:

It skriuwen fan gegevens nei de databank

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

Gegevens laden tiid kin fariearje ôfhinklik fan de snelheid skaaimerken fan it stasjon brûkt. Yn ús gefal duorret it lêzen en skriuwen binnen ien SSD of fan in flash-drive (boarnebestân) nei in SSD (DB) minder dan 10 minuten.

It duorret noch in pear sekonden om in kolom te meitsjen mei in hiel getal klasselabel en in yndekskolom (ORDERED INDEX) mei rigelnûmers wêrmei't observaasjes sille wurde sampled by it meitsjen fan batches:

Oanfoljende kolommen en yndeks oanmeitsje

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

Om it probleem op te lossen fan it meitsjen fan in batch op 'e flecht, moasten wy de maksimale snelheid berikke fan it ekstrahearjen fan willekeurige rigen út' e tabel doodles. Hjirfoar hawwe wy 3 trúkjes brûkt. De earste wie om de dimensjes te ferminderjen fan it type dat de observaasje-ID opslacht. Yn 'e orizjinele dataset is it type dat nedich is om de ID te bewarjen bigint, mar it oantal observaasjes makket it mooglik om har identifisearders te passen, gelyk oan it ordinale nûmer, yn it type int. It sykjen is yn dit gefal folle flugger. De twadde trúk wie te brûken ORDERED INDEX - wy kamen ta dit beslút empirysk, hawwen trochgien alle beskikbere opsjes. De tredde wie om parameterisearre queries te brûken. De essinsje fan 'e metoade is om it kommando ien kear út te fieren PREPARE mei dêropfolgjende gebrûk fan in taret útdrukking by it meitsjen fan in bondel fragen fan itselde type, mar yn feite is d'r in foardiel yn ferliking mei in ienfâldige SELECT bliken te wêzen binnen it berik fan statistyske flater.

It proses fan it uploaden fan gegevens ferbrûkt net mear as 450 MB RAM. Dat is, de beskreaune oanpak lit jo datasets ferpleatse dy't tsientallen gigabytes weagje op hast alle budzjet-hardware, ynklusyf guon single-board-apparaten, wat frij cool is.

Alles wat oerbliuwt is de snelheid fan it opheljen fan (willekeurige) gegevens te mjitten en de skaalfergrutting te evaluearjen by sampling fan batches fan ferskate grutte:

Databank 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 Recognition: hoe freonen te meitsjen mei R, C ++ en neurale netwurken

2. It tarieden fan batches

De hiele batch tarieding proses bestiet út de folgjende stappen:

  1. It parsearjen fan ferskate JSON's dy't vectoren fan stringen befetsje mei koördinaten fan punten.
  2. Drawing kleurde linen basearre op de koördinaten fan punten op in ôfbylding fan de fereaske grutte (Bygelyks, 256 × 256 of 128 × 128).
  3. Konvertearje de resultearjende ôfbyldings yn in tensor.

As ûnderdiel fan 'e konkurrinsje ûnder Python-kernels waard it probleem primêr oplost mei help fan OpenCV. Ien fan 'e ienfâldichste en meast foar de hân lizzende analogen yn R soe der sa útsjen:

JSON nei Tensor-konverzje ymplementearje yn 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)
}

Tekening wurdt útfierd mei standert R-ark en bewarre yn in tydlike PNG opslein yn RAM (op Linux binne tydlike R-mappen yn 'e map /tmp, monteard yn RAM). Dizze triem wurdt dan lêzen as in trijediminsjonale array mei nûmers fariearjend fan 0 oan 1. Dit is wichtich omdat in mear konvinsjonele BMP soe wurde lêzen yn in rau array mei hex kleur koades.

Litte wy it resultaat 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 Recognition: hoe freonen te meitsjen mei R, C ++ en neurale netwurken

De batch sels sil wurde foarme as folget:

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

Dizze ymplemintaasje like ús suboptimaal, om't de foarming fan grutte batches in ûnfatsoenlik lange tiid duorret, en wy besletten om te profitearjen fan 'e ûnderfining fan ús kollega's troch in krêftige bibleteek te brûken OpenCV. Op dat stuit wie d'r gjin ready-made pakket foar R (d'r is no gjinien), dus in minimale ymplemintaasje fan 'e fereaske funksjonaliteit waard skreaun yn C ++ mei yntegraasje yn R-koade mei Rcpp.

Om it probleem op te lossen waarden de folgjende pakketten en bibleteken brûkt:

  1. OpenCV foar it wurkjen mei bylden en it tekenjen fan linen. Brûkte foarôf ynstalleare systeembiblioteken en koptekstbestannen, lykas dynamyske keppeling.

  2. xtensor foar wurkjen mei multydimensionale arrays en tensors. Wy brûkten koptekstbestannen opnommen yn it R-pakket mei deselde namme. De bibleteek kinne jo wurkje mei multydimensionale arrays, sawol yn rige grutte en kolom grutte folchoarder.

  3. ndjson foar it parsearjen fan JSON. Dizze bibleteek wurdt brûkt yn xtensor automatysk as it oanwêzich is yn it projekt.

  4. RcppThread foar it organisearjen fan multi-threaded ferwurking fan in fektor út JSON. De koptekstbestannen brûkt troch dit pakket. Fan populêrder RcppParallel It pakket hat ûnder oare in ynboude loop-ûnderbrekkingsmeganisme.

It is it wurdich te wizen dat xtensor die bliken in goadsend te wêzen: neist it feit dat it wiidweidige funksjonaliteit en hege prestaasjes hat, bliken har ûntwikkelders frij responsyf te wêzen en fragen prompt en yn detail beantwurde. Mei har help wie it mooglik om transformaasjes fan OpenCV-matriksen yn xtensor-tensors út te fieren, lykas ek in manier om 3-diminsjonale byldtensoren te kombinearjen yn in 4-diminsjonale tensor fan 'e juste dimensje (de batch sels).

Materiaal foar it learen fan Rcpp, xtensor en 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

Om bestannen te kompilearjen dy't systeembestannen brûke en dynamyske keppeling mei biblioteken ynstalleare op it systeem, brûkten wy it plugin-meganisme ymplementearre yn it pakket Rcpp. Om automatysk paden en flaggen te finen, hawwe wy in populêr Linux-hulpprogramma brûkt pkg-config.

Implementaasje fan it Rcpp-plugin foar it brûken fan de OpenCV-bibleteek

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

As gefolch fan 'e operaasje fan' e plugin sille de folgjende wearden wurde ferfongen tidens it kompilaasjeproses:

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"

De ymplemintaasjekoade foar it parsearjen fan JSON en it generearjen fan in batch foar oerdracht nei it model wurdt jûn ûnder de spoiler. Foegje earst in lokale projektmap ta om te sykjen nei koptekstbestannen (nedich foar ndjson):

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

Ymplemintaasje fan JSON nei tensorkonverzje yn 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;
}

Dizze koade moat yn it bestân pleatst wurde src/cv_xt.cpp en kompilearje mei it kommando Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); ek nedich foar wurk nlohmann/json.hpp fan repository. De koade is ferdield yn ferskate funksjes:

  • to_xt - in sjabloanfunksje foar it transformearjen fan in ôfbyldingsmatrix (cv::Mat) nei in tensor xt::xtensor;

  • parse_json - de funksje parseart in JSON-string, ekstrakt de koördinaten fan punten, pakke se yn in fektor;

  • ocv_draw_lines - út de resultearjende fektor fan punten, lûkt mearkleurige linen;

  • process - kombinearret de boppesteande funksjes en foeget ek de mooglikheid ta om de resultearjende ôfbylding te skaaljen;

  • cpp_process_json_str - wrapper oer de funksje process, dy't it resultaat eksportearret nei in R-objekt (multdimensional array);

  • cpp_process_json_vector - wrapper oer de funksje cpp_process_json_str, wêrmei jo in stringvektor kinne ferwurkje yn multi-threaded modus.

Om mearkleurige linen te tekenjen, waard it HSV-kleurmodel brûkt, folge troch konverzje nei RGB. Litte wy it resultaat testen:

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

Quick Draw Doodle Recognition: hoe freonen te meitsjen mei R, C ++ en neurale netwurken
Fergeliking fan de snelheid fan ymplemintaasjes yn R en C ++

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

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

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

res_bench[, cols]

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

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

Quick Draw Doodle Recognition: hoe freonen te meitsjen mei R, C ++ en neurale netwurken

Sa't jo sjen kinne, de snelheid ferheging die bliken hiel wichtich, en it is net mooglik om te fangen C ++ koade troch parallelizing R koade.

3. Iterators foar it lossen fan batches út de databank

R hat in goed fertsjinne reputaasje foar it ferwurkjen fan gegevens dy't past yn RAM, wylst Python wurdt mear karakterisearre troch iterative gegevens ferwurking, sadat jo maklik en natuerlik útfiere out-of-core berekkeningen (berekkeningen mei help fan eksterne ûnthâld). In klassyk en relevant foarbyld foar ús yn 'e kontekst fan' e beskreaune probleem is djippe neurale netwurken oplaat troch de gradient-ôfstammingsmetoade mei approximaasje fan 'e gradient by elke stap mei in lyts part fan observaasjes, as mini-batch.

Djippe learramten skreaun yn Python hawwe spesjale klassen dy't iterators ymplementearje op basis fan gegevens: tabellen, ôfbyldings yn mappen, binêre formaten, ensfh Jo kinne gebrûk meitsje fan klearmakke opsjes of skriuwe jo eigen foar spesifike taken. Yn R kinne wy ​​profitearje fan alle funksjes fan 'e Python-bibleteek keras mei syn ferskate backends mei it pakket mei deselde namme, dat op syn beurt boppe op it pakket wurket retikulearje. Dat lêste fertsjinnet in apart lang artikel; it lit jo net allinich Python-koade útfiere fan R, mar kinne jo ek objekten oerdrage tusken R- en Python-sesjes, automatysk alle nedige typekonversaasjes útfiere.

Wy hawwe de needsaak om alle gegevens yn RAM op te slaan kwytrekke troch MonetDBLite te brûken, al it "neurale netwurk" wurk sil wurde útfierd troch de orizjinele koade yn Python, wy moatte gewoan in iterator oer de gegevens skriuwe, om't d'r neat klear is foar sa'n situaasje yn beide R of Python. D'r binne yn essinsje mar twa easken foar it: it moat batches yn in einleaze lus werombringe en syn steat bewarje tusken iteraasjes (de lêste yn R wurdt op 'e ienfâldichste manier ymplementearre mei slutingen). Earder wie it ferplicht om R-arrays eksplisyt te konvertearjen yn numpy-arrays binnen de iterator, mar de hjoeddeistige ferzje fan it pakket keras docht it sels.

De iterator foar training- en validaasjegegevens die bliken as folgjend te wêzen:

Iterator foar training- en validaasjegegevens

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

De funksje nimt as ynfier in fariabele mei in ferbining mei de databank, it oantal brûkte rigels, it oantal klassen, batchgrutte, skaal (scale = 1 komt oerien mei it werjaan fan ôfbyldings fan 256x256 piksels, scale = 0.5 - 128x128 piksels), kleurindikator (color = FALSE spesifisearret rendering yn griisskalen as brûkt color = TRUE elke streek wurdt tekene yn in nije kleur) en in foarferwurkingsindikator foar netwurken foarôf oplaat op imagenet. Dat lêste is nedich om pikselwearden te skaaljen fan it ynterval [0, 1] nei it ynterval [-1, 1], dat waard brûkt by it trainen fan de levere keras modellen.

De eksterne funksje befettet argumint type kontrôle, in tabel data.table mei willekeurich mingde line nûmers fan samples_index en batch nûmers, teller en maksimum oantal batches, en ek in SQL-ekspresje foar it lossen fan gegevens út de databank. Derneist definieare wy in rappe analoog fan 'e funksje binnen keras::to_categorical(). Wy brûkten hast alle gegevens foar training, litte in heal persint foar falidaasje, sadat de epochgrutte beheind waard troch de parameter steps_per_epoch wannear neamd keras::fit_generator(), en de betingst if (i > max_i) wurke allinnich foar de validaasje-iterator.

Yn 'e ynterne funksje wurde rige-yndeksen ophelle foar de folgjende batch, records wurde ûntladen út' e database mei de batchteller tanimmend, JSON-parsing (funksje cpp_process_json_vector(), skreaun yn C++) en meitsje arrays dy't oerienkomme mei ôfbyldings. Dan wurde ien-hot-fektors mei klasse-labels makke, arrays mei pikselwearden en labels wurde kombinearre yn in list, dat is de weromwearde. Om it wurk te fersnellen, brûkten wy it oanmeitsjen fan yndeksen yn tabellen data.table en wiziging fia de keppeling - sûnder dizze pakket "chips" data.table It is frij lestich foar te stellen om effektyf te wurkjen mei elke signifikante hoemannichte gegevens yn R.

De resultaten fan snelheidsmjittingen op in Core i5 laptop binne as folgjend:

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 Recognition: hoe freonen te meitsjen mei R, C ++ en neurale netwurken

As jo ​​in foldwaande hoemannichte RAM hawwe, kinne jo de wurking fan 'e databank serieus fersnelle troch it oer te setten nei deselde RAM (32 GB is genôch foar ús taak). Yn Linux is de partysje standert monteare /dev/shm, besette oant de helte fan 'e RAM-kapasiteit. Jo kinne mear markearje troch te bewurkjen /etc/fstabom in rekord like te krijen tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Wês wis dat jo opnij starte en it resultaat kontrolearje troch it kommando út te fieren df -h.

De iterator foar testgegevens liket folle ienfâldiger, om't de testdataset folslein yn RAM past:

Iterator foar testgegevens

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. Seleksje fan model arsjitektuer

De earste arsjitektuer brûkt wie mobilenet v1, wêrfan de funksjes wurde besprutsen yn dit berjocht. It is opnommen as standert keras en is dus beskikber yn it pakket mei deselde namme foar R. Mar as jo besykje it te brûken mei ienkanaalsôfbyldings, die bliken in frjemd ding: de ynfiertensor moat altyd de diminsje hawwe (batch, height, width, 3), dat is, it oantal kanalen kin net feroare wurde. D'r is gjin sa'n beheining yn Python, dus wy haasten en skreau ús eigen ymplemintaasje fan dizze arsjitektuer, nei it orizjinele artikel (sûnder de dropout dy't yn 'e keras-ferzje is):

Mobilenet v1-arsjitektuer

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

De neidielen fan dizze oanpak binne dúdlik. Ik wol in protte modellen testen, mar krekt oarsom, ik wol net elke arsjitektuer mei de hân oerskriuwe. Wy waarden ek ûntnommen fan de kâns om te brûken de gewichten fan modellen pre-trained op imagenet. Lykas gewoanlik holp it bestudearjen fan de dokumintaasje. Funksje get_config() kinne jo in beskriuwing fan it model krije yn in foarm geskikt foar bewurking (base_model_conf$layers - in reguliere R-list), en de funksje from_config() fiert de omkearde konverzje út nei in modelobjekt:

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)

No is it net dreech om in universele funksje te skriuwen om ien fan 'e levere te krijen keras modellen mei of sûnder gewichten trained op imagenet:

Funksje foar it laden fan klearmakke arsjitektueren

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

By it brûken fan ienkanaalôfbyldings wurde gjin foartrainde gewichten brûkt. Dit koe wurde reparearre: mei help fan de funksje get_weights() krije de modelgewichten yn 'e foarm fan in list mei R-arrays, feroarje de dimensje fan it earste elemint fan dizze list (troch ien kleurkanaal te nimmen of alle trije te gemiddelden), en laden dan de gewichten werom yn it model mei de funksje set_weights(). Wy hawwe dizze funksjonaliteit nea tafoege, om't op dit stadium al dúdlik wie dat it produktiver wie om mei kleurfoto's te wurkjen.

Wy hawwe de measte eksperiminten útfierd mei mobilenetferzjes 1 en 2, lykas resnet34. Mear moderne arsjitektueren lykas SE-ResNeXt prestearren goed yn dizze kompetysje. Spitigernôch hiene wy ​​gjin klearebare ymplemintaasjes ta ús foldwaan, en wy hawwe net skreaun ús eigen (mar wy sille grif skriuwe).

5. Parameterisaasje fan skripts

Foar gemak, alle koade foar it begjinnen fan training waard ûntwurpen as ien skript, parameterized mei help docopt as folget:

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)

Pakket docopt fertsjintwurdiget de útfiering http://docopt.org/ foar R. Mei har help wurde skripts lansearre mei ienfâldige kommando's lykas Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db of ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, as triem train_nn.R is útfierber (dit kommando sil begjinne mei training fan it model resnet50 op trije-kleuren ôfbyldings mjitten 128x128 piksels, de databank moat lizze yn de map /home/andrey/doodle_db). Jo kinne learsnelheid, optimizertype en alle oare oanpasbere parameters tafoegje oan 'e list. By it tarieden fan de publikaasje die bliken dat de arsjitektuer mobilenet_v2 út de hjoeddeiske ferzje keras yn R gebrûk kin net fanwege feroarings dy't net yn rekken brocht binne yn it R-pakket, wachtsje wy op dat se it reparearje.

Dizze oanpak makke it mooglik om eksperiminten mei ferskate modellen signifikant te fersnellen yn ferliking mei de mear tradisjonele lansearring fan skripts yn RStudio (wy notearje it pakket as in mooglik alternatyf tfruns). Mar it wichtichste foardiel is de mooglikheid om de lansearring fan skripts yn Docker of gewoan op 'e server maklik te behearjen, sûnder RStudio hjirfoar te ynstallearjen.

6. Dockerisaasje fan skripts

Wy brûkten Docker om portabiliteit fan 'e omjouwing te garandearjen foar trainingsmodellen tusken teamleden en foar rappe ynset yn' e wolk. Jo kinne begjinne yn 'e kunde te kommen mei dit ark, dat is relatyf ûngewoan foar in R programmeur, mei dit rige fan publikaasjes of video kursus.

Docker kinne jo sawol jo eigen ôfbyldings fanôf it begjin meitsje en oare ôfbyldings brûke as basis foar it meitsjen fan jo eigen. By it analysearjen fan de beskikbere opsjes, kamen wy ta de konklúzje dat it ynstallearjen fan NVIDIA, CUDA + cuDNN-bestjoerders en Python-biblioteken in frijwat voluminous diel fan 'e ôfbylding is, en wy besletten de offisjele ôfbylding as basis te nimmen tensorflow/tensorflow:1.12.0-gpu, it tafoegjen fan de nedige R-pakketten dêr.

It lêste docker-bestân seach der sa út:

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

Foar it gemak waarden de brûkte pakketten yn fariabelen set; it grutste part fan 'e skreaune skripts wurdt kopiearre binnen de konteners by gearkomste. Wy hawwe ek de kommando-shell feroare nei /bin/bash foar gemak fan gebrûk fan ynhâld /etc/os-release. Dit foarkaam de needsaak om de OS-ferzje yn 'e koade op te jaan.

Derneist waard in lyts bash-skript skreaun wêrmei jo in kontener kinne starte mei ferskate kommando's. Dit kinne bygelyks skripts wêze foar training fan neurale netwurken dy't earder yn 'e kontener waarden pleatst, as in kommando-shell foar it debuggen en kontrolearjen fan' e wurking fan 'e kontener:

Skript om de kontener te 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}

As dit bash-skript sûnder parameters útfierd wurdt, sil it skript binnen de kontener wurde neamd train_nn.R mei standertwearden; as it earste posysjonele argumint "bash" is, dan sil de kontener ynteraktyf begjinne mei in kommando-shell. Yn alle oare gefallen wurde de wearden fan posisjonele arguminten ferfongen: CMD="Rscript /app/train_nn.R $@".

It is de muoite wurdich op te merken dat de mappen mei boarnegegevens en databank, lykas de map foar it bewarjen fan trained modellen, binne yn 'e kontener fan it hostsysteem monteare, wêrtroch jo tagong krije ta de resultaten fan' e skripts sûnder ûnnedige manipulaasjes.

7. Mei help fan meardere GPUs op Google Cloud

Ien fan 'e funksjes fan' e konkurrinsje wie de heul lawaaierige gegevens (sjoch de titelôfbylding, liend fan @Leigh.plt fan ODS slack). Grutte batches helpe dit te bestriden, en nei eksperiminten op in PC mei 1 GPU, hawwe wy besletten trainingsmodellen op ferskate GPU's yn 'e wolk te behearskjen. Brûkte GoogleCloud (goede gids foar de basis) troch de grutte seleksje fan beskikbere konfiguraasjes, ridlike prizen en $ 300 bonus. Ut gierichheid bestelde ik in 4xV100 eksimplaar mei in SSD en in ton RAM, en dat wie in grutte flater. Sa'n masine fret jild gau op; jo kinne eksperimintearje sûnder in bewezen pipeline. Foar edukative doelen is it better om de K80 te nimmen. Mar de grutte hoemannichte RAM kaam goed fan pas - de wolk SSD makke net yndruk mei syn prestaasjes, sadat de databank waard oerbrocht nei dev/shm.

Fan it grutste belang is it koadefragmint ferantwurdlik foar it brûken fan meardere GPU's. Earst wurdt it model makke op 'e CPU mei in kontekstbehearder, krekt lykas yn 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
  )
})

Dan wurdt it net kompilearre (dit is wichtich) model kopiearre nei in bepaald oantal beskikbere GPU's, en pas dêrnei wurdt it kompilearre:

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

De klassike technyk fan it befriezen fan alle lagen útsein de lêste, training fan 'e lêste laach, unfreezing en retraining fan it hiele model foar ferskate GPU's koe net wurde ymplementearre.

Training waard kontrolearre sûnder gebrûk. tensorboard, ússels beheine ta it opnimmen fan logs en it bewarjen fan modellen mei ynformative nammen nei elk tiidrek:

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. Yn stee fan in konklúzje

In oantal problemen dy't wy binne tsjinkaam binne noch net oerwûn:

  • в keras d'r is gjin ready-made funksje foar automatysk sykjen nei de optimale learsnelheid (analoog lr_finder yn biblioteek fast.ai); Mei wat muoite is it mooglik om ymplemintaasjes fan tredden nei R te poarte, bygelyks, dit;
  • as gefolch fan it foarige punt wie it net mooglik om de juste trainingssnelheid te selektearjen by it brûken fan ferskate GPU's;
  • d'r is in tekoart oan moderne arsjitektueren foar neuronale netwurken, benammen dy dy't foarôf traind binne op imagenet;
  • gjin ien syklus belied en diskriminearjende lear tariven (cosinus annealing wie op ús fersyk útfierd, tank skeydan).

Hokker nuttige dingen waarden leard fan dizze kompetysje:

  • Op relatyf lege-macht hardware kinne jo wurkje mei fatsoenlike (in protte kearen de grutte fan RAM) voluminten gegevens sûnder pine. Plestik tas data.table besparret ûnthâld troch in-place wiziging fan tabellen, dy't foarkomt dat se kopiearje, en as se goed brûkt wurde, demonstrearje de mooglikheden hast altyd de heechste snelheid ûnder alle ark dat ús bekend is foar skripttalen. It bewarjen fan gegevens yn in databank lit jo, yn in protte gefallen, hielendal net tinke oer de needsaak om de folsleine dataset yn RAM te squeeze.
  • Stadige funksjes yn R kinne wurde ferfongen mei flugge yn C ++ mei help fan it pakket Rcpp. As neist gebrûk RcppThread of RcppParallel, wy krije cross-platform multi-threaded ymplemintaasjes, dus it is net nedich om de koade op it R-nivo te parallelisearjen.
  • Pakket Rcpp kin brûkt wurde sûnder serieuze kennis fan C ++, it fereaske minimum wurdt sketst hjir. Koptekstbestannen foar in oantal coole C-biblioteken lykas xtensor beskikber op CRAN, dat is, in ynfrastruktuer wurdt foarme foar de ymplemintaasje fan projekten dy't klearmakke C++-koade mei hege prestaasjes yntegrearje yn R. Oanfoljende gemak is syntaksis markearring en in statyske C ++ koade analysator yn RStudio.
  • docopt kinne jo selsstannige skripts útfiere mei parameters. Dit is handich foar gebrûk op in tsjinner op ôfstân, ynkl. ûnder docker. Yn RStudio is it ûngemaklik om in protte oeren fan eksperiminten út te fieren mei training fan neurale netwurken, en it ynstallearjen fan de IDE op 'e server sels is net altyd rjochtfeardige.
  • Docker soarget foar koadeportabiliteit en reprodusearberens fan resultaten tusken ûntwikkelders mei ferskate ferzjes fan it OS en bibleteken, lykas ek maklik útfiering op servers. Jo kinne de heule trainingpipeline starte mei mar ien kommando.
  • Google Cloud is in budzjetfreonlike manier om te eksperimintearjen op djoere hardware, mar jo moatte konfiguraasjes foarsichtich kieze.
  • It mjitten fan de snelheid fan yndividuele koadefragminten is heul nuttich, benammen by it kombinearjen fan R en C++, en mei it pakket bank - ek hiel maklik.

Oer it algemien wie dizze ûnderfining heul beleanjend en wy wurkje fierder om guon fan 'e problemen op te lossen.

Boarne: www.habr.com

Add a comment