Rapida Desegna Doodle-Rekono: kiel amikiĝi kun R, C++ kaj neŭralaj retoj

Rapida Desegna Doodle-Rekono: kiel amikiĝi kun R, C++ kaj neŭralaj retoj

Hej Habr!

Lastan aŭtunon, Kaggle aranĝis konkurson por klasifiki mane desegnitajn bildojn, Quick Draw Doodle Recognition, en kiu, interalie, partoprenis teamo de R-sciencistoj: Artem Klevtsova, Philippa Direktisto и Andrej Ogurcov. Ni ne detale priskribos la konkurson; tio jam estis farita lastatempa publikigo.

Ĉi-foje ĝi ne funkciis kun medalkultivado, sed multe da valora sperto estis akirita, do mi ŝatus rakonti al la komunumo pri kelkaj el la plej interesaj kaj utilaj aferoj pri Kagle kaj en ĉiutaga laboro. Inter la diskutataj temoj: malfacila vivo sen OpenCV, JSON-analizo (ĉi tiuj ekzemploj ekzamenas la integriĝon de C++-kodo en manuskriptojn aŭ pakaĵojn en R uzante Rcpp), parametrigo de skriptoj kaj dokerigo de la fina solvo. Ĉiu kodo de la mesaĝo en formo taŭga por ekzekuto estas havebla en deponejoj.

Enhavo:

  1. Efike ŝarĝu datumojn de CSV en MonetDB
  2. Preparado de aroj
  3. Iteratoroj por malŝarĝi arojn el la datumbazo
  4. Elektante Modelan Arkitekturon
  5. Skripto-parametrigo
  6. Dokerigo de skriptoj
  7. Uzante plurajn GPU-ojn en Google Cloud
  8. Anstataŭ konkludo

1. Efike ŝarĝu datumojn de CSV en la datumbazon de MonetDB

La datumoj en ĉi tiu konkurso estas provizitaj ne en la formo de pretaj bildoj, sed en la formo de 340 CSV-dosieroj (unu dosiero por ĉiu klaso) enhavantaj JSON-ojn kun punktaj koordinatoj. Konektante ĉi tiujn punktojn per linioj, ni ricevas finan bildon je 256x256 pikseloj. Ankaŭ por ĉiu rekordo estas etikedo indikanta ĉu la bildo estis ĝuste rekonita de la klasigilo uzata en la momento kiam la datumaro estis kolektita, dulitera kodo de la loĝlando de la aŭtoro de la bildo, unika identigilo, tempomarko. kaj klasnomo kiu kongruas kun la dosiernomo. Simpligita versio de la originaj datumoj pezas 7.4 GB en la arkivo kaj proksimume 20 GB post malpakado, la plenaj datumoj post malpakado okupas 240 GB. La aranĝantoj certigis ke ambaŭ versioj reproduktis la samajn desegnaĵojn, signifante ke la plena versio estis redunda. Ĉiukaze, stoki 50 milionojn da bildoj en grafikaj dosieroj aŭ en formo de tabeloj estis tuj konsiderata neprofita, kaj ni decidis kunfandi ĉiujn CSV-dosierojn el la arkivo. trajno_simpligita.zip en la datumbazon kun posta generacio de bildoj de la bezonata grandeco "sur la muŝo" por ĉiu aro.

Bone pruvita sistemo estis elektita kiel la DBMS MonetDB, nome efektivigo por R kiel pakaĵo MonetDBLite. La pako inkluzivas enigitan version de la datumbaza servilo kaj permesas vin preni la servilon rekte de R-sesio kaj labori kun ĝi tie. Krei datumbazon kaj konekti al ĝi estas faritaj per unu komando:

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

Ni devos krei du tabelojn: unu por ĉiuj datumoj, la alia por servaj informoj pri elŝutitaj dosieroj (utila se io misfunkcias kaj la procezo devas esti rekomencita post elŝuto de pluraj dosieroj):

