Adnabyddiaeth Gyflym Doodle: sut i wneud ffrindiau â rhwydweithiau R, C ++ a niwral

Adnabyddiaeth Gyflym Doodle: sut i wneud ffrindiau â rhwydweithiau R, C ++ a niwral

Hei Habr!

Y cwymp diwethaf, cynhaliodd Kaggle gystadleuaeth i ddosbarthu lluniau wedi'u tynnu â llaw, Quick Draw Doodle Recognition, lle cymerodd tîm o R-wyddonwyr ran, ymhlith eraill: Artem Klevtsova, Rheolwr Philippa и Andrey Ogurtsov. Ni fyddwn yn disgrifio’r gystadleuaeth yn fanwl; mae hynny eisoes wedi’i wneud yn cyhoeddiad diweddar.

Y tro hwn nid oedd yn gweithio gyda ffermio medalau, ond enillwyd llawer o brofiad gwerthfawr, felly hoffwn ddweud wrth y gymuned am nifer o'r pethau mwyaf diddorol a defnyddiol ar Kagle ac mewn gwaith bob dydd. Ymhlith y pynciau a drafodwyd: bywyd anodd hebddo OpenCV, dosrannu JSON (mae'r enghreifftiau hyn yn archwilio integreiddio cod C++ i sgriptiau neu becynnau yn R gan ddefnyddio Rcpp), parameterization o sgriptiau a dockerization o'r ateb terfynol. Mae'r holl god o'r neges ar ffurf sy'n addas i'w weithredu ar gael yn storfeydd.

Cynnwys:

  1. Llwythwch ddata o CSV yn effeithlon i MonetDB
  2. Paratoi sypiau
  3. Iterators ar gyfer dadlwytho sypiau o'r gronfa ddata
  4. Dewis Pensaernïaeth Fodel
  5. Paramedroli sgript
  6. Dockerization o sgriptiau
  7. Defnyddio GPUs lluosog ar Google Cloud
  8. Yn hytrach na i gasgliad

1. Llwytho data o CSV yn effeithlon i gronfa ddata MonetDB

Darperir y data yn y gystadleuaeth hon nid ar ffurf delweddau parod, ond ar ffurf 340 o ffeiliau CSV (un ffeil ar gyfer pob dosbarth) sy'n cynnwys JSONs gyda chyfesurynnau pwynt. Trwy gysylltu'r pwyntiau hyn â llinellau, rydym yn cael delwedd derfynol yn mesur 256x256 picsel. Hefyd ar gyfer pob cofnod mae label yn nodi a gafodd y llun ei adnabod yn gywir gan y dosbarthwr a ddefnyddiwyd ar yr adeg y casglwyd y set ddata, cod dwy lythyren o wlad breswyl awdur y llun, dynodwr unigryw, stamp amser ac enw dosbarth sy'n cyfateb i enw'r ffeil. Mae fersiwn symlach o'r data gwreiddiol yn pwyso 7.4 GB yn yr archif a thua 20 GB ar ôl dadbacio, mae'r data llawn ar ôl dadbacio yn cymryd 240 GB. Sicrhaodd y trefnwyr fod y ddau fersiwn yn atgynhyrchu'r un lluniadau, gan olygu nad oedd angen y fersiwn llawn. Beth bynnag, roedd storio 50 miliwn o ddelweddau mewn ffeiliau graffig neu ar ffurf araeau yn cael ei ystyried yn amhroffidiol ar unwaith, a gwnaethom benderfynu uno'r holl ffeiliau CSV o'r archif train_simplified.zip i mewn i'r gronfa ddata gyda'r genhedlaeth ddilynol o ddelweddau o'r maint gofynnol “ar y hedfan” ar gyfer pob swp.

Dewiswyd system sydd wedi'i phrofi'n dda fel y DBMS MonetDB, sef gweithrediad ar gyfer R fel pecyn MonetDBLite. Mae'r pecyn yn cynnwys fersiwn wedi'i fewnosod o weinydd y gronfa ddata ac yn caniatáu ichi godi'r gweinydd yn uniongyrchol o sesiwn R a gweithio gydag ef yno. Mae creu cronfa ddata a chysylltu ag ef yn cael ei berfformio gydag un gorchymyn:

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

Bydd angen i ni greu dau dabl: un ar gyfer yr holl ddata, y llall ar gyfer gwybodaeth gwasanaeth am ffeiliau wedi'u llwytho i lawr (defnyddiol os aiff rhywbeth o'i le a rhaid ailddechrau'r broses ar ôl lawrlwytho sawl ffeil):

Creu tablau

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

