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. استفاده از چندین پردازنده گرافیکی در Google Cloud
  8. به جای یک نتیجه گیری

1. به طور موثر داده ها را از CSV در پایگاه داده MonetDB بارگیری کنید

داده های این مسابقه نه به صورت تصاویر آماده، بلکه در قالب 340 فایل CSV (یک فایل برای هر کلاس) حاوی JSON با مختصات نقطه ارائه می شود. با اتصال این نقاط با خطوط، تصویر نهایی در ابعاد 256x256 پیکسل به دست می آید. همچنین برای هر رکورد برچسبی وجود دارد که نشان می دهد آیا تصویر به درستی توسط طبقه بندی کننده مورد استفاده در زمان جمع آوری مجموعه داده شناسایی شده است یا خیر، یک کد دو حرفی از کشور محل اقامت نویسنده تصویر، یک شناسه منحصر به فرد، یک مهر زمانی. و نام کلاسی که با نام فایل مطابقت دارد. وزن نسخه ساده شده داده های اصلی 7.4 گیگابایت در آرشیو و تقریباً 20 گیگابایت پس از باز کردن بسته بندی است. سازمان دهندگان اطمینان حاصل کردند که هر دو نسخه نقشه های یکسانی را تولید می کنند، به این معنی که نسخه کامل اضافی است. در هر صورت، ذخیره 240 میلیون تصویر در فایل های گرافیکی یا به صورت آرایه بلافاصله غیرمنفعت تلقی شد و تصمیم گرفتیم تمام فایل های CSV را از آرشیو ادغام کنیم. train_simplified.zip به پایگاه داده با تولید بعدی تصاویر با اندازه مورد نیاز "در حال پرواز" برای هر دسته.

یک سیستم به خوبی اثبات شده به عنوان DBMS انتخاب شد MonetDB، یعنی پیاده سازی برای R به عنوان یک بسته MonetDBLite. این بسته شامل یک نسخه تعبیه شده از سرور پایگاه داده است و به شما امکان می دهد سرور را مستقیماً از یک جلسه R انتخاب کنید و در آنجا با آن کار کنید. ایجاد پایگاه داده و اتصال به آن با یک دستور انجام می شود:

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

ما باید دو جدول ایجاد کنیم: یکی برای همه داده‌ها، دیگری برای اطلاعات سرویس در مورد فایل‌های دانلود شده (مفید است اگر مشکلی پیش بیاید و فرآیند باید پس از دانلود چندین فایل از سر گرفته شود):

ایجاد جداول

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

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

سریع ترین راه برای بارگذاری داده ها در پایگاه داده کپی مستقیم فایل های CSV با استفاده از دستور SQL - بود COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTجایی که tablename - نام جدول و path - مسیر فایل در حین کار با آرشیو، مشخص شد که پیاده سازی داخلی unzip in 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 ترفند استفاده کردیم. اولین مورد کاهش ابعاد نوعی بود که شناسه مشاهده را ذخیره می کند. در مجموعه داده اصلی، نوع مورد نیاز برای ذخیره شناسه است bigint، اما تعداد مشاهدات این امکان را فراهم می کند که شناسه های آنها برابر با عدد ترتیبی در نوع قرار گیرند. int. جستجو در این مورد بسیار سریعتر است. ترفند دوم استفاده بود ORDERED INDEX - ما به طور تجربی به این تصمیم رسیدیم و همه چیز را گذراندیم گزینه ها. سومین مورد استفاده از پرس و جوهای پارامتری بود. ماهیت روش این است که دستور را یک بار اجرا کنید PREPARE با استفاده بعدی از یک عبارت آماده شده هنگام ایجاد دسته ای از پرس و جو از همان نوع، اما در واقع مزیتی در مقایسه با یک عبارت ساده وجود دارد SELECT معلوم شد که در محدوده خطای آماری قرار دارد.

فرآیند آپلود داده ها بیش از 450 مگابایت رم مصرف نمی کند. یعنی رویکرد توصیف‌شده به شما امکان می‌دهد مجموعه‌های داده با وزن ده‌ها گیگابایت را روی تقریباً هر سخت‌افزار اقتصادی، از جمله برخی دستگاه‌های تک‌برد، جابه‌جا کنید، که بسیار جالب است.