Kreante tabelojn

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

La plej rapida maniero ŝargi datumojn en la datumbazon estis rekte kopii CSV-dosierojn per SQL - komando COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTkie tablename - tabelnomo kaj path - la vojo al la dosiero. Laborante kun la arkivo, estis malkovrite ke la enkonstruita efektivigo unzip en R ne funkcias ĝuste kun kelkaj dosieroj el la arkivo, do ni uzis la sistemon unzip (uzante la parametron getOption("unzip")).

Funkcio por skribi al la datumbazo

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

Se vi bezonas transformi la tabelon antaŭ ol skribi ĝin al la datumbazo, sufiĉas pasigi la argumenton preprocess funkcio kiu transformos la datumojn.

Kodo por sinsekve ŝargi datumojn en la datumbazon:

Skribante datumojn al la datumbazo

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

La tempo de ŝarĝo de datumoj povas varii laŭ la rapidecaj trajtoj de la uzata disko. En nia kazo, legi kaj skribi ene de unu SSD aŭ de flash drive (fontodosiero) al SSD (DB) daŭras malpli ol 10 minutojn.

Necesas ankoraŭ kelkaj sekundoj por krei kolumnon kun entjerklasa etikedo kaj indeksa kolumno (ORDERED INDEX) kun linionumeroj per kiuj observaĵoj estos provitaj dum kreado de aroj:

Kreante Pliajn Kolumnojn kaj Indekson

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

Por solvi la problemon krei aron sur la muŝo, ni bezonis atingi la maksimuman rapidecon ĉerpi hazardajn vicojn de la tablo. doodles. Por tio ni uzis 3 lertaĵojn. La unua estis redukti la dimensiecon de la tipo kiu stokas la observan ID. En la originala datumaro, la tipo necesa por stoki la ID estas bigint, sed la nombro da observoj ebligas alĝustigi iliajn identigilojn, egalajn al la orda nombro, en la tipon int. La serĉo estas multe pli rapida en ĉi tiu kazo. La dua lertaĵo estis uzi ORDERED INDEX — ni venis al ĉi tiu decido empirie, trarigardinte ĉiujn disponeblajn ebloj. La tria estis uzi parametrajn demandojn. La esenco de la metodo estas ekzekuti la komandon unufoje PREPARE kun posta uzo de preta esprimo kiam oni kreas aron da samtipaj demandoj, sed fakte estas avantaĝo kompare kun simpla. SELECT montriĝis en la intervalo de statistika eraro.

La procezo de alŝuto de datumoj konsumas ne pli ol 450 MB da RAM. Tio estas, la priskribita aliro permesas movi datumajn arojn pezantajn dekojn da gigabajtoj sur preskaŭ ajna buĝeta aparataro, inkluzive de iuj unu-tablaj aparatoj, kio estas sufiĉe mojosa.

Restas nur mezuri la rapidecon preni (hazardajn) datumojn kaj taksi la skalon dum provado de aroj de malsamaj grandecoj:

Referenco de datumbazo

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)

Rapida Desegna Doodle-Rekono: kiel amikiĝi kun R, C++ kaj neŭralaj retoj

2. Preparado de aroj

La tuta prepara procezo de aro konsistas el la sekvaj paŝoj:

  1. Analizante plurajn JSON-ojn enhavantajn vektorojn de ŝnuroj kun koordinatoj de punktoj.
  2. Desegni kolorajn liniojn surbaze de la koordinatoj de punktoj sur bildo de la bezonata grandeco (ekzemple, 256×256 aŭ 128×128).
  3. Konverti la rezultajn bildojn en tensoro.

Kiel parto de la konkurado inter Python-kernoj, la problemo estis solvita ĉefe uzante OpenCV. Unu el la plej simplaj kaj evidentaj analogoj en R aspektus tiel:

Efektivigo de JSON al Tensor-Konvertiĝo en 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)
}

