التعرف السريع على رسومات الشعار المبتكرة: كيفية تكوين صداقات باستخدام شبكات R وC++ والشبكات العصبية

التعرف السريع على رسومات الشعار المبتكرة: كيفية تكوين صداقات باستخدام شبكات R وC++ والشبكات العصبية

يا هبر!

في الخريف الماضي، استضافت Kaggle مسابقة لتصنيف الصور المرسومة يدويًا، وهي Quick Draw Doodle Recognition، والتي شارك فيها فريق من علماء R، من بين آخرين: أرتيم كليفتسوفا, مدير فيليبا и أندريه أوجورتسوف. لن نقوم بوصف المنافسة بالتفصيل، فقد تم ذلك بالفعل المنشور الأخير.

هذه المرة لم ينجح الأمر مع زراعة الميداليات، ولكن تم اكتساب الكثير من الخبرة القيمة، لذلك أود أن أخبر المجتمع عن عدد من الأشياء الأكثر إثارة للاهتمام والمفيدة في Kagle وفي العمل اليومي. ومن المواضيع التي تمت مناقشتها: الحياة الصعبة بدونها مكتبة برمجية مفتوحة للرؤية الحاسوبية، تحليل JSON (تفحص هذه الأمثلة تكامل كود C++ في البرامج النصية أو الحزم في R باستخدام RCP)، تحديد معلمات البرامج النصية وإرساء الحل النهائي. جميع التعليمات البرمجية من الرسالة في نموذج مناسب للتنفيذ متاحة في مستودعات.

المحتويات:

  1. تحميل البيانات من ملف CSV إلى MonetDB بكفاءة
  2. تحضير دفعات
  3. التكرارات لتفريغ الدُفعات من قاعدة البيانات
  4. اختيار العمارة النموذجية
  5. معلمات البرنامج النصي
  6. إرساء البرامج النصية
  7. استخدام وحدات معالجة الرسومات المتعددة على Google Cloud
  8. بدلا من خاتمة

1. قم بتحميل البيانات من ملف CSV بكفاءة إلى قاعدة بيانات MonetDB

لا يتم توفير البيانات في هذه المسابقة على شكل صور جاهزة، بل على شكل 340 ملف CSV (ملف واحد لكل فئة) تحتوي على JSONs مع إحداثيات النقطة. وبربط هذه النقاط بالخطوط نحصل على صورة نهائية بقياس 256×256 بكسل. يوجد أيضًا لكل سجل علامة تشير إلى ما إذا كان قد تم التعرف على الصورة بشكل صحيح بواسطة المصنف المستخدم في وقت جمع مجموعة البيانات، ورمز مكون من حرفين لبلد إقامة مؤلف الصورة، ومعرف فريد، وطابع زمني واسم فئة يطابق اسم الملف. نسخة مبسطة من البيانات الأصلية تزن 7.4 جيجابايت في الأرشيف وحوالي 20 جيجابايت بعد التفريغ، وتشغل البيانات الكاملة بعد التفريغ 240 جيجابايت. وتأكد المنظمون من أن كلا الإصدارين يستنسخان نفس الرسومات، مما يعني أن النسخة الكاملة كانت زائدة عن الحاجة. على أية حال، فإن تخزين 50 مليون صورة في ملفات رسومية أو على شكل مصفوفات اعتبر على الفور غير مربح، وقررنا دمج جميع ملفات CSV من الأرشيف Train_simplified.zip في قاعدة البيانات مع الإنشاء اللاحق للصور بالحجم المطلوب "سريعًا" لكل دفعة.

تم اختيار نظام مثبت جيدًا باسم نظام إدارة قواعد البيانات (DBMS). MonetDB، أي تطبيق لـ R كحزمة MonetDBLite. تتضمن الحزمة نسخة مضمنة من خادم قاعدة البيانات وتسمح لك بالتقاط الخادم مباشرة من جلسة R والعمل معه هناك. يتم إنشاء قاعدة بيانات والاتصال بها بأمر واحد:

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

سنحتاج إلى إنشاء جدولين: أحدهما لجميع البيانات، والآخر لمعلومات الخدمة حول الملفات التي تم تنزيلها (مفيد في حالة حدوث خطأ ما ويجب استئناف العملية بعد تنزيل عدة ملفات):