Y ffordd gyflymaf i lwytho data i'r gronfa ddata oedd copïo ffeiliau CSV yn uniongyrchol gan ddefnyddio SQL - gorchymyn COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTlle tablename - enw bwrdd a path - y llwybr i'r ffeil. Wrth weithio gyda'r archif, darganfuwyd bod y gweithredu adeiledig unzip nid yw yn R yn gweithio'n gywir gyda nifer o ffeiliau o'r archif, felly fe wnaethom ddefnyddio'r system unzip (gan ddefnyddio'r paramedr getOption("unzip")).

Swyddogaeth ar gyfer ysgrifennu i'r gronfa ddata

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

Os oes angen i chi drawsnewid y tabl cyn ei ysgrifennu i'r gronfa ddata, mae'n ddigon i basio'r ddadl i mewn preprocess swyddogaeth a fydd yn trawsnewid y data.

Cod ar gyfer llwytho data yn olynol i'r gronfa ddata:

Ysgrifennu data i'r gronfa ddata

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

Gall amser llwytho data amrywio yn dibynnu ar nodweddion cyflymder y gyriant a ddefnyddir. Yn ein hachos ni, mae darllen ac ysgrifennu o fewn un SSD neu o yriant fflach (ffeil ffynhonnell) i SSD (DB) yn cymryd llai na 10 munud.

Mae'n cymryd ychydig eiliadau mwy i greu colofn gyda label dosbarth cyfanrif a cholofn mynegai (ORDERED INDEX) gyda rhifau llinellau ar gyfer samplu arsylwadau wrth greu sypiau:

Creu Colofnau a Mynegai Ychwanegol

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

I ddatrys y broblem o greu swp ar y hedfan, roedd angen i ni gyflawni'r cyflymder uchaf o dynnu rhesi ar hap o'r bwrdd doodles. Ar gyfer hyn fe wnaethom ddefnyddio 3 tric. Y cyntaf oedd lleihau dimensiwn y math sy'n storio'r ID arsylwi. Yn y set ddata wreiddiol, y math sydd ei angen i storio'r ID yw bigint, ond mae nifer yr arsylwadau yn ei gwneud hi'n bosibl ffitio eu dynodwyr, sy'n hafal i'r rhif trefnol, i'r math int. Mae'r chwiliad yn llawer cyflymach yn yr achos hwn. Yr ail gamp oedd defnyddio ORDERED INDEX — daethom i’r penderfyniad hwn yn empirig, ar ôl mynd drwy bopeth a oedd ar gael opsiynau. Y trydydd oedd defnyddio ymholiadau paramedr. Hanfod y dull yw gweithredu'r gorchymyn unwaith PREPARE gyda defnydd dilynol o fynegiant parod wrth greu criw o ymholiadau o'r un math, ond mewn gwirionedd mae mantais o'i gymharu ag un syml SELECT troi allan i fod o fewn yr ystod o wallau ystadegol.

Nid yw'r broses o uwchlwytho data yn defnyddio mwy na 450 MB o RAM. Hynny yw, mae'r dull a ddisgrifir yn caniatáu ichi symud setiau data sy'n pwyso degau o gigabeit ar bron unrhyw galedwedd cyllideb, gan gynnwys rhai dyfeisiau bwrdd sengl, sy'n eithaf cŵl.

Y cyfan sydd ar ôl yw mesur cyflymder adalw data (ar hap) a gwerthuso’r raddfa wrth samplu sypiau o wahanol feintiau:

Meincnod cronfa ddata

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)

Adnabyddiaeth Gyflym Doodle: sut i wneud ffrindiau â rhwydweithiau R, C ++ a niwral

2. Paratoi sypiau

Mae'r broses gyfan o baratoi swp yn cynnwys y camau canlynol:

  1. Dosrannu sawl JSON sy'n cynnwys fectorau llinynnau gyda chyfesurynnau pwyntiau.
  2. Tynnu llinellau lliw yn seiliedig ar gyfesurynnau pwyntiau ar ddelwedd o'r maint gofynnol (er enghraifft, 256×256 neu 128×128).
  3. Trosi'r delweddau canlyniadol yn tensor.

Fel rhan o'r gystadleuaeth ymhlith cnewyllyn Python, datryswyd y broblem yn bennaf gan ddefnyddio OpenCV. Byddai un o'r analogau symlaf ac amlycaf yn R yn edrych fel hyn:

Gweithredu Trosi JSON i Tensor yn R

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

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

Perfformir lluniadu gan ddefnyddio offer R safonol a'i gadw i PNG dros dro sydd wedi'i storio yn RAM (ar Linux, mae cyfeirlyfrau R dros dro wedi'u lleoli yn y cyfeiriadur /tmp, wedi'i osod mewn RAM). Yna darllenir y ffeil hon fel arae tri dimensiwn gyda rhifau'n amrywio o 0 i 1. Mae hyn yn bwysig oherwydd byddai BMP mwy confensiynol yn cael ei ddarllen i mewn i arae amrwd gyda chodau lliw hecs.

Gadewch i ni brofi'r canlyniad:

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

Adnabyddiaeth Gyflym Doodle: sut i wneud ffrindiau â rhwydweithiau R, C ++ a niwral

Bydd y swp ei hun yn cael ei ffurfio fel a ganlyn:

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

