Quick Draw Doodle Recognition: R, C++ va neyron tarmoqlari bilan qanday do'stlashish mumkin

Quick Draw Doodle Recognition: R, C++ va neyron tarmoqlari bilan qanday do'stlashish mumkin

Hey Xabr!

O'tgan yilning kuzida Kaggle qo'lda chizilgan rasmlarni tasniflash bo'yicha tanlovni o'tkazdi, Quick Draw Doodle Recognition, unda boshqalar qatorida R-olimlar jamoasi ishtirok etdi: Artem Klevtsova, Filippa menejeri и Andrey Ogurtsov. Biz raqobatni batafsil tasvirlamaymiz, bu allaqachon qilingan so'nggi nashr.

Bu safar medal yetishtirish bilan ish bermadi, lekin juda ko'p qimmatli tajriba to'plandi, shuning uchun men jamoaga Kagle va kundalik ishda bir qator eng qiziqarli va foydali narsalar haqida aytib bermoqchiman. Muhokama qilingan mavzular orasida: qiyin hayotsiz OpenCV, JSON tahlili (bu misollar C++ kodini R dagi skriptlar yoki paketlarga integratsiyalashuvini ko'rib chiqadi. Rcpp), skriptlarni parametrlashtirish va yakuniy yechimni dokerlashtirish. Bajarish uchun mos shakldagi xabardagi barcha kodlar mavjud omborlar.

Mundarija:

  1. CSV-dan MonetDB-ga ma'lumotlarni samarali yuklang
  2. To'plamlarni tayyorlash
  3. Ma'lumotlar bazasidan partiyalarni tushirish uchun iteratorlar
  4. Arxitektura namunasini tanlash
  5. Skript parametrlari
  6. Skriptlarni dokerlashtirish
  7. Google Cloud-da bir nechta GPU-lardan foydalanish
  8. Xulosa o'rniga

1. MonetDB ma'lumotlar bazasiga CSV dan ma'lumotlarni samarali yuklang

Ushbu tanlovdagi ma'lumotlar tayyor tasvirlar ko'rinishida emas, balki nuqta koordinatalari bo'lgan JSON-larni o'z ichiga olgan 340 CSV fayli (har bir sinf uchun bitta fayl) shaklida taqdim etiladi. Ushbu nuqtalarni chiziqlar bilan bog'lash orqali biz 256x256 piksel o'lchamdagi yakuniy tasvirni olamiz. Shuningdek, har bir yozuv uchun ma'lumotlar to'plamini yig'ish paytida foydalanilgan klassifikator tomonidan rasm to'g'ri tan olinganligini ko'rsatadigan yorliq, rasm muallifining yashash joyining ikki harfli kodi, noyob identifikator, vaqt tamg'asi mavjud. va fayl nomiga mos keladigan sinf nomi. Asl ma'lumotlarning soddalashtirilgan versiyasi arxivda 7.4 Gb og'irlikda va qadoqdan chiqarilgandan keyin taxminan 20 Gb ni tashkil qiladi, o'rashdan keyin to'liq ma'lumot 240 Gb ni egallaydi. Tashkilotchilar ikkala versiyada ham bir xil chizmalarni takrorlashini ta'minlashdi, ya'ni to'liq versiya ortiqcha edi. Qanday bo'lmasin, 50 million tasvirni grafik fayllarda yoki massivlar shaklida saqlash darhol foydasiz deb topildi va biz arxivdagi barcha CSV fayllarini birlashtirishga qaror qildik. train_simplified.zip har bir partiya uchun kerakli o'lchamdagi rasmlarni keyinchalik yaratish bilan ma'lumotlar bazasiga kiriting.

DBMS sifatida yaxshi tasdiqlangan tizim tanlangan MonetDB, ya'ni paket sifatida R uchun dastur MonetDBLite. Paket ma'lumotlar bazasi serverining o'rnatilgan versiyasini o'z ichiga oladi va serverni to'g'ridan-to'g'ri R sessiyasidan olish va u erda u bilan ishlash imkonini beradi. Ma'lumotlar bazasini yaratish va unga ulanish bitta buyruq bilan amalga oshiriladi:

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

Biz ikkita jadval yaratishimiz kerak bo'ladi: biri barcha ma'lumotlar uchun, ikkinchisi yuklab olingan fayllar haqida xizmat ma'lumoti uchun (agar biror narsa noto'g'ri bo'lsa va bir nechta fayllarni yuklab olgandan keyin jarayonni davom ettirish kerak bo'lsa foydalidir):

Jadvallar yaratish

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

Ma'lumotlar bazasiga ma'lumotlarni yuklashning eng tezkor usuli bu SQL buyrug'i yordamida CSV fayllarini to'g'ridan-to'g'ri nusxalash edi COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTqayerda tablename - jadval nomi va path - faylga yo'l. Arxiv bilan ishlash jarayonida o'rnatilgan dastur mavjudligi aniqlandi unzip da R arxivdagi bir qator fayllar bilan to'g'ri ishlamaydi, shuning uchun biz tizimdan foydalandik unzip (parametr yordamida getOption("unzip")).

Ma'lumotlar bazasiga yozish funksiyasi

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

Agar jadvalni ma'lumotlar bazasiga yozishdan oldin uni o'zgartirish kerak bo'lsa, argumentga o'tish kifoya preprocess ma'lumotlarni o'zgartiradigan funktsiya.

Ma'lumotlar bazasiga ma'lumotlarni ketma-ket yuklash uchun kod:

Ma'lumotlar bazasiga ma'lumotlarni yozish

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

Ma'lumotni yuklash vaqti ishlatiladigan haydovchining tezlik xususiyatlariga qarab farq qilishi mumkin. Bizning holatda, bitta SSD ichida yoki flesh-diskdan (manba fayl) SSD (DB) ga o'qish va yozish 10 daqiqadan kamroq vaqtni oladi.

Butun son sinf yorlig'i va indeks ustunli ustun yaratish uchun yana bir necha soniya kerak bo'ladi (ORDERED INDEX) partiyalar yaratishda kuzatuvlar namunasi olinadigan qator raqamlari bilan:

Qo'shimcha ustunlar va indeks yaratish

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

Tezda partiyani yaratish muammosini hal qilish uchun biz jadvaldan tasodifiy qatorlarni olishning maksimal tezligiga erishishimiz kerak edi. doodles. Buning uchun biz 3 ta hiyla ishlatdik. Birinchisi, kuzatish identifikatorini saqlaydigan turning o'lchamini kamaytirish edi. Asl ma'lumotlar to'plamida identifikatorni saqlash uchun zarur bo'lgan tur bigint, lekin kuzatishlar soni ularning tartib raqamiga teng identifikatorlarini turga moslashtirish imkonini beradi. int. Bu holda qidiruv ancha tez bo'ladi. Ikkinchi hiyla ishlatish edi ORDERED INDEX - Biz hamma narsani ko'rib chiqib, empirik tarzda bu qarorga keldik imkoniyatlari. Uchinchisi, parametrlangan so'rovlardan foydalanish edi. Usulning mohiyati buyruqni bir marta bajarishdir PREPARE bir xil turdagi so'rovlar to'plamini yaratishda tayyorlangan iborani keyinchalik ishlatish bilan, lekin aslida oddiy bilan solishtirganda afzallik bor SELECT statistik xatolik chegarasida bo'lib chiqdi.

Ma'lumotlarni yuklash jarayoni 450 MB dan ko'p bo'lmagan operativ xotirani sarflaydi. Ya'ni, tavsiflangan yondashuv sizga o'nlab gigabayt og'irlikdagi ma'lumotlar to'plamlarini deyarli har qanday byudjet uskunasiga, shu jumladan ba'zi bir platali qurilmalarga ko'chirishga imkon beradi, bu juda zo'r.

Faqatgina (tasodifiy) ma'lumotlarni olish tezligini o'lchash va turli o'lchamdagi partiyalardan namuna olishda masshtabni baholash qoladi:

Ma'lumotlar bazasi 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++ va neyron tarmoqlari bilan qanday do'stlashish mumkin

2. Partiyalarni tayyorlash

Butun partiyani tayyorlash jarayoni quyidagi bosqichlardan iborat:

  1. Nuqtalar koordinatalari bilan satrlar vektorlarini o'z ichiga olgan bir nechta JSONlarni tahlil qilish.
  2. Kerakli o'lchamdagi (masalan, 256×256 yoki 128×128) tasvirdagi nuqtalar koordinatalari asosida rangli chiziqlar chizish.
  3. Olingan tasvirlarni tenzorga aylantirish.

Python yadrolari o'rtasidagi raqobat doirasida muammo birinchi navbatda foydalanish orqali hal qilindi OpenCV. R-dagi eng oddiy va eng aniq analoglardan biri quyidagicha ko'rinadi:

R-da JSON-ni Tensorga aylantirishni amalga oshirish

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

Chizish standart R asboblari yordamida amalga oshiriladi va RAMda saqlanadigan vaqtinchalik PNG formatida saqlanadi (Linux-da vaqtinchalik R kataloglari katalogda joylashgan. /tmp, RAMga o'rnatilgan). Keyinchalik bu fayl 0 dan 1 gacha bo'lgan raqamlar bilan uch o'lchovli massiv sifatida o'qiladi. Bu juda muhim, chunki an'anaviy BMP olti burchakli rang kodlari bilan xom massivda o'qiladi.

Natijani sinab ko'ramiz:

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++ va neyron tarmoqlari bilan qanday do'stlashish mumkin

To'plamning o'zi quyidagicha shakllantiriladi:

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

Ushbu amalga oshirish biz uchun maqbul bo'lmagandek tuyuldi, chunki katta partiyalarni shakllantirish juda uzoq vaqtni oladi va biz kuchli kutubxonadan foydalangan holda hamkasblarimiz tajribasidan foydalanishga qaror qildik. OpenCV. O'sha paytda R uchun tayyor paket yo'q edi (hozir yo'q), shuning uchun kerakli funksionallikning minimal bajarilishi C++ da R kodiga integratsiyalashgan holda yozilgan. Rcpp.

Muammoni hal qilish uchun quyidagi paketlar va kutubxonalar ishlatilgan:

  1. OpenCV tasvirlar va chizmalar bilan ishlash uchun. Oldindan o'rnatilgan tizim kutubxonalari va sarlavha fayllari, shuningdek, dinamik bog'lanish ishlatilgan.

  2. xtensor ko'p o'lchovli massivlar va tensorlar bilan ishlash uchun. Xuddi shu nomdagi R paketiga kiritilgan sarlavhali fayllardan foydalandik. Kutubxona ko'p o'lchovli massivlar bilan asosiy satr va ustunlar tartibida ishlash imkonini beradi.

  3. ndjson JSONni tahlil qilish uchun. Bu kutubxonada ishlatiladi xtensor agar u loyihada mavjud bo'lsa, avtomatik ravishda.

  4. RcppThread JSON dan vektorni ko'p tarmoqli qayta ishlashni tashkil qilish uchun. Ushbu paket tomonidan taqdim etilgan sarlavha fayllaridan foydalanilgan. Ko'proq mashhurlardan RcppParallel Paketda, boshqa narsalar qatorida, o'rnatilgan tsiklni uzish mexanizmi mavjud.

Shuni ta'kidlash kerak xtensor Bu ilohiy sovg'a bo'lib chiqdi: uning keng funktsionalligi va yuqori ishlashi bilan bir qatorda, uni ishlab chiquvchilari juda sezgir bo'lib chiqdilar va savollarga tez va batafsil javob berishdi. Ularning yordami bilan OpenCV matritsalarini xtensor tensorlariga aylantirish, shuningdek, 3 o'lchovli tasvir tensorlarini to'g'ri o'lchamdagi 4 o'lchovli tensorga (to'plamning o'zi) birlashtirish usulini amalga oshirish mumkin edi.

Rcpp, xtensor va RcppThreadni o'rganish uchun 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

Tizim fayllarini ishlatadigan va tizimda o'rnatilgan kutubxonalar bilan dinamik bog'langan fayllarni kompilyatsiya qilish uchun biz paketda o'rnatilgan plagin mexanizmidan foydalandik. Rcpp. Yo'llar va bayroqlarni avtomatik ravishda topish uchun biz mashhur Linux yordam dasturidan foydalandik pkg-config.

OpenCV kutubxonasidan foydalanish uchun Rcpp plaginini amalga oshirish

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

Plaginning ishlashi natijasida kompilyatsiya jarayonida quyidagi qiymatlar almashtiriladi:

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"

JSONni tahlil qilish va modelga uzatish uchun partiyani yaratish uchun dastur kodi spoyler ostida berilgan. Birinchidan, sarlavha fayllarini qidirish uchun mahalliy loyiha katalogini qo'shing (ndjson uchun kerak):

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

C++ da JSONni tenzorga aylantirishni amalga oshirish

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

Ushbu kod faylga joylashtirilishi kerak src/cv_xt.cpp va buyruq bilan kompilyatsiya qiling Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); ish uchun ham talab qilinadi nlohmann/json.hpp dan ombori. Kod bir nechta funktsiyalarga bo'lingan:

  • to_xt - tasvir matritsasini o'zgartirish uchun shablonli funktsiya (cv::Mat) tenzorga xt::xtensor;

  • parse_json — funksiya JSON satrini tahlil qiladi, nuqtalarning koordinatalarini chiqaradi, ularni vektorga joylashtiradi;

  • ocv_draw_lines — nuqtalarning hosil boʻlgan vektoridan koʻp rangli chiziqlar chizadi;

  • process — yuqoridagi funksiyalarni birlashtiradi, shuningdek, olingan tasvirni masshtablash imkoniyatini qo‘shadi;

  • cpp_process_json_str - funksiya ustidan o'rash process, natijani R-ob'ektga eksport qiladi (ko'p o'lchovli massiv);

  • cpp_process_json_vector - funksiya ustidan o'rash cpp_process_json_str, bu sizga ko'p tarmoqli rejimda satr vektorini qayta ishlash imkonini beradi.

