Pangakuan Doodle Gancang: kumaha carana ngadamel babaturan sareng R, C ++ sareng jaringan saraf

Pangakuan Doodle Gancang: kumaha carana ngadamel babaturan sareng R, C ++ sareng jaringan saraf

Héy Habr!

Musim gugur kamari, Kaggle ngayakeun kompetisi pikeun ngagolongkeun gambar anu digambar ku tangan, Quick Draw Doodle Recognition, dimana, diantarana, tim élmuwan Sunda milu: Artem Klevtsova, Manajer Philippa и Andrey Ogurtsov. Kami moal ngajelaskeun kompetisi sacara rinci; éta parantos dilakukeun dina publikasi panganyarna.

waktos Ieu teu dianggo kaluar kalawan tani medali, tapi loba pangalaman berharga ieu diala, jadi Abdi hoyong ngabejaan masarakat ngeunaan sababaraha hal paling metot jeung mangpaat dina Kagle sarta dina karya sapopoé. Di antara jejer dibahas: hirup susah tanpa OpenCV, JSON parsing (conto ieu nalungtik integrasi kode C++ kana skrip atawa bungkusan dina basa Sunda ngagunakeun Rcpp), parameterisasi skrip sareng dockerization tina solusi ahir. Sadaya kode tina pesen dina bentuk anu cocog pikeun palaksanaan sayogi di repositories.

eusi:

  1. Éfisién muka data tina CSV kana MonetDB
  2. Nyiapkeun bets
  3. Iterators pikeun unloading bets tina database
  4. Milih hiji Arsitéktur Modél
  5. Parameterisasi naskah
  6. Dockerization tina Aksara
  7. Ngagunakeun sababaraha GPUs on Google Cloud
  8. Gantina kacindekan

1. Efisién muka data tina CSV kana database MonetDB

Data dina kompetisi ieu disadiakeun teu dina bentuk gambar siap-dijieun, tapi dina bentuk 340 file CSV (hiji file pikeun tiap kelas) ngandung JSONs kalawan koordinat titik. Ku cara ngahubungkeun titik-titik ieu jeung garis, urang meunang gambar ahir ukuran 256x256 piksel. Ogé pikeun unggal rékaman aya labél anu nunjukkeun naha gambar éta leres-leres dikenal ku classifier anu dianggo nalika set data dikumpulkeun, kode dua hurup nagara tempat panulis gambar, identifier unik, stempel waktos. sareng nami kelas anu cocog sareng nami file. Versi saderhana tina data aslina beuratna 7.4 GB dina arsip sareng sakitar 20 GB saatos ngabongkar, data lengkep saatos ngabongkar nyandak 240 GB. Panitia mastikeun yén duanana versi ngahasilkeun gambar anu sami, hartosna versi lengkepna kaleuleuwihan. Dina naon waé, nyimpen 50 juta gambar dina file grafis atanapi dina bentuk arrays langsung dianggap teu nguntungkeun, sareng kami mutuskeun pikeun ngahijikeun sadaya file CSV tina arsip. train_simplified.zip kana database kalawan generasi saterusna gambar tina ukuran diperlukeun "dina laleur" pikeun tiap bets.

Hiji sistem well-dibuktikeun dipilih salaku DBMS MonetDB, nyaéta palaksanaan pikeun Sunda salaku pakét MonetDBLite. Paket kalebet vérsi pangladén databés anu dipasang sareng ngamungkinkeun anjeun nyandak pangladén langsung tina sési R sareng damel sareng éta di dinya. Nyiptakeun pangkalan data sareng nyambungkeunana dilakukeun ku hiji paréntah:

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

Urang kedah ngadamel dua tabel: hiji kanggo sadaya data, anu sanésna kanggo inpormasi jasa ngeunaan file anu diunduh (mangpaat upami aya anu salah sareng prosésna kedah diteruskeun saatos diunduh sababaraha file):

Nyieun tabél

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

Cara panggancangna pikeun ngamuat data kana pangkalan data nyaéta nyalin langsung file CSV nganggo paréntah SQL COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTdimana tablename - ngaran tabel sarta path - jalur ka file. Bari gawé bareng arsip, éta kapanggih yén palaksanaan diwangun-di unzip dina basa Sunda henteu dianggo leres sareng sajumlah file tina arsip, janten kami nganggo sistem unzip (ngagunakeun parameter getOption("unzip")).