Roedd y gweithrediad hwn yn ymddangos yn is-optimaidd i ni, gan fod ffurfio sypiau mawr yn cymryd amser anweddus o hir, a phenderfynom fanteisio ar brofiad ein cydweithwyr trwy ddefnyddio llyfrgell bwerus. OpenCV. Ar y pryd nid oedd pecyn parod ar gyfer R (nid oes un nawr), felly ychydig iawn o weithredu'r swyddogaeth ofynnol oedd wedi'i ysgrifennu yn C++ gydag integreiddio i god R gan ddefnyddio Rcpp.

I ddatrys y broblem, defnyddiwyd y pecynnau a'r llyfrgelloedd canlynol:

  1. OpenCV ar gyfer gweithio gyda delweddau a thynnu llinellau. Wedi defnyddio llyfrgelloedd system wedi'u gosod ymlaen llaw a ffeiliau pennawd, yn ogystal â chysylltiadau deinamig.

  2. xtensor ar gyfer gweithio gydag araeau a thenorau aml-ddimensiwn. Fe wnaethom ddefnyddio ffeiliau pennawd sydd wedi'u cynnwys yn y pecyn R o'r un enw. Mae'r llyfrgell yn caniatáu ichi weithio gydag araeau aml-ddimensiwn, yn nhrefn y prif resi a'r brif golofn.

  3. ndjson am dosrannu JSON. Defnyddir y llyfrgell hon yn xtensor yn awtomatig os yw'n bresennol yn y prosiect.

  4. RcppThread ar gyfer trefnu prosesu aml-edau fector gan JSON. Wedi defnyddio'r ffeiliau pennyn a ddarperir gan y pecyn hwn. O fwy poblogaidd RcppParallel Mae gan y pecyn, ymhlith pethau eraill, fecanwaith torri dolen adeiledig.

Dylid nodi bod xtensor Trodd allan i fod yn fendith: yn ogystal â'r ffaith bod ganddo ymarferoldeb helaeth a pherfformiad uchel, daeth ei ddatblygwyr yn eithaf ymatebol ac atebodd gwestiynau'n brydlon ac yn fanwl. Gyda'u cymorth, roedd yn bosibl trawsnewid matricsau OpenCV yn denorau xtensor, yn ogystal â ffordd o gyfuno tensorau delwedd 3-dimensiwn i mewn i densor 4-dimensiwn o'r dimensiwn cywir (y swp ei hun).

Deunyddiau ar gyfer dysgu Rcpp, xtensor ac RcppThread

https://thecoatlessprofessor.com/programming/unofficial-rcpp-api-documentation

https://docs.opencv.org/4.0.1/d7/dbd/group__imgproc.html

https://xtensor.readthedocs.io/en/latest/

https://xtensor.readthedocs.io/en/latest/file_loading.html#loading-json-data-into-xtensor

https://cran.r-project.org/web/packages/RcppThread/vignettes/RcppThread-vignette.pdf

I lunio ffeiliau sy'n defnyddio ffeiliau system a chysylltiadau deinamig â llyfrgelloedd sydd wedi'u gosod ar y system, gwnaethom ddefnyddio'r mecanwaith ategyn a weithredwyd yn y pecyn Rcpp. I ddod o hyd i lwybrau a baneri yn awtomatig, fe wnaethom ddefnyddio cyfleustodau Linux poblogaidd pkg-config.

Gweithredu'r ategyn Rcpp ar gyfer defnyddio'r llyfrgell OpenCV

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

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

O ganlyniad i weithrediad yr ategyn, bydd y gwerthoedd canlynol yn cael eu disodli yn ystod y broses lunio:

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"

Rhoddir y cod gweithredu ar gyfer dosrannu JSON a chynhyrchu swp i'w drosglwyddo i'r model o dan y sbwyliwr. Yn gyntaf, ychwanegwch gyfeiriadur prosiect lleol i chwilio am ffeiliau pennawd (angen ndjson):

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

Gweithredu trosi JSON i tensor yn C++

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

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

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

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

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

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

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

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

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

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

Dylid gosod y cod hwn yn y ffeil src/cv_xt.cpp a chrynhoi gyda'r gorchymyn Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); hefyd yn ofynnol ar gyfer gwaith nlohmann/json.hpp o ystorfa. Rhennir y cod yn sawl swyddogaeth:

  • to_xt — swyddogaeth dempled ar gyfer trawsnewid matrics delwedd (cv::Mat) i tensor xt::xtensor;

  • parse_json — mae'r ffwythiant yn dosrannu llinyn JSON, yn echdynnu cyfesurynnau pwyntiau, gan eu pacio mewn fector;

  • ocv_draw_lines — o'r fector pwyntiau canlyniadol, yn tynnu llinellau amryliw;

  • process — yn cyfuno'r swyddogaethau uchod a hefyd yn ychwanegu'r gallu i raddfa'r ddelwedd ganlyniadol;

  • cpp_process_json_str - deunydd lapio dros y swyddogaeth process, sy'n allforio'r canlyniad i R-gwrthrych (arae aml-ddimensiwn);

  • cpp_process_json_vector - deunydd lapio dros y swyddogaeth cpp_process_json_str, sy'n eich galluogi i brosesu fector llinyn mewn modd aml-edau.

