Quick Draw Doodle таануу: R, C++ жана нейрон тармактары менен кантип достошсо болот

Quick Draw Doodle таануу: R, C++ жана нейрон тармактары менен кантип достошсо болот

Эй Хабр!

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

Бул жолу медаль чарбасы менен майнап чыккан жок, бирок бир топ баалуу тажрыйба топтолду, ошондуктан мен коомчулукка Kagle жана күнүмдүк жумушта бир катар эң кызыктуу жана пайдалуу нерселерди айткым келет. Талкууланган темалардын арасында: ансыз оор жашоо OpenCV, JSON талдоо (бул мисалдар C++ кодунун скрипттерге же пакеттерге интеграциясын карап чыгат. Rcpp), сценарийлерди параметрлештирүү жана акыркы чечимди докерлештирүү. Аткаруу үчүн ылайыктуу формадагы билдирүүнүн бардык коду жеткиликтүү репозиторийлер.

Мазмуну:

  1. CSVден MonetDBге маалыматтарды эффективдүү жүктөңүз
  2. Партияларды даярдоо
  3. Маалымат базасынан партияларды түшүрүү үчүн итераторлор
  4. Архитектура моделин тандоо
  5. Скрипт параметрлери
  6. Скрипттердин докеризациясы
  7. Google Булутта бир нече 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 трюк колдондук. Биринчиси, байкоо идентификаторун сактаган түрдүн өлчөмдүүлүгүн азайтуу болгон. Түпнуска маалымат топтомунда ID сактоо үчүн талап кылынган түрү 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деги эң жөнөкөй жана эң айкын аналогдордун бири мындай болот:

JSONди тензордук конверсияга 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)
}

Тартуу стандарттык R куралдарынын жардамы менен аткарылат жана RAMда сакталган убактылуу PNGге сакталат (Linux'та, убактылуу R каталогдору каталогдо жайгашкан. /tmp, RAMга орнотулган). Бул файл андан кийин 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. xtensor көп өлчөмдүү массивдер жана тензорлор менен иштөө үчүн. Ошол эле аталыштагы R пакетине кирген баш файлдарды колдондук. Китепкана көп өлчөмдүү массивдер менен негизги катарда да, негизги тилкеде да иштөөгө мүмкүндүк берет.

  3. ndjson JSON талдоо үчүн. Бул китепкана колдонулат xtensor ал долбоордо бар болсо, автоматтык түрдө.

  4. RcppThread JSONден векторду көп жиптүү иштетүүнү уюштуруу үчүн. Бул топтом тарабынан берилген баш файлдар колдонулган. Популярдуураак RcppParallel Пакет, башка нерселер менен катар, орнотулган циклди үзүү механизмине ээ.

Бул белгилей кетсек болот, xtensor бул кудайдын сыйы болуп чыкты: анын кеңири функционалдуулугу жана жогорку өндүрүмдүүлүгү бар экендигинен тышкары, аны иштеп чыгуучулар абдан жоопкер болуп чыкты жана суроолорго ыкчам жана деталдуу жооп беришти. Алардын жардамы менен 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-config.

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 китепканасынын бардык мүмкүнчүлүктөрүнөн пайдалана алабыз кызыл өз кезегинде пакеттин үстүндө иштейт, ошол эле аталыштагы пакетти колдонуп, анын ар кандай аркалары менен торчо. Акыркысы өзүнчө узун макалага татыктуу; ал сизге Python кодун Rден иштетүүгө гана мүмкүндүк бербестен, 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++ жана нейрон тармактары менен кантип достошсо болот

Эгерде сизде жетиштүү көлөмдөгү RAM бар болсо, анда сиз аны ошол эле RAMга өткөрүп берүү менен маалымат базасынын иштешин олуттуу түрдө тездете аласыз (биздин милдетибиз үчүн 32 ГБ жетиштүү). Linux'та бөлүм демейки боюнча орнотулган /dev/shm, RAM сыйымдуулугунун жарымына чейин ээлейт. Сиз түзөтүү менен көбүрөөк баса аласыз /etc/fstabсыяктуу рекорд алуу үчүн tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Кайра жүктөө жана буйрукту иштетүү менен натыйжаны текшерүүнү унутпаңыз df -h.

Сыноо маалыматтарынын итератору алда канча жөнөкөй көрүнөт, анткени тесттик маалыматтар топтому толугу менен RAMга туура келет:

Сыноо маалыматтары үчүн итератор

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/ for 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. Бул коддо OS версиясын көрсөтүү зарылдыгынан качты.

Кошумчалай кетсек, ар кандай буйруктар менен контейнерди ишке киргизүүгө мүмкүндүк берген кичинекей 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 Булутта бир нече GPU колдонуу

Мелдештин өзгөчөлүктөрүнүн бири абдан ызы-чуу маалыматтар болду (титулдук сүрөттү карагыла, ODS Slack тартып @Leigh.plt алынган). Чоң партиялар муну менен күрөшүүгө жардам берет жана 1 GPU менен компьютерде эксперименттерден кийин, биз булуттагы бир нече GPU боюнча окутуу моделдерин өздөштүрүү чечимин кабыл алды. Колдонулган GoogleCloud (негиздери үчүн жакшы жол) жеткиликтүү конфигурациялардын чоң тандоосуна, акылга сыярлык баага жана $300 бонуска байланыштуу. Ач көздүктөн мен SSD жана бир тонна RAM менен 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)
)

