Quick Draw Doodle Recognition: hoe om vriende te maak met R, C++ en neurale netwerke

Quick Draw Doodle Recognition: hoe om vriende te maak met R, C++ en neurale netwerke

Haai Habr!

Verlede herfs het Kaggle 'n kompetisie aangebied om handgetekende prente te klassifiseer, Quick Draw Doodle Recognition, waaraan onder andere 'n span R-wetenskaplikes deelgeneem het: Artem Klevtsova, Philippa Bestuurder и Andrey Ogurtsov. Ons sal nie die kompetisie in detail beskryf nie; dit is reeds in onlangse publikasie.

Hierdie keer het dit nie uitgewerk met medaljeboerdery nie, maar baie waardevolle ondervinding is opgedoen, so ek wil graag vir die gemeenskap vertel van 'n aantal van die interessantste en nuttigste dinge op Kagle en in die alledaagse werk. Onder die onderwerpe wat bespreek is: moeilike lewe sonder OpenCV, JSON-ontleding (hierdie voorbeelde ondersoek die integrasie van C++-kode in skrifte of pakkette in R met Rcpp), parameterisering van skrifte en doktering van die finale oplossing. Alle kode van die boodskap in 'n vorm wat geskik is vir uitvoering is beskikbaar in bewaarplekke.

Inhoud:

  1. Laai data doeltreffend vanaf CSV na MonetDB
  2. Voorbereiding van groepe
  3. Iterators vir die aflaai van bondels uit die databasis
  4. Die keuse van 'n modelargitektuur
  5. Skripparameterisering
  6. Dockerisering van skrifte
  7. Gebruik verskeie GPU's op Google Cloud
  8. In plaas daarvan om 'n gevolgtrekking

1. Laai data doeltreffend vanaf CSV in die MonetDB-databasis

Die data in hierdie kompetisie word nie in die vorm van klaargemaakte beelde verskaf nie, maar in die vorm van 340 CSV-lêers (een lêer vir elke klas) wat JSONs met puntkoördinate bevat. Deur hierdie punte met lyne te verbind, kry ons 'n finale beeld wat 256x256 pixels meet. Ook vir elke rekord is daar 'n etiket wat aandui of die prent korrek herken is deur die klassifiseerder wat gebruik is toe die datastel versamel is, 'n tweeletterkode van die land van verblyf van die outeur van die prent, 'n unieke identifiseerder, 'n tydstempel en 'n klasnaam wat ooreenstem met die lêernaam. ’n Vereenvoudigde weergawe van die oorspronklike data weeg 7.4 GB in die argief en ongeveer 20 GB na uitpak, neem die volle data na uitpak 240 GB op. Die organiseerders het verseker dat beide weergawes dieselfde tekeninge weergee, wat beteken dat die volledige weergawe oorbodig was. In elk geval, die berging van 50 miljoen beelde in grafiese lêers of in die vorm van skikkings is onmiddellik as onwinsgewend beskou, en ons het besluit om alle CSV-lêers uit die argief saam te voeg train_simplified.zip in die databasis met daaropvolgende generering van beelde van die vereiste grootte "on the fly" vir elke bondel.

'n Goed bewese stelsel is as die DBBS gekies MonetDB, naamlik 'n implementering vir R as 'n pakket MonetDBLite. Die pakket sluit 'n ingebedde weergawe van die databasisbediener in en laat jou toe om die bediener direk vanaf 'n R-sessie op te tel en daar daarmee te werk. Die skep van 'n databasis en koppel daaraan word uitgevoer met een opdrag:

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

Ons sal twee tabelle moet skep: een vir alle data, die ander vir diensinligting oor afgelaaide lêers (nuttig as iets verkeerd loop en die proses moet hervat word nadat verskeie lêers afgelaai is):

Die skep van tabelle

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

Die vinnigste manier om data in die databasis te laai, was om CSV-lêers direk te kopieer met behulp van SQL - command COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTWaar tablename - tabel naam en path - die pad na die lêer. Terwyl daar met die argief gewerk is, is ontdek dat die ingeboude implementering unzip in R werk nie korrek met 'n aantal lêers uit die argief nie, daarom het ons die stelsel gebruik unzip (met behulp van die parameter getOption("unzip")).