I dynnu llinellau aml-liw, defnyddiwyd y model lliw HSV, ac yna trosi i RGB. Gadewch i ni brofi'r canlyniad:

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

Adnabyddiaeth Gyflym Doodle: sut i wneud ffrindiau â rhwydweithiau R, C ++ a niwral
Cymharu cyflymder gweithredu yn R ac C++

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

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

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

res_bench[, cols]

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

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

Adnabyddiaeth Gyflym Doodle: sut i wneud ffrindiau â rhwydweithiau R, C ++ a niwral

Fel y gallwch weld, roedd y cynnydd mewn cyflymder yn arwyddocaol iawn, ac nid yw'n bosibl dal i fyny â chod C ++ trwy gyfochrog â chod R.

3. Iterators ar gyfer dadlwytho sypiau o'r gronfa ddata

Mae gan R enw haeddiannol fel iaith ar gyfer prosesu data sy'n cyd-fynd â RAM, tra bod Python yn cael ei nodweddu'n fwy gan brosesu data ailadroddus, sy'n eich galluogi i weithredu cyfrifiadau y tu allan i'r craidd yn hawdd ac yn naturiol (cyfrifiadau gan ddefnyddio cof allanol). Enghraifft glasurol a pherthnasol i ni yng nghyd-destun y broblem a ddisgrifiwyd yw rhwydweithiau niwral dwfn a hyfforddwyd gan y dull disgyniad graddiant gyda brasamcan o'r graddiant ar bob cam gan ddefnyddio cyfran fechan o arsylwadau, neu swp bach.

Mae gan fframweithiau dysgu dwfn a ysgrifennwyd yn Python ddosbarthiadau arbennig sy'n gweithredu iterwyr yn seiliedig ar ddata: tablau, lluniau mewn ffolderi, fformatau deuaidd, ac ati Gallwch ddefnyddio opsiynau parod neu ysgrifennu eich rhai eich hun ar gyfer tasgau penodol. Yn R gallwn fanteisio ar holl nodweddion y llyfrgell Python keras gyda'i backends amrywiol gan ddefnyddio'r pecyn o'r un enw, sydd yn ei dro yn gweithio ar ben y pecyn tawelu. Mae'r olaf yn haeddu erthygl hir ar wahân; mae nid yn unig yn caniatáu ichi redeg cod Python o R, ond mae hefyd yn caniatáu ichi drosglwyddo gwrthrychau rhwng sesiynau R a Python, gan berfformio'r holl drawsnewidiadau math angenrheidiol yn awtomatig.

Cawsom wared ar yr angen i storio'r holl ddata yn RAM trwy ddefnyddio MonetDBLite, bydd yr holl waith “rhwydwaith niwral” yn cael ei berfformio gan y cod gwreiddiol yn Python, mae'n rhaid i ni ysgrifennu iterator dros y data, gan nad oes dim yn barod ar gyfer sefyllfa o'r fath naill ai yn R neu Python. Yn y bôn, dim ond dau ofyniad sydd ar ei gyfer: rhaid iddo ddychwelyd sypiau mewn dolen ddiddiwedd ac arbed ei gyflwr rhwng iteriadau (mae'r olaf yn R yn cael ei weithredu yn y ffordd symlaf gan ddefnyddio cau). Yn flaenorol, roedd yn ofynnol trosi araeau R yn araeau numpy y tu mewn i'r iterator yn benodol, ond fersiwn gyfredol y pecyn keras yn ei wneud ei hun.

Daeth yr iterator ar gyfer data hyfforddi a dilysu fel a ganlyn:

Iterator ar gyfer hyfforddiant a data dilysu

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

Mae'r ffwythiant yn cymryd fel mewnbwn newidyn gyda chysylltiad i'r gronfa ddata, nifer y llinellau a ddefnyddiwyd, nifer y dosbarthiadau, maint swp, graddfa (scale = 1 yn cyfateb i ddelweddau rendro o 256x256 picsel, scale = 0.5 — 128x128 picsel), dangosydd lliw (color = FALSE yn pennu rendrad mewn graddlwyd pan gaiff ei ddefnyddio color = TRUE caiff pob strôc ei dynnu mewn lliw newydd) a dangosydd rhagbrosesu ar gyfer rhwydweithiau sydd wedi'u hyfforddi ymlaen llaw ar imagenet. Mae angen yr olaf er mwyn graddio gwerthoedd picsel o'r cyfwng [0, 1] i'r cyfwng [-1, 1], a ddefnyddiwyd wrth hyfforddi'r cyfwng a gyflenwir keras modelau.

