Хурдан зурах Doodle таних: R, C++ болон мэдрэлийн сүлжээнүүдтэй хэрхэн нөхөрлөх вэ

Хурдан зурах Doodle таних: R, C++ болон мэдрэлийн сүлжээнүүдтэй хэрхэн нөхөрлөх вэ

Хөөе Хабр!

Өнгөрсөн намар Kaggle гараар зурсан зургуудыг ангилах, Quick Draw Doodle таних уралдааныг зохион байгуулсан бөгөөд үүнд R-эрдэмтдийн баг оролцов. Артем Клевцова, Филипп менежер и Андрей Огурцов. Бид өрсөлдөөнийг нарийвчлан тайлбарлахгүй; үүнийг аль хэдийн хийсэн сүүлийн үеийн хэвлэл.

Энэ удаад медалийн тариалалтаар бүтсэнгүй, гэхдээ маш их үнэ цэнэтэй туршлага хуримтлуулсан тул би Кагле болон өдөр тутмын ажилд хэрэгтэй хэд хэдэн сонирхолтой, хэрэгтэй зүйлийн талаар олон нийтэд хэлмээр байна. Хэлэлцсэн сэдвүүдийн дунд: хүнд хэцүү амьдрал OpenCV програм, JSON задлан шинжлэх (эдгээр жишээнүүд нь C++ кодыг R хэл дээрх скрипт эсвэл багцад нэгтгэхийг шалгадаг. Rcpp), скриптүүдийг параметржүүлэх, эцсийн шийдлийг докержуулах. Гүйцэтгэхэд тохиромжтой маягтын мессежийн бүх кодыг эндээс авах боломжтой агуулахууд.

Агуулга:

  1. CSV-ээс MonetDB руу өгөгдлийг үр дүнтэй ачаална уу
  2. Багц бэлдэж байна
  3. Өгөгдлийн сангаас багц буулгах давталт
  4. Архитектурын загвар сонгох
  5. Скриптийн параметржуулалт
  6. Скриптүүдийн докержуулалт
  7. Google Cloud дээр олон GPU ашиглах
  8. Оронд дүгнэлтийг

1. CSV-ээс MonetDB мэдээллийн санд өгөгдлийг үр дүнтэй ачаална уу

Энэхүү уралдааны өгөгдлийг бэлэн зураг хэлбэрээр биш, харин цэгийн координат бүхий JSON агуулсан 340 CSV файл (анги тус бүрт нэг файл) хэлбэрээр өгсөн болно. Эдгээр цэгүүдийг шугамаар холбосноор бид 256x256 пикселийн хэмжээтэй эцсийн дүрсийг авдаг. Мөн бичлэг тус бүрд тухайн зургийг өгөгдлийн багц цуглуулах үед ашигласан ангилагч зөв таньсан эсэхийг харуулсан шошго, зургийн зохиогчийн оршин суугаа улсын хоёр үсэгтэй код, өвөрмөц танигч, цагийн тэмдэг байна. болон файлын нэртэй таарах ангийн нэр. Анхны өгөгдлийн хялбаршуулсан хувилбар нь архивт 7.4 ГБ жинтэй бөгөөд задласны дараа ойролцоогоор 20 ГБ жинтэй, задласны дараа бүрэн өгөгдөл нь 240 ГБ эзэлнэ. Зохион байгуулагчид хоёр хувилбар нь ижил зургийг хуулбарласан байхаар баталгаажуулсан бөгөөд энэ нь бүтэн хувилбар нь илүүдэхгүй гэсэн үг юм. Ямар ч тохиолдолд 50 сая зургийг график файл эсвэл массив хэлбэрээр хадгалах нь ашиггүй гэж үзсэн тул архиваас бүх CSV файлуудыг нэгтгэхээр шийдсэн. train_simplified.zip Багц тус бүрд шаардлагатай хэмжээтэй зургийг дараа нь үүсгэн мэдээллийн санд оруулах.

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