تنها چیزی که باقی می‌ماند اندازه‌گیری سرعت بازیابی داده‌ها (تصادفی) و ارزیابی مقیاس‌بندی هنگام نمونه‌برداری از دسته‌هایی با اندازه‌های مختلف است:

معیار پایگاه داده

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. ترسیم خطوط رنگی بر اساس مختصات نقاط روی تصویری با اندازه مورد نیاز (مثلاً ۲۵۶×۲۵۶ یا ۱۲۸×۱۲۸).
  3. تبدیل تصاویر به دست آمده به تانسور.

به عنوان بخشی از رقابت بین هسته های پایتون، مشکل در درجه اول با استفاده از آن حل شد OpenCV. یکی از ساده ترین و واضح ترین آنالوگ های R به شکل زیر است:

پیاده سازی تبدیل JSON به Tensor در R

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

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

طراحی با استفاده از ابزارهای استاندارد R انجام می شود و در یک PNG موقت ذخیره شده در RAM ذخیره می شود (در لینوکس، دایرکتوری های موقت R در فهرست قرار دارند. /tmp، نصب شده در RAM). سپس این فایل به صورت یک آرایه سه بعدی با اعدادی از 0 تا 1 خوانده می شود.

بیایید نتیجه را آزمایش کنیم:

zip_file <- file.path("data", "train_simplified.zip")
csv_file <- "cat.csv"
unzip(zip_file, files = csv_file, exdir = tempdir(), 
      junkpaths = TRUE, unzip = getOption("unzip"))
tmp_data <- data.table::fread(file.path(tempdir(), csv_file), sep = ",", 
                              select = "drawing", nrows = 10000)
arr <- r_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256   3
plot(magick::image_read(arr))

Quick Draw Doodle Recognition: چگونه با R، C++ و شبکه های عصبی دوست شویم

خود دسته به صورت زیر تشکیل می شود:

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

این پیاده سازی برای ما غیربهینه به نظر می رسید، زیرا تشکیل دسته های بزرگ به طرز نامناسبی زمان زیادی می برد و تصمیم گرفتیم با استفاده از یک کتابخانه قدرتمند از تجربیات همکاران خود استفاده کنیم. OpenCV. در آن زمان هیچ بسته آماده ای برای R وجود نداشت (الان وجود ندارد)، بنابراین یک پیاده سازی حداقلی از عملکرد مورد نیاز در C ++ با ادغام در کد R با استفاده از Rcpp.

برای حل مشکل از بسته ها و کتابخانه های زیر استفاده شد:

  1. OpenCV برای کار با تصاویر و کشیدن خطوط. از کتابخانه های سیستمی از پیش نصب شده و فایل های هدر و همچنین پیوندهای پویا استفاده می شود.

  2. xtensor برای کار با آرایه ها و تانسورهای چند بعدی. ما از فایل های هدر موجود در بسته R با همین نام استفاده کردیم. این کتابخانه به شما امکان می دهد با آرایه های چند بعدی کار کنید، هم به ترتیب اصلی و هم به ترتیب ستون.

  3. ndjson برای تجزیه JSON. این کتابخانه در xtensor اگر در پروژه وجود داشته باشد به صورت خودکار

  4. RcppThread برای سازماندهی پردازش چند رشته ای یک بردار از JSON. از فایل های هدر ارائه شده توسط این بسته استفاده کرد. از محبوب تر RcppParallel بسته، در میان چیزهای دیگر، دارای مکانیزم داخلی وقفه حلقه است.

شایان ذکر است که است xtensor معلوم شد که یک موهبت الهی است: علاوه بر این که دارای عملکرد گسترده و عملکرد بالا است، توسعه دهندگان آن کاملاً پاسخگو بودند و به سؤالات فوری و با جزئیات پاسخ دادند. با کمک آنها، امکان پیاده سازی تبدیل ماتریس های OpenCV به تانسورهای xtensor و همچنین راهی برای ترکیب تانسورهای تصویر 3 بعدی در یک تانسور 4 بعدی با ابعاد صحیح (خود دسته) وجود داشت.

مواد برای یادگیری Rcpp، xtensor و RcppThread

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

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

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

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

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

