Quick Draw Doodle тану: R, C++ және нейрондық желілермен қалай достасуға болады

Quick Draw Doodle тану: R, C++ және нейрондық желілермен қалай достасуға болады

Эй Хабр!

Өткен жылдың күзінде Kaggle қолмен салынған суреттерді жіктеуге арналған, Quick Draw Doodle тану байқауын өткізді, оған басқалармен қатар R-ғалымдар тобы қатысты: Артем Клевцова, Филипп менеджері и Андрей Огурцов. Біз жарысты егжей-тегжейлі сипаттамаймыз, ол қазірдің өзінде жасалған соңғы жарияланым.

Бұл жолы медаль шаруашылығымен жұмыс істемеді, бірақ көптеген құнды тәжірибе жинақталды, сондықтан мен қоғамдастыққа Kagle және күнделікті жұмыстағы ең қызықты және пайдалы нәрселер туралы айтқым келеді. Талқыланған тақырыптардың ішінде: онсыз қиын өмір OpenCV, JSON талдауы (бұл мысалдар C++ кодының сценарийлерге немесе R тіліндегі бумаларға біріктірілуін қарастырады. Rcpp), сценарийлерді параметрлеу және соңғы шешімді докерлеу. Орындауға жарамды пішіндегі хабарламаның барлық коды қол жетімді репозиторийлер.

Мазмұны:

  1. CSV-ден MonetDB-ге деректерді тиімді жүктеңіз
  2. Топтамаларды дайындау
  3. Дерекқордан партияларды түсіруге арналған итераторлар
  4. Модельдік архитектураны таңдау
  5. Скриптті параметрлеу
  6. Скрипттерді докеризациялау
  7. Google Cloud жүйесінде бірнеше GPU пайдалану
  8. Орнына жасасу

1. CSV дерекқорынан MonetDB дерекқорына деректерді тиімді жүктеңіз

Бұл байқаудағы деректер дайын кескіндер түрінде емес, нүктелік координаттары бар JSON файлдары бар 340 CSV файлы (әр сынып үшін бір файл) түрінде берілген. Осы нүктелерді сызықтармен байланыстыру арқылы біз 256x256 пиксельді өлшейтін соңғы кескінді аламыз. Сондай-ақ әрбір жазба үшін деректер жиынтығын жинау кезінде пайдаланылған классификатор суретті дұрыс таныған-танбағанын көрсететін белгі, сурет авторының тұрғылықты жерінің екі әріптік коды, бірегей идентификатор, уақыт белгісі бар. және файл атауына сәйкес келетін сынып атауы. Бастапқы деректердің жеңілдетілген нұсқасы мұрағаттағы салмағы 7.4 ГБ және қаптаманы ашқаннан кейін шамамен 20 ГБ құрайды, орамнан шығарғаннан кейін толық деректер 240 ГБ алады. Ұйымдастырушылар екі нұсқаның бірдей сызбаларды шығаруын қамтамасыз етті, яғни толық нұсқасы артық болды. Қалай болғанда да, графикалық файлдарда немесе массивтер түрінде 50 миллион кескінді сақтау бірден тиімсіз деп саналды және біз мұрағаттағы барлық CSV файлдарын біріктіруді шештік. train_simplified.zip әр топтама үшін қажетті көлемдегі кескіндерді кейіннен генерациялаумен мәліметтер базасына енгізу.

ДҚБЖ ретінде жақсы дәлелденген жүйе таңдалды MonetDB, атап айтқанда R үшін пакет ретінде іске асыру MonetDBLite. Пакет дерекқор серверінің ендірілген нұсқасын қамтиды және серверді тікелей R сеансынан алуға және онымен сол жерде жұмыс істеуге мүмкіндік береді. Мәліметтер қорын құру және оған қосылу бір командамен орындалады:

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

Бізге екі кесте жасау керек болады: біреуі барлық деректер үшін, екіншісі жүктелген файлдар туралы қызметтік ақпарат үшін (егер бірдеңе дұрыс болмаса және процесті бірнеше файлды жүктеп алғаннан кейін қайта бастау керек болса пайдалы):

Кестелерді жасау

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

