Quick Draw Doodle Recognition: R, C++ və neyron şəbəkələri ilə necə dostluq etmək olar

Quick Draw Doodle Recognition: R, C++ və neyron şəbəkələri ilə necə dostluq etmək olar

Hey Habr!

Keçən ilin payızında Kaggle əl ilə çəkilmiş şəkilləri təsnif etmək üçün Quick Draw Doodle Recognition müsabiqəsinə ev sahibliyi etdi, burada digərləri ilə yanaşı, R-alimlərdən ibarət bir qrup iştirak etdi: Artem Klevtsova, Philippa meneceri и Andrey Ogurtsov. Müsabiqəni təfərrüatlı şəkildə təsvir etməyəcəyik; bu, artıq həyata keçirilib son nəşr.

Bu dəfə medal əkinçiliyi ilə nəticə vermədi, lakin çoxlu dəyərli təcrübə qazanıldı, buna görə də cəmiyyətə Kagle və gündəlik işdə bir sıra ən maraqlı və faydalı şeylər haqqında danışmaq istərdim. Müzakirə olunan mövzular arasında: onsuz çətin həyat OpenCV, JSON təhlili (bu nümunələr C++ kodunun R-də skriptlərə və ya paketlərə inteqrasiyasını araşdırır. Rcpp), skriptlərin parametrləşdirilməsi və yekun həllin dokerləşdirilməsi. İcra üçün uyğun bir formada olan mesajdan bütün kodlar mövcuddur depolar.

İçindekiler:

  1. CSV-dən MonetDB-yə məlumatları səmərəli şəkildə yükləyin
  2. Partiyaların hazırlanması
  3. Verilənlər bazasından partiyaları boşaltmaq üçün iteratorlar
  4. Model memarlığının seçilməsi
  5. Skript parametrləşdirilməsi
  6. Skriptlərin dokerləşdirilməsi
  7. Google Buludda çoxlu GPU-dan istifadə
  8. Bunun əvəzinə bir nəticəyə

1. CSV-dən məlumatları MonetDB verilənlər bazasına səmərəli şəkildə yükləyin

Bu müsabiqədəki məlumatlar hazır şəkillər şəklində deyil, nöqtə koordinatları olan JSON-ları ehtiva edən 340 CSV faylı (hər sinif üçün bir fayl) şəklində təqdim olunur. Bu nöqtələri xətlərlə birləşdirərək, 256x256 piksel ölçülü yekun şəkil alırıq. Həmçinin hər bir qeyd üçün məlumat toplusunun toplandığı zaman istifadə olunan təsnifatlandırıcı tərəfindən şəklin düzgün tanınıb-tanınmadığını göstərən etiket, şəkil müəllifinin yaşadığı ölkənin iki hərfli kodu, unikal identifikator, vaxt möhürü var. və fayl adına uyğun gələn sinif adı. Orijinal məlumatın sadələşdirilmiş versiyası arxivdə 7.4 GB və paketdən çıxarıldıqdan sonra təxminən 20 GB ağırlığında, paketdən çıxarıldıqdan sonra tam məlumat 240 GB tutur. Təşkilatçılar hər iki versiyanın eyni çertyojları əks etdirməsini təmin etdilər, yəni tam versiya lazımsız idi. İstənilən halda, 50 milyon şəkli qrafik fayllarda və ya massivlər şəklində saxlamaq dərhal zərərli hesab edildi və biz arxivdən bütün CSV fayllarını birləşdirməyə qərar verdik. train_simplified.zip hər bir partiya üçün "tez" tələb olunan ölçülü şəkillərin sonrakı yaradılması ilə verilənlər bazasına.

DBMS kimi yaxşı sübut edilmiş sistem seçilmişdir MonetDB, yəni paket kimi R üçün tətbiq MonetDBLite. Paket verilənlər bazası serverinin quraşdırılmış versiyasını ehtiva edir və serveri birbaşa R sessiyasından götürməyə və orada onunla işləməyə imkan verir. Verilənlər bazası yaratmaq və ona qoşulmaq bir əmrlə həyata keçirilir:

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

Biz iki cədvəl yaratmalıyıq: biri bütün məlumatlar üçün, digəri yüklənmiş fayllar haqqında xidmət məlumatı üçün (bir şey səhv olarsa və bir neçə fayl endirdikdən sonra proses davam etdirilməlidirsə faydalıdır):

Cədvəllərin yaradılması

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

Verilənləri verilənlər bazasına yükləməyin ən sürətli yolu SQL - əmrindən istifadə edərək birbaşa CSV fayllarının surətini çıxarmaq idi COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTHara tablename - masa adı və path - fayla gedən yol. Arxivlə işləyərkən daxili tətbiqin olduğu aşkar edildi unzip R-də arxivdəki bir sıra fayllarla düzgün işləmir, ona görə də sistemdən istifadə etdik unzip (parametrdən istifadə etməklə getOption("unzip")).