برای کامپایل فایل هایی که از فایل های سیستمی و پیوند پویا با کتابخانه های نصب شده روی سیستم استفاده می کنند، از مکانیزم پلاگین پیاده سازی شده در بسته استفاده کردیم. Rcpp. برای یافتن خودکار مسیرها و پرچم‌ها، از یک ابزار محبوب لینوکس استفاده کردیم پیکربندی pkg.

پیاده سازی پلاگین Rcpp برای استفاده از کتابخانه OpenCV

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

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

در نتیجه عملکرد افزونه، مقادیر زیر در طول فرآیند کامپایل جایگزین می شوند:

Rcpp:::.plugins$opencv()$env

# $PKG_CXXFLAGS
# [1] "-I/usr/include/opencv"
#
# $PKG_LIBS
# [1] "-lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_aruco -lopencv_bgsegm -lopencv_bioinspired -lopencv_ccalib -lopencv_datasets -lopencv_dpm -lopencv_face -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_line_descriptor -lopencv_optflow -lopencv_video -lopencv_plot -lopencv_reg -lopencv_saliency -lopencv_stereo -lopencv_structured_light -lopencv_phase_unwrapping -lopencv_rgbd -lopencv_viz -lopencv_surface_matching -lopencv_text -lopencv_ximgproc -lopencv_calib3d -lopencv_features2d -lopencv_flann -lopencv_xobjdetect -lopencv_objdetect -lopencv_ml -lopencv_xphoto -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -lopencv_photo -lopencv_imgproc -lopencv_core"

کد پیاده سازی برای تجزیه JSON و تولید یک دسته برای انتقال به مدل در زیر اسپویلر آورده شده است. ابتدا یک فهرست پروژه محلی برای جستجوی فایل‌های هدر اضافه کنید (برای ndjson لازم است):

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

پیاده سازی تبدیل JSON به تانسور در C++

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

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

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

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

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

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

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

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

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

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

این کد باید در فایل قرار داده شود src/cv_xt.cpp و با دستور کامپایل کنید Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); همچنین برای کار مورد نیاز است nlohmann/json.hpp از مخزن. کد به چندین عملکرد تقسیم می شود:

  • to_xt - یک تابع قالب برای تبدیل یک ماتریس تصویر (cv::Mat) به یک تانسور xt::xtensor;

  • parse_json - تابع یک رشته JSON را تجزیه می کند، مختصات نقاط را استخراج می کند و آنها را در یک بردار بسته بندی می کند.

  • ocv_draw_lines - از بردار حاصل از نقاط، خطوط چند رنگ ترسیم می کند.

  • process - توابع فوق را ترکیب می کند و همچنین توانایی مقیاس کردن تصویر حاصل را اضافه می کند.

  • cpp_process_json_str - لفاف بر روی عملکرد process، که نتیجه را به یک شی R (آرایه چند بعدی) صادر می کند.

  • cpp_process_json_vector - لفاف بر روی عملکرد cpp_process_json_str، که به شما امکان می دهد یک وکتور رشته را در حالت چند رشته ای پردازش کنید.

برای ترسیم خطوط چند رنگ، از مدل رنگی HSV و به دنبال آن تبدیل به RGB استفاده شد. بیایید نتیجه را آزمایش کنیم:

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

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++ و شبکه های عصبی دوست شویم

همانطور که می بینید، افزایش سرعت بسیار چشمگیر بود و نمی توان با موازی کردن کد R به کد ++C رسید.

3. تکرار کننده برای تخلیه دسته ها از پایگاه داده

R برای پردازش داده های متناسب با RAM شهرت خوبی دارد، در حالی که پایتون بیشتر با پردازش داده های تکراری مشخص می شود و به شما امکان می دهد محاسبات خارج از هسته (محاسبات با استفاده از حافظه خارجی) را به راحتی و به طور طبیعی پیاده سازی کنید. یک مثال کلاسیک و مرتبط برای ما در زمینه مسئله توصیف شده، شبکه‌های عصبی عمیق است که با روش نزول گرادیان با تقریب گرادیان در هر مرحله با استفاده از بخش کوچکی از مشاهدات یا مینی دسته‌ای آموزش داده شده‌اند.

