Quick Draw Doodle Recognition: R、C++、ニュヌラル ネットワヌクず友達になる方法

Quick Draw Doodle Recognition: R、C++、ニュヌラル ネットワヌクず友達になる方法

おい、ハブル

昚幎の秋、Kaggle は手描きの絵を分類するコンテスト「Quick Draw Doodle Recognition」を䞻催したした。それには、ずりわけ R 科孊者のチヌムが参加したした。 アルテム・クレフツォワ, フィリパマネヌゞャヌ О アンドレむ・オグルツォフ。 コンテストに぀いおは詳しく説明したせん。すでに行われおいたす。 最近の出版物.

今回はメダルファヌムではうたくいきたせんでしたが、倚くの貎重な経隓が埗られたので、Kagle や日垞業務で最も興味深く圹立぀こずの数々をコミュニティに䌝えおいきたいず思いたす。 議論されたトピックの䞭には、次のものが含たれたす OpenCV、JSON 解析 (これらの䟋では、R のスクリプトたたはパッケヌゞぞの C++ コヌドの統合を調べたす。 )、スクリプトのパラメヌタ化、最終゜リュヌションの Docker 化。 実行に適した圢匏のメッセヌゞのすべおのコヌドは、次の堎所にありたす。 リポゞトリ.

内容

  1. CSV から MonetDB にデヌタを効率的にロヌドする
  2. バッチの準備
  3. デヌタベヌスからバッチをアンロヌドするためのむテレヌタ
  4. モデル アヌキテクチャの遞択
  5. スクリプトのパラメヌタ化
  6. スクリプトの Docker 化
  7. Google Cloud での耇数の GPU の䜿甚
  8. 代わりに、結論の

1. CSV から MonetDB デヌタベヌスにデヌタを効率的にロヌドする

本コンテストのデヌタは既補の画像圢匏ではなく、点座暙を含むJSONを含むCSVファむル340個クラスごずに256ファむルの圢匏で提䟛されたす。 これらの点を線で結ぶず、256x7.4 ピクセルの最終画像が埗られたす。 たた、各レコヌドには、デヌタセットの収集時に䜿甚された分類子によっお画像が正しく認識されたかどうかを瀺すラベル、画像の䜜成者の居䜏囜の 20 文字コヌド、䞀意の識別子、タむムスタンプが含たれたす。ファむル名ず䞀臎するクラス名。 元のデヌタの簡易バヌゞョンの重さはアヌカむブ内で 240 GB、解凍埌は玄 50 GB、解凍埌の完党なデヌタは XNUMX GB になりたす。 䞻催者は䞡方のバヌゞョンで同じ図面を再珟するこずを保蚌したした。これは、完党版が冗長であるこずを意味したす。 いずれにせよ、XNUMX 䞇枚の画像をグラフィック ファむルたたは配列圢匏で保存するのは採算が合わないず刀断され、アヌカむブからすべおの CSV ファむルを結合するこずにしたした。 train_simplified.zip デヌタベヌスに保存され、その埌、バッチごずに必芁なサむズの画像が「その堎で」生成されたす。

DBMS ずしお実瞟のあるシステムが遞択されたした モネDB、぀たりパッケヌゞずしおの R の実装 モネDBLite。 このパッケヌゞにはデヌタベヌス サヌバヌの組み蟌みバヌゞョンが含たれおおり、R セッションからサヌバヌを盎接遞択しおそこで䜜業できるようになりたす。 デヌタベヌスの䜜成ずデヌタベヌスぞの接続は、次の XNUMX ぀のコマンドで実行されたす。

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

XNUMX ぀のテヌブルを䜜成する必芁がありたす。XNUMX ぀はすべおのデヌタ甚で、もう XNUMX ぀はダりンロヌドされたファむルに関するサヌビス情報甚です (䜕か問題が発生し、いく぀かのファむルをダりンロヌドした埌にプロセスを再開する必芁がある堎合に圹立ちたす)。

テヌブルの䜜成

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

デヌタをデヌタベヌスにロヌドする最速の方法は、SQL コマンドを䜿甚しお CSV ファむルを盎接コピヌするこずでした。 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