Дерекқорға деректерді жүктеудің ең жылдам жолы CSV файлдарын SQL пәрмені арқылы тікелей көшіру болды COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTқайда tablename - кесте атауы және path - файлға апаратын жол. Мұрағатпен жұмыс істеу барысында кірістірілген іске асыру екені анықталды unzip R ішінде мұрағаттағы бірқатар файлдармен дұрыс жұмыс істемейді, сондықтан біз жүйені қолдандық unzip (параметрін пайдалану getOption("unzip")).

Мәліметтер қорына жазу функциясы

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

Егер кестені дерекқорға жазбас бұрын түрлендіру қажет болса, аргументте өту жеткілікті preprocess деректерді түрлендіретін функция.

Дерекқорға деректерді дәйекті жүктеуге арналған код:

Мәліметтер базасына мәліметтерді жазу

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

Деректерді жүктеу уақыты пайдаланылатын дискінің жылдамдық сипаттамаларына байланысты өзгеруі мүмкін. Біздің жағдайда бір SSD ішінде немесе флэш-дисктен (бастапқы файл) SSD-ге (ДҚ) оқу және жазу 10 минуттан аз уақытты алады.

Бүтін класс белгісі және индекс бағанасы бар бағанды ​​жасау үшін тағы бірнеше секунд қажет болады (ORDERED INDEX) партияларды құру кезінде бақылаулар іріктелетін жол нөмірлерімен:

Қосымша бағандар мен индекс құру

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

Топтаманы жылдам құру мәселесін шешу үшін кестеден кездейсоқ жолдарды шығарудың максималды жылдамдығына жету керек болды. doodles. Ол үшін біз 3 трюк қолдандық. Біріншісі бақылау идентификаторын сақтайтын түрдің өлшемділігін азайту болды. Бастапқы деректер жинағында идентификаторды сақтау үшін қажетті түр болып табылады bigint, бірақ бақылаулар саны олардың реттік санға тең идентификаторларын түрге сәйкестендіруге мүмкіндік береді. int. Бұл жағдайда іздеу әлдеқайда жылдам. Екінші трюк пайдалану болды ORDERED INDEX — Біз бұл шешімге барлық қолда барды бастан өткере отырып, тәжірибелік жолмен келдік нұсқалары. Үшіншісі параметрленген сұрауларды пайдалану болды. Әдістің мәні - команданы бір рет орындау PREPARE бір типтегі сұраулар топтамасын жасау кезінде дайындалған өрнекті кейіннен қолданумен, бірақ іс жүзінде қарапайыммен салыстырғанда артықшылығы бар SELECT статистикалық қателік шегінде болып шықты.

Деректерді жүктеу процесі 450 МБ жедел жадты тұтынады. Яғни, сипатталған тәсіл салмағы ондаған гигабайт болатын деректер жиынын кез келген дерлік бюджеттік жабдықта, соның ішінде кейбір бір тақталы құрылғыларда жылжытуға мүмкіндік береді, бұл өте жақсы.

Әртүрлі өлшемдегі партияларды іріктеу кезінде деректерді алу (кездейсоқ) жылдамдығын өлшеу және масштабтауды бағалау ғана қалады:

Мәліметтер базасының эталоны

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 тану: R, C++ және нейрондық желілермен қалай достасуға болады

2. Топтамаларды дайындау

Топтаманы дайындаудың бүкіл процесі келесі қадамдардан тұрады:

  1. Нүктелердің координаталары бар жолдар векторлары бар бірнеше JSON талдауы.
  2. Қажетті өлшемдегі кескіндегі нүктелердің координаталары негізінде түрлі-түсті сызықтарды салу (мысалы, 256×256 немесе 128×128).
  3. Алынған кескіндерді тензорға түрлендіру.

Python ядролары арасындағы бәсекелестік аясында мәселе ең алдымен қолдану арқылы шешілді OpenCV. R тіліндегі ең қарапайым және айқын аналогтардың бірі келесідей болады:

R тілінде JSON тензор түрлендіруін енгізу

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

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