چارچوب های یادگیری عمیق نوشته شده در پایتون دارای کلاس های خاصی هستند که تکرار کننده ها را بر اساس داده ها پیاده سازی می کنند: جداول، تصاویر در پوشه ها، فرمت های باینری و غیره. در R می توانیم از تمام ویژگی های کتابخانه پایتون استفاده کنیم کراس با پشتوانه های مختلف خود با استفاده از بسته ای به همین نام، که به نوبه خود در بالای بسته کار می کند شبکه سازی. مورد دوم مستحق یک مقاله طولانی جداگانه است. این نه تنها به شما اجازه می دهد تا کد پایتون را از R اجرا کنید، بلکه به شما امکان می دهد اشیاء را بین جلسات R و Python منتقل کنید و به طور خودکار تمام تبدیل های نوع لازم را انجام دهید.

ما با استفاده از MonetDBLite از نیاز به ذخیره تمام داده ها در RAM خلاص شدیم، تمام کارهای "شبکه عصبی" توسط کد اصلی در پایتون انجام می شود، فقط باید یک تکرار کننده روی داده ها بنویسیم، زیرا چیزی آماده نیست. برای چنین وضعیتی در R یا Python. اساساً فقط دو الزام برای آن وجود دارد: باید دسته ها را در یک حلقه بی پایان برگرداند و حالت خود را بین تکرارها ذخیره کند (دومی در R با استفاده از بسته شدن به ساده ترین روش پیاده سازی می شود). قبلاً لازم بود که آرایه‌های R به طور صریح به آرایه‌های numpy در داخل تکرارکننده تبدیل شوند، اما نسخه فعلی بسته کراس خودش انجام می دهد

تکرار کننده برای داده های آموزش و اعتبارسنجی به شرح زیر است:

Iterator برای داده های آموزش و اعتبار سنجی

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

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

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

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

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

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

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

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

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

تابع یک متغیر با اتصال به پایگاه داده، تعداد خطوط استفاده شده، تعداد کلاس ها، اندازه دسته، مقیاس (scale = 1 مربوط به ارائه تصاویر 256x256 پیکسل است، scale = 0.5 - 128x128 پیکسل)، نشانگر رنگ (color = FALSE هنگام استفاده رندر را در مقیاس خاکستری مشخص می کند color = TRUE هر ضربه با رنگ جدید ترسیم می شود) و یک نشانگر پیش پردازش برای شبکه های از قبل آموزش دیده در imagenet. دومی برای مقیاس‌بندی مقادیر پیکسل از بازه [0، 1] تا بازه [-1، 1] مورد نیاز است، که هنگام آموزش موارد ارائه شده استفاده می‌شود. کراس مدل ها.

تابع خارجی شامل بررسی نوع آرگومان، یک جدول است data.table با اعداد خط به طور تصادفی مخلوط شده از samples_index و شماره دسته، شمارنده و حداکثر تعداد دسته، و همچنین یک عبارت SQL برای تخلیه داده ها از پایگاه داده. علاوه بر این، ما یک آنالوگ سریع از تابع داخل تعریف کردیم keras::to_categorical(). ما تقریباً از تمام داده ها برای آموزش استفاده کردیم، نیم درصد را برای اعتبار سنجی باقی گذاشتیم، بنابراین اندازه دوره توسط پارامتر محدود شد. steps_per_epoch هنگام تماس keras::fit_generator()، و شرایط if (i > max_i) فقط برای تکرار کننده اعتبارسنجی کار کرد.

در تابع داخلی، فهرست های ردیف برای دسته بعدی بازیابی می شوند، رکوردها با افزایش شمارنده دسته ای از پایگاه داده تخلیه می شوند، تجزیه JSON (عملکرد) cpp_process_json_vector()، نوشته شده در C++) و ایجاد آرایه های مربوط به تصاویر. سپس بردارهای تک داغ با برچسب‌های کلاس ایجاد می‌شوند، آرایه‌هایی با مقادیر پیکسل و برچسب‌ها در یک لیست ترکیب می‌شوند که مقدار بازگشتی است. برای سرعت بخشیدن به کار از ایجاد ایندکس در جداول استفاده کردیم data.table و اصلاح از طریق پیوند - بدون این بسته "تراشه" جدول داده تصور کار موثر با هر مقدار قابل توجهی از داده در R بسیار دشوار است.