Verilənlər bazasına yazmaq funksiyası

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

Əgər cədvəli verilənlər bazasına yazmadan əvvəl onu çevirmək lazımdırsa, arqumentə keçmək kifayətdir preprocess verilənləri çevirəcək funksiya.

Verilənlərin verilənlər bazasına ardıcıl yüklənməsi üçün kod:

Məlumat bazasına məlumatların yazılması

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

Məlumat yükləmə müddəti istifadə olunan sürücünün sürət xüsusiyyətlərindən asılı olaraq dəyişə bilər. Bizim vəziyyətimizdə bir SSD daxilində və ya flash sürücüdən (mənbə faylı) SSD-yə (DB) oxumaq və yazmaq 10 dəqiqədən az vaxt aparır.

Tam sinif etiketi və indeks sütunu olan sütun yaratmaq bir neçə saniyə çəkir (ORDERED INDEX) partiyaların yaradılması zamanı müşahidələrin nümunə götürüləcəyi sətir nömrələri ilə:

Əlavə Sütunların və İndeksin yaradılması

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

Tez bir toplu yaratmaq problemini həll etmək üçün cədvəldən təsadüfi cərgələrin çıxarılmasının maksimum sürətinə nail olmaq lazım idi. doodles. Bunun üçün 3 fənddən istifadə etdik. Birincisi, müşahidə identifikatorunu saxlayan növün ölçüsünü azaltmaq idi. Orijinal məlumat dəstində ID-ni saxlamaq üçün tələb olunan növdür bigint, lakin müşahidələrin sayı onların sıra nömrəsinə bərabər olan identifikatorlarını tipə uyğunlaşdırmağa imkan verir. int. Bu vəziyyətdə axtarış daha sürətli olur. İkinci hiylə istifadə etmək idi ORDERED INDEX — biz bu qərara bütün mövcud olanları keçərək empirik olaraq gəldik варианты. Üçüncüsü, parametrləşdirilmiş sorğulardan istifadə etmək idi. Metodun mahiyyəti əmri bir dəfə yerinə yetirməkdir PREPARE eyni tipli bir dəstə sorğu yaratarkən hazırlanmış ifadənin sonrakı istifadəsi ilə, lakin əslində sadə ilə müqayisədə bir üstünlük var SELECT statistik xəta daxilində olduğu ortaya çıxdı.

Məlumatların yüklənməsi prosesi 450 MB-dan çox RAM istehlak etmir. Yəni təsvir edilən yanaşma, demək olar ki, hər hansı bir büdcə aparatında, o cümlədən bəzi tək lövhəli cihazlarda onlarla gigabayt ağırlığında olan məlumat dəstlərini köçürməyə imkan verir ki, bu da olduqca gözəldir.

Qalan tək şey (təsadüfi) məlumatların əldə edilməsi sürətini ölçmək və müxtəlif ölçülü partiyalardan nümunə götürərkən miqyaslaşdırmanı qiymətləndirməkdir:

Verilənlər bazası benchmark

library(ggplot2)

set.seed(0)
# Подключение к базе данных
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))

# Функция для подготовки запроса на стороне сервера
prep_sql <- function(batch_size) {
  sql <- sprintf("PREPARE SELECT id FROM doodles WHERE id IN (%s)",
                 paste(rep("?", batch_size), collapse = ","))
  res <- DBI::dbSendQuery(con, sql)
  return(res)
}

# Функция для извлечения данных
fetch_data <- function(rs, batch_size) {
  ids <- sample(seq_len(n), batch_size)
  res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
  return(res)
}

# Проведение замера
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    rs <- prep_sql(batch_size)
    bench::mark(
      fetch_data(rs, batch_size),
      min_iterations = 50L
    )
  }
)
# Параметры бенчмарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   batch_size      min   median      max `itr/sec` total_time n_itr
#        <dbl> <bch:tm> <bch:tm> <bch:tm>     <dbl>   <bch:tm> <int>
# 1         16   23.6ms  54.02ms  93.43ms     18.8        2.6s    49
# 2         32     38ms  84.83ms 151.55ms     11.4       4.29s    49
# 3         64   63.3ms 175.54ms 248.94ms     5.85       8.54s    50
# 4        128   83.2ms 341.52ms 496.24ms     3.00      16.69s    50
# 5        256  232.8ms 653.21ms 847.44ms     1.58      31.66s    50
# 6        512  784.6ms    1.41s    1.98s     0.740       1.1m    49
# 7       1024  681.7ms    2.72s    4.06s     0.377      2.16m    49

ggplot(res_bench, aes(x = factor(batch_size), y = median, group = 1)) +
  geom_point() +
  geom_line() +
  ylab("median time, s") +
  theme_minimal()

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: R, C++ və neyron şəbəkələri ilə necə dostluq etmək olar