Ko'p rangli chiziqlarni chizish uchun HSV rang modeli ishlatilgan, so'ngra RGB ga o'tkazilgan. Natijani sinab ko'ramiz:

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++ va neyron tarmoqlari bilan qanday do'stlashish mumkin
R va C++ da amalga oshirish tezligini solishtirish

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++ va neyron tarmoqlari bilan qanday do'stlashish mumkin

Ko'rib turganingizdek, tezlikni oshirish juda muhim bo'lib chiqdi va R kodini parallellashtirish orqali C++ kodini ushlash mumkin emas.

3. Ma'lumotlar bazasidan partiyalarni tushirish uchun iteratorlar

R operativ xotiraga mos keladigan ma'lumotlarni qayta ishlash bo'yicha munosib obro'ga ega, Python esa ma'lumotlarni takroriy qayta ishlash bilan ajralib turadi, bu esa yadrodan tashqari hisob-kitoblarni (tashqi xotira yordamida hisob-kitoblarni) oson va tabiiy ravishda amalga oshirish imkonini beradi. Ta'riflangan muammo kontekstida biz uchun klassik va mos misol - bu gradient tushish usuli bilan o'qitilgan chuqur neyron tarmoqlari, kuzatuvlarning kichik qismi yoki mini-to'plam yordamida har bir qadamda gradientni yaqinlashtirish.

