Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf

Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf

Hei Habr!

Musim gugur yang lalu, Kaggle menyelenggarakan kompetisi untuk mengklasifikasikan gambar yang digambar tangan, Quick Draw Doodle Recognition, yang antara lain diikuti oleh tim R-scientist: Artem Klevtsova, Manajer Philippa ΠΈ Andrey Ogurtsov. Kami tidak akan menjelaskan kompetisi secara detail; itu sudah pernah dilakukan publikasi terbaru.

Kali ini tidak berhasil dengan farming medali, tapi banyak pengalaman berharga yang didapat, jadi saya ingin memberi tahu komunitas tentang beberapa hal paling menarik dan berguna di Kagle dan dalam pekerjaan sehari-hari. Di antara topik yang dibahas: hidup sulit tanpanya OpenCV, penguraian JSON (contoh ini memeriksa integrasi kode C++ ke dalam skrip atau paket di R menggunakan RCPP), parameterisasi skrip dan dockerisasi solusi akhir. Semua kode dari pesan dalam bentuk yang sesuai untuk dieksekusi tersedia di repositori.

Isi:

  1. Muat data dari CSV ke MonetDB secara efisien
  2. Mempersiapkan batch
  3. Iterator untuk membongkar batch dari database
  4. Memilih Arsitektur Model
  5. Parameterisasi skrip
  6. Dockerisasi skrip
  7. Menggunakan beberapa GPU di Google Cloud
  8. Alih-alih sebuah kesimpulan

1. Memuat data dari CSV ke database MonetDB secara efisien

Data dalam kompetisi ini disediakan bukan dalam bentuk gambar yang sudah jadi, melainkan dalam bentuk 340 file CSV (satu file untuk setiap kelas) yang berisi JSON dengan koordinat titik. Dengan menghubungkan titik-titik tersebut dengan garis, kita mendapatkan gambar akhir berukuran 256x256 piksel. Juga untuk setiap rekaman terdapat label yang menunjukkan apakah gambar tersebut dikenali dengan benar oleh pengklasifikasi yang digunakan pada saat kumpulan data dikumpulkan, kode dua huruf negara tempat tinggal pembuat gambar, pengidentifikasi unik, stempel waktu dan nama kelas yang cocok dengan nama file. Versi sederhana dari data asli berbobot 7.4 GB dalam arsip dan sekitar 20 GB setelah dibongkar, data lengkap setelah dibongkar membutuhkan 240 GB. Penyelenggara memastikan bahwa kedua versi mereproduksi gambar yang sama, yang berarti versi lengkapnya mubazir. Bagaimanapun, menyimpan 50 juta gambar dalam file grafik atau dalam bentuk array langsung dianggap tidak menguntungkan, dan kami memutuskan untuk menggabungkan semua file CSV dari arsip. kereta_sederhana.zip ke dalam database dengan pembuatan gambar berikutnya dengan ukuran yang diperlukan β€œon the fly” untuk setiap batch.

Sistem yang telah terbukti dipilih sebagai DBMS MonetDB, yaitu implementasi untuk R sebagai sebuah paket MonetDBLite. Paket ini mencakup versi server database yang tertanam dan memungkinkan Anda mengambil server langsung dari sesi R dan bekerja dengannya di sana. Membuat database dan menghubungkannya dilakukan dengan satu perintah:

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

Kita perlu membuat dua tabel: satu untuk semua data, yang lain untuk informasi layanan tentang file yang diunduh (berguna jika terjadi kesalahan dan proses harus dilanjutkan setelah mengunduh beberapa file):

Membuat tabel

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

Cara tercepat untuk memuat data ke dalam database adalah dengan langsung menyalin file CSV menggunakan perintah SQL COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTDimana tablename - nama tabel dan path - jalur ke file. Saat bekerja dengan arsip, ditemukan implementasi bawaan unzip di R tidak berfungsi dengan benar dengan sejumlah file dari arsip, jadi kami menggunakan sistem unzip (menggunakan parameter getOption("unzip")).

Berfungsi untuk menulis ke database

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

Jika Anda perlu mengubah tabel sebelum menulisnya ke database, cukup dengan meneruskan argumennya preprocess fungsi yang akan mengubah data.

Kode untuk memuat data secara berurutan ke dalam database:

Menulis data ke database

# Бписок Ρ„Π°ΠΉΠ»ΠΎΠ² для записи
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

Waktu pemuatan data dapat bervariasi tergantung pada karakteristik kecepatan drive yang digunakan. Dalam kasus kami, membaca dan menulis dalam satu SSD atau dari flash drive (file sumber) ke SSD (DB) membutuhkan waktu kurang dari 10 menit.

Dibutuhkan beberapa detik lagi untuk membuat kolom dengan label kelas bilangan bulat dan kolom indeks (ORDERED INDEX) dengan nomor baris yang akan dijadikan sampel observasi saat membuat batch:

Membuat Kolom dan Indeks Tambahan

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

Untuk mengatasi masalah pembuatan batch dengan cepat, kami perlu mencapai kecepatan maksimum dalam mengekstraksi baris acak dari tabel doodles. Untuk ini kami menggunakan 3 trik. Yang pertama adalah mengurangi dimensi tipe yang menyimpan ID observasi. Dalam kumpulan data asli, tipe yang diperlukan untuk menyimpan ID adalah bigint, tetapi jumlah observasi memungkinkan untuk memasukkan pengidentifikasinya, sama dengan nomor urut, ke dalam tipe int. Pencarian jauh lebih cepat dalam hal ini. Trik kedua adalah dengan menggunakan ORDERED INDEX β€” kami mengambil keputusan ini secara empiris, setelah melalui semua yang tersedia pilihan. Yang ketiga adalah menggunakan kueri berparameter. Inti dari metode ini adalah menjalankan perintah satu kali PREPARE dengan penggunaan ekspresi yang sudah disiapkan selanjutnya saat membuat sekumpulan kueri dengan tipe yang sama, tetapi sebenarnya ada keunggulan dibandingkan dengan yang sederhana SELECT ternyata berada dalam kisaran kesalahan statistik.

Proses upload data memakan RAM tidak lebih dari 450 MB. Artinya, pendekatan yang dijelaskan memungkinkan Anda memindahkan kumpulan data berbobot puluhan gigabyte di hampir semua perangkat keras beranggaran rendah, termasuk beberapa perangkat papan tunggal, dan ini cukup keren.

Yang tersisa hanyalah mengukur kecepatan pengambilan data (acak) dan mengevaluasi penskalaan saat mengambil sampel kumpulan dengan ukuran berbeda:

Tolok ukur basis data

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)

Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf

2. Mempersiapkan batch

Seluruh proses persiapan batch terdiri dari langkah-langkah berikut:

  1. Mengurai beberapa JSON yang berisi vektor string dengan koordinat titik.
  2. Menggambar garis berwarna berdasarkan koordinat titik pada gambar dengan ukuran yang diperlukan (misalnya 256Γ—256 atau 128Γ—128).
  3. Mengubah gambar yang dihasilkan menjadi tensor.

Sebagai bagian dari persaingan antar kernel Python, masalahnya diselesaikan terutama dengan menggunakan OpenCV. Salah satu analog paling sederhana dan paling jelas di R akan terlihat seperti ini:

Menerapkan Konversi JSON ke Tensor di 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)
}

Penggambaran dilakukan menggunakan alat R standar dan disimpan ke PNG sementara yang disimpan dalam RAM (di Linux, direktori R sementara terletak di direktori /tmp, dipasang di RAM). File ini kemudian dibaca sebagai array tiga dimensi dengan angka mulai dari 0 hingga 1. Hal ini penting karena BMP yang lebih konvensional akan dibaca ke dalam array mentah dengan kode warna hex.

Mari kita uji hasilnya:

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

Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf

Batch itu sendiri akan dibentuk sebagai berikut:

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

Implementasi ini tampaknya kurang optimal bagi kami, karena pembentukan batch besar membutuhkan waktu yang sangat lama, dan kami memutuskan untuk memanfaatkan pengalaman rekan-rekan kami dengan menggunakan perpustakaan yang kuat. OpenCV. Pada saat itu belum ada paket siap pakai untuk R (sekarang tidak ada), jadi implementasi minimal dari fungsionalitas yang diperlukan ditulis dalam C++ dengan integrasi ke dalam kode R menggunakan RCPP.

Untuk mengatasi masalah ini, paket dan perpustakaan berikut digunakan:

  1. OpenCV untuk bekerja dengan gambar dan menggambar garis. Pustaka sistem dan file header yang sudah diinstal sebelumnya, serta tautan dinamis digunakan.

  2. xtensor untuk bekerja dengan array dan tensor multidimensi. Kami menggunakan file header yang disertakan dalam paket R dengan nama yang sama. Pustaka ini memungkinkan Anda bekerja dengan array multidimensi, baik dalam urutan utama baris maupun kolom utama.

  3. ndjson untuk mengurai JSON. Perpustakaan ini digunakan di xtensor secara otomatis jika ada dalam proyek.

  4. Benang Rcpp untuk mengatur pemrosesan multi-utas vektor dari JSON. Menggunakan file header yang disediakan oleh paket ini. Dari yang lebih populer RcppParalel Paket tersebut, antara lain, memiliki mekanisme interupsi loop bawaan.

Perlu dicatat bahwa xtensor ternyata merupakan anugerah: selain memiliki fungsionalitas yang luas dan kinerja tinggi, pengembangnya ternyata cukup responsif dan menjawab pertanyaan dengan cepat dan detail. Dengan bantuan mereka, transformasi matriks OpenCV menjadi tensor xtensor dapat diimplementasikan, serta cara untuk menggabungkan tensor gambar 3 dimensi menjadi tensor 4 dimensi dengan dimensi yang benar (batch itu sendiri).

Materi pembelajaran Rcpp, xtensor dan 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

Untuk mengkompilasi file yang menggunakan file sistem dan tautan dinamis dengan perpustakaan yang diinstal pada sistem, kami menggunakan mekanisme plugin yang diterapkan dalam paket RCPP. Untuk menemukan jalur dan tanda secara otomatis, kami menggunakan utilitas Linux yang populer pkg-config.

Implementasi plugin Rcpp untuk menggunakan perpustakaan 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)
  ))
})

Sebagai hasil dari pengoperasian plugin, nilai-nilai berikut akan diganti selama proses kompilasi:

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"

Kode implementasi untuk mengurai JSON dan menghasilkan batch untuk transmisi ke model diberikan di bawah spoiler. Pertama, tambahkan direktori proyek lokal untuk mencari file header (diperlukan untuk ndjson):

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

Implementasi konversi JSON ke tensor di 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;
}

Kode ini harus ditempatkan di file src/cv_xt.cpp dan kompilasi dengan perintah Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); juga diperlukan untuk bekerja nlohmann/json.hpp dari gudang. Kode ini dibagi menjadi beberapa fungsi:

  • to_xt β€” fungsi templat untuk mengubah matriks gambar (cv::Mat) ke tensor xt::xtensor;

  • parse_json β€” fungsi mem-parsing string JSON, mengekstrak koordinat titik, mengemasnya ke dalam vektor;

  • ocv_draw_lines β€” dari vektor titik yang dihasilkan, menggambar garis multi-warna;

  • process β€” menggabungkan fungsi-fungsi di atas dan juga menambahkan kemampuan untuk menskalakan gambar yang dihasilkan;

  • cpp_process_json_str - membungkus fungsinya process, yang mengekspor hasilnya ke objek-R (array multidimensi);

  • cpp_process_json_vector - membungkus fungsinya cpp_process_json_str, yang memungkinkan Anda memproses vektor string dalam mode multi-utas.

Untuk menggambar garis multi-warna, digunakan model warna HSV, diikuti dengan konversi ke RGB. Mari kita uji hasilnya:

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

Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf
Perbandingan kecepatan implementasi di R dan 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") 

Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf

Seperti yang Anda lihat, peningkatan kecepatan ternyata sangat signifikan, dan tidak mungkin mengejar kode C++ dengan memparalelkan kode R.

3. Iterator untuk membongkar batch dari database

R memiliki reputasi yang baik dalam memproses data yang sesuai dengan RAM, sedangkan Python lebih dicirikan oleh pemrosesan data berulang, memungkinkan Anda mengimplementasikan penghitungan out-of-core dengan mudah dan alami (penghitungan menggunakan memori eksternal). Contoh klasik dan relevan bagi kita dalam konteks masalah yang dijelaskan adalah jaringan saraf dalam yang dilatih menggunakan metode penurunan gradien dengan perkiraan gradien pada setiap langkah menggunakan sebagian kecil observasi, atau mini-batch.

Kerangka kerja pembelajaran mendalam yang ditulis dengan Python memiliki kelas khusus yang mengimplementasikan iterator berdasarkan data: tabel, gambar dalam folder, format biner, dll. Anda dapat menggunakan opsi yang sudah jadi atau menulis sendiri untuk tugas tertentu. Di R kita bisa memanfaatkan semua fitur perpustakaan Python keras dengan berbagai backendnya menggunakan paket dengan nama yang sama, yang pada gilirannya berfungsi di atas paket tersebut diam. Yang terakhir ini layak mendapatkan artikel panjang yang terpisah; itu tidak hanya memungkinkan Anda menjalankan kode Python dari R, tetapi juga memungkinkan Anda mentransfer objek antara sesi R dan Python, secara otomatis melakukan semua konversi tipe yang diperlukan.

Kami menghilangkan kebutuhan untuk menyimpan semua data dalam RAM dengan menggunakan MonetDBLite, semua pekerjaan "jaringan saraf" akan dilakukan oleh kode asli dengan Python, kami hanya perlu menulis iterator pada data, karena tidak ada yang siap untuk situasi seperti itu di R atau Python. Pada dasarnya hanya ada dua persyaratan untuk itu: ia harus mengembalikan batch dalam loop tanpa akhir dan menyimpan statusnya di antara iterasi (yang terakhir di R diimplementasikan dengan cara paling sederhana menggunakan penutupan). Sebelumnya, diperlukan untuk secara eksplisit mengubah array R menjadi array numpy di dalam iterator, tetapi versi paket saat ini keras melakukannya sendiri.

Iterator untuk data pelatihan dan validasi ternyata sebagai berikut:

Iterator untuk data pelatihan dan validasi

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

Fungsi ini mengambil input variabel dengan koneksi ke database, jumlah baris yang digunakan, jumlah kelas, ukuran batch, skala (scale = 1 sesuai dengan rendering gambar 256x256 piksel, scale = 0.5 β€” 128x128 piksel), indikator warna (color = FALSE menentukan rendering dalam skala abu-abu saat digunakan color = TRUE setiap goresan digambar dengan warna baru) dan indikator pra-pemrosesan untuk jaringan yang telah dilatih sebelumnya di imagenet. Yang terakhir diperlukan untuk menskalakan nilai piksel dari interval [0, 1] ke interval [-1, 1], yang digunakan saat melatih perangkat yang disediakan keras model.

Fungsi eksternal berisi pemeriksaan tipe argumen, sebuah tabel data.table dengan nomor baris yang dicampur secara acak dari samples_index dan nomor batch, penghitung dan jumlah batch maksimum, serta ekspresi SQL untuk membongkar data dari database. Selain itu, kami mendefinisikan analog cepat dari fungsi di dalamnya keras::to_categorical(). Kami menggunakan hampir semua data untuk pelatihan, menyisakan setengah persen untuk validasi, sehingga ukuran epoch dibatasi oleh parameter steps_per_epoch saat dipanggil keras::fit_generator(), dan kondisinya if (i > max_i) hanya berfungsi untuk iterator validasi.

Dalam fungsi internal, indeks baris diambil untuk batch berikutnya, catatan diturunkan dari database dengan peningkatan penghitung batch, penguraian JSON (fungsi cpp_process_json_vector(), ditulis dalam C++) dan membuat array yang sesuai dengan gambar. Kemudian vektor one-hot dengan label kelas dibuat, array dengan nilai piksel dan label digabungkan menjadi sebuah daftar, yang merupakan nilai kembalian. Untuk mempercepat pekerjaan, kami menggunakan pembuatan indeks dalam tabel data.table dan modifikasi melalui tautan - tanpa paket β€œchip” ini tabel data Sulit membayangkan bekerja secara efektif dengan sejumlah besar data di R.

Hasil pengukuran kecepatan pada laptop Core i5 adalah sebagai berikut:

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

Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf

Jika Anda memiliki jumlah RAM yang cukup, Anda dapat mempercepat pengoperasian database dengan mentransfernya ke RAM yang sama (32 GB sudah cukup untuk tugas kami). Di Linux, partisi dipasang secara default /dev/shm, menghabiskan hingga setengah kapasitas RAM. Anda dapat menyorot lebih banyak dengan mengedit /etc/fstabuntuk mendapatkan rekaman seperti tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Pastikan untuk reboot dan periksa hasilnya dengan menjalankan perintah df -h.

Iterator untuk data pengujian terlihat jauh lebih sederhana, karena kumpulan data pengujian sepenuhnya cocok dengan RAM:

Iterator untuk data uji

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. Pemilihan model arsitektur

Arsitektur pertama yang digunakan adalah mobilenet v1, fitur-fiturnya dibahas dalam ini pesan. Ini disertakan sebagai standar keras dan, oleh karena itu, tersedia dalam paket dengan nama yang sama untuk R. Namun ketika mencoba menggunakannya dengan gambar saluran tunggal, terjadi hal yang aneh: tensor masukan harus selalu berdimensi (batch, height, width, 3), artinya jumlah saluran tidak dapat diubah. Tidak ada batasan seperti itu dalam Python, jadi kami bergegas dan menulis implementasi arsitektur ini sendiri, mengikuti artikel asli (tanpa dropout yang ada di versi keras):

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

Kerugian dari pendekatan ini sangat jelas. Saya ingin menguji banyak model, namun sebaliknya, saya tidak ingin menulis ulang setiap arsitektur secara manual. Kami juga kehilangan kesempatan untuk menggunakan bobot model yang telah dilatih sebelumnya di imagenet. Seperti biasa, mempelajari dokumentasi membantu. Fungsi get_config() memungkinkan Anda mendapatkan deskripsi model dalam bentuk yang sesuai untuk diedit (base_model_conf$layers - daftar R biasa), dan fungsinya from_config() melakukan konversi terbalik ke objek model:

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)

Sekarang tidak sulit untuk menulis fungsi universal untuk mendapatkan fungsi apa pun yang disediakan keras model dengan atau tanpa beban dilatih di imagenet:

Fungsi untuk memuat arsitektur yang sudah jadi

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

Saat menggunakan gambar saluran tunggal, tidak ada bobot yang telah dilatih sebelumnya yang digunakan. Ini bisa diperbaiki: menggunakan fungsi tersebut get_weights() dapatkan bobot model dalam bentuk daftar array R, ubah dimensi elemen pertama daftar ini (dengan mengambil satu saluran warna atau rata-rata ketiganya), lalu muat kembali bobot ke dalam model dengan fungsi set_weights(). Kami tidak pernah menambahkan fungsi ini, karena pada tahap ini sudah jelas bahwa bekerja dengan gambar berwarna lebih produktif.

Kami melakukan sebagian besar eksperimen menggunakan mobilenet versi 1 dan 2, serta resnet34. Arsitektur yang lebih modern seperti SE-ResNeXt tampil baik dalam kompetisi ini. Sayangnya, kami tidak memiliki implementasi yang sudah jadi, dan kami tidak menulis implementasi kami sendiri (tetapi kami pasti akan menulisnya).

5. Parameterisasi skrip

Untuk kenyamanan, semua kode untuk memulai pelatihan dirancang sebagai skrip tunggal, menggunakan parameterisasi dokumen sebagai berikut:

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)

Paket dokumen mewakili implementasinya http://docopt.org/ untuk R. Dengan bantuannya, skrip diluncurkan dengan perintah sederhana seperti 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, jika mengajukan train_nn.R dapat dieksekusi (perintah ini akan mulai melatih model resnet50 pada gambar tiga warna berukuran 128x128 piksel, database harus berada di dalam folder /home/andrey/doodle_db). Anda dapat menambahkan kecepatan pembelajaran, jenis pengoptimal, dan parameter lain yang dapat disesuaikan ke dalam daftar. Dalam proses penyusunan publikasi, ternyata arsitekturnya mobilenet_v2 dari versi saat ini keras dalam penggunaan R tidak harus karena perubahan yang tidak diperhitungkan dalam paket R, kami menunggu mereka memperbaikinya.

Pendekatan ini memungkinkan untuk mempercepat eksperimen dengan model yang berbeda secara signifikan dibandingkan dengan peluncuran skrip yang lebih tradisional di RStudio (kami mencatat paket ini sebagai alternatif yang memungkinkan. tfruns). Namun keunggulan utamanya adalah kemampuan untuk dengan mudah mengelola peluncuran skrip di Docker atau hanya di server, tanpa menginstal RStudio untuk ini.

6. Dockerisasi skrip

Kami menggunakan Docker untuk memastikan portabilitas lingkungan untuk model pelatihan antar anggota tim dan untuk penerapan cepat di cloud. Anda dapat mulai mengenal alat ini, yang relatif tidak biasa bagi seorang programmer R, dengan ini serangkaian publikasi atau kursus video.

Docker memungkinkan Anda membuat image Anda sendiri dari awal dan menggunakan image lain sebagai dasar untuk membuat image Anda sendiri. Saat menganalisis opsi yang tersedia, kami sampai pada kesimpulan bahwa menginstal NVIDIA, driver CUDA+cuDNN, dan pustaka Python adalah bagian gambar yang cukup banyak, dan kami memutuskan untuk menggunakan gambar resmi sebagai dasar. tensorflow/tensorflow:1.12.0-gpu, menambahkan paket R yang diperlukan di sana.

File buruh pelabuhan terakhir terlihat seperti ini:

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

Untuk kenyamanan, paket yang digunakan dimasukkan ke dalam variabel; sebagian besar skrip tertulis disalin ke dalam wadah selama perakitan. Kami juga mengubah shell perintah menjadi /bin/bash untuk kemudahan penggunaan konten /etc/os-release. Hal ini menghindari kebutuhan untuk menentukan versi OS dalam kode.

Selain itu, skrip bash kecil telah ditulis yang memungkinkan Anda meluncurkan container dengan berbagai perintah. Misalnya, ini bisa berupa skrip untuk melatih jaringan saraf yang sebelumnya ditempatkan di dalam penampung, atau shell perintah untuk melakukan debug dan memantau pengoperasian penampung:

Skrip untuk meluncurkan penampung

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

Jika skrip bash ini dijalankan tanpa parameter, skrip tersebut akan dipanggil di dalam container train_nn.R dengan nilai default; jika argumen posisi pertama adalah "bash", maka container akan dimulai secara interaktif dengan shell perintah. Dalam semua kasus lain, nilai argumen posisi diganti: CMD="Rscript /app/train_nn.R $@".

Perlu dicatat bahwa direktori dengan data sumber dan database, serta direktori untuk menyimpan model terlatih, dipasang di dalam wadah dari sistem host, yang memungkinkan Anda mengakses hasil skrip tanpa manipulasi yang tidak perlu.

7. Menggunakan banyak GPU di Google Cloud

Salah satu keistimewaan kompetisi ini adalah datanya yang sangat berisik (lihat gambar judul, dipinjam dari @Leigh.plt dari ODS slack). Batch besar membantu mengatasi hal ini, dan setelah melakukan percobaan pada PC dengan 1 GPU, kami memutuskan untuk menguasai model pelatihan pada beberapa GPU di cloud. GoogleCloud yang digunakan (panduan bagus untuk dasar-dasarnya) karena banyaknya pilihan konfigurasi yang tersedia, harga wajar, dan bonus $300. Karena keserakahan, saya memesan instance 4xV100 dengan SSD dan banyak RAM, dan itu adalah kesalahan besar. Mesin seperti itu menghabiskan uang dengan cepat; Anda bisa bangkrut saat bereksperimen tanpa saluran pipa yang terbukti. Untuk tujuan pendidikan, lebih baik menggunakan K80. Namun sejumlah besar RAM berguna - SSD cloud tidak terkesan dengan kinerjanya, sehingga database dipindahkan ke dev/shm.

Yang paling menarik adalah potongan kode yang bertanggung jawab untuk menggunakan banyak GPU. Pertama, model dibuat di CPU menggunakan manajer konteks, seperti di 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
  )
})

Kemudian model yang belum dikompilasi (ini penting) disalin ke sejumlah GPU yang tersedia, dan baru setelah itu dikompilasi:

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

Teknik klasik membekukan semua lapisan kecuali yang terakhir, melatih lapisan terakhir, mencairkan dan melatih ulang seluruh model untuk beberapa GPU tidak dapat diterapkan.

Pelatihan dipantau tanpa digunakan. papan tensor, membatasi diri untuk mencatat log dan menyimpan model dengan nama yang informatif setelah setiap zaman:

Panggilan balik

# Π¨Π°Π±Π»ΠΎΠ½ ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π° Π»ΠΎΠ³Π°
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. Daripada membuat kesimpulan

Beberapa permasalahan yang kami temui belum teratasi:

  • Π² keras tidak ada fungsi siap pakai untuk secara otomatis mencari kecepatan pemelajaran optimal (analog lr_finder di perpustakaan cepat); Dengan beberapa upaya, dimungkinkan untuk mem-porting implementasi pihak ketiga ke R, misalnya, ini;
  • sebagai konsekuensi dari poin sebelumnya, tidak mungkin memilih kecepatan pelatihan yang tepat saat menggunakan beberapa GPU;
  • kurangnya arsitektur jaringan saraf modern, terutama yang telah dilatih sebelumnya tentang imagenet;
  • tidak ada kebijakan satu siklus dan kecepatan pembelajaran yang diskriminatif (cosine annealing sesuai permintaan kami diimplementasikan, Terima kasih skidan).

Hal bermanfaat apa yang didapat dari kompetisi ini:

  • Pada perangkat keras berdaya relatif rendah, Anda dapat bekerja dengan volume data yang layak (berkali-kali lipat ukuran RAM) tanpa kesulitan. Kantong plastik tabel data menghemat memori karena modifikasi tabel di tempat, sehingga menghindari penyalinan, dan bila digunakan dengan benar, kemampuannya hampir selalu menunjukkan kecepatan tertinggi di antara semua alat bahasa skrip yang kami kenal. Menyimpan data dalam database memungkinkan Anda, dalam banyak kasus, untuk tidak memikirkan sama sekali tentang perlunya memasukkan seluruh kumpulan data ke dalam RAM.
  • Fungsi lambat di R dapat diganti dengan fungsi cepat di C++ menggunakan paket RCPP. Jika selain digunakan Benang Rcpp ΠΈΠ»ΠΈ RcppParalel, kami mendapatkan implementasi multi-utas lintas platform, jadi tidak perlu memparalelkan kode di level R.
  • Kemasan RCPP dapat digunakan tanpa pengetahuan serius tentang C++, persyaratan minimum telah diuraikan di sini. File header untuk sejumlah C-library keren seperti xtensor tersedia di CRAN, yaitu infrastruktur sedang dibentuk untuk implementasi proyek yang mengintegrasikan kode C++ berkinerja tinggi yang sudah jadi ke dalam R. Kenyamanan tambahan adalah penyorotan sintaksis dan penganalisis kode C++ statis di RStudio.
  • dokumen memungkinkan Anda menjalankan skrip mandiri dengan parameter. Ini nyaman untuk digunakan pada server jarak jauh, termasuk. di bawah buruh pelabuhan. Di RStudio, melakukan eksperimen berjam-jam dengan melatih jaringan saraf tidak nyaman, dan memasang IDE di server itu sendiri tidak selalu dapat dibenarkan.
  • Docker memastikan portabilitas kode dan reproduktifitas hasil antara pengembang dengan versi OS dan perpustakaan yang berbeda, serta kemudahan eksekusi di server. Anda dapat meluncurkan seluruh alur pelatihan hanya dengan satu perintah.
  • Google Cloud adalah cara yang hemat anggaran untuk bereksperimen pada perangkat keras yang mahal, namun Anda harus memilih konfigurasi dengan hati-hati.
  • Mengukur kecepatan setiap fragmen kode sangat berguna, terutama saat menggabungkan R dan C++, dan dengan paketnya bangku - juga sangat mudah.

Secara keseluruhan pengalaman ini sangat bermanfaat dan kami terus berupaya menyelesaikan beberapa masalah yang muncul.

Sumber: www.habr.com

Tambah komentar