2. Partiyaların hazırlanması

Bütün partiyanın hazırlanması prosesi aşağıdakı addımlardan ibarətdir:

  1. Nöqtələrin koordinatları olan sətirlərin vektorlarını ehtiva edən bir neçə JSON-un təhlili.
  2. Tələb olunan ölçüdə (məsələn, 256×256 və ya 128×128) şəkil üzərində nöqtələrin koordinatları əsasında rəngli xətlərin çəkilməsi.
  3. Yaranan şəkillərin tenzor çevrilməsi.

Python ləpələri arasında rəqabət çərçivəsində problem ilk növbədə istifadə etməklə həll edilib OpenCV. R-də ən sadə və ən bariz analoqlardan biri belə görünür:

R-də JSON-dan Tensor Çevrilməsinin həyata keçirilməsi

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

Rəsm standart R alətlərindən istifadə etməklə həyata keçirilir və RAM-da saxlanılan müvəqqəti PNG-də saxlanılır (Linux-da müvəqqəti R qovluqları kataloqda yerləşir) /tmp, RAM-a quraşdırılmışdır). Bu fayl daha sonra 0-dan 1-ə qədər rəqəmləri olan üçölçülü massiv kimi oxunur. Bu vacibdir, çünki daha adi BMP hex rəng kodları ilə xam massivdə oxunacaq.

Nəticəni yoxlayaq:

zip_file <- file.path("data", "train_simplified.zip")
csv_file <- "cat.csv"
unzip(zip_file, files = csv_file, exdir = tempdir(), 
      junkpaths = TRUE, unzip = getOption("unzip"))
tmp_data <- data.table::fread(file.path(tempdir(), csv_file), sep = ",", 
                              select = "drawing", nrows = 10000)
arr <- r_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256   3
plot(magick::image_read(arr))

Quick Draw Doodle Recognition: R, C++ və neyron şəbəkələri ilə necə dostluq etmək olar

Dəstənin özü aşağıdakı kimi formalaşacaq:

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

Bu tətbiq bizim üçün qeyri-optimal görünürdü, çünki böyük partiyaların formalaşması çox uzun müddət tələb edir və biz güclü kitabxanadan istifadə edərək həmkarlarımızın təcrübəsindən yararlanmaq qərarına gəldik. OpenCV. O dövrdə R üçün hazır paket yox idi (indi yoxdur), buna görə də tələb olunan funksionallığın minimal tətbiqi C++ dilində R koduna inteqrasiya ilə yazılmışdır. Rcpp.

Problemi həll etmək üçün aşağıdakı paketlər və kitabxanalardan istifadə edilmişdir:

  1. OpenCV şəkillər və rəsm xətləri ilə işləmək üçün. İstifadə olunmuş əvvəlcədən quraşdırılmış sistem kitabxanaları və başlıq faylları, həmçinin dinamik əlaqələndirmə.

  2. xtensor çoxölçülü massivlər və tensorlarla işləmək üçün. Eyni adlı R paketinə daxil olan başlıq fayllarından istifadə etdik. Kitabxana çoxölçülü massivlərlə həm əsas sıra, həm də sütun əsas qaydada işləməyə imkan verir.

  3. ndjson JSON-u təhlil etmək üçün. Bu kitabxanada istifadə olunur xtensor layihədə varsa avtomatik olaraq.

  4. RcppThread JSON-dan vektorun çox yivli işlənməsini təşkil etmək üçün. Bu paketin təqdim etdiyi başlıq fayllarından istifadə edilmişdir. Daha məşhur olandan RcppParalel Paket, digər şeylər arasında, daxili döngə kəsmə mexanizminə malikdir.

Qeyd edək ki, xtensor bir ilahi oldu: geniş funksionallıq və yüksək performansa malik olması ilə yanaşı, onun tərtibatçıları olduqca həssas oldular və suallara operativ və ətraflı cavab verdilər. Onların köməyi ilə OpenCV matrislərinin xtensor tensorlarına çevrilməsini həyata keçirmək, həmçinin 3 ölçülü təsvir tensorlarını düzgün ölçülü 4 ölçülü tensorda birləşdirmək yolu (partiyanın özü) mümkün olmuşdur.

Rcpp, xtensor və RcppThread öyrənmək üçün materiallar

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

Sistem fayllarından və sistemdə quraşdırılmış kitabxanalarla dinamik əlaqədən istifadə edən faylları tərtib etmək üçün paketdə tətbiq olunan plagin mexanizmindən istifadə etdik. Rcpp. Yolları və bayraqları avtomatik tapmaq üçün biz məşhur Linux yardım proqramından istifadə etdik pkg-konfiqurasiya.

OpenCV kitabxanasından istifadə üçün Rcpp plagininin tətbiqi

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