Fungsi pikeun nulis kana database

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

Upami anjeun kedah ngarobih méja sateuacan nyerat kana pangkalan data, éta cekap pikeun lulus dina argumen preprocess fungsi anu bakal ngarobah data.

Kode pikeun ngamuat data sacara berurutan kana pangkalan data:

Nulis data kana database

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

Waktu loading data bisa rupa-rupa gumantung kana ciri speed drive dipaké. Dina kasus urang, maca sareng nyerat dina hiji SSD atanapi tina flash drive (file sumber) ka SSD (DB) nyandak kirang ti 10 menit.

Butuh sababaraha detik deui pikeun nyieun kolom kalawan labél kelas integer jeung kolom indéks (ORDERED INDEX) kalawan nomer garis nu observasi bakal sampel nalika nyieun bets:

Nyiptakeun Kolom sareng Indéks Tambahan

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

Pikeun ngajawab masalah nyieun bets on laleur, urang diperlukeun pikeun ngahontal speed maksimum extracting baris acak tina tabél. doodles. Pikeun ieu kami nganggo 3 trik. Anu mimiti nyaéta pikeun ngirangan dimensi tina jinis anu nyimpen ID observasi. Dina set data aslina, tipe diperlukeun pikeun nyimpen ID nyaeta bigint, tapi jumlah observasi ngamungkinkeun pikeun nyocogkeun identifiers maranéhanana, sarua jeung nomer ordinal, kana tipe int. Pilarian langkung gancang dina hal ieu. Trik kadua nya éta ngagunakeun ORDERED INDEX - urang sumping ka kaputusan ieu émpiris, sanggeus ngaliwatan sagala aya pilihan. Katilu nya éta ngagunakeun queries parameterized. Intina metode nyaéta ngalaksanakeun paréntah sakali PREPARE kalayan ngagunakeun éksprési anu disiapkeun saenggeusna nalika nyiptakeun sakumpulan patarosan tina jinis anu sami, tapi kanyataanna aya kaunggulan dibandingkeun sareng anu saderhana. SELECT tétéla dina rentang kasalahan statistik.

Prosés unggah data meakeun henteu langkung ti 450 MB RAM. Nyaéta, pendekatan anu dijelaskeun ngamungkinkeun anjeun pikeun mindahkeun set data anu beuratna puluhan gigabyte dina ampir sadaya hardware anggaran, kalebet sababaraha alat papan tunggal, anu lumayan keren.

Sadaya anu tetep nyaéta pikeun ngukur kagancangan retrieving (acak) data sareng meunteun skala nalika nyandak sampling bets tina ukuran anu béda:

Patokan database

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)

Pangakuan Doodle Gancang: kumaha carana ngadamel babaturan sareng R, C ++ sareng jaringan saraf

2. Nyiapkeun bets

Sakabéh prosés persiapan angkatan diwangun ku léngkah-léngkah ieu:

  1. Parsing sababaraha JSON anu ngandung vektor string sareng koordinat titik.
  2. Ngagambar garis berwarna dumasar kana koordinat titik dina hiji gambar tina ukuran diperlukeun (Contona, 256 × 256 atawa 128 × 128).
  3. Ngarobah gambar hasilna kana tensor a.

Salaku bagian tina kompetisi diantara kernels Python, masalah ieu direngsekeun utamana ngagunakeun OpenCV. Salah sahiji analog anu pangbasajanna sareng paling atra dina basa Sunda bakal katingali sapertos kieu:

Nerapkeun JSON kana Konversi Tensor dina 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)
}

Ngagambar dilakukeun nganggo alat R standar sareng disimpen ka PNG samentawis anu disimpen dina RAM (dina Linux, diréktori Sunda samentawis aya dina diréktori. /tmp, dipasang dina RAM). file ieu lajeng dibaca salaku Asép Sunandar Sunarya tilu diménsi kalawan angka mimitian ti 0 nepi ka 1. Ieu penting sabab BMP leuwih konvensional bakal dibaca kana Asép Sunandar Sunarya atah jeung kode warna hex.

Hayu urang nguji hasilna:

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

Pangakuan Doodle Gancang: kumaha carana ngadamel babaturan sareng R, C ++ sareng jaringan saraf