إنشاء الجداول

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

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

كانت أسرع طريقة لتحميل البيانات في قاعدة البيانات هي نسخ ملفات 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 حيل. الأول كان تقليل أبعاد النوع الذي يخزن معرف المراقبة. في مجموعة البيانات الأصلية، النوع المطلوب لتخزين المعرف هو 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)

التعرف السريع على رسومات الشعار المبتكرة: كيفية تكوين صداقات باستخدام شبكات R وC++ والشبكات العصبية

2. تحضير الدفعات

تتكون عملية تحضير الدفعة بأكملها من الخطوات التالية:

  1. تحليل عدة JSONs تحتوي على متجهات سلاسل بإحداثيات النقاط.
  2. رسم خطوط ملونة بناءً على إحداثيات النقاط على صورة بالحجم المطلوب (مثلاً 256×256 أو 128×128).
  3. تحويل الصور الناتجة إلى موتر.

كجزء من المنافسة بين حبات بايثون، تم حل المشكلة في المقام الأول باستخدام مكتبة برمجية مفتوحة للرؤية الحاسوبية. أحد أبسط وأكثر نظائرها وضوحًا في لغة 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 مؤقت مخزن في ذاكرة الوصول العشوائي (في Linux، توجد أدلة R المؤقتة في الدليل /tmp، مثبتة في ذاكرة الوصول العشوائي). تتم بعد ذلك قراءة هذا الملف كمصفوفة ثلاثية الأبعاد بأرقام تتراوح من 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))

التعرف السريع على رسومات الشعار المبتكرة: كيفية تكوين صداقات باستخدام شبكات 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

بدا لنا هذا التنفيذ دون المستوى الأمثل، حيث أن تكوين دفعات كبيرة يستغرق وقتًا طويلاً بشكل غير لائق، وقررنا الاستفادة من خبرة زملائنا باستخدام مكتبة قوية مكتبة برمجية مفتوحة للرؤية الحاسوبية. في ذلك الوقت لم تكن هناك حزمة جاهزة لـ R (لا توجد الآن)، لذلك تمت كتابة الحد الأدنى من تنفيذ الوظائف المطلوبة بلغة C++ مع التكامل في كود R باستخدام RCP.

لحل المشكلة تم استخدام الحزم والمكتبات التالية:

  1. مكتبة برمجية مفتوحة للرؤية الحاسوبية للعمل مع الصور ورسم الخطوط. يتم استخدام مكتبات النظام وملفات الرأس المثبتة مسبقًا، بالإضافة إلى الارتباط الديناميكي.

  2. xtensor للعمل مع المصفوفات والموترات متعددة الأبعاد. استخدمنا ملفات الرأس المضمنة في حزمة R التي تحمل الاسم نفسه. تتيح لك المكتبة العمل مع المصفوفات متعددة الأبعاد، سواء في الترتيب الرئيسي للصف أو للعمود.

  3. ندجسون لتحليل JSON. يتم استخدام هذه المكتبة في xtensor تلقائيا إذا كان موجودا في المشروع.

  4. RcppThread لتنظيم معالجة متعددة الخيوط لمتجه من JSON. استخدم ملفات الرأس التي توفرها هذه الحزمة. من أكثر شعبية RcppParallel تحتوي الحزمة، من بين أشياء أخرى، على آلية مقاطعة حلقة مدمجة.

تجدر الإشارة إلى أن xtensor تبين أنها هبة من السماء: بالإضافة إلى كونها تتمتع بوظائف واسعة النطاق وأداء عالي، فقد تبين أن مطوريها كانوا مستجيبين تمامًا وأجابوا على الأسئلة بسرعة وبالتفصيل. بمساعدتهم، كان من الممكن تنفيذ تحويلات مصفوفات OpenCV إلى موترات xtensor، بالإضافة إلى طريقة لدمج موترات الصور ثلاثية الأبعاد في موتر رباعي الأبعاد بالبعد الصحيح (الدفعة نفسها).

مواد لتعلم 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

لتجميع الملفات التي تستخدم ملفات النظام والربط الديناميكي مع المكتبات المثبتة على النظام، استخدمنا آلية البرنامج المساعد المطبقة في الحزمة RCP. للعثور على المسارات والأعلام تلقائيًا، استخدمنا إحدى أدوات Linux الشائعة pkg-config.