Pluginin işləməsi nəticəsində tərtib prosesi zamanı aşağıdakı dəyərlər əvəz olunacaq:

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"

JSON-u təhlil etmək və modelə ötürmək üçün toplu yaratmaq üçün tətbiq kodu spoyler altında verilmişdir. Əvvəlcə başlıq fayllarını axtarmaq üçün yerli layihə kataloqu əlavə edin (ndjson üçün lazımdır):

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

C++ dilində JSON-un tenzor çevrilməsinə tətbiqi

// [[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;
}

Bu kod faylda yerləşdirilməlidir src/cv_xt.cpp və əmri ilə tərtib edin Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); iş üçün də tələb olunur nlohmann/json.hpp haqqında anbar. Kod bir neçə funksiyaya bölünür:

  • to_xt — şəkil matrisini çevirmək üçün şablon funksiyası (cv::Mat) tenzor üçün xt::xtensor;

  • parse_json — funksiya JSON sətirini təhlil edir, nöqtələrin koordinatlarını çıxarır, onları vektora yığır;

  • ocv_draw_lines — xalların nəticə vektorundan çoxrəngli xətlər çəkir;

  • process — yuxarıda göstərilən funksiyaları birləşdirir və həmçinin yaranan təsviri miqyaslaşdırmaq imkanı əlavə edir;

  • cpp_process_json_str - funksiyanın üzərinə sarğı process, nəticəni R-obyektinə ixrac edən (çoxölçülü massiv);

  • cpp_process_json_vector - funksiyanın üzərinə sarğı cpp_process_json_str, bu, çox yivli rejimdə sətir vektorunu emal etməyə imkan verir.

Çoxrəngli xətləri çəkmək üçün HSV rəng modelindən istifadə edilib, sonra RGB-yə çevrildi. Nəticəni yoxlayaq:

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

Quick Draw Doodle Recognition: R, C++ və neyron şəbəkələri ilə necə dostluq etmək olar
R və C++ dillərində tətbiqetmə sürətinin müqayisəsi

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

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

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

res_bench[, cols]

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

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

Quick Draw Doodle Recognition: R, C++ və neyron şəbəkələri ilə necə dostluq etmək olar

Göründüyü kimi, sürət artımı çox əhəmiyyətli oldu və R kodunu paralelləşdirməklə C++ kodunu tutmaq mümkün deyil.

3. Verilənlər bazasından partiyaların boşaldılması üçün iteratorlar

R operativ yaddaşa uyğun gələn məlumatların işlənməsi üzrə layiqli reputasiyaya malikdir, Python isə daha çox iterativ məlumat emalı ilə xarakterizə olunur ki, bu da əsasdan kənar hesablamaları (xarici yaddaşdan istifadə etməklə hesablamaları) asanlıqla və təbii şəkildə həyata keçirməyə imkan verir. Təsvir edilən problem kontekstində bizim üçün klassik və uyğun bir nümunə, müşahidələrin kiçik bir hissəsindən və ya mini-batch istifadə edərək, hər addımda gradientin yaxınlaşması ilə gradient eniş üsulu ilə öyrədilmiş dərin neyron şəbəkələridir.

Python-da yazılmış dərin öyrənmə çərçivələrində verilənlər əsasında iteratorları həyata keçirən xüsusi siniflər var: cədvəllər, qovluqlardakı şəkillər, ikili formatlar və s. Siz hazır variantlardan istifadə edə və ya konkret tapşırıqlar üçün özünüz yaza bilərsiniz. R-də biz Python kitabxanasının bütün xüsusiyyətlərindən yararlana bilərik keras öz növbəsində paketin üstündə işləyən eyni adlı paketdən istifadə edərək müxtəlif arxa ucları ilə retikulyasiya. Sonuncu ayrıca uzun məqaləyə layiqdir; o, nəinki R-dən Python kodunu işə salmağa imkan verir, həm də bütün lazımi tip çevrilmələrini avtomatik həyata keçirərək R və Python seansları arasında obyektləri ötürməyə imkan verir.

MonetDBLite istifadə edərək bütün məlumatları RAM-da saxlamaq ehtiyacından xilas olduq, bütün “neyroşəbəkə” işləri Python-da orijinal kodla yerinə yetiriləcək, sadəcə olaraq verilənlərin üzərində iterator yazmalıyıq, çünki hazır heç nə yoxdur. R və ya Python-da belə bir vəziyyət üçün. Bunun üçün mahiyyətcə yalnız iki tələb var: o, sonsuz bir döngədə partiyaları qaytarmalı və iterasiyalar arasında vəziyyətini saxlamalıdır (R-də sonuncu bağlanmalardan istifadə edərək ən sadə şəkildə həyata keçirilir). Əvvəllər, R massivlərini iterator daxilində açıq şəkildə numpy massivlərə çevirmək tələb olunurdu, lakin paketin cari versiyası keras özü edir.