نتایج اندازه گیری سرعت در لپ تاپ Core i5 به شرح زیر است:

معیار Iterator

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

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

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

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

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

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

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

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: چگونه با R، C++ و شبکه های عصبی دوست شویم

اگر مقدار رم کافی دارید، می توانید با انتقال دیتابیس به همین رم، به طور جدی به عملکرد دیتابیس سرعت ببخشید (32 گیگابایت برای کار ما کافی است). در لینوکس، پارتیشن به صورت پیش فرض نصب می شود /dev/shm، تا نیمی از ظرفیت رم را اشغال می کند. با ویرایش می توانید موارد بیشتری را برجسته کنید /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. انتخاب معماری مدل

اولین معماری مورد استفاده بود موبایل نت نسخه 1، که ویژگی های آن در این پیام به صورت استاندارد گنجانده شده است کراس و، بر این اساس، در بسته ای به همین نام برای R موجود است. اما هنگام تلاش برای استفاده از آن با تصاویر تک کانال، یک چیز عجیب مشخص شد: تانسور ورودی همیشه باید دارای ابعاد باشد. (batch, height, width, 3)، یعنی تعداد کانال ها قابل تغییر نیست. چنین محدودیتی در پایتون وجود ندارد، بنابراین ما عجله کردیم و پیاده سازی خودمان از این معماری را مطابق با مقاله اصلی (بدون حذفی که در نسخه 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)

اکنون نوشتن یک تابع جهانی برای به دست آوردن هر یک از موارد ارائه شده دشوار نیست کراس مدل های با یا بدون وزنه های آموزش دیده در imagenet:

تابعی برای بارگذاری معماری های آماده

get_model <- function(name = "mobilenet_v2",
                      input_shape = NULL,
                      weights = "imagenet",
                      pooling = "avg",
                      num_classes = NULL,
                      optimizer = keras::optimizer_adam(lr = 0.002),
                      loss = "categorical_crossentropy",
                      metrics = NULL,
                      color = TRUE,
                      compile = FALSE) {
  # Проверка аргументов
  checkmate::assert_string(name)
  checkmate::assert_integerish(input_shape, lower = 1, upper = 256, len = 3)
  checkmate::assert_count(num_classes)
  checkmate::assert_flag(color)
  checkmate::assert_flag(compile)

  # Получаем объект из пакета keras
  model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
  # Проверка наличия объекта в пакете
  if (is.null(model_fun)) {
    stop("Model ", shQuote(name), " not found.", call. = FALSE)
  }

  base_model <- model_fun(
    input_shape = input_shape,
    include_top = FALSE,
    weights = weights,
    pooling = pooling
  )

  # Если изображение не цветное, меняем размерность входа
  if (!color) {
    base_model_conf <- keras::get_config(base_model)
    base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
    base_model <- keras::from_config(base_model_conf)
  }

  predictions <- keras::get_layer(base_model, "global_average_pooling2d_1")$output
  predictions <- keras::layer_dense(predictions, units = num_classes, activation = "softmax")
  model <- keras::keras_model(
    inputs = base_model$input,
    outputs = predictions
  )

  if (compile) {
    keras::compile(
      object = model,
      optimizer = optimizer,
      loss = loss,
      metrics = metrics
    )
  }

  return(model)
}

هنگام استفاده از تصاویر تک کاناله، از وزنه های از پیش آموزش دیده استفاده نمی شود. این می تواند ثابت شود: با استفاده از تابع get_weights() وزن های مدل را به صورت فهرستی از آرایه های R دریافت کنید، بعد عنصر اول این لیست را تغییر دهید (با گرفتن یک کانال رنگ یا میانگین هر سه)، و سپس وزن ها را با تابع در مدل بارگذاری کنید. set_weights(). ما هرگز این قابلیت را اضافه نکردیم، زیرا در این مرحله از قبل مشخص بود که کار با تصاویر رنگی کارآمدتر است.