تنفيذ البرنامج المساعد Rcpp لاستخدام مكتبة OpenCV

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

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

نتيجة لتشغيل البرنامج المساعد، سيتم استبدال القيم التالية أثناء عملية التجميع:

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

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

يتم توفير رمز التنفيذ لتحليل JSON وإنشاء دفعة لنقلها إلى النموذج أسفل المفسد. أولاً، قم بإضافة دليل مشروع محلي للبحث عن ملفات الرأس (المطلوبة لـ ndjson):

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

تنفيذ JSON لتحويل الموتر في C++

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

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

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

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

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

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

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

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

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

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

يجب وضع هذا الكود في الملف src/cv_xt.cpp وتجميع مع الأمر Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); مطلوب أيضا للعمل nlohmann/json.hpp من مخزن. ينقسم الكود إلى عدة وظائف:

  • to_xt - دالة مقولبة لتحويل مصفوفة الصورة (cv::Mat) إلى الموتر xt::xtensor;

  • parse_json - تقوم الوظيفة بتوزيع سلسلة JSON، واستخراج إحداثيات النقاط، وتعبئتها في متجه؛

  • ocv_draw_lines — من متجه النقاط الناتج، يتم رسم خطوط متعددة الألوان؛

  • process — يجمع بين الوظائف المذكورة أعلاه ويضيف أيضًا القدرة على تغيير حجم الصورة الناتجة؛

  • cpp_process_json_str - المجمع على الوظيفة process، الذي يصدر النتيجة إلى كائن R (مصفوفة متعددة الأبعاد)؛

  • cpp_process_json_vector - المجمع على الوظيفة cpp_process_json_str، والذي يسمح لك بمعالجة ناقل السلسلة في الوضع متعدد الخيوط.

لرسم خطوط متعددة الألوان، تم استخدام نموذج الألوان HSV، يليه التحويل إلى RGB. دعونا نختبر النتيجة:

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

التعرف السريع على رسومات الشعار المبتكرة: كيفية تكوين صداقات باستخدام شبكات 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") 

التعرف السريع على رسومات الشعار المبتكرة: كيفية تكوين صداقات باستخدام شبكات R وC++ والشبكات العصبية

كما ترون، فقد تبين أن زيادة السرعة كبيرة جدًا، ولا يمكن اللحاق بكود C++ عن طريق موازنة كود R.

3. التكرارات لتفريغ الدفعات من قاعدة البيانات

تتمتع R بسمعة تستحقها عن جدارة في معالجة البيانات التي تناسب ذاكرة الوصول العشوائي (RAM)، بينما تتميز 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 يتوافق مع عرض صور بحجم 256 × 256 بكسل، scale = 0.5 — 128 × 128 بكسل)، مؤشر اللون (color = FALSE يحدد العرض بالتدرج الرمادي عند استخدامه color = TRUE يتم رسم كل حد بلون جديد) ومؤشر معالجة مسبقة للشبكات المدربة مسبقًا على imagenet. هذا الأخير ضروري لقياس قيم البكسل من الفاصل الزمني [0، 1] إلى الفاصل الزمني [-1، 1]، والذي تم استخدامه عند تدريب الموردة keras عارضات ازياء.

تحتوي الوظيفة الخارجية على فحص نوع الوسيطة وجدول data.table مع أرقام الأسطر المختلطة بشكل عشوائي من samples_index وأرقام الدُفعات والعداد والحد الأقصى لعدد الدُفعات، بالإضافة إلى تعبير SQL لتفريغ البيانات من قاعدة البيانات. بالإضافة إلى ذلك، قمنا بتعريف تماثل سريع للوظيفة الداخلية keras::to_categorical(). استخدمنا جميع البيانات تقريبًا للتدريب، وتركنا نصف بالمائة للتحقق من الصحة، لذلك كان حجم العصر محدودًا بالمعلمة steps_per_epoch عندما دعا keras::fit_generator()، والحالة if (i > max_i) عملت فقط مع مكرر التحقق من الصحة.