デヌタのロヌド時間は、䜿甚するドラむブの速床特性によっお異なる堎合がありたす。 私たちの堎合、10 ぀の SSD 内での読み取りず曞き蟌み、たたはフラッシュ ドラむブ (゜ヌス ファむル) から SSD (DB) ぞの読み取りず曞き蟌みには XNUMX 分もかかりたせん。

敎数クラス ラベルずむンデックス列 (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 ぀のトリックを䜿甚したした。 XNUMX ぀目は、芳枬 ID を栌玍する型の次元を削枛するこずでした。 元のデヌタセットでは、ID を保存するために必芁な型は次のずおりです。 bigintただし、芳枬倀の数により、序数に等しい識別子を型に圓おはめるこずができたす。 int。 この堎合、怜玢ははるかに高速になりたす。 XNUMX番目のトリックは䜿甚するこずでした ORDERED INDEX — 私たちは利甚可胜なすべおのこずを怜蚎した結果、経隓的にこの決定に達したした オプション。 XNUMX ぀目は、パラメヌタ化されたク゚リを䜿甚するこずでした。 このメ゜ッドの本質はコマンドを XNUMX 回実行するこずです PREPARE その埌、同じタむプのク゚リを倚数䜜成するずきに準備された匏を䜿甚したすが、実際には、単玔なク゚リず比范しお利点がありたす。 SELECT 統蚈誀差の範囲内であるこずが刀明したした。

デヌタのアップロヌドのプロセスで消費される RAM は 450 MB 以内です。 ぀たり、ここで説明したアプロヌチを䜿甚するず、シングルボヌド デバむスを含むほずんどすべおの予算のハヌドりェア䞊で数十ギガバむトのデヌタセットを移動できるようになり、これは非垞に優れおいたす。

残っおいるのは、(ランダム) デヌタの取埗速床を枬定し、さたざたなサむズのバッチをサンプリングするずきのスケヌリングを評䟡するこずだけです。

デヌタベヌスベンチマヌク

library(ggplot2)

set.seed(0)
# ППЎключеМОе к базе ЎаММых
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))

# ЀуМкцОя Ўля пПЎгПтПвкО запрПса Ма стПрПМе сервера
prep_sql <- function(batch_size) {
  sql <- sprintf("PREPARE SELECT id FROM doodles WHERE id IN (%s)",
                 paste(rep("?", batch_size), collapse = ","))
  res <- DBI::dbSendQuery(con, sql)
  return(res)
}

# ЀуМкцОя Ўля ОзвлечеМОя ЎаММых
fetch_data <- function(rs, batch_size) {
  ids <- sample(seq_len(n), batch_size)
  res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
  return(res)
}

# ПрПвеЎеМОе заЌера
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    rs <- prep_sql(batch_size)
    bench::mark(
      fetch_data(rs, batch_size),
      min_iterations = 50L
    )
  }
)
# ПараЌетры беМчЌарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   batch_size      min   median      max `itr/sec` total_time n_itr
#        <dbl> <bch:tm> <bch:tm> <bch:tm>     <dbl>   <bch:tm> <int>
# 1         16   23.6ms  54.02ms  93.43ms     18.8        2.6s    49
# 2         32     38ms  84.83ms 151.55ms     11.4       4.29s    49
# 3         64   63.3ms 175.54ms 248.94ms     5.85       8.54s    50
# 4        128   83.2ms 341.52ms 496.24ms     3.00      16.69s    50
# 5        256  232.8ms 653.21ms 847.44ms     1.58      31.66s    50
# 6        512  784.6ms    1.41s    1.98s     0.740       1.1m    49
# 7       1024  681.7ms    2.72s    4.06s     0.377      2.16m    49

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

DBI::dbDisconnect(con, shutdown = TRUE)

Quick Draw Doodle Recognition: R、C++、ニュヌラル ネットワヌクず友達になる方法

2. バッチの準備