Python-da yozilgan chuqur o'rganish ramkalarida ma'lumotlarga asoslangan iteratorlarni amalga oshiradigan maxsus sinflar mavjud: jadvallar, papkalardagi rasmlar, ikkilik formatlar va boshqalar. Siz tayyor variantlardan foydalanishingiz yoki muayyan vazifalar uchun o'zingizni yozishingiz mumkin. R da biz Python kutubxonasining barcha imkoniyatlaridan foydalanishimiz mumkin keralar bir xil nomdagi paketdan foydalangan holda turli xil backendlar bilan, bu esa o'z navbatida paketning tepasida ishlaydi retikulyatsiya. Ikkinchisi alohida uzun maqolaga loyiqdir; u nafaqat R dan Python kodini ishga tushirish imkonini beradi, balki R va Python seanslari o'rtasida ob'ektlarni o'tkazish imkonini beradi, avtomatik ravishda barcha kerakli turdagi konversiyalarni amalga oshiradi.

Biz MonetDBLite-dan foydalanib, barcha ma'lumotlarni operativ xotirada saqlash zaruratidan xalos bo'ldik, barcha "neyron tarmoq" ishlari Python-dagi asl kod bilan amalga oshiriladi, biz shunchaki ma'lumotlar ustiga iterator yozishimiz kerak, chunki tayyor hech narsa yo'q. R yoki Pythonda bunday vaziyat uchun. Buning uchun faqat ikkita talab mavjud: u cheksiz tsiklda to'plamlarni qaytarishi va iteratsiyalar orasida o'z holatini saqlashi kerak (oxirgi R-dagi eng oddiy tarzda yopishlar yordamida amalga oshiriladi). Ilgari, R massivlarini iterator ichidagi numpy massivlarga aniq aylantirish kerak edi, ammo paketning joriy versiyasi keralar o'zi qiladi.