Funksie om na die databasis te skryf

#' @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 jy die tabel moet transformeer voordat jy dit na die databasis skryf, is dit genoeg om die argument deur te gee preprocess funksie wat die data sal transformeer.

Kode vir die opeenvolgende laai van data in die databasis:

Die skryf van data na die databasis

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

Data laai tyd kan wissel na gelang van die spoed eienskappe van die aandrywer wat gebruik word. In ons geval neem lees en skryf binne een SSD of vanaf 'n flash drive (bronlêer) na 'n SSD (DB) minder as 10 minute.

Dit neem nog 'n paar sekondes om 'n kolom met 'n heelgetalklasetiket en 'n indekskolom (ORDERED INDEX) met reëlnommers waarmee waarnemings gemonster sal word wanneer groepe geskep word:

Skep addisionele kolomme en indeks

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 die probleem op te los om 'n bondel op die vlieg te skep, moes ons die maksimum spoed bereik om ewekansige rye uit die tabel te onttrek doodles. Hiervoor het ons 3 truuks gebruik. Die eerste was om die dimensionaliteit van die tipe wat die waarneming-ID stoor, te verminder. In die oorspronklike datastel is die tipe wat nodig is om die ID te stoor bigint, maar die aantal waarnemings maak dit moontlik om hul identifiseerders, gelyk aan die ranggetal, in die tipe in te pas int. Die soektog is in hierdie geval baie vinniger. Die tweede truuk was om te gebruik ORDERED INDEX - ons het empiries tot hierdie besluit gekom, nadat ons deur al die beskikbare gegaan het opsies. Die derde was om geparameteriseerde navrae te gebruik. Die essensie van die metode is om die opdrag een keer uit te voer PREPARE met die daaropvolgende gebruik van 'n voorbereide uitdrukking wanneer 'n klomp navrae van dieselfde tipe geskep word, maar in werklikheid is daar 'n voordeel in vergelyking met 'n eenvoudige een SELECT blyk binne die omvang van statistiese foute te wees.

Die proses om data op te laai, verbruik nie meer as 450 MB RAM nie. Dit wil sê, die beskryfde benadering laat jou toe om datastelle wat tientalle gigagrepe weeg op byna enige begroting hardeware te skuif, insluitend sommige enkelbord-toestelle, wat redelik cool is.

Al wat oorbly, is om die spoed van herwinning van (ewekansige) data te meet en die skaal te evalueer wanneer groepe van verskillende groottes monsters geneem word:

Databasis maatstaf

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 om vriende te maak met R, C++ en neurale netwerke

2. Voorbereiding van bondels

Die hele bondel voorbereidingsproses bestaan ​​uit die volgende stappe:

  1. Ontleding van verskeie JSONs wat vektore van stringe met koördinate van punte bevat.
  2. Teken gekleurde lyne gebaseer op die koördinate van punte op 'n beeld van die vereiste grootte (byvoorbeeld 256×256 of 128×128).
  3. Omskakeling van die resulterende beelde in 'n tensor.

As deel van die kompetisie onder Python-pitte, is die probleem hoofsaaklik opgelos met behulp van OpenCV. Een van die eenvoudigste en mees voor die hand liggende analoë in R sal soos volg lyk:

Implementering van JSON na Tensor-omskakeling in R

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

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

Tekening word uitgevoer met behulp van standaard R-gereedskap en gestoor in 'n tydelike PNG wat in RAM gestoor is (op Linux is tydelike R-gidse in die gids geleë /tmp, gemonteer in RAM). Hierdie lêer word dan gelees as 'n driedimensionele skikking met getalle wat wissel van 0 tot 1. Dit is belangrik omdat 'n meer konvensionele BMP in 'n rou skikking met hekskleurkodes gelees sal word.

Kom ons toets die resultaat:

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 om vriende te maak met R, C++ en neurale netwerke

Die bondel self sal soos volg gevorm word:

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

Hierdie implementering het vir ons suboptimaal gelyk, aangesien die vorming van groot groepe 'n onwelvoeglike lang tyd neem, en ons het besluit om voordeel te trek uit die ervaring van ons kollegas deur 'n kragtige biblioteek te gebruik OpenCV. Op daardie tydstip was daar geen klaargemaakte pakket vir R nie (daar is nie nou nie), so 'n minimale implementering van die vereiste funksionaliteit is in C++ geskryf met integrasie in R-kode deur gebruik te maak van Rcpp.