Mae'r swyddogaeth allanol yn cynnwys gwirio math dadl, tabl data.table gyda rhifau llinell cymysg ar hap o samples_index a rhifau swp, rhifydd ac uchafswm nifer y sypiau, yn ogystal â mynegiad SQL ar gyfer dadlwytho data o'r gronfa ddata. Yn ogystal, fe wnaethom ddiffinio analog cyflym o'r swyddogaeth y tu mewn keras::to_categorical(). Fe wnaethon ni ddefnyddio bron yr holl ddata ar gyfer hyfforddiant, gan adael hanner y cant i'w ddilysu, felly roedd maint yr epoc wedi'i gyfyngu gan y paramedr steps_per_epoch pan y'i gelwir keras::fit_generator(), a'r cyflwr if (i > max_i) gweithio ar gyfer yr iterator dilysu yn unig.

Yn y ffwythiant mewnol, caiff mynegeion rhes eu hadalw ar gyfer y swp nesaf, dadlwythir cofnodion o'r gronfa ddata gyda'r rhifydd swp yn cynyddu, dosrannu JSON (swyddogaeth cpp_process_json_vector(), wedi'i ysgrifennu yn C++) a chreu araeau sy'n cyfateb i luniau. Yna fectorau un-poeth gyda labeli dosbarth yn cael eu creu, araeau gyda gwerthoedd picsel a labeli yn cael eu cyfuno i mewn i restr, sef y gwerth dychwelyd. Er mwyn cyflymu gwaith, fe wnaethom ddefnyddio creu mynegeion mewn tablau data.table ac addasu trwy'r ddolen - heb y pecyn “sglodion” hyn data.tabl Mae'n eithaf anodd dychmygu gweithio'n effeithiol gydag unrhyw swm sylweddol o ddata yn R.

Mae canlyniadau mesuriadau cyflymder ar liniadur Craidd i5 fel a ganlyn:

Meincnod iterator

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

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

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

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

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

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

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Adnabyddiaeth Gyflym Doodle: sut i wneud ffrindiau â rhwydweithiau R, C ++ a niwral

Os oes gennych chi ddigon o RAM, gallwch chi gyflymu gweithrediad y gronfa ddata o ddifrif trwy ei drosglwyddo i'r un RAM hwn (mae 32 GB yn ddigon ar gyfer ein tasg). Yn Linux, mae'r rhaniad wedi'i osod yn ddiofyn /dev/shm, meddiannu hyd at hanner y capasiti RAM. Gallwch amlygu mwy trwy olygu /etc/fstabi gael record fel tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Gwnewch yn siŵr eich bod yn ailgychwyn a gwirio'r canlyniad trwy redeg y gorchymyn df -h.

Mae'r iterator ar gyfer data prawf yn edrych yn llawer symlach, gan fod set ddata'r prawf yn ffitio'n gyfan gwbl i RAM:

Iterator ar gyfer data prawf

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. Detholiad o bensaernïaeth fodel

Y bensaernïaeth gyntaf a ddefnyddiwyd oedd symudolnet v1, y mae nodweddion y rhain yn cael eu trafod yn hyn neges. Mae wedi'i gynnwys fel safon keras ac, yn unol â hynny, ar gael yn y pecyn o'r un enw ar gyfer R. Ond wrth geisio ei ddefnyddio gyda delweddau un sianel, trodd peth rhyfedd allan: rhaid i'r tensor mewnbwn bob amser fod â'r dimensiwn (batch, height, width, 3), hynny yw, ni ellir newid nifer y sianeli. Nid oes unrhyw gyfyngiad o'r fath yn Python, felly fe wnaethom ruthro ac ysgrifennu ein gweithrediad ein hunain o'r bensaernïaeth hon, gan ddilyn yr erthygl wreiddiol (heb y cwymp sydd yn fersiwn keras):

Pensaernïaeth Mobilenet v1

library(keras)

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

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

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

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

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

  inputs <- layer_input(shape = input_shape)

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

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

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

    return(model)
}

Mae anfanteision y dull hwn yn amlwg. Rwyf am brofi llawer o fodelau, ond i'r gwrthwyneb, nid wyf am ailysgrifennu pob pensaernïaeth â llaw. Cawsom hefyd ein hamddifadu o'r cyfle i ddefnyddio pwysau modelau a hyfforddwyd ymlaen llaw ar imagenet. Yn ôl yr arfer, roedd astudio'r ddogfennaeth o gymorth. Swyddogaeth get_config() caniatáu i chi gael disgrifiad o'r model mewn ffurf sy'n addas i'w olygu (base_model_conf$layers - rhestr R rheolaidd), a'r swyddogaeth from_config() yn perfformio'r trawsnewidiad cefn i wrthrych model:

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)

Nawr nid yw'n anodd ysgrifennu swyddogaeth gyffredinol i gael dim o'r cyflenwad a gyflenwir keras modelau gyda neu heb bwysau wedi'u hyfforddi ar imagenet:

Swyddogaeth ar gyfer llwytho pensaernïaeth parod

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

Wrth ddefnyddio delweddau un sianel, ni ddefnyddir unrhyw bwysau wedi'u hyfforddi ymlaen llaw. Gallai hyn fod yn sefydlog: defnyddio'r swyddogaeth get_weights() cael y pwysau model ar ffurf rhestr o araeau R, newid dimensiwn elfen gyntaf y rhestr hon (trwy gymryd un sianel lliw neu gyfartaleddu'r tri), ac yna llwythwch y pwysau yn ôl i'r model gyda'r swyddogaeth set_weights(). Ni wnaethom byth ychwanegu'r swyddogaeth hon, oherwydd ar hyn o bryd roedd yn amlwg eisoes ei bod yn fwy cynhyrchiol gweithio gyda lluniau lliw.

Fe wnaethom gynnal y rhan fwyaf o'r arbrofion gan ddefnyddio fersiynau mobilenet 1 a 2, yn ogystal ag resnet34. Perfformiodd pensaernïaeth fwy modern fel SE-ResNeXt yn dda yn y gystadleuaeth hon. Yn anffodus, nid oedd gennym weithrediadau parod ar gael inni, ac ni wnaethom ysgrifennu ein rhai ein hunain (ond byddwn yn bendant yn ysgrifennu).

5. Parameterization o sgriptiau

Er hwylustod, dyluniwyd yr holl god ar gyfer dechrau hyfforddiant fel un sgript, wedi'i baramedroli gan ddefnyddio docopt fel a ganlyn:

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)

Pecyn docopt cynrychioli’r gweithredu http://docopt.org/ ar gyfer R. Gyda'i help, mae sgriptiau'n cael eu lansio gyda gorchmynion syml fel Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db neu ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, os ffeil train_nn.R yn weithredadwy (bydd y gorchymyn hwn yn dechrau hyfforddi'r model resnet50 ar ddelweddau tri lliw yn mesur 128x128 picsel, rhaid lleoli'r gronfa ddata yn y ffolder /home/andrey/doodle_db). Gallwch ychwanegu cyflymder dysgu, math optimizer, ac unrhyw baramedrau customizable eraill at y rhestr. Yn y broses o baratoi'r cyhoeddiad, mae'n troi allan bod y bensaernïaeth mobilenet_v2 o'r fersiwn gyfredol keras mewn defnydd R Ni ddylai oherwydd newidiadau na chymerwyd i ystyriaeth yn y pecyn R, rydym yn aros iddynt ei drwsio.

Roedd y dull hwn yn ei gwneud hi'n bosibl cyflymu arbrofion yn sylweddol gyda modelau gwahanol o gymharu â lansiad mwy traddodiadol sgriptiau yn RStudio (nodwn y pecyn fel dewis arall posibl tfruns). Ond y brif fantais yw'r gallu i reoli lansiad sgriptiau yn Docker yn hawdd neu'n syml ar y gweinydd, heb osod RStudio ar gyfer hyn.

6. Dockerization o sgriptiau

Fe wnaethom ddefnyddio Docker i sicrhau hygludedd yr amgylchedd ar gyfer modelau hyfforddi rhwng aelodau'r tîm ac ar gyfer eu defnyddio'n gyflym yn y cwmwl. Gallwch chi ddechrau dod yn gyfarwydd â'r offeryn hwn, sy'n gymharol anarferol i raglennydd R, gyda hwn cyfres o gyhoeddiadau neu cwrs fideo.

Mae Docker yn caniatáu ichi greu eich delweddau eich hun o'r dechrau a defnyddio delweddau eraill fel sail ar gyfer creu rhai eich hun. Wrth ddadansoddi'r opsiynau sydd ar gael, daethom i'r casgliad bod gosod gyrwyr NVIDIA, CUDA + cuDNN a llyfrgelloedd Python yn rhan eithaf swmpus o'r ddelwedd, a phenderfynom gymryd y ddelwedd swyddogol fel sail. tensorflow/tensorflow:1.12.0-gpu, gan ychwanegu'r pecynnau R angenrheidiol yno.

Roedd ffeil y docwr terfynol yn edrych fel hyn:

Dockerfile

FROM tensorflow/tensorflow:1.12.0-gpu

MAINTAINER Artem Klevtsov <[email protected]>

SHELL ["/bin/bash", "-c"]

ARG LOCALE="en_US.UTF-8"
ARG APT_PKG="libopencv-dev r-base r-base-dev littler"
ARG R_BIN_PKG="futile.logger checkmate data.table rcpp rapidjsonr dbi keras jsonlite curl digest remotes"
ARG R_SRC_PKG="xtensor RcppThread docopt MonetDBLite"
ARG PY_PIP_PKG="keras"
ARG DIRS="/db /app /app/data /app/models /app/logs"

