Quick Draw Doodle Recognition: πώς να κάνετε φίλους με R, C++ και νευρωνικά δίκτυα

Quick Draw Doodle Recognition: πώς να κάνετε φίλους με R, C++ και νευρωνικά δίκτυα

Γεια σου Χαμπρ!

Το περασμένο φθινόπωρο, ο Kaggle φιλοξένησε έναν διαγωνισμό για την ταξινόμηση των χειροποίητων εικόνων, Quick Draw Doodle Recognition, στον οποίο, μεταξύ άλλων, συμμετείχε μια ομάδα επιστημόνων R: Άρτεμ Κλεβτσόβα, Philippa Manager и Αντρέι Ογκούρτσοφ. Δεν θα περιγράψουμε λεπτομερώς τον διαγωνισμό· αυτό έχει ήδη γίνει στο πρόσφατη δημοσίευση.

Αυτή τη φορά δεν λειτούργησε με την καλλιέργεια μεταλλίων, αλλά αποκτήθηκε πολύτιμη εμπειρία, επομένως θα ήθελα να πω στην κοινότητα μια σειρά από τα πιο ενδιαφέροντα και χρήσιμα πράγματα στο Kagle και στην καθημερινή εργασία. Μεταξύ των θεμάτων που συζητήθηκαν: δύσκολη ζωή χωρίς OpenCV, ανάλυση JSON (αυτά τα παραδείγματα εξετάζουν την ενσωμάτωση του κώδικα C++ σε σενάρια ή πακέτα στο R χρησιμοποιώντας Rcpp), παραμετροποίηση scripts και dockerization της τελικής λύσης. Όλος ο κώδικας από το μήνυμα σε μορφή κατάλληλη για εκτέλεση είναι διαθέσιμος στο αποθετήρια.

Περιεχόμενα:

  1. Αποτελεσματική φόρτωση δεδομένων από το CSV στο MonetDB
  2. Προετοιμασία παρτίδων
  3. Iterators για την εκφόρτωση παρτίδων από τη βάση δεδομένων
  4. Επιλογή Αρχιτεκτονικής Μοντέλου
  5. Παραμετροποίηση σεναρίου
  6. Dockerization σεναρίων
  7. Χρήση πολλαπλών GPU στο Google Cloud
  8. Αντί για ένα συμπέρασμα

1. Αποτελεσματική φόρτωση δεδομένων από το CSV στη βάση δεδομένων MonetDB