Сурет стандартты R құралдары арқылы орындалады және жедел жадта сақталған уақытша PNG файлына сақталады (Linux жүйесінде уақытша R каталогтары каталогта орналасқан. /tmp, жедел жадқа орнатылған). Содан кейін бұл файл 0-ден 1-ге дейінгі сандары бар үш өлшемді массив ретінде оқылады. Бұл маңызды, өйткені әдеттегі BMP он алтылық түс кодтары бар өңделмеген массивте оқылады.

Нәтижені сынап көрейік:

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 тану: R, C++ және нейрондық желілермен қалай достасуға болады

Топтаманың өзі келесідей қалыптасады:

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

Бұл іске асыру бізге оңтайлы болып көрінді, өйткені үлкен партияларды қалыптастыру өте ұзақ уақытты алады және біз қуатты кітапхананы пайдалану арқылы әріптестеріміздің тәжірибесін пайдалануды шештік. OpenCV. Ол кезде R үшін дайын пакет болған жоқ (қазір жоқ), сондықтан қажетті функционалдылықтың минималды орындалуы C++ тілінде R кодына біріктіру арқылы жазылған. Rcpp.

Мәселені шешу үшін келесі пакеттер мен кітапханалар пайдаланылды:

  1. OpenCV кескіндермен және сызықтармен жұмыс істеуге арналған. Алдын ала орнатылған жүйелік кітапханалар мен тақырып файлдары, сондай-ақ динамикалық байланыстыру қолданылады.

  2. кстензор көпөлшемді массивтермен және тензорлармен жұмыс істеуге арналған. Біз аттас R бумасына енгізілген тақырып файлдарын қолдандық. Кітапхана көпөлшемді массивтермен негізгі қатарда да, негізгі баған тәртібінде де жұмыс істеуге мүмкіндік береді.

  3. ndjson JSON талдауы үшін. Бұл кітапханада пайдаланылады кстензор ол жобада бар болса, автоматты түрде.

  4. RcppThread JSON векторының көп ағынды өңдеуін ұйымдастыруға арналған. Осы бумамен қамтамасыз етілген тақырып файлдары пайдаланылды. Танымалдан RcppParallel Пакетте, басқалармен қатар, кірістірілген циклды үзу механизмі бар.

Ол Айта кету керек, кстензор құдайдың сыйы болды: оның кең функционалдығы мен жоғары өнімділігімен қатар, оны әзірлеушілер өте жауапты болып шықты және сұрақтарға тез және егжей-тегжейлі жауап берді. Олардың көмегімен OpenCV матрицаларын ктензор тензорларына түрлендіруді, сондай-ақ 3 өлшемді кескін тензорларын дұрыс өлшемдегі 4 өлшемді тензорға біріктіру тәсілін (партияның өзі) жүзеге асыру мүмкін болды.

Rcpp, xtensor және 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

Жүйелік файлдарды және жүйеде орнатылған кітапханалармен динамикалық байланыстыруды пайдаланатын файлдарды құрастыру үшін біз пакетте енгізілген плагин механизмін қолдандық. Rcpp. Жолдар мен жалауларды автоматты түрде табу үшін біз танымал Linux утилитасын қолдандық pkg-конфигурациясы.

OpenCV кітапханасын пайдалану үшін Rcpp плагинін енгізу

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

Плагин жұмысының нәтижесінде компиляция процесінде келесі мәндер ауыстырылады:

Rcpp:::.plugins$opencv()$env

# $PKG_CXXFLAGS
# [1] "-I/usr/include/opencv"
#
# $PKG_LIBS
# [1] "-lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_datasets -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_line_descriptor -lopencv_optflow -lopencv_video -lopencv_plot -lopencv_reg -lopencv_saliency -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_rgbd -lopencv_viz -lopencv_surface_matching -lopencv_text -lopencv_ximgproc -lopencv_calib3d -lopencv_features2d -lopencv_flann -lopencv_xobjdetect -lopencv_objdetect -lopencv_ml -lopencv_xphoto -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -lopencv_photo -lopencv_imgproc -lopencv_core"

JSON талдауына арналған іске асыру коды және үлгіге жіберу үшін пакетті генерациялау спойлер астында берілген. Алдымен тақырып файлдарын іздеу үшін жергілікті жоба каталогын қосыңыз (ndjson үшін қажет):

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