Təlim və doğrulama məlumatları üçün iterator aşağıdakı kimi oldu:

Təlim və doğrulama məlumatları üçün iterator

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

Funksiya verilənlər bazası ilə əlaqəsi olan dəyişəni, istifadə olunan sətirlərin sayını, siniflərin sayını, partiyanın ölçüsünü, miqyasını daxil edir.scale = 1 256x256 piksel təsvirlərin göstərilməsinə uyğundur, scale = 0.5 — 128x128 piksel), rəng göstəricisi (color = FALSE istifadə edildikdə boz rəngdə göstərilməsini müəyyən edir color = TRUE hər bir vuruş yeni rəngdə çəkilir) və imagenet-də əvvəlcədən öyrədilmiş şəbəkələr üçün əvvəlcədən emal göstəricisi. Sonuncu, piksel dəyərlərini [0, 1] intervalından [-1, 1] intervalına qədər miqyaslaşdırmaq üçün lazımdır, bu da təqdim olunanları öyrədərkən istifadə olunur. keras modellər.

Xarici funksiyada arqument növünün yoxlanılması, cədvəl var data.table -dən təsadüfi qarışıq sətir nömrələri ilə samples_index və partiya nömrələri, sayğac və partiyaların maksimum sayı, həmçinin verilənlər bazasından məlumatların boşaldılması üçün SQL ifadəsi. Bundan əlavə, biz daxildə funksiyanın sürətli analoqunu təyin etdik keras::to_categorical(). Təlim üçün demək olar ki, bütün məlumatları istifadə etdik, yoxlama üçün yarım faiz buraxdıq, buna görə də dövr ölçüsü parametrlə məhdudlaşdı. steps_per_epoch çağıranda keras::fit_generator(), və şərt if (i > max_i) yalnız doğrulama iteratoru üçün işləmişdir.

Daxili funksiyada cərgə indeksləri növbəti partiya üçün götürülür, qeydlər toplu sayğacın artması ilə verilənlər bazasından boşaldılır, JSON təhlili (funksiya) cpp_process_json_vector(), C++ dilində yazılmışdır) və şəkillərə uyğun massivlərin yaradılması. Sonra sinif etiketləri olan bir isti vektorlar yaradılır, piksel dəyərləri və etiketləri olan massivlər siyahıya birləşdirilir ki, bu da qaytarılan dəyərdir. İşi sürətləndirmək üçün cədvəllərdə indekslərin yaradılmasından istifadə etdik data.table və link vasitəsilə modifikasiya - bu "çiplər" paketi olmadan məlumat cədvəli R-də hər hansı əhəmiyyətli miqdarda məlumatla effektiv işləməyi təsəvvür etmək olduqca çətindir.

Core i5 noutbukunda sürət ölçmələrinin nəticələri aşağıdakı kimidir:

İterator benchmark

library(Rcpp)
library(keras)
library(ggplot2)

source("utils/rcpp.R")
source("utils/keras_iterator.R")

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

ind <- seq_len(DBI::dbGetQuery(con, "SELECT count(*) FROM doodles")[[1L]])
num_classes <- DBI::dbGetQuery(con, "SELECT max(label_int) + 1 FROM doodles")[[1L]]

# Индексы для обучающей выборки
train_ind <- sample(ind, floor(length(ind) * 0.995))
# Индексы для проверочной выборки
val_ind <- ind[-train_ind]
rm(ind)
# Коэффициент масштаба
scale <- 0.5

# Проведение замера
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    it1 <- train_generator(
      db_connection = con,
      samples_index = train_ind,
      num_classes = num_classes,
      batch_size = batch_size,
      scale = scale
    )
    bench::mark(
      it1(),
      min_iterations = 50L
    )
  }
)
# Параметры бенчмарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   batch_size      min   median      max `itr/sec` total_time n_itr
#        <dbl> <bch:tm> <bch:tm> <bch:tm>     <dbl>   <bch:tm> <int>
# 1         16     25ms  64.36ms   92.2ms     15.9       3.09s    49
# 2         32   48.4ms 118.13ms 197.24ms     8.17       5.88s    48
# 3         64   69.3ms 117.93ms 181.14ms     8.57       5.83s    50
# 4        128  157.2ms 240.74ms 503.87ms     3.85      12.71s    49
# 5        256  359.3ms 613.52ms 988.73ms     1.54       30.5s    47
# 6        512  884.7ms    1.53s    2.07s     0.674      1.11m    45
# 7       1024     2.7s    3.83s    5.47s     0.261      2.81m    44

ggplot(res_bench, aes(x = factor(batch_size), y = median, group = 1)) +
    geom_point() +
    geom_line() +
    ylab("median time, s") +
    theme_minimal()

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: R, C++ və neyron şəbəkələri ilə necə dostluq etmək olar

