ProHoster > Blog > yönetim > Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur
Hızlı Çizim Doodle Tanıma: R, C++ ve sinir ağlarıyla nasıl arkadaş olunur
Ey Habr!
Geçen sonbaharda Kaggle, elle çizilmiş resimleri sınıflandırmak için Quick Draw Doodle Recognition adlı bir yarışmaya ev sahipliği yaptı; bu yarışmaya diğerlerinin yanı sıra R-bilim adamlarından oluşan bir ekip de katıldı: Artem Klevtsova, Philippa Yönetici и Andrey Ogurtsov. Yarışmayı ayrıntılı olarak anlatmayacağız; bu zaten yapıldı. son yayın.
Bu sefer madalya yetiştirmede işler yürümedi, ancak çok sayıda değerli deneyim kazanıldı, bu yüzden topluluğa Kagle ve günlük işlerdeki en ilginç ve faydalı şeylerden birkaçını anlatmak istiyorum. Tartışılan konular arasında: onsuz zor yaşam OpenCV, JSON ayrıştırma (bu örnekler, C++ kodunun R'deki komut dosyalarına veya paketlere entegrasyonunu inceler. Rcpp), komut dosyalarının parametrelendirilmesi ve son çözümün dockerizasyonu. Mesajdaki tüm kodlar yürütmeye uygun bir biçimde mevcuttur. depolar.
1. Verileri CSV'den MonetDB veritabanına verimli bir şekilde yükleyin
Bu yarışmadaki veriler hazır görseller şeklinde değil, nokta koordinatlı JSON'ları içeren 340 CSV dosyası (her sınıf için bir dosya) şeklinde sağlanmaktadır. Bu noktaları çizgilerle birleştirerek 256x256 piksel boyutunda son bir görüntü elde ediyoruz. Ayrıca her kayıt için, veri kümesinin toplandığı sırada kullanılan sınıflandırıcı tarafından resmin doğru şekilde tanınıp tanınmadığını belirten bir etiket, resmin yazarının ikamet ettiği ülkenin iki harfli kodu, benzersiz bir tanımlayıcı, bir zaman damgası bulunur. ve dosya adıyla eşleşen bir sınıf adı. Orijinal verilerin basitleştirilmiş bir versiyonu arşivde 7.4 GB ağırlığında ve paket açıldıktan sonra yaklaşık 20 GB ağırlığında olup, paket açıldıktan sonra tam veri 240 GB yer kaplar. Organizatörler her iki versiyonun da aynı çizimleri üretmesini sağladılar, bu da tam versiyonun gereksiz olduğu anlamına geliyordu. Her durumda, 50 milyon görüntüyü grafik dosyalarında veya diziler biçiminde depolamanın hemen kârsız olduğu düşünüldü ve arşivdeki tüm CSV dosyalarını birleştirmeye karar verdik. train_simplified.zip Her parti için gerekli boyuttaki görüntülerin sonraki nesilleri ile birlikte veritabanına "anında" gönderilir.
DBMS olarak kanıtlanmış bir sistem seçildi MonetDByani R'nin paket olarak uygulanması MonetDBLite. Paket, veritabanı sunucusunun yerleşik bir sürümünü içerir ve sunucuyu doğrudan bir R oturumundan alıp orada çalışmanıza olanak tanır. Bir veritabanı oluşturmak ve ona bağlanmak tek bir komutla gerçekleştirilir:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
İki tablo oluşturmamız gerekecek: biri tüm veriler için, diğeri indirilen dosyalar hakkındaki hizmet bilgileri için (bir şeyler ters giderse ve birkaç dosya indirildikten sonra işlemin devam ettirilmesi gerekiyorsa kullanışlıdır):
Veritabanına veri yüklemenin en hızlı yolu CSV dosyalarını SQL - komutunu kullanarak doğrudan kopyalamaktı COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTNerede tablename - tablo adı ve path - dosyanın yolu. Arşivle çalışırken yerleşik uygulamanın olduğu keşfedildi unzip R'de arşivdeki bazı dosyalarla düzgün çalışmıyor, bu yüzden sistemi kullandık unzip (parametreyi kullanarak getOption("unzip")).
Veritabanına yazma fonksiyonu
#' @title Извлечение и загрузка файлов
#'
#' @description
#' Извлечение CSV-файлов из ZIP-архива и загрузка их в базу данных
#'
#' @param con Объект подключения к базе данных (класс `MonetDBEmbeddedConnection`).
#' @param tablename Название таблицы в базе данных.
#' @oaram zipfile Путь к ZIP-архиву.
#' @oaram filename Имя файла внури ZIP-архива.
#' @param preprocess Функция предобработки, которая будет применена извлечённому файлу.
#' Должна принимать один аргумент `data` (объект `data.table`).
#'
#' @return `TRUE`.
#'
upload_file <- function(con, tablename, zipfile, filename, preprocess = NULL) {
# Проверка аргументов
checkmate::assert_class(con, "MonetDBEmbeddedConnection")
checkmate::assert_string(tablename)
checkmate::assert_string(filename)
checkmate::assert_true(DBI::dbExistsTable(con, tablename))
checkmate::assert_file_exists(zipfile, access = "r", extension = "zip")
checkmate::assert_function(preprocess, args = c("data"), null.ok = TRUE)
# Извлечение файла
path <- file.path(tempdir(), filename)
unzip(zipfile, files = filename, exdir = tempdir(),
junkpaths = TRUE, unzip = getOption("unzip"))
on.exit(unlink(file.path(path)))
# Применяем функция предобработки
if (!is.null(preprocess)) {
.data <- data.table::fread(file = path)
.data <- preprocess(data = .data)
data.table::fwrite(x = .data, file = path, append = FALSE)
rm(.data)
}
# Запрос к БД на импорт CSV
sql <- sprintf(
"COPY OFFSET 2 INTO %s FROM '%s' USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORT",
tablename, path
)
# Выполнение запроса к БД
DBI::dbExecute(con, sql)
# Добавление записи об успешной загрузке в служебную таблицу
DBI::dbExecute(con, sprintf("INSERT INTO upload_log(file_name, uploaded) VALUES('%s', true)",
filename))
return(invisible(TRUE))
}
# Список файлов для записи
files <- unzip(zipfile, list = TRUE)$Name
# Список исключений, если часть файлов уже была загружена
to_skip <- DBI::dbGetQuery(con, "SELECT file_name FROM upload_log")[[1L]]
files <- setdiff(files, to_skip)
if (length(files) > 0L) {
# Запускаем таймер
tictoc::tic()
# Прогресс бар
pb <- txtProgressBar(min = 0L, max = length(files), style = 3)
for (i in seq_along(files)) {
upload_file(con = con, tablename = "doodles",
zipfile = zipfile, filename = files[i])
setTxtProgressBar(pb, i)
}
close(pb)
# Останавливаем таймер
tictoc::toc()
}
# 526.141 sec elapsed - копирование SSD->SSD
# 558.879 sec elapsed - копирование USB->SSD
Veri yükleme süresi, kullanılan sürücünün hız özelliklerine bağlı olarak değişebilir. Bizim durumumuzda, bir SSD içinde veya bir flash sürücüden (kaynak dosya) bir SSD'ye (DB) okuma ve yazma işlemi 10 dakikadan az sürer.
Tamsayı sınıfı etiketine ve dizin sütununa sahip bir sütun oluşturmak birkaç saniye daha sürer (ORDERED INDEX) gruplar oluşturulurken gözlemlerin örnekleneceği satır numaralarıyla birlikte:
Ek Sütun ve Dizin Oluşturma
message("Generate lables")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD label_int int"))
invisible(DBI::dbExecute(con, "UPDATE doodles SET label_int = dense_rank() OVER (ORDER BY word) - 1"))
message("Generate row numbers")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD id serial"))
invisible(DBI::dbExecute(con, "CREATE ORDERED INDEX doodles_id_ord_idx ON doodles(id)"))
Anında toplu iş oluşturma sorununu çözmek için, tablodan rastgele satırları çıkarma konusunda maksimum hıza ulaşmamız gerekiyordu. doodles. Bunun için 3 numara kullandık. Bunlardan ilki, gözlem kimliğini saklayan türün boyutluluğunu azaltmaktı. Orijinal veri setinde kimliği saklamak için gereken tür bigint, ancak gözlemlerin sayısı sıra numarasına eşit tanımlayıcılarının türe sığmasını mümkün kılar int. Bu durumda arama çok daha hızlıdır. İkinci numara kullanmaktı ORDERED INDEX — bu karara, mevcut tüm yöntemleri inceledikten sonra ampirik olarak ulaştık. seçenekleri. Üçüncüsü parametreli sorgular kullanmaktı. Yöntemin özü, komutu bir kez yürütmektir PREPARE aynı türden bir grup sorgu oluştururken hazırlanmış bir ifadenin daha sonra kullanılmasıyla, ancak aslında basit bir ifadeyle karşılaştırıldığında bir avantaj vardır SELECT istatistiksel hata aralığında olduğu ortaya çıktı.
Veri yükleme işlemi 450 MB'tan fazla RAM tüketmez. Yani açıklanan yaklaşım, onlarca gigabayt ağırlığındaki veri kümelerini, bazı tek kartlı cihazlar da dahil olmak üzere hemen hemen her bütçeye uygun donanımda taşımanıza olanak tanır ki bu oldukça harika.
Geriye kalan tek şey, (rastgele) veri alma hızını ölçmek ve farklı boyutlardaki partileri numune alırken ölçeklendirmeyi değerlendirmektir:
Veritabanı karşılaştırması
library(ggplot2)
set.seed(0)
# Подключение к базе данных
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
# Функция для подготовки запроса на стороне сервера
prep_sql <- function(batch_size) {
sql <- sprintf("PREPARE SELECT id FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ","))
res <- DBI::dbSendQuery(con, sql)
return(res)
}
# Функция для извлечения данных
fetch_data <- function(rs, batch_size) {
ids <- sample(seq_len(n), batch_size)
res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
return(res)
}
# Проведение замера
res_bench <- bench::press(
batch_size = 2^(4:10),
{
rs <- prep_sql(batch_size)
bench::mark(
fetch_data(rs, batch_size),
min_iterations = 50L
)
}
)
# Параметры бенчмарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]
# batch_size min median max `itr/sec` total_time n_itr
# <dbl> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 16 23.6ms 54.02ms 93.43ms 18.8 2.6s 49
# 2 32 38ms 84.83ms 151.55ms 11.4 4.29s 49
# 3 64 63.3ms 175.54ms 248.94ms 5.85 8.54s 50
# 4 128 83.2ms 341.52ms 496.24ms 3.00 16.69s 50
# 5 256 232.8ms 653.21ms 847.44ms 1.58 31.66s 50
# 6 512 784.6ms 1.41s 1.98s 0.740 1.1m 49
# 7 1024 681.7ms 2.72s 4.06s 0.377 2.16m 49
ggplot(res_bench, aes(x = factor(batch_size), y = median, group = 1)) +
geom_point() +
geom_line() +
ylab("median time, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE)
2. Grupların hazırlanması
Parti hazırlama sürecinin tamamı aşağıdaki adımlardan oluşur:
Nokta koordinatlarına sahip dize vektörleri içeren birkaç JSON'un ayrıştırılması.
Gerekli boyuttaki bir görüntü üzerindeki noktaların koordinatlarına göre renkli çizgiler çizmek (örneğin, 256×256 veya 128×128).
Ortaya çıkan görüntülerin tensöre dönüştürülmesi.
Python çekirdekleri arasındaki rekabetin bir parçası olarak sorun öncelikle Python çekirdekleri kullanılarak çözüldü. OpenCV. R'deki en basit ve en belirgin analoglardan biri şöyle görünecektir:
R'de JSON'u Tensör Dönüşümüne Uygulamak
r_process_json_str <- function(json, line.width = 3,
color = TRUE, scale = 1) {
# Парсинг JSON
coords <- jsonlite::fromJSON(json, simplifyMatrix = FALSE)
tmp <- tempfile()
# Удаляем временный файл по завершению функции
on.exit(unlink(tmp))
png(filename = tmp, width = 256 * scale, height = 256 * scale, pointsize = 1)
# Пустой график
plot.new()
# Размер окна графика
plot.window(xlim = c(256 * scale, 0), ylim = c(256 * scale, 0))
# Цвета линий
cols <- if (color) rainbow(length(coords)) else "#000000"
for (i in seq_along(coords)) {
lines(x = coords[[i]][[1]] * scale, y = coords[[i]][[2]] * scale,
col = cols[i], lwd = line.width)
}
dev.off()
# Преобразование изображения в 3-х мерный массив
res <- png::readPNG(tmp)
return(res)
}
r_process_json_vector <- function(x, ...) {
res <- lapply(x, r_process_json_str, ...)
# Объединение 3-х мерных массивов картинок в 4-х мерный в тензор
res <- do.call(abind::abind, c(res, along = 0))
return(res)
}
Çizim, standart R araçları kullanılarak gerçekleştirilir ve RAM'de depolanan geçici bir PNG'ye kaydedilir (Linux'ta, geçici R dizinleri dizinde bulunur) /tmp, RAM'e monte edilmiştir). Bu dosya daha sonra 0 ile 1 arasında değişen sayıların yer aldığı üç boyutlu bir dizi olarak okunur. Bu önemlidir çünkü daha geleneksel bir BMP, onaltılık renk kodlarına sahip ham bir diziye okunacaktır.
Büyük partilerin oluşturulması çok uzun zaman aldığından bu uygulama bizim için yetersiz göründü ve güçlü bir kütüphane kullanarak meslektaşlarımızın deneyimlerinden yararlanmaya karar verdik. OpenCV. O zamanlar R için hazır bir paket yoktu (şu anda yok), bu nedenle gerekli işlevselliğin minimal bir uygulaması, R koduna entegrasyonla C++ ile yazıldı. Rcpp.
Sorunu çözmek için aşağıdaki paketler ve kütüphaneler kullanıldı:
OpenCV görüntülerle çalışmak ve çizgiler çizmek için. Önceden yüklenmiş sistem kitaplıkları ve başlık dosyalarının yanı sıra dinamik bağlantı da kullanılır.
uzatıcı çok boyutlu diziler ve tensörlerle çalışmak için. Aynı isimli R paketinde yer alan başlık dosyalarını kullandık. Kitaplık, çok boyutlu dizilerle hem ana satır hem de sütun ana sırasına göre çalışmanıza olanak tanır.
ndjson JSON'u ayrıştırmak için. Bu kütüphane şu amaçlarla kullanılır: uzatıcı projede mevcutsa otomatik olarak.
Rcpp Konusu JSON'dan bir vektörün çok iş parçacıklı işlenmesini düzenlemek için. Bu paket tarafından sağlanan başlık dosyaları kullanıldı. Daha popüler olanlardan RcppParalel Paket, diğer şeylerin yanı sıra yerleşik bir döngü kesme mekanizmasına sahiptir.
Bu unutulmamalıdır ki uzatıcı bir nimet olduğu ortaya çıktı: kapsamlı işlevsellik ve yüksek performansa sahip olmasının yanı sıra, geliştiricilerinin oldukça duyarlı olduğu ve soruları hızlı ve ayrıntılı bir şekilde yanıtladığı ortaya çıktı. Onların yardımıyla, OpenCV matrislerinin xtensör tensörlerine dönüşümlerinin yanı sıra 3 boyutlu görüntü tensörlerini doğru boyuttaki 4 boyutlu bir tensörde (topluluğun kendisi) birleştirmenin bir yolunu uygulamak mümkün oldu.
Rcpp, xtensor ve RcppThread'i öğrenmek için materyaller
Sistem dosyalarını ve sistemde kurulu kütüphanelerle dinamik bağlantıyı kullanan dosyaları derlemek için pakette uygulanan eklenti mekanizmasını kullandık. Rcpp. Yolları ve bayrakları otomatik olarak bulmak için popüler bir Linux yardımcı programını kullandık pkg-config.
OpenCV kütüphanesini kullanmak için Rcpp eklentisinin uygulanması
JSON'u ayrıştırmak ve modele aktarım için bir parti oluşturmak için uygulama kodu spoiler altında verilmiştir. Öncelikle başlık dosyalarını aramak için yerel bir proje dizini ekleyin (ndjson için gereklidir):
// [[Rcpp::plugins(cpp14)]]
// [[Rcpp::plugins(opencv)]]
// [[Rcpp::depends(xtensor)]]
// [[Rcpp::depends(RcppThread)]]
#include <xtensor/xjson.hpp>
#include <xtensor/xadapt.hpp>
#include <xtensor/xview.hpp>
#include <xtensor-r/rtensor.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <Rcpp.h>
#include <RcppThread.h>
// Синонимы для типов
using RcppThread::parallelFor;
using json = nlohmann::json;
using points = xt::xtensor<double,2>; // Извлечённые из JSON координаты точек
using strokes = std::vector<points>; // Извлечённые из JSON координаты точек
using xtensor3d = xt::xtensor<double, 3>; // Тензор для хранения матрицы изоображения
using xtensor4d = xt::xtensor<double, 4>; // Тензор для хранения множества изображений
using rtensor3d = xt::rtensor<double, 3>; // Обёртка для экспорта в R
using rtensor4d = xt::rtensor<double, 4>; // Обёртка для экспорта в R
// Статические константы
// Размер изображения в пикселях
const static int SIZE = 256;
// Тип линии
// См. https://en.wikipedia.org/wiki/Pixel_connectivity#2-dimensional
const static int LINE_TYPE = cv::LINE_4;
// Толщина линии в пикселях
const static int LINE_WIDTH = 3;
// Алгоритм ресайза
// https://docs.opencv.org/3.1.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121
const static int RESIZE_TYPE = cv::INTER_LINEAR;
// Шаблон для конвертирования OpenCV-матрицы в тензор
template <typename T, int NCH, typename XT=xt::xtensor<T,3,xt::layout_type::column_major>>
XT to_xt(const cv::Mat_<cv::Vec<T, NCH>>& src) {
// Размерность целевого тензора
std::vector<int> shape = {src.rows, src.cols, NCH};
// Общее количество элементов в массиве
size_t size = src.total() * NCH;
// Преобразование cv::Mat в xt::xtensor
XT res = xt::adapt((T*) src.data, size, xt::no_ownership(), shape);
return res;
}
// Преобразование JSON в список координат точек
strokes parse_json(const std::string& x) {
auto j = json::parse(x);
// Результат парсинга должен быть массивом
if (!j.is_array()) {
throw std::runtime_error("'x' must be JSON array.");
}
strokes res;
res.reserve(j.size());
for (const auto& a: j) {
// Каждый элемент массива должен быть 2-мерным массивом
if (!a.is_array() || a.size() != 2) {
throw std::runtime_error("'x' must include only 2d arrays.");
}
// Извлечение вектора точек
auto p = a.get<points>();
res.push_back(p);
}
return res;
}
// Отрисовка линий
// Цвета HSV
cv::Mat ocv_draw_lines(const strokes& x, bool color = true) {
// Исходный тип матрицы
auto stype = color ? CV_8UC3 : CV_8UC1;
// Итоговый тип матрицы
auto dtype = color ? CV_32FC3 : CV_32FC1;
auto bg = color ? cv::Scalar(0, 0, 255) : cv::Scalar(255);
auto col = color ? cv::Scalar(0, 255, 220) : cv::Scalar(0);
cv::Mat img = cv::Mat(SIZE, SIZE, stype, bg);
// Количество линий
size_t n = x.size();
for (const auto& s: x) {
// Количество точек в линии
size_t n_points = s.shape()[1];
for (size_t i = 0; i < n_points - 1; ++i) {
// Точка начала штриха
cv::Point from(s(0, i), s(1, i));
// Точка окончания штриха
cv::Point to(s(0, i + 1), s(1, i + 1));
// Отрисовка линии
cv::line(img, from, to, col, LINE_WIDTH, LINE_TYPE);
}
if (color) {
// Меняем цвет линии
col[0] += 180 / n;
}
}
if (color) {
// Меняем цветовое представление на RGB
cv::cvtColor(img, img, cv::COLOR_HSV2RGB);
}
// Меняем формат представления на float32 с диапазоном [0, 1]
img.convertTo(img, dtype, 1 / 255.0);
return img;
}
// Обработка JSON и получение тензора с данными изображения
xtensor3d process(const std::string& x, double scale = 1.0, bool color = true) {
auto p = parse_json(x);
auto img = ocv_draw_lines(p, color);
if (scale != 1) {
cv::Mat out;
cv::resize(img, out, cv::Size(), scale, scale, RESIZE_TYPE);
cv::swap(img, out);
out.release();
}
xtensor3d arr = color ? to_xt<double,3>(img) : to_xt<double,1>(img);
return arr;
}
// [[Rcpp::export]]
rtensor3d cpp_process_json_str(const std::string& x,
double scale = 1.0,
bool color = true) {
xtensor3d res = process(x, scale, color);
return res;
}
// [[Rcpp::export]]
rtensor4d cpp_process_json_vector(const std::vector<std::string>& x,
double scale = 1.0,
bool color = false) {
size_t n = x.size();
size_t dim = floor(SIZE * scale);
size_t channels = color ? 3 : 1;
xtensor4d res({n, dim, dim, channels});
parallelFor(0, n, [&x, &res, scale, color](int i) {
xtensor3d tmp = process(x[i], scale, color);
auto view = xt::view(res, i, xt::all(), xt::all(), xt::all());
view = tmp;
});
return res;
}
Bu kod dosyaya yerleştirilmelidir src/cv_xt.cpp ve komutla derleyin Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); iş için de gerekli nlohmann/json.hpp arasında depo. Kod çeşitli işlevlere ayrılmıştır:
to_xt — bir görüntü matrisini dönüştürmek için şablonlanmış bir işlev (cv::Mat) bir tensöre xt::xtensor;
parse_json — işlev bir JSON dizesini ayrıştırır, noktaların koordinatlarını çıkarır ve bunları bir vektöre paketler;
ocv_draw_lines - ortaya çıkan nokta vektöründen çok renkli çizgiler çizer;
process — yukarıdaki işlevleri birleştirir ve aynı zamanda ortaya çıkan görüntüyü ölçeklendirme yeteneğini de ekler;
cpp_process_json_str - fonksiyonun üzerine sarıcı processsonucu bir R nesnesine (çok boyutlu dizi) aktaran;
cpp_process_json_vector - fonksiyonun üzerine sarıcı cpp_process_json_str, bir dize vektörünü çok iş parçacıklı modda işlemenize olanak tanır.
Çok renkli çizgiler çizmek için HSV renk modeli kullanıldı ve ardından RGB'ye dönüştürüldü. Sonucu test edelim:
Görüldüğü gibi hız artışı çok ciddi boyutlarda oldu ve R kodunu paralelleştirerek C++ kodunu yakalamak mümkün değil.
3. Toplu işlerin veritabanından kaldırılması için yineleyiciler
R, RAM'e sığan verileri işleme konusunda haklı bir üne sahiptir; Python ise daha çok yinelemeli veri işlemeyle karakterize edilir ve çekirdek dışı hesaplamaları (harici bellek kullanan hesaplamalar) kolayca ve doğal bir şekilde uygulamanıza olanak tanır. Tanımlanan problem bağlamında bizim için klasik ve ilgili bir örnek, gözlemlerin küçük bir kısmı veya mini parti kullanılarak her adımda gradyanın yaklaşık olarak tahmin edildiği gradyan iniş yöntemiyle eğitilen derin sinir ağlarıdır.
Python'da yazılan derin öğrenme çerçeveleri, verilere dayalı yineleyiciler uygulayan özel sınıflara sahiptir: tablolar, klasörlerdeki resimler, ikili formatlar vb. Hazır seçenekleri kullanabilir veya belirli görevler için kendinizinkini yazabilirsiniz. R'de Python kütüphanesinin tüm özelliklerinden yararlanabiliriz keras Aynı adı taşıyan paketi kullanan çeşitli arka uçlarıyla, bu da paketin üstünde çalışır ağsı. İkincisi ayrı bir uzun makaleyi hak ediyor; yalnızca Python kodunu R'den çalıştırmanıza izin vermekle kalmaz, aynı zamanda gerekli tüm tür dönüşümlerini otomatik olarak gerçekleştirerek nesneleri R ve Python oturumları arasında aktarmanıza da olanak tanır.
MonetDBLite kullanarak tüm verileri RAM'de saklama zorunluluğundan kurtulduk, Python'daki tüm “sinir ağı” işleri orijinal kodla yapılacak, hazır bir şey olmadığı için veriler üzerine sadece bir yineleyici yazmamız gerekiyor. R veya Python'da böyle bir durum için. Aslında bunun için yalnızca iki gereksinim vardır: toplu işlemleri sonsuz bir döngüde döndürmeli ve yinelemeler arasında durumunu kaydetmelidir (ikincisi R'de en basit şekilde kapatmalar kullanılarak uygulanır). Daha önce, R dizilerinin yineleyici içinde açıkça numpy dizilere dönüştürülmesi gerekiyordu, ancak paketin mevcut sürümü keras kendisi yapıyor.
Eğitim ve doğrulama verileri için yineleyicinin aşağıdaki gibi olduğu ortaya çıktı:
Eğitim ve doğrulama verileri için yineleyici
train_generator <- function(db_connection = con,
samples_index,
num_classes = 340,
batch_size = 32,
scale = 1,
color = FALSE,
imagenet_preproc = FALSE) {
# Проверка аргументов
checkmate::assert_class(con, "DBIConnection")
checkmate::assert_integerish(samples_index)
checkmate::assert_count(num_classes)
checkmate::assert_count(batch_size)
checkmate::assert_number(scale, lower = 0.001, upper = 5)
checkmate::assert_flag(color)
checkmate::assert_flag(imagenet_preproc)
# Перемешиваем, чтобы брать и удалять использованные индексы батчей по порядку
dt <- data.table::data.table(id = sample(samples_index))
# Проставляем номера батчей
dt[, batch := (.I - 1L) %/% batch_size + 1L]
# Оставляем только полные батчи и индексируем
dt <- dt[, if (.N == batch_size) .SD, keyby = batch]
# Устанавливаем счётчик
i <- 1
# Количество батчей
max_i <- dt[, max(batch)]
# Подготовка выражения для выгрузки
sql <- sprintf(
"PREPARE SELECT drawing, label_int FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ",")
)
res <- DBI::dbSendQuery(con, sql)
# Аналог keras::to_categorical
to_categorical <- function(x, num) {
n <- length(x)
m <- numeric(n * num)
m[x * n + seq_len(n)] <- 1
dim(m) <- c(n, num)
return(m)
}
# Замыкание
function() {
# Начинаем новую эпоху
if (i > max_i) {
dt[, id := sample(id)]
data.table::setkey(dt, batch)
# Сбрасываем счётчик
i <<- 1
max_i <<- dt[, max(batch)]
}
# ID для выгрузки данных
batch_ind <- dt[batch == i, id]
# Выгрузка данных
batch <- DBI::dbFetch(DBI::dbBind(res, as.list(batch_ind)), n = -1)
# Увеличиваем счётчик
i <<- i + 1
# Парсинг JSON и подготовка массива
batch_x <- cpp_process_json_vector(batch$drawing, scale = scale, color = color)
if (imagenet_preproc) {
# Шкалирование c интервала [0, 1] на интервал [-1, 1]
batch_x <- (batch_x - 0.5) * 2
}
batch_y <- to_categorical(batch$label_int, num_classes)
result <- list(batch_x, batch_y)
return(result)
}
}
İşlev, veri tabanına bağlantısı olan bir değişkeni, kullanılan satır sayısını, sınıf sayısını, parti boyutunu, ölçeği () girdi olarak alır.scale = 1 256x256 piksellik görsellerin oluşturulmasına karşılık gelir, scale = 0.5 — 128x128 piksel), renk göstergesi (color = FALSE kullanıldığında gri tonlamalı görüntü oluşturmayı belirtir color = TRUE her vuruş yeni bir renkte çizilir) ve imagenet'te önceden eğitilmiş ağlar için bir ön işleme göstergesidir. İkincisi, sağlanan piksellerin eğitimi sırasında kullanılan [0, 1] aralığından [-1, 1] aralığına kadar piksel değerlerini ölçeklendirmek için gereklidir. keras modeller.
Harici fonksiyon argüman tipi kontrolünü, bir tabloyu içerir. data.table rastgele karışık satır numaralarıyla samples_index ve toplu iş numaraları, sayaç ve maksimum toplu iş sayısının yanı sıra veritabanından veri kaldırmak için bir SQL ifadesi. Ek olarak, içindeki fonksiyonun hızlı bir analogunu tanımladık. keras::to_categorical(). Neredeyse tüm verileri eğitim için kullandık, yüzde yarımı doğrulama için bıraktık, bu nedenle çağ boyutu parametreyle sınırlıydı steps_per_epoch çağrıldığında keras::fit_generator()ve durum if (i > max_i) yalnızca doğrulama yineleyici için çalıştı.
Dahili fonksiyonda, bir sonraki toplu iş için satır dizinleri alınır, toplu iş sayacı artırılarak kayıtlar veritabanından kaldırılır, JSON ayrıştırma (işlev) cpp_process_json_vector(), C++ ile yazılmış) ve resimlere karşılık gelen diziler oluşturma. Daha sonra sınıf etiketlerine sahip tek sıcak vektörler oluşturulur, piksel değerlerine sahip diziler ve etiketler, dönüş değeri olan bir liste halinde birleştirilir. Çalışmayı hızlandırmak için tablolarda indeks oluşturmayı kullandık data.table ve bağlantı aracılığıyla değişiklik - bu "cips" paketi olmadan veri tablosu R'de önemli miktarda veriyle etkili bir şekilde çalışmayı hayal etmek oldukça zordur.
Core i5 dizüstü bilgisayardaki hız ölçümlerinin sonuçları aşağıdaki gibidir:
Yeterli miktarda RAM'iniz varsa, veritabanını aynı RAM'e aktararak ciddi şekilde hızlandırabilirsiniz (32 GB görevimiz için yeterlidir). Linux'ta bölüm varsayılan olarak takılıdır /dev/shm, RAM kapasitesinin yarısına kadar yer kaplar. Düzenleyerek daha fazlasını vurgulayabilirsiniz /etc/fstabgibi bir kayıt almak için tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Komutu çalıştırarak yeniden başlattığınızdan ve sonucu kontrol ettiğinizden emin olun. df -h.
Test veri kümesi tamamen RAM'e sığdığından, test verileri yineleyicisi çok daha basit görünüyor:
Test verileri için yineleyici
test_generator <- function(dt,
batch_size = 32,
scale = 1,
color = FALSE,
imagenet_preproc = FALSE) {
# Проверка аргументов
checkmate::assert_data_table(dt)
checkmate::assert_count(batch_size)
checkmate::assert_number(scale, lower = 0.001, upper = 5)
checkmate::assert_flag(color)
checkmate::assert_flag(imagenet_preproc)
# Проставляем номера батчей
dt[, batch := (.I - 1L) %/% batch_size + 1L]
data.table::setkey(dt, batch)
i <- 1
max_i <- dt[, max(batch)]
# Замыкание
function() {
batch_x <- cpp_process_json_vector(dt[batch == i, drawing],
scale = scale, color = color)
if (imagenet_preproc) {
# Шкалирование c интервала [0, 1] на интервал [-1, 1]
batch_x <- (batch_x - 0.5) * 2
}
result <- list(batch_x)
i <<- i + 1
return(result)
}
}
4. Model mimarisinin seçimi
Kullanılan ilk mimari mobil ağ v1özellikleri tartışılan Bu İleti. Standart olarak dahildir keras ve buna göre R için aynı adı taşıyan pakette mevcuttur. Ancak bunu tek kanallı görüntülerle kullanmaya çalışırken garip bir şey ortaya çıktı: giriş tensörünün her zaman boyutuna sahip olması gerekir (batch, height, width, 3)yani kanal sayısı değiştirilemez. Python'da böyle bir sınırlama yoktur, bu nedenle aceleyle bu mimariye ilişkin kendi uygulamamızı, orijinal makaleyi takip ederek (keras sürümündeki bırakma olmadan) yazdık:
Bu yaklaşımın dezavantajları açıktır. Birçok modeli test etmek istiyorum ama tam tersine her mimariyi manuel olarak yeniden yazmak istemiyorum. Ayrıca imagenet üzerinde önceden eğitilmiş modellerin ağırlıklarını kullanma fırsatından da mahrum kaldık. Her zamanki gibi belgeleri incelemek yardımcı oldu. İşlev get_config() modelin açıklamasını düzenlemeye uygun bir biçimde almanızı sağlar (base_model_conf$layers - normal bir R listesi) ve işlev from_config() bir model nesnesine ters dönüşümü gerçekleştirir:
Artık sağlananlardan herhangi birini elde etmek için evrensel bir fonksiyon yazmak zor değil keras imagenet'te eğitilmiş ağırlıkları olan veya olmayan modeller:
Hazır mimarileri yükleme işlevi
get_model <- function(name = "mobilenet_v2",
input_shape = NULL,
weights = "imagenet",
pooling = "avg",
num_classes = NULL,
optimizer = keras::optimizer_adam(lr = 0.002),
loss = "categorical_crossentropy",
metrics = NULL,
color = TRUE,
compile = FALSE) {
# Проверка аргументов
checkmate::assert_string(name)
checkmate::assert_integerish(input_shape, lower = 1, upper = 256, len = 3)
checkmate::assert_count(num_classes)
checkmate::assert_flag(color)
checkmate::assert_flag(compile)
# Получаем объект из пакета keras
model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
# Проверка наличия объекта в пакете
if (is.null(model_fun)) {
stop("Model ", shQuote(name), " not found.", call. = FALSE)
}
base_model <- model_fun(
input_shape = input_shape,
include_top = FALSE,
weights = weights,
pooling = pooling
)
# Если изображение не цветное, меняем размерность входа
if (!color) {
base_model_conf <- keras::get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- keras::from_config(base_model_conf)
}
predictions <- keras::get_layer(base_model, "global_average_pooling2d_1")$output
predictions <- keras::layer_dense(predictions, units = num_classes, activation = "softmax")
model <- keras::keras_model(
inputs = base_model$input,
outputs = predictions
)
if (compile) {
keras::compile(
object = model,
optimizer = optimizer,
loss = loss,
metrics = metrics
)
}
return(model)
}
Tek kanallı görüntüler kullanıldığında önceden eğitilmiş ağırlıklar kullanılmaz. Bu düzeltilebilir: işlevin kullanılması get_weights() Model ağırlıklarını R dizilerinin bir listesi biçiminde alın, bu listenin ilk öğesinin boyutunu değiştirin (bir renk kanalı alarak veya üçünün ortalamasını alarak) ve ardından ağırlıkları, işlevli modele geri yükleyin. set_weights(). Bu işlevi hiçbir zaman eklemedik çünkü bu aşamada renkli resimlerle çalışmanın daha verimli olduğu zaten açıktı.
Deneylerin çoğunu mobilenet sürüm 1 ve 2'nin yanı sıra resnet34'ü kullanarak gerçekleştirdik. SE-ResNeXt gibi daha modern mimariler bu yarışmada iyi performans gösterdi. Ne yazık ki elimizde hazır uygulamalar yoktu ve kendimiz yazmadık (ama mutlaka yazacağız).
5. Komut dosyalarının parametrelendirilmesi
Kolaylık sağlamak amacıyla, eğitime başlamaya yönelik tüm kod, tek bir komut dosyası olarak tasarlandı ve şu şekilde parametrelendirildi: belge следующим обрахом:
doc <- '
Usage:
train_nn.R --help
train_nn.R --list-models
train_nn.R [options]
Options:
-h --help Show this message.
-l --list-models List available models.
-m --model=<model> Neural network model name [default: mobilenet_v2].
-b --batch-size=<size> Batch size [default: 32].
-s --scale-factor=<ratio> Scale factor [default: 0.5].
-c --color Use color lines [default: FALSE].
-d --db-dir=<path> Path to database directory [default: Sys.getenv("db_dir")].
-r --validate-ratio=<ratio> Validate sample ratio [default: 0.995].
-n --n-gpu=<number> Number of GPUs [default: 1].
'
args <- docopt::docopt(doc)
Paket belge uygulamayı temsil eder http://docopt.org/ R için. Onun yardımıyla komut dosyaları aşağıdaki gibi basit komutlarla başlatılır: Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db veya ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, eğer dosya train_nn.R çalıştırılabilir (bu komut modeli eğitmeye başlayacaktır) resnet50 128x128 piksel boyutundaki üç renkli görüntülerde veritabanının klasörde bulunması gerekir /home/andrey/doodle_db). Listeye öğrenme hızını, optimize edici türünü ve diğer özelleştirilebilir parametreleri ekleyebilirsiniz. Yayını hazırlama sürecinde mimarinin ortaya çıktığı ortaya çıktı. mobilenet_v2 mevcut versiyondan keras R kullanımında yapmamalısın R paketinde dikkate alınmayan değişiklikler nedeniyle düzeltmelerini bekliyoruz.
Bu yaklaşım, RStudio'daki daha geleneksel komut dosyalarının başlatılmasıyla karşılaştırıldığında farklı modellerle yapılan deneyleri önemli ölçüde hızlandırmayı mümkün kıldı (paketin olası bir alternatif olduğunu belirtiyoruz) tfrunlar). Ancak asıl avantaj, bunun için RStudio'yu yüklemeden Docker'da veya yalnızca sunucuda komut dosyalarının başlatılmasını kolayca yönetebilme yeteneğidir.
6. Komut dosyalarının dockerizasyonu
Ekip üyeleri arasındaki eğitim modelleri ve bulutta hızlı dağıtım için ortamın taşınabilirliğini sağlamak amacıyla Docker'ı kullandık. Bir R programcısı için nispeten alışılmadık olan bu araçla tanışmaya şu adresten başlayabilirsiniz: bu dizi yayın veya video kursu.
Docker, hem kendi görsellerinizi sıfırdan oluşturmanıza hem de kendi görsellerinizi oluşturmak için diğer görselleri temel olarak kullanmanıza olanak tanır. Mevcut seçenekleri analiz ederken NVIDIA, CUDA+cuDNN sürücüleri ve Python kitaplıklarının kurulumunun görüntünün oldukça büyük bir parçası olduğu sonucuna vardık ve resmi görüntüyü temel almaya karar verdik tensorflow/tensorflow:1.12.0-gpu, gerekli R paketlerini oraya ekliyorum.
Kolaylık sağlamak için kullanılan paketler değişkenlere yerleştirildi; Yazılı komut dosyalarının büyük bir kısmı montaj sırasında kapların içine kopyalanır. Ayrıca komut kabuğunu da şu şekilde değiştirdik: /bin/bash içeriğin kullanım kolaylığı için /etc/os-release. Bu, kodda işletim sistemi sürümünü belirtme ihtiyacını ortadan kaldırdı.
Ek olarak, çeşitli komutlarla bir kapsayıcıyı başlatmanıza olanak tanıyan küçük bir bash betiği yazılmıştır. Örneğin bunlar, daha önce konteynerin içine yerleştirilmiş sinir ağlarının eğitimi için komut dosyaları veya konteynerin çalışmasının hatalarını ayıklamak ve izlemek için bir komut kabuğu olabilir:
Kapsayıcıyı başlatmak için komut dosyası
#!/bin/sh
DBDIR=${PWD}/db
LOGSDIR=${PWD}/logs
MODELDIR=${PWD}/models
DATADIR=${PWD}/data
ARGS="--runtime=nvidia --rm -v ${DBDIR}:/db -v ${LOGSDIR}:/app/logs -v ${MODELDIR}:/app/models -v ${DATADIR}:/app/data"
if [ -z "$1" ]; then
CMD="Rscript /app/train_nn.R"
elif [ "$1" = "bash" ]; then
ARGS="${ARGS} -ti"
else
CMD="Rscript /app/train_nn.R $@"
fi
docker run ${ARGS} doodles-tf ${CMD}
Bu bash betiği parametresiz çalıştırılırsa betik konteynerin içinde çağrılacaktır. train_nn.R varsayılan değerlerle; eğer ilk konumsal argüman "bash" ise, kap bir komut kabuğuyla etkileşimli olarak başlayacaktır. Diğer tüm durumlarda, konumsal argümanların değerleri değiştirilir: CMD="Rscript /app/train_nn.R $@".
Kaynak verileri ve veritabanını içeren dizinlerin yanı sıra eğitilmiş modelleri kaydetme dizininin, gereksiz manipülasyonlar olmadan komut dosyalarının sonuçlarına erişmenizi sağlayan ana sistemden konteynerin içine monte edildiğini belirtmekte fayda var.
7. Google Cloud'da birden fazla GPU kullanma
Yarışmanın özelliklerinden biri de çok gürültülü verilerdi (başlık resmine bakın, ODS Slack'ten @Leigh.plt'den ödünç alınmıştır). Büyük gruplar bununla mücadeleye yardımcı oluyor ve 1 GPU'lu bir bilgisayar üzerinde yaptığımız denemelerden sonra, buluttaki çeşitli GPU'lar üzerindeki eğitim modellerinde uzmanlaşmaya karar verdik. Kullanılan GoogleCloud (temel bilgiler için iyi bir rehber) mevcut konfigürasyonların geniş seçimi, makul fiyatlar ve 300 $ bonus nedeniyle. Açgözlülükten SSD ve bir ton RAM'e sahip bir 4xV100 örneği sipariş ettim ve bu büyük bir hataydı. Böyle bir makine parayı hızla tüketir; kanıtlanmış bir üretim hattı olmadan denemeler yaparak iflas edebilirsiniz. Eğitim amaçlı olarak K80'i almak daha iyidir. Ancak büyük miktarda RAM kullanışlı oldu - bulut SSD performansından etkilenmedi, bu nedenle veritabanı dev/shm.
En çok ilgi çeken, birden fazla GPU'nun kullanılmasından sorumlu olan kod parçasıdır. İlk olarak model, Python'da olduğu gibi bir içerik yöneticisi kullanılarak CPU üzerinde oluşturulur:
Sonuncusu dışındaki tüm katmanları dondurma, son katmanı eğitme, dondurmayı çözme ve tüm modeli birkaç GPU için yeniden eğitme şeklindeki klasik teknik uygulanamadı.
Eğitim kullanılmadan izlendi. gergi tahtası, kendimizi günlükleri kaydetmek ve her dönemden sonra bilgilendirici adlarla modelleri kaydetmekle sınırlandırıyoruz:
Karşılaştığımız bazı sorunların henüz üstesinden gelinmedi:
в keras Optimum öğrenme oranını (analog) otomatik olarak aramak için hazır bir işlev yoktur. lr_finder kütüphanede hızlı.ai); Biraz çaba sarf ederek üçüncü taraf uygulamalarını R'ye taşımak mümkündür, örneğin: bu;
önceki noktanın bir sonucu olarak, birden fazla GPU kullanırken doğru eğitim hızını seçmek mümkün değildi;
modern sinir ağı mimarilerinin, özellikle de imagenet üzerinde önceden eğitilmiş olanların eksikliği;
tek döngü politikası yok ve ayrımcı öğrenme oranları (kosinüs tavlaması bizim isteğimiz üzerine yapıldı) uygulananteşekkür ederim skeydan).
Bu yarışmadan ne gibi yararlı şeyler öğrenildi:
Nispeten düşük güçlü donanımlarda, makul miktarda (RAM'in birçok katı) veri hacmiyle sorunsuz bir şekilde çalışabilirsiniz. Naylon poşet veri tablosu tabloların yerinde değiştirilmesi nedeniyle hafızadan tasarruf sağlar, bu da kopyalanmayı önler ve doğru kullanıldığında yetenekleri, komut dosyası dilleri için bildiğimiz tüm araçlar arasında neredeyse her zaman en yüksek hızı gösterir. Verileri bir veritabanına kaydetmek, çoğu durumda tüm veri kümesini RAM'e sıkıştırma ihtiyacını hiç düşünmemenizi sağlar.
R'deki yavaş işlevler, paket kullanılarak C++'daki hızlı işlevlerle değiştirilebilir Rcpp. Eğer kullanıma ek olarak Rcpp Konusu veya RcppParalel, platformlar arası çok iş parçacıklı uygulamalar elde ederiz, dolayısıyla kodu R düzeyinde paralelleştirmeye gerek yoktur.
paket Rcpp ciddi C++ bilgisi olmadan kullanılabilir, gerekli minimum değerler özetlenmiştir burada. Gibi bir dizi harika C kütüphanesi için başlık dosyaları uzatıcı CRAN üzerinde mevcut yani hazır yüksek performanslı C++ kodunu R'ye entegre eden projelerin hayata geçirilmesi için altyapı oluşturuluyor. Ek kolaylık, sözdizimi vurgulama ve RStudio'daki statik C++ kod analizörüdür.
belge bağımsız komut dosyalarını parametrelerle çalıştırmanıza olanak tanır. Bu, uzak bir sunucuda kullanım için uygundur. liman işçisi altında. RStudio'da, sinir ağlarının eğitimi ile saatlerce deney yapmak sakıncalıdır ve IDE'yi sunucunun kendisine kurmak her zaman haklı değildir.
Docker, farklı işletim sistemi ve kitaplık sürümlerine sahip geliştiriciler arasında kod taşınabilirliği ve sonuçların tekrarlanabilirliğinin yanı sıra sunucularda yürütme kolaylığı sağlar. Tüm eğitim hattını tek bir komutla başlatabilirsiniz.
Google Cloud, pahalı donanımlar üzerinde denemeler yapmanın bütçeye uygun bir yoludur ancak yapılandırmaları dikkatli seçmeniz gerekir.
Bireysel kod parçalarının hızını ölçmek, özellikle R ve C++ paketlerini birleştirirken çok faydalıdır. bank - ayrıca çok kolay.
Genel olarak bu deneyim çok faydalıydı ve ortaya çıkan bazı sorunları çözmek için çalışmaya devam ediyoruz.