C++ тілінде JSON тензорлық түрлендіруді жүзеге асыру

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

Бұл кодты файлға орналастыру керек src/cv_xt.cpp және пәрменімен құрастырыңыз Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); жұмыс үшін де қажет nlohmann/json.hpp -дан репозиторий. Код бірнеше функцияларға бөлінеді:

  • to_xt — кескін матрицасын түрлендіруге арналған шаблондық функция (cv::Mat) тензорға xt::xtensor;

  • parse_json — функция JSON жолын талдайды, нүктелердің координаталарын шығарады, оларды векторға салады;

  • ocv_draw_lines — нүктелердің алынған векторынан түрлі-түсті сызықтарды салады;

  • process — жоғарыда аталған функцияларды біріктіреді, сонымен қатар алынған кескінді масштабтау мүмкіндігін қосады;

  • cpp_process_json_str - функцияның үстінен орау process, ол нәтижені R-объектіге экспорттайды (көпөлшемді массив);

  • cpp_process_json_vector - функцияның үстінен орау cpp_process_json_str, ол көп ағынды режимде жол векторын өңдеуге мүмкіндік береді.

Көп түсті сызықтарды салу үшін HSV түс үлгісі қолданылды, содан кейін RGB түрлендіру. Нәтижені сынап көрейік:

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

Quick Draw Doodle тану: R, C++ және нейрондық желілермен қалай достасуға болады
R және 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") 

Quick Draw Doodle тану: R, C++ және нейрондық желілермен қалай достасуға болады

Көріп отырғаныңыздай, жылдамдықты арттыру өте маңызды болды және R кодын параллельдеу арқылы C++ кодын қуып жету мүмкін емес.

3. Дерекқордан партияларды түсіруге арналған итераторлар

R оперативті жадыға сәйкес келетін деректерді өңдеу бойынша лайықты беделге ие, ал Python деректердің қайталанатын өңделуімен сипатталады, бұл негізгіден тыс есептеулерді (сыртқы жадты пайдалану арқылы есептеулер) оңай және табиғи түрде жүзеге асыруға мүмкіндік береді. Сипатталған мәселе контекстіндегі біз үшін классикалық және өзекті мысал - бақылаулардың шағын бөлігін немесе шағын топтаманы пайдалана отырып, әрбір қадамда градиентті жақындату арқылы градиентті түсіру әдісімен оқытылатын терең нейрондық желілер.

Python тілінде жазылған терең оқыту шеңберлерінде деректерге негізделген итераторларды іске асыратын арнайы сыныптар бар: кестелер, қалталардағы суреттер, екілік пішімдер және т.б. Сіз дайын опцияларды пайдалана аласыз немесе нақты тапсырмалар үшін өзіңізді жаза аласыз. R тілінде біз Python кітапханасының барлық мүмкіндіктерін пайдалана аламыз кера өз кезегінде пакеттің үстінде жұмыс істейтін аттас буманы пайдаланатын әртүрлі серверлерімен торлы. Соңғысы бөлек ұзақ мақалаға лайық; ол R-ден Python кодын іске қосуға мүмкіндік беріп қана қоймайды, сонымен қатар барлық қажетті түр түрлендірулерін автоматты түрде орындай отырып, R және Python сеанстары арасында нысандарды тасымалдауға мүмкіндік береді.

MonetDBLite көмегімен біз барлық деректерді жедел жадта сақтау қажеттілігінен құтылдық, барлық «нейрондық желі» жұмысы Python-дағы бастапқы кодпен орындалады, деректердің үстінен итераторды жазу керек, өйткені дайын ештеңе жоқ. мұндай жағдай үшін R немесе Python. Оған тек екі ғана талап бар: ол шексіз циклде топтамаларды қайтаруы және итерациялар арасында күйін сақтауы керек (соңғы R-де жабуларды қолдану арқылы қарапайым түрде жүзеге асырылады). Бұрын итератор ішіндегі R массивтерін сандық массивтерге нақты түрлендіру қажет болды, бірақ пакеттің ағымдағы нұсқасы кера өзі жасайды.