バッチ準備プロセス党䜓は次の手順で構成されたす。

  1. 点の座暙を持぀文字列のベクトルを含む耇数の JSON を解析したす。
  2. 必芁なサむズ (256×256 たたは 128×128 など) の画像䞊の点の座暙に基づいお色付きの線を描画したす。
  3. 結果のむメヌゞをテン゜ルに倉換したす。

Python カヌネル間の競争の䞀環ずしお、この問題は䞻に次の方法を䜿甚しお解決されたした。 OpenCV。 R の最も単玔か぀明癜な類䌌物の XNUMX ぀は次のようになりたす。

R での JSON から Tensor ぞの倉換の実装

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 ツヌルを䜿甚しお実行され、RAM に保存された䞀時 PNG に保存されたす (Linux では、䞀時 R ディレクトリは次のディレクトリにありたす) /tmp、RAMにマりントされたす。 このファむルは、0 から 1 の範囲の数倀を持぀ XNUMX 次元配列ずしお読み取られたす。埓来の BMP は XNUMX 進カラヌ コヌドを含む生の配列に読み蟌たれるため、これは重芁です。

結果をテストしおみたしょう:

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

Quick Draw Doodle Recognition: R、C++、ニュヌラル ネットワヌクず友達になる方法

バッチ自䜓は次のように圢成されたす。

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

倧芏暡なバッチの圢成には非垞に長い時間がかかるため、この実装は私たちにずっお最適ずは蚀えたせんでした。そしお、匷力なラむブラリを䜿甚しお同僚の経隓を掻甚するこずにしたした。 OpenCV。 圓時、R 甚の既補のパッケヌゞはありたせんでした (珟圚はありたせん)。そのため、必芁な機胜の最小限の実装は C++ で蚘述され、以䞋を䜿甚しお R コヌドに統合されたした。 .

この問題を解決するために、次のパッケヌゞずラむブラリが䜿甚されたした。

  1. OpenCV 画像の操䜜や線の描画に䜿甚したす。 プリむンストヌルされたシステム ラむブラリずヘッダヌ ファむル、およびダむナミック リンクを䜿甚したした。

  2. ゚クステン゜ル 倚次元配列ずテン゜ルを操䜜するためのものです。 同じ名前の R パッケヌゞに含たれるヘッダヌ ファむルを䜿甚したした。 このラむブラリを䜿甚するず、行優先順序ず列優先順序の䞡方で倚次元配列を操䜜できたす。

  3. ンゞ゜ン JSON を解析するため。 このラむブラリは以䞋で䜿甚されたす ゚クステン゜ル プロゞェクト内に存圚する堎合は自動的に実行されたす。

  4. Rcppスレッド JSON からのベクトルのマルチスレッド凊理を組織化したす。 このパッケヌゞで提䟛されるヘッダヌ ファむルを䜿甚したした。 より人気のあるものから RcppParallel このパッケヌゞには、ずりわけルヌプ割り蟌みメカニズムが組み蟌たれおいたす。

これは、こずは泚目に倀したす ゚クステン゜ル それは倩の恵みでした。広範な機胜ず高いパフォヌマンスを備えおいるこずに加えお、開発者は非垞に反応が良く、質問に迅速か぀詳现に答えおくれたした。 圌らの助けにより、OpenCV 行列の xtensor テン゜ルぞの倉換ず、3 次元画像テン゜ルを正しい次元 (バッチ自䜓) の 4 次元テン゜ルに結合する方法を実装するこずができたした。

Rcpp、xtensor、RcppThreadを孊ぶための教材

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

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

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

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

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

システム ファむルを䜿甚するファむルず、システムにむンストヌルされおいるラむブラリずの動的リンクをコンパむルするには、パッケヌゞに実装されおいるプラ​​グむン メカニズムを䜿甚したした。 。 パスずフラグを自動的に怜玢するために、䞀般的な Linux ナヌティリティを䜿甚したした。 パッケヌゞ構成.

OpenCVラむブラリを䜿甚するためのRcppプラグむンの実装

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

C++ での JSON からテン゜ルぞの倉換の実装

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

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

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

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

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

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

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

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

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

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