Desegno estas farita per normaj R-iloj kaj konservita al provizora PNG konservita en RAM (en Linukso, provizoraj R-dosierujoj troviĝas en la dosierujo. /tmp, muntita en RAM). Ĉi tiu dosiero tiam estas legita kiel tridimensia tabelo kun nombroj intervalantaj de 0 ĝis 1. Tio estas grava ĉar pli konvencia BMP estus legita en krudan tabelon kun deksesaj kolorkodoj.

Ni provu la rezulton:

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

Rapida Desegna Doodle-Rekono: kiel amikiĝi kun R, C++ kaj neŭralaj retoj

La aro mem estos formita jene:

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

Ĉi tiu efektivigo ŝajnis al ni suboptimuma, ĉar la formado de grandaj aroj daŭras maldece longan tempon, kaj ni decidis profiti la sperton de niaj kolegoj uzante potencan bibliotekon. OpenCV. En tiu tempo ekzistis neniu preta pakaĵo por R (ekzistas neniu nun), do minimuma efektivigo de la postulata funkcieco estis skribita en C++ kun integriĝo en R-kodon uzante Rcpp.

Por solvi la problemon, la sekvaj pakaĵoj kaj bibliotekoj estis uzataj:

  1. OpenCV por labori kun bildoj kaj desegni liniojn. Uzis antaŭinstalitajn sistembibliotekojn kaj kapdosierojn, same kiel dinamikan ligon.

  2. xtensor por labori kun plurdimensiaj tabeloj kaj tensoroj. Ni uzis kapdosierojn inkluzivitajn en la R-pakaĵo de la sama nomo. La biblioteko permesas al vi labori kun plurdimensiaj tabeloj, ambaŭ en vico-ĉefa kaj kolumna plej granda ordo.

  3. ndjson por analizi JSON. Ĉi tiu biblioteko estas uzata en xtensor aŭtomate se ĝi ĉeestas en la projekto.

  4. RcppFadeno por organizi multfadenan prilaboradon de vektoro de JSON. Uzis la kapdosierojn provizitajn de ĉi tiu pako. De pli populara RcppParalelo La pakaĵo, interalie, havas enkonstruitan buklan interrompan mekanismon.

Indas rimarki tion xtensor rezultis esti donaco: krom la fakto, ke ĝi havas ampleksan funkciecon kaj altan rendimenton, ĝiaj programistoj montriĝis sufiĉe respondemaj kaj respondis demandojn rapide kaj detale. Kun ilia helpo, eblis efektivigi transformojn de OpenCV-matricoj en xtensor-tensoro, same kiel manieron kombini 3-dimensiajn bildtensoro en 4-dimensia tensoro de la ĝusta dimensio (la aro mem).

Materialoj por lerni Rcpp, xtensor kaj 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

Por kompili dosierojn, kiuj uzas sistemajn dosierojn kaj dinamikan ligon kun bibliotekoj instalitaj en la sistemo, ni uzis la kromprogramon efektivigitan en la pako Rcpp. Por aŭtomate trovi vojojn kaj flagojn, ni uzis popularan Linuksan ilon pkg-config.

Efektivigo de la aldonaĵo Rcpp por uzi la OpenCV-bibliotekon

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

Kiel rezulto de la operacio de la kromaĵo, la sekvaj valoroj estos anstataŭigitaj dum la kompilprocezo:

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"

La efektiviga kodo por analizi JSON kaj generi aron por transdono al la modelo estas donita sub la spoiler. Unue, aldonu lokan projekt-dosierujon por serĉi kapdosierojn (bezonatajn por ndjson):

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

Efektivigo de JSON al tensorkonverto en 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;
}