bets sorangan bakal kabentuk saperti kieu:

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

Palaksanaan ieu sigana suboptimal pikeun urang, saprak formasi bets badag nyokot hiji waktu indecently lila, sarta kami mutuskeun pikeun ngamangpaatkeun pangalaman kolega urang ku ngagunakeun perpustakaan kuat. OpenCV. Dina waktos éta henteu acan aya pakét anu siap pikeun R (aya ayeuna), janten palaksanaan minimal tina fungsionalitas anu diperyogikeun ditulis dina C ++ sareng integrasi kana kode R nganggo Rcpp.

Pikeun ngajawab masalah, pakét sareng perpustakaan ieu dianggo:

  1. OpenCV pikeun gawé bareng gambar sareng ngagambar garis. Dipaké pustaka sistem anu tos dipasang sareng file header, ogé tautan dinamis.

  2. xtensor pikeun gawé bareng arrays multidimensional na tensors. Kami nganggo file lulugu anu kalebet dina pakét R anu nami anu sami. Perpustakaan ngidinan Anjeun pikeun digawekeun ku arrays multidimensional, duanana dina urutan utama baris jeung kolom urutan utama.

  3. ndjson pikeun parsing JSON. Perpustakaan ieu dianggo dina xtensor otomatis lamun éta hadir dina proyék.

  4. RcppThread pikeun ngatur ngolah multi-threaded tina vektor ti JSON. Dipaké file lulugu disadiakeun ku pakét ieu. Ti leuwih populér RcppParalel Bungkusan, diantarana, gaduh mékanisme interupsi loop anu diwangun.

Eta sia noting yén xtensor Tétéla janten anugerah: salian kanyataan yén éta ngagaduhan pungsionalitas éksténsif sareng kinerja anu luhur, pamekarna tétéla rada responsif sareng ngajawab patarosan gancang sareng detil. Kalayan pitulungna, éta mungkin pikeun nerapkeun transformasi matriks OpenCV kana tensor xtensor, kitu ogé cara pikeun ngagabungkeun tensor gambar 3 diménsi kana tensor 4 diménsi tina dimensi anu leres (angkatan sorangan).

Bahan pangajaran Rcpp, xtensor jeung 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

Pikeun nyusun file anu nganggo file sistem sareng tautan dinamis sareng perpustakaan anu dipasang dina sistem, kami nganggo mékanisme plugin anu dilaksanakeun dina bungkusan. Rcpp. Pikeun milarian jalur sareng bandéra sacara otomatis, kami nganggo utilitas Linux anu populér pkg-config.

Palaksanaan plugin Rcpp pikeun ngagunakeun perpustakaan OpenCV

Rcpp::registerPlugin("opencv", function() {
  # Возможные названия пакета
  pkg_config_name <- c("opencv", "opencv4")
  # Бинарный файл утилиты pkg-config
  pkg_config_bin <- Sys.which("pkg-config")
  # Проврека наличия утилиты в системе
  checkmate::assert_file_exists(pkg_config_bin, access = "x")
  # Проверка наличия файла настроек OpenCV для pkg-config
  check <- sapply(pkg_config_name, 
                  function(pkg) system(paste(pkg_config_bin, pkg)))
  if (all(check != 0)) {
    stop("OpenCV config for the pkg-config not found", call. = FALSE)
  }

  pkg_config_name <- pkg_config_name[check == 0]
  list(env = list(
    PKG_CXXFLAGS = system(paste(pkg_config_bin, "--cflags", pkg_config_name), 
                          intern = TRUE),
    PKG_LIBS = system(paste(pkg_config_bin, "--libs", pkg_config_name), 
                      intern = TRUE)
  ))
})

Salaku hasil tina operasi plugin, nilai-nilai ieu bakal diganti nalika prosés kompilasi:

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"

Kodeu palaksanaan pikeun parsing JSON sarta generating bets pikeun transmisi ka model dibikeun handapeun spoiler nu. Kahiji, tambahkeun diréktori proyék lokal pikeun milarian file lulugu (diperyogikeun pikeun ndjson):

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

Palaksanaan JSON kana konversi tensor dina 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;
}