Τα δεδομένα σε αυτόν τον διαγωνισμό παρέχονται όχι με τη μορφή έτοιμων εικόνων, αλλά με τη μορφή 340 αρχείων CSV (ένα αρχείο για κάθε τάξη) που περιέχουν JSON με συντεταγμένες σημείων. Συνδέοντας αυτά τα σημεία με γραμμές, παίρνουμε μια τελική εικόνα διαστάσεων 256x256 pixel. Επίσης για κάθε εγγραφή υπάρχει μια ετικέτα που υποδεικνύει εάν η εικόνα αναγνωρίστηκε σωστά από τον ταξινομητή που χρησιμοποιήθηκε τη στιγμή που συλλέχθηκε το σύνολο δεδομένων, ένας κωδικός δύο γραμμάτων της χώρας κατοικίας του συγγραφέα της εικόνας, ένα μοναδικό αναγνωριστικό, μια χρονική σήμανση και ένα όνομα τάξης που ταιριάζει με το όνομα αρχείου. Μια απλοποιημένη έκδοση των αρχικών δεδομένων ζυγίζει 7.4 GB στο αρχείο και περίπου 20 GB μετά την αποσυσκευασία, τα πλήρη δεδομένα μετά την αποσυσκευασία καταλαμβάνουν 240 GB. Οι διοργανωτές εξασφάλισαν ότι και οι δύο εκδόσεις αναπαρήγαγαν τα ίδια σχέδια, που σημαίνει ότι η πλήρης έκδοση ήταν περιττή. Σε κάθε περίπτωση, η αποθήκευση 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 ή από μια μονάδα flash (αρχείο αρχείου) σε ένα 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 κόλπα. Το πρώτο ήταν να μειωθεί η διάσταση του τύπου που αποθηκεύει το αναγνωριστικό παρατήρησης. Στο αρχικό σύνολο δεδομένων, ο τύπος που απαιτείται για την αποθήκευση του αναγνωριστικού είναι bigint, αλλά ο αριθμός των παρατηρήσεων καθιστά δυνατή την προσαρμογή των αναγνωριστικών τους, ίσων με τον τακτικό αριθμό, στον τύπο int. Η αναζήτηση είναι πολύ πιο γρήγορη σε αυτή την περίπτωση. Το δεύτερο κόλπο ήταν να το χρησιμοποιήσετε ORDERED INDEX — Καταλήξαμε σε αυτήν την απόφαση εμπειρικά, έχοντας περάσει όλα τα διαθέσιμα επιλογές. Το τρίτο ήταν η χρήση παραμετροποιημένων ερωτημάτων. Η ουσία της μεθόδου είναι να εκτελέσετε την εντολή μία φορά PREPARE με επακόλουθη χρήση μιας προετοιμασμένης έκφρασης κατά τη δημιουργία μιας δέσμης ερωτημάτων του ίδιου τύπου, αλλά στην πραγματικότητα υπάρχει ένα πλεονέκτημα σε σύγκριση με μια απλή SELECT αποδείχθηκε ότι ήταν εντός του εύρους του στατιστικού σφάλματος.

Η διαδικασία μεταφόρτωσης δεδομένων δεν καταναλώνει περισσότερο από 450 MB μνήμης RAM. Δηλαδή, η περιγραφόμενη προσέγγιση σάς επιτρέπει να μετακινήσετε σύνολα δεδομένων που ζυγίζουν δεκάδες gigabyte σε σχεδόν οποιοδήποτε υλικό προϋπολογισμού, συμπεριλαμβανομένων ορισμένων συσκευών με μία πλακέτα, κάτι που είναι πολύ ωραίο.

Το μόνο που απομένει είναι να μετρήσουμε την ταχύτητα ανάκτησης (τυχαία) δεδομένων και να αξιολογήσουμε την κλίμακα κατά τη δειγματοληψία παρτίδων διαφορετικών μεγεθών:

Σημείο αναφοράς βάσης δεδομένων

library(ggplot2)

set.seed(0)
# Подключение к базе данных
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))

# Функция для подготовки запроса на стороне сервера
prep_sql <- function(batch_size) {
  sql <- sprintf("PREPARE SELECT id FROM doodles WHERE id IN (%s)",
                 paste(rep("?", batch_size), collapse = ","))
  res <- DBI::dbSendQuery(con, sql)
  return(res)
}

# Функция для извлечения данных
fetch_data <- function(rs, batch_size) {
  ids <- sample(seq_len(n), batch_size)
  res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
  return(res)
}

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

#   batch_size      min   median      max `itr/sec` total_time n_itr
#        <dbl> <bch:tm> <bch:tm> <bch:tm>     <dbl>   <bch:tm> <int>
# 1         16   23.6ms  54.02ms  93.43ms     18.8        2.6s    49
# 2         32     38ms  84.83ms 151.55ms     11.4       4.29s    49
# 3         64   63.3ms 175.54ms 248.94ms     5.85       8.54s    50
# 4        128   83.2ms 341.52ms 496.24ms     3.00      16.69s    50
# 5        256  232.8ms 653.21ms 847.44ms     1.58      31.66s    50
# 6        512  784.6ms    1.41s    1.98s     0.740       1.1m    49
# 7       1024  681.7ms    2.72s    4.06s     0.377      2.16m    49

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: πώς να κάνετε φίλους με R, C++ και νευρωνικά δίκτυα