このコヌドはファむルに配眮する必芁がありたす src/cv_xt.cpp コマンドでコンパむルしたす Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); 仕事にも必芁な nlohmann/json.hpp の リポゞトリ。 コヌドはいく぀かの関数に分割されおいたす。

  • to_xt — 画像行列を倉換するためのテンプレヌト関数 (cv::Mat) をテン゜ルに xt::xtensor;

  • parse_json — この関数は JSON 文字列を解析し、点の座暙を抜出しおベクトルにパックしたす。

  • ocv_draw_lines — 結果ずしお埗られる点のベクトルから、耇数色の線を描画したす。

  • process — 䞊蚘の機胜を組み合わせお、結果の画像を拡倧瞮小する機胜も远加したす。

  • cpp_process_json_str - 関数のラッパヌ process、結果を R オブゞェクト (倚次元配列) に゚クスポヌトしたす。

  • cpp_process_json_vector - 関数のラッパヌ cpp_process_json_strこれにより、文字列ベクトルをマルチスレッド モヌドで凊理できるようになりたす。

耇数色の線を描画するには、HSV カラヌ モデルを䜿甚し、その埌 RGB に倉換したした。 結果をテストしおみたしょう:

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

Quick Draw Doodle Recognition: R、C++、ニュヌラル ネットワヌクず友達になる方法
R ず C++ での実装速床の比范

res_bench <- bench::mark(
  r_process_json_str(tmp_data[4, drawing], scale = 0.5),
  cpp_process_json_str(tmp_data[4, drawing], scale = 0.5),
  check = FALSE,
  min_iterations = 100
)
# ПараЌетры беМчЌарка
cols <- c("expression", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]

#   expression                min     median       max `itr/sec` total_time  n_itr
#   <chr>                <bch:tm>   <bch:tm>  <bch:tm>     <dbl>   <bch:tm>  <int>
# 1 r_process_json_str     3.49ms     3.55ms    4.47ms      273.      490ms    134
# 2 cpp_process_json_str   1.94ms     2.02ms    5.32ms      489.      497ms    243

library(ggplot2)
# ПрПвеЎеМОе заЌера
res_bench <- bench::press(
  batch_size = 2^(4:10),
  {
    .data <- tmp_data[sample(seq_len(.N), batch_size), drawing]
    bench::mark(
      r_process_json_vector(.data, scale = 0.5),
      cpp_process_json_vector(.data,  scale = 0.5),
      min_iterations = 50,
      check = FALSE
    )
  }
)

res_bench[, cols]

#    expression   batch_size      min   median      max `itr/sec` total_time n_itr
#    <chr>             <dbl> <bch:tm> <bch:tm> <bch:tm>     <dbl>   <bch:tm> <int>
#  1 r                   16   50.61ms  53.34ms  54.82ms    19.1     471.13ms     9
#  2 cpp                 16    4.46ms   5.39ms   7.78ms   192.      474.09ms    91
#  3 r                   32   105.7ms 109.74ms 212.26ms     7.69        6.5s    50
#  4 cpp                 32    7.76ms  10.97ms  15.23ms    95.6     522.78ms    50
#  5 r                   64  211.41ms 226.18ms 332.65ms     3.85      12.99s    50
#  6 cpp                 64   25.09ms  27.34ms  32.04ms    36.0        1.39s    50
#  7 r                  128   534.5ms 627.92ms 659.08ms     1.61      31.03s    50
#  8 cpp                128   56.37ms  58.46ms  66.03ms    16.9        2.95s    50
#  9 r                  256     1.15s    1.18s    1.29s     0.851     58.78s    50
# 10 cpp                256  114.97ms 117.39ms 130.09ms     8.45       5.92s    50
# 11 r                  512     2.09s    2.15s    2.32s     0.463       1.8m    50
# 12 cpp                512  230.81ms  235.6ms 261.99ms     4.18      11.97s    50
# 13 r                 1024        4s    4.22s     4.4s     0.238       3.5m    50
# 14 cpp               1024  410.48ms 431.43ms 462.44ms     2.33      21.45s    50

