Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur

Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur

Ey Habr!

Geçen sonbaharda Kaggle, elle çizilmiş resimleri sınıflandırmak için Quick Draw Doodle Recognition adlı bir yarışmaya ev sahipliği yaptı; bu yarışmaya diğerlerinin yanı sıra R-bilim adamlarından oluşan bir ekip de katıldı: Artem Klevtsova, Philippa Yönetici и Andrey Ogurtsov. Yarışmayı ayrıntılı olarak anlatmayacağız; bu zaten yapıldı. son yayın.

Bu sefer madalya yetiştirmede işler yürümedi, ancak çok sayıda değerli deneyim kazanıldı, bu yüzden topluluğa Kagle ve günlük işlerdeki en ilginç ve faydalı şeylerden birkaçını anlatmak istiyorum. Tartışılan konular arasında: onsuz zor yaşam OpenCV, JSON ayrıştırma (bu örnekler, C++ kodunun R'deki komut dosyalarına veya paketlere entegrasyonunu inceler. Rcpp), komut dosyalarının parametrelendirilmesi ve son çözümün dockerizasyonu. Mesajdaki tüm kodlar yürütmeye uygun bir biçimde mevcuttur. depolar.

İçindekiler:

  1. Verileri CSV'den MonetDB'ye verimli bir şekilde yükleyin
  2. Grupların hazırlanması
  3. Toplu işlerin veritabanından kaldırılması için yineleyiciler
  4. Model Mimarisinin Seçilmesi
  5. Komut dosyası parametrelendirmesi
  6. Komut dosyalarının dockerizasyonu
  7. Google Cloud'da birden fazla GPU kullanma
  8. Bunun yerine bir sonuca

1. Verileri CSV'den MonetDB veritabanına verimli bir şekilde yükleyin

Bu yarışmadaki veriler hazır görseller şeklinde değil, nokta koordinatlı JSON'ları içeren 340 CSV dosyası (her sınıf için bir dosya) şeklinde sağlanmaktadır. Bu noktaları çizgilerle birleştirerek 256x256 piksel boyutunda son bir görüntü elde ediyoruz. Ayrıca her kayıt için, veri kümesinin toplandığı sırada kullanılan sınıflandırıcı tarafından resmin doğru şekilde tanınıp tanınmadığını belirten bir etiket, resmin yazarının ikamet ettiği ülkenin iki harfli kodu, benzersiz bir tanımlayıcı, bir zaman damgası bulunur. ve dosya adıyla eşleşen bir sınıf adı. Orijinal verilerin basitleştirilmiş bir versiyonu arşivde 7.4 GB ağırlığında ve paket açıldıktan sonra yaklaşık 20 GB ağırlığında olup, paket açıldıktan sonra tam veri 240 GB yer kaplar. Organizatörler her iki versiyonun da aynı çizimleri üretmesini sağladılar, bu da tam versiyonun gereksiz olduğu anlamına geliyordu. Her durumda, 50 milyon görüntüyü grafik dosyalarında veya diziler biçiminde depolamanın hemen kârsız olduğu düşünüldü ve arşivdeki tüm CSV dosyalarını birleştirmeye karar verdik. train_simplified.zip Her parti için gerekli boyuttaki görüntülerin sonraki nesilleri ile birlikte veritabanına "anında" gönderilir.

DBMS olarak kanıtlanmış bir sistem seçildi MonetDByani R'nin paket olarak uygulanması MonetDBLite. Paket, veritabanı sunucusunun yerleşik bir sürümünü içerir ve sunucuyu doğrudan bir R oturumundan alıp orada çalışmanıza olanak tanır. Bir veritabanı oluşturmak ve ona bağlanmak tek bir komutla gerçekleştirilir:

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

İki tablo oluşturmamız gerekecek: biri tüm veriler için, diğeri indirilen dosyalar hakkındaki hizmet bilgileri için (bir şeyler ters giderse ve birkaç dosya indirildikten sonra işlemin devam ettirilmesi gerekiyorsa kullanışlıdır):

Tablo oluşturma

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

Veritabanına veri yüklemenin en hızlı yolu CSV dosyalarını SQL - komutunu kullanarak doğrudan kopyalamaktı COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTNerede tablename - tablo adı ve path - dosyanın yolu. Arşivle çalışırken yerleşik uygulamanın olduğu keşfedildi unzip R'de arşivdeki bazı dosyalarla düzgün çalışmıyor, bu yüzden sistemi kullandık unzip (parametreyi kullanarak getOption("unzip")).

Veritabanına yazma fonksiyonu

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

Tabloyu veritabanına yazmadan önce dönüştürmeniz gerekiyorsa argümanı iletmeniz yeterlidir. preprocess Verileri dönüştürecek işlev.

Veritabanına sırayla veri yüklemek için kod:

Veri tabanına veri yazma

# Список файлов для записи
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

Veri yükleme süresi, kullanılan sürücünün hız özelliklerine bağlı olarak değişebilir. Bizim durumumuzda, bir SSD içinde veya bir flash sürücüden (kaynak dosya) bir SSD'ye (DB) okuma ve yazma işlemi 10 dakikadan az sürer.

Tamsayı sınıfı etiketine ve dizin sütununa sahip bir sütun oluşturmak birkaç saniye daha sürer (ORDERED INDEX) gruplar oluşturulurken gözlemlerin örnekleneceği satır numaralarıyla birlikte:

Ek Sütun ve Dizin Oluşturma

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

Anında toplu iş oluşturma sorununu çözmek için, tablodan rastgele satırları çıkarma konusunda maksimum hıza ulaşmamız gerekiyordu. doodles. Bunun için 3 numara kullandık. Bunlardan ilki, gözlem kimliğini saklayan türün boyutluluğunu azaltmaktı. Orijinal veri setinde kimliği saklamak için gereken tür bigint, ancak gözlemlerin sayısı sıra numarasına eşit tanımlayıcılarının türe sığmasını mümkün kılar int. Bu durumda arama çok daha hızlıdır. İkinci numara kullanmaktı ORDERED INDEX — bu karara, mevcut tüm yöntemleri inceledikten sonra ampirik olarak ulaştık. seçenekleri. Üçüncüsü parametreli sorgular kullanmaktı. Yöntemin özü, komutu bir kez yürütmektir PREPARE aynı türden bir grup sorgu oluştururken hazırlanmış bir ifadenin daha sonra kullanılmasıyla, ancak aslında basit bir ifadeyle karşılaştırıldığında bir avantaj vardır SELECT istatistiksel hata aralığında olduğu ortaya çıktı.

Veri yükleme işlemi 450 MB'tan fazla RAM tüketmez. Yani açıklanan yaklaşım, onlarca gigabayt ağırlığındaki veri kümelerini, bazı tek kartlı cihazlar da dahil olmak üzere hemen hemen her bütçeye uygun donanımda taşımanıza olanak tanır ki bu oldukça harika.

Geriye kalan tek şey, (rastgele) veri alma hızını ölçmek ve farklı boyutlardaki partileri numune alırken ölçeklendirmeyi değerlendirmektir:

Veritabanı karşılaştırması

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)

Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur

2. Grupların hazırlanması

Parti hazırlama sürecinin tamamı aşağıdaki adımlardan oluşur:

  1. Nokta koordinatlarına sahip dize vektörleri içeren birkaç JSON'un ayrıştırılması.
  2. Gerekli boyuttaki bir görüntü üzerindeki noktaların koordinatlarına göre renkli çizgiler çizmek (örneğin, 256×256 veya 128×128).
  3. Ortaya çıkan görüntülerin tensöre dönüştürülmesi.

Python çekirdekleri arasındaki rekabetin bir parçası olarak sorun öncelikle Python çekirdekleri kullanılarak çözüldü. OpenCV. R'deki en basit ve en belirgin analoglardan biri şöyle görünecektir:

R'de JSON'u Tensör Dönüşümüne Uygulamak

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

Çizim, standart R araçları kullanılarak gerçekleştirilir ve RAM'de depolanan geçici bir PNG'ye kaydedilir (Linux'ta, geçici R dizinleri dizinde bulunur) /tmp, RAM'e monte edilmiştir). Bu dosya daha sonra 0 ile 1 arasında değişen sayıların yer aldığı üç boyutlu bir dizi olarak okunur. Bu önemlidir çünkü daha geleneksel bir BMP, onaltılık renk kodlarına sahip ham bir diziye okunacaktır.

