Эътироф кардани Doodle Quick Draw: чӣ гуна бо R, C++ ва шабакаҳои нейронӣ дӯстӣ кардан мумкин аст

Эътироф кардани Doodle Quick Draw: чӣ гуна бо R, C++ ва шабакаҳои нейронӣ дӯстӣ кардан мумкин аст

Эй Ҳабр!

Тирамоҳи соли гузашта, Kaggle озмун барои таснифи расмҳои дастӣ кашидашуда, Quick Draw Doodle Recognition баргузор гардид, ки дар он, дар қатори дигарон, як гурӯҳи олимони R-олимон ширкат доштанд: Артем Клевцова, Филипп Менеҷер и Андрей Огурцов. Мо рақобатро ба таври муфассал тавсиф намекунем; ин аллакай дар он анҷом дода шудааст нашри охирин.

Ин дафъа он бо кишоварзии медалӣ натиҷа надод, аммо таҷрибаи зиёди арзишманд ба даст оварда шуд, бинобар ин ман мехоҳам ба ҷомеа дар бораи як қатор чизҳои ҷолибтарин ва муфид дар бораи Kagle ва дар кори ҳаррӯза нақл кунам. Дар байни мавзуъхои мухокимашуда: хаёти душвор бе Опенчв, таҳлили JSON (ин мисолҳо ҳамгироии коди C++-ро ба скриптҳо ё бастаҳо дар R истифода мебаранд Rcpp), параметризатсияи скриптҳо ва докеризатсияи ҳалли ниҳоӣ. Ҳама коди паём дар шакле, ки барои иҷро мувофиқ аст, дар анборҳо.

Мундариҷа:

  1. Маълумотро аз CSV ба MonetDB самаранок бор кунед
  2. Тайёр кардани дастаҳо
  3. Итераторҳо барои борфарорӣ аз пойгоҳи додаҳо
  4. Интихоби меъмории намунавӣ
  5. Параметризатсияи скрипт
  6. Докеризатсияи скриптҳо
  7. Истифодаи GPU-ҳои сершумор дар Google Cloud
  8. Ба ҷои хулоса

1. Маълумотро аз CSV ба базаи MonetDB самаранок бор кунед

Маълумот дар ин озмун на дар шакли тасвирҳои тайёр, балки дар шакли 340 файли CSV (як файл барои ҳар як синф) дорои JSON-ҳо бо координатҳои нуқтаҳо пешниҳод карда мешаванд. Бо пайваст кардани ин нуқтаҳо бо хатҳо, мо тасвири ниҳоии андозаи 256x256 пиксел мегирем. Инчунин барои ҳар як сабт нишонае мавҷуд аст, ки нишон медиҳад, ки оё расм аз ҷониби таснифоте, ки дар вақти ҷамъоварии маълумот истифода шудааст, дуруст эътироф шудааст ё не, рамзи ду ҳарфӣ аз кишвари истиқомати муаллифи расм, идентификатори беназир, тамғаи вақт ва номи синф, ки ба номи файл мувофиқат мекунад. Вазни нусхаи соддакардашудаи маълумоти аслӣ дар бойгонӣ 7.4 ГБ ва пас аз кушодан тақрибан 20 ГБ аст, маълумоти пурра пас аз кушодан 240 ГБ-ро мегирад. Созмондиҳандагон кафолат доданд, ки ҳарду версия як расмҳоро такрор мекунанд, яъне версияи пурраи он зиёдатист. Дар ҳар сурат, нигоҳ доштани 50 миллион тасвир дар файлҳои графикӣ ё дар шакли массивҳо фавран зиёновар ҳисобида шуд ва мо тасмим гирифтем, ки ҳамаи файлҳои CSV-ро аз бойгонӣ якҷоя кунем. train_simplified.zip ба махзани маълумот бо тавлиди минбаъдаи тасвирҳои андозаи зарурии "дар парвоз" барои ҳар як партия.

Системаи хуб исботшуда ҳамчун DBMS интихоб карда шуд 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 (DB) камтар аз 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-и мушоҳидаро нигоҳ медорад. Дар маҷмӯи маълумоти аслӣ, навъи лозим барои нигоҳ доштани ID аст bigint, аммо шумораи мушоҳидаҳо имкон медиҳад, ки идентификаторҳои онҳо, ки ба рақами тартиби баробаранд, ба намуд мувофиқ карда шаванд. int. Дар ин ҳолат ҷустуҷӯ хеле тезтар аст. Ҳиллаи дуюм истифода бурдан буд ORDERED INDEX — мо тамоми имко-ниятхоро аз назар гузаронда, ба таври тачрибавй ба ин карор омадем имконоти. Сеюм ин истифодаи дархостҳои параметрӣ буд. Мохияти усул як маротиба ичро кардани фармон аст PREPARE бо истифодаи минбаъдаи ифодаи омодашуда ҳангоми эҷоди як қатор дархостҳои як навъ, аммо дар асл дар муқоиса бо як оддӣ бартарӣ вуҷуд дорад SELECT дар доираи хатохои статистикй баромад.