Өгөгдлийн санд өгөгдөл ачаалах хамгийн хурдан арга бол SQL - командыг ашиглан CSV файлуудыг шууд хуулах явдал байв 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 таних: R, C++ болон мэдрэлийн сүлжээнүүдтэй хэрхэн нөхөрлөх вэ

2. Багц бэлтгэх

Багцыг бэлтгэх бүх үйл явц нь дараах үе шатуудаас бүрдэнэ.

  1. Цэгүүдийн координат бүхий мөрийн векторуудыг агуулсан хэд хэдэн JSON-г задлан шинжилж байна.
  2. Шаардлагатай хэмжээтэй зураг дээрх цэгүүдийн координат дээр үндэслэн өнгөт шугам зурах (жишээлбэл, 256 × 256 эсвэл 128 × 128).
  3. Үүссэн зургуудыг тензор болгон хувиргах.

Python цөмүүдийн дундах өрсөлдөөний хүрээнд асуудлыг үндсэндээ ашиглан шийдсэн OpenCV програм. R-ийн хамгийн энгийн бөгөөд ойлгомжтой аналогуудын нэг нь иймэрхүү харагдах болно.

R хэл дээр JSON-ийг тензор руу хөрвүүлэхийг хэрэгжүүлж байна

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

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

Зургийг стандарт R хэрэглүүр ашиглан хийж, RAM-д хадгалагдсан түр зуурын PNG файлд хадгалдаг (Линукс дээр түр зуурын 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 таних: R, C++ болон мэдрэлийн сүлжээнүүдтэй хэрхэн нөхөрлөх вэ

Багц өөрөө дараах байдлаар үүснэ.

res <- r_process_json_vector(tmp_data[1:4, drawing], scale = 0.5)
str(res)
 # num [1:4, 1:128, 1:128, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
 # - attr(*, "dimnames")=List of 4
 #  ..$ : NULL
 #  ..$ : NULL
 #  ..$ : NULL
 #  ..$ : NULL

Их хэмжээний багц үүсгэх нь зохисгүй урт хугацаа шаарддаг тул бид хүчирхэг номын сан ашиглан хамтран ажиллагсдынхаа туршлагыг ашиглахаар шийдсэн тул энэхүү хэрэгжилт нь бидэнд тохиромжгүй мэт санагдсан. OpenCV програм. Тэр үед R-д зориулсан бэлэн багц байхгүй байсан (одоо байхгүй), тиймээс шаардлагатай функцүүдийн хамгийн бага хэрэгжилтийг C++ хэл дээр R кодтой нэгтгэн бичсэн. Rcpp.

Асуудлыг шийдвэрлэхийн тулд дараах багц, сангуудыг ашигласан болно.

  1. OpenCV програм зурагтай ажиллах, шугам зурахад зориулагдсан. Урьдчилан суулгасан системийн номын сан, толгой файлууд, мөн динамик холболтыг ашигласан.

  2. кстенсор олон хэмжээст массив болон тензоруудтай ажиллахад зориулагдсан. Бид ижил нэртэй R багцад багтсан толгой файлуудыг ашигласан. Номын сан нь олон хэмжээст массивтай ажиллах боломжийг олгодог, гол мөрөн болон баганын үндсэн дарааллаар.

  3. нджсон JSON-г задлан шинжлэхэд зориулагдсан. Энэ номын санг ашиглаж байна кстенсор хэрэв төсөлд байгаа бол автоматаар.

  4. RcppThread JSON-аас векторын олон урсгалтай боловсруулалтыг зохион байгуулахад зориулагдсан. Энэ багцаас өгсөн толгой файлуудыг ашигласан. Илүү алдартай хүмүүсээс RcppParallel Багц нь бусад зүйлсээс гадна давталтын тасалдлын механизмтай.

Үүнийг анхаарах хэрэгтэй кстенсор Энэ нь бурхны хишиг болж хувирсан: энэ нь өргөн цар хүрээтэй, өндөр гүйцэтгэлтэй байхаас гадна түүний хөгжүүлэгчид нэлээд хариу үйлдэл үзүүлж, асуултуудад шуурхай, дэлгэрэнгүй хариулсан. Тэдгээрийн тусламжтайгаар OpenCV матрицыг xtensor тензор болгон хувиргах, мөн 3 хэмжээст зургийн тензоруудыг зөв хэмжээст 4 хэмжээст тензор болгон нэгтгэх арга (багц өөрөө) боломжтой болсон.

Rcpp, xtensor болон RcppThread сурахад зориулсан материалууд

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

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

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

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

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

Системд суулгасан сангуудтай динамик холболт, системийн файлуудыг ашигладаг файлуудыг эмхэтгэхийн тулд бид багцад хэрэгжсэн залгаасын механизмыг ашигласан. Rcpp. Замууд болон тугуудыг автоматаар олохын тулд бид алдартай Linux хэрэгслийг ашигласан pkg-тохиргоо.

OpenCV номын санг ашиглахад зориулсан Rcpp залгаасын хэрэгжилт

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

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

Plugin-ийн үйл ажиллагааны үр дүнд эмхэтгэлийн явцад дараах утгуудыг орлуулах болно.

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

Хурдан зурах 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") 

Хурдан зурах Doodle таних: R, C++ болон мэдрэлийн сүлжээнүүдтэй хэрхэн нөхөрлөх вэ

Таны харж байгаагаар хурдны өсөлт нь маш их ач холбогдолтой болсон бөгөөд R кодыг параллель болгох замаар C++ кодыг гүйцэх боломжгүй юм.

3. Өгөгдлийн сангаас багц буулгах давталт

R нь RAM-д тохирох өгөгдлийг боловсруулдаг нэр хүндтэй байдаг бол Python нь давталттай өгөгдөл боловсруулдгаараа онцлогтой бөгөөд энэ нь үндсэн бус тооцооллыг (гадаад санах ой ашиглан тооцоолох) хялбар бөгөөд байгалийн аргаар хэрэгжүүлэх боломжийг олгодог. Тайлбарласан асуудлын хүрээнд бидний хувьд сонгодог бөгөөд хамааралтай жишээ бол ажиглалтын багахан хэсгийг эсвэл мини багцыг ашиглан алхам бүрт градиентийг ойртуулах замаар градиент буурах аргаар сургасан гүн мэдрэлийн сүлжээ юм.

Python хэл дээр бичигдсэн гүнзгий сургалтын системүүд нь өгөгдөлд суурилсан давталтуудыг хэрэгжүүлдэг тусгай ангиудтай: хүснэгт, хавтас дахь зураг, хоёртын формат гэх мэт. Та бэлэн сонголтуудыг ашиглах эсвэл тодорхой ажлуудад зориулж өөрөө бичиж болно. R дээр бид Python номын сангийн бүх боломжуудыг ашиглах боломжтой керас ижил нэртэй багцыг ашиглан төрөл бүрийн арын хэсгүүдтэй бөгөөд энэ нь эргээд багцын дээд талд ажилладаг торлог. Сүүлийнх нь тусдаа урт өгүүлэл байх ёстой; Энэ нь танд R-ээс Python кодыг ажиллуулах боломжийг олгодог төдийгүй R болон Python сесс хооронд объектуудыг шилжүүлэх, шаардлагатай бүх төрлийн хөрвүүлэлтийг автоматаар гүйцэтгэх боломжийг олгодог.

MonetDBLite ашиглан бид бүх өгөгдлийг RAM-д хадгалах шаардлагаас салсан, бүх "мэдрэлийн сүлжээ"-ийн ажлыг 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 таних: R, C++ болон мэдрэлийн сүлжээнүүдтэй хэрхэн нөхөрлөх вэ

Хэрэв танд хангалттай хэмжээний RAM байгаа бол та өгөгдлийн сангийн үйл ажиллагааг ижил RAM руу шилжүүлэх замаар хурдасгах боломжтой (бидний даалгаварт 32 ГБ хангалттай). Линукс дээр хуваалтыг анхдагчаар суулгадаг /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. Скриптүүдийг параметржүүлэх

Тохиромжтой болгох үүднээс сургалт эхлэх бүх кодыг нэг скрипт хэлбэрээр зохион бүтээсэн бөгөөд параметрүүдийг ашиглан тохируулсан болно docopt дараах байдлаар:

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)