Kode ieu kedah disimpen dina file src/cv_xt.cpp sarta compile kalawan paréntah Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); ogé diperlukeun pikeun digawé nlohmann/json.hpp ti gudang. Kode dibagi kana sababaraha fungsi:

  • to_xt - fungsi citakan pikeun ngarobih matriks gambar (cv::Mat) kana tensor xt::xtensor;

  • parse_json - fungsi parses string JSON, extracts koordinat titik, packing kana vektor a;

  • ocv_draw_lines - ti vektor hasilna titik, draws garis multi-warna;

  • process - ngagabungkeun fungsi di luhur sareng ogé nambihan kamampuan pikeun skala gambar anu hasilna;

  • cpp_process_json_str - wrapper leuwih fungsi process, anu ngékspor hasil ka obyék R (array multidimensional);

  • cpp_process_json_vector - wrapper leuwih fungsi cpp_process_json_str, nu ngidinan Anjeun pikeun ngolah vektor string dina modeu multi-threaded.

Pikeun ngagambar garis multi-warna, modél warna HSV dipaké, dituturkeun ku konversi kana RGB. Hayu urang nguji hasilna:

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

Pangakuan Doodle Gancang: kumaha carana ngadamel babaturan sareng R, C ++ sareng jaringan saraf
Babandingan laju palaksanaan di Sunda jeung 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") 

Pangakuan Doodle Gancang: kumaha carana ngadamel babaturan sareng R, C ++ sareng jaringan saraf

Sakumaha anjeun tiasa tingali, kanaékan speed tétéla jadi pohara signifikan, sarta teu mungkin keur nyekel nepi ka C ++ kode ku parallelizing kode Sunda.

3. Iterators pikeun unloading bets tina database

Urang Sunda boga reputasi well-deserved pikeun ngolah data nu fits dina RAM, bari Python leuwih dicirikeun ku ngolah data iterative, ngamungkinkeun Anjeun pikeun gampang jeung alami nerapkeun kaluar-of-core itungan (itungan ngagunakeun memori éksternal). Conto klasik sareng relevan pikeun urang dina konteks masalah anu dijelaskeun nyaéta jaringan saraf jero anu dilatih ku metode turunan gradién kalayan perkiraan gradién dina unggal léngkah nganggo sabagian leutik observasi, atanapi mini-angkatan.

Kerangka pembelajaran jero anu ditulis dina Python ngagaduhan kelas khusus anu ngalaksanakeun iterator dumasar kana data: tabél, gambar dina polder, format binér, jsb. Dina basa Sunda urang bisa ngamangpaatkeun sagala fitur perpustakaan Python keras kalawan rupa backends na ngagunakeun pakét tina ngaran anu sarua, anu dina gilirannana jalan dina luhureun bungkusan nyarioskeun deui. Panungtungan pantes artikel panjang misah; eta mah ngan saukur ngidinan Anjeun pikeun ngajalankeun kode Python ti Sunda, tapi ogé ngidinan Anjeun pikeun mindahkeun objék antara R jeung Python sesi, otomatis nedunan sagala conversions tipe perlu.

Urang nyingkirkeun kabutuhan pikeun nyimpen sadaya data dina RAM ku ngagunakeun MonetDBLite, sadaya karya "jaringan saraf" bakal dilakukeun ku kode asli dina Python, urang ngan ukur kedah nyerat iterator dina data, sabab teu aya anu siap. pikeun kaayaan kitu boh di Sunda atawa Python. Dasarna ngan ukur dua sarat pikeun éta: éta kedah uih deui bets dina loop anu henteu terbatas sareng nyimpen kaayaanana antara iterations (anu terakhir dina basa Sunda dilaksanakeun ku cara anu paling sederhana nganggo panutupanana). Saméméhna, éta diperlukeun pikeun eksplisit ngarobah arrays R kana arrays numpy jero iterator, tapi versi ayeuna tina pakét. keras ngalakukeun eta sorangan.

Iterator pikeun latihan sareng validasi data tétéla sapertos kieu:

Iterator pikeun latihan sareng data validasi

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