O'qitish va tekshirish ma'lumotlari uchun iterator quyidagicha bo'ldi:

Trening va tekshirish ma'lumotlari uchun 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)
  }
}

Funktsiya ma'lumotlar bazasiga ulanishi bilan o'zgaruvchini, foydalanilgan qatorlar sonini, sinflar sonini, to'plam hajmini, masshtabni kiritadi (scale = 1 256x256 pikselli tasvirlarni ko'rsatishga mos keladi, scale = 0.5 — 128x128 piksel), rang ko'rsatkichi (color = FALSE foydalanilganda kul rangda renderlashni belgilaydi color = TRUE har bir zarba yangi rangda chizilgan) va imagenetda oldindan o'qitilgan tarmoqlar uchun oldindan ishlov berish ko'rsatkichi. Ikkinchisi piksel qiymatlarini [0, 1] oraliqdan [-1, 1] oralig'iga o'tkazish uchun kerak bo'lib, u taqdim etilganlarni o'rgatishda ishlatilgan. keralar modellar.

Tashqi funktsiyada argument turini tekshirish, jadval mavjud data.table dan tasodifiy aralash qator raqamlari bilan samples_index va partiya raqamlari, hisoblagich va partiyalarning maksimal soni, shuningdek ma'lumotlar bazasidan ma'lumotlarni tushirish uchun SQL ifodasi. Bundan tashqari, biz ichidagi funksiyaning tezkor analogini aniqladik keras::to_categorical(). Ta'lim uchun deyarli barcha ma'lumotlardan foydalandik va tekshirish uchun yarim foiz qoldirdik, shuning uchun davr hajmi parametr bilan cheklangan edi. steps_per_epoch chaqirilganda keras::fit_generator(), va shart if (i > max_i) faqat tekshirish iteratori uchun ishlagan.