Ĉi tiu kodo devus esti metita en la dosieron src/cv_xt.cpp kaj kompilu per la komando Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); ankaŭ bezonata por laboro nlohmann/json.hpp el deponejo. La kodo estas dividita en plurajn funkciojn:

  • to_xt — ŝablona funkcio por transformi bildan matricon (cv::Mat) al tensoro xt::xtensor;

  • parse_json — la funkcio analizas JSON-ĉenon, ĉerpas la koordinatojn de punktoj, pakante ilin en vektoron;

  • ocv_draw_lines — el la rezulta vektoro de punktoj, desegnas multkolorajn liniojn;

  • process — kombinas la suprajn funkciojn kaj ankaŭ aldonas la kapablon skali la rezultan bildon;

  • cpp_process_json_str - envolvaĵo super la funkcio process, kiu eksportas la rezulton al R-objekto (multdimensia tabelo);

  • cpp_process_json_vector - envolvaĵo super la funkcio cpp_process_json_str, kiu permesas vin prilabori kordvektoron en multfadena reĝimo.

Por desegni plurkolorajn liniojn, la HSV-kolormodelo estis utiligita, sekvita per konvertiĝo al RGB. Ni provu la rezulton:

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

Rapida Desegna Doodle-Rekono: kiel amikiĝi kun R, C++ kaj neŭralaj retoj
Komparo de la rapideco de efektivigoj en R kaj 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") 

Rapida Desegna Doodle-Rekono: kiel amikiĝi kun R, C++ kaj neŭralaj retoj

Kiel vi povas vidi, la rapidecpliiĝo montriĝis tre signifa, kaj ne eblas atingi C++-kodon paraleligante R-kodon.

3. Iteratoroj por malŝarĝi arojn el la datumbazo

R havas merititan reputacion pri prilaborado de datumoj, kiuj taŭgas en RAM, dum Python estas pli karakterizita per ripeta datumtraktado, ebligante vin facile kaj nature efektivigi eksterkernajn kalkulojn (kalkulojn uzante eksteran memoron). Klasika kaj grava ekzemplo por ni en la kunteksto de la priskribita problemo estas profundaj neŭralaj retoj trejnitaj per la gradienta deveno-metodo kun aproksimado de la gradiento ĉe ĉiu paŝo uzante malgrandan parton de observoj, aŭ mini-aro.

Profunda lernado-kadroj skribitaj en Python havas specialajn klasojn, kiuj efektivigas iterantojn bazitajn sur datumoj: tabeloj, bildoj en dosierujoj, binaraj formatoj, ktp. Vi povas uzi pretajn opciojn aŭ skribi viajn proprajn por specifaj taskoj. En R ni povas utiligi ĉiujn funkciojn de la Python-biblioteko keras kun ĝiaj diversaj backends uzante la samnoman pakaĵon, kiu siavice funkcias sur la pakaĵo reteca. Tiu lasta meritas apartan longan artikolon; ĝi ne nur permesas al vi ruli Python-kodon de R, sed ankaŭ permesas translokigi objektojn inter R kaj Python-sesioj, aŭtomate plenumante ĉiujn necesajn tipkonvertojn.

Ni forigis la bezonon stoki ĉiujn datumojn en RAM uzante MonetDBlite, la tuta laboro de "neŭrala reto" estos farita per la originala kodo en Python, ni nur devas skribi iteratoron super la datumoj, ĉar nenio pretas. por tia situacio en aŭ R aŭ Python. Estas esence nur du postuloj por ĝi: ĝi devas resendi arojn en senfina buklo kaj konservi sian staton inter ripetoj (ĉi-lasta en R estas efektivigita en la plej simpla maniero uzante fermojn). Antaŭe, estis postulate eksplicite konverti R-tabelojn en numpy-tabelojn ene de la iteratoro, sed la nuna versio de la pako keras faras ĝin mem.

La iteratoro por trejnado kaj validumado de datumoj montriĝis kiel sekvas:

Iteratoro por trejnado kaj validumado de datumoj

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