Əgər kifayət qədər operativ yaddaşınız varsa, onu eyni RAM-a köçürərək verilənlər bazasının işini ciddi surətdə sürətləndirə bilərsiniz (32 GB bizim vəzifəmiz üçün kifayətdir). Linux-da bölmə standart olaraq quraşdırılmışdır /dev/shm, RAM tutumunun yarısına qədərini tutur. Redaktə etməklə daha çox vurğulaya bilərsiniz /etc/fstabkimi rekord əldə etmək tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Əmri işlətməklə yenidən başladın və nəticəni yoxladığınızdan əmin olun df -h.

Test məlumatları üçün iterator daha sadə görünür, çünki test məlumat dəsti tamamilə RAM-a uyğundur:

Test məlumatları üçün iterator

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. Model arxitekturasının seçilməsi

İlk istifadə edilən memarlıq idi mobilenet v1, xüsusiyyətlərindən bəhs olunur bu mesaj. Standart olaraq daxil edilmişdir keras və müvafiq olaraq, R üçün eyni adlı paketdə mövcuddur. Ancaq onu tək kanallı şəkillərlə istifadə etməyə çalışarkən, qəribə bir şey çıxdı: giriş tensoru həmişə ölçüyə sahib olmalıdır. (batch, height, width, 3), yəni kanalların sayı dəyişdirilə bilməz. Python-da belə bir məhdudiyyət yoxdur, buna görə də orijinal məqaləyə əməl edərək (keras versiyasında olan buraxılış olmadan) tələsdik və bu arxitekturanın öz tətbiqini yazdıq:

Mobilenet v1 arxitekturası

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

Bu yanaşmanın mənfi cəhətləri göz qabağındadır. Mən bir çox modeli sınamaq istəyirəm, amma əksinə, hər bir arxitekturanı əl ilə yenidən yazmaq istəmirəm. Biz həmçinin imagenet-də əvvəlcədən hazırlanmış modellərin çəkilərindən istifadə etmək imkanından məhrum olduq. Həmişə olduğu kimi, sənədləri öyrənmək kömək etdi. Funksiya get_config() redaktə üçün uyğun formada modelin təsvirini əldə etməyə imkan verir (base_model_conf$layers - müntəzəm R siyahısı) və funksiya from_config() model obyektinə tərs çevrilməni həyata keçirir:

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)

İndi verilən hər hansı birini əldə etmək üçün universal bir funksiya yazmaq çətin deyil keras imagenet-də hazırlanmış çəkisi olan və ya olmayan modellər:

Hazır arxitekturaların yüklənməsi funksiyası

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

Tək kanallı şəkillərdən istifadə edərkən əvvəlcədən hazırlanmış çəkilərdən istifadə edilmir. Bu düzəldilə bilər: funksiyadan istifadə etməklə get_weights() model çəkilərini R massivlərinin siyahısı şəklində əldə edin, bu siyahının birinci elementinin ölçüsünü dəyişdirin (bir rəng kanalı götürməklə və ya hər üçünü orta hesabla alaraq) və sonra funksiya ilə çəkiləri yenidən modelə yükləyin set_weights(). Biz bu funksionallığı heç vaxt əlavə etmədik, çünki bu mərhələdə rəngli şəkillərlə işləməyin daha məhsuldar olduğu artıq aydın idi.

Təcrübələrin çoxunu mobilenet versiyalarının 1 və 2, eləcə də resnet34-dən istifadə edərək həyata keçirdik. SE-ResNeXt kimi daha müasir arxitekturalar bu müsabiqədə yaxşı çıxış etdi. Təəssüf ki, bizim ixtiyarımızda hazır tətbiqetmələr yox idi və özümüz yazmadıq (amma mütləq yazacağıq).

5. Skriptlərin parametrləşdirilməsi

Rahatlıq üçün, təlimə başlamaq üçün bütün kodlar istifadə edərək parametrləşdirilmiş vahid bir skript kimi hazırlanmışdır dokopt belədir:

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)

Paketi dokopt həyata keçirilməsini ifadə edir http://docopt.org/ for R. Onun köməyi ilə skriptlər kimi sadə əmrlərlə işə salınır Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db və ya ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, əgər fayl train_nn.R icra edilə biləndir (bu əmr modeli öyrətməyə başlayacaq resnet50 128x128 piksel ölçülü üç rəngli şəkillərdə verilənlər bazası qovluqda yerləşməlidir. /home/andrey/doodle_db). Siyahıya öyrənmə sürəti, optimallaşdırıcı növü və hər hansı digər fərdiləşdirilə bilən parametrlər əlavə edə bilərsiniz. Nəşrin hazırlanması prosesində məlum oldu ki, memarlıq mobilenet_v2 cari versiyadan keras R istifadə edə bilməz R paketində nəzərə alınmayan dəyişikliklərə görə, onların düzəldilməsini gözləyirik.

