Хөөе Хабр!
Өнгөрсөн намар Kaggle гараар зурсан зургуудыг ангилах, Quick Draw Doodle таних уралдааныг зохион байгуулсан бөгөөд үүнд R-эрдэмтдийн баг оролцов.
Энэ удаад медалийн тариалалтаар бүтсэнгүй, гэхдээ маш их үнэ цэнэтэй туршлага хуримтлуулсан тул би Кагле болон өдөр тутмын ажилд хэрэгтэй хэд хэдэн сонирхолтой, хэрэгтэй зүйлийн талаар олон нийтэд хэлмээр байна. Хэлэлцсэн сэдвүүдийн дунд: хүнд хэцүү амьдрал OpenCV програм, JSON задлан шинжлэх (эдгээр жишээнүүд нь C++ кодыг R хэл дээрх скрипт эсвэл багцад нэгтгэхийг шалгадаг. Rcpp), скриптүүдийг параметржүүлэх, эцсийн шийдлийг докержуулах. Гүйцэтгэхэд тохиромжтой маягтын мессежийн бүх кодыг эндээс авах боломжтой
Агуулга:
CSV-ээс MonetDB руу өгөгдлийг үр дүнтэй ачаална уу Багц бэлдэж байна Өгөгдлийн сангаас багц буулгах давталт Архитектурын загвар сонгох Скриптийн параметржуулалт Скриптүүдийн докержуулалт Google Cloud дээр олон GPU ашиглах Оронд дүгнэлтийг
1. CSV-ээс MonetDB мэдээллийн санд өгөгдлийг үр дүнтэй ачаална уу
Энэхүү уралдааны өгөгдлийг бэлэн зураг хэлбэрээр биш, харин цэгийн координат бүхий JSON агуулсан 340 CSV файл (анги тус бүрт нэг файл) хэлбэрээр өгсөн болно. Эдгээр цэгүүдийг шугамаар холбосноор бид 256x256 пикселийн хэмжээтэй эцсийн дүрсийг авдаг. Мөн бичлэг тус бүрд тухайн зургийг өгөгдлийн багц цуглуулах үед ашигласан ангилагч зөв таньсан эсэхийг харуулсан шошго, зургийн зохиогчийн оршин суугаа улсын хоёр үсэгтэй код, өвөрмөц танигч, цагийн тэмдэг байна. болон файлын нэртэй таарах ангийн нэр. Анхны өгөгдлийн хялбаршуулсан хувилбар нь архивт 7.4 ГБ жинтэй бөгөөд задласны дараа ойролцоогоор 20 ГБ жинтэй, задласны дараа бүрэн өгөгдөл нь 240 ГБ эзэлнэ. Зохион байгуулагчид хоёр хувилбар нь ижил зургийг хуулбарласан байхаар баталгаажуулсан бөгөөд энэ нь бүтэн хувилбар нь илүүдэхгүй гэсэн үг юм. Ямар ч тохиолдолд 50 сая зургийг график файл эсвэл массив хэлбэрээр хадгалах нь ашиггүй гэж үзсэн тул архиваас бүх CSV файлуудыг нэгтгэхээр шийдсэн. train_simplified.zip Багц тус бүрд шаардлагатай хэмжээтэй зургийг дараа нь үүсгэн мэдээллийн санд оруулах.
DBMS болгон сайн батлагдсан системийг сонгосон MonetDB, тухайлбал R-д зориулсан хэрэгжилт
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
Бид хоёр хүснэгт үүсгэх шаардлагатай: нэг нь бүх өгөгдөл, нөгөө нь татаж авсан файлуудын талаарх үйлчилгээний мэдээлэл (хэрэв ямар нэг зүйл буруу болвол ашигтай бөгөөд хэд хэдэн файлыг татаж авсны дараа процессыг үргэлжлүүлэх шаардлагатай):
Хүснэгтүүдийг үүсгэх
if (!DBI::dbExistsTable(con, "doodles")) {
DBI::dbCreateTable(
con = con,
name = "doodles",
fields = c(
"countrycode" = "char(2)",
"drawing" = "text",
"key_id" = "bigint",
"recognized" = "bool",
"timestamp" = "timestamp",
"word" = "text"
)
)
}
if (!DBI::dbExistsTable(con, "upload_log")) {
DBI::dbCreateTable(
con = con,
name = "upload_log",
fields = c(
"id" = "serial",
"file_name" = "text UNIQUE",
"uploaded" = "bool DEFAULT false"
)
)
}
Өгөгдлийн санд өгөгдөл ачаалах хамгийн хурдан арга бол 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
Өгөгдөл ачаалах хугацаа нь ашигласан хөтчийн хурдны шинж чанараас хамаарч өөр өөр байж болно. Манай тохиолдолд нэг SSD дотор эсвэл флаш дискнээс (эх файл) SSD (DB) руу унших, бичихэд 10 минутаас бага хугацаа шаардагдана.
Бүхэл ангийн шошго, индекс багана (ORDERED INDEX
) багц үүсгэх үед ажиглалтаас дээж авах мөрийн дугаартай:
Нэмэлт багана, индекс үүсгэх
message("Generate lables")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD label_int int"))
invisible(DBI::dbExecute(con, "UPDATE doodles SET label_int = dense_rank() OVER (ORDER BY word) - 1"))
message("Generate row numbers")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD id serial"))
invisible(DBI::dbExecute(con, "CREATE ORDERED INDEX doodles_id_ord_idx ON doodles(id)"))
Багц үүсгэх асуудлыг шийдэхийн тулд бид хүснэгтээс санамсаргүй мөрүүдийг гаргаж авах хамгийн дээд хурдыг олж авах шаардлагатай болсон. doodles
. Үүний тулд бид 3 заль мэхийг ашигласан. Эхнийх нь ажиглалтын ID-г хадгалдаг төрлийн хэмжээст байдлыг багасгах явдал байв. Анхны өгөгдлийн багцад ID-г хадгалахад шаардлагатай төрөл байна bigint
, гэхдээ ажиглалтын тоо нь эрэмбийн тоотой тэнцүү тодорхойлогчийг төрөлд оруулах боломжтой болгодог. int
. Энэ тохиолдолд хайлт илүү хурдан байдаг. Хоёр дахь заль мэх бол ашиглах явдал байв ORDERED INDEX
- Бид боломжтой бүх зүйлийг туршиж үзээд эмпирик байдлаар ийм шийдвэрт хүрсэн PREPARE
Дараа нь ижил төрлийн асуулга үүсгэхдээ бэлтгэсэн илэрхийллийг ашигладаг боловч үнэндээ энгийн асуулттай харьцуулахад давуу талтай байдаг. SELECT
статистикийн алдааны хүрээнд гарсан байна.
Өгөгдөл байршуулах процесс нь 450 МБ-аас ихгүй RAM зарцуулдаг. Өөрөөр хэлбэл, тайлбарласан арга нь хэдэн арван гигабайт жинтэй өгөгдлийн багцыг бараг ямар ч төсөвт төхөөрөмж, тэр дундаа зарим нэг самбарт төхөөрөмж дээр шилжүүлэх боломжийг олгодог бөгөөд энэ нь үнэхээр гайхалтай юм.
Өөр өөр хэмжээтэй багцыг түүвэрлэхдээ (санамсаргүй) өгөгдлийг олж авах хурдыг хэмжиж, масштабыг үнэлэх л үлдлээ.
Өгөгдлийн сангийн жишиг
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)
2. Багц бэлтгэх
Багцыг бэлтгэх бүх үйл явц нь дараах үе шатуудаас бүрдэнэ.
- Цэгүүдийн координат бүхий мөрийн векторуудыг агуулсан хэд хэдэн JSON-г задлан шинжилж байна.
- Шаардлагатай хэмжээтэй зураг дээрх цэгүүдийн координат дээр үндэслэн өнгөт шугам зурах (жишээлбэл, 256 × 256 эсвэл 128 × 128).
- Үүссэн зургуудыг тензор болгон хувиргах.
Python цөмүүдийн дундах өрсөлдөөний хүрээнд асуудлыг үндсэндээ ашиглан шийдсэн OpenCV програм. R-ийн хамгийн энгийн бөгөөд ойлгомжтой аналогуудын нэг нь иймэрхүү харагдах болно.
R хэл дээр JSON-ийг тензор руу хөрвүүлэхийг хэрэгжүүлж байна
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 файлд хадгалдаг (Линукс дээр түр зуурын R лавлахууд нь лавлахад байрладаг. /tmp
, RAM-д суурилуулсан). Дараа нь энэ файлыг 0-ээс 1 хүртэлх тоо бүхий гурван хэмжээст массив болгон уншина. Энэ нь илүү уламжлалт BMP-ийг зургаан өнцөгт өнгөний код бүхий түүхий массив болгон уншихад чухал ач холбогдолтой юм.
Үр дүнг туршиж үзье:
zip_file <- file.path("data", "train_simplified.zip")
csv_file <- "cat.csv"
unzip(zip_file, files = csv_file, exdir = tempdir(),
junkpaths = TRUE, unzip = getOption("unzip"))
tmp_data <- data.table::fread(file.path(tempdir(), csv_file), sep = ",",
select = "drawing", nrows = 10000)
arr <- r_process_json_str(tmp_data[4, drawing])
dim(arr)
# [1] 256 256 3
plot(magick::image_read(arr))
Багц өөрөө дараах байдлаар үүснэ.
res <- r_process_json_vector(tmp_data[1:4, drawing], scale = 0.5)
str(res)
# num [1:4, 1:128, 1:128, 1:3] 1 1 1 1 1 1 1 1 1 1 ...
# - attr(*, "dimnames")=List of 4
# ..$ : NULL
# ..$ : NULL
# ..$ : NULL
# ..$ : NULL
Их хэмжээний багц үүсгэх нь зохисгүй урт хугацаа шаарддаг тул бид хүчирхэг номын сан ашиглан хамтран ажиллагсдынхаа туршлагыг ашиглахаар шийдсэн тул энэхүү хэрэгжилт нь бидэнд тохиромжгүй мэт санагдсан. OpenCV програм. Тэр үед R-д зориулсан бэлэн багц байхгүй байсан (одоо байхгүй), тиймээс шаардлагатай функцүүдийн хамгийн бага хэрэгжилтийг C++ хэл дээр R кодтой нэгтгэн бичсэн. Rcpp.
Асуудлыг шийдвэрлэхийн тулд дараах багц, сангуудыг ашигласан болно.
-
OpenCV програм зурагтай ажиллах, шугам зурахад зориулагдсан. Урьдчилан суулгасан системийн номын сан, толгой файлууд, мөн динамик холболтыг ашигласан.
-
кстенсор олон хэмжээст массив болон тензоруудтай ажиллахад зориулагдсан. Бид ижил нэртэй R багцад багтсан толгой файлуудыг ашигласан. Номын сан нь олон хэмжээст массивтай ажиллах боломжийг олгодог, гол мөрөн болон баганын үндсэн дарааллаар.
-
нджсон JSON-г задлан шинжлэхэд зориулагдсан. Энэ номын санг ашиглаж байна кстенсор хэрэв төсөлд байгаа бол автоматаар.
-
RcppThread JSON-аас векторын олон урсгалтай боловсруулалтыг зохион байгуулахад зориулагдсан. Энэ багцаас өгсөн толгой файлуудыг ашигласан. Илүү алдартай хүмүүсээс RcppParallel Багц нь бусад зүйлсээс гадна давталтын тасалдлын механизмтай.
Үүнийг анхаарах хэрэгтэй кстенсор Энэ нь бурхны хишиг болж хувирсан: энэ нь өргөн цар хүрээтэй, өндөр гүйцэтгэлтэй байхаас гадна түүний хөгжүүлэгчид нэлээд хариу үйлдэл үзүүлж, асуултуудад шуурхай, дэлгэрэнгүй хариулсан. Тэдгээрийн тусламжтайгаар OpenCV матрицыг xtensor тензор болгон хувиргах, мөн 3 хэмжээст зургийн тензоруудыг зөв хэмжээст 4 хэмжээст тензор болгон нэгтгэх арга (багц өөрөө) боломжтой болсон.
Rcpp, xtensor болон RcppThread сурахад зориулсан материалууд
Системд суулгасан сангуудтай динамик холболт, системийн файлуудыг ашигладаг файлуудыг эмхэтгэхийн тулд бид багцад хэрэгжсэн залгаасын механизмыг ашигласан. Rcpp. Замууд болон тугуудыг автоматаар олохын тулд бид алдартай Linux хэрэгслийг ашигласан pkg-тохиргоо.
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)
))
})
Plugin-ийн үйл ажиллагааны үр дүнд эмхэтгэлийн явцад дараах утгуудыг орлуулах болно.
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))
R ба C++ хэл дээрх хэрэгжилтийн хурдыг харьцуулах
res_bench <- bench::mark(
r_process_json_str(tmp_data[4, drawing], scale = 0.5),
cpp_process_json_str(tmp_data[4, drawing], scale = 0.5),
check = FALSE,
min_iterations = 100
)
# Параметры бенчмарка
cols <- c("expression", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]
# expression min median max `itr/sec` total_time n_itr
# <chr> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 r_process_json_str 3.49ms 3.55ms 4.47ms 273. 490ms 134
# 2 cpp_process_json_str 1.94ms 2.02ms 5.32ms 489. 497ms 243
library(ggplot2)
# Проведение замера
res_bench <- bench::press(
batch_size = 2^(4:10),
{
.data <- tmp_data[sample(seq_len(.N), batch_size), drawing]
bench::mark(
r_process_json_vector(.data, scale = 0.5),
cpp_process_json_vector(.data, scale = 0.5),
min_iterations = 50,
check = FALSE
)
}
)
res_bench[, cols]
# expression batch_size min median max `itr/sec` total_time n_itr
# <chr> <dbl> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 r 16 50.61ms 53.34ms 54.82ms 19.1 471.13ms 9
# 2 cpp 16 4.46ms 5.39ms 7.78ms 192. 474.09ms 91
# 3 r 32 105.7ms 109.74ms 212.26ms 7.69 6.5s 50
# 4 cpp 32 7.76ms 10.97ms 15.23ms 95.6 522.78ms 50
# 5 r 64 211.41ms 226.18ms 332.65ms 3.85 12.99s 50
# 6 cpp 64 25.09ms 27.34ms 32.04ms 36.0 1.39s 50
# 7 r 128 534.5ms 627.92ms 659.08ms 1.61 31.03s 50
# 8 cpp 128 56.37ms 58.46ms 66.03ms 16.9 2.95s 50
# 9 r 256 1.15s 1.18s 1.29s 0.851 58.78s 50
# 10 cpp 256 114.97ms 117.39ms 130.09ms 8.45 5.92s 50
# 11 r 512 2.09s 2.15s 2.32s 0.463 1.8m 50
# 12 cpp 512 230.81ms 235.6ms 261.99ms 4.18 11.97s 50
# 13 r 1024 4s 4.22s 4.4s 0.238 3.5m 50
# 14 cpp 1024 410.48ms 431.43ms 462.44ms 2.33 21.45s 50
ggplot(res_bench, aes(x = factor(batch_size), y = median,
group = expression, color = expression)) +
geom_point() +
geom_line() +
ylab("median time, s") +
theme_minimal() +
scale_color_discrete(name = "", labels = c("cpp", "r")) +
theme(legend.position = "bottom")
Таны харж байгаагаар хурдны өсөлт нь маш их ач холбогдолтой болсон бөгөөд R кодыг параллель болгох замаар C++ кодыг гүйцэх боломжгүй юм.
3. Өгөгдлийн сангаас багц буулгах давталт
R нь RAM-д тохирох өгөгдлийг боловсруулдаг нэр хүндтэй байдаг бол Python нь давталттай өгөгдөл боловсруулдгаараа онцлогтой бөгөөд энэ нь үндсэн бус тооцооллыг (гадаад санах ой ашиглан тооцоолох) хялбар бөгөөд байгалийн аргаар хэрэгжүүлэх боломжийг олгодог. Тайлбарласан асуудлын хүрээнд бидний хувьд сонгодог бөгөөд хамааралтай жишээ бол ажиглалтын багахан хэсгийг эсвэл мини багцыг ашиглан алхам бүрт градиентийг ойртуулах замаар градиент буурах аргаар сургасан гүн мэдрэлийн сүлжээ юм.
Python хэл дээр бичигдсэн гүнзгий сургалтын системүүд нь өгөгдөлд суурилсан давталтуудыг хэрэгжүүлдэг тусгай ангиудтай: хүснэгт, хавтас дахь зураг, хоёртын формат гэх мэт. Та бэлэн сонголтуудыг ашиглах эсвэл тодорхой ажлуудад зориулж өөрөө бичиж болно. R дээр бид Python номын сангийн бүх боломжуудыг ашиглах боломжтой керас ижил нэртэй багцыг ашиглан төрөл бүрийн арын хэсгүүдтэй бөгөөд энэ нь эргээд багцын дээд талд ажилладаг торлог. Сүүлийнх нь тусдаа урт өгүүлэл байх ёстой; Энэ нь танд R-ээс Python кодыг ажиллуулах боломжийг олгодог төдийгүй R болон Python сесс хооронд объектуудыг шилжүүлэх, шаардлагатай бүх төрлийн хөрвүүлэлтийг автоматаар гүйцэтгэх боломжийг олгодог.
MonetDBLite ашиглан бид бүх өгөгдлийг RAM-д хадгалах шаардлагаас салсан, бүх "мэдрэлийн сүлжээ"-ийн ажлыг Python-ийн анхны кодоор хийх болно, бэлэн зүйл байхгүй тул бид өгөгдөл дээр давталт бичихэд л хангалттай. R эсвэл Python дээрх ийм нөхцөл байдлын хувьд. Үүнд үндсэндээ хоёр шаардлага бий: энэ нь багцуудыг эцэс төгсгөлгүй давталтаар буцааж, давталтын хооронд төлөвөө хадгалах ёстой (R-д сүүлийнх нь хаалтыг ашиглан хамгийн энгийн аргаар хэрэгждэг). Өмнө нь давталт дотор R массивыг numpy массив болгон хувиргах шаардлагатай байсан боловч багцын одоогийн хувилбар керас өөрөө хийдэг.
Сургалт, баталгаажуулалтын өгөгдөлд зориулсан давталт нь дараах байдалтай болсон.
Сургалт, баталгаажуулалтын өгөгдөлд зориулсан давталт
train_generator <- function(db_connection = con,
samples_index,
num_classes = 340,
batch_size = 32,
scale = 1,
color = FALSE,
imagenet_preproc = FALSE) {
# Проверка аргументов
checkmate::assert_class(con, "DBIConnection")
checkmate::assert_integerish(samples_index)
checkmate::assert_count(num_classes)
checkmate::assert_count(batch_size)
checkmate::assert_number(scale, lower = 0.001, upper = 5)
checkmate::assert_flag(color)
checkmate::assert_flag(imagenet_preproc)
# Перемешиваем, чтобы брать и удалять использованные индексы батчей по порядку
dt <- data.table::data.table(id = sample(samples_index))
# Проставляем номера батчей
dt[, batch := (.I - 1L) %/% batch_size + 1L]
# Оставляем только полные батчи и индексируем
dt <- dt[, if (.N == batch_size) .SD, keyby = batch]
# Устанавливаем счётчик
i <- 1
# Количество батчей
max_i <- dt[, max(batch)]
# Подготовка выражения для выгрузки
sql <- sprintf(
"PREPARE SELECT drawing, label_int FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ",")
)
res <- DBI::dbSendQuery(con, sql)
# Аналог keras::to_categorical
to_categorical <- function(x, num) {
n <- length(x)
m <- numeric(n * num)
m[x * n + seq_len(n)] <- 1
dim(m) <- c(n, num)
return(m)
}
# Замыкание
function() {
# Начинаем новую эпоху
if (i > max_i) {
dt[, id := sample(id)]
data.table::setkey(dt, batch)
# Сбрасываем счётчик
i <<- 1
max_i <<- dt[, max(batch)]
}
# ID для выгрузки данных
batch_ind <- dt[batch == i, id]
# Выгрузка данных
batch <- DBI::dbFetch(DBI::dbBind(res, as.list(batch_ind)), n = -1)
# Увеличиваем счётчик
i <<- i + 1
# Парсинг JSON и подготовка массива
batch_x <- cpp_process_json_vector(batch$drawing, scale = scale, color = color)
if (imagenet_preproc) {
# Шкалирование c интервала [0, 1] на интервал [-1, 1]
batch_x <- (batch_x - 0.5) * 2
}
batch_y <- to_categorical(batch$label_int, num_classes)
result <- list(batch_x, batch_y)
return(result)
}
}
Функц нь мэдээллийн сантай холбогдсон хувьсагч, ашигласан мөрийн тоо, ангийн тоо, багцын хэмжээ, масштаб (scale = 1
256x256 пикселийн дүрсийг үзүүлэхэд тохирно, scale = 0.5
- 128x128 пиксел), өнгөт заагч (color = FALSE
ашиглах үед саарал өнгөөр дүрслэхийг зааж өгдөг color = TRUE
цус харвалт бүрийг шинэ өнгөөр зурсан) болон imagenet дээр урьдчилан бэлтгэгдсэн сүлжээнүүдийн урьдчилсан боловсруулалтын үзүүлэлт. Сүүлийнх нь пикселийн утгыг [0, 1] интервалаас [-1, 1] интервал хүртэл хэмжихэд шаардлагатай бөгөөд үүнийг нийлүүлсэн төхөөрөмжийг сургах үед ашигладаг байсан. керас загварууд.
Гадаад функц нь аргументийн төрлийг шалгах, хүснэгтийг агуулдаг data.table
-аас санамсаргүй холилдсон мөрийн дугаартай samples_index
ба багцын дугаар, тоологч ба хамгийн их багцын тоо, мөн мэдээллийн сангаас өгөгдлийг буулгах SQL илэрхийлэл. Нэмж дурдахад бид доторх функцийн хурдан аналогийг тодорхойлсон keras::to_categorical()
. Бид бараг бүх өгөгдлийг сургалтанд ашигласан бөгөөд баталгаажуулалтад хагас хувийг үлдээсэн тул эрин үеийн хэмжээг параметрээр хязгаарласан. steps_per_epoch
дуудсан үед keras::fit_generator()
, нөхцөл байдал if (i > max_i)
зөвхөн баталгаажуулалтын давталт дээр ажилласан.
Дотоод функцэд мөрийн индексүүдийг дараагийн багцад авах, багц тоологч нэмэгдэхийн хамт өгөгдлийн сангаас бичлэгүүдийг буулгах, JSON задлан шинжлэх (функц) cpp_process_json_vector()
, C++ хэл дээр бичигдсэн) болон зурагт тохирсон массив үүсгэх. Дараа нь ангиллын шошготой нэг халуун векторуудыг үүсгэж, пикселийн утгатай массивууд болон шошго бүхий жагсаалтад нэгтгэгддэг бөгөөд энэ нь буцах утга юм. Ажлыг хурдасгахын тулд бид хүснэгтэд индекс үүсгэх аргыг ашигласан data.table
холбоосоор дамжуулан өөрчлөх - эдгээр багц "чипүүд"гүйгээр мэдээлэл.хүснэгт R-д ямар нэгэн их хэмжээний өгөгдөлтэй үр дүнтэй ажиллахыг төсөөлөхөд хэцүү байдаг.
Core i5 зөөврийн компьютер дээрх хурд хэмжилтийн үр дүн дараах байдалтай байна.
Давталтын жишиг
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)
Хэрэв танд хангалттай хэмжээний RAM байгаа бол та өгөгдлийн сангийн үйл ажиллагааг ижил RAM руу шилжүүлэх замаар хурдасгах боломжтой (бидний даалгаварт 32 ГБ хангалттай). Линукс дээр хуваалтыг анхдагчаар суулгадаг /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. Загварын архитектурын сонголт
Анхны архитектурыг ашигласан (batch, height, width, 3)
, өөрөөр хэлбэл сувгийн тоог өөрчлөх боломжгүй. Python-д ийм хязгаарлалт байхгүй тул бид анхны нийтлэлийн дагуу (keras хувилбарт байгаа завсарлагагүйгээр) энэхүү архитектурын хэрэгжилтийг яаравчлан бичсэн.
Mobilenet v1 архитектур
library(keras)
top_3_categorical_accuracy <- custom_metric(
name = "top_3_categorical_accuracy",
metric_fn = function(y_true, y_pred) {
metric_top_k_categorical_accuracy(y_true, y_pred, k = 3)
}
)
layer_sep_conv_bn <- function(object,
filters,
alpha = 1,
depth_multiplier = 1,
strides = c(2, 2)) {
# NB! depth_multiplier != resolution multiplier
# https://github.com/keras-team/keras/issues/10349
layer_depthwise_conv_2d(
object = object,
kernel_size = c(3, 3),
strides = strides,
padding = "same",
depth_multiplier = depth_multiplier
) %>%
layer_batch_normalization() %>%
layer_activation_relu() %>%
layer_conv_2d(
filters = filters * alpha,
kernel_size = c(1, 1),
strides = c(1, 1)
) %>%
layer_batch_normalization() %>%
layer_activation_relu()
}
get_mobilenet_v1 <- function(input_shape = c(224, 224, 1),
num_classes = 340,
alpha = 1,
depth_multiplier = 1,
optimizer = optimizer_adam(lr = 0.002),
loss = "categorical_crossentropy",
metrics = c("categorical_crossentropy",
top_3_categorical_accuracy)) {
inputs <- layer_input(shape = input_shape)
outputs <- inputs %>%
layer_conv_2d(filters = 32, kernel_size = c(3, 3), strides = c(2, 2), padding = "same") %>%
layer_batch_normalization() %>%
layer_activation_relu() %>%
layer_sep_conv_bn(filters = 64, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 128, strides = c(2, 2)) %>%
layer_sep_conv_bn(filters = 128, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 256, strides = c(2, 2)) %>%
layer_sep_conv_bn(filters = 256, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(2, 2)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 512, strides = c(1, 1)) %>%
layer_sep_conv_bn(filters = 1024, strides = c(2, 2)) %>%
layer_sep_conv_bn(filters = 1024, strides = c(1, 1)) %>%
layer_global_average_pooling_2d() %>%
layer_dense(units = num_classes) %>%
layer_activation_softmax()
model <- keras_model(
inputs = inputs,
outputs = outputs
)
model %>% compile(
optimizer = optimizer,
loss = loss,
metrics = metrics
)
return(model)
}
Энэ аргын сул тал нь ойлгомжтой. Би маш олон загвар туршиж үзэхийг хүсч байгаа ч эсрэгээрээ архитектур бүрийг гараар дахин бичихийг хүсэхгүй байна. Мөн бид 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)
Одоо нийлүүлсэн аль нэгийг нь авахын тулд бүх нийтийн функцийг бичих нь тийм ч хэцүү биш юм керас Imagenet дээр бэлтгэгдсэн жинтэй эсвэл жингүй загварууд:
Бэлэн архитектурыг ачаалах функц
get_model <- function(name = "mobilenet_v2",
input_shape = NULL,
weights = "imagenet",
pooling = "avg",
num_classes = NULL,
optimizer = keras::optimizer_adam(lr = 0.002),
loss = "categorical_crossentropy",
metrics = NULL,
color = TRUE,
compile = FALSE) {
# Проверка аргументов
checkmate::assert_string(name)
checkmate::assert_integerish(input_shape, lower = 1, upper = 256, len = 3)
checkmate::assert_count(num_classes)
checkmate::assert_flag(color)
checkmate::assert_flag(compile)
# Получаем объект из пакета keras
model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
# Проверка наличия объекта в пакете
if (is.null(model_fun)) {
stop("Model ", shQuote(name), " not found.", call. = FALSE)
}
base_model <- model_fun(
input_shape = input_shape,
include_top = FALSE,
weights = weights,
pooling = pooling
)
# Если изображение не цветное, меняем размерность входа
if (!color) {
base_model_conf <- keras::get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- keras::from_config(base_model_conf)
}
predictions <- keras::get_layer(base_model, "global_average_pooling2d_1")$output
predictions <- keras::layer_dense(predictions, units = num_classes, activation = "softmax")
model <- keras::keras_model(
inputs = base_model$input,
outputs = predictions
)
if (compile) {
keras::compile(
object = model,
optimizer = optimizer,
loss = loss,
metrics = metrics
)
}
return(model)
}
Нэг сувгийн зургийг ашиглахдаа урьдчилан бэлтгэсэн жинг ашигладаггүй. Үүнийг засах боломжтой: функцийг ашиглан get_weights()
загварын жинг R массивын жагсаалт хэлбэрээр авах, энэ жагсаалтын эхний элементийн хэмжээсийг өөрчлөх (нэг өнгөний сувгийг авах эсвэл гурвыг нь дундажлах замаар), дараа нь жинг функцээр загварт буцааж ачаална уу. set_weights()
. Бид энэ функцийг хэзээ ч нэмээгүй, учир нь энэ үе шатанд өнгөт зурагтай ажиллах нь илүү үр дүнтэй болох нь тодорхой болсон.
Бид ихэнх туршилтыг mobilenet 1 ба 2 хувилбар, мөн resnet34 ашиглан хийсэн. SE-ResNeXt гэх мэт илүү орчин үеийн архитектурууд энэ тэмцээнд сайн ажилласан. Харамсалтай нь, бидний мэдэлд бэлэн хэрэгжүүлэлт байхгүй байсан бөгөөд бид өөрсдөө бичээгүй (гэхдээ бид заавал бичих болно).
5. Скриптүүдийг параметржүүлэх
Тохиромжтой болгох үүднээс сургалт эхлэх бүх кодыг нэг скрипт хэлбэрээр зохион бүтээсэн бөгөөд параметрүүдийг ашиглан тохируулсан болно
doc <- '
Usage:
train_nn.R --help
train_nn.R --list-models
train_nn.R [options]
Options:
-h --help Show this message.
-l --list-models List available models.
-m --model=<model> Neural network model name [default: mobilenet_v2].
-b --batch-size=<size> Batch size [default: 32].
-s --scale-factor=<ratio> Scale factor [default: 0.5].
-c --color Use color lines [default: FALSE].
-d --db-dir=<path> Path to database directory [default: Sys.getenv("db_dir")].
-r --validate-ratio=<ratio> Validate sample ratio [default: 0.995].
-n --n-gpu=<number> Number of GPUs [default: 1].
'
args <- docopt::docopt(doc)
Багц docopt хэрэгжилтийг илэрхийлж байна Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db
буюу ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db
, хэрэв файл train_nn.R
гүйцэтгэх боломжтой (энэ тушаал нь загварыг сургаж эхэлнэ resnet50
128x128 пикселийн хэмжээтэй гурван өнгийн зураг дээр мэдээллийн сан нь хавтсанд байрлах ёстой. /home/andrey/doodle_db
). Та сургалтын хурд, оновчтой болгох төрөл болон бусад тохируулж болох параметрүүдийг жагсаалтад нэмж болно. Хэвлэлийг бэлтгэх явцад архитектур нь тодорхой болсон mobilenet_v2
одоогийн хувилбараас керас R-д ашиглах
Энэхүү арга нь RStudio дахь скриптүүдийн уламжлалт хувилбартай харьцуулахад янз бүрийн загваруудын туршилтыг мэдэгдэхүйц хурдасгах боломжийг олгосон (бид багцыг боломжит хувилбар гэж тэмдэглэж байна.
6. Скриптүүдийг докержуулах
Бид Docker-ийг багийн гишүүдийн хооронд загвар сургах, үүлэн дээр хурдан байршуулах орчныг зөөвөрлөхөд ашигласан. Та R програмистын хувьд ер бусын энэ хэрэгсэлтэй танилцаж эхлэх боломжтой
Docker танд өөрийн зургийг эхнээс нь үүсгэх, бусад зургуудыг өөрийн болгох үндэс болгон ашиглах боломжийг олгодог. Боломжтой сонголтуудыг шинжлэхдээ бид NVIDIA, CUDA+cuDNN драйверууд болон Python сангуудыг суулгах нь зургийн нэлээд том хэсэг юм гэсэн дүгнэлтэд хүрсэн бөгөөд бид албан ёсны зургийг үндэс болгон авахаар шийдсэн. tensorflow/tensorflow:1.12.0-gpu
, шаардлагатай R багцуудыг тэнд нэмнэ.
Эцсийн докер файл дараах байдалтай байв.
Докер файл
FROM tensorflow/tensorflow:1.12.0-gpu
MAINTAINER Artem Klevtsov <[email protected]>
SHELL ["/bin/bash", "-c"]
ARG LOCALE="en_US.UTF-8"
ARG APT_PKG="libopencv-dev r-base r-base-dev littler"
ARG R_BIN_PKG="futile.logger checkmate data.table rcpp rapidjsonr dbi keras jsonlite curl digest remotes"
ARG R_SRC_PKG="xtensor RcppThread docopt MonetDBLite"
ARG PY_PIP_PKG="keras"
ARG DIRS="/db /app /app/data /app/models /app/logs"
RUN source /etc/os-release &&
echo "deb https://cloud.r-project.org/bin/linux/ubuntu ${UBUNTU_CODENAME}-cran35/" > /etc/apt/sources.list.d/cran35.list &&
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E084DAB9 &&
add-apt-repository -y ppa:marutter/c2d4u3.5 &&
add-apt-repository -y ppa:timsc/opencv-3.4 &&
apt-get update &&
apt-get install -y locales &&
locale-gen ${LOCALE} &&
apt-get install -y --no-install-recommends ${APT_PKG} &&
ln -s /usr/lib/R/site-library/littler/examples/install.r /usr/local/bin/install.r &&
ln -s /usr/lib/R/site-library/littler/examples/install2.r /usr/local/bin/install2.r &&
ln -s /usr/lib/R/site-library/littler/examples/installGithub.r /usr/local/bin/installGithub.r &&
echo 'options(Ncpus = parallel::detectCores())' >> /etc/R/Rprofile.site &&
echo 'options(repos = c(CRAN = "https://cloud.r-project.org"))' >> /etc/R/Rprofile.site &&
apt-get install -y $(printf "r-cran-%s " ${R_BIN_PKG}) &&
install.r ${R_SRC_PKG} &&
pip install ${PY_PIP_PKG} &&
mkdir -p ${DIRS} &&
chmod 777 ${DIRS} &&
rm -rf /tmp/downloaded_packages/ /tmp/*.rds &&
rm -rf /var/lib/apt/lists/*
COPY utils /app/utils
COPY src /app/src
COPY tests /app/tests
COPY bin/*.R /app/
ENV DBDIR="/db"
ENV CUDA_HOME="/usr/local/cuda"
ENV PATH="/app:${PATH}"
WORKDIR /app
VOLUME /db
VOLUME /app
CMD bash
Тохиромжтой болгохын тулд ашигласан багцуудыг хувьсагчид оруулсан; Бичсэн скриптүүдийн дийлэнх хэсгийг угсрах явцад савны дотор хуулж авдаг. Бид мөн тушаалын бүрхүүлийг өөрчилсөн /bin/bash
агуулгыг ашиглахад хялбар болгох үүднээс /etc/os-release
. Энэ нь кодонд үйлдлийн системийн хувилбарыг зааж өгөх шаардлагагүй болсон.
Нэмж дурдахад, янз бүрийн команд бүхий контейнер ажиллуулах боломжийг олгодог жижиг 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 ашиглах
Тэмцээний нэг онцлог нь маш их чимээ шуугиантай өгөгдөл байсан (ODS slack-аас @Leigh.plt-аас зээлсэн гарчгийн зургийг үзнэ үү). Их хэмжээний багц нь үүнтэй тэмцэхэд тусалдаг бөгөөд 1 GPU бүхий компьютер дээр туршилт хийсний дараа бид үүлэн доторх хэд хэдэн GPU дээр сургалтын загваруудыг эзэмшихээр шийдсэн. Ашигласан GoogleCloud (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. Дүгнэлтийн оронд
Бидний тулгарсан хэд хэдэн бэрхшээлийг даван туулж чадаагүй байна:
- в керас Сургалтын оновчтой хурдыг автоматаар хайхад бэлэн функц байхгүй (аналог
lr_finder
номын санд байна хурдан.ai); Зарим хүчин чармайлтаар гуравдагч этгээдийн хэрэгжүүлэлтийг R руу шилжүүлэх боломжтой, жишээлбэл,энэ нь ; - өмнөх цэгийн үр дүнд хэд хэдэн GPU ашиглах үед сургалтын зөв хурдыг сонгох боломжгүй байсан;
- орчин үеийн мэдрэлийн сүлжээний архитектур, ялангуяа imagenet дээр урьдчилан бэлтгэгдсэн бүтэц дутагдалтай байна;
- хэн ч мөчлөгийн бодлого, ялгаварлан гадуурхах сургалтын хувь хэмжээ (косинусыг нөхөх нь бидний хүсэлтээр хийгдсэн).
хэрэгжүүлсэн , Баярлалааскайдан ).
Энэ тэмцээнээс ямар хэрэгтэй зүйл сурсан бэ:
- Харьцангуй бага хүчин чадалтай техник хангамж дээр та хангалттай (RAM-аас олон дахин их) хэмжээний өгөгдөлтэй ажиллах боломжтой. Гялгар уут мэдээлэл.хүснэгт Хүснэгтүүдийг газар дээр нь өөрчилснөөр санах ойг хэмнэж, тэдгээрийг хуулахаас зайлсхийдэг бөгөөд зөв ашиглавал түүний чадвар нь скрипт хэл дээр бидний мэддэг бүх хэрэгслүүдийн дунд бараг үргэлж хамгийн өндөр хурдыг харуулдаг. Өгөгдлийн санд өгөгдлийг хадгалах нь ихэнх тохиолдолд өгөгдлийн багцыг бүхэлд нь RAM-д шахах хэрэгцээний талаар огт бодохгүй байх боломжийг олгодог.
- R хэл дээрх удаан функцийг багцыг ашиглан C++ хэл дээрх хурдан функцээр сольж болно Rcpp. Хэрэв хэрэглэхээс гадна RcppThread буюу RcppParallel, бид хөндлөн платформын олон урсгалтай хэрэгжилтийг авдаг тул R түвшинд кодыг зэрэгцүүлэх шаардлагагүй болно.
- Багц Rcpp C++-ийн талаар ноцтой мэдлэггүйгээр ашиглаж болно, шаардлагатай доод хэмжээг тодорхойлсон
энд . зэрэг хэд хэдэн гайхалтай C-номын сангийн толгой файлууд кстенсор CRAN дээр ашиглах боломжтой, өөрөөр хэлбэл бэлэн өндөр гүйцэтгэлтэй C++ кодыг R-д нэгтгэх төслүүдийг хэрэгжүүлэх дэд бүтэц бий болж байна. Нэмэлт тав тухтай байдал нь синтаксийг тодруулах, RStudio дахь статик C++ кодын анализатор юм. - docopt параметр бүхий бие даасан скриптүүдийг ажиллуулах боломжийг танд олгоно. Энэ нь алсын сервер дээр ашиглахад тохиромжтой, үүнд. докерын дор. RStudio-д мэдрэлийн сүлжээг сургах олон цагийн туршилт хийх нь тохиромжгүй бөгөөд сервер дээр IDE суулгах нь үргэлж зөвтгөгддөггүй.
- Docker нь үйлдлийн систем болон номын сангуудын өөр өөр хувилбар бүхий хөгжүүлэгчдийн хооронд код зөөвөрлөх, үр дүнг дахин гаргах, сервер дээр ажиллахад хялбар байдлыг баталгаажуулдаг. Та зөвхөн нэг тушаалаар бүх сургалтын шугамыг эхлүүлж болно.
- Google Cloud нь үнэтэй техник хангамж дээр туршилт хийх төсөвт ээлтэй арга боловч та тохиргоог анхааралтай сонгох хэрэгтэй.
- Тусдаа кодын фрагментуудын хурдыг хэмжих нь ялангуяа R ба C++ болон багцтай хослуулах үед маш ашигтай байдаг. вандан - бас маш хялбар.
Ерөнхийдөө энэ туршлага маш их үр өгөөжтэй байсан бөгөөд бид хөндөгдсөн зарим асуудлыг шийдвэрлэхээр үргэлжлүүлэн ажиллаж байна.
Эх сурвалж: www.habr.com