La funkcio prenas kiel enigo variablon kun konekto al la datumbazo, la nombroj da linioj uzataj, la nombro da klasoj, aro grandeco, skalo (scale = 1 respondas al bildigo de bildoj de 256x256 pikseloj, scale = 0.5 — 128x128 pikseloj), kolorindikilo (color = FALSE specifas bildigon en grizskalo kiam uzata color = TRUE ĉiu streko estas desegnita en nova koloro) kaj antaŭpretiga indikilo por retoj antaŭtrejnitaj sur imagenet. Ĉi-lasta necesas por skali pikselvalorojn de la intervalo [0, 1] al la intervalo [-1, 1], kiu estis uzata dum trejnado de la provizita. keras modeloj.

La ekstera funkcio enhavas argumentan tipon kontroladon, tabelon data.table kun hazarde miksitaj linionombroj de samples_index kaj aroj, nombrilo kaj maksimuma nombro da aroj, same kiel SQL-esprimo por malŝarĝi datumojn el la datumbazo. Aldone, ni difinis rapidan analogon de la funkcio ene keras::to_categorical(). Ni uzis preskaŭ ĉiujn datumojn por trejnado, lasante duonan procenton por validigo, do la epoka grandeco estis limigita de la parametro steps_per_epoch kiam vokita keras::fit_generator(), kaj la kondiĉo if (i > max_i) nur funkciis por la validiga iteratoro.

En la interna funkcio, vicindeksoj estas prenitaj por la sekva aro, rekordoj estas malŝarĝitaj el la datumbazo kun la bata nombrilo pliiĝanta, JSON-analizo (funkcio cpp_process_json_vector(), skribita en C++) kaj kreante tabelojn respondajn al bildoj. Tiam unu-varmaj vektoroj kun klasaj etikedoj estas kreitaj, tabeloj kun pikselaj valoroj kaj etikedoj estas kombinitaj en liston, kiu estas la revena valoro. Por akceli laboron, ni uzis la kreadon de indeksoj en tabeloj data.table kaj modifo per la ligilo - sen ĉi tiuj pakaĵoj "blatoj" datumo.tablo Estas sufiĉe malfacile imagi labori efike kun iu ajn grava kvanto da datumoj en R.

La rezultoj de rapidecmezuradoj sur tekkomputilo Core i5 estas jenaj:

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)

Rapida Desegna Doodle-Rekono: kiel amikiĝi kun R, C++ kaj neŭralaj retoj

Se vi havas sufiĉan kvanton da RAM, vi povas serioze akceli la funkciadon de la datumbazo transdonante ĝin al ĉi tiu sama RAM (32 GB sufiĉas por nia tasko). En Linukso, la sekcio estas muntita defaŭlte /dev/shm, okupante ĝis duono de la RAM-kapacito. Vi povas reliefigi pli per redaktado /etc/fstabakiri rekordon kiel tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Nepre rekomencu kaj kontrolu la rezulton rulante la komandon df -h.

La iteratoro por testaj datumoj aspektas multe pli simpla, ĉar la testadatumaro konvenas tute en RAM:

Iteratoro por testaj datumoj

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. Elekto de modela arkitekturo

La unua arkitekturo uzata estis mobilenet v1, kies trajtoj estas diskutitaj en ĉi tio mesaĝo. Ĝi estas inkluzivita kiel normo keras kaj, sekve, disponeblas en la samnoma pakaĵo por R. Sed kiam oni provis uzi ĝin kun unu-kanalaj bildoj, stranga afero rezultis: la eniga tensoro devas ĉiam havi la dimension. (batch, height, width, 3), tio estas, la nombro da kanaloj ne povas esti ŝanĝita. Ne ekzistas tia limigo en Python, do ni rapidis kaj skribis nian propran efektivigon de ĉi tiu arkitekturo, sekvante la originalan artikolon (sen la forlaso kiu estas en la keras-versio):

Mobilenet v1 arkitekturo

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