ggplot(res_bench, aes(x = factor(batch_size), y = median, 
                      group =  expression, color = expression)) +
  geom_point() +
  geom_line() +
  ylab("median time, s") +
  theme_minimal() +
  scale_color_discrete(name = "", labels = c("cpp", "r")) +
  theme(legend.position = "bottom") 

Quick Draw Doodle Recognition: R、C++、ニュヌラル ネットワヌクず友達になる方法

ご芧のずおり、速床の向䞊は非垞に倧幅であるこずが刀明し、R コヌドを䞊列化しおも C++ コヌドに远い぀くこずはできたせん。

3. デヌタベヌスからバッチをアンロヌドするためのむテレヌタ

R は RAM に収たるデヌタ凊理で定評がありたすが、Python は反埩的なデヌタ凊理により特城があり、アりトオブコア蚈算 (倖郚メモリを䜿甚した蚈算) を簡単か぀自然に実装できたす。 説明されおいる問題のコンテキストにおいお、叀兞的で関連性のある䟋は、芳枬倀のごく䞀郚たたはミニバッチを䜿甚しお各ステップでの募配を近䌌する募配降䞋法によっおトレヌニングされたディヌプ ニュヌラル ネットワヌクです。

Python で曞かれた深局孊習フレヌムワヌクには、テヌブル、フォルダヌ内の画像、バむナリ圢匏などのデヌタに基づいおむテレヌタヌを実装する特別なクラスがありたす。既補のオプションを䜿甚するこずも、特定のタスク甚に独自のオプションを䜜成するこずもできたす。 R では、Python ラむブラリのすべおの機胜を利甚できたす。 keras 同じ名前のパッケヌゞを䜿甚するさたざたなバック゚ンドで、パッケヌゞの䞊で動䜜したす。 網状。 埌者に぀いおは、別の長い蚘事を曞く䟡倀がありたす。 R から Python コヌドを実行できるだけでなく、R ず Python セッション間でオブゞェクトを転送し、必芁な型倉換をすべお自動的に実行するこずもできたす。

MonetDBLite を䜿甚するこずで、すべおのデヌタを RAM に保存する必芁がなくなりたした。すべおの「ニュヌラル ネットワヌク」䜜業は Python の元のコヌドによっお実行されたす。䜕も準備ができおいないため、デヌタにむテレヌタを蚘述するだけで枈みたす。このような状況には、R たたは Python を䜿甚したす。 基本的に、その芁件は XNUMX ぀だけです。無限ルヌプでバッチを返すこずず、反埩間でその状態を保存するこずです (R における埌者は、クロヌゞャを䜿甚する最も単玔な方法で実装されたす)。 以前は、むテレヌタ内で R 配列を numpy 配列に明瀺的に倉換する必芁がありたしたが、珟圚のバヌゞョンのパッケヌゞでは keras 自分でやるのです。

トレヌニング デヌタず怜蚌デヌタのむテレヌタは次のようになりたした。

トレヌニングおよび怜蚌デヌタのむテレヌタ

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

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

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

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

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

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

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

    # ПарсОМг JSON О пПЎгПтПвка ЌассОва
    batch_x <- cpp_process_json_vector(batch$drawing, scale = scale, color = color)
    if (imagenet_preproc) {
      # КкалОрПваМОе c ОМтервала [0, 1] Ма ОМтервал [-1, 1]
      batch_x <- (batch_x - 0.5) * 2
    }

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

この関数は、デヌタベヌスぞの接続、䜿甚される行数、クラス数、バッチ サむズ、スケヌル (scale = 1 256x256 ピクセルのレンダリング画像に察応し、 scale = 0.5 — 128x128 ピクセル)、カラヌむンゞケヌタヌ (color = FALSE 䜿甚時にグレヌスケヌルでのレンダリングを指定したす color = TRUE 各ストロヌクは新しい色で描画されたす) ず、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)

Quick Draw Doodle Recognition: R、C++、ニュヌラル ネットワヌクず友達になる方法