Раванди боркунии маълумот на бештар аз 450 МБ RAM-ро сарф мекунад. Яъне, равиши тавсифшуда ба шумо имкон медиҳад, ки маҷмӯаҳои маълумотҳои вазнашон даҳҳо гигабайтро дар ҳама гуна сахтафзори буҷетӣ, аз ҷумла баъзе дастгоҳҳои якбора интиқол диҳед, ки хеле хуб аст.

Танҳо чен кардани суръати дарёфти маълумот (тасодуфӣ) ва арзёбии миқёс ҳангоми интихоби партияҳои андозаҳои гуногун боқӣ мемонад:

Нишондиҳандаи пойгоҳи додаҳо

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)

Эътироф кардани Doodle Quick Draw: чӣ гуна бо R, C++ ва шабакаҳои нейронӣ дӯстӣ кардан мумкин аст

2. Тайёр кардани дастаҳо

Тамоми раванди омодасозии партия аз қадамҳои зерин иборат аст:

  1. Таҳлили якчанд JSON-ҳои дорои векторҳои сатр бо координатаҳои нуқтаҳо.
  2. кашидани хатҳои ранга дар асоси координатаҳои нуқтаҳо дар тасвири андозаи зарурӣ (масалан, 256×256 ё 128×128).
  3. Табдил додани тасвирҳои натиҷа ба тензор.

Дар доираи рақобат байни ядроҳои Python, мушкилот пеш аз ҳама бо истифода аз он ҳал карда шуданд Опенчв. Яке аз аналогҳои соддатарин ва возеҳтарин дар 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 иҷро карда мешавад ва дар PNG-и муваққатии дар RAM захирашуда захира карда мешавад (дар 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))

Эътироф кардани Doodle Quick Draw: чӣ гуна бо 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

Ин татбиқ барои мо ғайримуқаррарӣ менамуд, зеро ташаккули порчаҳои калон вақти бениҳоят тӯлонӣ мегирад ва мо тасмим гирифтем, ки аз таҷрибаи ҳамкасбони худ бо истифода аз китобхонаи пурқувват истифода барем. Опенчв. Дар он вақт бастаи тайёр барои R вуҷуд надошт (ҳоло вуҷуд надорад), бинобар ин татбиқи ҳадди ақали функсияҳои зарурӣ дар C++ бо ҳамгироӣ ба рамзи R бо истифода аз Rcpp.

Барои ҳалли мушкилот, бастаҳо ва китобхонаҳои зерин истифода шуданд:

  1. Опенчв барои кор бо тасвирҳо ва хатҳои кашидан. Китобхонаҳои система ва файлҳои сарлавҳаи қаблан насбшуда, инчунин пайвасти динамикӣ истифода мешаванд.

  2. кстензор барои кор бо массивҳои бисёрченака ва тензорҳо. Мо файлҳои сарлавҳаро истифода бурдем, ки дар бастаи R бо ҳамон ном дохил карда шудаанд. Китобхона ба шумо имкон медиҳад, ки бо массивҳои бисёрченака ҳам дар сатри асосӣ ва ҳам дар сутуни асосӣ кор кунед.

  3. нджсон барои таҳлили 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-танзим.

Амалисозии плагини Rcpp барои истифодаи китобхонаи OpenCV

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

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

Дар натиҷаи кори плагин, дар ҷараёни тартибдиҳӣ арзишҳои зерин иваз карда мешаванд:

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

Амалисозии JSON ба табдили тензор дар C++

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

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

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

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

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

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

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

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

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

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

Ин код бояд дар файл ҷойгир карда шавад 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))

Эътироф кардани Doodle Quick Draw: чӣ гуна бо 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") 

Эътироф кардани Doodle Quick Draw: чӣ гуна бо R, C++ ва шабакаҳои нейронӣ дӯстӣ кардан мумкин аст

Тавре ки шумо мебинед, афзоиши суръат хеле назаррас буд ва тавассути параллелизатсияи рамзи R ба рамзи C++ расида гирифтан ғайриимкон аст.

3. Итераторҳо барои борфарорӣ аз пойгоҳи додаҳо

R барои коркарди маълумоте, ки ба хотираи оперативӣ мувофиқ аст, эътибори шоиста дорад, дар ҳоле ки Python бештар коркарди такрории додаҳо тавсиф мешавад, ки ба шумо имкон медиҳад, ки ҳисобҳои берун аз асосиро (ҳисобҳо бо истифода аз хотираи беруна) ба осонӣ ва табиатан амалӣ кунед. Намунаи классикӣ ва мувофиқ барои мо дар заминаи мушкилоти тавсифшуда шабакаҳои амиқи нейронӣ мебошанд, ки бо усули пастшавии градиент бо наздикшавии градиент дар ҳар як қадам бо истифода аз як қисми ками мушоҳидаҳо ё хурди партия омӯхта шудаанд.