La malavantaĝoj de ĉi tiu aliro estas evidentaj. Mi volas testi multajn modelojn, sed male, mi ne volas reverki ĉiun arkitekturon permane. Ni ankaŭ estis senigitaj de la ŝanco uzi la pezojn de modeloj antaŭtrejnitaj sur imagenet. Kiel kutime, studi la dokumentaron helpis. Funkcio get_config() permesas al vi ricevi priskribon de la modelo en formo taŭga por redaktado (base_model_conf$layers - regula R-listo), kaj la funkcio from_config() elfaras la inversan konvertiĝon al modelobjekto:

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)

Nun ne estas malfacile skribi universalan funkcion por akiri iun ajn el la provizitaj keras modeloj kun aŭ sen pezoj trejnitaj sur imagenet:

Funkcio por ŝarĝi pretajn arkitekturojn

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

Dum uzado de unu-kanalaj bildoj, neniuj antaŭtrejnitaj pezoj estas uzitaj. Ĉi tio povus esti riparita: uzante la funkcion get_weights() akiru la modelajn pezojn en la formo de listo de R-tabeloj, ŝanĝu la dimension de la unua elemento de ĉi tiu listo (prenante unu kolorkanalon aŭ averaĝante ĉiujn tri), kaj poste ŝarĝu la pezojn reen en la modelon kun la funkcio. set_weights(). Ni neniam aldonis ĉi tiun funkcion, ĉar en ĉi tiu etapo jam estis klare, ke estas pli produktive labori kun koloraj bildoj.

Ni faris la plej multajn el la eksperimentoj uzante mobilenet-versiojn 1 kaj 2, same kiel resnet34. Pli modernaj arkitekturoj kiel ekzemple SE-ResNeXt rezultis bone en tiu konkurado. Bedaŭrinde, ni ne havis pretajn efektivigojn je nia dispono, kaj ni ne skribis nian propran (sed ni certe skribos).

5. Parametrigo de skriptoj

Por komforto, ĉiu kodo por komenci trejnadon estis desegnita kiel ununura skripto, parametrigita uzante docopt kiel sekvas:

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)

Pako docopt reprezentas la efektivigon http://docopt.org/ por R. Kun ĝia helpo, skriptoj estas lanĉitaj per simplaj komandoj kiel Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, se dosiero train_nn.R estas plenumebla (ĉi tiu komando komencos trejni la modelon resnet50 sur trikoloraj bildoj je 128x128 pikseloj, la datumbazo devas troviĝi en la dosierujo /home/andrey/doodle_db). Vi povas aldoni lernrapidecon, optimumigan tipon kaj ajnajn aliajn agordeblajn parametrojn al la listo. En la procezo de preparado de la publikigado, rezultis, ke la arkitekturo mobilenet_v2 el la nuna versio keras en R-uzo ne povas pro ŝanĝoj ne konsiderataj en la R-pakaĵo, ni atendas ke ili riparu ĝin.

Ĉi tiu aliro ebligis signife akceli eksperimentojn kun malsamaj modeloj kompare kun la pli tradicia lanĉo de skriptoj en RStudio (ni notas la pakaĵon kiel ebla alternativo tfruns). Sed la ĉefa avantaĝo estas la kapablo facile administri la lanĉon de skriptoj en Docker aŭ simple sur la servilo, sen instali RStudio por tio.

6. Dokerigo de skriptoj

Ni uzis Docker por certigi porteblon de la medio por trejnado de modeloj inter teamanoj kaj por rapida disfaldo en la nubo. Vi povas komenci konatiĝi kun ĉi tiu ilo, kiu estas relative nekutima por R-programisto, kun ĉi tio serio de eldonaĵoj aŭ videokurso.

Docker permesas vin kaj krei viajn proprajn bildojn de nulo kaj uzi aliajn bildojn kiel bazon por krei viajn proprajn. Analizinte la disponeblajn opciojn, ni alvenis al la konkludo, ke instali NVIDIA, CUDA+cuDNN-ŝoforojn kaj Python-bibliotekojn estas sufiĉe volumena parto de la bildo, kaj ni decidis preni la oficialan bildon kiel bazon. tensorflow/tensorflow:1.12.0-gpu, aldonante la necesajn R-pakaĵojn tie.