2. Προετοιμασία παρτίδων

Η όλη διαδικασία προετοιμασίας της παρτίδας αποτελείται από τα ακόλουθα βήματα:

  1. Ανάλυση πολλών JSON που περιέχουν διανύσματα συμβολοσειρών με συντεταγμένες σημείων.
  2. Σχεδιάζοντας έγχρωμες γραμμές με βάση τις συντεταγμένες σημείων σε μια εικόνα του απαιτούμενου μεγέθους (για παράδειγμα, 256×256 ή 128×128).
  3. Μετατροπή των εικόνων που προκύπτουν σε τανυστήρα.

Ως μέρος του ανταγωνισμού μεταξύ των πυρήνων Python, το πρόβλημα επιλύθηκε κυρίως χρησιμοποιώντας OpenCV. Ένα από τα απλούστερα και πιο προφανή ανάλογα στο R θα μοιάζει με αυτό:

Εφαρμογή της μετατροπής JSON σε Tensor στο 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))

Quick Draw Doodle Recognition: πώς να κάνετε φίλους με R, C++ και νευρωνικά δίκτυα

Η ίδια η παρτίδα θα σχηματιστεί ως εξής:

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

Αυτή η υλοποίηση μας φάνηκε μη βέλτιστη, καθώς ο σχηματισμός μεγάλων παρτίδων διαρκεί απρεπώς πολύ και αποφασίσαμε να εκμεταλλευτούμε την εμπειρία των συναδέλφων μας χρησιμοποιώντας μια ισχυρή βιβλιοθήκη OpenCV. Εκείνη την εποχή δεν υπήρχε έτοιμο πακέτο για το R (δεν υπάρχει τώρα), έτσι μια ελάχιστη υλοποίηση της απαιτούμενης λειτουργικότητας γράφτηκε σε C++ με ενσωμάτωση στον κώδικα R χρησιμοποιώντας Rcpp.

Για την επίλυση του προβλήματος χρησιμοποιήθηκαν τα ακόλουθα πακέτα και βιβλιοθήκες:

  1. OpenCV για εργασία με εικόνες και σχεδίαση γραμμών. Χρησιμοποιούνται προεγκατεστημένες βιβλιοθήκες συστήματος και αρχεία κεφαλίδων, καθώς και δυναμική σύνδεση.

  2. xtensor για εργασία με πολυδιάστατους πίνακες και τανυστές. Χρησιμοποιήσαμε αρχεία κεφαλίδας που περιλαμβάνονται στο ομώνυμο πακέτο R. Η βιβλιοθήκη σάς επιτρέπει να εργάζεστε με πολυδιάστατους πίνακες, τόσο σε μεγάλη σειρά όσο και σε σειρά στηλών.

  3. ndjson για ανάλυση JSON. Αυτή η βιβλιοθήκη χρησιμοποιείται σε xtensor αυτόματα εάν υπάρχει στο έργο.

  4. RcppThread για την οργάνωση επεξεργασίας πολλαπλών νημάτων ενός φορέα από JSON. Χρησιμοποίησε τα αρχεία κεφαλίδας που παρέχονται από αυτό το πακέτο. Από πιο δημοφιλή RcppΠαράλληλο Το πακέτο, μεταξύ άλλων, διαθέτει ενσωματωμένο μηχανισμό διακοπής βρόχου.

Θα πρέπει να σημειωθεί ότι οι xtensor αποδείχθηκε θεϊκό δώρο: εκτός από το γεγονός ότι έχει εκτεταμένη λειτουργικότητα και υψηλή απόδοση, οι προγραμματιστές του αποδείχθηκαν αρκετά ανταποκρινόμενοι και απάντησαν σε ερωτήσεις έγκαιρα και λεπτομερώς. Με τη βοήθειά τους, ήταν δυνατό να υλοποιηθούν οι μετασχηματισμοί των πινάκων OpenCV σε τανυστές xtensor, καθώς και ένας τρόπος συνδυασμού τρισδιάστατων τανυστών εικόνας σε έναν 3-διάστατο τανυστή της σωστής διάστασης (η ίδια η παρτίδα).