Bu yanaşma RStudio-da skriptlərin daha ənənəvi işə salınması ilə müqayisədə fərqli modellərlə təcrübələri əhəmiyyətli dərəcədə sürətləndirməyə imkan verdi (paketi mümkün alternativ kimi qeyd edirik. tfruns). Ancaq əsas üstünlük, bunun üçün RStudio quraşdırmadan Docker-də və ya sadəcə serverdə skriptlərin işə salınmasını asanlıqla idarə etmək imkanıdır.

6. Skriptlərin dokerləşdirilməsi

Biz komanda üzvləri arasında modelləri öyrətmək və buludda sürətli yerləşdirmə üçün mühitin daşınmasını təmin etmək üçün Docker-dən istifadə etdik. R proqramçısı üçün nisbətən qeyri-adi olan bu alətlə tanış olmağa başlaya bilərsiniz bu nəşrlər seriyası və ya video kurs.

Docker sizə həm sıfırdan öz şəkillərinizi yaratmağa, həm də öz şəkillərinizi yaratmaq üçün əsas kimi digər şəkillərdən istifadə etməyə imkan verir. Mövcud variantları təhlil edərkən belə bir nəticəyə gəldik ki, NVIDIA, CUDA+cuDNN sürücüləri və Python kitabxanalarının quraşdırılması təsvirin kifayət qədər həcmli hissəsidir və biz rəsmi şəkli əsas götürməyə qərar verdik. tensorflow/tensorflow:1.12.0-gpu, orada lazımi R paketlərini əlavə edin.

Son docker faylı belə görünürdü:

Docker faylı

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

Rahatlıq üçün istifadə olunan paketlər dəyişənlərə yerləşdirildi; yazılı skriptlərin əsas hissəsi montaj zamanı konteynerlərin içərisinə köçürülür. Biz də əmr qabığını dəyişdirdik /bin/bash məzmundan istifadə rahatlığı üçün /etc/os-release. Bu, kodda OS versiyasını qeyd etmək ehtiyacının qarşısını aldı.

Bundan əlavə, müxtəlif əmrləri olan bir konteyneri işə salmağa imkan verən kiçik bir bash skripti yazılmışdır. Məsələn, bunlar əvvəllər konteynerin içərisinə yerləşdirilən neyron şəbəkələrini öyrətmək üçün skriptlər və ya konteynerin işini sazlamaq və monitorinq etmək üçün əmr qabığı ola bilər:

Konteyneri işə salmaq üçün skript

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

Bu bash skripti parametrlərsiz işlədilirsə, skript konteyner daxilində çağırılacaq train_nn.R standart dəyərlərlə; ilk mövqe arqumenti "bash" olarsa, konteyner interaktiv olaraq əmr qabığı ilə başlayacaq. Bütün digər hallarda mövqe arqumentlərinin dəyərləri əvəz olunur: CMD="Rscript /app/train_nn.R $@".

Qeyd etmək lazımdır ki, mənbə məlumatları və verilənlər bazası olan kataloqlar, eləcə də öyrədilmiş modelləri saxlamaq üçün kataloqlar host sistemindən konteynerin içərisinə quraşdırılmışdır ki, bu da lazımsız manipulyasiyalar olmadan skriptlərin nəticələrinə daxil olmağa imkan verir.

7. Google Cloud-da çoxlu GPU-dan istifadə

Müsabiqənin xüsusiyyətlərindən biri çox səs-küylü məlumatlar idi (ODS slack-dən @Leigh.plt-dan götürülmüş başlıq şəklinə baxın). Böyük partiyalar bununla mübarizə aparmağa kömək edir və 1 GPU-lu kompüterdə təcrübələrdən sonra biz buludda bir neçə GPU-da təlim modellərini mənimsəməyə qərar verdik. İstifadə olunmuş GoogleCloud (əsaslar üçün yaxşı bələdçi) mövcud konfiqurasiyaların böyük seçimi, münasib qiymətlər və 300$ bonus sayəsində. Acgözlükdən SSD və bir ton RAM ilə 4xV100 nümunəsi sifariş etdim və bu, böyük səhv idi. Belə bir maşın pulu tez yeyir, sübut edilmiş boru kəməri olmadan sınaqdan keçirə bilərsiniz. Təhsil məqsədləri üçün K80 almaq daha yaxşıdır. Ancaq böyük miqdarda RAM lazımlı oldu - bulud SSD performansı ilə heyran olmadı, buna görə verilənlər bazası köçürüldü. dev/shm.

Çoxlu GPU-ların istifadəsinə cavabdeh olan kod parçası ən çox maraq doğurur. Birincisi, model Python-da olduğu kimi kontekst menecerindən istifadə edərək CPU-da yaradılır:

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