Чаҳорчӯбаҳои омӯзиши амиқ, ки бо Python навишта шудаанд, синфҳои махсус доранд, ки итераторҳоро дар асоси додаҳо амалӣ мекунанд: ҷадвалҳо, тасвирҳо дар ҷузвдонҳо, форматҳои дуӣ ва ғайра. Шумо метавонед имконоти тайёрро истифода баред ё барои вазифаҳои мушаххас худатонро нависед. Дар R мо метавонем аз тамоми хусусиятҳои китобхонаи Python истифода барем керас бо пуштибонии гуногуни он бо истифода аз бастаи ҳамон ном, ки дар навбати худ дар болои баста кор мекунад ретикулятсия. Охирин сазовори мақолаи алоҳидаи дароз аст; он на танҳо ба шумо имкон медиҳад, ки рамзи Python-ро аз R иҷро кунед, балки инчунин ба шумо имкон медиҳад, ки объектҳоро байни сеансҳои R ва Python интиқол диҳед, ба таври худкор ҳама табдилдиҳии навъи заруриро иҷро кунед.

Мо аз зарурати нигоҳ доштани ҳама маълумот дар хотираи оперативӣ бо истифода аз MonetDBLite халос шудем, ҳама кори "шабакаи нейронӣ" тавассути коди аслӣ дар Python анҷом дода мешавад, мо танҳо бояд итераторро дар болои маълумот нависем, зеро ҳеҷ чиз омода нест. барои чунин вазъият дар R ё Python. Барои он танҳо ду талабот вуҷуд дорад: он бояд партияҳоро дар як ҳалқаи беохир баргардонад ва ҳолати худро дар байни такрорҳо нигоҳ дорад (охирин дар R бо роҳи соддатарин бо истифода аз бастаҳо амалӣ карда мешавад). Пештар, ба таври возеҳ табдил додани массивҳои R ба массивҳои numpy дар дохили итератор талаб карда мешуд, аммо версияи ҷории баста керас худаш мекунад.

Итератор барои маълумотҳои омӯзишӣ ва тасдиқкунӣ чунин буд:

Итератор барои маълумотҳои омӯзиш ва тасдиқ

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)

Эътироф кардани Doodle Quick Draw: чӣ гуна бо R, C++ ва шабакаҳои нейронӣ дӯстӣ кардан мумкин аст

Агар шумо миқдори кофии хотираи оперативӣ дошта бошед, шумо метавонед кори пойгоҳи додаҳоро тавассути интиқоли он ба ҳамон хотираи оперативӣ суръат бахшед (барои вазифаи мо 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 чунин маҳдудият вуҷуд надорад, аз ин рӯ мо шитоб кардем ва татбиқи ин меъмории худамонро пас аз мақолаи аслӣ навиштем (бе тарки тарки версияи керас):

Меъмории 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(). Мо ҳеҷ гоҳ ин функсияро илова накардаем, зеро дар ин марҳила аллакай маълум буд, ки кор бо расмҳои ранга самараноктар аст.

Мо аксари таҷрибаҳоро бо истифода аз версияҳои 1 ва 2 mobilenet ва инчунин 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). Аммо бартарии асосӣ ин қобилияти ба осонӣ идора кардани оғози скриптҳо дар Docker ё танҳо дар сервер, бе насб кардани RStudio барои ин аст.

6. Докеризатсияи скриптҳо

Мо Docker-ро барои таъмини интиқоли муҳити зист барои омӯзиши моделҳо дар байни аъзоёни даста ва ҷойгиркунии зуд дар абр истифода бурдем. Шумо метавонед бо ин асбоб шинос шавед, ки барои барномасози R нисбатан ғайриоддӣ аст, бо ин силсилаи нашрияҳо ё курси видеоӣ.

Docker ба шумо имкон медиҳад, ки ҳам аз сифр тасвирҳои шахсии худро эҷод кунед ва ҳам дигар тасвирҳоро ҳамчун асос барои эҷоди худ истифода баред. Ҳангоми таҳлили имконоти мавҷуда, мо ба хулосае омадем, ки насб кардани драйверҳои NVIDIA, CUDA+cuDNN ва китобхонаҳои Python як қисми хеле ҳаҷми тасвир аст ва мо тасмим гирифтем, ки тасвири расмиро ҳамчун асос гирем. tensorflow/tensorflow:1.12.0-gpu, илова кардани бастаҳои зарурии R дар он ҷо.

Файли ниҳоии docker чунин менамуд:

файли докер

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. Истифодаи GPU-ҳои сершумор дар Google Cloud

Яке аз хусусиятҳои озмун ин маълумоти хеле пурғавғо буд (ба расми унвон нигаред, ки аз @Leigh.plt аз ODS slack гирифта шудааст). Партияҳои калон дар мубориза бо ин кӯмак мекунанд ва пас аз таҷрибаҳо дар компютери дорои 1 GPU, мо тасмим гирифтем, ки моделҳои омӯзиширо дар якчанд GPU дар абр азхуд кунем. GoogleCloud истифодашуда (дастури хуб ба асосҳои) аз сабаби интихоби калони конфигуратсияҳои дастрас, нархи оқилона ва $300 бонус. Аз рӯи тамаъ, ман як намунаи 4xV100 бо SSD ва як тонна RAM фармоиш додам ва ин хатои калон буд. Чунин мошин пулро зуд мехӯрад; шумо метавонед бидуни қубури исботшуда ба озмоишҳои шикаста гузаред. Барои мақсадҳои таълимӣ, беҳтар аст, ки K80 гиред. Аммо миқдори зиёди хотираи оперативӣ муфид буд - абрии SSD бо кори худ ба ҳайрат наовард, бинобар ин пойгоҳи додаҳо ба dev/shm.

Аз ҳама таваҷҷӯҳи бештар порчаи рамзест, ки барои истифодаи якчанд GPU масъул аст. Аввалан, модел дар CPU бо истифода аз менеҷери контекст, ба мисли Python сохта мешавад:

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

Сипас модели тартибнашуда (ин муҳим аст) ба миқдори додаи 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) маълумот бидуни дард кор кунед. Халтаи пластикӣ маълумот. ҷадвал хотираро аз ҳисоби тағир додани ҷадвалҳо дар ҷои худ сарфа мекунад, ки аз нусхабардории онҳо канорагирӣ мекунад ва ҳангоми истифодаи дуруст, қобилиятҳои он қариб ҳамеша суръати баландтаринро дар байни ҳамаи асбобҳое, ки барои забонҳои скрипт ба мо маълуманд, нишон медиҳанд. Нигоҳ доштани маълумот дар пойгоҳи додаҳо ба шумо имкон медиҳад, ки дар бисёр ҳолатҳо дар бораи зарурати фишурдани тамоми маҷмӯаи додаҳо ба RAM фикр накунед.
  • Функсияҳои сустро дар R метавон бо функсияҳои зуд дар C++ бо истифода аз баста иваз кард Rcpp. Агар илова ба истифода RcppThread ё RcppParallel, мо татбиқҳои бисёрсоҳавии кросс-платформаро ба даст меорем, аз ин рӯ лозим нест, ки кодро дар сатҳи R параллелизатсия кунем.
  • Баста Rcpp метавонад бидуни дониши ҷиддии C++ истифода шавад, ҳадди ақали зарурӣ нишон дода шудааст дар ин ҷо. Файлҳои сарлавҳа барои як қатор китобхонаҳои зебои C ба монанди кстензор дар CRAN дастрас аст, яъне инфрасохтор барои татбиқи лоиҳаҳое ташаккул меёбад, ки рамзи тайёри баландсифати C++-ро ба R муттаҳид мекунанд. Бароҳатии иловагӣ равшансозии синтаксис ва таҳлилгари рамзи статикии C++ дар RStudio мебошад.
  • докопт ба шумо имкон медиҳад, ки скриптҳои мустақилро бо параметрҳо иҷро кунед. Ин барои истифода дар сервери дурдаст қулай аст, аз ҷумла. дар зери докер. Дар RStudio гузаронидани таҷрибаҳои бисёрсоата бо омӯзиши шабакаҳои нейрон номувофиқ аст ва насби IDE дар худи сервер на ҳамеша асоснок аст.
  • Docker интиқоли рамзҳо ва такроршавандагии натиҷаҳоро байни таҳиягарон бо версияҳои гуногуни ОС ва китобхонаҳо ва инчунин осонии иҷро дар серверҳо таъмин мекунад. Шумо метавонед бо як фармон тамоми лӯлаи омӯзиширо оғоз кунед.
  • Google Cloud як роҳи муносиби буҷет барои озмоиш дар сахтафзори гаронбаҳост, аммо шумо бояд конфигуратсияҳоро бодиққат интихоб кунед.
  • Андозаи суръати порчаҳои коди инфиродӣ хеле муфид аст, махсусан ҳангоми омезиши R ва C++ ва бо баста дубора - инчунин хеле осон.

Дар маҷмӯъ, ин таҷриба хеле муфид буд ва мо барои ҳалли баъзе масъалаҳои ба миён гузошташуда корамонро идома медиҳем.

Манбаъ: will.com

Илова Эзоҳ