Υλικά για εκμάθηση Rcpp, xtensor και RcppThread

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

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

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

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

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

Για τη μεταγλώττιση αρχείων που χρησιμοποιούν αρχεία συστήματος και δυναμική σύνδεση με βιβλιοθήκες που είναι εγκατεστημένες στο σύστημα, χρησιμοποιήσαμε τον μηχανισμό προσθήκης που εφαρμόζεται στο πακέτο Rcpp. Για να βρούμε αυτόματα μονοπάτια και σημαίες, χρησιμοποιήσαμε ένα δημοφιλές βοηθητικό πρόγραμμα Linux pkg-config.

Υλοποίηση του πρόσθετου 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))

Quick Draw Doodle Recognition: πώς να κάνετε φίλους με R, C++ και νευρωνικά δίκτυα
Σύγκριση της ταχύτητας των υλοποιήσεων σε R και C++

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

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

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

res_bench[, cols]

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

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

Quick Draw Doodle Recognition: πώς να κάνετε φίλους με R, C++ και νευρωνικά δίκτυα

Όπως μπορείτε να δείτε, η αύξηση της ταχύτητας αποδείχθηκε πολύ σημαντική και δεν είναι δυνατό να καλυφθεί ο κώδικας C++ παραλληλίζοντας τον κώδικα R.

3. Iterators για την εκφόρτωση παρτίδων από τη βάση δεδομένων

Το R έχει μια καλή φήμη για την επεξεργασία δεδομένων που ταιριάζουν στη μνήμη RAM, ενώ η Python χαρακτηρίζεται περισσότερο από επαναληπτική επεξεργασία δεδομένων, επιτρέποντάς σας να πραγματοποιείτε εύκολα και φυσικά υπολογισμούς εκτός πυρήνα (υπολογισμοί με χρήση εξωτερικής μνήμης). Ένα κλασικό και σχετικό παράδειγμα για εμάς στο πλαίσιο του περιγραφόμενου προβλήματος είναι τα βαθιά νευρωνικά δίκτυα που εκπαιδεύονται με τη μέθοδο gradient descent με προσέγγιση της βαθμίδας σε κάθε βήμα χρησιμοποιώντας ένα μικρό τμήμα παρατηρήσεων ή μίνι-παρτίδα.

Τα πλαίσια Deep Learning που είναι γραμμένα σε Python έχουν ειδικές κλάσεις που υλοποιούν επαναλήπτες με βάση δεδομένα: πίνακες, εικόνες σε φακέλους, δυαδικές μορφές κ.λπ. Μπορείτε να χρησιμοποιήσετε έτοιμες επιλογές ή να γράψετε τις δικές σας για συγκεκριμένες εργασίες. Στο R μπορούμε να εκμεταλλευτούμε όλες τις δυνατότητες της βιβλιοθήκης Python keras με τα διάφορα backend του χρησιμοποιώντας το ομώνυμο πακέτο, το οποίο με τη σειρά του λειτουργεί πάνω από το πακέτο δικτυώ. Το τελευταίο αξίζει ένα ξεχωριστό εκτενές άρθρο. όχι μόνο σας επιτρέπει να εκτελείτε κώδικα Python από το R, αλλά σας επιτρέπει επίσης να μεταφέρετε αντικείμενα μεταξύ συνεδριών R και Python, εκτελώντας αυτόματα όλες τις απαραίτητες μετατροπές τύπων.