Багц docopt хэрэгжилтийг илэрхийлж байна 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. Энэ нь кодонд үйлдлийн системийн хувилбарыг зааж өгөх шаардлагагүй болсон.

Нэмж дурдахад, янз бүрийн команд бүхий контейнер ажиллуулах боломжийг олгодог жижиг bash скрипт бичсэн. Жишээлбэл, эдгээр нь өмнө нь контейнер дотор байрлуулсан мэдрэлийн сүлжээг сургах скриптүүд эсвэл дибаг хийх, контейнерийн ажиллагааг хянах командын бүрхүүл байж болно.

Контейнерийг эхлүүлэх скрипт

#!/bin/sh

DBDIR=${PWD}/db
LOGSDIR=${PWD}/logs
MODELDIR=${PWD}/models
DATADIR=${PWD}/data
ARGS="--runtime=nvidia --rm -v ${DBDIR}:/db -v ${LOGSDIR}:/app/logs -v ${MODELDIR}:/app/models -v ${DATADIR}:/app/data"

if [ -z "$1" ]; then
    CMD="Rscript /app/train_nn.R"
elif [ "$1" = "bash" ]; then
    ARGS="${ARGS} -ti"
else
    CMD="Rscript /app/train_nn.R $@"
fi

docker run ${ARGS} doodles-tf ${CMD}