Ichki funktsiyada qator indekslari keyingi to'plam uchun olinadi, yozuvlar ma'lumotlar bazasidan yuk hisoblagichi ko'paygan holda olib tashlanadi, JSON tahlili (funktsiyasi). cpp_process_json_vector(), C++ da yozilgan) va rasmlarga mos massivlar yaratish. Keyin sinf yorliqlari bo'lgan bir-hot vektorlar yaratiladi, piksel qiymatlari va teglari bo'lgan massivlar ro'yxatga birlashtiriladi, bu qaytish qiymati hisoblanadi. Ishni tezlashtirish uchun biz jadvallarda indekslar yaratishdan foydalandik data.table va havola orqali o'zgartirish - bu "chiplar" paketisiz ma'lumotlar jadvali R.dagi har qanday muhim miqdordagi ma'lumotlar bilan samarali ishlashni tasavvur qilish juda qiyin.

Core i5 noutbukida tezlikni o'lchash natijalari quyidagicha:

Iterator benchmark

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

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

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

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

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

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

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: R, C++ va neyron tarmoqlari bilan qanday do'stlashish mumkin

Agar sizda etarli miqdordagi operativ xotira bo'lsa, ma'lumotlar bazasini xuddi shu RAMga o'tkazish orqali jiddiy ravishda tezlashtirishingiz mumkin (bizning vazifamiz uchun 32 GB etarli). Linuxda bo'lim sukut bo'yicha o'rnatiladi /dev/shm, operativ xotira hajmining yarmigacha egallagan. Tahrirlash orqali ko'proq ta'kidlashingiz mumkin /etc/fstabkabi rekordni olish uchun tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Buyruqni ishga tushirish orqali qayta ishga tushirishni va natijani tekshirishni unutmang df -h.

Test ma'lumotlari uchun iterator ancha sodda ko'rinadi, chunki test ma'lumotlar to'plami to'liq RAMga mos keladi:

Sinov ma'lumotlari uchun 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 arxitekturasini tanlash

Birinchi arxitektura ishlatilgan mobilenet v1, uning xususiyatlari muhokama qilinadi bu xabar. U standart sifatida kiritilgan keralar va shunga ko'ra, R uchun bir xil nomdagi paketda mavjud. Ammo uni bitta kanalli tasvirlar bilan ishlatishga harakat qilganda, g'alati narsa chiqdi: kirish tenzori har doim o'lchamga ega bo'lishi kerak. (batch, height, width, 3), ya'ni kanallar sonini o'zgartirib bo'lmaydi. Python-da bunday cheklov yo'q, shuning uchun biz shoshilinch ravishda ushbu arxitekturani o'zimiz amalga oshirishni yozdik va asl maqolaga amal qildik (keras versiyasida qoldirmasdan):