Sonucu test edelim:

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

Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur

Partinin kendisi aşağıdaki gibi oluşturulacaktır:

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

Büyük partilerin oluşturulması çok uzun zaman aldığından bu uygulama bizim için yetersiz göründü ve güçlü bir kütüphane kullanarak meslektaşlarımızın deneyimlerinden yararlanmaya karar verdik. OpenCV. O zamanlar R için hazır bir paket yoktu (şu anda yok), bu nedenle gerekli işlevselliğin minimal bir uygulaması, R koduna entegrasyonla C++ ile yazıldı. Rcpp.

Sorunu çözmek için aşağıdaki paketler ve kütüphaneler kullanıldı:

  1. OpenCV görüntülerle çalışmak ve çizgiler çizmek için. Önceden yüklenmiş sistem kitaplıkları ve başlık dosyalarının yanı sıra dinamik bağlantı da kullanılır.

  2. uzatıcı çok boyutlu diziler ve tensörlerle çalışmak için. Aynı isimli R paketinde yer alan başlık dosyalarını kullandık. Kitaplık, çok boyutlu dizilerle hem ana satır hem de sütun ana sırasına göre çalışmanıza olanak tanır.

  3. ndjson JSON'u ayrıştırmak için. Bu kütüphane şu amaçlarla kullanılır: uzatıcı projede mevcutsa otomatik olarak.

  4. Rcpp Konusu JSON'dan bir vektörün çok iş parçacıklı işlenmesini düzenlemek için. Bu paket tarafından sağlanan başlık dosyaları kullanıldı. Daha popüler olanlardan RcppParalel Paket, diğer şeylerin yanı sıra yerleşik bir döngü kesme mekanizmasına sahiptir.

Bu unutulmamalıdır ki uzatıcı bir nimet olduğu ortaya çıktı: kapsamlı işlevsellik ve yüksek performansa sahip olmasının yanı sıra, geliştiricilerinin oldukça duyarlı olduğu ve soruları hızlı ve ayrıntılı bir şekilde yanıtladığı ortaya çıktı. Onların yardımıyla, OpenCV matrislerinin xtensör tensörlerine dönüşümlerinin yanı sıra 3 boyutlu görüntü tensörlerini doğru boyuttaki 4 boyutlu bir tensörde (topluluğun kendisi) birleştirmenin bir yolunu uygulamak mümkün oldu.

Rcpp, xtensor ve RcppThread'i öğrenmek için materyaller

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

Sistem dosyalarını ve sistemde kurulu kütüphanelerle dinamik bağlantıyı kullanan dosyaları derlemek için pakette uygulanan eklenti mekanizmasını kullandık. Rcpp. Yolları ve bayrakları otomatik olarak bulmak için popüler bir Linux yardımcı programını kullandık pkg-config.

OpenCV kütüphanesini kullanmak için Rcpp eklentisinin uygulanması

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

Eklentinin çalışması sonucunda derleme işlemi sırasında aşağıdaki değerler değiştirilecektir:

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'u ayrıştırmak ve modele aktarım için bir parti oluşturmak için uygulama kodu spoiler altında verilmiştir. Öncelikle başlık dosyalarını aramak için yerel bir proje dizini ekleyin (ndjson için gereklidir):

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

JSON'un C++'da tensöre dönüştürülmesinin uygulanması

// [[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;
}