في الوظيفة الداخلية، يتم استرداد فهارس الصفوف للدفعة التالية، ويتم إلغاء تحميل السجلات من قاعدة البيانات مع زيادة عداد الدُفعات، وتحليل JSON (الوظيفة cpp_process_json_vector()، مكتوبة بلغة C++) وإنشاء صفائف تتوافق مع الصور. ثم يتم إنشاء ناقلات واحدة ساخنة مع تسميات فئة، ويتم دمج المصفوفات ذات قيم البكسل والتسميات في قائمة، وهي القيمة المرجعة. لتسريع العمل، استخدمنا إنشاء الفهارس في الجداول 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)

التعرف السريع على رسومات الشعار المبتكرة: كيفية تكوين صداقات باستخدام شبكات R وC++ والشبكات العصبية

إذا كان لديك كمية كافية من ذاكرة الوصول العشوائي (RAM)، فيمكنك تسريع قاعدة البيانات بشكل جدي عن طريق نقلها إلى نفس ذاكرة الوصول العشوائي (32 جيجابايت كافية لمهمتنا). في Linux، يتم تثبيت القسم افتراضيًا /dev/shm، تشغل ما يصل إلى نصف سعة ذاكرة الوصول العشوائي (RAM). يمكنك تسليط الضوء على المزيد عن طريق التحرير /etc/fstabللحصول على سجل مثل tmpfs /dev/shm tmpfs defaults,size=25g 0 0. تأكد من إعادة التشغيل والتحقق من النتيجة عن طريق تشغيل الأمر df -h.

يبدو مكرر بيانات الاختبار أبسط بكثير، نظرًا لأن مجموعة بيانات الاختبار تتناسب تمامًا مع ذاكرة الوصول العشوائي:

مكرر لبيانات الاختبار

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)أي أنه لا يمكن تغيير عدد القنوات. لا يوجد مثل هذا القيد في بايثون، لذلك سارعنا وكتبنا تطبيقنا الخاص لهذه البنية، متبعين المقالة الأصلية (بدون التسرب الموجود في إصدار keras):

بنية موبايل نت V1

library(keras)

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

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

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

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

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

  inputs <- layer_input(shape = input_shape)

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

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

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

    return(model)
}

عيوب هذا النهج واضحة. أريد اختبار الكثير من النماذج، ولكن على العكس من ذلك، لا أريد إعادة كتابة كل بنية يدويًا. لقد حرمنا أيضًا من فرصة استخدام أوزان النماذج التي تم تدريبها مسبقًا على imagenet. وكالعادة، ساعدت دراسة التوثيق. وظيفة get_config() يتيح لك الحصول على وصف للنموذج في شكل مناسب للتحرير (base_model_conf$layers - قائمة R العادية)، والدالة from_config() ينفذ التحويل العكسي إلى كائن نموذج:

base_model_conf <- get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- from_config(base_model_conf)

الآن ليس من الصعب كتابة دالة عالمية للحصول على أي مما تم توفيره keras النماذج ذات الأوزان أو بدونها المدربة على imagenet:

وظيفة لتحميل البنى الجاهزة

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

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

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

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

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

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

  return(model)
}

عند استخدام الصور ذات القناة الواحدة، لا يتم استخدام أي أوزان مدربة مسبقًا. يمكن إصلاح ذلك: باستخدام الوظيفة get_weights() احصل على أوزان النموذج في شكل قائمة من صفائف R، وقم بتغيير بُعد العنصر الأول في هذه القائمة (عن طريق أخذ قناة لون واحدة أو حساب متوسط ​​الثلاثة جميعها)، ثم قم بتحميل الأوزان مرة أخرى إلى النموذج باستخدام الوظيفة set_weights(). لم نضيف هذه الوظيفة مطلقًا، لأنه في هذه المرحلة كان من الواضح بالفعل أن العمل مع الصور الملونة كان أكثر إنتاجية.

لقد أجرينا معظم التجارب باستخدام الإصدارين 1 و2 من mobilenet، بالإضافة إلى resnet34. كان أداء المزيد من البنى الحديثة مثل SE-ResNeXt جيدًا في هذه المنافسة. لسوء الحظ، لم يكن لدينا تطبيقات جاهزة تحت تصرفنا، ولم نكتب تطبيقاتنا الخاصة (لكننا سنكتب بالتأكيد).

5. معلمات البرامج النصية