Жаттығу және тексеру деректеріне арналған итератор келесідей болды:

Жаттығу және тексеру деректеріне арналған итератор

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

Функция деректер базасына қосылымы бар айнымалы мәнді, пайдаланылған жолдар санын, сыныптар санын, пакет өлшемін, масштабты енгізу ретінде қабылдайды.scale = 1 256x256 пиксельдік кескіндерді көрсетуге сәйкес келеді, scale = 0.5 — 128x128 пиксель, түсті индикатор (color = FALSE пайдаланған кезде сұр реңкте көрсетуді көрсетеді color = TRUE әрбір штрих жаңа түспен сызылады) және imagenet желісінде алдын ала дайындалған желілер үшін алдын ала өңдеу көрсеткіші. Соңғысы пиксель мәндерін [0, 1] аралығынан [-1, 1] аралығына дейін масштабтау үшін қажет, ол жеткізілгенді үйрету кезінде қолданылады. кера модельдер.

Сыртқы функцияда аргумент түрін тексеру, кесте бар data.table бастап кездейсоқ араласқан жол нөмірлерімен samples_index және топтама нөмірлері, есептегіш және партиялардың максималды саны, сонымен қатар дерекқордан деректерді түсіруге арналған SQL өрнегі. Сонымен қатар, біз ішіндегі функцияның жылдам аналогын анықтадық keras::to_categorical(). Біз барлық дерлік деректерді оқыту үшін қолдандық, жарты пайызын валидацияға қалдырдық, сондықтан дәуір өлшемі параметрмен шектелді. steps_per_epoch шақырғанда keras::fit_generator(), және шарт if (i > max_i) тек тексеру итераторы үшін жұмыс істеді.

Ішкі функцияда жол индекстері келесі бума үшін шығарылады, жазбалар пакеттік есептегіштің ұлғаюымен дерекқордан босатылады, JSON талдауы (функция) cpp_process_json_vector(), C++ тілінде жазылған) және суреттерге сәйкес массивтер құру. Содан кейін класс белгілері бар бір ыстық векторлар жасалады, пиксельдік мәндері мен белгілері бар массивтер қайтарылатын мән болып табылатын тізімге біріктіріледі. Жұмысты жылдамдату үшін біз кестелерде индекстер құруды қолдандық data.table және сілтеме арқылы өзгерту - бұл «чиптер» пакетінсіз деректер кестесі R-де кез келген маңызды деректер көлемімен тиімді жұмыс істеуді елестету өте қиын.

Core i5 ноутбукіндегі жылдамдықты өлшеу нәтижелері келесідей:

Итератор эталоны

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 тану: R, C++ және нейрондық желілермен қалай достасуға болады

Егер сізде жедел жадтың жеткілікті мөлшері болса, оны дәл сол жедел жадқа көшіру арқылы дерекқордың жұмысын айтарлықтай жылдамдатуға болады (біздің тапсырмамыз үшін 32 ГБ жеткілікті). Linux жүйесінде бөлім әдепкі бойынша орнатылады /dev/shm, ЖЖҚ сыйымдылығының жартысына дейін алады. Өңдеу арқылы көбірек бөлектеуге болады /etc/fstabсияқты рекорд алу үшін tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Қайта жүктеп, пәрменді іске қосу арқылы нәтижені тексеріңіз df -h.

Сынақ деректеріне арналған итератор әлдеқайда қарапайым көрінеді, өйткені сынақ деректер жинағы толығымен жедел жадқа сәйкес келеді:

Сынақ деректеріне арналған итератор

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. Модельдік архитектураны таңдау

Ең бірінші архитектура қолданылды mobilenet v1, ерекшеліктері талқыланады бұл хабар. Ол стандартты түрде енгізілген кера және, тиісінше, R үшін аттас пакетте қол жетімді. Бірақ оны бір арналы кескіндермен пайдалануға тырысқанда, біртүрлі нәрсе шықты: кіріс тензоры әрқашан өлшемге ие болуы керек. (batch, height, width, 3), яғни арналар санын өзгерту мүмкін емес. Python-да мұндай шектеулер жоқ, сондықтан біз бастапқы мақалаға сәйкес (keras нұсқасындағы оқудан шығып кетудісіз) осы архитектураны өзіміз енгізуді асығыс жаздық:

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