十分な量の RAM がある堎合は、デヌタベヌスを同じ RAM に転送するこずで、デヌタベヌスの動䜜を倧幅に高速化できたす (このタスクには 32 GB で十分です)。 Linux では、パヌティションはデフォルトでマりントされたす /dev/shm、RAM容量の最倧半分を占有したす。 線集するこずでさらに匷調衚瀺できたす /etc/fstabのようなレコヌドを取埗するには tmpfs /dev/shm tmpfs defaults,size=25g 0 0。 必ず再起動し、コマンドを実行しお結果を確認しおください。 df -h.

テスト デヌタセットは完党に RAM に収たるため、テスト デヌタのむテレヌタは非垞に単玔に芋えたす。

テストデヌタのむテレヌタ

test_generator <- function(dt,
                           batch_size = 32,
                           scale = 1,
                           color = FALSE,
                           imagenet_preproc = FALSE) {

  # ПрПверка аргуЌеМтПв
  checkmate::assert_data_table(dt)
  checkmate::assert_count(batch_size)
  checkmate::assert_number(scale, lower = 0.001, upper = 5)
  checkmate::assert_flag(color)
  checkmate::assert_flag(imagenet_preproc)

  # ПрПставляеЌ МПЌера батчей
  dt[, batch := (.I - 1L) %/% batch_size + 1L]
  data.table::setkey(dt, batch)
  i <- 1
  max_i <- dt[, max(batch)]

  # ЗаЌыкаМОе
  function() {
    batch_x <- cpp_process_json_vector(dt[batch == i, drawing], 
                                       scale = scale, color = color)
    if (imagenet_preproc) {
      # КкалОрПваМОе c ОМтервала [0, 1] Ма ОМтервал [-1, 1]
      batch_x <- (batch_x - 0.5) * 2
    }
    result <- list(batch_x)
    i <<- i + 1
    return(result)
  }
}

4. モデルアヌキテクチャの遞択

最初に䜿甚されたアヌキテクチャは モバむルネット v1の機胜に぀いおは、 これ メッセヌゞ。 暙準で付属しおいたす keras したがっお、R の同じ名前のパッケヌゞで利甚できたす。しかし、これを単䞀チャネルの画像で䜿甚しようずするず、奇劙なこずが刀明したした。入力テン゜ルは垞に次の次元を持぀必芁がありたす。 (batch, height, width, 3)぀たり、チャネル数は倉曎できたせん。 Python にはそのような制限がないため、元の蚘事に埓っお (keras バヌゞョンにあるドロップアりトなしで) 急いでこのアヌキテクチャの独自の実装を䜜成したした。

モバむルネット 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 配列のリストの圢匏でモデルの重みを取埗し、このリストの最初の芁玠の次元を倉曎し (XNUMX ぀のカラヌ チャネルを取埗するか、XNUMX ぀すべおを平均するこずによっお)、関数を䜿甚しお重みをモデルにロヌドし盎したす。 set_weights()。 この段階では、カラヌ画像を凊理する方が生産性が高いこずがすでに明らかであったため、この機胜は远加されたせんでした。

ほずんどの実隓は、mobilenet バヌゞョン 1 ず 2、および resnet34 を䜿甚しお実行したした。 SE-ResNeXt などの最新のアヌキテクチャがこの競争で奜成瞟を収めたした。 残念ながら、自由に䜿える既補の実装がなく、独自の実装を䜜成したせんでした (ただし、必ず䜜成したす)。

5. スクリプトのパラメヌタ化

䟿宜䞊、トレヌニングを開始するためのすべおのコヌドは、次を䜿甚しおパラメヌタ化された単䞀のスクリプトずしお蚭蚈されたした。 ドックプト 次のようにしたす。

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

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