Акыркысынан башка бардык катмарларды тоңдуруп коюунун, акыркы катмарды үйрөтүүнүн, бир нече GPU үчүн бардык моделди тоңдурбоонун жана кайра даярдоонун классикалык ыкмасын ишке ашыруу мүмкүн эмес.

Колдонулбастан машыгууга мониторинг жүргүзүлдү. тензорборд, журналдарды жазуу жана ар бир доордон кийин маалыматтык аталыштар менен моделдерди сактоо менен чектелебиз:

Кайра чалуулар

# Шаблон имени файла лога
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 китепканада fast.ai); Кээ бир күч-аракет менен, үчүнчү тараптын ишке ашырууларын Rга өткөрүүгө болот, мисалы, бул;
  • мурунку пункттун натыйжасында, бир нече GPU колдонууда туура окутуу ылдамдыгын тандоо мүмкүн болгон жок;
  • заманбап нейрон тармактарынын архитектураларынын, айрыкча imagenetте алдын ала даярдалган архитектуралардын жетишсиздиги;
  • эч кимдин цикл саясаты жана дискриминациялык окутуу курстары (косинустарды күйдүрүү биздин өтүнүчүбүз боюнча болгон ишке ашырылган, Рахмат сага скайдан).

Бул сынактан кандай пайдалуу нерселер үйрөнүлдү:

  • Салыштырмалуу аз кубаттуу жабдыктарда сиз татыктуу (RAMдан көп эсе чоң) көлөмдөгү маалымат менен оорутпай иштей аласыз. Пластик баштык маалымат.стол жадыбалдарды жеринде модификациялоонун эсебинен эстутумду үнөмдөйт, бул аларды көчүрүүдөн качат жана туура колдонулганда анын мүмкүнчүлүктөрү дээрлик ар дайым скрипт тилдери үчүн бизге белгилүү болгон бардык куралдардын ичинен эң жогорку ылдамдыкты көрсөтөт. Маалыматтар базасында маалыматтарды сактоо, көп учурларда, бүт маалымат топтомун оперативдик эс тутумга кысуу зарылдыгы жөнүндө такыр ойлонбоого мүмкүндүк берет.
  • R тилиндеги жай функцияларды пакетти колдонуу менен C++ тилинде тез функциялар менен алмаштырууга болот Rcpp. колдонууга кошумча болсо RcppThread же RcppParallel, биз кайчылаш-платформалуу көп агымдуу ишке ашырууларды алабыз, ошондуктан R деңгээлинде кодду параллелдештирүүнүн кереги жок.
  • Пакет Rcpp C++ тилин олуттуу билбестен колдонсо болот, талап кылынган минимум көрсөтүлгөн бул жерде. сыяктуу бир катар сонун C-китепканалары үчүн баш файлдар xtensor CRANда жеткиликтүү, башкача айтканда, даяр жогорку натыйжалуу C++ кодун Rге интеграциялаган долбоорлорду ишке ашыруу үчүн инфраструктура түзүлүүдө. Кошумча ыңгайлуулук - бул синтаксисти бөлүп көрсөтүү жана RStudioдагы статикалык C++ код анализатору.
  • докопт параметрлери менен өз алдынча скрипттерди иштетүүгө мүмкүндүк берет. Бул алыскы серверде колдонууга ыңгайлуу, анын ичинде. докер астында. RStudioдо нейрондук тармактарды окутуу менен көп сааттык эксперименттерди өткөрүү ыңгайсыз жана сервердин өзүнө IDE орнотуу дайыма эле өзүн актай бербейт.
  • Docker OS жана китепканалардын ар кандай версиялары бар иштеп чыгуучулардын ортосунда коддун көчүрүлүшүн жана натыйжалардын кайталанышын, ошондой эле серверлерде аткаруунун оңойлугун камсыздайт. Сиз бир гана буйрук менен бүт окутуу түтүгүн ишке киргизе аласыз.
  • Google Булут - кымбат жабдыктарда эксперимент жүргүзүүнүн бюджетке ыңгайлуу жолу, бирок конфигурацияларды кылдаттык менен тандап алышыңыз керек.
  • Жеке код фрагменттеринин ылдамдыгын өлчөө абдан пайдалуу, айрыкча R жана C++ айкалыштырууда жана пакет менен орун - ошондой эле абдан жеңил.

Жалпысынан бул тажрыйба абдан пайдалуу болду жана биз көтөрүлгөн маселелердин айрымдарын чечүүнүн үстүндө иштейбиз.

Source: www.habr.com

Комментарий кошуу