Mobilenet v1 arxitekturasi

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

Ushbu yondashuvning kamchiliklari aniq. Men ko'plab modellarni sinab ko'rmoqchiman, lekin aksincha, har bir arxitekturani qo'lda qayta yozishni xohlamayman. Shuningdek, biz imagenet-da oldindan o'qitilgan modellarning og'irliklaridan foydalanish imkoniyatidan mahrum bo'ldik. Odatdagidek, hujjatlarni o'rganish yordam berdi. Funktsiya get_config() tahrirlash uchun mos shaklda model tavsifini olish imkonini beradi (base_model_conf$layers - oddiy R ro'yxati) va funksiya from_config() model ob'ektiga teskari aylantirishni amalga oshiradi:

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)

Endi taqdim etilganlardan birini olish uchun universal funktsiyani yozish qiyin emas keralar imagenet-da o'qitilgan vaznli yoki og'irliksiz modellar:

Tayyor arxitekturalarni yuklash funksiyasi

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

Bir kanalli tasvirlardan foydalanilganda, oldindan tayyorlangan og'irliklar ishlatilmaydi. Buni tuzatish mumkin: funktsiyadan foydalanish get_weights() model og'irliklarini R massivlari ro'yxati ko'rinishida oling, ushbu ro'yxatning birinchi elementining o'lchamini o'zgartiring (bitta rangli kanalni olish yoki uchtasini o'rtacha hisoblash orqali) va keyin vaznlarni funksiya bilan modelga qayta yuklang. set_weights(). Biz bu funksiyani hech qachon qo'shmaganmiz, chunki bu bosqichda rangli rasmlar bilan ishlash yanada samaraliroq ekanligi ayon bo'ldi.

Biz eksperimentlarning aksariyatini mobilenet 1 va 2 versiyalari, shuningdek resnet34 yordamida amalga oshirdik. SE-ResNeXt kabi zamonaviy arxitekturalar ushbu tanlovda yaxshi natijalarga erishdi. Afsuski, bizning ixtiyorimizda tayyor dasturlar yo'q edi va biz o'zimizni yozmadik (lekin biz albatta yozamiz).

5. Skriptlarni parametrlashtirish

Qulaylik uchun, treningni boshlash uchun barcha kodlar yordamida parametrlashtirilgan yagona skript sifatida yaratilgan dokopt quyida bayon qilinganidek:

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)

Paket dokopt amalga oshirilishini ifodalaydi http://docopt.org/ for R. Uning yordami bilan skriptlar kabi oddiy buyruqlar bilan ishga tushiriladi Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db yoki ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, agar fayl train_nn.R bajariladi (bu buyruq modelni o'rgatishni boshlaydi resnet50 128x128 piksel o'lchamdagi uch rangli tasvirlarda ma'lumotlar bazasi papkada joylashgan bo'lishi kerak /home/andrey/doodle_db). Roʻyxatga oʻrganish tezligi, optimallashtiruvchi turi va boshqa sozlanishi mumkin boʻlgan parametrlarni qoʻshishingiz mumkin. Nashrni tayyorlash jarayonida ma'lum bo'ldiki, arxitektura mobilenet_v2 joriy versiyadan keralar R da foydalanish mumkin emas R paketida e'tiborga olinmagan o'zgarishlar tufayli biz ularni tuzatishlarini kutmoqdamiz.

Ushbu yondashuv RStudio-da skriptlarning an'anaviy ishga tushirilishi bilan solishtirganda turli modellar bilan tajribalarni sezilarli darajada tezlashtirishga imkon berdi (biz paketni mumkin bo'lgan alternativa sifatida ta'kidlaymiz. tfruns). Ammo asosiy afzallik - buning uchun RStudio-ni o'rnatmasdan, Docker-da yoki oddiygina serverda skriptlarni ishga tushirishni osongina boshqarish qobiliyati.

6. Skriptlarni dokerlashtirish

Biz Docker-dan jamoa a'zolari o'rtasida modellarni o'rgatish va bulutda tezkor joylashtirish uchun muhitning portativligini ta'minlash uchun foydalandik. R dasturchi uchun nisbatan odatiy bo'lmagan ushbu vosita bilan tanishishni boshlashingiz mumkin bu nashrlar seriyasi yoki video kurs.