La fina docker-dosiero aspektis jene:

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

Por oportuno, la pakaĵoj uzitaj estis metitaj en variablojn; la plejparto de la skribitaj skribaĵoj estas kopiita ene de la ujoj dum kunigo. Ni ankaŭ ŝanĝis la komandan ŝelon al /bin/bash por facileco de uzado de enhavo /etc/os-release. Ĉi tio evitis la bezonon specifi la OS-version en la kodo.

Aldone, malgranda bash-skripto estis skribita, kiu ebligas al vi lanĉi ujon kun diversaj komandoj. Ekzemple, ĉi tiuj povus esti skriptoj por trejnado de neŭralaj retoj, kiuj antaŭe estis metitaj ene de la ujo, aŭ komanda ŝelo por senararigado kaj monitorado de la funkciado de la ujo:

Skripto por lanĉi la ujon

#!/bin/sh

DBDIR=${PWD}/db
LOGSDIR=${PWD}/logs
MODELDIR=${PWD}/models
DATADIR=${PWD}/data
ARGS="--runtime=nvidia --rm -v ${DBDIR}:/db -v ${LOGSDIR}:/app/logs -v ${MODELDIR}:/app/models -v ${DATADIR}:/app/data"

if [ -z "$1" ]; then
    CMD="Rscript /app/train_nn.R"
elif [ "$1" = "bash" ]; then
    ARGS="${ARGS} -ti"
else
    CMD="Rscript /app/train_nn.R $@"
fi

docker run ${ARGS} doodles-tf ${CMD}

Se ĉi tiu bash-skripto estas rulita sen parametroj, la skripto estos vokita en la ujo train_nn.R kun defaŭltaj valoroj; se la unua pozicia argumento estas "bash", tiam la ujo komenciĝos interage kun komanda ŝelo. En ĉiuj aliaj kazoj, la valoroj de poziciaj argumentoj estas anstataŭigitaj: CMD="Rscript /app/train_nn.R $@".

Indas noti, ke la dosierujoj kun fontaj datumoj kaj datumbazo, same kiel la dosierujo por konservi trejnitajn modelojn, estas muntitaj en la ujo de la gastiga sistemo, kio ebligas al vi aliri la rezultojn de la skriptoj sen nenecesaj manipuladoj.

7. Uzante plurajn GPU-ojn en Google Cloud

Unu el la karakterizaĵoj de la konkurso estis la tre bruaj datumoj (vidu la titolbildon, pruntitan de @Leigh.plt de ODS-slack). Grandaj aroj helpas kontraŭbatali ĉi tion, kaj post eksperimentoj en komputilo kun 1 GPU, ni decidis majstri trejnajn modelojn sur pluraj GPU-oj en la nubo. Uzita GoogleCloud (bona gvidilo al la bazaĵoj) pro la granda elekto de disponeblaj agordoj, akcepteblaj prezoj kaj $300 gratifiko. Pro avideco, mi mendis 4xV100 ekzemplon kun SSD kaj tuno da RAM, kaj tio estis granda eraro. Tia maŝino rapide manĝas monon; vi povas rompi eksperimenti sen pruvita dukto. Por edukaj celoj, estas pli bone preni la K80. Sed la granda kvanto da RAM estis utila - la nuba SSD ne impresis pri sia agado, do la datumbazo estis translokigita al dev/shm.

Plej interesa estas la kodfragmento respondeca por uzi plurajn GPU-ojn. Unue, la modelo estas kreita sur la CPU uzante kuntekstan administranton, same kiel en 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
  )
})

Tiam la nekompilita (tio gravas) modelo estas kopiita al donita nombro da disponeblaj GPU-oj, kaj nur post tio ĝi estas kompilita:

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