ما بیشتر آزمایش‌ها را با استفاده از نسخه‌های موبایل نت 1 و 2 و همچنین resnet34 انجام دادیم. معماری های مدرن تری مانند SE-ResNeXt در این رقابت عملکرد خوبی داشتند. متأسفانه پیاده سازی های آماده ای در اختیار نداشتیم و خودمان هم ننوشتیم (اما حتما خواهیم نوشت).

5. پارامترسازی اسکریپت ها

برای راحتی، تمام کدهای شروع آموزش به صورت یک اسکریپت واحد طراحی شده است که با استفاده از پارامتر تعیین شده است سند به شرح زیر است:

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

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

بسته بندی سند اجرای را نشان می دهد http://docopt.org/ برای R. با کمک آن، اسکریپت ها با دستورات ساده مانند Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db یا ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db، اگر فایل train_nn.R قابل اجرا است (این دستور آموزش مدل را شروع می کند resnet50 در تصاویر سه رنگ با اندازه 128x128 پیکسل، پایگاه داده باید در پوشه قرار داشته باشد. /home/andrey/doodle_db). می توانید سرعت یادگیری، نوع بهینه ساز و هر پارامتر قابل تنظیم دیگری را به لیست اضافه کنید. در مراحل آماده سازی نشریه معلوم شد که معماری mobilenet_v2 از نسخه فعلی کراس در استفاده از R نمیتونم به دلیل تغییراتی که در پکیج R لحاظ نشده است، منتظر رفع آن هستیم.

این رویکرد سرعت قابل توجهی در آزمایش‌ها با مدل‌های مختلف در مقایسه با راه‌اندازی سنتی‌تر اسکریپت‌ها در RStudio را امکان‌پذیر کرد (ما به این بسته به عنوان یک جایگزین احتمالی اشاره می‌کنیم. tfruns). اما مزیت اصلی امکان مدیریت آسان راه اندازی اسکریپت ها در Docker یا به سادگی بر روی سرور بدون نصب RStudio برای این کار است.

6. داکرسازی اسکریپت ها

ما از Docker برای اطمینان از قابل حمل بودن محیط برای مدل‌های آموزشی بین اعضای تیم و برای استقرار سریع در فضای ابری استفاده کردیم. می توانید با این ابزار که برای یک برنامه نویس R نسبتاً غیرعادی است آشنا شوید این سری انتشارات یا دوره ویدیویی.

Docker به شما این امکان را می دهد که هم تصاویر خود را از ابتدا ایجاد کنید و هم از تصاویر دیگر به عنوان پایه ای برای ایجاد تصاویر خود استفاده کنید. هنگام تجزیه و تحلیل گزینه‌های موجود، به این نتیجه رسیدیم که نصب درایورهای NVIDIA، CUDA+cuDNN و کتابخانه‌های Python بخش نسبتاً حجیمی از تصویر است و تصمیم گرفتیم تصویر رسمی را به‌عنوان پایه در نظر بگیریم. tensorflow/tensorflow:1.12.0-gpu، بسته های R لازم را در آنجا اضافه کنید.

فایل docker نهایی به شکل زیر بود:

dockerfile

FROM tensorflow/tensorflow:1.12.0-gpu

MAINTAINER Artem Klevtsov <[email protected]>

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

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

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

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

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

WORKDIR /app

VOLUME /db
VOLUME /app

CMD bash

برای راحتی، بسته های مورد استفاده در متغیرها قرار گرفتند. بخش عمده ای از اسکریپت های نوشته شده در داخل کانتینرها در طول مونتاژ کپی می شود. پوسته فرمان را نیز به تغییر دادیم /bin/bash برای سهولت استفاده از مطالب /etc/os-release. با این کار نیازی به مشخص کردن نسخه سیستم عامل در کد نیست.

علاوه بر این، یک اسکریپت bash کوچک نوشته شده است که به شما امکان می دهد یک کانتینر را با دستورات مختلف راه اندازی کنید. به عنوان مثال، اینها می‌توانند اسکریپت‌هایی برای آموزش شبکه‌های عصبی باشند که قبلاً در داخل کانتینر قرار داده شده‌اند، یا یک پوسته فرمان برای اشکال‌زدایی و نظارت بر عملکرد کانتینر:

اسکریپت برای راه اندازی کانتینر

#!/bin/sh

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

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

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