Docker sizga noldan o'z rasmlaringizni yaratishga va o'zingizni yaratish uchun asos sifatida boshqa rasmlardan foydalanishga imkon beradi. Mavjud variantlarni tahlil qilar ekanmiz, biz NVIDIA, CUDA+cuDNN drayverlari va Python kutubxonalarini o'rnatish tasvirning juda katta qismi ekanligi haqidagi xulosaga keldik va biz rasmiy rasmni asos qilib olishga qaror qildik. tensorflow/tensorflow:1.12.0-gpu, u erda kerakli R paketlarini qo'shing.

Yakuniy docker fayli quyidagicha ko'rinishga ega edi:

Docker fayli

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

Qulaylik uchun ishlatilgan paketlar o'zgaruvchilarga qo'yildi; yozma skriptlarning asosiy qismi yig'ish paytida konteynerlar ichida ko'chiriladi. Biz buyruq qobig'ini ham o'zgartirdik /bin/bash kontentdan foydalanish qulayligi uchun /etc/os-release. Bu kodda OS versiyasini ko'rsatish zaruratining oldini oldi.

Bundan tashqari, turli xil buyruqlar bilan konteynerni ishga tushirishga imkon beruvchi kichik bash skripti yozilgan. Masalan, bular konteyner ichiga ilgari joylashtirilgan neyron tarmoqlarni o'rgatish uchun skriptlar yoki konteyner ishini disk raskadrovka va monitoring qilish uchun buyruq qobig'i bo'lishi mumkin:

Konteynerni ishga tushirish uchun 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}

Agar bu bash skripti parametrlarsiz ishga tushirilsa, skript konteyner ichida chaqiriladi train_nn.R standart qiymatlar bilan; agar birinchi pozitsion argument "bash" bo'lsa, konteyner interaktiv ravishda buyruq qobig'i bilan boshlanadi. Boshqa barcha holatlarda pozitsion argumentlarning qiymatlari almashtiriladi: CMD="Rscript /app/train_nn.R $@".

Shuni ta'kidlash kerakki, manba ma'lumotlari va ma'lumotlar bazasi bilan kataloglar, shuningdek o'qitilgan modellarni saqlash uchun kataloglar host tizimidan konteyner ichiga o'rnatilgan bo'lib, bu sizga keraksiz manipulyatsiyalarsiz skriptlar natijalariga kirish imkonini beradi.

7. Google Cloud’da bir nechta GPU’lardan foydalanish

Tanlovning xususiyatlaridan biri juda shovqinli ma'lumotlar edi (ODS slack-dan @Leigh.plt dan olingan sarlavha rasmiga qarang). Katta partiyalar bunga qarshi kurashishga yordam beradi va 1 GPUli kompyuterda tajribalardan so'ng biz bulutdagi bir nechta GPU-larda o'quv modellarini o'zlashtirishga qaror qildik. Ishlatilgan GoogleCloud (asoslar uchun yaxshi qo'llanma) mavjud konfiguratsiyalarning katta tanlovi, maqbul narxlar va 300 dollar bonus tufayli. Ochko'zlikdan men SSD va bir tonna operativ xotiraga ega 4xV100 namunasini buyurtma qildim va bu katta xato edi. Bunday mashina pulni tezda yutib yuboradi, siz isbotlangan quvur liniyasisiz singan tajribaga o'tishingiz mumkin. Ta'lim maqsadlarida K80 ni olish yaxshiroqdir. Ammo katta hajmdagi operativ xotira foydali bo'ldi - bulutli SSD o'zining ishlashi bilan hayratda qoldirmadi, shuning uchun ma'lumotlar bazasi o'tkazildi dev/shm.

Bir nechta GPU-lardan foydalanish uchun javobgar bo'lgan kod qismi eng katta qiziqish uyg'otadi. Birinchidan, model Python-da bo'lgani kabi kontekst menejeri yordamida CPUda yaratiladi:

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

Keyin kompilyatsiya qilinmagan (bu muhim) model ma'lum miqdordagi mavjud GPU-larga ko'chiriladi va shundan keyingina kompilyatsiya qilinadi:

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

Oxirgi qatlamdan tashqari barcha qatlamlarni muzlatish, oxirgi qatlamni o'rgatish, bir nechta GPU uchun butun modelni muzlatish va qayta o'qitishning klassik texnikasini amalga oshirib bo'lmadi.

Mashqlar foydalanilmasdan kuzatildi. tensorboard, har bir davrdan keyin jurnallarni yozib olish va ma'lumot beruvchi nomlar bilan modellarni saqlash bilan cheklanib qolamiz:

Qayta qo'ng'iroqlar

# Шаблон имени файла лога
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. Xulosa o‘rniga

Biz duch kelgan bir qator muammolar hali ham hal qilinmagan:

  • в keralar Optimal o'rganish tezligini avtomatik ravishda qidirish uchun tayyor funksiya mavjud emas (analog lr_finder kutubxonada fast.ai); Ba'zi harakatlar bilan uchinchi tomon dasturlarini R ga o'tkazish mumkin, masalan, bu;
  • oldingi nuqta natijasida, bir nechta GPU-lardan foydalanganda to'g'ri o'qitish tezligini tanlash mumkin emas edi;
  • zamonaviy neyron tarmoqlari arxitekturalari, ayniqsa imagenet-da oldindan o'qitilganlar etishmasligi;
  • hech kim tsikl siyosati va kamsituvchi o'rganish tezligi (kosinus tavlanishi bizning iltimosimiz bo'yicha edi amalga oshirildi, rahmat skeydan).

