ProHoster > Blog > Pangangasiwa > Quick Draw Doodle Recognition: kung paano makipagkaibigan sa R, C++ at neural network
Quick Draw Doodle Recognition: kung paano makipagkaibigan sa R, C++ at neural network
Hoy Habr!
Noong nakaraang taglagas, nag-host si Kaggle ng kumpetisyon upang pag-uri-uriin ang mga larawang iginuhit ng kamay, Quick Draw Doodle Recognition, kung saan, bukod sa iba pa, isang pangkat ng mga R-scientist ang nakibahagi: Artem Klevtsova, Tagapamahala ng Philippa ΠΈ Andrey Ogurtsov. Hindi namin ilalarawan nang detalyado ang kumpetisyon; nagawa na iyon sa kamakailang publikasyon.
Sa pagkakataong ito ay hindi ito gumana sa pagsasaka ng medalya, ngunit maraming mahalagang karanasan ang natamo, kaya gusto kong sabihin sa komunidad ang tungkol sa ilang pinakakawili-wili at kapaki-pakinabang na mga bagay sa Kagle at sa pang-araw-araw na gawain. Kabilang sa mga paksang tinalakay: mahirap na buhay kung wala OpenCV, JSON parsing (sinusuri ng mga halimbawang ito ang pagsasama ng C++ code sa mga script o package sa R ββgamit ang Rcpp), parameterization ng mga script at dockerization ng panghuling solusyon. Ang lahat ng code mula sa mensahe sa isang form na angkop para sa pagpapatupad ay magagamit sa mga repositoryo.
1. Mahusay na mag-load ng data mula sa CSV sa MonetDB database
Ang data sa kompetisyong ito ay ibinibigay hindi sa anyo ng mga yari na larawan, ngunit sa anyo ng 340 CSV file (isang file para sa bawat klase) na naglalaman ng mga JSON na may mga point coordinates. Sa pamamagitan ng pagkonekta sa mga puntong ito sa mga linya, nakakakuha kami ng panghuling larawan na may sukat na 256x256 pixels. Para din sa bawat record ay may label na nagsasaad kung ang larawan ay nakilala nang tama ng classifier na ginamit sa oras na nakolekta ang dataset, isang dalawang-titik na code ng bansang tinitirhan ng may-akda ng larawan, isang natatanging identifier, isang timestamp at isang pangalan ng klase na tumutugma sa pangalan ng file. Ang isang pinasimpleng bersyon ng orihinal na data ay tumitimbang ng 7.4 GB sa archive at humigit-kumulang 20 GB pagkatapos i-unpack, ang buong data pagkatapos i-unpack ay tumatagal ng 240 GB. Tiniyak ng mga organizer na ang parehong mga bersyon ay muling ginawa ang parehong mga guhit, ibig sabihin ang buong bersyon ay kalabisan. Sa anumang kaso, ang pag-iimbak ng 50 milyong mga imahe sa mga graphic na file o sa anyo ng mga array ay agad na itinuturing na hindi kumikita, at nagpasya kaming pagsamahin ang lahat ng mga CSV file mula sa archive train_simplified.zip sa database na may kasunod na henerasyon ng mga larawan ng kinakailangang laki "on the fly" para sa bawat batch.
Ang isang mahusay na napatunayang sistema ay napili bilang DBMS MonetDB, ibig sabihin ay isang pagpapatupad para sa R ββbilang isang pakete MonetDBLite. Kasama sa package ang isang naka-embed na bersyon ng database server at pinapayagan kang kunin ang server nang direkta mula sa isang R session at magtrabaho kasama nito doon. Ang paglikha ng isang database at pagkonekta dito ay isinasagawa gamit ang isang utos:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
Kakailanganin naming gumawa ng dalawang talahanayan: isa para sa lahat ng data, ang isa para sa impormasyon ng serbisyo tungkol sa mga na-download na file (kapaki-pakinabang kung may mali at kailangang ipagpatuloy ang proseso pagkatapos mag-download ng ilang file):
Ang pinakamabilis na paraan upang mai-load ang data sa database ay ang direktang pagkopya ng mga CSV file gamit ang SQL - command COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTSaan tablename - pangalan ng talahanayan at path - ang landas patungo sa file. Habang nagtatrabaho sa archive, natuklasan na ang built-in na pagpapatupad unzip sa R ay hindi gumagana nang tama sa isang bilang ng mga file mula sa archive, kaya ginamit namin ang system unzip (gamit ang parameter getOption("unzip")).
Kung kailangan mong ibahin ang anyo ng talahanayan bago isulat ito sa database, ito ay sapat na upang pumasa sa argumento preprocess function na magbabago ng data.
Code para sa sunud-sunod na paglo-load ng data sa database:
Ang oras ng paglo-load ng data ay maaaring mag-iba depende sa mga katangian ng bilis ng drive na ginamit. Sa aming kaso, ang pagbabasa at pagsusulat sa loob ng isang SSD o mula sa isang flash drive (source file) patungo sa isang SSD (DB) ay tumatagal ng wala pang 10 minuto.
Tumatagal pa ng ilang segundo para gumawa ng column na may integer class label at index column (ORDERED INDEX) na may mga numero ng linya kung saan isasampol ang mga obserbasyon kapag gumagawa ng mga batch:
Paglikha ng Mga Karagdagang Column at 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)"))
Upang malutas ang problema ng paglikha ng isang batch sa mabilisang, kailangan naming makamit ang pinakamataas na bilis ng pagkuha ng mga random na hilera mula sa talahanayan doodles. Para dito gumamit kami ng 3 trick. Ang una ay upang bawasan ang dimensionality ng uri na nag-iimbak ng observation ID. Sa orihinal na set ng data, ang uri na kinakailangan upang maiimbak ang ID ay bigint, ngunit ginagawang posible ng bilang ng mga obserbasyon na magkasya ang kanilang mga identifier, katumbas ng ordinal na numero, sa uri int. Ang paghahanap ay mas mabilis sa kasong ito. Ang pangalawang trick ay ang paggamit ORDERED INDEX β empirically nakarating kami sa desisyong ito, na napagdaanan ang lahat ng magagamit mga pagpipilian. Ang pangatlo ay gumamit ng mga parameterized na query. Ang kakanyahan ng pamamaraan ay upang maisagawa ang utos nang isang beses PREPARE na may kasunod na paggamit ng isang inihandang expression kapag lumilikha ng isang grupo ng mga query ng parehong uri, ngunit sa katunayan mayroong isang kalamangan kumpara sa isang simple SELECT lumabas na nasa saklaw ng statistical error.
Ang proseso ng pag-upload ng data ay gumagamit ng hindi hihigit sa 450 MB ng RAM. Iyon ay, binibigyang-daan ka ng inilarawang diskarte na ilipat ang mga dataset na tumitimbang ng sampu-sampung gigabytes sa halos anumang hardware na badyet, kabilang ang ilang mga single-board device, na medyo cool.
Ang natitira na lang ay sukatin ang bilis ng pagkuha (random) na data at suriin ang pag-scale kapag nagsa-sample ng mga batch na may iba't ibang laki:
Ang buong proseso ng paghahanda ng batch ay binubuo ng mga sumusunod na hakbang:
Pag-parse ng ilang JSON na naglalaman ng mga vector ng mga string na may mga coordinate ng mga puntos.
Pagguhit ng mga linyang may kulay batay sa mga coordinate ng mga punto sa isang imahe ng kinakailangang laki (halimbawa, 256Γ256 o 128Γ128).
Pag-convert ng mga nagresultang larawan sa isang tensor.
Bilang bahagi ng kumpetisyon sa mga kernel ng Python, ang problema ay nalutas pangunahin gamit OpenCV. Ang isa sa pinakasimpleng at pinaka-halatang analogues sa R ββay magiging ganito:
Ang pagguhit ay ginagawa gamit ang karaniwang R tool at nai-save sa isang pansamantalang PNG na nakaimbak sa RAM (sa Linux, ang mga pansamantalang R na direktoryo ay matatagpuan sa direktoryo /tmp, naka-mount sa RAM). Ang file na ito ay babasahin bilang isang three-dimensional array na may mga numerong mula 0 hanggang 1. Mahalaga ito dahil ang isang mas kumbensyonal na BMP ay mababasa sa isang raw array na may mga hex na color code.
Ang pagpapatupad na ito ay tila suboptimal sa amin, dahil ang pagbuo ng malalaking batch ay tumatagal ng napakatagal na panahon, at nagpasya kaming samantalahin ang karanasan ng aming mga kasamahan sa pamamagitan ng paggamit ng isang malakas na library OpenCV. Sa oras na iyon ay walang handa na pakete para sa R ββ(wala na ngayon), kaya isang minimal na pagpapatupad ng kinakailangang pag-andar ay isinulat sa C++ na may pagsasama sa R ββcode gamit ang Rcpp.
Upang malutas ang problema, ginamit ang mga sumusunod na pakete at aklatan:
OpenCV para sa pagtatrabaho sa mga larawan at pagguhit ng mga linya. Gumamit ng mga paunang naka-install na library ng system at mga file ng header, pati na rin ang dynamic na pag-link.
xtensor para sa pagtatrabaho sa mga multidimensional na array at tensor. Gumamit kami ng mga file ng header na kasama sa R ββpackage na may parehong pangalan. Binibigyang-daan ka ng library na magtrabaho kasama ang mga multidimensional na array, pareho sa row major at column major order.
ndjson para sa pag-parse ng JSON. Ang aklatan na ito ay ginagamit sa xtensor awtomatikong kung ito ay naroroon sa proyekto.
RcppThread para sa pag-aayos ng multi-threaded na pagproseso ng isang vector mula sa JSON. Ginamit ang mga file ng header na ibinigay ng package na ito. Mula sa mas sikat RcppParallel Ang package, bukod sa iba pang mga bagay, ay may built-in na loop interrupt mechanism.
Dapat ito ay nabanggit na xtensor naging isang kaloob ng diyos: bilang karagdagan sa katotohanan na mayroon itong malawak na pag-andar at mataas na pagganap, ang mga developer nito ay naging medyo tumutugon at sumagot ng mga tanong kaagad at detalyado. Sa kanilang tulong, naging posible na ipatupad ang mga pagbabagong-anyo ng OpenCV matrice sa mga xtensor tensor, pati na rin ang isang paraan upang pagsamahin ang 3-dimensional na mga tensor ng imahe sa isang 4-dimensional na tensor ng tamang dimensyon (ang batch mismo).
Mga materyales para sa pag-aaral ng Rcpp, xtensor at RcppThread
Upang mag-compile ng mga file na gumagamit ng mga system file at dynamic na pag-link sa mga library na naka-install sa system, ginamit namin ang mekanismo ng plugin na ipinatupad sa package Rcpp. Upang awtomatikong mahanap ang mga landas at flag, gumamit kami ng isang sikat na utility ng Linux pkg-config.
Pagpapatupad ng Rcpp plugin para sa paggamit ng OpenCV library
Ang code ng pagpapatupad para sa pag-parse ng JSON at pagbuo ng isang batch para sa paghahatid sa modelo ay ibinibigay sa ilalim ng spoiler. Una, magdagdag ng lokal na direktoryo ng proyekto upang maghanap ng mga file ng header (kinakailangan para sa ndjson):
// [[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;
}
Ang code na ito ay dapat ilagay sa file src/cv_xt.cpp at isama ang utos Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); kailangan din para sa trabaho nlohmann/json.hpp ng imbakan. Ang code ay nahahati sa ilang mga function:
to_xt β isang templated function para sa pagbabago ng isang image matrix (cv::Mat) sa isang tensor xt::xtensor;
parse_json β ang function ay nag-parse ng isang string ng JSON, kinukuha ang mga coordinate ng mga puntos, i-pack ang mga ito sa isang vector;
ocv_draw_lines β mula sa nagresultang vector ng mga puntos, gumuhit ng maraming kulay na mga linya;
process β pinagsasama ang mga pag-andar sa itaas at nagdaragdag din ng kakayahang sukatin ang resultang imahe;
cpp_process_json_str - wrapper sa ibabaw ng function process, na nag-e-export ng resulta sa isang R-object (multidimensional array);
cpp_process_json_vector - wrapper sa ibabaw ng function cpp_process_json_str, na nagbibigay-daan sa iyong iproseso ang isang string vector sa multi-threaded mode.
Upang gumuhit ng maraming kulay na mga linya, ginamit ang modelo ng kulay ng HSV, na sinusundan ng conversion sa RGB. Subukan natin ang resulta:
Tulad ng nakikita mo, ang pagtaas ng bilis ay naging napaka makabuluhan, at hindi posible na abutin ang C++ code sa pamamagitan ng pagpaparis ng R code.
3. Mga iterator para sa pagbabawas ng mga batch mula sa database
Ang R ay may mahusay na karapat-dapat na reputasyon para sa pagproseso ng data na akma sa RAM, habang ang Python ay higit na nailalarawan sa pamamagitan ng umuulit na pagproseso ng data, na nagbibigay-daan sa iyong madali at natural na magpatupad ng mga out-of-core na kalkulasyon (mga kalkulasyon gamit ang panlabas na memorya). Ang isang klasiko at may-katuturang halimbawa para sa amin sa konteksto ng inilarawang problema ay ang mga malalim na neural network na sinanay ng gradient descent method na may approximation ng gradient sa bawat hakbang gamit ang isang maliit na bahagi ng mga obserbasyon, o mini-batch.
Ang mga deep learning framework na nakasulat sa Python ay may mga espesyal na klase na nagpapatupad ng mga iterator batay sa data: mga talahanayan, mga larawan sa mga folder, binary na format, atbp. Maaari kang gumamit ng mga handa na opsyon o magsulat ng sarili mo para sa mga partikular na gawain. Sa R maaari nating samantalahin ang lahat ng mga tampok ng library ng Python matigas kasama ang iba't ibang backend nito gamit ang package na may parehong pangalan, na gumagana naman sa ibabaw ng package bigkasin. Ang huli ay nararapat sa isang hiwalay na mahabang artikulo; ito ay hindi lamang nagbibigay-daan sa iyo upang patakbuhin ang Python code mula sa R, ngunit pinapayagan ka ring maglipat ng mga bagay sa pagitan ng R at Python session, awtomatikong gumaganap ng lahat ng kinakailangang uri ng mga conversion.
Inalis namin ang pangangailangan na mag-imbak ng lahat ng data sa RAM sa pamamagitan ng paggamit ng MonetDBlite, lahat ng gawaing "neural network" ay isasagawa ng orihinal na code sa Python, kailangan lang naming magsulat ng isang iterator sa data, dahil walang handa para sa ganoong sitwasyon sa alinman sa R ββo Python. Mayroong dalawang mga kinakailangan lamang para dito: dapat itong ibalik ang mga batch sa isang walang katapusang loop at i-save ang estado nito sa pagitan ng mga pag-ulit (ang huli sa R ββay ipinatupad sa pinakasimpleng paraan gamit ang mga pagsasara). Noong nakaraan, kinakailangan na tahasang i-convert ang mga R arrays sa mga numpy array sa loob ng iterator, ngunit ang kasalukuyang bersyon ng package matigas ginagawa niya mismo.
Ang iterator para sa data ng pagsasanay at pagpapatunay ay naging ang mga sumusunod:
Iterator para sa data ng pagsasanay at pagpapatunay
Kinukuha ng function bilang input ang isang variable na may koneksyon sa database, ang mga bilang ng mga linyang ginamit, ang bilang ng mga klase, laki ng batch, sukat (scale = 1 tumutugma sa pag-render ng mga larawan ng 256x256 pixels, scale = 0.5 β 128x128 pixels), tagapagpahiwatig ng kulay (color = FALSE tumutukoy sa pag-render sa grayscale kapag ginamit color = TRUE bawat stroke ay iginuhit sa isang bagong kulay) at isang preprocessing indicator para sa mga network na pre-trained sa imagenet. Ang huli ay kinakailangan upang masukat ang mga halaga ng pixel mula sa pagitan [0, 1] hanggang sa pagitan [-1, 1], na ginamit kapag sinasanay ang ibinigay matigas mga modelo.
Ang panlabas na function ay naglalaman ng pagsusuri ng uri ng argumento, isang talahanayan data.table na may random na pinaghalong mga numero ng linya mula sa samples_index at mga batch number, counter at maximum na bilang ng mga batch, pati na rin ang SQL expression para sa pag-unload ng data mula sa database. Bilang karagdagan, tinukoy namin ang isang mabilis na analogue ng function sa loob keras::to_categorical(). Ginamit namin ang halos lahat ng data para sa pagsasanay, na nag-iiwan ng kalahating porsyento para sa pagpapatunay, kaya ang sukat ng panahon ay limitado ng parameter steps_per_epoch kapag tinawag keras::fit_generator(), at ang kondisyon if (i > max_i) gumana lang para sa validation iterator.
Sa panloob na function, kinukuha ang mga row index para sa susunod na batch, ang mga record ay dini-load mula sa database na may pagtaas ng batch counter, JSON parsing (function cpp_process_json_vector(), nakasulat sa C++) at paglikha ng mga arrays na naaayon sa mga larawan. Pagkatapos ay nilikha ang mga one-hot vector na may mga label ng klase, ang mga array na may mga halaga ng pixel at mga label ay pinagsama sa isang listahan, na siyang halaga ng pagbabalik. Upang mapabilis ang trabaho, ginamit namin ang paglikha ng mga index sa mga talahanayan data.table at pagbabago sa pamamagitan ng link - nang wala itong mga package na "chips" talaan ng mga impormasyon Medyo mahirap isipin na gumagana nang epektibo sa anumang makabuluhang halaga ng data sa R.
Ang mga resulta ng mga pagsukat ng bilis sa isang Core i5 laptop ay ang mga sumusunod:
Kung mayroon kang sapat na dami ng RAM, maaari mong seryosong pabilisin ang pagpapatakbo ng database sa pamamagitan ng paglilipat nito sa parehong RAM (sapat na ang 32 GB para sa aming gawain). Sa Linux, ang partition ay naka-mount bilang default /dev/shm, na sumasakop ng hanggang kalahati ng kapasidad ng RAM. Maaari kang mag-highlight ng higit pa sa pamamagitan ng pag-edit /etc/fstabpara makakuha ng record like tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Tiyaking i-reboot at suriin ang resulta sa pamamagitan ng pagpapatakbo ng command df -h.
Ang iterator para sa data ng pagsubok ay mukhang mas simple, dahil ang test dataset ay ganap na umaangkop sa RAM:
Ang unang arkitektura na ginamit ay mobilenet v1, ang mga tampok nito ay tinalakay sa Ito mensahe. Ito ay kasama bilang pamantayan matigas at, nang naaayon, ay magagamit sa pakete ng parehong pangalan para sa R. Ngunit kapag sinusubukang gamitin ito sa mga single-channel na imahe, isang kakaibang bagay ang lumabas: ang input tensor ay dapat palaging may sukat (batch, height, width, 3), ibig sabihin, hindi mababago ang bilang ng mga channel. Walang ganoong limitasyon sa Python, kaya nagmadali kami at nagsulat ng sarili naming pagpapatupad ng arkitektura na ito, kasunod ng orihinal na artikulo (nang walang dropout na nasa hard version):
Ang mga disadvantages ng diskarteng ito ay halata. Gusto kong subukan ang maraming mga modelo, ngunit sa kabaligtaran, hindi ko nais na muling isulat nang manu-mano ang bawat arkitektura. Pinagkaitan din kami ng pagkakataong gamitin ang mga timbang ng mga modelong pre-trained sa imagenet. Gaya ng dati, nakatulong ang pag-aaral sa dokumentasyon. Function get_config() nagbibigay-daan sa iyo na makakuha ng paglalarawan ng modelo sa isang form na angkop para sa pag-edit (base_model_conf$layers - isang regular na listahan ng R), at ang function from_config() nagsasagawa ng reverse conversion sa isang modelong object:
Ngayon hindi mahirap magsulat ng isang unibersal na function upang makuha ang alinman sa mga ibinigay matigas mga modelong mayroon o walang mga timbang na sinanay sa imagenet:
Function para sa pag-load ng mga yari na arkitektura
Kapag gumagamit ng mga single-channel na imahe, walang paunang sinanay na timbang ang ginagamit. Ito ay maaaring maayos: gamit ang function get_weights() kunin ang mga timbang ng modelo sa anyo ng isang listahan ng mga R array, baguhin ang dimensyon ng unang elemento ng listahang ito (sa pamamagitan ng pagkuha ng isang color channel o pag-average sa lahat ng tatlo), at pagkatapos ay i-load ang mga timbang pabalik sa modelo gamit ang function set_weights(). Hindi namin idinagdag ang pagpapaandar na ito, dahil sa yugtong ito ay malinaw na na mas produktibo ang pagtatrabaho sa mga larawang may kulay.
Isinagawa namin ang karamihan sa mga eksperimento gamit ang mobilenet na bersyon 1 at 2, pati na rin ang resnet34. Mas mahusay na gumanap ang mas modernong arkitektura gaya ng SE-ResNeXt sa kompetisyong ito. Sa kasamaang palad, wala kaming handa na mga pagpapatupad sa aming pagtatapon, at hindi kami sumulat ng aming sarili (ngunit tiyak na magsusulat kami).
5. Parameterization ng mga script
Para sa kaginhawahan, ang lahat ng code para sa pagsisimula ng pagsasanay ay idinisenyo bilang isang solong script, na naka-parameter gamit docopt tulad ng sumusunod:
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)
Package docopt kumakatawan sa pagpapatupad http://docopt.org/ para sa R. Sa tulong nito, ang mga script ay inilunsad gamit ang mga simpleng utos tulad ng Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db o ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, kung file train_nn.R ay maipapatupad (ang utos na ito ay magsisimulang magsanay sa modelo resnet50 sa tatlong-kulay na mga imahe na may sukat na 128x128 pixels, ang database ay dapat na matatagpuan sa folder /home/andrey/doodle_db). Maaari kang magdagdag ng bilis ng pag-aaral, uri ng optimizer, at anumang iba pang nako-customize na parameter sa listahan. Sa proseso ng paghahanda ng publikasyon, lumabas na ang arkitektura mobilenet_v2 mula sa kasalukuyang bersyon matigas sa paggamit ng R hindi pwede dahil sa mga pagbabagong hindi isinasaalang-alang sa R ββpackage, hinihintay namin silang ayusin ito.
Ang diskarte na ito ay naging posible upang makabuluhang mapabilis ang mga eksperimento na may iba't ibang mga modelo kumpara sa mas tradisyonal na paglulunsad ng mga script sa RStudio (napansin namin ang package bilang isang posibleng alternatibo tfruns). Ngunit ang pangunahing bentahe ay ang kakayahang madaling pamahalaan ang paglulunsad ng mga script sa Docker o sa server lamang, nang hindi nag-i-install ng RStudio para dito.
6. Dockerization ng mga script
Ginamit namin ang Docker para matiyak ang portability ng environment para sa mga modelo ng pagsasanay sa pagitan ng mga miyembro ng team at para sa mabilis na pag-deploy sa cloud. Maaari kang magsimulang maging pamilyar sa tool na ito, na medyo hindi karaniwan para sa isang R programmer, na may ito serye ng mga publikasyon o kursong video.
Pinapayagan ka ng Docker na parehong lumikha ng iyong sariling mga imahe mula sa simula at gumamit ng iba pang mga imahe bilang batayan para sa paglikha ng iyong sarili. Kapag pinag-aaralan ang mga magagamit na opsyon, napagpasyahan namin na ang pag-install ng NVIDIA, CUDA+cuDNN drivers at Python library ay isang medyo malaking bahagi ng imahe, at nagpasya kaming kunin ang opisyal na imahe bilang batayan. tensorflow/tensorflow:1.12.0-gpu, pagdaragdag ng mga kinakailangang R package doon.
Para sa kaginhawahan, ang mga pakete na ginamit ay inilagay sa mga variable; ang karamihan sa mga nakasulat na script ay kinopya sa loob ng mga lalagyan sa panahon ng pagpupulong. Binago din namin ang command shell sa /bin/bash para sa kadalian ng paggamit ng nilalaman /etc/os-release. Iniiwasan nito ang pangangailangang tukuyin ang bersyon ng OS sa code.
Bilang karagdagan, isang maliit na script ng bash ang isinulat na nagbibigay-daan sa iyo upang maglunsad ng isang lalagyan na may iba't ibang mga utos. Halimbawa, ang mga ito ay maaaring mga script para sa pagsasanay ng mga neural network na dating inilagay sa loob ng container, o isang command shell para sa pag-debug at pagsubaybay sa pagpapatakbo ng container:
Script upang ilunsad ang lalagyan
#!/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}
Kung ang bash script na ito ay tatakbo nang walang mga parameter, ang script ay tatawagin sa loob ng container train_nn.R na may mga default na halaga; kung ang unang positional na argumento ay "bash", kung gayon ang lalagyan ay magsisimulang interactive sa isang command shell. Sa lahat ng iba pang mga kaso, ang mga halaga ng mga positional na argumento ay pinapalitan: CMD="Rscript /app/train_nn.R $@".
Kapansin-pansin na ang mga direktoryo na may mapagkukunan ng data at database, pati na rin ang direktoryo para sa pag-save ng mga sinanay na modelo, ay naka-mount sa loob ng lalagyan mula sa host system, na nagbibigay-daan sa iyo upang ma-access ang mga resulta ng mga script nang walang mga hindi kinakailangang manipulasyon.
7. Paggamit ng maraming GPU sa Google Cloud
Isa sa mga tampok ng kumpetisyon ay ang napakaingay na data (tingnan ang pamagat na larawan, na hiniram mula kay @Leigh.plt mula sa ODS slack). Nakakatulong ang malalaking batch na labanan ito, at pagkatapos ng mga eksperimento sa isang PC na may 1 GPU, nagpasya kaming mag-master ng mga modelo ng pagsasanay sa ilang GPU sa cloud. Gumamit ng GoogleCloud (magandang gabay sa mga pangunahing kaalaman) dahil sa malaking seleksyon ng mga available na configuration, makatwirang presyo at $300 na bonus. Dahil sa kasakiman, nag-order ako ng 4xV100 instance na may SSD at isang toneladang RAM, at iyon ay isang malaking pagkakamali. Ang ganitong makina ay kumakain ng pera nang mabilis; maaari kang masira ang pag-eksperimento nang walang isang napatunayang pipeline. Para sa mga layuning pang-edukasyon, mas mainam na kunin ang K80. Ngunit ang malaking halaga ng RAM ay madaling gamitin - ang cloud SSD ay hindi humanga sa pagganap nito, kaya ang database ay inilipat sa dev/shm.
Ang pinaka-interesante ay ang code fragment na responsable para sa paggamit ng maraming GPU. Una, ang modelo ay nilikha sa CPU gamit ang isang tagapamahala ng konteksto, tulad ng sa Python:
Pagkatapos ay ang hindi pinagsama-sama (ito ay mahalaga) na modelo ay kinopya sa isang naibigay na bilang ng mga magagamit na GPU, at pagkatapos lamang na ito ay pinagsama-sama:
Ang klasikong pamamaraan ng pagyeyelo ng lahat ng mga layer maliban sa huling isa, pagsasanay sa huling layer, pag-unfreeze at muling pagsasanay sa buong modelo para sa ilang mga GPU ay hindi maipatupad.
Ang pagsasanay ay sinusubaybayan nang walang paggamit. tensorboard, nililimitahan ang ating sarili sa pag-record ng mga log at pag-save ng mga modelo na may mga pangalang nagbibigay-kaalaman pagkatapos ng bawat panahon:
Ang ilang mga problema na nakatagpo namin ay hindi pa nagtagumpay:
Π² matigas walang handa na function para sa awtomatikong paghahanap para sa pinakamainam na rate ng pag-aaral (analogue lr_finder sa library mabilis.ai); Sa ilang pagsisikap, posibleng i-port ang mga pagpapatupad ng third-party sa R, halimbawa, ito;
bilang resulta ng nakaraang punto, hindi posible na piliin ang tamang bilis ng pagsasanay kapag gumagamit ng ilang mga GPU;
may kakulangan ng mga modernong arkitektura ng neural network, lalo na ang mga pre-trained sa imagenet;
walang one cycle policy at discriminative learning rate (cosine annealing ay sa aming kahilingan ipinatupad, salamat skeydan).
Anong mga kapaki-pakinabang na bagay ang natutunan mula sa kompetisyong ito:
Sa medyo mababang lakas na hardware, maaari kang magtrabaho nang may disenteng (maraming beses ang laki ng RAM) na dami ng data nang walang sakit. Plastik na bag talaan ng mga impormasyon nakakatipid ng memorya dahil sa in-place na pagbabago ng mga talahanayan, na umiiwas sa pagkopya sa mga ito, at kapag ginamit nang tama, ang mga kakayahan nito ay halos palaging nagpapakita ng pinakamataas na bilis sa lahat ng mga tool na kilala sa amin para sa mga wika ng script. Ang pag-save ng data sa isang database ay nagbibigay-daan sa iyo, sa maraming mga kaso, na huwag isipin ang lahat tungkol sa pangangailangan na i-squeeze ang buong dataset sa RAM.
Ang mga mabagal na function sa R ββay maaaring mapalitan ng mabilis sa C++ gamit ang package Rcpp. Kung bukod sa paggamit RcppThread o RcppParallel, nakakakuha kami ng mga cross-platform na multi-threaded na pagpapatupad, kaya hindi na kailangang iparallelize ang code sa R ββlevel.
Package Rcpp maaaring gamitin nang walang seryosong kaalaman sa C++, ang kinakailangang minimum ay nakabalangkas dito. Header file para sa isang bilang ng mga cool na C-library tulad ng xtensor magagamit sa CRAN, iyon ay, isang imprastraktura ay nabuo para sa pagpapatupad ng mga proyekto na nagsasama ng handa na mataas na pagganap na C++ code sa R. Ang karagdagang kaginhawahan ay ang pag-highlight ng syntax at isang static na C++ code analyzer sa RStudio.
docopt nagbibigay-daan sa iyong magpatakbo ng mga self-contained na script na may mga parameter. Ito ay maginhawa para sa paggamit sa isang malayong server, kasama. sa ilalim ng pantalan. Sa RStudio, hindi maginhawang magsagawa ng maraming oras ng mga eksperimento sa pagsasanay ng mga neural network, at ang pag-install ng IDE sa server mismo ay hindi palaging makatwiran.
Tinitiyak ng Docker ang code portability at reproducibility ng mga resulta sa pagitan ng mga developer na may iba't ibang bersyon ng OS at mga library, pati na rin ang kadalian ng pagpapatupad sa mga server. Maaari mong ilunsad ang buong pipeline ng pagsasanay sa isang utos lamang.
Ang Google Cloud ay isang paraan ng badyet para mag-eksperimento sa mamahaling hardware, ngunit kailangan mong maingat na pumili ng mga configuration.
Ang pagsukat ng bilis ng mga indibidwal na fragment ng code ay lubhang kapaki-pakinabang, lalo na kapag pinagsama ang R at C++, at kasama ang package hukuman - napakadali din.
Sa pangkalahatan, napakahusay ng karanasang ito at patuloy kaming nagsusumikap upang malutas ang ilan sa mga isyung iniharap.