Sonra tərtib edilməmiş (bu vacibdir) model müəyyən sayda mövcud GPU-ya kopyalanır və yalnız bundan sonra tərtib edilir:

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

Sonuncudan başqa bütün təbəqələrin dondurulması, sonuncu qatın öyrədilməsi, bütün modeli bir neçə GPU üçün dondurulması və yenidən hazırlanmasının klassik texnikası həyata keçirilə bilmədi.

Məşq istifadə edilmədən izlənildi. tensorboard, özümüzü qeydləri qeyd etmək və hər dövrdən sonra məlumatlandırıcı adlarla modelləri saxlamaqla məhdudlaşdırırıq:

Geri zənglər

# Шаблон имени файла лога
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. Nəticə əvəzinə

Qarşılaşdığımız bir sıra problemlər hələ də aradan qaldırılmamışdır:

  • в keras Optimal öyrənmə sürətini avtomatik axtarmaq üçün hazır funksiya yoxdur (analoq lr_finder kitabxanada fast.ai); Bəzi səylərlə üçüncü tərəf tətbiqlərini R-ə köçürmək mümkündür, məsələn, bu;
  • əvvəlki nöqtənin nəticəsi olaraq, bir neçə GPU-dan istifadə edərkən düzgün məşq sürətini seçmək mümkün olmadı;
  • müasir neyroşəbəkə arxitekturalarının, xüsusən imagenet-də əvvəlcədən öyrədilmiş arxitekturaların çatışmazlığı var;
  • heç bir dövr siyasəti və ayrı-seçkilikçi öyrənmə dərəcələri (kosinus tavlaması bizim istəyimizlə idi həyata keçirilən, təşəkkürlər skeydan).

Bu müsabiqədən hansı faydalı şeylər öyrənildi:

  • Nisbətən aşağı gücə malik aparatlarda, ağrısız layiqli (RAM ölçüsündən dəfələrlə böyük) həcmli məlumatlarla işləyə bilərsiniz. Plastik torba məlumat cədvəli cədvəllərin yerində modifikasiyası sayəsində yaddaşa qənaət edir ki, bu da onları kopyalamaqdan yayınır və düzgün istifadə edildikdə onun imkanları demək olar ki, həmişə skript dilləri üçün bizə məlum olan bütün alətlər arasında ən yüksək sürət nümayiş etdirir. Verilənlərin verilənlər bazasında saxlanması, bir çox hallarda, bütün məlumat dəstini RAM-a sıxmaq ehtiyacı barədə heç düşünməməyə imkan verir.
  • R-də yavaş funksiyalar paketdən istifadə edərək C++ dilində sürətli funksiyalarla əvəz edilə bilər Rcpp. İstifadəyə əlavə olaraq RcppThread və ya RcppParalel, biz çarpaz platformalı çox yivli tətbiqlər əldə edirik, ona görə də kodu R səviyyəsində paralelləşdirməyə ehtiyac yoxdur.
  • Paket Rcpp C++ dilini ciddi biliyi olmadan istifadə oluna bilər, tələb olunan minimum göstəricilər göstərilmişdir burada. kimi bir sıra gözəl C-kitabxanaları üçün başlıq faylları xtensor CRAN-da mövcuddur, yəni hazır yüksək performanslı C++ kodunu R-yə inteqrasiya edən layihələrin həyata keçirilməsi üçün infrastruktur formalaşdırılır. Əlavə rahatlıq sintaksisin vurğulanması və RStudio-da statik C++ kod analizatorudur.
  • dokopt parametrləri ilə müstəqil skriptləri işə salmağa imkan verir. Bu, uzaq bir serverdə istifadə üçün əlverişlidir, o cümlədən. doker altında. RStudio-da neyron şəbəkələri öyrətməklə çox saatlıq təcrübələr aparmaq əlverişsizdir və IDE-nin serverdə quraşdırılması həmişə özünü doğrultmur.
  • Docker, OS və kitabxanaların müxtəlif versiyaları olan tərtibatçılar arasında kodun daşınmasını və nəticələrin təkrar istehsalını, həmçinin serverlərdə icra asanlığını təmin edir. Siz yalnız bir əmrlə bütün təlim kəmərini işə sala bilərsiniz.
  • Google Cloud bahalı aparat üzərində təcrübə aparmaq üçün büdcəyə uyğun bir yoldur, lakin siz konfiqurasiyaları diqqətlə seçməlisiniz.
  • Fərdi kod fraqmentlərinin sürətinin ölçülməsi xüsusilə R və C++ və paketlə birləşdirildikdə çox faydalıdır. dəzgah - həm də çox asandır.

Bütövlükdə bu təcrübə çox faydalı oldu və biz qaldırılan məsələlərin bəzilərini həll etmək üçün işləməyə davam edirik.

Mənbə: www.habr.com

Добавить комментарий