Bu kod dosyaya yerleştirilmelidir src/cv_xt.cpp ve komutla derleyin Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); iş için de gerekli nlohmann/json.hpp arasında depo. Kod çeşitli işlevlere ayrılmıştır:

  • to_xt — bir görüntü matrisini dönüştürmek için şablonlanmış bir işlev (cv::Mat) bir tensöre xt::xtensor;

  • parse_json — işlev bir JSON dizesini ayrıştırır, noktaların koordinatlarını çıkarır ve bunları bir vektöre paketler;

  • ocv_draw_lines - ortaya çıkan nokta vektöründen çok renkli çizgiler çizer;

  • process — yukarıdaki işlevleri birleştirir ve aynı zamanda ortaya çıkan görüntüyü ölçeklendirme yeteneğini de ekler;

  • cpp_process_json_str - fonksiyonun üzerine sarıcı processsonucu bir R nesnesine (çok boyutlu dizi) aktaran;

  • cpp_process_json_vector - fonksiyonun üzerine sarıcı cpp_process_json_str, bir dize vektörünü çok iş parçacıklı modda işlemenize olanak tanır.

Çok renkli çizgiler çizmek için HSV renk modeli kullanıldı ve ardından RGB'ye dönüştürüldü. Sonucu test edelim:

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

Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur
R ve C++'daki uygulamaların hızlarının karşılaştırılması

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

Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur

Görüldüğü gibi hız artışı çok ciddi boyutlarda oldu ve R kodunu paralelleştirerek C++ kodunu yakalamak mümkün değil.

3. Toplu işlerin veritabanından kaldırılması için yineleyiciler

R, RAM'e sığan verileri işleme konusunda haklı bir üne sahiptir; Python ise daha çok yinelemeli veri işlemeyle karakterize edilir ve çekirdek dışı hesaplamaları (harici bellek kullanan hesaplamalar) kolayca ve doğal bir şekilde uygulamanıza olanak tanır. Tanımlanan problem bağlamında bizim için klasik ve ilgili bir örnek, gözlemlerin küçük bir kısmı veya mini parti kullanılarak her adımda gradyanın yaklaşık olarak tahmin edildiği gradyan iniş yöntemiyle eğitilen derin sinir ağlarıdır.

Python'da yazılan derin öğrenme çerçeveleri, verilere dayalı yineleyiciler uygulayan özel sınıflara sahiptir: tablolar, klasörlerdeki resimler, ikili formatlar vb. Hazır seçenekleri kullanabilir veya belirli görevler için kendinizinkini yazabilirsiniz. R'de Python kütüphanesinin tüm özelliklerinden yararlanabiliriz keras Aynı adı taşıyan paketi kullanan çeşitli arka uçlarıyla, bu da paketin üstünde çalışır ağsı. İkincisi ayrı bir uzun makaleyi hak ediyor; yalnızca Python kodunu R'den çalıştırmanıza izin vermekle kalmaz, aynı zamanda gerekli tüm tür dönüşümlerini otomatik olarak gerçekleştirerek nesneleri R ve Python oturumları arasında aktarmanıza da olanak tanır.

MonetDBLite kullanarak tüm verileri RAM'de saklama zorunluluğundan kurtulduk, Python'daki tüm “sinir ağı” işleri orijinal kodla yapılacak, hazır bir şey olmadığı için veriler üzerine sadece bir yineleyici yazmamız gerekiyor. R veya Python'da böyle bir durum için. Aslında bunun için yalnızca iki gereksinim vardır: toplu işlemleri sonsuz bir döngüde döndürmeli ve yinelemeler arasında durumunu kaydetmelidir (ikincisi R'de en basit şekilde kapatmalar kullanılarak uygulanır). Daha önce, R dizilerinin yineleyici içinde açıkça numpy dizilere dönüştürülmesi gerekiyordu, ancak paketin mevcut sürümü keras kendisi yapıyor.

Eğitim ve doğrulama verileri için yineleyicinin aşağıdaki gibi olduğu ortaya çıktı:

Eğitim ve doğrulama verileri için yineleyici

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