للراحة، تم تصميم جميع التعليمات البرمجية لبدء التدريب كبرنامج نصي واحد، مع معلمات باستخدام docpt على النحو التالي:

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)

صفقة docpt يمثل التنفيذ 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 بالنسبة للصور ثلاثية الألوان مقاس 128 × 128 بكسل، يجب أن تكون قاعدة البيانات موجودة في المجلد /home/andrey/doodle_db). يمكنك إضافة سرعة التعلم ونوع المحسن وأي معلمات أخرى قابلة للتخصيص إلى القائمة. وفي عملية إعداد المنشور، اتضح أن الهندسة المعمارية mobilenet_v2 من الإصدار الحالي keras في استخدام R ممنوع نظرًا للتغييرات التي لم يتم أخذها في الاعتبار في حزمة R، فإننا ننتظر منهم إصلاحها.

أتاح هذا النهج تسريع التجارب باستخدام نماذج مختلفة بشكل كبير مقارنة بالإطلاق التقليدي للنصوص البرمجية في RStudio (نلاحظ الحزمة كبديل محتمل com.tfruns). لكن الميزة الرئيسية هي القدرة على إدارة تشغيل البرامج النصية بسهولة في Docker أو ببساطة على الخادم، دون تثبيت RStudio لهذا الغرض.

6. إرساء البرامج النصية

استخدمنا Docker لضمان إمكانية نقل البيئة لنماذج التدريب بين أعضاء الفريق وللنشر السريع في السحابة. يمكنك البدء في التعرف على هذه الأداة، وهو أمر غير معتاد نسبيًا بالنسبة لمبرمج R هذا سلسلة من المنشورات أو دورة الفيديو.

يتيح لك Docker إنشاء صورك الخاصة من البداية واستخدام صور أخرى كأساس لإنشاء صورك الخاصة. عند تحليل الخيارات المتاحة، توصلنا إلى استنتاج مفاده أن تثبيت برامج تشغيل NVIDIA وCUDA+cuDNN ومكتبات Python يعد جزءًا ضخمًا إلى حد ما من الصورة، وقررنا أن نأخذ الصورة الرسمية كأساس tensorflow/tensorflow:1.12.0-gpu، وإضافة حزم R اللازمة هناك.

يبدو ملف الإرساء النهائي كما يلي:

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). تساعد الدفعات الكبيرة في مكافحة ذلك، وبعد التجارب على جهاز كمبيوتر مزود بوحدة معالجة رسومات واحدة، قررنا إتقان نماذج التدريب على العديد من وحدات معالجة الرسومات في السحابة. تستخدم جوجل كلاود (دليل جيد للأساسيات) نظرًا للمجموعة الكبيرة من التكوينات المتاحة والأسعار المعقولة ومكافأة قدرها 300 دولار. بدافع الجشع، طلبت نسخة 4xV100 مزودة بمحرك أقراص SSD وكمية كبيرة من ذاكرة الوصول العشوائي، وكان ذلك خطأً كبيرًا. مثل هذه الآلة تستهلك الأموال بسرعة، ويمكنك أن تفلس من خلال إجراء التجارب دون وجود خط أنابيب مثبت. للأغراض التعليمية، فمن الأفضل أن تأخذ K80. لكن الكمية الكبيرة من ذاكرة الوصول العشوائي كانت مفيدة - لم يكن محرك SSD السحابي مثيرًا للإعجاب بأدائه، لذلك تم نقل قاعدة البيانات إليه dev/shm.

الأكثر أهمية هو جزء التعليمات البرمجية المسؤول عن استخدام وحدات معالجة الرسومات المتعددة. أولاً، يتم إنشاء النموذج على وحدة المعالجة المركزية باستخدام مدير السياق، تمامًا كما هو الحال في 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
  )
})

ثم يتم نسخ النموذج غير المترجم (وهذا أمر مهم) إلى عدد معين من وحدات معالجة الرسومات المتاحة، وبعد ذلك فقط يتم تجميعه:

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

لا يمكن تنفيذ التقنية الكلاسيكية المتمثلة في تجميد جميع الطبقات باستثناء الطبقة الأخيرة، وتدريب الطبقة الأخيرة، وإلغاء تجميد النموذج بأكمله وإعادة تدريبه على العديد من وحدات معالجة الرسومات.