اگر این اسکریپت bash بدون پارامتر اجرا شود، اسکریپت در داخل ظرف فراخوانی می شود train_nn.R با مقادیر پیش فرض؛ اگر اولین آرگومان موقعیتی "bash" باشد، ظرف به صورت تعاملی با پوسته فرمان شروع می شود. در تمام موارد دیگر، مقادیر آرگومان های موقعیتی جایگزین می شوند: CMD="Rscript /app/train_nn.R $@".

شایان ذکر است که دایرکتوری ها با داده های منبع و پایگاه داده و همچنین دایرکتوری برای ذخیره مدل های آموزش دیده در داخل کانتینر از سیستم میزبان نصب شده اند که به شما امکان می دهد بدون دستکاری های غیر ضروری به نتایج اسکریپت ها دسترسی داشته باشید.

7. استفاده از چندین پردازنده گرافیکی در Google Cloud

یکی از ویژگی های این رقابت داده های بسیار پر سر و صدا بود (تصویر عنوان را ببینید که از @Leigh.plt از ODS slack گرفته شده است). دسته‌های بزرگ به مبارزه با این امر کمک می‌کنند، و پس از آزمایش‌هایی که روی رایانه شخصی با ۱ GPU انجام دادیم، تصمیم گرفتیم مدل‌های آموزشی را بر روی چندین GPU در فضای ابری تسلط دهیم. استفاده از GoogleCloud (راهنمای خوبی برای اصول اولیه) به دلیل انتخاب زیاد پیکربندی های موجود، قیمت های مناسب و پاداش 300 دلاری. از روی حرص و طمع، یک نمونه 4xV100 با یک SSD و یک تن رم سفارش دادم و این یک اشتباه بزرگ بود. چنین ماشینی به سرعت پول را می خورد؛ شما می توانید بدون یک خط لوله اثبات شده آزمایش کنید. برای اهداف آموزشی بهتر است K80 بگیرید. اما مقدار زیادی از RAM مفید بود - SSD ابری با عملکرد خود تحت تأثیر قرار نگرفت، بنابراین پایگاه داده به dev/shm.

بیشترین علاقه بخش کدی است که مسئول استفاده از چندین GPU است. ابتدا، مدل بر روی CPU با استفاده از یک مدیر زمینه ایجاد می شود، درست مانند پایتون:

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

سپس مدل کامپایل نشده (این مهم است) در تعداد معینی از GPUهای موجود کپی می شود و تنها پس از آن کامپایل می شود:

model <- keras::multi_gpu_model(model_cpu, gpus = n_gpu)
keras::compile(
  object = model,
  optimizer = keras::optimizer_adam(lr = 0.0004),
  loss = "categorical_crossentropy",
  metrics = c(top_3_categorical_accuracy)
)

تکنیک کلاسیک انجماد همه لایه‌ها به جز آخرین لایه، آموزش آخرین لایه، باز کردن انجماد و آموزش مجدد کل مدل برای چندین GPU قابل اجرا نبود.

آموزش بدون استفاده نظارت شد. تانسوربرد، خود را محدود به ثبت گزارش ها و ذخیره مدل هایی با نام های آموزنده بعد از هر دوره می کنیم:

تماس های تلفنی

# Шаблон имени файла лога
log_file_tmpl <- file.path("logs", sprintf(
  "%s_%d_%dch_%s.csv",
  model_name,
  dim_size,
  channels,
  format(Sys.time(), "%Y%m%d%H%M%OS")
))
# Шаблон имени файла модели
model_file_tmpl <- file.path("models", sprintf(
  "%s_%d_%dch_{epoch:02d}_{val_loss:.2f}.h5",
  model_name,
  dim_size,
  channels
))

callbacks_list <- list(
  keras::callback_csv_logger(
    filename = log_file_tmpl
  ),
  keras::callback_early_stopping(
    monitor = "val_loss",
    min_delta = 1e-4,
    patience = 8,
    verbose = 1,
    mode = "min"
  ),
  keras::callback_reduce_lr_on_plateau(
    monitor = "val_loss",
    factor = 0.5, # уменьшаем lr в 2 раза
    patience = 4,
    verbose = 1,
    min_delta = 1e-4,
    mode = "min"
  ),
  keras::callback_model_checkpoint(
    filepath = model_file_tmpl,
    monitor = "val_loss",
    save_best_only = FALSE,
    save_weights_only = FALSE,
    mode = "min"
  )
)