パッケヌゞ ドックプト 実装を衚したす http://docopt.org/ R の堎合、その助けを借りお、スクリプトは次のような単玔なコマンドで起動されたす。 Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db たたは ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db、ファむルの堎合 train_nn.R 実行可胜です (このコマンドはモデルのトレヌニングを開始したす) resnet50 128x128 ピクセルの XNUMX 色画像の堎合、デヌタベヌスは次のフォルダヌに配眮する必芁がありたす。 /home/andrey/doodle_db。 孊習速床、オプティマむザヌのタむプ、その他のカスタマむズ可胜なパラメヌタヌをリストに远加できたす。 出版物の準備の過皋で、アヌキテクチャが次のずおりであるこずが刀明したした。 mobilenet_v2 珟圚のバヌゞョンから keras R䜿甚䞭 しおはいけたせん R パッケヌゞでは倉曎が考慮されおいないため、修正されるのを埅っおいたす。

このアプロヌチにより、RStudio でスクリプトを起動する埓来の方法ず比范しお、さたざたなモデルでの実隓を倧幅に高速化するこずができたした (代替手段ずしおパッケヌゞに泚目したす) tfruns。 ただし、䞻な利点は、RStudio をむンストヌルしなくおも、Docker たたは単にサヌバヌ䞊でスクリプトの起動を簡単に管理できるこずです。

6. スクリプトの Docker 化

Docker を䜿甚しお、チヌム メンバヌ間でモデルをトレヌニングし、クラりドに迅速にデプロむするための環境の移怍性を確保したした。 R プログラマヌにずっおは比范的珍しいこのツヌルに぀いおは、次の手順で始めるこずができたす。 この シリヌズの出版物や ビデオコヌス.

Docker を䜿甚するず、独自のむメヌゞを最初から䜜成するこずも、他のむメヌゞを独自のむメヌゞを䜜成するためのベヌスずしお䜿甚するこずもできたす。 利甚可胜なオプションを分析したずころ、NVIDIA、CUDA+cuDNN ドラむバヌ、および Python ラむブラリのむンストヌルがむメヌゞのかなりの郚分であるずいう結論に達し、公匏むメヌゞを基瀎ずしお採甚するこずにしたした。 tensorflow/tensorflow:1.12.0-gpu、そこに必芁な R パッケヌゞを远加したす。

最終的な docker ファむルは次のようになりたす。

ドッカヌファむル

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。 これにより、コヌド内で OS バヌゞョンを指定する必芁がなくなりたした。

さらに、さたざたなコマンドを䜿甚しおコンテナヌを起動できるようにする小さな 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 での耇数の GPU の䜿甚

コンテストの特城の 1 ぀は、非垞にノむズの倚いデヌタでした (ODS スラックの @Leigh.plt から借甚したタむトル画像を参照)。 倧芏暡なバッチはこれに察凊するのに圹立ちたす。XNUMX GPU を搭茉した PC で実隓した埌、クラりド内の耇数の GPU でトレヌニング モデルをマスタヌするこずにしたした。 GoogleCloudを䜿甚基本ぞの良いガむド利甚可胜な構成の遞択肢が豊富で、手頃な䟡栌ず 300 ドルのボヌナスがあるためです。 欲に駆られお、SSD ず倧量の RAM を搭茉した 4xV100 むンスタンスを泚文したしたが、それは倧きな間違いでした。 このようなマシンはすぐにお金を䜿い果たしたす。実蚌枈みのパむプラむンがなければ実隓は砎産する可胜性がありたす。 教育目的の堎合は、K80 を受講するこずをお勧めしたす。 しかし、倧量の RAM が圹に立ちたした。クラりド SSD のパフォヌマンスは印象に残らなかったので、デヌタベヌスは次の堎所に転送されたした。 dev/shm.

最も興味深いのは、耇数の GPU の䜿甚を担圓するコヌド フラグメントです。 たず、Python ず同様に、コンテキスト マネヌゞャヌを䜿甚しお CPU 䞊にモデルが䜜成されたす。

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

次に、コンパむルされおいない (これは重芁です) モデルが、指定された数の䜿甚可胜な GPU にコピヌされ、その埌でのみコンパむルされたす。

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

最埌の局を陀くすべおの局をフリヌズし、最埌の局をトレヌニングし、モデル党䜓をフリヌズ解陀しお再トレヌニングするずいう叀兞的な手法は、耇数の GPU に察しお実装できたせんでした。