Fungsina nyandak salaku input variabel sareng sambungan kana pangkalan data, jumlah garis anu dianggo, jumlah kelas, ukuran batch, skala (scale = 1 pakait sareng rendering gambar 256x256 piksel, scale = 0.5 — 128x128 piksel), indikator warna (color = FALSE nangtukeun rendering dina grayscale lamun dipaké color = TRUE unggal stroke digambar dina warna anyar) jeung indikator preprocessing pikeun jaringan tos dilatih dina imagenet. Anu terakhir diperyogikeun pikeun skala nilai piksel tina interval [0, 1] ka interval [-1, 1], anu dianggo nalika ngalatih anu disayogikeun. keras modél.

Fungsi éksternal ngandung mariksa tipe argumen, méja data.table kalawan acak dicampur nomer garis ti samples_index jeung nomer bets, counter jeung jumlah maksimum bets, kitu ogé hiji éksprési SQL pikeun unloading data tina database. Salaku tambahan, kami netepkeun analog gancang tina fungsi di jero keras::to_categorical(). Urang dipaké ampir kabéh data pikeun latihan, ninggalkeun satengah persén pikeun validasi, jadi ukuran epoch ieu diwatesan ku parameter. steps_per_epoch lamun disebut keras::fit_generator(), jeung kaayaan if (i > max_i) ngan digawé pikeun iterator validasi.

Dina fungsi internal, indéks baris dicandak pikeun angkatan salajengna, rékaman dibongkar tina pangkalan data sareng konter bets ningkat, parsing JSON (fungsi cpp_process_json_vector(), ditulis dina C++) jeung nyieun arrays pakait jeung gambar. Lajeng vektor hiji-panas kalawan labél kelas dijieun, arrays kalawan nilai piksel na labél digabungkeun kana daptar, nu nilai balik. Pikeun ngagancangkeun pagawéan, kami nganggo nyiptakeun indéks dina tabél data.table sareng modifikasi ngalangkungan tautan - tanpa pakét "chip" ieu data.tabel Hese pisan ngabayangkeun damel sacara efektif sareng jumlah data anu signifikan dina basa Sunda.

Hasil pangukuran laju dina laptop Core i5 nyaéta kieu:

patokan Iterator

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)

Pangakuan Doodle Gancang: kumaha carana ngadamel babaturan sareng R, C ++ sareng jaringan saraf

Upami anjeun gaduh jumlah RAM anu cekap, anjeun tiasa sacara serius nyepetkeun operasi database ku nransferkeunana ka RAM anu sami (32 GB cekap pikeun tugas urang). Dina Linux, partisi dipasang sacara standar /dev/shm, Nempatan nepi ka satengah kapasitas RAM. Anjeun tiasa nyorot langkung seueur ku ngédit /etc/fstabpikeun meunangkeun catetan kawas tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Pastikeun pikeun reboot sareng pariksa hasilna ku ngajalankeun paréntah df -h.

Iterator pikeun data tés katingalina langkung saderhana, sabab set data tés pas sadayana kana RAM:

Iterator pikeun data tés

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. Pamilihan arsitéktur modél

Arsitéktur munggaran dipaké nyaéta mobilenet v1, fitur anu dibahas dina ieu pesen. Ieu kaasup salaku standar keras jeung, sasuai, geus sadia dina pakét tina ngaran anu sarua pikeun R. Tapi lamun nyobian ngagunakeun eta kalawan gambar single-channel, hiji hal aneh tétéla: tensor input kudu salawasna mibanda dimensi. (batch, height, width, 3), nyaeta, jumlah saluran teu bisa dirobah. Henteu aya watesan sapertos kitu dina Python, janten urang buru-buru sareng nyerat palaksanaan arsitéktur ieu nyalira, nuturkeun tulisan asli (tanpa dropout anu aya dina versi keras):

Arsitéktur Mobilenet v1

library(keras)

top_3_categorical_accuracy <- custom_metric(
    name = "top_3_categorical_accuracy",
    metric_fn = function(y_true, y_pred) {
         metric_top_k_categorical_accuracy(y_true, y_pred, k = 3)
    }
)