8. به جای نتیجه گیری

تعدادی از مشکلاتی که ما با آن مواجه شده ایم هنوز برطرف نشده است:

  • в کراس هیچ تابع آماده ای برای جستجوی خودکار نرخ یادگیری بهینه (آنالوگ) وجود ندارد lr_finder در کتابخانه سریع .ai) با کمی تلاش، می توان پیاده سازی های شخص ثالث را به R پورت کرد، به عنوان مثال، این;
  • در نتیجه نکته قبلی، انتخاب سرعت آموزش صحیح در هنگام استفاده از چندین GPU امکان پذیر نبود.
  • کمبود معماری شبکه های عصبی مدرن، به ویژه آنهایی که از قبل در ایمیج نت آموزش دیده اند، وجود ندارد.
  • هیچ یک از سیاست‌های چرخه‌ای و نرخ‌های یادگیری تبعیض‌آمیز (آنیل کسینوس به درخواست ما بود اجرا شد، با تشکر اسکیدان).

چه نکات مفیدی از این مسابقه آموخته شد:

  • در سخت افزار نسبتا کم مصرف، می توانید با حجم مناسب (بسیار برابر رم) داده بدون دردسر کار کنید. کیسه پلاستیکی جدول داده حافظه را به دلیل تغییر در محل جداول ذخیره می کند، که از کپی کردن آنها جلوگیری می کند، و در صورت استفاده صحیح، قابلیت های آن تقریباً همیشه بالاترین سرعت را در بین همه ابزارهای شناخته شده برای زبان های اسکریپت نشان می دهد. ذخیره داده ها در یک پایگاه داده به شما این امکان را می دهد که در بسیاری از موارد اصلاً به نیاز به فشرده سازی کل مجموعه داده در RAM فکر نکنید.
  • توابع آهسته در R را می توان با توابع سریع در C++ با استفاده از بسته جایگزین کرد Rcpp. اگر علاوه بر استفاده RcppThread یا RcppParallel، پیاده سازی های چند رشته ای بین پلتفرمی را دریافت می کنیم، بنابراین نیازی به موازی سازی کد در سطح R نیست.
  • بسته بندی Rcpp می توان بدون دانش جدی از C++ استفاده کرد، حداقل مورد نیاز مشخص شده است اینجا. فایل‌های هدر برای تعدادی از کتابخانه‌های جالب C مانند xtensor موجود در CRAN، یعنی زیرساختی برای اجرای پروژه‌هایی که کدهای C++ آماده با کارایی بالا را در R ادغام می‌کند، در حال شکل‌گیری است. راحتی اضافی برجسته کردن نحو و تحلیلگر کد C++ استاتیک در RStudio است.
  • سند به شما امکان می دهد اسکریپت های مستقل را با پارامترها اجرا کنید. این برای استفاده در یک سرور راه دور راحت است، از جمله. زیر داکر در RStudio انجام چندین ساعت آزمایش با آموزش شبکه های عصبی ناخوشایند است و نصب IDE بر روی خود سرور همیشه قابل توجیه نیست.
  • Docker قابلیت حمل کد و تکرارپذیری نتایج را بین توسعه دهندگان با نسخه های مختلف سیستم عامل و کتابخانه ها و همچنین سهولت اجرا در سرورها تضمین می کند. شما می توانید کل خط لوله آموزشی را تنها با یک دستور راه اندازی کنید.
  • Google Cloud روشی مقرون به صرفه برای آزمایش سخت افزارهای گران قیمت است، اما باید تنظیمات را با دقت انتخاب کنید.
  • اندازه گیری سرعت تک تک قطعات کد بسیار مفید است، به خصوص هنگام ترکیب R و C++، و با بسته. نیمکت - همچنین بسیار آسان است.

به طور کلی این تجربه بسیار مفید بود و ما همچنان به کار برای حل برخی از مسائل مطرح شده ادامه می دهیم.

منبع: www.habr.com

اضافه کردن نظر