Ξεφορτωθήκαμε την ανάγκη αποθήκευσης όλων των δεδομένων στη μνήμη RAM χρησιμοποιώντας το MonetDBLite, όλη η εργασία «νευρωνικού δικτύου» θα εκτελεστεί από τον αρχικό κώδικα στην Python, πρέπει απλώς να γράψουμε έναν επαναλήπτη πάνω από τα δεδομένα, καθώς δεν υπάρχει τίποτα έτοιμο για μια τέτοια κατάσταση είτε στο R είτε στο Python. Υπάρχουν ουσιαστικά μόνο δύο απαιτήσεις για αυτό: πρέπει να επιστρέψει παρτίδες σε έναν ατελείωτο βρόχο και να αποθηκεύσει την κατάστασή του μεταξύ των επαναλήψεων (το τελευταίο στο R υλοποιείται με τον απλούστερο τρόπο χρησιμοποιώντας κλεισίματα). Προηγουμένως, απαιτούνταν ρητά να μετατραπούν οι πίνακες R σε numpy πίνακες μέσα στον επαναλήπτη, αλλά η τρέχουσα έκδοση του πακέτου keras το κάνει μόνη της.

Ο επαναλήπτης για τα δεδομένα εκπαίδευσης και επικύρωσης αποδείχτηκε ως εξής:

Iterator για δεδομένα εκπαίδευσης και επικύρωσης

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 pixel, scale = 0.5 — 128x128 pixel), έγχρωμη ένδειξη (color = FALSE καθορίζει την απόδοση σε κλίμακα του γκρι όταν χρησιμοποιείται color = TRUE κάθε διαδρομή σχεδιάζεται με νέο χρώμα) και μια ένδειξη προεπεξεργασίας για δίκτυα προεκπαιδευμένα στο imagenet. Το τελευταίο χρειάζεται για να κλιμακωθούν οι τιμές των εικονοστοιχείων από το διάστημα [0, 1] στο διάστημα [-1, 1], το οποίο χρησιμοποιήθηκε κατά την εκπαίδευση του παρεχόμενου keras μοντέλα.

Η εξωτερική συνάρτηση περιέχει έλεγχο τύπου ορίσματος, έναν πίνακα data.table με τυχαία μικτούς αριθμούς γραμμών από samples_index και αριθμοί παρτίδας, μετρητής και μέγιστος αριθμός παρτίδων, καθώς και μια έκφραση SQL για την εκφόρτωση δεδομένων από τη βάση δεδομένων. Επιπλέον, ορίσαμε ένα γρήγορο ανάλογο της λειτουργίας στο εσωτερικό keras::to_categorical(). Χρησιμοποιήσαμε σχεδόν όλα τα δεδομένα για την εκπαίδευση, αφήνοντας μισό τοις εκατό για επικύρωση, επομένως το μέγεθος της εποχής περιορίστηκε από την παράμετρο steps_per_epoch κατά την κλήση keras::fit_generator(), και την κατάσταση if (i > max_i) λειτούργησε μόνο για τον επαναλήπτη επικύρωσης.

Στην εσωτερική συνάρτηση, τα ευρετήρια σειρών ανακτώνται για την επόμενη παρτίδα, οι εγγραφές εκφορτώνονται από τη βάση δεδομένων με τον μετρητή παρτίδας να αυξάνεται, ανάλυση JSON (συνάρτηση cpp_process_json_vector(), γραμμένο σε C++) και δημιουργία πινάκων που αντιστοιχούν σε εικόνες. Στη συνέχεια δημιουργούνται one-hot διανύσματα με ετικέτες κλάσεων, πίνακες με τιμές pixel και ετικέτες συνδυάζονται σε μια λίστα, η οποία είναι η τιμή επιστροφής. Για να επιταχύνουμε την εργασία, χρησιμοποιήσαμε τη δημιουργία ευρετηρίων σε πίνακες data.table και τροποποίηση μέσω του συνδέσμου - χωρίς αυτά τα "τσιπ" πακέτου πίνακας δεδομένων Είναι αρκετά δύσκολο να φανταστεί κανείς ότι λειτουργεί αποτελεσματικά με οποιοδήποτε σημαντικό όγκο δεδομένων στο R.