Ushbu musobaqadan qanday foydali narsalar o'rganildi:

  • Nisbatan kam quvvatli uskunada siz og'riqsiz munosib (RAM hajmidan ko'p marta katta) hajmdagi ma'lumotlar bilan ishlashingiz mumkin. Plastik sumka ma'lumotlar jadvali jadvallarni joyida o'zgartirish tufayli xotirani tejaydi, bu ularni nusxalashdan qochadi va to'g'ri ishlatilganda deyarli har doim bizga ma'lum bo'lgan skript tillari uchun barcha vositalar orasida eng yuqori tezlikni namoyish etadi. Ma'lumotlar bazasida ma'lumotlarni saqlash, ko'p hollarda, butun ma'lumotlar to'plamini RAMga siqish kerakligi haqida umuman o'ylamaslikka imkon beradi.
  • R dagi sekin funksiyalar paket yordamida C++ da tez funksiyalar bilan almashtirilishi mumkin Rcpp. Agar foydalanishdan tashqari RcppThread yoki RcppParallel, biz o'zaro platformali ko'p tarmoqli ilovalarni olamiz, shuning uchun kodni R darajasida parallellashtirishga hojat yo'q.
  • Paket Rcpp C++ tilini jiddiy bilmagan holda foydalanish mumkin, talab qilinadigan minimal miqdor ko'rsatilgan shu yerda. kabi bir qator ajoyib C-kutubxonalar uchun sarlavha fayllari xtensor CRAN-da mavjud, ya'ni R-ga tayyor yuqori samarali C++ kodini integratsiyalashgan loyihalarni amalga oshirish uchun infratuzilma shakllantirilmoqda. Qo'shimcha qulaylik - sintaksisni ajratib ko'rsatish va RStudio'da statik C++ kod analizatori.
  • dokopt parametrlari bilan mustaqil skriptlarni ishga tushirish imkonini beradi. Bu masofaviy serverda foydalanish uchun qulay, shu jumladan. docker ostida. RStudio-da neyron tarmoqlarni o'qitish bilan ko'p soatlik tajribalar o'tkazish noqulay va IDE-ni serverga o'rnatish har doim ham oqlanmaydi.
  • Docker OS va kutubxonalarning turli versiyalari bilan ishlab chiquvchilar o'rtasida kodning ko'chishi va natijalarning takrorlanishini, shuningdek, serverlarda bajarilishi qulayligini ta'minlaydi. Siz faqat bitta buyruq bilan butun o'quv quvurini ishga tushirishingiz mumkin.
  • Google Cloud - qimmat qurilmalarda tajriba o'tkazishning byudjetga qulay usuli, ammo siz konfiguratsiyalarni diqqat bilan tanlashingiz kerak.
  • Alohida kod bo'laklarining tezligini o'lchash juda foydali, ayniqsa R va C ++ ni birlashtirganda va paket bilan dastgoh - shuningdek, juda oson.

Umuman olganda, bu tajriba juda foydali bo'ldi va biz ko'tarilgan ba'zi muammolarni hal qilish ustida ishlashda davom etamiz.

Manba: www.habr.com

a Izoh qo'shish