تمت مراقبة التدريب دون استخدام. تينسور بورد، ونقتصر على تسجيل السجلات وحفظ النماذج بأسماء إعلامية بعد كل عصر:

عمليات الاسترجاعات

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

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

8. بدلا من الاستنتاج

هناك عدد من المشاكل التي واجهناها ولم يتم التغلب عليها بعد:

  • в keras لا توجد وظيفة جاهزة للبحث تلقائيًا عن معدل التعلم الأمثل (التناظري lr_finder في المكتبة سريع); مع بعض الجهد، من الممكن نقل تطبيقات الطرف الثالث إلى R، على سبيل المثال، هذا;
  • ونتيجة للنقطة السابقة، لم يكن من الممكن تحديد سرعة التدريب الصحيحة عند استخدام العديد من وحدات معالجة الرسومات؛
  • هناك نقص في بنيات الشبكات العصبية الحديثة، وخاصة تلك التي تم تدريبها مسبقًا على Imagenet؛
  • لا توجد سياسة دورة واحدة ومعدلات التعلم التمييزية (كان التلدين جيب التمام بناء على طلبنا مُنفّذ، شكرا لك سكيدان).

ما الأشياء المفيدة التي تعلمتها من هذه المسابقة:

  • على أجهزة منخفضة الطاقة نسبيًا، يمكنك العمل بكميات مناسبة من البيانات (أضعاف حجم ذاكرة الوصول العشوائي) دون أي ألم. حقيبة بلاستيكية جدول البيانات يحفظ الذاكرة بسبب تعديل الجداول في مكانها، مما يتجنب نسخها، وعند استخدامها بشكل صحيح، تظهر قدراتها دائمًا أعلى سرعة بين جميع الأدوات المعروفة لدينا في لغات البرمجة النصية. يتيح لك حفظ البيانات في قاعدة بيانات، في كثير من الحالات، عدم التفكير على الإطلاق في الحاجة إلى ضغط مجموعة البيانات بأكملها في ذاكرة الوصول العشوائي (RAM).
  • يمكن استبدال الوظائف البطيئة في R بوظائف سريعة في C++ باستخدام الحزمة RCP. إذا بالإضافة إلى الاستخدام RcppThread أو RcppParallel، نحصل على تطبيقات متعددة الخيوط عبر الأنظمة الأساسية، لذلك ليست هناك حاجة لموازاة الكود على مستوى R.
  • طَرد RCP يمكن استخدامه دون معرفة جادة بـ C++، وتم تحديد الحد الأدنى المطلوب هنا. ملفات الرأس لعدد من مكتبات C الرائعة مثل xtensor متاح على CRAN، أي أنه يتم تشكيل بنية تحتية لتنفيذ المشاريع التي تدمج كود C++ عالي الأداء الجاهز في R. الراحة الإضافية هي تسليط الضوء على بناء الجملة ومحلل كود C++ الثابت في RStudio.
  • docpt يسمح لك بتشغيل البرامج النصية القائمة بذاتها مع المعلمات. وهذا مناسب للاستخدام على خادم بعيد، بما في ذلك. تحت عامل الميناء. في RStudio، من غير الملائم إجراء تجارب لساعات طويلة مع تدريب الشبكات العصبية، كما أن تثبيت IDE على الخادم نفسه ليس له ما يبرره دائمًا.
  • يضمن Docker إمكانية نقل التعليمات البرمجية وإمكانية تكرار النتائج بين المطورين الذين لديهم إصدارات مختلفة من نظام التشغيل والمكتبات، فضلاً عن سهولة التنفيذ على الخوادم. يمكنك تشغيل مسار التدريب بأكمله بأمر واحد فقط.
  • تُعد Google Cloud وسيلة مناسبة للميزانية لتجربة الأجهزة باهظة الثمن، ولكن عليك اختيار التكوينات بعناية.
  • يعد قياس سرعة أجزاء التعليمات البرمجية الفردية مفيدًا جدًا، خاصة عند الجمع بين R وC++ ومع الحزمة مقعد - سهل جدًا أيضًا.

بشكل عام، كانت هذه التجربة مجزية للغاية ونحن نواصل العمل على حل بعض المشكلات المثارة.

المصدر: www.habr.com

إضافة تعليق