Хэрэв энэ bash скриптийг параметргүйгээр ажиллуулбал скрипт нь контейнер дотор дуудагдах болно train_nn.R анхдагч утгуудтай; хэрэв эхний байрлалын аргумент нь "bash" бол контейнер нь командын бүрхүүлтэй интерактив байдлаар эхэлнэ. Бусад бүх тохиолдолд байрлалын аргументуудын утгыг орлуулна. CMD="Rscript /app/train_nn.R $@".

Эх сурвалжийн өгөгдөл, мэдээллийн сан бүхий лавлахууд, түүнчлэн бэлтгэгдсэн загваруудыг хадгалах лавлах нь хост системээс контейнер дотор суурилагдсан бөгөөд энэ нь скриптүүдийн үр дүнд шаардлагагүй залруулга хийхгүйгээр нэвтрэх боломжийг олгодог гэдгийг тэмдэглэх нь зүйтэй.

7. Google Cloud дээр олон GPU ашиглах

Тэмцээний нэг онцлог нь маш их чимээ шуугиантай өгөгдөл байсан (ODS slack-аас @Leigh.plt-аас зээлсэн гарчгийн зургийг үзнэ үү). Их хэмжээний багц нь үүнтэй тэмцэхэд тусалдаг бөгөөд 1 GPU бүхий компьютер дээр туршилт хийсний дараа бид үүлэн доторх хэд хэдэн GPU дээр сургалтын загваруудыг эзэмшихээр шийдсэн. Ашигласан GoogleCloud (үндсэн ойлголтуудын сайн гарын авлага) боломжит тохируулгын өргөн сонголт, боломжийн үнэ, 300 долларын урамшуулал зэргээс шалтгаалан. Шуналын үүднээс би SSD, олон тонн RAM бүхий 4xV100 инстанс захиалсан бөгөөд энэ нь маш том алдаа байсан. Ийм машин мөнгө хурдан иддэг тул та батлагдсан шугам хоолойгүйгээр эвдэрсэн туршилт хийж болно. Боловсролын зорилгоор K80 авах нь дээр. Гэхдээ их хэмжээний RAM нь хэрэг болсон - үүлэн 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 номын санд байна хурдан.ai); Зарим хүчин чармайлтаар гуравдагч этгээдийн хэрэгжүүлэлтийг R руу шилжүүлэх боломжтой, жишээлбэл, энэ нь;
  • өмнөх цэгийн үр дүнд хэд хэдэн GPU ашиглах үед сургалтын зөв хурдыг сонгох боломжгүй байсан;
  • орчин үеийн мэдрэлийн сүлжээний архитектур, ялангуяа imagenet дээр урьдчилан бэлтгэгдсэн бүтэц дутагдалтай байна;
  • хэн ч мөчлөгийн бодлого, ялгаварлан гадуурхах сургалтын хувь хэмжээ (косинусыг нөхөх нь бидний хүсэлтээр хийгдсэн). хэрэгжүүлсэн, Баярлалаа скайдан).