RUN source /etc/os-release && 
    echo "deb https://cloud.r-project.org/bin/linux/ubuntu ${UBUNTU_CODENAME}-cran35/" > /etc/apt/sources.list.d/cran35.list && 
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E084DAB9 && 
    add-apt-repository -y ppa:marutter/c2d4u3.5 && 
    add-apt-repository -y ppa:timsc/opencv-3.4 && 
    apt-get update && 
    apt-get install -y locales && 
    locale-gen ${LOCALE} && 
    apt-get install -y --no-install-recommends ${APT_PKG} && 
    ln -s /usr/lib/R/site-library/littler/examples/install.r /usr/local/bin/install.r && 
    ln -s /usr/lib/R/site-library/littler/examples/install2.r /usr/local/bin/install2.r && 
    ln -s /usr/lib/R/site-library/littler/examples/installGithub.r /usr/local/bin/installGithub.r && 
    echo 'options(Ncpus = parallel::detectCores())' >> /etc/R/Rprofile.site && 
    echo 'options(repos = c(CRAN = "https://cloud.r-project.org"))' >> /etc/R/Rprofile.site && 
    apt-get install -y $(printf "r-cran-%s " ${R_BIN_PKG}) && 
    install.r ${R_SRC_PKG} && 
    pip install ${PY_PIP_PKG} && 
    mkdir -p ${DIRS} && 
    chmod 777 ${DIRS} && 
    rm -rf /tmp/downloaded_packages/ /tmp/*.rds && 
    rm -rf /var/lib/apt/lists/*

COPY utils /app/utils
COPY src /app/src
COPY tests /app/tests
COPY bin/*.R /app/

ENV DBDIR="/db"
ENV CUDA_HOME="/usr/local/cuda"
ENV PATH="/app:${PATH}"

WORKDIR /app

VOLUME /db
VOLUME /app

CMD bash

Er hwylustod, rhoddwyd y pecynnau a ddefnyddiwyd mewn newidynnau; mae mwyafrif y sgriptiau ysgrifenedig yn cael eu copïo y tu mewn i'r cynwysyddion yn ystod y gwasanaeth. Fe wnaethom hefyd newid y plisgyn gorchymyn i /bin/bash er hwylustod defnyddio cynnwys /etc/os-release. Roedd hyn yn osgoi'r angen i nodi'r fersiwn OS yn y cod.

Yn ogystal, ysgrifennwyd sgript bash fach sy'n eich galluogi i lansio cynhwysydd gyda gorchmynion amrywiol. Er enghraifft, gallai'r rhain fod yn sgriptiau ar gyfer hyfforddi rhwydweithiau niwral a osodwyd yn flaenorol y tu mewn i'r cynhwysydd, neu gragen orchymyn ar gyfer dadfygio a monitro gweithrediad y cynhwysydd:

Sgript i lansio'r cynhwysydd

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

Os yw'r sgript bash hon yn cael ei redeg heb baramedrau, bydd y sgript yn cael ei alw y tu mewn i'r cynhwysydd train_nn.R gyda gwerthoedd diofyn; os mai "bash" yw'r ddadl leoliadol gyntaf, yna bydd y cynhwysydd yn dechrau'n rhyngweithiol gyda chragen gorchymyn. Ym mhob achos arall, amnewidir gwerthoedd dadleuon lleoliadol: CMD="Rscript /app/train_nn.R $@".

Mae'n werth nodi bod y cyfeiriaduron gyda data ffynhonnell a chronfa ddata, yn ogystal â'r cyfeiriadur ar gyfer arbed modelau hyfforddedig, wedi'u gosod y tu mewn i'r cynhwysydd o'r system westeiwr, sy'n eich galluogi i gael mynediad at ganlyniadau'r sgriptiau heb driniaethau diangen.

7. Defnyddio GPUs lluosog ar Google Cloud

Un o nodweddion y gystadleuaeth oedd y data swnllyd iawn (gweler y llun teitl, wedi ei fenthyg o @Leigh.plt o ODS slack). Mae sypiau mawr yn helpu i frwydro yn erbyn hyn, ac ar ôl arbrofion ar gyfrifiadur personol gydag 1 GPU, fe benderfynon ni feistroli modelau hyfforddi ar sawl GPU yn y cwmwl. Wedi defnyddio GoogleCloud (arweiniad da i'r pethau sylfaenol) oherwydd y dewis mawr o gyfluniadau sydd ar gael, prisiau rhesymol a bonws o $300. Allan o drachwant, archebais enghraifft 4xV100 gyda SSD a thunnell o RAM, ac roedd hynny'n gamgymeriad mawr. Mae peiriant o'r fath yn bwyta arian yn gyflym; gallwch chi fynd i arbrofi heb unrhyw biblinell brofedig. At ddibenion addysgol, mae'n well cymryd y K80. Ond daeth y swm mawr o RAM yn ddefnyddiol - ni wnaeth yr SSD cwmwl argraff ar ei berfformiad, felly trosglwyddwyd y gronfa ddata i dev/shm.

O ddiddordeb mwyaf yw'r darn cod sy'n gyfrifol am ddefnyddio GPUs lluosog. Yn gyntaf, mae'r model yn cael ei greu ar y CPU gan ddefnyddio rheolwr cyd-destun, yn union fel yn Python:

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

Yna mae'r model heb ei lunio (mae hyn yn bwysig) yn cael ei gopïo i nifer benodol o GPUs sydd ar gael, a dim ond ar ôl hynny y caiff ei lunio:

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

Ni ellid gweithredu'r dechneg glasurol o rewi pob haen ac eithrio'r un olaf, hyfforddi'r haen olaf, dadrewi ac ailhyfforddi'r model cyfan ar gyfer sawl GPU.

Roedd hyfforddiant yn cael ei fonitro heb ei ddefnyddio. bwrdd tensor, gan gyfyngu ein hunain i gofnodi logiau ac arbed modelau gydag enwau llawn gwybodaeth ar ôl pob cyfnod:

Galwadau yn ôl

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

Nid yw nifer o broblemau yr ydym wedi dod ar eu traws wedi’u goresgyn eto:

  • в keras nid oes swyddogaeth parod ar gyfer chwilio'n awtomatig am y gyfradd ddysgu optimaidd (analog lr_finder yn y llyfrgell cyflym.ai); Gyda pheth ymdrech, mae'n bosibl trosglwyddo gweithrediadau trydydd parti i R, er enghraifft, hyn;
  • o ganlyniad i'r pwynt blaenorol, nid oedd yn bosibl dewis y cyflymder hyfforddi cywir wrth ddefnyddio sawl GPU;
  • mae diffyg saernïaeth rhwydwaith niwral modern, yn enwedig y rhai sydd wedi'u hyfforddi ymlaen llaw ar imagenet;
  • polisi dim un cylch a chyfraddau dysgu gwahaniaethol (roedd anelio cosin ar ein cais ni gweithredu, diolch skeydan).

Pa bethau defnyddiol a ddysgwyd o’r gystadleuaeth hon:

  • Ar galedwedd pŵer cymharol isel, gallwch weithio gyda chyfeintiau data gweddus (llawer gwaith maint RAM) heb boen. Bag plastig data.tabl yn arbed cof oherwydd addasu tablau yn eu lle, sy'n osgoi eu copïo, a phan gânt eu defnyddio'n gywir, mae ei alluoedd bron bob amser yn dangos y cyflymder uchaf ymhlith yr holl offer sy'n hysbys i ni ar gyfer ieithoedd sgriptio. Mae arbed data mewn cronfa ddata yn caniatáu ichi, mewn llawer o achosion, beidio â meddwl o gwbl am yr angen i wasgu'r set ddata gyfan i RAM.
  • Gellir disodli swyddogaethau araf yn R â rhai cyflym yn C ++ gan ddefnyddio'r pecyn Rcpp. Os yn ychwanegol at ddefnydd RcppThread neu RcppParallel, rydym yn cael gweithrediadau traws-lwyfan aml-edau, felly nid oes angen i parallelize y cod ar y lefel R.
  • Pecyn Rcpp Gellir ei ddefnyddio heb wybodaeth ddifrifol am C++, amlinellir yr isafswm gofynnol yma. Ffeiliau pennawd ar gyfer nifer o lyfrgelloedd C cŵl fel xtensor ar gael ar CRAN, hynny yw, mae seilwaith yn cael ei ffurfio ar gyfer gweithredu prosiectau sy'n integreiddio cod C ++ perfformiad uchel parod yn R. Cyfleustra ychwanegol yw tynnu sylw at gystrawen a dadansoddwr cod C ++ statig yn RStudio.
  • docopt yn eich galluogi i redeg sgriptiau hunangynhwysol gyda pharamedrau. Mae hwn yn gyfleus i'w ddefnyddio ar weinydd pell, gan gynnwys. dan docker. Yn RStudio, mae'n anghyfleus cynnal oriau lawer o arbrofion gyda hyfforddi rhwydweithiau niwral, ac nid yw gosod y DRhA ar y gweinydd ei hun bob amser yn gyfiawn.
  • Mae Docker yn sicrhau hygludedd cod ac atgynhyrchu canlyniadau rhwng datblygwyr â gwahanol fersiynau o'r OS a llyfrgelloedd, yn ogystal â rhwyddineb gweithredu ar weinyddion. Gallwch chi lansio'r biblinell hyfforddi gyfan gydag un gorchymyn yn unig.
  • Mae Google Cloud yn ffordd gyfeillgar i'r gyllideb i arbrofi ar galedwedd drud, ond mae angen i chi ddewis ffurfweddiadau yn ofalus.
  • Mae mesur cyflymder darnau cod unigol yn ddefnyddiol iawn, yn enwedig wrth gyfuno R a C ++, a gyda'r pecyn mainc - hefyd yn hawdd iawn.

Ar y cyfan, roedd y profiad hwn yn werth chweil ac rydym yn parhau i weithio i ddatrys rhai o'r materion a godwyd.

Ffynhonnell: hab.com

Ychwanegu sylw