layer_sep_conv_bn <- function(object, 
                              filters,
                              alpha = 1,
                              depth_multiplier = 1,
                              strides = c(2, 2)) {

  # NB! depth_multiplier !=  resolution multiplier
  # https://github.com/keras-team/keras/issues/10349

  layer_depthwise_conv_2d(
    object = object,
    kernel_size = c(3, 3), 
    strides = strides,
    padding = "same",
    depth_multiplier = depth_multiplier
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() %>%
  layer_conv_2d(
    filters = filters * alpha,
    kernel_size = c(1, 1), 
    strides = c(1, 1)
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() 
}

get_mobilenet_v1 <- function(input_shape = c(224, 224, 1),
                             num_classes = 340,
                             alpha = 1,
                             depth_multiplier = 1,
                             optimizer = optimizer_adam(lr = 0.002),
                             loss = "categorical_crossentropy",
                             metrics = c("categorical_crossentropy",
                                         top_3_categorical_accuracy)) {

  inputs <- layer_input(shape = input_shape)

  outputs <- inputs %>%
    layer_conv_2d(filters = 32, kernel_size = c(3, 3), strides = c(2, 2), padding = "same") %>%
    layer_batch_normalization() %>% 
    layer_activation_relu() %>%
    layer_sep_conv_bn(filters = 64, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 128, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 128, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 256, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 256, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 1024, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 1024, strides = c(1, 1)) %>%
    layer_global_average_pooling_2d() %>%
    layer_dense(units = num_classes) %>%
    layer_activation_softmax()

    model <- keras_model(
      inputs = inputs,
      outputs = outputs
    )

    model %>% compile(
      optimizer = optimizer,
      loss = loss,
      metrics = metrics
    )

    return(model)
}

Karugian tina pendekatan ieu atra. Abdi hoyong nguji seueur model, tapi sabalikna, kuring henteu hoyong nyerat ulang unggal arsitéktur sacara manual. Kami ogé dicabut kasempetan pikeun ngagunakeun beurat modél anu tos dilatih dina imagenet. Sakumaha biasa, diajar dokuméntasi ngabantosan. Fungsi get_config() ngidinan Anjeun pikeun meunangkeun pedaran model dina formulir cocog pikeun ngédit (base_model_conf$layers - daptar Sunda biasa), sarta fungsi from_config() ngalakukeun konvérsi sabalikna ka obyék modél:

base_model_conf <- get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- from_config(base_model_conf)

Ayeuna teu hese nulis fungsi universal pikeun ménta salah sahiji disadiakeun keras model nganggo atanapi tanpa beurat dilatih dina imagenet:

Fungsi pikeun loading arsitéktur siap-dijieun

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

Lamun maké gambar saluran tunggal, teu dipaké beurat pretrained. Ieu bisa dibenerkeun: ngagunakeun fungsi get_weights() Kéngingkeun beurat modél dina bentuk daptar arrays R, robih diménsi unsur mimiti daptar ieu (ku nyandak hiji saluran warna atanapi rata-rata tilu), teras muatkeun beurat deui kana modél kalayan fungsina. set_weights(). Kami henteu pernah nambihan fungsionalitas ieu, sabab dina tahap ieu parantos jelas yén éta langkung produktif damel sareng gambar warna.

Urang ngalaksanakeun lolobana percobaan ngagunakeun mobilenet versi 1 jeung 2, kitu ogé resnet34. Arsitéktur anu langkung modern sapertos SE-ResNeXt ngalaksanakeun saé dina kompetisi ieu. Hanjakalna, urang henteu ngagaduhan palaksanaan anu siap-siap, sareng urang henteu nyerat nyalira (tapi urang pasti bakal nyerat).

5. Parameterisasi naskah

Pikeun genah, sadaya kode pikeun ngamimitian latihan dirancang salaku hiji naskah, parameterized ngagunakeun docopt saperti kieu:

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)

Pakét docopt ngagambarkeun palaksanaan http://docopt.org/ pikeun R. Kalayan pitulung na, Aksara dibuka kalayan paréntah basajan kawas Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db atawa ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, lamun file train_nn.R tiasa dieksekusi (paréntah ieu bakal ngamimitian ngalatih modél resnet50 dina gambar tilu warna ukuran 128x128 piksel, pangkalan data kedah aya dina polder /home/andrey/doodle_db). Anjeun tiasa nambihan kacepetan diajar, jinis pangoptimal, sareng parameter anu tiasa disaluyukeun kana daptar. Dina prosés nyiapkeun publikasi, tétéla yén arsitéktur mobilenet_v2 ti versi ayeuna keras dina pamakéan Sunda teu kedah alatan parobahan teu dicokot kana akun dina pakét Sunda, urang ngantosan aranjeunna pikeun ngalereskeun eta.

Pendekatan ieu ngamungkinkeun pikeun nyepetkeun ékspérimén sacara signifikan sareng modél anu béda dibandingkeun sareng peluncuran skrip anu langkung tradisional di RStudio (urang perhatikeun pakét salaku alternatif anu mungkin. tfruns). Tapi kauntungan utama nyaéta kamampuan pikeun gampang ngatur peluncuran skrip di Docker atanapi ngan saukur dina server, tanpa masang RStudio pikeun ieu.

6. Dockerization tina naskah

Kami nganggo Docker pikeun mastikeun portabilitas lingkungan pikeun modél latihan antara anggota tim sareng panyebaran gancang dina méga. Anjeun bisa ngamimitian meunang acquainted jeung alat ieu, nu relatif mahiwal pikeun programmer Sunda, jeung ieu runtuyan publikasi atawa kursus video.

Docker ngidinan Anjeun pikeun duanana nyieun gambar sorangan ti scratch tur ngagunakeun gambar séjén salaku dadasar pikeun nyieun sorangan. Nalika nganalisa pilihan anu sayogi, kami dugi ka kacindekan yén masang NVIDIA, supir CUDA + cuDNN sareng perpustakaan Python mangrupikeun bagian gambar anu lumayan ageung, sareng kami mutuskeun nyandak gambar resmi salaku dasar. tensorflow/tensorflow:1.12.0-gpu, nambahkeun bungkusan Sunda perlu aya.

File docker ahir sapertos kieu:

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

Pikeun genah, bungkusan anu dianggo dilebetkeun kana variabel; sabagéan ageung naskah tinulis disalin di jero wadah nalika ngarakit. Urang ogé ngarobah cangkang paréntah pikeun /bin/bash pikeun betah pamakéan eusi /etc/os-release. Ieu ngahindarkeun kabutuhan pikeun nangtukeun versi OS dina kode.

Salaku tambahan, skrip bash leutik ditulis anu ngamungkinkeun anjeun ngaluncurkeun wadah kalayan sagala rupa paréntah. Salaku conto, ieu tiasa janten skrip pikeun ngalatih jaringan saraf anu sateuacana disimpen di jero wadahna, atanapi cangkang paréntah pikeun debugging sareng ngawaskeun operasi wadahna:

Skrip pikeun ngaluncurkeun wadahna

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

Upami skrip bash ieu dijalankeun tanpa parameter, naskah bakal disebat di jero wadahna train_nn.R kalawan nilai standar; lamun argumen posisional kahiji nyaeta "bash", lajeng wadahna bakal ngamimitian interaktif jeung cangkang paréntah. Dina sakabeh kasus sejen, nilai argumen posisional diganti: CMD="Rscript /app/train_nn.R $@".

Perhatos yén diréktori kalayan data sumber sareng pangkalan data, ogé diréktori pikeun nyimpen modél anu dilatih, dipasang di jero wadah tina sistem host, anu ngamungkinkeun anjeun ngaksés hasil naskah tanpa manipulasi anu teu perlu.

7. Ngagunakeun sababaraha GPUs on Google Cloud

Salah sahiji fitur kompetisi éta data pisan ribut (tingali gambar judul, injeuman ti @Leigh.plt ti ODS slack). bets badag mantuan merangan ieu, sarta sanggeus percobaan dina PC kalawan 1 GPU, urang mutuskeun hiji master model latihan dina sababaraha GPUs dina awan. GoogleCloud dipaké (pituduh alus pikeun dasar) alatan Pilihan badag tina konfigurasi sadia, harga lumrah tur $ 300 bonus. Kusabab karanjingan, kuring maréntahkeun conto 4xV100 kalayan SSD sareng ton RAM, sareng éta mangrupikeun kasalahan anu ageung. Mesin sapertos kitu ngahakan artos gancang; anjeun tiasa nyobian ékspérimén tanpa pipa anu kabuktian. Pikeun tujuan atikan, éta hadé nyandak K80. Tapi jumlah RAM anu ageung tiasa dianggo - SSD awan henteu ngingetkeun kinerjana, janten pangkalan data ditransferkeun ka dev/shm.

Kapentingan greatest nyaeta sempalan kode jawab pamakéan sababaraha GPUs. Kahiji, modél didamel dina CPU nganggo manajer kontéks, sapertos dina 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
  )
})