Энэ тэмцээнээс ямар хэрэгтэй зүйл сурсан бэ:

  • Харьцангуй бага хүчин чадалтай техник хангамж дээр та хангалттай (RAM-аас олон дахин их) хэмжээний өгөгдөлтэй ажиллах боломжтой. Гялгар уут мэдээлэл.хүснэгт Хүснэгтүүдийг газар дээр нь өөрчилснөөр санах ойг хэмнэж, тэдгээрийг хуулахаас зайлсхийдэг бөгөөд зөв ашиглавал түүний чадвар нь скрипт хэл дээр бидний мэддэг бүх хэрэгслүүдийн дунд бараг үргэлж хамгийн өндөр хурдыг харуулдаг. Өгөгдлийн санд өгөгдлийг хадгалах нь ихэнх тохиолдолд өгөгдлийн багцыг бүхэлд нь RAM-д шахах хэрэгцээний талаар огт бодохгүй байх боломжийг олгодог.
  • R хэл дээрх удаан функцийг багцыг ашиглан C++ хэл дээрх хурдан функцээр сольж болно Rcpp. Хэрэв хэрэглэхээс гадна RcppThread буюу RcppParallel, бид хөндлөн платформын олон урсгалтай хэрэгжилтийг авдаг тул R түвшинд кодыг зэрэгцүүлэх шаардлагагүй болно.
  • Багц Rcpp C++-ийн талаар ноцтой мэдлэггүйгээр ашиглаж болно, шаардлагатай доод хэмжээг тодорхойлсон энд. зэрэг хэд хэдэн гайхалтай C-номын сангийн толгой файлууд кстенсор CRAN дээр ашиглах боломжтой, өөрөөр хэлбэл бэлэн өндөр гүйцэтгэлтэй C++ кодыг R-д нэгтгэх төслүүдийг хэрэгжүүлэх дэд бүтэц бий болж байна. Нэмэлт тав тухтай байдал нь синтаксийг тодруулах, RStudio дахь статик C++ кодын анализатор юм.
  • docopt параметр бүхий бие даасан скриптүүдийг ажиллуулах боломжийг танд олгоно. Энэ нь алсын сервер дээр ашиглахад тохиромжтой, үүнд. докерын дор. RStudio-д мэдрэлийн сүлжээг сургах олон цагийн туршилт хийх нь тохиромжгүй бөгөөд сервер дээр IDE суулгах нь үргэлж зөвтгөгддөггүй.
  • Docker нь үйлдлийн систем болон номын сангуудын өөр өөр хувилбар бүхий хөгжүүлэгчдийн хооронд код зөөвөрлөх, үр дүнг дахин гаргах, сервер дээр ажиллахад хялбар байдлыг баталгаажуулдаг. Та зөвхөн нэг тушаалаар бүх сургалтын шугамыг эхлүүлж болно.
  • Google Cloud нь үнэтэй техник хангамж дээр туршилт хийх төсөвт ээлтэй арга боловч та тохиргоог анхааралтай сонгох хэрэгтэй.
  • Тусдаа кодын фрагментуудын хурдыг хэмжих нь ялангуяа R ба C++ болон багцтай хослуулах үед маш ашигтай байдаг. вандан - бас маш хялбар.

Ерөнхийдөө энэ туршлага маш их үр өгөөжтэй байсан бөгөөд бид хөндөгдсөн зарим асуудлыг шийдвэрлэхээр үргэлжлүүлэн ажиллаж байна.

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх