ProHoster > Quick Draw Doodle Rikonoxximent: kif tagħmel ħbieb ma 'R, C++ u netwerks newrali
Quick Draw Doodle Rikonoxximent: kif tagħmel ħbieb ma 'R, C++ u netwerks newrali
Ħej Habr!
Il-ħarifa li għaddiet, Kaggle ospitat kompetizzjoni biex tikklassifika stampi miġbuda bl-idejn, Quick Draw Doodle Recognition, li fiha, fost oħrajn, ħadu sehem tim ta’ R-xjentisti: Artem Klevtsova, Philippa Manager и Andrey Ogurtsov. Mhux se niddeskrivu l-kompetizzjoni fid-dettall; dan diġà sar fih pubblikazzjoni riċenti.
Din id-darba ma ħadmitx bit-trobbija tal-midalji, iżda nkisbet ħafna esperjenza siewja, għalhekk nixtieq ngħid lill-komunità dwar numru mill-aktar affarijiet interessanti u utli fuq Kagle u fix-xogħol ta 'kuljum. Fost is-suġġetti diskussi: ħajja diffiċli mingħajr OpenCV, parsing JSON (dawn l-eżempji jeżaminaw l-integrazzjoni tal-kodiċi C++ fi skripts jew pakketti f'R bl-użu Rcpp), parametrizzazzjoni ta 'skripts u dockerization tas-soluzzjoni finali. Il-kodiċi kollu mill-messaġġ f'forma adattata għall-eżekuzzjoni huwa disponibbli fi repożitorji.
1. Tagħbija b'mod effiċjenti d-dejta minn CSV fid-database MonetDB
Id-dejta f'din il-kompetizzjoni hija pprovduta mhux fil-forma ta 'immaġini lesti, iżda fil-forma ta' 340 fajl CSV (fajl wieħed għal kull klassi) li fihom JSONs b'koordinati tal-punti. Billi tgħaqqad dawn il-punti b'linji, niksbu immaġni finali li tkejjel 256x256 pixels. Għal kull rekord hemm ukoll tikketta li tindika jekk l-istampa kinitx rikonoxxuta b'mod korrett mill-klassifikatur użat fiż-żmien meta nġabar id-dataset, kodiċi b'żewġ ittri tal-pajjiż tar-residenza tal-awtur tal-istampa, identifikatur uniku, timestamp u isem tal-klassi li jaqbel mal-isem tal-fajl. Verżjoni simplifikata tad-dejta oriġinali tiżen 7.4 GB fl-arkivju u madwar 20 GB wara l-ispakkjar, id-dejta sħiħa wara l-ispakkjar tieħu 240 GB. L-organizzaturi żguraw li ż-żewġ verżjonijiet jirriproduċu l-istess tpinġijiet, jiġifieri l-verżjoni sħiħa kienet żejda. Fi kwalunkwe każ, il-ħażna ta '50 miljun immaġini f'fajls grafiċi jew fil-forma ta' arrays tqieset immedjatament bħala mhux ta 'profitt, u ddeċidejna li ngħaqqdu l-fajls CSV kollha mill-arkivju train_simplifikata.zip fid-database bil-ġenerazzjoni sussegwenti ta 'immaġini tad-daqs meħtieġ "fuq it-titjir" għal kull lott.
Ingħażlet sistema ppruvata sew bħala d-DBMS MonetDB, jiġifieri implimentazzjoni għal R bħala pakkett MonetDBLite. Il-pakkett jinkludi verżjoni inkorporata tas-server tad-database u jippermettilek li taqbad is-server direttament minn sessjoni R u taħdem miegħu hemmhekk. Il-ħolqien ta 'database u l-konnessjoni magħha jsiru bi kmand wieħed:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
Ikollna bżonn noħolqu żewġ tabelli: waħda għad-dejta kollha, l-oħra għall-informazzjoni tas-servizz dwar il-fajls imniżżla (utli jekk xi ħaġa tmur ħażin u l-proċess irid jerġa’ jibda wara li tniżżel diversi fajls):
L-iktar mod mgħaġġel biex titgħabba d-data fid-database kien li tikkopja direttament il-fajls CSV billi tuża SQL - kmand COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTfejn tablename - isem il-mejda u path - il-mogħdija għall-fajl. Filwaqt li taħdem ma 'l-arkivju, ġie skopert li l-implimentazzjoni integrata unzip f'R ma taħdimx b'mod korrett ma 'numru ta' fajls mill-arkivju, għalhekk użajna s-sistema unzip (bl-użu tal-parametru getOption("unzip")).
Funzjoni għall-kitba fid-database
#' @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))
}
Jekk għandek bżonn tikkonverti t-tabella qabel tiktebha fid-database, huwa biżżejjed li tgħaddi l-argument preprocess funzjoni li se tittrasforma d-data.
# Список файлов для записи
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
Il-ħin tat-tagħbija tad-dejta jista' jvarja skont il-karatteristiċi tal-veloċità tad-drajv użat. Fil-każ tagħna, il-qari u l-kitba fi ħdan SSD wieħed jew minn flash drive (fajl sors) għal SSD (DB) jieħu inqas minn 10 minuti.
Jieħu ftit sekondi oħra biex tinħoloq kolonna b'tikketta ta' klassi sħiħa u kolonna ta' indiċi (ORDERED INDEX) bin-numri tal-linji li bihom se jittieħdu kampjuni tal-osservazzjonijiet meta jinħolqu lottijiet:
Ħolqien ta 'Kolonni Addizzjonali u Indiċi
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)"))
Biex issolvi l-problema tal-ħolqien ta 'lott fuq il-fly, kellna bżonn niksbu l-veloċità massima ta' estrazzjoni ta 'ringieli każwali mit-tabella doodles. Għal dan użajna 3 tricks. L-ewwel kien li titnaqqas id-dimensjonalità tat-tip li jaħżen l-ID ta 'osservazzjoni. Fis-sett tad-dejta oriġinali, it-tip meħtieġ biex tinħażen l-ID hija bigint, iżda n-numru ta' osservazzjonijiet jagħmilha possibbli li l-identifikaturi tagħhom jitwaħħlu, ugwali għan-numru ordinali, fit-tip int. It-tfittxija hija ħafna aktar mgħaġġla f'dan il-każ. It-tieni trick kien li tuża ORDERED INDEX — wasalna għal din id-deċiżjoni b'mod empiriku, wara li għaddejna minn kull disponibbli għażliet. It-tielet kien li tuża mistoqsijiet parametrizzati. L-essenza tal-metodu hija li tesegwixxi l-kmand darba PREPARE b'użu sussegwenti ta 'espressjoni ppreparata meta toħloq mazz ta' mistoqsijiet tal-istess tip, iżda fil-fatt hemm vantaġġ meta mqabbel ma 'wieħed sempliċi SELECT irriżulta li kien fil-medda ta’ żball statistiku.
Il-proċess ta 'uploading tad-data jikkonsma mhux aktar minn 450 MB ta' RAM. Jiġifieri, l-approċċ deskritt jippermettilek li tiċċaqlaq settijiet ta 'dejta li jiżnu għexieren ta' gigabytes fuq kważi kull ħardwer tal-baġit, inklużi xi apparati b'bord wieħed, li huwa pjuttost frisk.
Jibqa' biss li titkejjel il-veloċità tal-irkupru tad-dejta (b'mod każwali) u tevalwa l-iskala meta jittieħdu kampjuni ta' lottijiet ta' daqsijiet differenti:
Benchmark tad-database
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. Tħejjija tal-lottijiet
Il-proċess kollu tal-preparazzjoni tal-lott jikkonsisti fil-passi li ġejjin:
Parsing ta 'diversi JSONs li fihom vettori ta' kordi b'koordinati ta 'punti.
Tpinġija ta 'linji kkuluriti bbażati fuq il-koordinati tal-punti fuq immaġni tad-daqs meħtieġ (per eżempju, 256 × 256 jew 128 × 128).
Konverżjoni tal-immaġini li jirriżultaw f'tensor.
Bħala parti mill-kompetizzjoni fost il-kernels Python, il-problema ġiet solvuta primarjament bl-użu OpenCV. Wieħed mill-aktar analogi sempliċi u ovvji f'R ikun jidher bħal dan:
L-implimentazzjoni ta' JSON għal Tensor Konverżjoni f'R
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)
}
It-tpinġija titwettaq bl-użu ta’ għodod R standard u ssejvjata f’PN temporanju maħżuna fir-RAM (fuq Linux, direttorji R temporanji jinsabu fid-direttorju /tmp, immuntat fir-RAM). Dan il-fajl imbagħad jinqara bħala firxa tridimensjonali b'numri li jvarjaw minn 0 sa 1. Dan huwa importanti għaliex BMP aktar konvenzjonali jinqara f'firxa mhux maħduma b'kodiċi tal-kulur hex.
Din l-implimentazzjoni dehret subottimali għalina, peress li l-formazzjoni ta 'lottijiet kbar tieħu żmien indeċentiment twil, u ddeċidejna li nieħdu vantaġġ mill-esperjenza tal-kollegi tagħna billi nużaw librerija b'saħħitha OpenCV. Dak iż-żmien ma kien hemm l-ebda pakkett lest għal R (issa m'hemm xejn), għalhekk implimentazzjoni minima tal-funzjonalità meħtieġa kienet miktuba f'C++ b'integrazzjoni fil-kodiċi R bl-użu Rcpp.
Biex issolvi l-problema, intużaw il-pakketti u l-libreriji li ġejjin:
OpenCV għall-ħidma ma 'immaġini u tpinġija linji. Użati libreriji tas-sistema installati minn qabel u fajls header, kif ukoll linking dinamiku.
xtensor għall-ħidma ma 'arrays multidimensjonali u tensors. Aħna użajna fajls header inklużi fil-pakkett R tal-istess isem. Il-librerija tippermettilek taħdem ma 'arrays multidimensjonali, kemm f'ordni maġġuri ta' ringiela kif ukoll f'ordni maġġuri tal-kolonna.
ndjson għall-parsing JSON. Din il-librerija tintuża fi xtensor awtomatikament jekk ikun preżenti fil-proġett.
RcppThread għall-organizzazzjoni ta 'proċessar multi-threaded ta' vettur minn JSON. Uża l-fajls header ipprovduti minn dan il-pakkett. Minn aktar popolari RcppParallel Il-pakkett, fost affarijiet oħra, għandu mekkaniżmu ta 'interruzzjoni ta' loop integrat.
Għandu jiġi nnutat li l- xtensor irriżulta li kien godsend: minbarra l-fatt li għandha funzjonalità estensiva u prestazzjoni għolja, l-iżviluppaturi tagħha rriżultaw li kienu pjuttost reattivi u wieġbu l-mistoqsijiet fil-pront u fid-dettall. Bl-għajnuna tagħhom, kien possibbli li jiġu implimentati trasformazzjonijiet ta 'matriċi OpenCV f'tensuri ta' xtensor, kif ukoll mod biex jgħaqqdu tensuri tal-immaġni 3-dimensjonali f'tensuri 4-dimensjonali tad-dimensjoni korretta (il-lott innifsu).
Biex niġbru fajls li jużaw fajls tas-sistema u konnessjoni dinamika mal-libreriji installati fis-sistema, użajna l-mekkaniżmu tal-plugin implimentat fil-pakkett Rcpp. Biex insibu awtomatikament mogħdijiet u bnadar, użajna utilità Linux popolari pkg-config.
Il-kodiċi ta 'implimentazzjoni għall-parsing JSON u l-ġenerazzjoni ta' lott għat-trażmissjoni lill-mudell huwa mogħti taħt l-ispoiler. L-ewwel, żid direttorju tal-proġett lokali biex tfittex fajls header (meħtieġa għal ndjson):
Implimentazzjoni ta 'JSON għal konverżjoni tat-tensor f'C++
// [[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;
}
Dan il-kodiċi għandu jitqiegħed fil-fajl src/cv_xt.cpp u ikkumpila mal-kmand Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); meħtieġa wkoll għax-xogħol nlohmann/json.hpp ta ' repożitorju. Il-kodiċi huwa maqsum f'diversi funzjonijiet:
to_xt — funzjoni b'mudell għat-trasformazzjoni ta' matriċi ta' immaġini (cv::Mat) għal tensur xt::xtensor;
Kif tistgħu taraw, iż-żieda fil-veloċità rriżultat li kienet sinifikanti ħafna, u mhux possibbli li tlaħħaq mal-kodiċi C++ billi titqabbad il-kodiċi R.
3. Iteraturi għall-ħatt ta' lottijiet mid-database
R għandu reputazzjoni mistħoqqa għall-ipproċessar tad-dejta li tidħol fir-RAM, filwaqt li Python huwa aktar ikkaratterizzat minn ipproċessar ta 'dejta iterattiv, li jippermettilek timplimenta faċilment u b'mod naturali kalkoli barra mill-qalba (kalkoli li jużaw memorja esterna). Eżempju klassiku u rilevanti għalina fil-kuntest tal-problema deskritta huwa netwerks newrali fonda mħarrġa bil-metodu ta 'niżla gradjent b'approssimazzjoni tal-gradjent f'kull pass bl-użu ta' porzjon żgħir ta 'osservazzjonijiet, jew mini-lott.
Oqfsa ta' tagħlim profond miktuba f'Python għandhom klassijiet speċjali li jimplimentaw iteraturi bbażati fuq data: tabelli, stampi f'folders, formati binarji, eċċ. Tista' tuża għażliet lesti jew tikteb tiegħek għal kompiti speċifiċi. F'R nistgħu nieħdu vantaġġ mill-karatteristiċi kollha tal-librerija Python keras bid-diversi backends tagħha li jużaw il-pakkett tal-istess isem, li min-naħa tiegħu jaħdem fuq il-pakkett retikulat. Dan tal-aħħar jistħoqqlu artiklu twil separat; mhux biss jippermettilek tmexxi kodiċi Python minn R, iżda tippermetti wkoll li tittrasferixxi oġġetti bejn sessjonijiet R u Python, awtomatikament twettaq il-konverżjonijiet tat-tip kollha meħtieġa.
Neħles mill-ħtieġa li naħżnu d-dejta kollha fir-RAM billi nużaw MonetDBlite, ix-xogħol kollu ta '"netwerk newrali" se jsir mill-kodiċi oriġinali f'Python, irridu biss niktbu iteratur fuq id-dejta, peress li m'hemm xejn lest għal sitwazzjoni bħal din jew f'R jew Python. Essenzjalment hemm biss żewġ rekwiżiti għaliha: għandu jirritorna lottijiet f'linja bla tarf u jiffranka l-istat tiegħu bejn iterazzjonijiet (din tal-aħħar f'R huwa implimentat bl-aktar mod sempliċi bl-użu ta 'għeluq). Preċedentement, kien meħtieġ li tikkonverti b'mod espliċitu l-arrays R f'arrays numpy ġewwa l-iteratur, iżda l-verżjoni attwali tal-pakkett keras tagħmel dan hi stess.
L-iteratur għat-taħriġ u d-dejta tal-validazzjoni rriżulta li kien kif ġej:
Iteratur għal data ta' taħriġ u validazzjoni
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)
}
}
Il-funzjoni tieħu bħala input varjabbli b'konnessjoni mad-database, in-numri ta' linji użati, in-numru ta' klassijiet, id-daqs tal-lott, l-iskala (scale = 1 jikkorrispondi għall-għoti ta' immaġini ta' 256x256 pixels, scale = 0.5 — 128x128 pixels), indikatur tal-kulur (color = FALSE jispeċifika rendering fil-griż meta jintuża color = TRUE kull stroke hija mfassla b'kulur ġdid) u indikatur ta 'preproċessar għal netwerks imħarrġa minn qabel fuq imagenet. Dan tal-aħħar huwa meħtieġ sabiex il-valuri tal-pixel jiġu skalati mill-intervall [0, 1] għall-intervall [-1, 1], li ntuża meta tħarreġ il-fornut. keras mudelli.
Il-funzjoni esterna fiha verifika tat-tip ta 'argument, tabella data.table b'numri tal-linja mħallta bl-addoċċ minn samples_index u numri tal-lott, counter u numru massimu ta 'lottijiet, kif ukoll espressjoni SQL għall-ħatt ta' data mid-database. Barra minn hekk, iddefinijna analogu mgħaġġel tal-funzjoni ġewwa keras::to_categorical(). Aħna użajna kważi d-dejta kollha għat-taħriġ, u ħallew nofs fil-mija għall-validazzjoni, għalhekk id-daqs tal-epoka kien limitat mill-parametru steps_per_epoch meta tissejjaħ keras::fit_generator(), u l-kundizzjoni if (i > max_i) ħadem biss għall-iteratur tal-validazzjoni.
Fil-funzjoni interna, l-indiċi tar-ringieli jiġu rkuprati għall-lott li jmiss, ir-rekords jinħattu mid-database bil-counter tal-lott jiżdied, parsing JSON (funzjoni cpp_process_json_vector(), miktuba f'C++) u toħloq arrays li jikkorrispondu għal stampi. Imbagħad jinħolqu vettori one-hot b'tikketti tal-klassi, matriċi b'valuri tal-pixel u tikketti huma kkombinati f'lista, li hija l-valur tar-ritorn. Biex tħaffef ix-xogħol, użajna l-ħolqien ta 'indiċi fit-tabelli data.table u modifika permezz tal-link - mingħajr dawn il-pakketti "ċipep" data.table Huwa pjuttost diffiċli li wieħed jimmaġina li taħdem b'mod effettiv ma 'kwalunkwe ammont sinifikanti ta' dejta f'R.
Ir-riżultati tal-kejl tal-veloċità fuq laptop Core i5 huma kif ġej:
Jekk għandek ammont suffiċjenti ta 'RAM, tista' tħaffef serjament it-tħaddim tad-database billi tittrasferiha għal din l-istess RAM (32 GB huwa biżżejjed għall-kompitu tagħna). Fil-Linux, il-partizzjoni hija mmuntata awtomatikament /dev/shm, li jokkupa sa nofs il-kapaċità RAM. Tista 'tenfasizza aktar billi teditja /etc/fstabbiex tikseb rekord simili tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Kun żgur li terġa 'tibda u tiċċekkja r-riżultat billi tħaddem il-kmand df -h.
L-iteratur għad-dejta tat-test jidher ħafna aktar sempliċi, peress li s-sett tad-dejta tat-test jidħol kompletament fir-RAM:
Iteratur għad-dejta tat-test
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. Għażla ta 'arkitettura mudell
L-ewwel arkitettura użata kienet mobilenet v1, li l-karatteristiċi tagħhom huma diskussi fi dan messaġġ. Huwa inkluż bħala standard keras u, għalhekk, huwa disponibbli fil-pakkett tal-istess isem għal R. Imma meta ppruvat tużah ma 'immaġini b'kanal wieħed, irriżulta ħaġa stramba: it-tensor tal-input għandu dejjem ikollu d-dimensjoni (batch, height, width, 3), jiġifieri, in-numru ta 'kanali ma jistax jinbidel. M'hemm l-ebda limitazzjoni bħal din f'Python, għalhekk għaġġelna u ktibna l-implimentazzjoni tagħna stess ta 'din l-arkitettura, wara l-artiklu oriġinali (mingħajr it-tneħħija li hija fil-verżjoni keras):
L-iżvantaġġi ta 'dan l-approċċ huma ovvji. Irrid nittestja ħafna mudelli, iżda għall-kuntrarju, ma rridx nikteb kull arkitettura manwalment. Ġejna wkoll imċaħħda mill-opportunità li nużaw il-piżijiet ta 'mudelli mħarrġa minn qabel fuq imagenet. Bħas-soltu, l-istudju tad-dokumentazzjoni għen. Funzjoni get_config() jippermettilek tikseb deskrizzjoni tal-mudell f'forma adattata għall-editjar (base_model_conf$layers - lista R regolari), u l-funzjoni from_config() twettaq il-konverżjoni inversa għal oġġett mudell:
Issa mhuwiex diffiċli li tikteb funzjoni universali biex tikseb xi wieħed mill-fornuti keras mudelli bi jew mingħajr piżijiet imħarrġa fuq imagenet:
Funzjoni għat-tagħbija ta 'arkitetturi lesti
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)
}
Meta tuża immaġini b'kanal wieħed, ma jintużaw l-ebda piżijiet imħarrġa minn qabel. Dan jista 'jiġi ffissat: bl-użu tal-funzjoni get_weights() ġib il-piżijiet tal-mudell fil-forma ta 'lista ta' arrays R, ibdel id-dimensjoni tal-ewwel element ta 'din il-lista (billi tieħu kanal ta' kulur wieħed jew tagħmel medja tat-tlieta), u mbagħad tagħbija l-piżijiet lura fil-mudell bil-funzjoni set_weights(). Aħna qatt ma żidna din il-funzjonalità, għaliex f'dan l-istadju kien diġà ċar li kien aktar produttiv li taħdem bi stampi bil-kulur.
Aħna wettaqna ħafna mill-esperimenti bl-użu tal-verżjonijiet tal-mobilenet 1 u 2, kif ukoll resnet34. Arkitetturi aktar moderni bħal SE-ResNeXt marru tajjeb f'din il-kompetizzjoni. Sfortunatament, ma kellniex implimentazzjonijiet lesti għad-dispożizzjoni tagħna, u ma ktibniex tagħna (iżda żgur se niktbu).
5. Parametrizzazzjoni ta 'skripts
Għall-konvenjenza, il-kodiċi kollu għall-bidu tat-taħriġ kien iddisinjat bħala skript wieħed, parametrizzat bl-użu docpt kif ġej:
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)
Pakkett docpt jirrappreżenta l-implimentazzjoni http://docopt.org/ għal R. Bl-għajnuna tagħha, skripts huma mnedija b'kmandi sempliċi bħal Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db jew ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, jekk fajl train_nn.R huwa eżekutibbli (dan il-kmand jibda jħarreġ il-mudell resnet50 fuq stampi bi tliet kuluri li jkejlu 128x128 pixels, id-database trid tkun tinsab fil-folder /home/andrey/doodle_db). Tista 'żżid il-veloċità tat-tagħlim, it-tip ta' ottimizzatur, u kwalunkwe parametru personalizzabbli ieħor mal-lista. Fil-proċess tat-tħejjija tal-pubblikazzjoni, irriżulta li l-arkitettura mobilenet_v2 mill-verżjoni attwali keras fl-użu R m'għandhomx minħabba bidliet mhux ikkunsidrati fil-pakkett R, aħna qed nistennew li jirranġawh.
Dan l-approċċ għamilha possibbli li jitħaffu b'mod sinifikanti l-esperimenti b'mudelli differenti meta mqabbla mat-tnedija aktar tradizzjonali ta 'skripts f'RStudio (nnotaw il-pakkett bħala alternattiva possibbli tfruns). Iżda l-vantaġġ ewlieni huwa l-abbiltà li tmexxi faċilment it-tnedija ta 'skripts f'Docker jew sempliċiment fuq is-server, mingħajr ma tinstalla RStudio għal dan.
6. Dockerization ta 'skripts
Aħna użajna Docker biex niżguraw il-portabbiltà tal-ambjent għal mudelli ta 'taħriġ bejn il-membri tat-tim u għal skjerament rapidu fil-cloud. Tista 'tibda tiffamiljarizza ma' din l-għodda, li hija relattivament mhux tas-soltu għal R programmer, bil dan sensiela ta’ pubblikazzjonijiet jew kors bil-vidjo.
Docker jippermettilek kemm toħloq immaġini tiegħek mill-bidu kif ukoll tuża stampi oħra bħala bażi għall-ħolqien tiegħek. Meta analizzajna l-għażliet disponibbli, wasalna għall-konklużjoni li l-installazzjoni tas-sewwieqa NVIDIA, CUDA + cuDNN u libreriji Python hija parti pjuttost voluminuża tal-immaġni, u ddeċidejna li nieħdu l-immaġni uffiċjali bħala bażi tensorflow/tensorflow:1.12.0-gpu, billi żżid il-pakketti R meħtieġa hemmhekk.
Għall-konvenjenza, il-pakketti użati tpoġġew f'varjabbli; il-biċċa l-kbira tal-iskripts miktuba huma kkupjati ġewwa l-kontenituri waqt l-assemblaġġ. Bdilna wkoll il-qoxra tal-kmand għal /bin/bash għall-faċilità tal-użu tal-kontenut /etc/os-release. Dan evita l-ħtieġa li tiġi speċifikata l-verżjoni OS fil-kodiċi.
Barra minn hekk, inkiteb script bash żgħir li jippermettilek tniedi kontenitur b'diversi kmandi. Pereżempju, dawn jistgħu jkunu skripts għat-taħriġ tan-netwerks newrali li qabel kienu mqiegħda ġewwa l-kontenitur, jew qoxra tal-kmand għad-debugging u l-monitoraġġ tal-operat tal-kontenitur:
Script biex tniedi l-kontenitur
#!/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}
Jekk dan l-iskript bash jitmexxa mingħajr parametri, l-iskript se jissejjaħ ġewwa l-kontenitur train_nn.R b'valuri awtomatiċi; jekk l-ewwel argument pożizzjonali huwa "bash", allura l-kontenitur se jibda b'mod interattiv b'qoxra ta 'kmand. Fil-każijiet l-oħra kollha, il-valuri tal-argumenti pożizzjonali huma sostitwiti: CMD="Rscript /app/train_nn.R $@".
Ta 'min jinnota li d-direttorji b'data tas-sors u database, kif ukoll id-direttorju għall-iffrankar ta' mudelli mħarrġa, huma mmuntati ġewwa l-kontenitur mis-sistema ospitanti, li jippermettilek li taċċessa r-riżultati tal-iskripts mingħajr manipulazzjonijiet bla bżonn.
7. L-użu ta 'GPU multipli fuq Google Cloud
Waħda mill-karatteristiċi tal-kompetizzjoni kienet id-dejta storbjuża ħafna (ara l-istampa tat-titlu, mislufa minn @Leigh.plt minn ODS slack). Lottijiet kbar jgħinu fil-ġlieda kontra dan, u wara esperimenti fuq PC b'1 GPU, iddeċidejna li nikkontrollaw mudelli ta 'taħriġ fuq diversi GPUs fil-cloud. Użat GoogleCloud (gwida tajba għall-affarijiet bażiċi) minħabba l-għażla kbira ta 'konfigurazzjonijiet disponibbli, prezzijiet raġonevoli u bonus ta' $300. Minn regħba, ordnajt eżempju 4xV100 b'SSD u ton ta 'RAM, u dan kien żball kbir. Magna bħal din tiekol flus malajr; tista 'tmur tkisser tesperimenta mingħajr pipeline ippruvat. Għal skopijiet edukattivi, huwa aħjar li tieħu l-K80. Iżda l-ammont kbir ta 'RAM ġie utli - is-sħab SSD ma impressjonax bil-prestazzjoni tiegħu, għalhekk id-database ġiet trasferita għal dev/shm.
Ta 'l-akbar interess huwa l-framment tal-kodiċi responsabbli għall-użu ta' GPU multipli. L-ewwel, il-mudell jinħoloq fuq is-CPU billi juża maniġer tal-kuntest, bħal f'Python:
It-teknika klassika tal-iffriżar tas-saffi kollha ħlief l-aħħar wieħed, it-taħriġ tal-aħħar saff, l-unfreezing u t-taħriġ mill-ġdid tal-mudell kollu għal diversi GPUs ma setgħetx tiġi implimentata.
It-taħriġ kien immonitorjat mingħajr użu. tensorboard, nillimitaw lilna nfusna biex nirreġistraw zkuk u nsalvaw mudelli b'ismijiet informattivi wara kull epoka:
Għadd ta' problemi li ltqajna magħhom għadhom ma ġewx megħluba:
в keras m'hemm l-ebda funzjoni lesta biex titfittex awtomatikament l-aħjar rata ta' tagħlim (analogu lr_finder fil-librerija fast.ai); B'xi sforz, huwa possibbli li l-implimentazzjonijiet ta' partijiet terzi jiġu trasferiti għal R, pereżempju, dan;
bħala konsegwenza tal-punt preċedenti, ma kienx possibbli li tagħżel il-veloċità tat-taħriġ korretta meta tuża diversi GPUs;
hemm nuqqas ta 'arkitetturi moderni tan-netwerk newrali, speċjalment dawk imħarrġa minn qabel fuq imagenet;
l-ebda politika ta' ċiklu wieħed u rati ta' tagħlim diskriminattivi (l-ittemprar tal-cosine kien fuq talba tagħna implimentati, Grazzi skeydan).
X'affarijiet utli tgħallmu minn din il-kompetizzjoni:
Fuq ħardwer ta 'enerġija relattivament baxxa, tista' taħdem b'volumi ta 'dejta deċenti (ħafna drabi d-daqs ta' RAM) mingħajr uġigħ. Borża tal-plastik data.table jiffranka l-memorja minħabba modifika fil-post tat-tabelli, li tevita li tikkopjahom, u meta tintuża b'mod korrett, il-kapaċitajiet tagħha kważi dejjem juru l-ogħla veloċità fost l-għodod kollha magħrufa lilna għal-lingwi tal-iskript. L-iffrankar tad-dejta f'database jippermettilek, f'ħafna każijiet, li ma taħseb xejn dwar il-ħtieġa li tagħfas id-dataset kollu fir-RAM.
Funzjonijiet bil-mod f'R jistgħu jiġu sostitwiti b'dawk veloċi f'C++ bl-użu tal-pakkett Rcpp. Jekk minbarra l-użu RcppThread jew RcppParallel, Ikollna implimentazzjonijiet multi-kamin multi-pjattaformi, għalhekk m'hemmx bżonn li nipparallelizzaw il-kodiċi fil-livell R.
Pakkett Rcpp jista 'jintuża mingħajr għarfien serju ta' C++, il-minimu meħtieġ huwa deskritt fil-qosor hawn. Fajls header għal numru ta 'libreriji C jibred simili xtensor disponibbli fuq CRAN, jiġifieri, qed tiġi ffurmata infrastruttura għall-implimentazzjoni ta 'proġetti li jintegraw kodiċi C++ ta' prestazzjoni għolja lest f'R. Konvenjenza addizzjonali hija l-enfasi tas-sintassi u analizzatur statiku tal-kodiċi C++ f'RStudio.
docpt jippermettilek tmexxi skripts awtonomi b'parametri. Dan huwa konvenjenti għall-użu fuq server remot, inkl. taħt docker. F'RStudio, huwa inkonvenjenti li twettaq ħafna sigħat ta 'esperimenti b'netwerks newrali ta' taħriġ, u l-installazzjoni tal-IDE fuq is-server innifsu mhux dejjem ikun iġġustifikat.
Docker jiżgura l-portabbiltà tal-kodiċi u r-riproduċibbiltà tar-riżultati bejn l-iżviluppaturi b'verżjonijiet differenti tal-OS u l-libreriji, kif ukoll faċilità ta 'eżekuzzjoni fuq servers. Tista 'tniedi l-pipeline kollu tat-taħriġ bi kmand wieħed biss.
Google Cloud huwa mod faċli għall-baġit biex tesperimenta fuq ħardwer għali, iżda trid tagħżel il-konfigurazzjonijiet bir-reqqa.
Il-kejl tal-veloċità tal-frammenti tal-kodiċi individwali huwa utli ħafna, speċjalment meta tgħaqqad R u C++, u mal-pakkett bank - wkoll faċli ħafna.
B’mod ġenerali din l-esperjenza kienet ta’ sodisfazzjon kbir u nkomplu naħdmu biex insolvu wħud mill-kwistjonijiet imqajma.