La klasika tekniko frostigi ĉiujn tavolojn krom la lasta, trejni la lastan tavolon, malfrosti kaj retrejni la tutan modelon por pluraj GPU-oj ne povus esti efektivigita.

Trejnado estis monitorita sen uzo. tensortabulo, limigante nin por registri protokolojn kaj konservi modelojn kun informaj nomoj post ĉiu epoko:

Revokoj

# Шаблон имени файла лога
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. Anstata konkludo

Kelkaj problemoj kiujn ni renkontis ankoraŭ ne estis venkitaj:

  • в keras ne ekzistas preta funkcio por aŭtomate serĉi la optimuman lernprocenton (analoga lr_finder en biblioteko rapida.ai); Kun iom da peno, eblas porti triajn efektivigojn al R, ekzemple, ĉi tio;
  • kiel konsekvenco de la antaŭa punkto, ne eblis elekti la ĝustan trejnan rapidon uzante plurajn GPU-ojn;
  • mankas modernaj neŭralaj retaj arkitekturoj, precipe tiuj antaŭtrejnitaj ĉe imagenet;
  • nenia ciklopolitiko kaj diskriminaciaj lernprocentoj (kosinuso-kalado estis laŭ nia peto efektivigita, Dankon skeydan).

Kiajn utilajn aferojn oni lernis el ĉi tiu konkurso:

  • Sur relative malalta potenco aparataro, vi povas labori kun decaj (multfoje la grandeco de RAM) volumoj de datumoj sen doloro. Plasta sako datumo.tablo ŝparas memoron pro surloka modifo de tabeloj, kio evitas kopii ilin, kaj kiam ĝuste uzataj, ĝiaj kapabloj preskaŭ ĉiam montras la plej altan rapidecon inter ĉiuj iloj konataj de ni por skriptlingvoj. Konservado de datumoj en datumbazo permesas vin, en multaj kazoj, tute ne pensi pri la bezono elpremi la tutan datumaron en RAM.
  • Malrapidaj funkcioj en R povas esti anstataŭigitaj per rapidaj en C++ uzante la pakaĵon Rcpp. Se krom uzi RcppFadenoRcppParalelo, ni ricevas transplatformajn multfadenajn efektivigojn, do ne necesas paraleligi la kodon ĉe la R-nivelo.
  • Pako Rcpp povas esti uzata sen serioza scio pri C++, la bezonata minimumo estas skizita tie. Kapodosieroj por kelkaj bonegaj C-bibliotekoj kiel xtensor havebla sur CRAN, tio estas, infrastrukturo estas formita por la efektivigo de projektoj kiuj integras pretan alt-efikecan C++-kodon en R. Plia oportuno estas sintaksa reliefigo kaj senmova C++-koda analizilo en RStudio.
  • docopt permesas al vi ruli memstarajn skriptojn kun parametroj. Ĉi tio estas oportuna por uzi en fora servilo, inkl. sub docker. En RStudio, estas maloportune fari multajn horojn da eksperimentoj kun trejnado de neŭralaj retoj, kaj instali la IDE sur la servilo mem ne ĉiam estas pravigita.
  • Docker certigas kodan porteblon kaj reprodukteblecon de rezultoj inter programistoj kun malsamaj versioj de la OS kaj bibliotekoj, kaj ankaŭ facilecon de ekzekuto sur serviloj. Vi povas lanĉi la tutan trejnan dukton per nur unu komando.
  • Google Cloud estas buĝeta maniero eksperimenti pri multekosta aparataro, sed vi devas zorge elekti agordojn.
  • Mezuri la rapidecon de individuaj kodfragmentoj estas tre utila, precipe kiam oni kombinas R kaj C++, kaj kun la pakaĵo. benko - ankaŭ tre facila.

Ĝenerale ĉi tiu sperto estis tre rekompenca kaj ni daŭre laboras por solvi kelkajn el la levitaj problemoj.

fonto: www.habr.com

Aldoni komenton