トレヌニングは䜿甚せずに監芖されたした。 テン゜ルボヌド、各゚ポックの埌にログを蚘録し、有益な名前を付けおモデルを保存するこずに限定したす。

コヌルバック

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

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

8. 結論ではなく

私たちが遭遇した倚くの問題はただ克服されおいたせん。

  • в keras 最適な孊習率を自動的に怜玢する既補の機胜はありたせんアナログ lr_finder 図曞通で 高速.ai); ある皋床の努力をすれば、サヌドパヌティの実装を R に移怍するこずができたす。たずえば、次のようになりたす。 この;
  • 前の点の結果ずしお、耇数の GPU を䜿甚する堎合、正しいトレヌニング速床を遞択するこずができたせんでした。
  • 最新のニュヌラル ネットワヌク アヌキテクチャ、特に imagenet で事前トレヌニングされたものが䞍足しおいたす。
  • XNUMX サむクル ポリシヌず識別孊習率はありたせん (コサむン アニヌリングは私たちの芁望に応じお行われたした) 実装された、ありがずうございたした スカむダン).

このコンテストから孊んだ有益なこずは次のずおりです。

  • 比范的䜎電力のハヌドりェアでは、たずもな (RAM の䜕倍ものサむズの) ボリュヌムのデヌタを苊痛なく凊理できたす。 ビニヌル袋 デヌタ衚 テヌブルのむンプレヌス倉曎によりメモリが節玄され、テヌブルのコピヌが回避されたす。たた、正しく䜿甚するず、その機胜はほずんどの堎合、スクリプト蚀語甚ずしお知られおいるすべおのツヌルの䞭で最高の速床を瀺したす。 デヌタをデヌタベヌスに保存するず、倚くの堎合、デヌタセット党䜓を RAM に詰め蟌む必芁性に぀いおたったく考える必芁がなくなりたす。
  • R の遅い関数は、パッケヌゞを䜿甚しお C++ の速い関数に眮き換えるこずができたす 。 さらに䜿甚する堎合 Rcppスレッド たたは RcppParallel、クロスプラットフォヌムのマルチスレッド実装が埗られるため、R レベルでコヌドを䞊列化する必芁はありたせん。
  • パッケヌゞ別  C++ の深い知識がなくおも䜿甚できたすが、最䜎限必芁なものが抂説されおいたす ここで。 次のような倚くの優れた C ラむブラリのヘッダヌ ファむル ゚クステン゜ル ぀たり、既補の高性胜 C++ コヌドを R に統合するプロゞェクトの実装のためのむンフラストラクチャが圢成されおいたす。 さらに䟿利なのは、RStudio の構文ハむラむトず静的 C++ コヌド アナラむザヌです。
  • ドックプト パラメヌタヌを䜿甚しお自己完結型スクリプトを実行できたす。 これは、リモヌト サヌバヌで䜿甚する堎合に䟿利です。 ドッカヌの䞋。 RStudio では、ニュヌラル ネットワヌクをトレヌニングするための䜕時間もの実隓を行うのは䞍䟿であり、サヌバヌ自䜓に IDE をむンストヌルするこずが必ずしも正圓であるずは限りたせん。
  • Docker は、異なるバヌゞョンの OS やラむブラリを䜿甚する開発者間でのコヌドの移怍性ず結果の再珟性、およびサヌバヌでの実行の容易さを保蚌したす。 たった XNUMX ぀のコマンドでトレヌニング パむプラむン党䜓を起動できたす。
  • Google Cloud は、高䟡なハヌドりェアを詊すための予算に優しい方法ですが、構成を慎重に遞択する必芁がありたす。
  • 個々のコヌドフラグメントの速床を枬定するこずは、特に R ず C++ を組み合わせたり、パッケヌゞず組み合わせたりする堎合に非垞に圹立ちたす。 ベンチ - これも非垞に簡単です。

党䜓ずしお、この経隓は非垞に䟡倀のあるものであり、私たちは提起された問題のいく぀かを解決するために匕き続き取り組んでいたす。

出所 habr.com

コメントを远加したす