Τα αποτελέσματα των μετρήσεων ταχύτητας σε φορητό υπολογιστή Core i5 είναι τα εξής:

Σημείο αναφοράς Iterator

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

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

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

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

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

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

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: πώς να κάνετε φίλους με R, C++ και νευρωνικά δίκτυα

Εάν έχετε επαρκή ποσότητα μνήμης RAM, μπορείτε να επιταχύνετε σοβαρά τη λειτουργία της βάσης δεδομένων μεταφέροντάς την στην ίδια μνήμη RAM (32 GB είναι αρκετά για την εργασία μας). Στο Linux, το διαμέρισμα είναι προσαρτημένο από προεπιλογή /dev/shm, καταλαμβάνοντας έως και τη μισή χωρητικότητα RAM. Μπορείτε να επισημάνετε περισσότερα με επεξεργασία /etc/fstabγια να πάρει ένα ρεκόρ όπως tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Φροντίστε να κάνετε επανεκκίνηση και ελέγξτε το αποτέλεσμα εκτελώντας την εντολή df -h.

Ο επαναλήπτης για τα δεδομένα δοκιμής φαίνεται πολύ πιο απλός, καθώς το σύνολο δεδομένων δοκιμής ταιριάζει εξ ολοκλήρου στη μνήμη RAM:

Iterator για δεδομένα δοκιμής

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, τα χαρακτηριστικά του οποίου αναλύονται στο Αυτό μήνυμα. Περιλαμβάνεται στάνταρ keras και, κατά συνέπεια, διατίθεται στην ομώνυμη συσκευασία για το R. Αλλά όταν προσπαθείτε να το χρησιμοποιήσετε με εικόνες μονού καναλιού, αποδείχθηκε ένα περίεργο πράγμα: ο τανυστής εισόδου πρέπει πάντα να έχει τη διάσταση (batch, height, width, 3), δηλαδή, ο αριθμός των καναλιών δεν μπορεί να αλλάξει. Δεν υπάρχει τέτοιος περιορισμός στην Python, οπότε βιάσαμε και γράψαμε τη δική μας υλοποίηση αυτής της αρχιτεκτονικής, ακολουθώντας το αρχικό άρθρο (χωρίς το dropout που υπάρχει στην έκδοση 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)

Τώρα δεν είναι δύσκολο να γράψετε μια καθολική συνάρτηση για να αποκτήσετε οποιαδήποτε από τις παρεχόμενες keras μοντέλα με ή χωρίς βάρη εκπαιδευμένα στο 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/ για το 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 pixel, η βάση δεδομένων πρέπει να βρίσκεται στο φάκελο /home/andrey/doodle_db). Μπορείτε να προσθέσετε ταχύτητα εκμάθησης, τύπο βελτιστοποιητή και οποιεσδήποτε άλλες προσαρμόσιμες παραμέτρους στη λίστα. Στη διαδικασία προετοιμασίας της έκδοσης, προέκυψε ότι η αρχιτεκτονική mobilenet_v2 από την τρέχουσα έκδοση keras σε χρήση R δεν μπορεί λόγω αλλαγών που δεν λαμβάνονται υπόψη στο πακέτο R, περιμένουμε να το φτιάξουν.

Αυτή η προσέγγιση κατέστησε δυνατή τη σημαντική επιτάχυνση των πειραμάτων με διαφορετικά μοντέλα σε σύγκριση με την πιο παραδοσιακή εκκίνηση σεναρίων στο RStudio (σημειώνουμε το πακέτο ως πιθανή εναλλακτική tfruns). Αλλά το κύριο πλεονέκτημα είναι η δυνατότητα εύκολης διαχείρισης της εκκίνησης σεναρίων στο Docker ή απλά στον διακομιστή, χωρίς να εγκαταστήσετε το RStudio για αυτό.

6. Dockerization σεναρίων

Χρησιμοποιήσαμε το Docker για να διασφαλίσουμε τη φορητότητα του περιβάλλοντος για μοντέλα εκπαίδευσης μεταξύ των μελών της ομάδας και για γρήγορη ανάπτυξη στο cloud. Μπορείτε να αρχίσετε να εξοικειώνεστε με αυτό το εργαλείο, το οποίο είναι σχετικά ασυνήθιστο για έναν προγραμματιστή R, με αυτό σειρά εκδόσεων ή βίντεο μάθημα.

Το Docker σάς επιτρέπει να δημιουργείτε τις δικές σας εικόνες από την αρχή και να χρησιμοποιείτε άλλες εικόνες ως βάση για τη δημιουργία των δικών σας. Κατά την ανάλυση των διαθέσιμων επιλογών, καταλήξαμε στο συμπέρασμα ότι η εγκατάσταση προγραμμάτων οδήγησης NVIDIA, CUDA+cuDNN και βιβλιοθηκών Python είναι ένα αρκετά ογκώδες μέρος της εικόνας και αποφασίσαμε να πάρουμε ως βάση την επίσημη εικόνα tensorflow/tensorflow:1.12.0-gpu, προσθέτοντας εκεί τα απαραίτητα πακέτα R.

Το τελικό αρχείο docker έμοιαζε ως εξής:

Dockerfile

FROM tensorflow/tensorflow:1.12.0-gpu

MAINTAINER Artem Klevtsov <[email protected]>

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

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

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

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

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

WORKDIR /app

VOLUME /db
VOLUME /app

CMD bash

Για ευκολία, τα πακέτα που χρησιμοποιήθηκαν τοποθετήθηκαν σε μεταβλητές. το μεγαλύτερο μέρος των γραπτών σεναρίων αντιγράφεται μέσα στα δοχεία κατά τη συναρμολόγηση. Αλλάξαμε επίσης το κέλυφος εντολών σε /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 στο cloud. Χρησιμοποιημένος GoogleCloud (καλός οδηγός για τα βασικά) λόγω της μεγάλης επιλογής των διαθέσιμων διαμορφώσεων, των λογικών τιμών και του μπόνους 300$. Από απληστία, παρήγγειλα ένα παράδειγμα 4xV100 με SSD και έναν τόνο RAM, και αυτό ήταν μεγάλο λάθος. Ένα τέτοιο μηχάνημα τρώει χρήματα γρήγορα· μπορείς να πειραματιστείς χωρίς έναν αποδεδειγμένο αγωγό. Για εκπαιδευτικούς λόγους, είναι καλύτερο να πάρετε το K80. Αλλά η μεγάλη ποσότητα μνήμης RAM ήταν χρήσιμη - το cloud 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. Αντί για συμπέρασμα

Ορισμένα προβλήματα που αντιμετωπίσαμε δεν έχουν ακόμη ξεπεραστεί:

  • в keras δεν υπάρχει έτοιμη λειτουργία για αυτόματη αναζήτηση του βέλτιστου ρυθμού εκμάθησης (αναλογική lr_finder στην ΒΙΒΛΙΟΘΗΚΗ γρήγορα) Με κάποια προσπάθεια, είναι δυνατή η μεταφορά εφαρμογών τρίτων στο R, για παράδειγμα, αυτό;
  • Ως συνέπεια του προηγούμενου σημείου, δεν ήταν δυνατό να επιλεγεί η σωστή ταχύτητα εκπαίδευσης κατά τη χρήση πολλών GPU.
  • Υπάρχει έλλειψη σύγχρονων αρχιτεκτονικών νευρωνικών δικτύων, ειδικά εκείνων που είναι προεκπαιδευμένες στο imagenet.
  • Κανένας κύκλος πολιτικής και μεροληπτικά ποσοστά μάθησης (η ανόπτηση συνημιτόνου ήταν κατόπιν αιτήματός μας εφαρμόστηκε, ευχαριστώ skeydan).

Ποια χρήσιμα πράγματα μάθαμε από αυτόν τον διαγωνισμό:

  • Σε υλικό σχετικά χαμηλής κατανάλωσης, μπορείτε να εργαστείτε με αξιοπρεπείς (πολλαπλάσιου μεγέθους RAM) όγκους δεδομένων χωρίς πόνο. Πλαστική σακούλα πίνακας δεδομένων εξοικονομεί μνήμη λόγω της επιτόπιας τροποποίησης των πινάκων, η οποία αποφεύγει την αντιγραφή τους, και όταν χρησιμοποιείται σωστά, οι δυνατότητές του δείχνουν σχεδόν πάντα την υψηλότερη ταχύτητα μεταξύ όλων των γνωστών εργαλείων για τις γλώσσες δέσμης ενεργειών. Η αποθήκευση δεδομένων σε μια βάση δεδομένων σάς επιτρέπει, σε πολλές περιπτώσεις, να μην σκέφτεστε καθόλου την ανάγκη συμπίεσης ολόκληρου του συνόλου δεδομένων στη μνήμη RAM.
  • Οι αργές συναρτήσεις στο R μπορούν να αντικατασταθούν με γρήγορες στη C++ χρησιμοποιώντας το πακέτο Rcpp. Εάν εκτός από τη χρήση RcppThread ή RcppΠαράλληλο, λαμβάνουμε υλοποιήσεις πολλαπλών νημάτων μεταξύ πλατφορμών, επομένως δεν χρειάζεται να παραλληλίσουμε τον κώδικα σε επίπεδο R.
  • πακέτο Rcpp μπορεί να χρησιμοποιηθεί χωρίς σοβαρές γνώσεις C++, περιγράφεται το απαιτούμενο ελάχιστο εδώ. Αρχεία κεφαλίδας για μια σειρά από συναρπαστικές βιβλιοθήκες C όπως xtensor διαθέσιμο στο CRAN, δηλαδή διαμορφώνεται υποδομή για την υλοποίηση έργων που ενσωματώνουν έτοιμο κώδικα C++ υψηλής απόδοσης στο R. Πρόσθετη ευκολία είναι η επισήμανση σύνταξης και ένας στατικός αναλυτής κώδικα C++ στο RStudio.
  • docopt σας επιτρέπει να εκτελείτε αυτόνομα σενάρια με παραμέτρους. Αυτό είναι βολικό για χρήση σε απομακρυσμένο διακομιστή, συμ. υπό αποβάθρα. Στο RStudio, δεν είναι βολικό να διεξάγετε πολλές ώρες πειραμάτων με εκπαιδευτικά νευρωνικά δίκτυα και η εγκατάσταση του IDE στον ίδιο τον διακομιστή δεν δικαιολογείται πάντα.
  • Το Docker διασφαλίζει τη φορητότητα του κώδικα και την αναπαραγωγιμότητα των αποτελεσμάτων μεταξύ προγραμματιστών με διαφορετικές εκδόσεις του λειτουργικού συστήματος και τις βιβλιοθήκες, καθώς και την ευκολία εκτέλεσης σε διακομιστές. Μπορείτε να εκκινήσετε ολόκληρη τη γραμμή εκπαίδευσης με μία μόνο εντολή.
  • Το Google Cloud είναι ένας φιλικός προς τον προϋπολογισμό τρόπος για να πειραματιστείτε σε ακριβό υλικό, αλλά πρέπει να επιλέξετε τις διαμορφώσεις προσεκτικά.
  • Η μέτρηση της ταχύτητας μεμονωμένων τμημάτων κώδικα είναι πολύ χρήσιμη, ειδικά όταν συνδυάζουμε R και C++ και με το πακέτο πάγκος - επίσης πολύ εύκολο.

Συνολικά αυτή η εμπειρία ήταν πολύ ικανοποιητική και συνεχίζουμε να εργαζόμαστε για να επιλύσουμε ορισμένα από τα ζητήματα που τέθηκαν.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο