Quick Draw Doodle Recognition: วิธีผูกมิตรกับ R, C++ และโครงข่ายประสาทเทียม

Quick Draw Doodle Recognition: วิธีผูกมิตรกับ R, C++ และโครงข่ายประสาทเทียม

เฮ้ ฮับ!

ฤดูใบไม้ร่วงที่แล้ว Kaggle ได้จัดการแข่งขันเพื่อจัดประเภทภาพที่วาดด้วยมือ Quick Draw Doodle Recognition ซึ่งมีทีมนักวิทยาศาสตร์ R เข้าร่วมด้วย: อาร์เทม เคลฟต์โซวา, ผู้จัดการฟิลิปปา и อันเดรย์ โอกูร์ตซอฟ. เราจะไม่บรรยายรายละเอียดการแข่งขันที่ได้ดำเนินการไปแล้วค่ะ สิ่งพิมพ์ล่าสุด.

ครั้งนี้มันไม่ได้ผลกับการฟาร์มเหรียญรางวัล แต่ได้รับประสบการณ์อันมีค่ามากมาย ดังนั้นฉันอยากจะบอกชุมชนเกี่ยวกับสิ่งที่น่าสนใจและมีประโยชน์ที่สุดจำนวนหนึ่งบน Kagle และในการทำงานในแต่ละวัน ในหัวข้อที่กล่าวถึง: ชีวิตที่ยากลำบากหากปราศจาก OpenCV, การแยกวิเคราะห์ JSON (ตัวอย่างเหล่านี้ตรวจสอบการรวมโค้ด C++ เข้ากับสคริปต์หรือแพ็คเกจใน R โดยใช้ RCPP) การกำหนดพารามิเตอร์ของสคริปต์และการเชื่อมต่อของโซลูชันขั้นสุดท้าย รหัสทั้งหมดจากข้อความในรูปแบบที่เหมาะสมสำหรับการดำเนินการมีอยู่ใน ที่เก็บ.

สารบัญ:

  1. โหลดข้อมูลจาก CSV เข้าสู่ MonetDB อย่างมีประสิทธิภาพ
  2. กำลังเตรียมแบทช์
  3. ตัววนซ้ำสำหรับการขนถ่ายแบตช์จากฐานข้อมูล
  4. การเลือกสถาปัตยกรรมแบบจำลอง
  5. การกำหนดพารามิเตอร์สคริปต์
  6. การเทียบท่าของสคริปต์
  7. การใช้ GPU หลายตัวบน Google Cloud
  8. แทนการสรุป

1. โหลดข้อมูลจาก CSV ลงในฐานข้อมูล MonetDB อย่างมีประสิทธิภาพ

ข้อมูลในการแข่งขันครั้งนี้ไม่ได้จัดเตรียมไว้ในรูปแบบของรูปภาพสำเร็จรูป แต่อยู่ในรูปแบบของไฟล์ CSV 340 ไฟล์ (หนึ่งไฟล์สำหรับแต่ละคลาส) ที่มี JSON ที่มีพิกัดจุด เมื่อเชื่อมต่อจุดเหล่านี้ด้วยเส้น เราจะได้ภาพสุดท้ายที่มีขนาด 256x256 พิกเซล นอกจากนี้ สำหรับแต่ละบันทึกจะมีป้ายกำกับที่ระบุว่ารูปภาพนั้นได้รับการยอมรับอย่างถูกต้องโดยตัวแยกประเภทที่ใช้ในขณะที่รวบรวมชุดข้อมูล หรือไม่ รหัสตัวอักษรสองตัวของประเทศที่พำนักของผู้เขียนรูปภาพ ตัวระบุที่ไม่ซ้ำกัน การประทับเวลา และชื่อคลาสที่ตรงกับชื่อไฟล์ ข้อมูลต้นฉบับเวอร์ชันที่เรียบง่ายจะมีน้ำหนัก 7.4 GB ในไฟล์เก็บถาวร และประมาณ 20 GB หลังจากการแตกไฟล์ ข้อมูลทั้งหมดหลังจากการแตกไฟล์จะใช้พื้นที่ 240 GB ผู้จัดงานตรวจสอบให้แน่ใจว่าทั้งสองเวอร์ชันทำซ้ำภาพวาดเดียวกัน ซึ่งหมายความว่าเวอร์ชันเต็มซ้ำซ้อน ไม่ว่าในกรณีใด การจัดเก็บรูปภาพ 50 ล้านภาพในไฟล์กราฟิกหรือในรูปแบบของอาร์เรย์จะถือว่าไม่ทำกำไรทันที และเราตัดสินใจรวมไฟล์ CSV ทั้งหมดจากไฟล์เก็บถาวร train_siimpled.zip ลงในฐานข้อมูลพร้อมรูปภาพรุ่นต่อมาในขนาดที่ต้องการ “ได้ทันที” สำหรับแต่ละชุด

เลือกระบบที่ได้รับการพิสูจน์แล้วอย่างดีเป็น DBMS โมเน็ทดีบีคือการใช้งานสำหรับ R เป็นแพ็คเกจ โมเนต์ดีบีไลท์. แพคเกจประกอบด้วยเซิร์ฟเวอร์ฐานข้อมูลเวอร์ชันฝังตัวและอนุญาตให้คุณรับเซิร์ฟเวอร์ได้โดยตรงจากเซสชัน R และทำงานกับเซิร์ฟเวอร์ที่นั่น การสร้างฐานข้อมูลและการเชื่อมต่อทำได้ด้วยคำสั่งเดียว:

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

เราจะต้องสร้างตารางสองตาราง: ตารางหนึ่งสำหรับข้อมูลทั้งหมด อีกตารางหนึ่งสำหรับข้อมูลบริการเกี่ยวกับไฟล์ที่ดาวน์โหลด (มีประโยชน์หากมีสิ่งผิดปกติเกิดขึ้นและต้องดำเนินกระบวนการต่อหลังจากดาวน์โหลดไฟล์หลายไฟล์):

การสร้างตาราง

if (!DBI::dbExistsTable(con, "doodles")) {
  DBI::dbCreateTable(
    con = con,
    name = "doodles",
    fields = c(
      "countrycode" = "char(2)",
      "drawing" = "text",
      "key_id" = "bigint",
      "recognized" = "bool",
      "timestamp" = "timestamp",
      "word" = "text"
    )
  )
}

if (!DBI::dbExistsTable(con, "upload_log")) {
  DBI::dbCreateTable(
    con = con,
    name = "upload_log",
    fields = c(
      "id" = "serial",
      "file_name" = "text UNIQUE",
      "uploaded" = "bool DEFAULT false"
    )
  )
}

วิธีที่เร็วที่สุดในการโหลดข้อมูลลงในฐานข้อมูลคือการคัดลอกไฟล์ CSV โดยตรงโดยใช้คำสั่ง SQL COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTที่ไหน tablename - ชื่อตารางและ path - เส้นทางไปยังไฟล์ ในขณะที่ทำงานกับไฟล์เก็บถาวรก็พบว่ามีการใช้งานในตัว unzip ใน R ทำงานไม่ถูกต้องกับไฟล์จำนวนหนึ่งจากไฟล์เก็บถาวร ดังนั้นเราจึงใช้ระบบ unzip (โดยใช้พารามิเตอร์ getOption("unzip")).

ฟังก์ชั่นการเขียนลงฐานข้อมูล

#' @title Извлечение и загрузка файлов
#'
#' @description
#' Извлечение CSV-файлов из ZIP-архива и загрузка их в базу данных
#'
#' @param con Объект подключения к базе данных (класс `MonetDBEmbeddedConnection`).
#' @param tablename Название таблицы в базе данных.
#' @oaram zipfile Путь к ZIP-архиву.
#' @oaram filename Имя файла внури ZIP-архива.
#' @param preprocess Функция предобработки, которая будет применена извлечённому файлу.
#'   Должна принимать один аргумент `data` (объект `data.table`).
#'
#' @return `TRUE`.
#'
upload_file <- function(con, tablename, zipfile, filename, preprocess = NULL) {
  # Проверка аргументов
  checkmate::assert_class(con, "MonetDBEmbeddedConnection")
  checkmate::assert_string(tablename)
  checkmate::assert_string(filename)
  checkmate::assert_true(DBI::dbExistsTable(con, tablename))
  checkmate::assert_file_exists(zipfile, access = "r", extension = "zip")
  checkmate::assert_function(preprocess, args = c("data"), null.ok = TRUE)

  # Извлечение файла
  path <- file.path(tempdir(), filename)
  unzip(zipfile, files = filename, exdir = tempdir(), 
        junkpaths = TRUE, unzip = getOption("unzip"))
  on.exit(unlink(file.path(path)))

  # Применяем функция предобработки
  if (!is.null(preprocess)) {
    .data <- data.table::fread(file = path)
    .data <- preprocess(data = .data)
    data.table::fwrite(x = .data, file = path, append = FALSE)
    rm(.data)
  }

  # Запрос к БД на импорт CSV
  sql <- sprintf(
    "COPY OFFSET 2 INTO %s FROM '%s' USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORT",
    tablename, path
  )
  # Выполнение запроса к БД
  DBI::dbExecute(con, sql)

  # Добавление записи об успешной загрузке в служебную таблицу
  DBI::dbExecute(con, sprintf("INSERT INTO upload_log(file_name, uploaded) VALUES('%s', true)",
                              filename))

  return(invisible(TRUE))
}

หากคุณต้องการแปลงตารางก่อนที่จะเขียนลงในฐานข้อมูล ก็เพียงพอแล้วที่จะส่งผ่านอาร์กิวเมนต์ preprocess ฟังก์ชั่นที่จะแปลงข้อมูล

รหัสสำหรับการโหลดข้อมูลตามลำดับลงในฐานข้อมูล:

การเขียนข้อมูลลงฐานข้อมูล

# Список файлов для записи
files <- unzip(zipfile, list = TRUE)$Name

# Список исключений, если часть файлов уже была загружена
to_skip <- DBI::dbGetQuery(con, "SELECT file_name FROM upload_log")[[1L]]
files <- setdiff(files, to_skip)

if (length(files) > 0L) {
  # Запускаем таймер
  tictoc::tic()
  # Прогресс бар
  pb <- txtProgressBar(min = 0L, max = length(files), style = 3)
  for (i in seq_along(files)) {
    upload_file(con = con, tablename = "doodles", 
                zipfile = zipfile, filename = files[i])
    setTxtProgressBar(pb, i)
  }
  close(pb)
  # Останавливаем таймер
  tictoc::toc()
}

# 526.141 sec elapsed - копирование SSD->SSD
# 558.879 sec elapsed - копирование USB->SSD

เวลาในการโหลดข้อมูลอาจแตกต่างกันไปขึ้นอยู่กับลักษณะความเร็วของไดรฟ์ที่ใช้ ในกรณีของเรา การอ่านและเขียนภายใน SSD หนึ่งตัวหรือจากแฟลชไดรฟ์ (ไฟล์ต้นฉบับ) ไปยัง SSD (DB) ใช้เวลาน้อยกว่า 10 นาที

ใช้เวลาอีกไม่กี่วินาทีในการสร้างคอลัมน์ที่มีป้ายกำกับคลาสจำนวนเต็มและคอลัมน์ดัชนี (ORDERED INDEX) ด้วยหมายเลขบรรทัดที่ใช้สุ่มตัวอย่างการสังเกตเมื่อสร้างแบทช์:

การสร้างคอลัมน์และดัชนีเพิ่มเติม

message("Generate lables")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD label_int int"))
invisible(DBI::dbExecute(con, "UPDATE doodles SET label_int = dense_rank() OVER (ORDER BY word) - 1"))

message("Generate row numbers")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD id serial"))
invisible(DBI::dbExecute(con, "CREATE ORDERED INDEX doodles_id_ord_idx ON doodles(id)"))

เพื่อแก้ปัญหาการสร้างชุดงานทันที เราจำเป็นต้องบรรลุความเร็วสูงสุดในการแยกแถวสุ่มออกจากตาราง doodles. สำหรับสิ่งนี้เราใช้ 3 เทคนิค ประการแรกคือการลดขนาดของประเภทที่เก็บ ID การสังเกต ในชุดข้อมูลดั้งเดิม ประเภทที่จำเป็นในการจัดเก็บ ID คือ bigintแต่จำนวนการสังเกตทำให้สามารถใส่ตัวระบุซึ่งเท่ากับเลขลำดับลงในประเภทได้ int. ในกรณีนี้การค้นหาจะเร็วขึ้นมาก เคล็ดลับที่สองคือการใช้ ORDERED INDEX — เราตัดสินใจเรื่องนี้ด้วยประสบการณ์โดยผ่านทุกสิ่งที่มีอยู่แล้ว ตัวเลือก. ประการที่สามคือการใช้แบบสอบถามแบบกำหนดพารามิเตอร์ สาระสำคัญของวิธีนี้คือการรันคำสั่งครั้งเดียว PREPARE ด้วยการใช้นิพจน์ที่เตรียมไว้ในภายหลังเมื่อสร้างกลุ่มข้อความค้นหาประเภทเดียวกัน แต่ในความเป็นจริงแล้วมีข้อได้เปรียบเมื่อเปรียบเทียบกับข้อความธรรมดา SELECT ปรากฏว่าอยู่ในช่วงข้อผิดพลาดทางสถิติ

กระบวนการอัปโหลดข้อมูลใช้ RAM ไม่เกิน 450 MB นั่นคือวิธีการที่อธิบายไว้ช่วยให้คุณสามารถย้ายชุดข้อมูลที่มีน้ำหนักหลายสิบกิกะไบต์บนฮาร์ดแวร์ราคาประหยัดเกือบทุกชนิด รวมถึงอุปกรณ์บอร์ดเดี่ยวบางตัวซึ่งค่อนข้างเจ๋ง

สิ่งที่เหลืออยู่คือการวัดความเร็วของการดึงข้อมูล (สุ่ม) และประเมินขนาดเมื่อสุ่มตัวอย่างแบทช์ที่มีขนาดต่างกัน:

เกณฑ์มาตรฐานฐานข้อมูล

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 เป็นการแปลงเทนเซอร์ใน 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. เอ็กซ์เทนเซอร์ สำหรับการทำงานกับอาร์เรย์และเทนเซอร์หลายมิติ เราใช้ไฟล์ส่วนหัวที่รวมอยู่ในแพ็คเกจ R ที่มีชื่อเดียวกัน ไลบรารีช่วยให้คุณทำงานกับอาร์เรย์หลายมิติ ทั้งในแถวหลักและลำดับคอลัมน์หลัก

  3. เอ็นเจสัน สำหรับการแยกวิเคราะห์ JSON ห้องสมุดนี้ใช้ใน เอ็กซ์เทนเซอร์ โดยอัตโนมัติหากมีอยู่ในโปรเจ็กต์

  4. RcppThread สำหรับการจัดระเบียบการประมวลผลเวกเตอร์แบบมัลติเธรดจาก JSON ใช้ไฟล์ส่วนหัวที่จัดทำโดยแพ็คเกจนี้ จากความนิยมมากขึ้น Rcppขนาน เหนือสิ่งอื่นใด แพคเกจนี้มีกลไกการขัดจังหวะแบบวนซ้ำในตัว

มันเป็นที่น่าสังเกตว่า เอ็กซ์เทนเซอร์ กลายเป็นสวรรค์: นอกเหนือจากข้อเท็จจริงที่ว่ามันมีฟังก์ชันการทำงานที่กว้างขวางและประสิทธิภาพสูงแล้ว นักพัฒนายังพบว่าค่อนข้างตอบสนองและตอบคำถามได้อย่างรวดเร็วและละเอียด ด้วยความช่วยเหลือของพวกเขา จึงเป็นไปได้ที่จะนำการแปลงเมทริกซ์ OpenCV ไปเป็นเทนเซอร์เอ็กซ์เทนเซอร์ รวมถึงวิธีการรวมเทนเซอร์รูปภาพ 3 มิติเป็นเทนเซอร์ 4 มิติของขนาดที่ถูกต้อง (แบทช์เอง)

สื่อการเรียนรู้ Rcpp, xtensor และ RcppThread

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

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

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

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

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

ในการรวบรวมไฟล์ที่ใช้ไฟล์ระบบและการลิงก์แบบไดนามิกกับไลบรารีที่ติดตั้งบนระบบ เราใช้กลไกปลั๊กอินที่นำมาใช้ในแพ็คเกจ RCPP. เพื่อค้นหาเส้นทางและแฟล็กโดยอัตโนมัติ เราใช้ยูทิลิตี Linux ยอดนิยม pkg-config.php.

การใช้งานปลั๊กอิน 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 - wrapper เหนือฟังก์ชัน processซึ่งส่งออกผลลัพธ์ไปยังวัตถุ R (อาร์เรย์หลายมิติ)

  • cpp_process_json_vector - wrapper เหนือฟังก์ชัน 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. ตัววนซ้ำสำหรับการขนถ่ายแบตช์จากฐานข้อมูล

R มีชื่อเสียงที่สมควรได้รับในการประมวลผลข้อมูลที่เหมาะกับ RAM ในขณะที่ Python มีลักษณะพิเศษมากกว่าคือการประมวลผลข้อมูลแบบวนซ้ำ ช่วยให้คุณสามารถนำการคำนวณนอกคอร์ไปใช้ได้อย่างง่ายดายและเป็นธรรมชาติ (การคำนวณโดยใช้หน่วยความจำภายนอก) ตัวอย่างคลาสสิกและเกี่ยวข้องสำหรับเราในบริบทของปัญหาที่อธิบายไว้คือโครงข่ายประสาทเทียมเชิงลึกที่ได้รับการฝึกโดยวิธีไล่ระดับลงพร้อมการประมาณของการไล่ระดับสีในแต่ละขั้นตอนโดยใช้การสังเกตส่วนเล็กๆ หรือชุดย่อย

เฟรมเวิร์กการเรียนรู้เชิงลึกที่เขียนด้วย Python มีคลาสพิเศษที่ใช้ตัววนซ้ำตามข้อมูล เช่น ตาราง รูปภาพในโฟลเดอร์ รูปแบบไบนารี ฯลฯ คุณสามารถใช้ตัวเลือกสำเร็จรูปหรือเขียนของคุณเองสำหรับงานเฉพาะได้ ใน R เราสามารถใช้ประโยชน์จากฟีเจอร์ทั้งหมดของไลบรารี Python ได้ Keras ด้วยแบ็กเอนด์ที่หลากหลายโดยใช้แพ็คเกจชื่อเดียวกัน ซึ่งจะทำงานทับแพ็คเกจ ทำซ้ำ. ส่วนหลังสมควรได้รับบทความขนาดยาวแยกต่างหาก ไม่เพียงแต่ช่วยให้คุณสามารถเรียกใช้โค้ด Python จาก R เท่านั้น แต่ยังช่วยให้คุณสามารถถ่ายโอนอ็อบเจ็กต์ระหว่างเซสชัน R และ Python โดยดำเนินการแปลงประเภทที่จำเป็นทั้งหมดโดยอัตโนมัติ

เรากำจัดความจำเป็นในการจัดเก็บข้อมูลทั้งหมดใน RAM โดยใช้ MonetDBLite งาน "โครงข่ายประสาทเทียม" ทั้งหมดจะดำเนินการโดยโค้ดต้นฉบับใน Python เราเพียงแค่ต้องเขียนตัววนซ้ำทับข้อมูลเนื่องจากไม่มีอะไรพร้อม สำหรับสถานการณ์ดังกล่าวใน R หรือ Python โดยพื้นฐานแล้วมีเพียงสองข้อกำหนดเท่านั้น: ต้องส่งคืนแบตช์ในวงวนไม่มีที่สิ้นสุดและบันทึกสถานะระหว่างการวนซ้ำ (อย่างหลังใน R ถูกนำไปใช้ในวิธีที่ง่ายที่สุดโดยใช้การปิด) ก่อนหน้านี้ จำเป็นต้องแปลงอาร์เรย์ R เป็นอาร์เรย์ numpy ภายในตัววนซ้ำอย่างชัดเจน แต่เป็นเวอร์ชันปัจจุบันของแพ็คเกจ Keras ทำมันเอง

ตัววนซ้ำสำหรับข้อมูลการฝึกอบรมและการตรวจสอบความถูกต้องมีดังนี้:

ตัววนซ้ำสำหรับข้อมูลการฝึกอบรมและการตรวจสอบความถูกต้อง

train_generator <- function(db_connection = con,
                            samples_index,
                            num_classes = 340,
                            batch_size = 32,
                            scale = 1,
                            color = FALSE,
                            imagenet_preproc = FALSE) {
  # Проверка аргументов
  checkmate::assert_class(con, "DBIConnection")
  checkmate::assert_integerish(samples_index)
  checkmate::assert_count(num_classes)
  checkmate::assert_count(batch_size)
  checkmate::assert_number(scale, lower = 0.001, upper = 5)
  checkmate::assert_flag(color)
  checkmate::assert_flag(imagenet_preproc)

  # Перемешиваем, чтобы брать и удалять использованные индексы батчей по порядку
  dt <- data.table::data.table(id = sample(samples_index))
  # Проставляем номера батчей
  dt[, batch := (.I - 1L) %/% batch_size + 1L]
  # Оставляем только полные батчи и индексируем
  dt <- dt[, if (.N == batch_size) .SD, keyby = batch]
  # Устанавливаем счётчик
  i <- 1
  # Количество батчей
  max_i <- dt[, max(batch)]

  # Подготовка выражения для выгрузки
  sql <- sprintf(
    "PREPARE SELECT drawing, label_int FROM doodles WHERE id IN (%s)",
    paste(rep("?", batch_size), collapse = ",")
  )
  res <- DBI::dbSendQuery(con, sql)

  # Аналог keras::to_categorical
  to_categorical <- function(x, num) {
    n <- length(x)
    m <- numeric(n * num)
    m[x * n + seq_len(n)] <- 1
    dim(m) <- c(n, num)
    return(m)
  }

  # Замыкание
  function() {
    # Начинаем новую эпоху
    if (i > max_i) {
      dt[, id := sample(id)]
      data.table::setkey(dt, batch)
      # Сбрасываем счётчик
      i <<- 1
      max_i <<- dt[, max(batch)]
    }

    # ID для выгрузки данных
    batch_ind <- dt[batch == i, id]
    # Выгрузка данных
    batch <- DBI::dbFetch(DBI::dbBind(res, as.list(batch_ind)), n = -1)

    # Увеличиваем счётчик
    i <<- i + 1

    # Парсинг JSON и подготовка массива
    batch_x <- cpp_process_json_vector(batch$drawing, scale = scale, color = color)
    if (imagenet_preproc) {
      # Шкалирование c интервала [0, 1] на интервал [-1, 1]
      batch_x <- (batch_x - 0.5) * 2
    }

    batch_y <- to_categorical(batch$label_int, num_classes)
    result <- list(batch_x, batch_y)
    return(result)
  }
}

ฟังก์ชันนี้รับอินพุตตัวแปรที่มีการเชื่อมต่อกับฐานข้อมูล จำนวนบรรทัดที่ใช้ จำนวนคลาส ขนาดแบทช์ สเกล (scale = 1 สอดคล้องกับการเรนเดอร์รูปภาพขนาด 256x256 พิกเซล scale = 0.5 — 128x128 พิกเซล) ตัวแสดงสี (color = FALSE ระบุการเรนเดอร์ในระดับสีเทาเมื่อใช้ color = TRUE แต่ละจังหวะจะถูกวาดด้วยสีใหม่) และตัวบ่งชี้การประมวลผลล่วงหน้าสำหรับเครือข่ายที่ได้รับการฝึกอบรมล่วงหน้าบนอิมเมจเน็ต จำเป็นต้องใช้ส่วนหลังเพื่อปรับขนาดค่าพิกเซลจากช่วงเวลา [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++) และสร้างอาร์เรย์ที่สอดคล้องกับรูปภาพ จากนั้นเวกเตอร์ยอดนิยมที่มีป้ายกำกับคลาสจะถูกสร้างขึ้น อาร์เรย์ที่มีค่าพิกเซลและป้ายกำกับจะรวมกันเป็นรายการซึ่งเป็นค่าที่ส่งคืน เพื่อเร่งการทำงาน เราใช้การสร้างดัชนีในตาราง data.table และแก้ไขผ่านลิงค์ - โดยไม่ต้องใช้แพ็คเกจ "ชิป" เหล่านี้ ตารางข้อมูล เป็นเรื่องยากมากที่จะจินตนาการถึงการทำงานอย่างมีประสิทธิภาพกับข้อมูลจำนวนมากใน R

ผลลัพธ์ของการวัดความเร็วบนแล็ปท็อป Core i5 มีดังนี้:

มาตรฐานตัววนซ้ำ

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

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

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

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

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

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

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle 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 ทั้งหมด:

ตัววนซ้ำสำหรับข้อมูลการทดสอบ

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. การเลือกสถาปัตยกรรมแบบจำลอง

สถาปัตยกรรมแรกที่ใช้คือ โมบายเน็ต v1คุณสมบัติที่จะกล่าวถึงใน นี้ ข้อความ. รวมอยู่ในมาตรฐานแล้ว Keras และดังนั้นจึงมีอยู่ในแพ็คเกจชื่อเดียวกันสำหรับ R แต่เมื่อพยายามใช้กับภาพช่องเดียวก็เกิดเรื่องแปลกขึ้น: เทนเซอร์อินพุตต้องมีมิติเสมอ (batch, height, width, 3)คือไม่สามารถเปลี่ยนจำนวนช่องได้ ไม่มีข้อจำกัดดังกล่าวใน Python ดังนั้นเราจึงรีบเร่งและเขียนการนำสถาปัตยกรรมนี้ไปใช้ของเราเอง ตามบทความต้นฉบับ (โดยไม่มีการออกกลางคันที่อยู่ในเวอร์ชัน keras):

สถาปัตยกรรม Mobilenet v1

library(keras)

top_3_categorical_accuracy <- custom_metric(
    name = "top_3_categorical_accuracy",
    metric_fn = function(y_true, y_pred) {
         metric_top_k_categorical_accuracy(y_true, y_pred, k = 3)
    }
)

layer_sep_conv_bn <- function(object, 
                              filters,
                              alpha = 1,
                              depth_multiplier = 1,
                              strides = c(2, 2)) {

  # NB! depth_multiplier !=  resolution multiplier
  # https://github.com/keras-team/keras/issues/10349

  layer_depthwise_conv_2d(
    object = object,
    kernel_size = c(3, 3), 
    strides = strides,
    padding = "same",
    depth_multiplier = depth_multiplier
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() %>%
  layer_conv_2d(
    filters = filters * alpha,
    kernel_size = c(1, 1), 
    strides = c(1, 1)
  ) %>%
  layer_batch_normalization() %>% 
  layer_activation_relu() 
}

get_mobilenet_v1 <- function(input_shape = c(224, 224, 1),
                             num_classes = 340,
                             alpha = 1,
                             depth_multiplier = 1,
                             optimizer = optimizer_adam(lr = 0.002),
                             loss = "categorical_crossentropy",
                             metrics = c("categorical_crossentropy",
                                         top_3_categorical_accuracy)) {

  inputs <- layer_input(shape = input_shape)

  outputs <- inputs %>%
    layer_conv_2d(filters = 32, kernel_size = c(3, 3), strides = c(2, 2), padding = "same") %>%
    layer_batch_normalization() %>% 
    layer_activation_relu() %>%
    layer_sep_conv_bn(filters = 64, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 128, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 128, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 256, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 256, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
    layer_sep_conv_bn(filters = 1024, strides = c(2, 2)) %>%
    layer_sep_conv_bn(filters = 1024, strides = c(1, 1)) %>%
    layer_global_average_pooling_2d() %>%
    layer_dense(units = num_classes) %>%
    layer_activation_softmax()

    model <- keras_model(
      inputs = inputs,
      outputs = outputs
    )

    model %>% compile(
      optimizer = optimizer,
      loss = loss,
      metrics = metrics
    )

    return(model)
}

ข้อเสียของวิธีนี้ชัดเจน ฉันต้องการทดสอบโมเดลจำนวนมาก แต่ในทางกลับกัน ฉันไม่ต้องการเขียนสถาปัตยกรรมแต่ละแบบด้วยตนเอง นอกจากนี้เรายังขาดโอกาสในการใช้น้ำหนักของแบบจำลองที่ได้รับการฝึกอบรมล่วงหน้าบนอิมเมจเน็ต ตามปกติแล้วการศึกษาเอกสารช่วยได้ การทำงาน 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. การกำหนดพารามิเตอร์ของสคริปต์

เพื่อความสะดวก โค้ดทั้งหมดสำหรับการเริ่มต้นการฝึกอบรมได้รับการออกแบบให้เป็นสคริปต์เดียว โดยใช้พารามิเตอร์ เอกสาร ดังต่อไปนี้:

doc <- '
Usage:
  train_nn.R --help
  train_nn.R --list-models
  train_nn.R [options]

Options:
  -h --help                   Show this message.
  -l --list-models            List available models.
  -m --model=<model>          Neural network model name [default: mobilenet_v2].
  -b --batch-size=<size>      Batch size [default: 32].
  -s --scale-factor=<ratio>   Scale factor [default: 0.5].
  -c --color                  Use color lines [default: FALSE].
  -d --db-dir=<path>          Path to database directory [default: Sys.getenv("db_dir")].
  -r --validate-ratio=<ratio> Validate sample ratio [default: 0.995].
  -n --n-gpu=<number>         Number of GPUs [default: 1].
'
args <- docopt::docopt(doc)

บรรจุภัณฑ์ เอกสาร แสดงถึงการดำเนินการ http://docopt.org/ สำหรับ R ด้วยความช่วยเหลือ สคริปต์จะถูกเปิดใช้งานด้วยคำสั่งง่ายๆ เช่น Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db หรือ ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_dbถ้าเป็นไฟล์ train_nn.R สามารถเรียกใช้งานได้ (คำสั่งนี้จะเริ่มฝึกโมเดล resnet50 สำหรับภาพสามสีขนาด 128x128 พิกเซล ฐานข้อมูลจะต้องอยู่ในโฟลเดอร์ /home/andrey/doodle_db). คุณสามารถเพิ่มความเร็วการเรียนรู้ ประเภทเครื่องมือเพิ่มประสิทธิภาพ และพารามิเตอร์ที่ปรับแต่งได้อื่นๆ ลงในรายการได้ ในกระบวนการเตรียมสิ่งพิมพ์ปรากฎว่าทางสถาปัตยกรรม mobilenet_v2 จากเวอร์ชันปัจจุบัน Keras ในการใช้งาน R จะต้องไม่ เนื่องจากการเปลี่ยนแปลงที่ไม่ได้คำนึงถึงในแพ็คเกจ R เรากำลังรอให้พวกเขาแก้ไข

วิธีการนี้ทำให้สามารถเร่งความเร็วการทดสอบด้วยโมเดลที่แตกต่างกันได้อย่างมากเมื่อเทียบกับการเปิดตัวสคริปต์แบบดั้งเดิมใน RStudio (เราสังเกตว่าแพ็คเกจนี้เป็นทางเลือกที่เป็นไปได้ tfruns). แต่ข้อได้เปรียบหลักคือความสามารถในการจัดการการเปิดตัวสคริปต์ใน Docker หรือบนเซิร์ฟเวอร์ได้อย่างง่ายดายโดยไม่ต้องติดตั้ง RStudio สำหรับสิ่งนี้

6. การเทียบท่าของสคริปต์

เราใช้ Docker เพื่อให้แน่ใจว่าสภาพแวดล้อมสามารถพกพาได้สำหรับโมเดลการฝึกอบรมระหว่างสมาชิกในทีมและเพื่อการปรับใช้อย่างรวดเร็วในระบบคลาวด์ คุณสามารถเริ่มทำความคุ้นเคยกับเครื่องมือนี้ได้ ซึ่งถือว่าค่อนข้างไม่ธรรมดาสำหรับโปรแกรมเมอร์ R นี้ ชุดสิ่งพิมพ์หรือ หลักสูตรวิดีโอ.

Docker ช่วยให้คุณสร้างภาพของคุณเองตั้งแต่เริ่มต้น และใช้ภาพอื่นเป็นพื้นฐานในการสร้างภาพของคุณเอง เมื่อวิเคราะห์ตัวเลือกที่มีอยู่ เราได้ข้อสรุปว่าการติดตั้งไดรเวอร์ NVIDIA, CUDA+cuDNN และไลบรารี Python เป็นส่วนที่ค่อนข้างใหญ่ของอิมเมจ และเราตัดสินใจที่จะใช้อิมเมจอย่างเป็นทางการเป็นพื้นฐาน tensorflow/tensorflow:1.12.0-gpuโดยเพิ่มแพ็คเกจ R ที่จำเป็นที่นั่น

ไฟล์นักเทียบท่าสุดท้ายมีลักษณะดังนี้:

ไฟล์นักเทียบท่า

FROM tensorflow/tensorflow:1.12.0-gpu

MAINTAINER Artem Klevtsov <[email protected]>

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

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

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

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

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

WORKDIR /app

VOLUME /db
VOLUME /app

CMD bash

เพื่อความสะดวก แพ็คเกจที่ใช้จะถูกใส่ลงในตัวแปร สคริปต์ที่เขียนส่วนใหญ่จะถูกคัดลอกภายในคอนเทนเนอร์ระหว่างการประกอบ เรายังเปลี่ยนเชลล์คำสั่งเป็น /bin/bash เพื่อความสะดวกในการใช้งานเนื้อหา /etc/os-release. เพื่อหลีกเลี่ยงความจำเป็นในการระบุเวอร์ชันของระบบปฏิบัติการในโค้ด

นอกจากนี้ยังมีการเขียนสคริปต์ทุบตีขนาดเล็กที่ช่วยให้คุณสามารถเปิดคอนเทนเนอร์ด้วยคำสั่งต่างๆ ตัวอย่างเช่น สคริปต์เหล่านี้อาจเป็นสคริปต์สำหรับฝึกโครงข่ายประสาทเทียมที่เคยวางไว้ภายในคอนเทนเนอร์ หรือเชลล์คำสั่งสำหรับการดีบักและติดตามการทำงานของคอนเทนเนอร์:

สคริปต์เพื่อเปิดคอนเทนเนอร์

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

หากสคริปต์ทุบตีนี้ทำงานโดยไม่มีพารามิเตอร์ สคริปต์จะถูกเรียกภายในคอนเทนเนอร์ train_nn.R ด้วยค่าเริ่มต้น หากอาร์กิวเมนต์ตำแหน่งแรกคือ "bash" คอนเทนเนอร์จะเริ่มทำงานแบบโต้ตอบกับเชลล์คำสั่ง ในกรณีอื่น ๆ ทั้งหมด ค่าของอาร์กิวเมนต์ตำแหน่งจะถูกแทนที่: CMD="Rscript /app/train_nn.R $@".

เป็นที่น่าสังเกตว่าไดเร็กทอรีที่มีข้อมูลต้นฉบับและฐานข้อมูลตลอดจนไดเร็กทอรีสำหรับบันทึกโมเดลที่ได้รับการฝึกอบรมนั้นติดตั้งอยู่ในคอนเทนเนอร์จากระบบโฮสต์ซึ่งช่วยให้คุณเข้าถึงผลลัพธ์ของสคริปต์โดยไม่ต้องมีการปรับเปลี่ยนที่ไม่จำเป็น

7. การใช้ GPU หลายตัวบน Google Cloud

คุณลักษณะอย่างหนึ่งของการแข่งขันคือข้อมูลที่มีเสียงดังมาก (ดูรูปชื่อเรื่อง ยืมมาจาก @Leigh.plt จาก ODS slack) แบตช์จำนวนมากช่วยต่อสู้กับสิ่งนี้ และหลังจากการทดลองบนพีซีที่มี 1 GPU เราก็ตัดสินใจที่จะเชี่ยวชาญโมเดลการฝึกบน GPU หลายตัวในระบบคลาวด์ ใช้ GoogleCloud (คำแนะนำที่ดีเกี่ยวกับพื้นฐาน) เนื่องจากมีการกำหนดค่าให้เลือกมากมาย ราคาที่สมเหตุสมผล และโบนัส $300 ด้วยความโลภ ฉันจึงสั่งซื้ออินสแตนซ์ 4xV100 พร้อม SSD และ RAM จำนวนมาก และนั่นถือเป็นความผิดพลาดครั้งใหญ่ เครื่องจักรดังกล่าวกินเงินอย่างรวดเร็วคุณสามารถทำลายการทดลองได้โดยไม่ต้องมีไปป์ไลน์ที่พิสูจน์แล้ว เพื่อวัตถุประสงค์ทางการศึกษาควรใช้ K80 จะดีกว่า แต่ 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 ในห้องสมุด รวดเร็ว.ai); ด้วยความพยายามบางอย่าง คุณสามารถพอร์ตการใช้งานของบุคคลที่สามไปยัง R ได้ ตัวอย่างเช่น นี้;
  • จากประเด็นที่แล้ว ไม่สามารถเลือกความเร็วการฝึกที่ถูกต้องได้เมื่อใช้ GPU หลายตัว
  • ยังขาดสถาปัตยกรรมเครือข่ายประสาทสมัยใหม่ โดยเฉพาะอย่างยิ่งที่ได้รับการฝึกอบรมล่วงหน้าเกี่ยวกับอิมเมจเน็ต
  • ไม่มีนโยบายวงจรเดียวและอัตราการเรียนรู้แบบเลือกปฏิบัติ (การหลอมโคไซน์ตามคำขอของเรา ดำเนินการ, ขอขอบคุณ สกายแดน).

สิ่งที่เป็นประโยชน์ที่ได้เรียนรู้จากการแข่งขันครั้งนี้:

  • บนฮาร์ดแวร์ที่ใช้พลังงานค่อนข้างต่ำ คุณสามารถทำงานกับข้อมูลในปริมาณที่เหมาะสม (ขนาด RAM หลายเท่า) ได้โดยไม่มีปัญหา ถุงพลาสติก ตารางข้อมูล ช่วยประหยัดหน่วยความจำเนื่องจากการปรับเปลี่ยนตารางแบบแทนที่ ซึ่งหลีกเลี่ยงการคัดลอกตาราง และเมื่อใช้อย่างถูกต้อง ความสามารถของตารางมักจะแสดงให้เห็นถึงความเร็วสูงสุดในบรรดาเครื่องมือทั้งหมดที่เรารู้จักในภาษาสคริปต์ การบันทึกข้อมูลในฐานข้อมูล ในหลายกรณี ช่วยให้คุณไม่ต้องคิดถึงความจำเป็นในการบีบชุดข้อมูลทั้งหมดลงใน RAM เลย
  • ฟังก์ชันที่ช้าใน R สามารถถูกแทนที่ด้วยฟังก์ชันที่เร็วใน C ++ โดยใช้แพ็คเกจ RCPP. หากนอกเหนือจากการใช้งานแล้ว RcppThread หรือ Rcppขนานเราได้รับการใช้งานแบบมัลติเธรดข้ามแพลตฟอร์ม ดังนั้นจึงไม่จำเป็นต้องขนานโค้ดที่ระดับ R
  • บรรจุุภัณฑ์ RCPP สามารถใช้งานได้โดยไม่ต้องมีความรู้ C++ อย่างจริงจัง มีการสรุปขั้นต่ำที่จำเป็นไว้ ที่นี่. ไฟล์ส่วนหัวสำหรับ C-libraries เจ๋ง ๆ จำนวนมากเช่น เอ็กซ์เทนเซอร์ มีอยู่ใน CRAN นั่นคือโครงสร้างพื้นฐานกำลังถูกสร้างขึ้นสำหรับการดำเนินโครงการที่รวมโค้ด C ++ ประสิทธิภาพสูงสำเร็จรูปเข้ากับ R ความสะดวกเพิ่มเติมคือการเน้นไวยากรณ์และตัววิเคราะห์โค้ด C++ แบบคงที่ใน RStudio
  • เอกสาร ช่วยให้คุณสามารถรันสคริปต์ที่มีอยู่ในตัวเองพร้อมพารามิเตอร์ สะดวกสำหรับการใช้งานบนเซิร์ฟเวอร์ระยะไกลรวมถึง ใต้นักเทียบท่า ใน RStudio ไม่สะดวกที่จะทำการทดลองหลายชั่วโมงกับการฝึกอบรมโครงข่ายประสาทเทียมและการติดตั้ง IDE บนเซิร์ฟเวอร์นั้นไม่ได้เป็นสิ่งที่สมเหตุสมผลเสมอไป
  • Docker รับประกันความสามารถในการเคลื่อนย้ายโค้ดและความสามารถในการทำซ้ำของผลลัพธ์ระหว่างนักพัฒนาที่มีระบบปฏิบัติการและไลบรารีเวอร์ชันต่างกัน รวมถึงความง่ายในการดำเนินการบนเซิร์ฟเวอร์ คุณสามารถเปิดใช้ไปป์ไลน์การฝึกอบรมทั้งหมดได้ด้วยคำสั่งเดียว
  • Google Cloud เป็นวิธีที่ประหยัดงบในการทดลองกับฮาร์ดแวร์ราคาแพง แต่คุณต้องเลือกการกำหนดค่าอย่างระมัดระวัง
  • การวัดความเร็วของแต่ละส่วนของโค้ดมีประโยชน์มาก โดยเฉพาะอย่างยิ่งเมื่อรวม R และ C++ และเข้ากับแพ็คเกจ ม้านั่ง - ยังง่ายมาก

โดยรวมแล้วประสบการณ์นี้คุ้มค่ามาก และเรายังคงทำงานต่อไปเพื่อแก้ไขปัญหาบางส่วนที่เกิดขึ้น

ที่มา: will.com

เพิ่มความคิดเห็น