Om die probleem op te los, is die volgende pakkette en biblioteke gebruik:

  1. OpenCV om met beelde te werk en lyne te teken. Gebruik vooraf geïnstalleerde stelselbiblioteke en koplêers, sowel as dinamiese koppeling.

  2. xtensor vir werk met multidimensionele skikkings en tensors. Ons het koplêers gebruik wat in die R-pakket met dieselfde naam ingesluit is. Die biblioteek laat jou toe om met multidimensionele skikkings te werk, beide in ry-hoof- en kolom-hoofvolgorde.

  3. ndjson vir die ontleding van JSON. Hierdie biblioteek word gebruik in xtensor outomaties as dit in die projek teenwoordig is.

  4. RcppThread vir die organisering van multi-threaded verwerking van 'n vektor van JSON. Gebruik die koplêers wat deur hierdie pakket verskaf word. Van meer gewild RcppParallel Die pakket het onder meer 'n ingeboude lusonderbrekingsmeganisme.

Daar moet op gelet word dat xtensor blyk 'n uitkoms te wees: benewens die feit dat dit uitgebreide funksionaliteit en hoë werkverrigting het, blyk dit dat die ontwikkelaars redelik reageer en vrae vinnig en in detail beantwoord het. Met hul hulp was dit moontlik om transformasies van OpenCV-matrikse in xtensor-tensors te implementeer, sowel as 'n manier om 3-dimensionele beeldtensors te kombineer in 'n 4-dimensionele tensor van die korrekte dimensie (die bondel self).

Materiaal om Rcpp, xtensor en RcppThread te leer

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 lêers saam te stel wat stelsellêers en dinamiese skakeling gebruik met biblioteke wat op die stelsel geïnstalleer is, het ons die inpropmeganisme gebruik wat in die pakket geïmplementeer is Rcpp. Om paaie en vlae outomaties te vind, het ons 'n gewilde Linux-hulpmiddel gebruik pkg-konfigurasie.

Implementering van die Rcpp-inprop vir die gebruik van die OpenCV-biblioteek

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 gevolg van die werking van die inprop, sal die volgende waardes tydens die samestellingsproses vervang word:

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"

Die implementeringskode vir die ontleding van JSON en die generering van 'n bondel vir oordrag na die model word onder die bederf gegee. Voeg eers 'n plaaslike projekgids by om na koplêers te soek (nodig vir ndjson):

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

Implementering van JSON na tensor omskakeling in C++

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

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

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

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

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

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

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

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

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

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

Hierdie kode moet in die lêer geplaas word src/cv_xt.cpp en stel saam met die opdrag Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); ook nodig vir werk nlohmann/json.hpp van bewaarplek. Die kode is in verskeie funksies verdeel:

  • to_xt - 'n sjabloonfunksie vir die transformasie van 'n beeldmatriks (cv::Mat) na 'n tensor xt::xtensor;

  • parse_json — die funksie ontleed 'n JSON-string, onttrek die koördinate van punte, pak hulle in 'n vektor;

  • ocv_draw_lines - uit die gevolglike vektor van punte, trek veelkleurige lyne;

  • process - kombineer die bogenoemde funksies en voeg ook die vermoë by om die resulterende beeld te skaal;

  • cpp_process_json_str - vou oor die funksie process, wat die resultaat na 'n R-voorwerp (multidimensionele skikking) uitvoer;

  • cpp_process_json_vector - vou oor die funksie cpp_process_json_str, wat jou toelaat om 'n stringvektor in multi-threaded modus te verwerk.

Om veelkleurige lyne te trek, is die HSV-kleurmodel gebruik, gevolg deur omskakeling na RGB. Kom ons toets die resultaat:

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 om vriende te maak met R, C++ en neurale netwerke
Vergelyking van die spoed van implementering in 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 om vriende te maak met R, C++ en neurale netwerke

Soos u kan sien, het die spoedverhoging baie betekenisvol geblyk te wees, en dit is nie moontlik om C++-kode in te haal deur R-kode te paralleliseer nie.

3. Iterators om groepe van die databasis af te laai

R het 'n welverdiende reputasie vir die verwerking van data wat in RAM pas, terwyl Python meer gekenmerk word deur iteratiewe dataverwerking, wat jou toelaat om maklik en natuurlik buite-kern-berekeninge te implementeer (berekeninge met behulp van eksterne geheue). 'n Klassieke en relevante voorbeeld vir ons in die konteks van die beskryfde probleem is diep neurale netwerke wat opgelei word deur die gradiënt-afkomsmetode met benadering van die gradiënt by elke stap met behulp van 'n klein gedeelte van waarnemings, of mini-batch.

Diep leerraamwerke wat in Python geskryf is, het spesiale klasse wat iterators implementeer gebaseer op data: tabelle, prente in dopgehou, binêre formate, ens. Jy kan klaargemaakte opsies gebruik of jou eie skryf vir spesifieke take. In R kan ons voordeel trek uit al die kenmerke van die Python-biblioteek keras met sy verskillende backends met behulp van die pakket met dieselfde naam, wat op sy beurt bo-op die pakket werk netjies. Laasgenoemde verdien 'n aparte lang artikel; dit laat jou nie net toe om Python-kode vanaf R te laat loop nie, maar laat jou ook toe om voorwerpe tussen R- en Python-sessies oor te dra, wat outomaties al die nodige tipe omskakelings uitvoer.

Ons het ontslae geraak van die behoefte om al die data in RAM te stoor deur MonetDBLite te gebruik, al die "neurale netwerk" werk sal uitgevoer word deur die oorspronklike kode in Python, ons moet net 'n iterator oor die data skryf, aangesien daar niks gereed is nie vir so 'n situasie in óf R óf Python. Daar is in wese net twee vereistes daarvoor: dit moet bondels in 'n eindelose lus terugstuur en sy toestand tussen iterasies stoor (laasgenoemde in R word op die eenvoudigste manier geïmplementeer deur sluitings te gebruik). Voorheen was dit nodig om R-skikkings eksplisiet om te skakel in numpy skikkings binne die iterator, maar die huidige weergawe van die pakket keras doen dit self.

Die iterator vir opleiding- en valideringsdata blyk soos volg te wees:

Iterator vir opleiding en validering data

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

Die funksie neem as invoer 'n veranderlike met 'n verbinding met die databasis, die aantal lyne wat gebruik word, die aantal klasse, bondelgrootte, skaal (scale = 1 stem ooreen met die weergawe van beelde van 256x256 piksels, scale = 0.5 — 128x128 piksels), kleuraanwyser (color = FALSE spesifiseer lewering in grysskaal wanneer dit gebruik word color = TRUE elke slag word in 'n nuwe kleur geteken) en 'n voorverwerkingsaanwyser vir netwerke wat vooraf op imagenet opgelei is. Laasgenoemde is nodig om pixelwaardes van die interval [0, 1] na die interval [-1, 1] te skaal wat gebruik is tydens die opleiding van die verskafde keras modelle.

Die eksterne funksie bevat argument tipe kontrolering, 'n tabel data.table met ewekansig gemengde lynnommers van samples_index en bondelnommers, teller en maksimum aantal bondels, sowel as 'n SQL-uitdrukking vir die aflaai van data vanaf die databasis. Daarbenewens het ons 'n vinnige analoog van die funksie binne gedefinieer keras::to_categorical(). Ons het byna al die data vir opleiding gebruik, en 'n halwe persent gelaat vir validering, so die epoggrootte is beperk deur die parameter steps_per_epoch wanneer geroep word keras::fit_generator(), en die toestand if (i > max_i) gewerk net vir die validering iterator.

In die interne funksie word ry-indekse vir die volgende bondel opgespoor, rekords word van die databasis afgelaai met die bondelteller wat toeneem, JSON-parsing (funksie cpp_process_json_vector(), geskryf in C++) en skep skikkings wat ooreenstem met prente. Dan word een-warm vektore met klasetikette geskep, skikkings met pixelwaardes en etikette word gekombineer in 'n lys, wat die terugkeerwaarde is. Om werk te bespoedig, het ons die skepping van indekse in tabelle gebruik data.table en wysiging via die skakel - sonder hierdie pakket "skyfies" data.tabel Dit is nogal moeilik om jou voor te stel om effektief met enige beduidende hoeveelheid data in R te werk.

Die resultate van spoedmetings op 'n Core i5-skootrekenaar is soos volg:

Iterator maatstaf

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 om vriende te maak met R, C++ en neurale netwerke

As u 'n voldoende hoeveelheid RAM het, kan u die werking van die databasis ernstig bespoedig deur dit na dieselfde RAM oor te dra (32 GB is genoeg vir ons taak). In Linux is die partisie by verstek gemonteer /dev/shm, wat tot die helfte van die RAM-kapasiteit beset. Jy kan meer uitlig deur te wysig /etc/fstabom 'n rekord like te kry tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Maak seker dat u herlaai en die resultaat nagaan deur die opdrag uit te voer df -h.

Die iterator vir toetsdata lyk baie eenvoudiger, aangesien die toetsdatastel heeltemal in RAM pas:

Iterator vir toetsdata

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. Keuse van modelargitektuur

Die eerste argitektuur wat gebruik is, was mobiele netwerk v1, waarvan die kenmerke bespreek word in hierdie boodskap. Dit is standaard ingesluit keras en, dienooreenkomstig, is beskikbaar in die pakket met dieselfde naam vir R. Maar wanneer dit met enkelkanaalbeelde probeer gebruik word, het 'n vreemde ding geblyk: die invoertensor moet altyd die dimensie hê (batch, height, width, 3), dit wil sê, die aantal kanale kan nie verander word nie. Daar is nie so 'n beperking in Python nie, so ons het gehaas en ons eie implementering van hierdie argitektuur geskryf, na aanleiding van die oorspronklike artikel (sonder die uitval wat in die keras-weergawe is):

Mobilenet v1 argitektuur

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

Die nadele van hierdie benadering is duidelik. Ek wil baie modelle toets, maar inteendeel, ek wil nie elke argitektuur met die hand oorskryf nie. Ons is ook die geleentheid ontneem om die gewigte van modelle wat vooraf op imagenet opgelei is, te gebruik. Soos gewoonlik het die bestudering van die dokumentasie gehelp. Funksie get_config() laat jou toe om 'n beskrywing van die model te kry in 'n vorm wat geskik is vir redigering (base_model_conf$layers - 'n gereelde R-lys), en die funksie from_config() voer die omgekeerde omskakeling na 'n modelvoorwerp uit:

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)

Nou is dit nie moeilik om 'n universele funksie te skryf om enige van die verskafde te bekom nie keras modelle met of sonder gewigte opgelei op imagenet:

Funksie vir die laai van klaargemaakte argitekture

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

Wanneer enkelkanaalbeelde gebruik word, word geen voorafopgeleide gewigte gebruik nie. Dit kan reggestel word: met behulp van die funksie get_weights() kry die modelgewigte in die vorm van 'n lys van R-skikkings, verander die dimensie van die eerste element van hierdie lys (deur een kleurkanaal te neem of al drie te gemiddelde), en laai dan die gewigte terug in die model met die funksie set_weights(). Ons het nooit hierdie funksionaliteit bygevoeg nie, want in hierdie stadium was dit reeds duidelik dat dit meer produktief is om met kleurprente te werk.

Ons het die meeste van die eksperimente uitgevoer met behulp van mobielenet weergawes 1 en 2, sowel as resnet34. Meer moderne argitekture soos SE-ResNeXt het goed gevaar in hierdie kompetisie. Ongelukkig het ons nie klaargemaakte implementerings tot ons beskikking gehad nie, en ons het nie ons eie geskryf nie (maar ons sal beslis skryf).

5. Parameterisering van skrifte

Gerieflikheidshalwe is alle kode vir die begin van opleiding ontwerp as 'n enkele skrif, geparameteriseer met behulp van dokter soos volg:

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 dokter die implementering verteenwoordig http://docopt.org/ vir R. Met sy hulp word skrifte geloods met eenvoudige opdragte soos 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 lêer train_nn.R is uitvoerbaar (hierdie opdrag sal die model begin oefen resnet50 op driekleur beelde wat 128x128 pixels meet, moet die databasis in die vouer geleë wees /home/andrey/doodle_db). U kan leerspoed, tipe optimaliseerder en enige ander aanpasbare parameters by die lys voeg. In die proses van die voorbereiding van die publikasie, het dit geblyk dat die argitektuur mobilenet_v2 vanaf die huidige weergawe keras in R gebruik moet nie as gevolg van veranderinge wat nie in die R-pakket in ag geneem is nie, wag ons dat hulle dit regstel.

Hierdie benadering het dit moontlik gemaak om eksperimente met verskillende modelle aansienlik te bespoedig in vergelyking met die meer tradisionele bekendstelling van skrifte in RStudio (ons neem kennis van die pakket as 'n moontlike alternatief tfruns). Maar die grootste voordeel is die vermoë om die bekendstelling van skrifte maklik in Docker of bloot op die bediener te bestuur, sonder om RStudio hiervoor te installeer.

6. Dockerisering van skrifte

Ons het Docker gebruik om oordraagbaarheid van die omgewing te verseker vir opleidingsmodelle tussen spanlede en vir vinnige ontplooiing in die wolk. Jy kan begin om kennis te maak met hierdie instrument, wat relatief ongewoon is vir 'n R programmeerder, met hierdie reeks publikasies of video kursus.

Docker laat jou toe om jou eie beelde van nuuts af te skep en ander beelde as basis te gebruik om jou eie te skep. Toe ons die beskikbare opsies ontleed het, het ons tot die gevolgtrekking gekom dat die installering van NVIDIA-, CUDA+cuDNN-bestuurders en Python-biblioteke 'n redelik lywige deel van die prent is, en ons het besluit om die amptelike prent as basis te neem tensorflow/tensorflow:1.12.0-gpu, en voeg die nodige R-pakkette daar by.

Die finale docker-lêer het soos volg gelyk:

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

Gerieflikheidshalwe is die pakkette wat gebruik is in veranderlikes geplaas; die grootste deel van die geskrewe skrifte word tydens samestelling binne-in die houers gekopieer. Ons het ook die opdragdop verander na /bin/bash vir die gemak van gebruik van inhoud /etc/os-release. Dit het die behoefte vermy om die OS-weergawe in die kode te spesifiseer.

Boonop is 'n klein bash-skrif geskryf waarmee u 'n houer met verskillende opdragte kan begin. Dit kan byvoorbeeld skrifte wees vir die opleiding van neurale netwerke wat voorheen in die houer geplaas is, of 'n opdragdop vir ontfouting en monitering van die werking van die houer:

Skrip om die houer te begin

#!/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 hierdie bash script sonder parameters uitgevoer word, sal die script binne die houer geroep word train_nn.R met verstekwaardes; as die eerste posisionele argument "bash" is, dan sal die houer interaktief begin met 'n opdragdop. In alle ander gevalle word die waardes van posisionele argumente vervang: CMD="Rscript /app/train_nn.R $@".

Dit is opmerklik dat die gidse met brondata en databasis, sowel as die gids vir die stoor van opgeleide modelle, binne die houer van die gasheerstelsel gemonteer is, wat jou toelaat om toegang tot die resultate van die skrifte te kry sonder onnodige manipulasies.

7. Gebruik verskeie GPU's op Google Wolk

Een van die kenmerke van die kompetisie was die baie raserige data (sien die titelprent, geleen by @Leigh.plt van ODS slack). Groot bondels help om dit te bekamp, ​​en na eksperimente op 'n rekenaar met 1 GPU, het ons besluit om opleidingsmodelle op verskeie GPU's in die wolk te bemeester. Gebruik GoogleCloud (goeie gids tot die basiese beginsels) as gevolg van die groot keuse van beskikbare konfigurasies, billike pryse en $300 bonus. Uit gierigheid het ek 'n 4xV100-instansie met 'n SSD en 'n ton RAM bestel, en dit was 'n groot fout. So 'n masjien eet vinnig geld op; jy kan stukkend gaan eksperimenteer sonder 'n bewese pyplyn. Vir opvoedkundige doeleindes is dit beter om die K80 te neem. Maar die groot hoeveelheid RAM het handig te pas gekom - die wolk SSD het nie beïndruk met sy werkverrigting nie, so die databasis is oorgedra na dev/shm.

Van die grootste belang is die kodefragment wat verantwoordelik is vir die gebruik van veelvuldige GPU's. Eerstens word die model op die SVE geskep met behulp van 'n konteksbestuurder, net soos in Python:

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

Dan word die ongekompileerde (dit is belangrik) model na 'n gegewe aantal beskikbare GPU's gekopieer, en eers daarna word dit saamgestel:

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

Die klassieke tegniek van vries alle lae behalwe die laaste een, opleiding van die laaste laag, ontvries en heropleiding van die hele model vir verskeie GPU's kon nie geïmplementeer word nie.

Opleiding is sonder gebruik gemonitor. tensorbord, beperk ons ​​tot die opneem van logs en stoor modelle met insiggewende name na elke epog:

Terugbelopings

# Шаблон имени файла лога
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. In plaas van 'n gevolgtrekking

'n Aantal probleme wat ons teëgekom het, is nog nie oorkom nie:

  • в keras daar is geen klaargemaakte funksie om outomaties na die optimale leertempo te soek nie (analoog lr_finder in biblioteek vinnig.ai); Met 'n bietjie moeite is dit moontlik om derdeparty-implementerings na R oor te dra, byvoorbeeld, hierdie;
  • as gevolg van die vorige punt was dit nie moontlik om die korrekte opleidingspoed te kies wanneer verskeie GPU's gebruik word nie;
  • daar is 'n gebrek aan moderne neurale netwerkargitekture, veral dié wat vooraf op imagenet opgelei is;
  • geen eensiklusbeleid en diskriminerende leerkoerse nie (kosinusgloeiing was op ons versoek geïmplementeer, dankie skeydan).

Watter nuttige dinge is uit hierdie kompetisie geleer:

  • Op hardeware met relatief lae krag kan jy sonder pyn met ordentlike (baie keer die grootte van RAM) datavolumes werk. Plastiese sak data.tabel spaar geheue as gevolg van in-plek wysiging van tabelle, wat die kopiëring daarvan vermy, en wanneer dit korrek gebruik word, demonstreer sy vermoëns byna altyd die hoogste spoed onder alle gereedskap wat aan ons bekend is vir skriftale. Deur data in 'n databasis te stoor, kan jy in baie gevalle glad nie dink aan die behoefte om die hele datastel in RAM in te druk nie.
  • Stadige funksies in R kan vervang word met vinnige funksies in C++ deur die pakket te gebruik Rcpp. As bykomend tot gebruik RcppThread of RcppParallel, kry ons kruis-platform multi-threaded implementerings, so dit is nie nodig om die kode op die R-vlak te paralleliseer nie.
  • Pakket Rcpp gebruik kan word sonder ernstige kennis van C++, word die vereiste minimum uiteengesit hier. Koplêers vir 'n aantal oulike C-biblioteke soos xtensor beskikbaar op CRAN, dit wil sê 'n infrastruktuur word gevorm vir die implementering van projekte wat klaargemaakte hoëprestasie C++-kode in R integreer. Bykomende gerief is sintaksisverligting en 'n statiese C++-kode-ontleder in RStudio.
  • dokter laat jou toe om selfstandige skrifte met parameters uit te voer. Dit is gerieflik vir gebruik op 'n afgeleë bediener, insluitend. onder docker. In RStudio is dit ongerieflik om baie ure se eksperimente uit te voer met die opleiding van neurale netwerke, en die installering van die IDE op die bediener self is nie altyd geregverdig nie.
  • Docker verseker kodeoordraagbaarheid en reproduceerbaarheid van resultate tussen ontwikkelaars met verskillende weergawes van die bedryfstelsel en biblioteke, sowel as die gemak van uitvoering op bedieners. Jy kan die hele opleidingspyplyn met net een opdrag begin.
  • Google Cloud is 'n begrotingsvriendelike manier om met duur hardeware te eksperimenteer, maar jy moet konfigurasies versigtig kies.
  • Om die spoed van individuele kodefragmente te meet is baie nuttig, veral wanneer R en C++ gekombineer word, en met die pakket bank - ook baie maklik.

Oor die algemeen was hierdie ervaring baie lonend en ons gaan voort om te werk om sommige van die kwessies wat geopper is op te los.

Bron: will.com

Voeg 'n opmerking