İşlev, veri tabanına bağlantısı olan bir değişkeni, kullanılan satır sayısını, sınıf sayısını, parti boyutunu, ölçeği () girdi olarak alır.scale = 1 256x256 piksellik görsellerin oluşturulmasına karşılık gelir, scale = 0.5 — 128x128 piksel), renk göstergesi (color = FALSE kullanıldığında gri tonlamalı görüntü oluşturmayı belirtir color = TRUE her vuruş yeni bir renkte çizilir) ve imagenet'te önceden eğitilmiş ağlar için bir ön işleme göstergesidir. İkincisi, sağlanan piksellerin eğitimi sırasında kullanılan [0, 1] aralığından [-1, 1] aralığına kadar piksel değerlerini ölçeklendirmek için gereklidir. keras modeller.

Harici fonksiyon argüman tipi kontrolünü, bir tabloyu içerir. data.table rastgele karışık satır numaralarıyla samples_index ve toplu iş numaraları, sayaç ve maksimum toplu iş sayısının yanı sıra veritabanından veri kaldırmak için bir SQL ifadesi. Ek olarak, içindeki fonksiyonun hızlı bir analogunu tanımladık. keras::to_categorical(). Neredeyse tüm verileri eğitim için kullandık, yüzde yarımı doğrulama için bıraktık, bu nedenle çağ boyutu parametreyle sınırlıydı steps_per_epoch çağrıldığında keras::fit_generator()ve durum if (i > max_i) yalnızca doğrulama yineleyici için çalıştı.

Dahili fonksiyonda, bir sonraki toplu iş için satır dizinleri alınır, toplu iş sayacı artırılarak kayıtlar veritabanından kaldırılır, JSON ayrıştırma (işlev) cpp_process_json_vector(), C++ ile yazılmış) ve resimlere karşılık gelen diziler oluşturma. Daha sonra sınıf etiketlerine sahip tek sıcak vektörler oluşturulur, piksel değerlerine sahip diziler ve etiketler, dönüş değeri olan bir liste halinde birleştirilir. Çalışmayı hızlandırmak için tablolarda indeks oluşturmayı kullandık data.table ve bağlantı aracılığıyla değişiklik - bu "cips" paketi olmadan veri tablosu R'de önemli miktarda veriyle etkili bir şekilde çalışmayı hayal etmek oldukça zordur.

Core i5 dizüstü bilgisayardaki hız ölçümlerinin sonuçları aşağıdaki gibidir:

Yineleyici karşılaştırması

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)

Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur

Yeterli miktarda RAM'iniz varsa, veritabanını aynı RAM'e aktararak ciddi şekilde hızlandırabilirsiniz (32 GB görevimiz için yeterlidir). Linux'ta bölüm varsayılan olarak takılıdır /dev/shm, RAM kapasitesinin yarısına kadar yer kaplar. Düzenleyerek daha fazlasını vurgulayabilirsiniz /etc/fstabgibi bir kayıt almak için tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Komutu çalıştırarak yeniden başlattığınızdan ve sonucu kontrol ettiğinizden emin olun. df -h.

Test veri kümesi tamamen RAM'e sığdığından, test verileri yineleyicisi çok daha basit görünüyor:

Test verileri için yineleyici

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. Model mimarisinin seçimi

Kullanılan ilk mimari mobil ağ v1özellikleri tartışılan Bu İleti. Standart olarak dahildir keras ve buna göre R için aynı adı taşıyan pakette mevcuttur. Ancak bunu tek kanallı görüntülerle kullanmaya çalışırken garip bir şey ortaya çıktı: giriş tensörünün her zaman boyutuna sahip olması gerekir (batch, height, width, 3)yani kanal sayısı değiştirilemez. Python'da böyle bir sınırlama yoktur, bu nedenle aceleyle bu mimariye ilişkin kendi uygulamamızı, orijinal makaleyi takip ederek (keras sürümündeki bırakma olmadan) yazdık:

Mobilenet v1 mimarisi

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

