ProHoster > Blog > башкаруу > Quick Draw Doodle таануу: R, C++ жана нейрон тармактары менен кантип достошсо болот
Quick Draw Doodle таануу: R, C++ жана нейрон тармактары менен кантип достошсо болот
Эй Хабр!
Өткөн жылдын күзүндө Kaggle кол менен тартылган сүрөттөрдү классификациялоо боюнча сынак өткөрдү, Quick Draw Doodle таануу, ага башкалардын арасында R-илимпоздор командасы катышты: Артем Клевцова, Филипп менеджер и Андрей Огурцов. Биз мелдешти майда-чүйдөсүнө чейин сүрөттөп бербейбиз, ал буга чейин жасалган акыркы басылма.
Бул жолу медаль чарбасы менен майнап чыккан жок, бирок бир топ баалуу тажрыйба топтолду, ошондуктан мен коомчулукка Kagle жана күнүмдүк жумушта бир катар эң кызыктуу жана пайдалуу нерселерди айткым келет. Талкууланган темалардын арасында: ансыз оор жашоо OpenCV, JSON талдоо (бул мисалдар C++ кодунун скрипттерге же пакеттерге интеграциясын карап чыгат. Rcpp), сценарийлерди параметрлештирүү жана акыркы чечимди докерлештирүү. Аткаруу үчүн ылайыктуу формадагы билдирүүнүн бардык коду жеткиликтүү репозиторийлер.
1. CSVден маалыматтарды MonetDB маалымат базасына эффективдүү жүктөңүз
Бул сынактагы маалыматтар даяр сүрөттөр түрүндө эмес, чекит координаттары бар JSONдарды камтыган 340 CSV файлдары (ар бир класс үчүн бир файл) түрүндө берилген. Бул чекиттерди сызыктар менен туташтыруу менен биз 256x256 пиксел өлчөмүндөгү акыркы сүрөттү алабыз. Ошондой эле ар бир жазуу үчүн маалымат топтомун чогултуу учурунда колдонулган классификатор тарабынан сүрөт туура таанылган-таанылбаганын көрсөтүүчү этикетка, сүрөттүн авторунун жашаган өлкөнүн эки тамгалуу коду, уникалдуу идентификатор, убакыт белгиси бар. жана файл атына дал келген класстын аталышы. Түпнуска берилиштердин жөнөкөйлөштүрүлгөн версиясы архивде 7.4 ГБ жана таңгактан чыгаргандан кийин болжол менен 20 ГБ салмакты түзөт, таңгактан чыгаргандан кийин толук маалымат 240 ГБ ээлейт. Уюштуруучулар эки версиянын тең чиймелердин бирдей болушун камсыз кылышкан, демек толук версия ашыкча болгон. Кандай болгон күндө да, графикалык файлдарда же массивдер түрүндө 50 миллион сүрөттөрдү сактоо дароо эле пайдасыз деп табылды жана биз архивдеги бардык CSV файлдарын бириктирүүнү чечтик. train_simplified.zip Ар бир партия үчүн керектүү өлчөмдөгү сүрөттөрдү кийинки генерациялоо менен маалымат базасына киргизүү.
МБС катары жакшы далилденген система тандалды MonetDB, тактап айтканда R үчүн пакет катары ишке ашыруу MonetDBLite. Пакет маалымат базасы серверинин орнотулган версиясын камтыйт жана серверди түздөн-түз R сеансынан алып, ал жерде аны менен иштөөгө мүмкүндүк берет. Маалыматтар базасын түзүү жана ага кошулуу бир буйрук менен ишке ашырылат:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
Бизге эки таблица түзүшүбүз керек болот: бири бардык маалыматтар үчүн, экинчиси жүктөлүп алынган файлдар жөнүндө тейлөө маалыматы үчүн (эгер бир нерсе туура эмес болуп калса жана процессти бир нече файлдарды жүктөп алгандан кийин кайра баштоо керек болсо пайдалуу):
Маалыматтар базасына маалыматтарды жүктөөнүн эң ылдам жолу CSV файлдарын SQL - буйругун колдонуп түздөн-түз көчүрүү болгон COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTкайда tablename - столдун аталышы жана path - файлга жол. Архив менен иш алып баруу маалында анын ичине ишке киргизилгени аныкталган unzip R ичинде архивдеги бир катар файлдар менен туура иштебейт, ошондуктан биз системаны колдондук unzip (параметрди колдонуу менен getOption("unzip")).
Маалыматтар базасына жазуу функциясы
#' @title Извлечение и загрузка файлов
#'
#' @description
#' Извлечение CSV-файлов из ZIP-архива и загрузка их в базу данных
#'
#' @param con Объект подключения к базе данных (класс `MonetDBEmbeddedConnection`).
#' @param tablename Название таблицы в базе данных.
#' @oaram zipfile Путь к ZIP-архиву.
#' @oaram filename Имя файла внури ZIP-архива.
#' @param preprocess Функция предобработки, которая будет применена извлечённому файлу.
#' Должна принимать один аргумент `data` (объект `data.table`).
#'
#' @return `TRUE`.
#'
upload_file <- function(con, tablename, zipfile, filename, preprocess = NULL) {
# Проверка аргументов
checkmate::assert_class(con, "MonetDBEmbeddedConnection")
checkmate::assert_string(tablename)
checkmate::assert_string(filename)
checkmate::assert_true(DBI::dbExistsTable(con, tablename))
checkmate::assert_file_exists(zipfile, access = "r", extension = "zip")
checkmate::assert_function(preprocess, args = c("data"), null.ok = TRUE)
# Извлечение файла
path <- file.path(tempdir(), filename)
unzip(zipfile, files = filename, exdir = tempdir(),
junkpaths = TRUE, unzip = getOption("unzip"))
on.exit(unlink(file.path(path)))
# Применяем функция предобработки
if (!is.null(preprocess)) {
.data <- data.table::fread(file = path)
.data <- preprocess(data = .data)
data.table::fwrite(x = .data, file = path, append = FALSE)
rm(.data)
}
# Запрос к БД на импорт CSV
sql <- sprintf(
"COPY OFFSET 2 INTO %s FROM '%s' USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORT",
tablename, path
)
# Выполнение запроса к БД
DBI::dbExecute(con, sql)
# Добавление записи об успешной загрузке в служебную таблицу
DBI::dbExecute(con, sprintf("INSERT INTO upload_log(file_name, uploaded) VALUES('%s', true)",
filename))
return(invisible(TRUE))
}
Эгерде сиз таблицаны маалымат базасына жазуудан мурун трансформациялооңуз керек болсо, анда аргументке өтүү жетиштүү preprocess маалыматтарды өзгөртө турган функция.
Маалыматтар базасына маалыматтарды ырааттуу жүктөө үчүн код:
Маалыматтар базасына маалыматтарды жазуу
# Список файлов для записи
files <- unzip(zipfile, list = TRUE)$Name
# Список исключений, если часть файлов уже была загружена
to_skip <- DBI::dbGetQuery(con, "SELECT file_name FROM upload_log")[[1L]]
files <- setdiff(files, to_skip)
if (length(files) > 0L) {
# Запускаем таймер
tictoc::tic()
# Прогресс бар
pb <- txtProgressBar(min = 0L, max = length(files), style = 3)
for (i in seq_along(files)) {
upload_file(con = con, tablename = "doodles",
zipfile = zipfile, filename = files[i])
setTxtProgressBar(pb, i)
}
close(pb)
# Останавливаем таймер
tictoc::toc()
}
# 526.141 sec elapsed - копирование SSD->SSD
# 558.879 sec elapsed - копирование USB->SSD
Маалыматты жүктөө убактысы колдонулган дисктин ылдамдыгына жараша өзгөрүшү мүмкүн. Биздин учурда, бир SSD ичинде же флэш-дисктен (булак файлынан) SSDге (МБ) окуу жана жазуу 10 мүнөткө жетпеген убакытты алат.
Бүтүн класс белгиси жана индекс тилкеси (ORDERED INDEX) партияларды түзүүдө байкоолор тандалып алынуучу сап номерлери менен:
Кошумча мамычаларды жана индексти түзүү
message("Generate lables")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD label_int int"))
invisible(DBI::dbExecute(con, "UPDATE doodles SET label_int = dense_rank() OVER (ORDER BY word) - 1"))
message("Generate row numbers")
invisible(DBI::dbExecute(con, "ALTER TABLE doodles ADD id serial"))
invisible(DBI::dbExecute(con, "CREATE ORDERED INDEX doodles_id_ord_idx ON doodles(id)"))
Учурунда партияны түзүү маселесин чечүү үчүн, биз үстөлдөн кокус саптарды чыгаруунун максималдуу ылдамдыгына жетишишибиз керек болчу. doodles. Бул үчүн биз 3 трюк колдондук. Биринчиси, байкоо идентификаторун сактаган түрдүн өлчөмдүүлүгүн азайтуу болгон. Түпнуска маалымат топтомунда ID сактоо үчүн талап кылынган түрү bigint, бирок байкоолордун саны алардын идентификаторлорун иреттик санга барабар түргө туура келтирүүгө мүмкүндүк берет. int. Бул учурда издөө бир топ ылдам болот. Экинчи куулук колдонуу болду ORDERED INDEX — Биз бул чечимге бардыгын карап чыгып, эмпирикалык жол менен келдик параметрлери. Үчүнчүсү параметрленген сурамдарды колдонуу болгон. Методдун маңызы – буйрукту бир жолу аткаруу PREPARE ошол эле түрдөгү сурамдардын тобун түзүүдө даярдалган туюнтманы кийинчерээк колдонуу менен, бирок чындыгында жөнөкөйгө салыштырмалуу артыкчылыгы бар SELECT статистикалык катанын чегинде болуп чыкты.
Маалыматтарды жүктөө процесси 450 МБ оперативдүү эстутумду талап кылат. Башкача айтканда, сүрөттөлгөн ыкма дээрлик бардык бюджеттик жабдыктарга, анын ичинде кээ бир бир такталуу түзмөктөргө салмагы ондогон гигабайт болгон маалымат топтомдорун жылдырууга мүмкүндүк берет, бул абдан сонун.
Болгону (кокус) маалыматтарды алуу ылдамдыгын өлчөө жана ар кандай өлчөмдөгү партиялардын үлгүлөрүн алууда масштабды баалоо гана калды:
Берилиштер базасынын эталону
library(ggplot2)
set.seed(0)
# Подключение к базе данных
con <- DBI::dbConnect(MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
# Функция для подготовки запроса на стороне сервера
prep_sql <- function(batch_size) {
sql <- sprintf("PREPARE SELECT id FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ","))
res <- DBI::dbSendQuery(con, sql)
return(res)
}
# Функция для извлечения данных
fetch_data <- function(rs, batch_size) {
ids <- sample(seq_len(n), batch_size)
res <- DBI::dbFetch(DBI::dbBind(rs, as.list(ids)))
return(res)
}
# Проведение замера
res_bench <- bench::press(
batch_size = 2^(4:10),
{
rs <- prep_sql(batch_size)
bench::mark(
fetch_data(rs, batch_size),
min_iterations = 50L
)
}
)
# Параметры бенчмарка
cols <- c("batch_size", "min", "median", "max", "itr/sec", "total_time", "n_itr")
res_bench[, cols]
# batch_size min median max `itr/sec` total_time n_itr
# <dbl> <bch:tm> <bch:tm> <bch:tm> <dbl> <bch:tm> <int>
# 1 16 23.6ms 54.02ms 93.43ms 18.8 2.6s 49
# 2 32 38ms 84.83ms 151.55ms 11.4 4.29s 49
# 3 64 63.3ms 175.54ms 248.94ms 5.85 8.54s 50
# 4 128 83.2ms 341.52ms 496.24ms 3.00 16.69s 50
# 5 256 232.8ms 653.21ms 847.44ms 1.58 31.66s 50
# 6 512 784.6ms 1.41s 1.98s 0.740 1.1m 49
# 7 1024 681.7ms 2.72s 4.06s 0.377 2.16m 49
ggplot(res_bench, aes(x = factor(batch_size), y = median, group = 1)) +
geom_point() +
geom_line() +
ylab("median time, s") +
theme_minimal()
DBI::dbDisconnect(con, shutdown = TRUE)
2. Партияларды даярдоо
Бүт партияны даярдоо процесси төмөнкү этаптардан турат:
чекиттердин координаттары менен саптардын векторлорун камтыган бир нече JSON талдоо.
Керектүү өлчөмдөгү сүрөткө чекиттердин координаттарынын негизинде түстүү сызыктарды тартуу (мисалы, 256×256 же 128×128).
Алынган сүрөттөрдү тензорго айландыруу.
Python ядролорунун ортосундагы мелдештин алкагында көйгөй биринчи кезекте колдонуу менен чечилди OpenCV. Rдеги эң жөнөкөй жана эң айкын аналогдордун бири мындай болот:
JSONди тензордук конверсияга 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)
}
Тартуу стандарттык R куралдарынын жардамы менен аткарылат жана RAMда сакталган убактылуу PNGге сакталат (Linux'та, убактылуу R каталогдору каталогдо жайгашкан. /tmp, RAMга орнотулган). Бул файл андан кийин 0дөн 1ге чейинки сандары бар үч өлчөмдүү массив катары окулат. Бул маанилүү, анткени кадимки BMP он алтылык түстүү коддору бар чийки массивде окулат.
Бул ишке ашыруу биз үчүн оптималдуу эместей көрүндү, анткени чоң партияларды түзүү өтө көп убакытты талап кылат жана биз күчтүү китепкананы колдонуу менен кесиптештерибиздин тажрыйбасынан пайдаланууну чечтик. OpenCV. Ал убакта R үчүн даяр пакет болгон эмес (азыр жок), ошондуктан талап кылынган функциянын минималдуу ишке ашырылышы C++ тилинде R кодуна интеграциялоо менен жазылган. Rcpp.
Көйгөйдү чечүү үчүн төмөнкү пакеттер жана китепканалар колдонулган:
OpenCV сүрөттөр жана сызыктар менен иштөө үчүн. Алдын ала орнотулган системалык китепканалар жана баш файлдар, ошондой эле динамикалык байланыштар колдонулат.
xtensor көп өлчөмдүү массивдер жана тензорлор менен иштөө үчүн. Ошол эле аталыштагы R пакетине кирген баш файлдарды колдондук. Китепкана көп өлчөмдүү массивдер менен негизги катарда да, негизги тилкеде да иштөөгө мүмкүндүк берет.
ndjson JSON талдоо үчүн. Бул китепкана колдонулат xtensor ал долбоордо бар болсо, автоматтык түрдө.
RcppThread JSONден векторду көп жиптүү иштетүүнү уюштуруу үчүн. Бул топтом тарабынан берилген баш файлдар колдонулган. Популярдуураак RcppParallel Пакет, башка нерселер менен катар, орнотулган циклди үзүү механизмине ээ.
Бул белгилей кетсек болот, xtensor бул кудайдын сыйы болуп чыкты: анын кеңири функционалдуулугу жана жогорку өндүрүмдүүлүгү бар экендигинен тышкары, аны иштеп чыгуучулар абдан жоопкер болуп чыкты жана суроолорго ыкчам жана деталдуу жооп беришти. Алардын жардамы менен OpenCV матрицаларын кстензордук тензорлорго трансформациялоо, ошондой эле 3 өлчөмдүү сүрөт тензорлорун туура өлчөмдөгү 4 өлчөмдүү тензорго айкалыштыруу ыкмасын ишке ашыруу мүмкүн болду (партиянын өзү).
Rcpp, xtensor жана RcppThread үйрөнүү үчүн материалдар
Системалык файлдарды жана системада орнотулган китепканалар менен динамикалык байланышты колдонгон файлдарды компиляциялоо үчүн биз пакетте ишке ашырылган плагин механизмин колдондук. Rcpp. Автоматтык түрдө жолдорду жана желектерди табуу үчүн биз популярдуу Linux утилитасын колдондук pkg-config.
OpenCV китепканасын колдонуу үчүн Rcpp плагинин ишке ашыруу
JSON талдоо жана моделге өткөрүү үчүн партияны түзүү үчүн ишке ашыруу коду спойлердин астында берилген. Биринчиден, баш файлдарды издөө үчүн жергиликтүү долбоордун каталогун кошуңуз (ndjson үчүн керек):
C++ тилинде тензордук конверсияга JSONди ишке ашыруу
// [[Rcpp::plugins(cpp14)]]
// [[Rcpp::plugins(opencv)]]
// [[Rcpp::depends(xtensor)]]
// [[Rcpp::depends(RcppThread)]]
#include <xtensor/xjson.hpp>
#include <xtensor/xadapt.hpp>
#include <xtensor/xview.hpp>
#include <xtensor-r/rtensor.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <Rcpp.h>
#include <RcppThread.h>
// Синонимы для типов
using RcppThread::parallelFor;
using json = nlohmann::json;
using points = xt::xtensor<double,2>; // Извлечённые из JSON координаты точек
using strokes = std::vector<points>; // Извлечённые из JSON координаты точек
using xtensor3d = xt::xtensor<double, 3>; // Тензор для хранения матрицы изоображения
using xtensor4d = xt::xtensor<double, 4>; // Тензор для хранения множества изображений
using rtensor3d = xt::rtensor<double, 3>; // Обёртка для экспорта в R
using rtensor4d = xt::rtensor<double, 4>; // Обёртка для экспорта в R
// Статические константы
// Размер изображения в пикселях
const static int SIZE = 256;
// Тип линии
// См. https://en.wikipedia.org/wiki/Pixel_connectivity#2-dimensional
const static int LINE_TYPE = cv::LINE_4;
// Толщина линии в пикселях
const static int LINE_WIDTH = 3;
// Алгоритм ресайза
// https://docs.opencv.org/3.1.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121
const static int RESIZE_TYPE = cv::INTER_LINEAR;
// Шаблон для конвертирования OpenCV-матрицы в тензор
template <typename T, int NCH, typename XT=xt::xtensor<T,3,xt::layout_type::column_major>>
XT to_xt(const cv::Mat_<cv::Vec<T, NCH>>& src) {
// Размерность целевого тензора
std::vector<int> shape = {src.rows, src.cols, NCH};
// Общее количество элементов в массиве
size_t size = src.total() * NCH;
// Преобразование cv::Mat в xt::xtensor
XT res = xt::adapt((T*) src.data, size, xt::no_ownership(), shape);
return res;
}
// Преобразование JSON в список координат точек
strokes parse_json(const std::string& x) {
auto j = json::parse(x);
// Результат парсинга должен быть массивом
if (!j.is_array()) {
throw std::runtime_error("'x' must be JSON array.");
}
strokes res;
res.reserve(j.size());
for (const auto& a: j) {
// Каждый элемент массива должен быть 2-мерным массивом
if (!a.is_array() || a.size() != 2) {
throw std::runtime_error("'x' must include only 2d arrays.");
}
// Извлечение вектора точек
auto p = a.get<points>();
res.push_back(p);
}
return res;
}
// Отрисовка линий
// Цвета HSV
cv::Mat ocv_draw_lines(const strokes& x, bool color = true) {
// Исходный тип матрицы
auto stype = color ? CV_8UC3 : CV_8UC1;
// Итоговый тип матрицы
auto dtype = color ? CV_32FC3 : CV_32FC1;
auto bg = color ? cv::Scalar(0, 0, 255) : cv::Scalar(255);
auto col = color ? cv::Scalar(0, 255, 220) : cv::Scalar(0);
cv::Mat img = cv::Mat(SIZE, SIZE, stype, bg);
// Количество линий
size_t n = x.size();
for (const auto& s: x) {
// Количество точек в линии
size_t n_points = s.shape()[1];
for (size_t i = 0; i < n_points - 1; ++i) {
// Точка начала штриха
cv::Point from(s(0, i), s(1, i));
// Точка окончания штриха
cv::Point to(s(0, i + 1), s(1, i + 1));
// Отрисовка линии
cv::line(img, from, to, col, LINE_WIDTH, LINE_TYPE);
}
if (color) {
// Меняем цвет линии
col[0] += 180 / n;
}
}
if (color) {
// Меняем цветовое представление на RGB
cv::cvtColor(img, img, cv::COLOR_HSV2RGB);
}
// Меняем формат представления на float32 с диапазоном [0, 1]
img.convertTo(img, dtype, 1 / 255.0);
return img;
}
// Обработка JSON и получение тензора с данными изображения
xtensor3d process(const std::string& x, double scale = 1.0, bool color = true) {
auto p = parse_json(x);
auto img = ocv_draw_lines(p, color);
if (scale != 1) {
cv::Mat out;
cv::resize(img, out, cv::Size(), scale, scale, RESIZE_TYPE);
cv::swap(img, out);
out.release();
}
xtensor3d arr = color ? to_xt<double,3>(img) : to_xt<double,1>(img);
return arr;
}
// [[Rcpp::export]]
rtensor3d cpp_process_json_str(const std::string& x,
double scale = 1.0,
bool color = true) {
xtensor3d res = process(x, scale, color);
return res;
}
// [[Rcpp::export]]
rtensor4d cpp_process_json_vector(const std::vector<std::string>& x,
double scale = 1.0,
bool color = false) {
size_t n = x.size();
size_t dim = floor(SIZE * scale);
size_t channels = color ? 3 : 1;
xtensor4d res({n, dim, dim, channels});
parallelFor(0, n, [&x, &res, scale, color](int i) {
xtensor3d tmp = process(x[i], scale, color);
auto view = xt::view(res, i, xt::all(), xt::all(), xt::all());
view = tmp;
});
return res;
}
Бул код файлга жайгаштырылышы керек src/cv_xt.cpp жана буйругу менен түзүңүз Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); жумуш үчүн да талап кылынат nlohmann/json.hpp чейин репозиторий. Код бир нече функцияларга бөлүнөт:
to_xt — сүрөт матрицасын өзгөртүү үчүн шаблондук функция (cv::Mat) тензорго xt::xtensor;
parse_json — функция JSON сабын талдайт, чекиттердин координаталарын чыгарып, аларды векторго топтойт;
ocv_draw_lines — алынган чекиттердин векторунан көп түстүү сызыктарды тартат;
process — жогорудагы функцияларды айкалыштырат, ошондой эле пайда болгон сүрөттү масштабдоо мүмкүнчүлүгүн кошот;
cpp_process_json_str - функциянын үстүнө ороп коюу process, натыйжаны R-объектке (көп өлчөмдүү массив) экспорттойт;
cpp_process_json_vector - функциянын үстүнө ороп коюу cpp_process_json_str, бул сизге көп жиптүү режимде сап векторун иштетүүгө мүмкүндүк берет.
Көп түстүү сызыктарды тартуу үчүн HSV түс модели колдонулган, андан кийин RGBге айландырылган. Келгиле, натыйжаны сынап көрөлү:
Көрүнүп тургандай, ылдамдыктын өсүшү абдан маанилүү болуп чыкты жана R кодун параллелизациялоо менен C++ кодун кууп чыгуу мүмкүн эмес.
3. Маалымат базасынан партияларды түшүрүү үчүн итераторлор
R оперативдүү эстутумга туура келген маалыматтарды иштетүү боюнча татыктуу репутацияга ээ, ал эми Python маалыматтарды кайталанма иштетүү менен мүнөздөлөт, бул сизге өзөктөн тышкаркы эсептөөлөрдү (тышкы эстутумду колдонуу менен эсептөөлөрдү) оңой жана табигый түрдө ишке ашырууга мүмкүндүк берет. Сүрөттөлгөн маселенин контекстинде биз үчүн классикалык жана актуалдуу мисал болуп градиенттин түшүү ыкмасы менен үйрөтүлгөн терең нейрон тармактары, байкоолордун аз бөлүгүн же мини-партияны колдонуу менен ар бир кадамда градиентти жакындатуу болуп саналат.
Python тилинде жазылган терең үйрөнүү фреймворктеринде маалыматтарга негизделген итераторлорду ишке ашыруучу атайын класстар бар: таблицалар, папкалардагы сүрөттөр, бинардык форматтар ж.б. Сиз даяр варианттарды колдонсоңуз же конкреттүү тапшырмалар үчүн өзүңүздүн жазсаңыз болот. R тилинде биз Python китепканасынын бардык мүмкүнчүлүктөрүнөн пайдалана алабыз кызыл өз кезегинде пакеттин үстүндө иштейт, ошол эле аталыштагы пакетти колдонуп, анын ар кандай аркалары менен торчо. Акыркысы өзүнчө узун макалага татыктуу; ал сизге Python кодун Rден иштетүүгө гана мүмкүндүк бербестен, R жана Python сеанстарынын ортосунда объекттерди өткөрүүгө мүмкүндүк берет, автоматтык түрдө бардык зарыл түрдөгү конверсияларды аткарат.
MonetDBLite аркылуу биз оперативдик эс тутумда бардык маалыматтарды сактоо зарылдыгынан арылдык, бардык "нейрондук тармак" иштери Pythonдогу баштапкы код менен аткарылат, биз жөн гана дайындардын үстүнөн итераторду жазышыбыз керек, анткени даяр эч нерсе жок. же R же Python мындай кырдаал үчүн. Ал үчүн эки гана талап бар: ал партияларды чексиз циклде кайтарып, итерациялардын ортосунда өз абалын сактоосу керек (акыркы R-де жабууларды колдонуу менен эң жөнөкөй жол менен ишке ашырылат). Буга чейин, R массивдерин итератордун ичиндеги сандык массивдерге ачык айландыруу талап кылынган, бирок пакеттин учурдагы версиясы кызыл өзү жасайт.
Окутуу жана текшерүү маалыматтары үчүн итератор төмөнкүдөй болуп чыкты:
Окутуу жана текшерүү маалыматтары үчүн итератор
train_generator <- function(db_connection = con,
samples_index,
num_classes = 340,
batch_size = 32,
scale = 1,
color = FALSE,
imagenet_preproc = FALSE) {
# Проверка аргументов
checkmate::assert_class(con, "DBIConnection")
checkmate::assert_integerish(samples_index)
checkmate::assert_count(num_classes)
checkmate::assert_count(batch_size)
checkmate::assert_number(scale, lower = 0.001, upper = 5)
checkmate::assert_flag(color)
checkmate::assert_flag(imagenet_preproc)
# Перемешиваем, чтобы брать и удалять использованные индексы батчей по порядку
dt <- data.table::data.table(id = sample(samples_index))
# Проставляем номера батчей
dt[, batch := (.I - 1L) %/% batch_size + 1L]
# Оставляем только полные батчи и индексируем
dt <- dt[, if (.N == batch_size) .SD, keyby = batch]
# Устанавливаем счётчик
i <- 1
# Количество батчей
max_i <- dt[, max(batch)]
# Подготовка выражения для выгрузки
sql <- sprintf(
"PREPARE SELECT drawing, label_int FROM doodles WHERE id IN (%s)",
paste(rep("?", batch_size), collapse = ",")
)
res <- DBI::dbSendQuery(con, sql)
# Аналог keras::to_categorical
to_categorical <- function(x, num) {
n <- length(x)
m <- numeric(n * num)
m[x * n + seq_len(n)] <- 1
dim(m) <- c(n, num)
return(m)
}
# Замыкание
function() {
# Начинаем новую эпоху
if (i > max_i) {
dt[, id := sample(id)]
data.table::setkey(dt, batch)
# Сбрасываем счётчик
i <<- 1
max_i <<- dt[, max(batch)]
}
# ID для выгрузки данных
batch_ind <- dt[batch == i, id]
# Выгрузка данных
batch <- DBI::dbFetch(DBI::dbBind(res, as.list(batch_ind)), n = -1)
# Увеличиваем счётчик
i <<- i + 1
# Парсинг JSON и подготовка массива
batch_x <- cpp_process_json_vector(batch$drawing, scale = scale, color = color)
if (imagenet_preproc) {
# Шкалирование c интервала [0, 1] на интервал [-1, 1]
batch_x <- (batch_x - 0.5) * 2
}
batch_y <- to_categorical(batch$label_int, num_classes)
result <- list(batch_x, batch_y)
return(result)
}
}
Функция киргизүү катары маалымат базасына байланышы бар өзгөрмөнү, колдонулган саптардын санын, класстардын санын, пакеттин көлөмүн, масштабын (scale = 1 256x256 пикселдик сүрөттөрдү көрсөтүүгө туура келет, scale = 0.5 — 128x128 пиксел), түс индикатору (color = FALSE колдонулганда боз түстө көрсөтүүнү белгилейт color = TRUE ар бир штрих жаңы түстө тартылган) жана imagenetте алдын ала үйрөтүлгөн тармактар үчүн алдын ала иштетүү көрсөткүчү. Акыркысы пикселдик маанилерди [0, 1] аралыктан [-1, 1] аралыкка чейин масштабдоо үчүн керек, ал берилгенди үйрөтүүдө колдонулган кызыл моделдер.
Тышкы функция аргументтин түрүн текшерүүнү, таблицаны камтыйт data.table келген туш келди аралаш сызык номерлери менен samples_index жана партиялардын номерлери, эсептегич жана партиялардын максималдуу саны, ошондой эле маалымат базасынан маалыматтарды түшүрүү үчүн SQL туюнтмасы. Мындан тышкары, биз ичиндеги функциянын тез аналогун аныктадык keras::to_categorical(). Биз окутуу үчүн дээрлик бардык маалыматтарды колдондук, жарым пайызын валидацияга калтырдык, ошондуктан доордун өлчөмү параметр менен чектелген steps_per_epoch чакырганда keras::fit_generator(), жана абалы if (i > max_i) текшерүү итератору үчүн гана иштеген.
Ички функцияда сап индекстери кийинки партия үчүн чыгарылат, пакеттик эсептегич көбөйүү менен жазуулар маалымат базасынан түшүрүлөт, JSON талдоо (функция) cpp_process_json_vector(), C++ тилинде жазылган) жана сүрөттөргө ылайыктуу массивдерди түзүү. Андан кийин класстын энбелгилери бар бир ысык векторлор түзүлөт, пикселдик маанилери жана энбелгилери бар массивдер тизмеге бириктирилет, бул кайтаруу мааниси. Ишти тездетүү үчүн биз таблицаларда индекстерди түзүүнү колдондук data.table жана шилтеме аркылуу өзгөртүү - бул пакеттин "чиптери" жок маалымат.стол R-де кандайдыр бир олуттуу көлөмдөгү маалыматтар менен эффективдүү иштөөнү элестетүү кыйын.
Эгерде сизде жетиштүү көлөмдөгү RAM бар болсо, анда сиз аны ошол эле RAMга өткөрүп берүү менен маалымат базасынын иштешин олуттуу түрдө тездете аласыз (биздин милдетибиз үчүн 32 ГБ жетиштүү). Linux'та бөлүм демейки боюнча орнотулган /dev/shm, RAM сыйымдуулугунун жарымына чейин ээлейт. Сиз түзөтүү менен көбүрөөк баса аласыз /etc/fstabсыяктуу рекорд алуу үчүн tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Кайра жүктөө жана буйрукту иштетүү менен натыйжаны текшерүүнү унутпаңыз df -h.
Сыноо маалыматтарынын итератору алда канча жөнөкөй көрүнөт, анткени тесттик маалыматтар топтому толугу менен RAMга туура келет:
Сыноо маалыматтары үчүн итератор
test_generator <- function(dt,
batch_size = 32,
scale = 1,
color = FALSE,
imagenet_preproc = FALSE) {
# Проверка аргументов
checkmate::assert_data_table(dt)
checkmate::assert_count(batch_size)
checkmate::assert_number(scale, lower = 0.001, upper = 5)
checkmate::assert_flag(color)
checkmate::assert_flag(imagenet_preproc)
# Проставляем номера батчей
dt[, batch := (.I - 1L) %/% batch_size + 1L]
data.table::setkey(dt, batch)
i <- 1
max_i <- dt[, max(batch)]
# Замыкание
function() {
batch_x <- cpp_process_json_vector(dt[batch == i, drawing],
scale = scale, color = color)
if (imagenet_preproc) {
# Шкалирование c интервала [0, 1] на интервал [-1, 1]
batch_x <- (batch_x - 0.5) * 2
}
result <- list(batch_x)
i <<- i + 1
return(result)
}
}
4. Архитектуранын моделин тандоо
колдонулган биринчи архитектура болгон mobilenet v1, анын өзгөчөлүктөрү талкууланат бул билдирүү. Ал стандарт катары киргизилген кызыл жана, демек, R үчүн бирдей аталыштагы пакетте жеткиликтүү. Бирок аны бир каналдуу сүрөттөр менен колдонууга аракет кылып жатканда, бир кызык нерсе чыкты: киргизүү тензору ар дайым өлчөмгө ээ болушу керек. (batch, height, width, 3), башкача айтканда, каналдардын санын өзгөртүү мүмкүн эмес. Pythonдо мындай чектөө жок, ошондуктан биз шашып, баштапкы макалага ылайык, бул архитектураны өзүбүздүн ишке ашырууну жаздык (keras версиясында калтырылган жок):
Бул ыкманын кемчиликтери ачык эле көрүнүп турат. Мен көптөгөн моделдерди сынап көргүм келет, бирок, тескерисинче, ар бир архитектураны кол менен кайра жазгым келбейт. Биз дагы imagenetте алдын ала даярдалган моделдердин салмагын колдонуу мүмкүнчүлүгүнөн ажырап калдык. Адаттагыдай эле, документтерди изилдөө жардам берди. Функция get_config() моделдин сыпаттамасын оңдоого ылайыктуу формада алууга мүмкүндүк берет (base_model_conf$layers - кадимки R тизмеси) жана функция from_config() моделдик объектке тескери өзгөртүүнү ишке ашырат:
Эми берилген функциялардын бирин алуу үчүн универсалдуу функцияны жазуу кыйын эмес кызыл Imagenetте машыккан салмагы бар же жок моделдер:
Даяр архитектураларды жүктөө функциясы
get_model <- function(name = "mobilenet_v2",
input_shape = NULL,
weights = "imagenet",
pooling = "avg",
num_classes = NULL,
optimizer = keras::optimizer_adam(lr = 0.002),
loss = "categorical_crossentropy",
metrics = NULL,
color = TRUE,
compile = FALSE) {
# Проверка аргументов
checkmate::assert_string(name)
checkmate::assert_integerish(input_shape, lower = 1, upper = 256, len = 3)
checkmate::assert_count(num_classes)
checkmate::assert_flag(color)
checkmate::assert_flag(compile)
# Получаем объект из пакета keras
model_fun <- get0(paste0("application_", name), envir = asNamespace("keras"))
# Проверка наличия объекта в пакете
if (is.null(model_fun)) {
stop("Model ", shQuote(name), " not found.", call. = FALSE)
}
base_model <- model_fun(
input_shape = input_shape,
include_top = FALSE,
weights = weights,
pooling = pooling
)
# Если изображение не цветное, меняем размерность входа
if (!color) {
base_model_conf <- keras::get_config(base_model)
base_model_conf$layers[[1]]$config$batch_input_shape[[4]] <- 1L
base_model <- keras::from_config(base_model_conf)
}
predictions <- keras::get_layer(base_model, "global_average_pooling2d_1")$output
predictions <- keras::layer_dense(predictions, units = num_classes, activation = "softmax")
model <- keras::keras_model(
inputs = base_model$input,
outputs = predictions
)
if (compile) {
keras::compile(
object = model,
optimizer = optimizer,
loss = loss,
metrics = metrics
)
}
return(model)
}
Бир каналдуу сүрөттөрдү колдонууда алдын ала даярдалган салмактар колдонулбайт. Муну оңдоого болот: функцияны колдонуу get_weights() моделдин салмагын R массивдеринин тизмеси түрүндө алыңыз, бул тизменин биринчи элементинин өлчөмүн өзгөртүңүз (бир түс каналын алуу же үчөөнү тең орточо алуу менен), андан кийин салмактарды функция менен моделге кайра жүктөңүз set_weights(). Биз бул функцияны эч качан кошподук, анткени бул этапта түстүү сүрөттөр менен иштөө кыйла жемиштүү экени айкын болгон.
Биз эксперименттердин көбүн mobilenetтин 1 жана 2 версияларын, ошондой эле resnet34 аркылуу жүргүздүк. Бул сынакта SE-ResNeXt сыяктуу заманбап архитектуралар жакшы аткарышты. Тилекке каршы, биздин карамагыбызда даяр ишке ашыруулар болгон эмес жана өзүбүздүн оюбузду да жазган жокпуз (бирок сөзсүз жазабыз).
5. Скрипттерди параметрлештирүү
Ыңгайлуу болуу үчүн, окутууну баштоонун бардык коду бир скрипт катары иштелип чыккан, параметрленген докопт төмөнкүдөй:
doc <- '
Usage:
train_nn.R --help
train_nn.R --list-models
train_nn.R [options]
Options:
-h --help Show this message.
-l --list-models List available models.
-m --model=<model> Neural network model name [default: mobilenet_v2].
-b --batch-size=<size> Batch size [default: 32].
-s --scale-factor=<ratio> Scale factor [default: 0.5].
-c --color Use color lines [default: FALSE].
-d --db-dir=<path> Path to database directory [default: Sys.getenv("db_dir")].
-r --validate-ratio=<ratio> Validate sample ratio [default: 0.995].
-n --n-gpu=<number> Number of GPUs [default: 1].
'
args <- docopt::docopt(doc)
таңгак докопт ишке ашырууну билдирет http://docopt.org/ for R. Анын жардамы менен скрипттер сыяктуу жөнөкөй буйруктар менен ишке киргизилет Rscript bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db же ./bin/train_nn.R -m resnet50 -c -d /home/andrey/doodle_db, эгерде файл train_nn.R аткарылуучу (бул буйрук моделди үйрөтө баштайт resnet50 128x128 пиксел өлчөмүндөгү үч түстүү сүрөттөрдө маалымат базасы папкада жайгашуусу керек. /home/andrey/doodle_db). Тизмеге окуу ылдамдыгын, оптимизатордун түрүн жана башка настройкаланган параметрлерди кошо аласыз. Басылманы даярдоо процессинде архитектура болуп чыкты mobilenet_v2 учурдагы версиясынан кызыл R колдонуу нельзя R топтомунда эске алынбаган өзгөрүүлөргө байланыштуу, биз аларды оңдоону күтүп жатабыз.
Бул ыкма RStudio скрипттеринин салттуу ишке киргизүүсүнө салыштырмалуу ар кандай моделдер менен эксперименттерди кыйла тездетүүгө мүмкүндүк берди (мүмкүн альтернатива катары пакетти белгилейбиз. tfruns). Бирок негизги артыкчылыгы - бул үчүн RStudio орнотпостон, Dockerде же жөн эле серверде скрипттерди ишке киргизүүнү оңой башкаруу мүмкүнчүлүгү.
6. Скрипттерди докеризациялоо
Биз Dockerди команда мүчөлөрүнүн ортосунда моделдерди үйрөтүү жана булутта тез жайылтуу үчүн чөйрөнүн көчмөлугун камсыз кылуу үчүн колдондук. Сиз R программисти үчүн адаттан тыш болгон бул курал менен тааныша баштасаңыз болот бул басылмалардын сериясы же видео курс.
Docker сизге нөлдөн баштап өз сүрөттөрүңүздү жаратууга жана башка сүрөттөрдү өзүңүздүн сүрөтүңүздү түзүү үчүн негиз катары колдонууга мүмкүнчүлүк берет. Жеткиликтүү варианттарды талдоодо, биз NVIDIA, CUDA+cuDNN драйверлерин жана Python китепканаларын орнотуу сүрөттүн кыйла көлөмдүү бөлүгү деген тыянакка келдик жана биз расмий сүрөттү негиз катары алууну чечтик. tensorflow/tensorflow:1.12.0-gpu, керектүү R пакеттерин кошуу.
Ыңгайлуулук үчүн колдонулган пакеттер өзгөрмөлөргө салынган; жазылган сценарийлердин негизги бөлүгү чогултуу учурунда контейнерлердин ичинде көчүрүлөт. Биз ошондой эле буйрук кабыгын өзгөрттүк /bin/bash мазмунду колдонуунун жөнөкөйлүгү үчүн /etc/os-release. Бул коддо OS версиясын көрсөтүү зарылдыгынан качты.
Кошумчалай кетсек, ар кандай буйруктар менен контейнерди ишке киргизүүгө мүмкүндүк берген кичинекей bash сценарийи жазылган. Мисалы, булар контейнердин ичине мурда жайгаштырылган нейрондук тармактарды окутуу үчүн сценарийлер же мүчүлүштүктөрдү оңдоо жана контейнердин иштешин көзөмөлдөө үчүн командалык кабык болушу мүмкүн:
Контейнерди ишке киргизүү үчүн скрипт
#!/bin/sh
DBDIR=${PWD}/db
LOGSDIR=${PWD}/logs
MODELDIR=${PWD}/models
DATADIR=${PWD}/data
ARGS="--runtime=nvidia --rm -v ${DBDIR}:/db -v ${LOGSDIR}:/app/logs -v ${MODELDIR}:/app/models -v ${DATADIR}:/app/data"
if [ -z "$1" ]; then
CMD="Rscript /app/train_nn.R"
elif [ "$1" = "bash" ]; then
ARGS="${ARGS} -ti"
else
CMD="Rscript /app/train_nn.R $@"
fi
docker run ${ARGS} doodles-tf ${CMD}
Эгер бул bash скрипти параметрсиз иштетилсе, скрипт контейнердин ичинде чакырылат train_nn.R демейки маанилери менен; биринчи позициялык аргумент "bash" болсо, анда контейнер буйрук кабыгы менен интерактивдүү түрдө башталат. Бардык башка учурларда, позициялык аргументтердин маанилери алмаштырылат: CMD="Rscript /app/train_nn.R $@".
Белгилей кетчү нерсе, булак маалыматтары жана маалымат базасы бар каталогдор, ошондой эле үйрөтүлгөн моделдерди сактоо үчүн каталогдор хост тутумунан контейнердин ичине орнотулган, бул скрипттердин натыйжаларына ашыкча манипуляцияларсыз жетүүгө мүмкүндүк берет.
7. Google Булутта бир нече GPU колдонуу
Мелдештин өзгөчөлүктөрүнүн бири абдан ызы-чуу маалыматтар болду (титулдук сүрөттү карагыла, ODS Slack тартып @Leigh.plt алынган). Чоң партиялар муну менен күрөшүүгө жардам берет жана 1 GPU менен компьютерде эксперименттерден кийин, биз булуттагы бир нече GPU боюнча окутуу моделдерин өздөштүрүү чечимин кабыл алды. Колдонулган GoogleCloud (негиздери үчүн жакшы жол) жеткиликтүү конфигурациялардын чоң тандоосуна, акылга сыярлык баага жана $300 бонуска байланыштуу. Ач көздүктөн мен SSD жана бир тонна RAM менен 4xV100 инстанциясына буйрук бердим, бул чоң жаңылыштык болду. Мындай машина акчаны бат эле жеп салат, сиз далилденген түтүксүз эле сынган экспериментке барсаңыз болот. Билим берүү максатында K80 алган жакшы. Бирок оперативдүү эстутумдун чоң көлөмү жардамга келди - булут SSD анын иштеши менен таң калтырган жок, ошондуктан маалымат базасы dev/shm.
Эң кызыктуусу - бир нече GPU колдонуу үчүн жооптуу код фрагменти. Биринчиден, модель Pythonдогудай контекст менеджери аркылуу CPUда түзүлөт:
Акыркысынан башка бардык катмарларды тоңдуруп коюунун, акыркы катмарды үйрөтүүнүн, бир нече GPU үчүн бардык моделди тоңдурбоонун жана кайра даярдоонун классикалык ыкмасын ишке ашыруу мүмкүн эмес.
Колдонулбастан машыгууга мониторинг жүргүзүлдү. тензорборд, журналдарды жазуу жана ар бир доордон кийин маалыматтык аталыштар менен моделдерди сактоо менен чектелебиз:
в кызыл оптималдуу окуу ылдамдыгын автоматтык түрдө издөө үчүн даяр функция жок (аналогдук lr_finder китепканада fast.ai); Кээ бир күч-аракет менен, үчүнчү тараптын ишке ашырууларын Rга өткөрүүгө болот, мисалы, бул;
мурунку пункттун натыйжасында, бир нече GPU колдонууда туура окутуу ылдамдыгын тандоо мүмкүн болгон жок;
заманбап нейрон тармактарынын архитектураларынын, айрыкча imagenetте алдын ала даярдалган архитектуралардын жетишсиздиги;
эч кимдин цикл саясаты жана дискриминациялык окутуу курстары (косинустарды күйдүрүү биздин өтүнүчүбүз боюнча болгон ишке ашырылган, Рахмат сага скайдан).
Бул сынактан кандай пайдалуу нерселер үйрөнүлдү:
Салыштырмалуу аз кубаттуу жабдыктарда сиз татыктуу (RAMдан көп эсе чоң) көлөмдөгү маалымат менен оорутпай иштей аласыз. Пластик баштык маалымат.стол жадыбалдарды жеринде модификациялоонун эсебинен эстутумду үнөмдөйт, бул аларды көчүрүүдөн качат жана туура колдонулганда анын мүмкүнчүлүктөрү дээрлик ар дайым скрипт тилдери үчүн бизге белгилүү болгон бардык куралдардын ичинен эң жогорку ылдамдыкты көрсөтөт. Маалыматтар базасында маалыматтарды сактоо, көп учурларда, бүт маалымат топтомун оперативдик эс тутумга кысуу зарылдыгы жөнүндө такыр ойлонбоого мүмкүндүк берет.
R тилиндеги жай функцияларды пакетти колдонуу менен C++ тилинде тез функциялар менен алмаштырууга болот Rcpp. колдонууга кошумча болсо RcppThread же RcppParallel, биз кайчылаш-платформалуу көп агымдуу ишке ашырууларды алабыз, ошондуктан R деңгээлинде кодду параллелдештирүүнүн кереги жок.
Пакет Rcpp C++ тилин олуттуу билбестен колдонсо болот, талап кылынган минимум көрсөтүлгөн бул жерде. сыяктуу бир катар сонун C-китепканалары үчүн баш файлдар xtensor CRANда жеткиликтүү, башкача айтканда, даяр жогорку натыйжалуу C++ кодун Rге интеграциялаган долбоорлорду ишке ашыруу үчүн инфраструктура түзүлүүдө. Кошумча ыңгайлуулук - бул синтаксисти бөлүп көрсөтүү жана RStudioдагы статикалык C++ код анализатору.
докопт параметрлери менен өз алдынча скрипттерди иштетүүгө мүмкүндүк берет. Бул алыскы серверде колдонууга ыңгайлуу, анын ичинде. докер астында. RStudioдо нейрондук тармактарды окутуу менен көп сааттык эксперименттерди өткөрүү ыңгайсыз жана сервердин өзүнө IDE орнотуу дайыма эле өзүн актай бербейт.
Docker OS жана китепканалардын ар кандай версиялары бар иштеп чыгуучулардын ортосунда коддун көчүрүлүшүн жана натыйжалардын кайталанышын, ошондой эле серверлерде аткаруунун оңойлугун камсыздайт. Сиз бир гана буйрук менен бүт окутуу түтүгүн ишке киргизе аласыз.
Google Булут - кымбат жабдыктарда эксперимент жүргүзүүнүн бюджетке ыңгайлуу жолу, бирок конфигурацияларды кылдаттык менен тандап алышыңыз керек.
Жеке код фрагменттеринин ылдамдыгын өлчөө абдан пайдалуу, айрыкча R жана C++ айкалыштырууда жана пакет менен орун - ошондой эле абдан жеңил.
Жалпысынан бул тажрыйба абдан пайдалуу болду жана биз көтөрүлгөн маселелердин айрымдарын чечүүнүн үстүндө иштейбиз.