Lajeng model uncompiled (ieu penting) disalin ka sajumlah tina GPUs sadia, sarta ngan sanggeus éta disusun:

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

Téhnik klasik tina katirisan sadaya lapisan kecuali anu terakhir, ngalatih lapisan anu terakhir, ngabebaskeun sareng ngalatih deui sadayana modél pikeun sababaraha GPU henteu tiasa dilaksanakeun.

Latihan diawaskeun tanpa dianggo. tensorboard, ngawatesan diri pikeun ngarékam log sareng nyimpen modél nganggo nami informatif saatos unggal jaman:

Telepon balik

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

Sajumlah masalah anu urang hadapi teu acan kaungkulan:

  • в keras teu aya fungsi siap-siap pikeun otomatis milarian tingkat diajar optimal (analog lr_finder dina perpustakaan gancang.ai); Kalawan sababaraha usaha, kasebut nyaéta dimungkinkeun pikeun port implementations pihak katilu ka R, contona,. ieu;
  • salaku konsekuensi tina titik saméméhna, teu mungkin pikeun milih speed latihan bener lamun ngagunakeun sababaraha GPUs;
  • aya kakurangan arsitéktur jaringan saraf modern, khususna anu tos dilatih dina imagenet;
  • euweuh kawijakan hiji siklus jeung ongkos learning diskriminatif (kosinus annealing éta dina pamundut urang dilaksanakeun, Hatur nuhun skeydan).