Bu yaklaşımın dezavantajları açıktır. Birçok modeli test etmek istiyorum ama tam tersine her mimariyi manuel olarak yeniden yazmak istemiyorum. Ayrıca imagenet üzerinde önceden eğitilmiş modellerin ağırlıklarını kullanma fırsatından da mahrum kaldık. Her zamanki gibi belgeleri incelemek yardımcı oldu. İşlev get_config() modelin açıklamasını düzenlemeye uygun bir biçimde almanızı sağlar (base_model_conf$layers - normal bir R listesi) ve işlev from_config() bir model nesnesine ters dönüşümü gerçekleştirir:

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)

Artık sağlananlardan herhangi birini elde etmek için evrensel bir fonksiyon yazmak zor değil keras imagenet'te eğitilmiş ağırlıkları olan veya olmayan modeller:

Hazır mimarileri yükleme işlevi

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

Tek kanallı görüntüler kullanıldığında önceden eğitilmiş ağırlıklar kullanılmaz. Bu düzeltilebilir: işlevin kullanılması get_weights() Model ağırlıklarını R dizilerinin bir listesi biçiminde alın, bu listenin ilk öğesinin boyutunu değiştirin (bir renk kanalı alarak veya üçünün ortalamasını alarak) ve ardından ağırlıkları, işlevli modele geri yükleyin. set_weights(). Bu işlevi hiçbir zaman eklemedik çünkü bu aşamada renkli resimlerle çalışmanın daha verimli olduğu zaten açıktı.

Deneylerin çoğunu mobilenet sürüm 1 ve 2'nin yanı sıra resnet34'ü kullanarak gerçekleştirdik. SE-ResNeXt gibi daha modern mimariler bu yarışmada iyi performans gösterdi. Ne yazık ki elimizde hazır uygulamalar yoktu ve kendimiz yazmadık (ama mutlaka yazacağız).

5. Komut dosyalarının parametrelendirilmesi

Kolaylık sağlamak amacıyla, eğitime başlamaya yönelik tüm kod, tek bir komut dosyası olarak tasarlandı ve şu şekilde parametrelendirildi: belge следующим обрахом:

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

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

Paket belge uygulamayı temsil eder http://docopt.org/ R için. Onun yardımıyla komut dosyaları aşağıdaki gibi basit komutlarla başlatılır: Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db veya ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, eğer dosya train_nn.R çalıştırılabilir (bu komut modeli eğitmeye başlayacaktır) resnet50 128x128 piksel boyutundaki üç renkli görüntülerde veritabanının klasörde bulunması gerekir /home/andrey/doodle_db). Listeye öğrenme hızını, optimize edici türünü ve diğer özelleştirilebilir parametreleri ekleyebilirsiniz. Yayını hazırlama sürecinde mimarinin ortaya çıktığı ortaya çıktı. mobilenet_v2 mevcut versiyondan keras R kullanımında yapmamalısın R paketinde dikkate alınmayan değişiklikler nedeniyle düzeltmelerini bekliyoruz.

Bu yaklaşım, RStudio'daki daha geleneksel komut dosyalarının başlatılmasıyla karşılaştırıldığında farklı modellerle yapılan deneyleri önemli ölçüde hızlandırmayı mümkün kıldı (paketin olası bir alternatif olduğunu belirtiyoruz) tfrunlar). Ancak asıl avantaj, bunun için RStudio'yu yüklemeden Docker'da veya yalnızca sunucuda komut dosyalarının başlatılmasını kolayca yönetebilme yeteneğidir.

6. Komut dosyalarının dockerizasyonu

Ekip üyeleri arasındaki eğitim modelleri ve bulutta hızlı dağıtım için ortamın taşınabilirliğini sağlamak amacıyla Docker'ı kullandık. Bir R programcısı için nispeten alışılmadık olan bu araçla tanışmaya şu adresten başlayabilirsiniz: bu dizi yayın veya video kursu.

Docker, hem kendi görsellerinizi sıfırdan oluşturmanıza hem de kendi görsellerinizi oluşturmak için diğer görselleri temel olarak kullanmanıza olanak tanır. Mevcut seçenekleri analiz ederken NVIDIA, CUDA+cuDNN sürücüleri ve Python kitaplıklarının kurulumunun görüntünün oldukça büyük bir parçası olduğu sonucuna vardık ve resmi görüntüyü temel almaya karar verdik tensorflow/tensorflow:1.12.0-gpu, gerekli R paketlerini oraya ekliyorum.

Son liman işçisi dosyası şöyle görünüyordu:

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

Kolaylık sağlamak için kullanılan paketler değişkenlere yerleştirildi; Yazılı komut dosyalarının büyük bir kısmı montaj sırasında kapların içine kopyalanır. Ayrıca komut kabuğunu da şu şekilde değiştirdik: /bin/bash içeriğin kullanım kolaylığı için /etc/os-release. Bu, kodda işletim sistemi sürümünü belirtme ihtiyacını ortadan kaldırdı.

Ek olarak, çeşitli komutlarla bir kapsayıcıyı başlatmanıza olanak tanıyan küçük bir bash betiği yazılmıştır. Örneğin bunlar, daha önce konteynerin içine yerleştirilmiş sinir ağlarının eğitimi için komut dosyaları veya konteynerin çalışmasının hatalarını ayıklamak ve izlemek için bir komut kabuğu olabilir:

Kapsayıcıyı başlatmak için komut dosyası

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

Bu bash betiği parametresiz çalıştırılırsa betik konteynerin içinde çağrılacaktır. train_nn.R varsayılan değerlerle; eğer ilk konumsal argüman "bash" ise, kap bir komut kabuğuyla etkileşimli olarak başlayacaktır. Diğer tüm durumlarda, konumsal argümanların değerleri değiştirilir: CMD="Rscript /app/train_nn.R $@".

Kaynak verileri ve veritabanını içeren dizinlerin yanı sıra eğitilmiş modelleri kaydetme dizininin, gereksiz manipülasyonlar olmadan komut dosyalarının sonuçlarına erişmenizi sağlayan ana sistemden konteynerin içine monte edildiğini belirtmekte fayda var.

7. Google Cloud'da birden fazla GPU kullanma

Yarışmanın özelliklerinden biri de çok gürültülü verilerdi (başlık resmine bakın, ODS Slack'ten @Leigh.plt'den ödünç alınmıştır). Büyük gruplar bununla mücadeleye yardımcı oluyor ve 1 GPU'lu bir bilgisayar üzerinde yaptığımız denemelerden sonra, buluttaki çeşitli GPU'lar üzerindeki eğitim modellerinde uzmanlaşmaya karar verdik. Kullanılan GoogleCloud (temel bilgiler için iyi bir rehber) mevcut konfigürasyonların geniş seçimi, makul fiyatlar ve 300 $ bonus nedeniyle. Açgözlülükten SSD ve bir ton RAM'e sahip bir 4xV100 örneği sipariş ettim ve bu büyük bir hataydı. Böyle bir makine parayı hızla tüketir; kanıtlanmış bir üretim hattı olmadan denemeler yaparak iflas edebilirsiniz. Eğitim amaçlı olarak K80'i almak daha iyidir. Ancak büyük miktarda RAM kullanışlı oldu - bulut SSD performansından etkilenmedi, bu nedenle veritabanı dev/shm.

En çok ilgi çeken, birden fazla GPU'nun kullanılmasından sorumlu olan kod parçasıdır. İlk olarak model, Python'da olduğu gibi bir içerik yöneticisi kullanılarak CPU üzerinde oluşturulur:

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

Daha sonra derlenmemiş (bu önemlidir) model belirli sayıda mevcut GPU'ya kopyalanır ve ancak bundan sonra derlenir:

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

Sonuncusu dışındaki tüm katmanları dondurma, son katmanı eğitme, dondurmayı çözme ve tüm modeli birkaç GPU için yeniden eğitme şeklindeki klasik teknik uygulanamadı.

Eğitim kullanılmadan izlendi. gergi tahtası, kendimizi günlükleri kaydetmek ve her dönemden sonra bilgilendirici adlarla modelleri kaydetmekle sınırlandırıyoruz:

Geri aramalar

# Шаблон имени файла лога
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. Sonuç yerine

Karşılaştığımız bazı sorunların henüz üstesinden gelinmedi:

  • в keras Optimum öğrenme oranını (analog) otomatik olarak aramak için hazır bir işlev yoktur. lr_finder kütüphanede hızlı.ai); Biraz çaba sarf ederek üçüncü taraf uygulamalarını R'ye taşımak mümkündür, örneğin: bu;
  • önceki noktanın bir sonucu olarak, birden fazla GPU kullanırken doğru eğitim hızını seçmek mümkün değildi;
  • modern sinir ağı mimarilerinin, özellikle de imagenet üzerinde önceden eğitilmiş olanların eksikliği;
  • tek döngü politikası yok ve ayrımcı öğrenme oranları (kosinüs tavlaması bizim isteğimiz üzerine yapıldı) uygulananteşekkür ederim skeydan).

Bu yarışmadan ne gibi yararlı şeyler öğrenildi:

  • Nispeten düşük güçlü donanımlarda, makul miktarda (RAM'in birçok katı) veri hacmiyle sorunsuz bir şekilde çalışabilirsiniz. Naylon poşet veri tablosu tabloların yerinde değiştirilmesi nedeniyle hafızadan tasarruf sağlar, bu da kopyalanmayı önler ve doğru kullanıldığında yetenekleri, komut dosyası dilleri için bildiğimiz tüm araçlar arasında neredeyse her zaman en yüksek hızı gösterir. Verileri bir veritabanına kaydetmek, çoğu durumda tüm veri kümesini RAM'e sıkıştırma ihtiyacını hiç düşünmemenizi sağlar.
  • R'deki yavaş işlevler, paket kullanılarak C++'daki hızlı işlevlerle değiştirilebilir Rcpp. Eğer kullanıma ek olarak Rcpp Konusu veya RcppParalel, platformlar arası çok iş parçacıklı uygulamalar elde ederiz, dolayısıyla kodu R düzeyinde paralelleştirmeye gerek yoktur.
  • paket Rcpp ciddi C++ bilgisi olmadan kullanılabilir, gerekli minimum değerler özetlenmiştir burada. Gibi bir dizi harika C kütüphanesi için başlık dosyaları uzatıcı CRAN üzerinde mevcut yani hazır yüksek performanslı C++ kodunu R'ye entegre eden projelerin hayata geçirilmesi için altyapı oluşturuluyor. Ek kolaylık, sözdizimi vurgulama ve RStudio'daki statik C++ kod analizörüdür.
  • belge bağımsız komut dosyalarını parametrelerle çalıştırmanıza olanak tanır. Bu, uzak bir sunucuda kullanım için uygundur. liman işçisi altında. RStudio'da, sinir ağlarının eğitimi ile saatlerce deney yapmak sakıncalıdır ve IDE'yi sunucunun kendisine kurmak her zaman haklı değildir.
  • Docker, farklı işletim sistemi ve kitaplık sürümlerine sahip geliştiriciler arasında kod taşınabilirliği ve sonuçların tekrarlanabilirliğinin yanı sıra sunucularda yürütme kolaylığı sağlar. Tüm eğitim hattını tek bir komutla başlatabilirsiniz.
  • Google Cloud, pahalı donanımlar üzerinde denemeler yapmanın bütçeye uygun bir yoludur ancak yapılandırmaları dikkatli seçmeniz gerekir.
  • Bireysel kod parçalarının hızını ölçmek, özellikle R ve C++ paketlerini birleştirirken çok faydalıdır. bank - ayrıca çok kolay.

Genel olarak bu deneyim çok faydalıydı ve ortaya çıkan bazı sorunları çözmek için çalışmaya devam ediyoruz.

Kaynak: habr.com

Yorum ekle