Бұл әдістің кемшіліктері анық. Мен көптеген үлгілерді сынағым келеді, бірақ керісінше, әр архитектураны қолмен қайта жазғым келмейді. Біз сондай-ақ imagenet-те алдын ала дайындалған модельдердің салмағын пайдалану мүмкіндігінен айырылдық. Әдеттегідей, құжаттаманы зерттеу көмектесті. Функция get_config() өңдеуге қолайлы пішінде үлгінің сипаттамасын алуға мүмкіндік береді (base_model_conf$layers - кәдімгі R тізімі) және функция from_config() үлгі нысанына кері түрлендіруді орындайды:

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)

Енді берілгендердің кез келгенін алу үшін әмбебап функцияны жазу қиын емес кера Imagenet желісінде дайындалған салмақтары бар немесе жоқ үлгілер:

Дайын архитектураларды жүктеу функциясы

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

Бір арналы кескіндерді пайдалану кезінде алдын ала дайындалған салмақтар пайдаланылмайды. Мұны түзетуге болады: функцияны пайдалану get_weights() үлгі салмақтарын R массивтерінің тізімі түрінде алыңыз, осы тізімнің бірінші элементінің өлшемін өзгертіңіз (бір түсті арнаны алу немесе үшеуін де орташалау арқылы), содан кейін салмақтарды функциямен модельге қайта жүктеңіз. set_weights(). Біз бұл функцияны ешқашан қосқан жоқпыз, өйткені осы кезеңде түрлі-түсті суреттермен жұмыс істеу тиімдірек екені белгілі болды.

Біз тәжірибелердің көпшілігін mobilenet 1 және 2 нұсқаларын, сондай-ақ resnet34 арқылы жүргіздік. Бұл байқауда SE-ResNeXt сияқты заманауи архитектуралар жақсы өнер көрсетті. Өкінішке орай, біздің қолымызда дайын енгізулер болмады және біз өзімізді жазған жоқпыз (бірақ міндетті түрде жазамыз).

5. Сценарийлерді параметрлеу

Ыңғайлы болу үшін оқуды бастауға арналған барлық код бір сценарий ретінде құрастырылған, параметрленген құжаттама келесідей:

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)

Пакет құжаттама жүзеге асыруды білдіреді http://docopt.org/ үшін R. Оның көмегімен сценарийлер сияқты қарапайым пәрмендермен іске қосылады Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db немесе ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, файл болса train_nn.R орындалатын болады (бұл пәрмен үлгіні үйретуді бастайды resnet50 128x128 пиксель өлшеміндегі үш түсті кескіндерде дерекқор қалтада орналасуы керек /home/andrey/doodle_db). Тізімге оқу жылдамдығын, оңтайландыру түрін және кез келген басқа теңшелетін параметрлерді қосуға болады. Басылымды дайындау барысында сәулет болып шықты mobilenet_v2 ағымдағы нұсқадан кера R тілінде қолданылады емес тиіс R пакетінде ескерілмеген өзгерістерге байланысты біз олардың оны түзетуін күтеміз.

Бұл тәсіл RStudio-да сценарийлерді дәстүрлі іске қосумен салыстырғанда әртүрлі модельдермен эксперименттерді айтарлықтай жылдамдатуға мүмкіндік берді (біз пакетті ықтимал балама ретінде атап өтеміз) tfruns). Бірақ басты артықшылығы - бұл үшін RStudio-ны орнатпай-ақ, Docker-те немесе жай серверде сценарийлерді іске қосуды оңай басқару мүмкіндігі.

6. Сценарийлерді докеризациялау

Топ мүшелері арасында модельдерді оқыту және бұлтта жылдам орналастыру үшін ортаның тасымалдануын қамтамасыз ету үшін Docker қолдандық. R программисті үшін әдеттен тыс бұл құралмен танысуды бастауға болады бұл жарияланымдар сериясы немесе бейне курс.