Naon mangpaat anu diajar tina kompetisi ieu:

  • Dina hardware rélatif low-daya, anjeun tiasa dianggo kalayan santun (sababaraha kali ukuran RAM) volume data tanpa nyeri. Kantong plastik data.tabel ngaheéat mémori alatan modifikasi dina-tempat tabel, nu avoids nyalin aranjeunna, sarta lamun dipaké leres, kamampuhan na ampir sok demonstrate speed pangluhurna diantara sakabeh parabot dipikawanoh kami pikeun basa scripting. Nyimpen data dina database a ngidinan Anjeun, dina loba kasus, teu mikir pisan ngeunaan kudu squeeze sakabéh dataset kana RAM.
  • Fungsi slow dina basa Sunda bisa diganti ku gancang dina C ++ ngagunakeun iket Rcpp. Lamun salian ngagunakeun RcppThread atawa RcppParalel, urang meunang palaksanaan multi-threaded cross-platform, jadi teu kudu parallelize kode dina tingkat R.
  • Bungkusan Rcpp bisa dipaké tanpa pangaweruh serius ngeunaan C ++, minimum diperlukeun outlined di dieu. file lulugu pikeun sajumlah cool C-perpustakaan kawas xtensor sayogi dina CRAN, nyaéta, infrastruktur dibentuk pikeun palaksanaan proyék anu ngahijikeun kode C ++ kinerja tinggi siap-siap kana R. Genah tambahan nyaéta panyorot sintaksis sareng analisa kode C ++ statik dina RStudio.
  • docopt ngidinan Anjeun pikeun ngajalankeun Aksara timer ngandung parameter. Ieu merenah pikeun pamakéan dina server jauh, incl. handapeun docker. Dina RStudio, teu merenah pikeun ngalakukeun sababaraha jam percobaan kalayan latihan jaringan saraf, sarta masang IDE dina server sorangan teu salawasna diyakinkeun.
  • Docker ensures portability kode jeung reproducibility hasil antara pamekar jeung versi béda tina OS jeung perpustakaan, kitu ogé betah palaksanaan on server. Anjeun tiasa ngaluncurkeun sadaya jalur latihan kalayan ngan ukur hiji paréntah.
  • Google Cloud mangrupikeun cara anu ramah anggaran pikeun ékspérimén dina hardware anu mahal, tapi anjeun kedah milih konfigurasi sacara saksama.
  • Ngukur laju fragmén kode individu pohara kapaké, khususna nalika ngagabungkeun R sareng C ++, sareng sareng pakét. bangku - ogé pohara gampang.

Gemblengna pangalaman ieu pisan rewarding sarta kami terus digawé pikeun ngabéréskeun sababaraha masalah diangkat.

sumber: www.habr.com

Tambahkeun komentar