Docker сізге өз суреттеріңізді нөлден жасауға және басқа кескіндерді өзіңізді құру үшін негіз ретінде пайдалануға мүмкіндік береді. Қол жетімді опцияларды талдай отырып, біз NVIDIA, CUDA+cuDNN драйверлерін және Python кітапханаларын орнату кескіннің жеткілікті көлемді бөлігі болып табылады деген қорытындыға келдік және біз ресми суретті негізге алуды шештік. tensorflow/tensorflow:1.12.0-gpu, қажетті R бумаларын қосыңыз.

Соңғы докер файлы келесідей болды:

Докер файлы

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

Ыңғайлы болу үшін пайдаланылған пакеттер айнымалыларға қойылды; жазбаша сценарийлердің негізгі бөлігі құрастыру кезінде контейнерлердің ішіне көшіріледі. Біз сондай-ақ пәрмен қабығын өзгерттік /bin/bash мазмұнды пайдаланудың қарапайымдылығы үшін /etc/os-release. Бұл кодта ОЖ нұсқасын көрсету қажеттілігінен аулақ болды.

Сонымен қатар, әртүрлі командалары бар контейнерді іске қосуға мүмкіндік беретін шағын bash сценарийі жазылды. Мысалы, бұл контейнер ішінде бұрын орналастырылған нейрондық желілерді оқытуға арналған сценарийлер немесе контейнер жұмысын жөндеуге және бақылауға арналған пәрмен қабығы болуы мүмкін:

Контейнерді іске қосу үшін сценарий

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

Егер бұл bash сценарийі параметрлерсіз іске қосылса, сценарий контейнер ішінде шақырылады train_nn.R әдепкі мәндерімен; егер бірінші позициялық аргумент «bash» болса, онда контейнер пәрмен қабығымен интерактивті түрде басталады. Барлық басқа жағдайларда позициялық аргументтердің мәндері ауыстырылады: CMD="Rscript /app/train_nn.R $@".

Айта кету керек, бастапқы деректер мен дерекқоры бар каталогтар, сондай-ақ оқытылған үлгілерді сақтауға арналған каталогтар хост жүйесінен контейнер ішінде орнатылған, бұл сценарийлердің нәтижелеріне қажетсіз манипуляцияларсыз қол жеткізуге мүмкіндік береді.

7. Google Cloud жүйесінде бірнеше GPU пайдалану

Байқаудың ерекшеліктерінің бірі өте шулы деректер болды (ODS slack сайтынан @Leigh.plt алынған тақырыптық суретті қараңыз). Үлкен партиялар мұнымен күресуге көмектеседі және 1 графикалық процессоры бар компьютерде эксперименттерден кейін біз бұлттағы бірнеше графикалық процессорлардағы оқу үлгілерін меңгеруді шештік. Қолданылған GoogleCloud (негіздеріне жақсы нұсқаулық) қолжетімді конфигурациялардың үлкен таңдауына, қолайлы бағаларға және $300 бонусқа байланысты. Ашкөздіктен мен SSD және бір тонна жедел жады бар 4xV100 данасына тапсырыс бердім, бұл үлкен қателік болды. Мұндай машина ақшаны тез жейді, сіз дәлелденген құбырсыз эксперимент жасай аласыз. Білім беру мақсатында K80 алған дұрыс. Бірақ оперативті жадтың үлкен көлемі пайдалы болды - бұлтты SSD өнімділігімен таңдандырмады, сондықтан дерекқор келесіге ауыстырылды dev/shm.

Ең үлкен қызығушылық бірнеше GPU пайдалану үшін жауапты код фрагменті болып табылады. Біріншіден, модель Python-дағы сияқты контекстік менеджер арқылы CPU-да жасалады:

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

Содан кейін құрастырылмаған (бұл маңызды) модель қол жетімді GPU-лардың берілген санына көшіріледі, содан кейін ғана ол құрастырылады:

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

Соңғысынан басқа барлық қабаттарды мұздату, соңғы қабатты оқыту, бірнеше графикалық процессорлар үшін бүкіл модельді мұздату және қайта даярлаудың классикалық әдісін жүзеге асыру мүмкін болмады.

Жаттығу пайдаланылмай бақыланды. тензор тақтасы, журналдарды жазумен және әр дәуірден кейін ақпараттық атаулары бар үлгілерді сақтаумен шектелеміз:

Кері қоңыраулар

# Шаблон имени файла лога
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. Қорытындының орнына

Біз кездескен бірқатар мәселелер әлі шешілген жоқ:

  • в кера Оңтайлы оқу жылдамдығын автоматты түрде іздеуге арналған дайын функция жоқ (аналог lr_finder кітапханада жылдам.ai); Кейбір күш-жігермен үшінші тарап енгізулерін R-ге тасымалдауға болады, мысалы, бұл;
  • алдыңғы тармақтың салдары ретінде бірнеше GPU пайдалану кезінде дұрыс жаттығу жылдамдығын таңдау мүмкін болмады;
  • қазіргі заманғы нейрондық желінің архитектурасы, әсіресе imagenet-те алдын ала дайындалған архитектуралардың жетіспеушілігі;
  • бірде-бір цикл саясаты және дискриминативті оқыту жылдамдығы (косинусты күйдіру біздің сұранысымыз бойынша болды жүзеге асырылды, Рақмет сізге скайдан).

Бұл байқаудан қандай пайдалы нәрселер үйренді:

  • Салыстырмалы түрде төмен қуатты аппараттық құралда сіз ауыртпалықсыз лайықты (RAM көлемінен бірнеше есе үлкен) деректер көлемімен жұмыс істей аласыз. Пластикалық пакет деректер кестесі кестелердің орнында модификациясы есебінен жадты үнемдейді, бұл оларды көшіруді болдырмайды және дұрыс пайдаланған кезде оның мүмкіндіктері сценарий тілдері үшін бізге белгілі барлық құралдардың арасында әрқашан дерлік ең жоғары жылдамдықты көрсетеді. Дерекқорда деректерді сақтау көп жағдайда барлық деректер жинағын жедел жадқа сығу қажеттілігі туралы мүлде ойламауға мүмкіндік береді.
  • R тіліндегі баяу функцияларды пакетті пайдаланып C++ тілінде жылдам функциялармен ауыстыруға болады Rcpp. Егер пайдалануға қосымша болса RcppThread немесе RcppParallel, біз кросс-платформалы көп ағынды іске асыруларды аламыз, сондықтан R деңгейінде кодты параллельдеудің қажеті жоқ.
  • Пакет Rcpp C++ тілін байыпты білмей пайдалануға болады, қажетті минимум көрсетілген осында. Бірқатар керемет C-кітапханаларына арналған тақырып файлдары кстензор CRAN жүйесінде қолжетімді, яғни дайын жоғары өнімді C++ кодын R-ге біріктіретін жобаларды жүзеге асыру үшін инфрақұрылым қалыптасуда. Қосымша ыңғайлылық - синтаксисті бөлектеу және RStudio-дағы статикалық C++ код анализаторы.
  • құжаттама параметрлері бар дербес сценарийлерді іске қосуға мүмкіндік береді. Бұл қашықтағы серверде пайдалану үшін ыңғайлы, соның ішінде. докер астында. RStudio-да нейрондық желілерді оқытумен көп сағаттық эксперименттер жүргізу ыңғайсыз, ал серверде IDE орнатудың өзі әрқашан ақтала бермейді.
  • Docker операциялық жүйенің әртүрлі нұсқалары мен кітапханалары бар әзірлеушілер арасында кодтың тасымалдануын және нәтижелердің қайталануын, сондай-ақ серверлерде орындаудың қарапайымдылығын қамтамасыз етеді. Бір ғана пәрменмен бүкіл жаттығу құбырын іске қосуға болады.
  • Google Cloud - қымбат жабдықта тәжірибе жасаудың бюджеттік тәсілі, бірақ конфигурацияларды мұқият таңдау керек.
  • Жеке код фрагменттерінің жылдамдығын өлшеу өте пайдалы, әсіресе R және C++ біріктіру кезінде және пакетпен орындық - сонымен қатар өте оңай.

Тұтастай алғанда, бұл тәжірибе өте пайдалы болды және біз көтерілген мәселелерді шешу үшін жұмысты жалғастырамыз.

Ақпарат көзі: www.habr.com

пікір қалдыру