ProHoster > blog > administrasi > Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf
Pengenalan Doodle Gambar Cepat: cara berteman dengan R, C++, dan jaringan saraf
Hei Habr!
Musim gugur yang lalu, Kaggle menyelenggarakan kompetisi untuk mengklasifikasikan gambar yang digambar tangan, Quick Draw Doodle Recognition, yang antara lain diikuti oleh tim R-scientist: Artem Klevtsova, Manajer Philippa ΠΈ Andrey Ogurtsov. Kami tidak akan menjelaskan kompetisi secara detail; itu sudah pernah dilakukan publikasi terbaru.
Kali ini tidak berhasil dengan farming medali, tapi banyak pengalaman berharga yang didapat, jadi saya ingin memberi tahu komunitas tentang beberapa hal paling menarik dan berguna di Kagle dan dalam pekerjaan sehari-hari. Di antara topik yang dibahas: hidup sulit tanpanya OpenCV, penguraian JSON (contoh ini memeriksa integrasi kode C++ ke dalam skrip atau paket di R menggunakan RCPP), parameterisasi skrip dan dockerisasi solusi akhir. Semua kode dari pesan dalam bentuk yang sesuai untuk dieksekusi tersedia di repositori.
1. Memuat data dari CSV ke database MonetDB secara efisien
Data dalam kompetisi ini disediakan bukan dalam bentuk gambar yang sudah jadi, melainkan dalam bentuk 340 file CSV (satu file untuk setiap kelas) yang berisi JSON dengan koordinat titik. Dengan menghubungkan titik-titik tersebut dengan garis, kita mendapatkan gambar akhir berukuran 256x256 piksel. Juga untuk setiap rekaman terdapat label yang menunjukkan apakah gambar tersebut dikenali dengan benar oleh pengklasifikasi yang digunakan pada saat kumpulan data dikumpulkan, kode dua huruf negara tempat tinggal pembuat gambar, pengidentifikasi unik, stempel waktu dan nama kelas yang cocok dengan nama file. Versi sederhana dari data asli berbobot 7.4 GB dalam arsip dan sekitar 20 GB setelah dibongkar, data lengkap setelah dibongkar membutuhkan 240 GB. Penyelenggara memastikan bahwa kedua versi mereproduksi gambar yang sama, yang berarti versi lengkapnya mubazir. Bagaimanapun, menyimpan 50 juta gambar dalam file grafik atau dalam bentuk array langsung dianggap tidak menguntungkan, dan kami memutuskan untuk menggabungkan semua file CSV dari arsip. kereta_sederhana.zip ke dalam database dengan pembuatan gambar berikutnya dengan ukuran yang diperlukan βon the flyβ untuk setiap batch.
Sistem yang telah terbukti dipilih sebagai DBMS MonetDB, yaitu implementasi untuk R sebagai sebuah paket MonetDBLite. Paket ini mencakup versi server database yang tertanam dan memungkinkan Anda mengambil server langsung dari sesi R dan bekerja dengannya di sana. Membuat database dan menghubungkannya dilakukan dengan satu perintah:
con <- DBI::dbConnect(drv = MonetDBLite::MonetDBLite(), Sys.getenv("DBDIR"))
Kita perlu membuat dua tabel: satu untuk semua data, yang lain untuk informasi layanan tentang file yang diunduh (berguna jika terjadi kesalahan dan proses harus dilanjutkan setelah mengunduh beberapa file):
Cara tercepat untuk memuat data ke dalam database adalah dengan langsung menyalin file CSV menggunakan perintah SQL COPY OFFSET 2 INTO tablename FROM path USING DELIMITERS ',','n','"' NULL AS '' BEST EFFORTDimana tablename - nama tabel dan path - jalur ke file. Saat bekerja dengan arsip, ditemukan implementasi bawaan unzip di R tidak berfungsi dengan benar dengan sejumlah file dari arsip, jadi kami menggunakan sistem unzip (menggunakan parameter getOption("unzip")).
Waktu pemuatan data dapat bervariasi tergantung pada karakteristik kecepatan drive yang digunakan. Dalam kasus kami, membaca dan menulis dalam satu SSD atau dari flash drive (file sumber) ke SSD (DB) membutuhkan waktu kurang dari 10 menit.
Dibutuhkan beberapa detik lagi untuk membuat kolom dengan label kelas bilangan bulat dan kolom indeks (ORDERED INDEX) dengan nomor baris yang akan dijadikan sampel observasi saat membuat batch:
Membuat Kolom dan Indeks Tambahan
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)"))
Untuk mengatasi masalah pembuatan batch dengan cepat, kami perlu mencapai kecepatan maksimum dalam mengekstraksi baris acak dari tabel doodles. Untuk ini kami menggunakan 3 trik. Yang pertama adalah mengurangi dimensi tipe yang menyimpan ID observasi. Dalam kumpulan data asli, tipe yang diperlukan untuk menyimpan ID adalah bigint, tetapi jumlah observasi memungkinkan untuk memasukkan pengidentifikasinya, sama dengan nomor urut, ke dalam tipe int. Pencarian jauh lebih cepat dalam hal ini. Trik kedua adalah dengan menggunakan ORDERED INDEX β kami mengambil keputusan ini secara empiris, setelah melalui semua yang tersedia pilihan. Yang ketiga adalah menggunakan kueri berparameter. Inti dari metode ini adalah menjalankan perintah satu kali PREPARE dengan penggunaan ekspresi yang sudah disiapkan selanjutnya saat membuat sekumpulan kueri dengan tipe yang sama, tetapi sebenarnya ada keunggulan dibandingkan dengan yang sederhana SELECT ternyata berada dalam kisaran kesalahan statistik.
Proses upload data memakan RAM tidak lebih dari 450 MB. Artinya, pendekatan yang dijelaskan memungkinkan Anda memindahkan kumpulan data berbobot puluhan gigabyte di hampir semua perangkat keras beranggaran rendah, termasuk beberapa perangkat papan tunggal, dan ini cukup keren.
Yang tersisa hanyalah mengukur kecepatan pengambilan data (acak) dan mengevaluasi penskalaan saat mengambil sampel kumpulan dengan ukuran berbeda:
Seluruh proses persiapan batch terdiri dari langkah-langkah berikut:
Mengurai beberapa JSON yang berisi vektor string dengan koordinat titik.
Menggambar garis berwarna berdasarkan koordinat titik pada gambar dengan ukuran yang diperlukan (misalnya 256Γ256 atau 128Γ128).
Mengubah gambar yang dihasilkan menjadi tensor.
Sebagai bagian dari persaingan antar kernel Python, masalahnya diselesaikan terutama dengan menggunakan OpenCV. Salah satu analog paling sederhana dan paling jelas di R akan terlihat seperti ini:
Penggambaran dilakukan menggunakan alat R standar dan disimpan ke PNG sementara yang disimpan dalam RAM (di Linux, direktori R sementara terletak di direktori /tmp, dipasang di RAM). File ini kemudian dibaca sebagai array tiga dimensi dengan angka mulai dari 0 hingga 1. Hal ini penting karena BMP yang lebih konvensional akan dibaca ke dalam array mentah dengan kode warna hex.
Implementasi ini tampaknya kurang optimal bagi kami, karena pembentukan batch besar membutuhkan waktu yang sangat lama, dan kami memutuskan untuk memanfaatkan pengalaman rekan-rekan kami dengan menggunakan perpustakaan yang kuat. OpenCV. Pada saat itu belum ada paket siap pakai untuk R (sekarang tidak ada), jadi implementasi minimal dari fungsionalitas yang diperlukan ditulis dalam C++ dengan integrasi ke dalam kode R menggunakan RCPP.
Untuk mengatasi masalah ini, paket dan perpustakaan berikut digunakan:
OpenCV untuk bekerja dengan gambar dan menggambar garis. Pustaka sistem dan file header yang sudah diinstal sebelumnya, serta tautan dinamis digunakan.
xtensor untuk bekerja dengan array dan tensor multidimensi. Kami menggunakan file header yang disertakan dalam paket R dengan nama yang sama. Pustaka ini memungkinkan Anda bekerja dengan array multidimensi, baik dalam urutan utama baris maupun kolom utama.
ndjson untuk mengurai JSON. Perpustakaan ini digunakan di xtensor secara otomatis jika ada dalam proyek.
Benang Rcpp untuk mengatur pemrosesan multi-utas vektor dari JSON. Menggunakan file header yang disediakan oleh paket ini. Dari yang lebih populer RcppParalel Paket tersebut, antara lain, memiliki mekanisme interupsi loop bawaan.
Perlu dicatat bahwa xtensor ternyata merupakan anugerah: selain memiliki fungsionalitas yang luas dan kinerja tinggi, pengembangnya ternyata cukup responsif dan menjawab pertanyaan dengan cepat dan detail. Dengan bantuan mereka, transformasi matriks OpenCV menjadi tensor xtensor dapat diimplementasikan, serta cara untuk menggabungkan tensor gambar 3 dimensi menjadi tensor 4 dimensi dengan dimensi yang benar (batch itu sendiri).
Untuk mengkompilasi file yang menggunakan file sistem dan tautan dinamis dengan perpustakaan yang diinstal pada sistem, kami menggunakan mekanisme plugin yang diterapkan dalam paket RCPP. Untuk menemukan jalur dan tanda secara otomatis, kami menggunakan utilitas Linux yang populer pkg-config.
Implementasi plugin Rcpp untuk menggunakan perpustakaan OpenCV
Kode implementasi untuk mengurai JSON dan menghasilkan batch untuk transmisi ke model diberikan di bawah spoiler. Pertama, tambahkan direktori proyek lokal untuk mencari file header (diperlukan untuk 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;
}
Kode ini harus ditempatkan di file src/cv_xt.cpp dan kompilasi dengan perintah Rcpp::sourceCpp(file = "src/cv_xt.cpp", env = .GlobalEnv); juga diperlukan untuk bekerja nlohmann/json.hpp dari gudang. Kode ini dibagi menjadi beberapa fungsi:
to_xt β fungsi templat untuk mengubah matriks gambar (cv::Mat) ke tensor xt::xtensor;
parse_json β fungsi mem-parsing string JSON, mengekstrak koordinat titik, mengemasnya ke dalam vektor;
ocv_draw_lines β dari vektor titik yang dihasilkan, menggambar garis multi-warna;
process β menggabungkan fungsi-fungsi di atas dan juga menambahkan kemampuan untuk menskalakan gambar yang dihasilkan;
cpp_process_json_str - membungkus fungsinya process, yang mengekspor hasilnya ke objek-R (array multidimensi);
cpp_process_json_vector - membungkus fungsinya cpp_process_json_str, yang memungkinkan Anda memproses vektor string dalam mode multi-utas.
Untuk menggambar garis multi-warna, digunakan model warna HSV, diikuti dengan konversi ke RGB. Mari kita uji hasilnya:
Seperti yang Anda lihat, peningkatan kecepatan ternyata sangat signifikan, dan tidak mungkin mengejar kode C++ dengan memparalelkan kode R.
3. Iterator untuk membongkar batch dari database
R memiliki reputasi yang baik dalam memproses data yang sesuai dengan RAM, sedangkan Python lebih dicirikan oleh pemrosesan data berulang, memungkinkan Anda mengimplementasikan penghitungan out-of-core dengan mudah dan alami (penghitungan menggunakan memori eksternal). Contoh klasik dan relevan bagi kita dalam konteks masalah yang dijelaskan adalah jaringan saraf dalam yang dilatih menggunakan metode penurunan gradien dengan perkiraan gradien pada setiap langkah menggunakan sebagian kecil observasi, atau mini-batch.
Kerangka kerja pembelajaran mendalam yang ditulis dengan Python memiliki kelas khusus yang mengimplementasikan iterator berdasarkan data: tabel, gambar dalam folder, format biner, dll. Anda dapat menggunakan opsi yang sudah jadi atau menulis sendiri untuk tugas tertentu. Di R kita bisa memanfaatkan semua fitur perpustakaan Python keras dengan berbagai backendnya menggunakan paket dengan nama yang sama, yang pada gilirannya berfungsi di atas paket tersebut diam. Yang terakhir ini layak mendapatkan artikel panjang yang terpisah; itu tidak hanya memungkinkan Anda menjalankan kode Python dari R, tetapi juga memungkinkan Anda mentransfer objek antara sesi R dan Python, secara otomatis melakukan semua konversi tipe yang diperlukan.
Kami menghilangkan kebutuhan untuk menyimpan semua data dalam RAM dengan menggunakan MonetDBLite, semua pekerjaan "jaringan saraf" akan dilakukan oleh kode asli dengan Python, kami hanya perlu menulis iterator pada data, karena tidak ada yang siap untuk situasi seperti itu di R atau Python. Pada dasarnya hanya ada dua persyaratan untuk itu: ia harus mengembalikan batch dalam loop tanpa akhir dan menyimpan statusnya di antara iterasi (yang terakhir di R diimplementasikan dengan cara paling sederhana menggunakan penutupan). Sebelumnya, diperlukan untuk secara eksplisit mengubah array R menjadi array numpy di dalam iterator, tetapi versi paket saat ini keras melakukannya sendiri.
Iterator untuk data pelatihan dan validasi ternyata sebagai berikut:
Fungsi ini mengambil input variabel dengan koneksi ke database, jumlah baris yang digunakan, jumlah kelas, ukuran batch, skala (scale = 1 sesuai dengan rendering gambar 256x256 piksel, scale = 0.5 β 128x128 piksel), indikator warna (color = FALSE menentukan rendering dalam skala abu-abu saat digunakan color = TRUE setiap goresan digambar dengan warna baru) dan indikator pra-pemrosesan untuk jaringan yang telah dilatih sebelumnya di imagenet. Yang terakhir diperlukan untuk menskalakan nilai piksel dari interval [0, 1] ke interval [-1, 1], yang digunakan saat melatih perangkat yang disediakan keras model.
Fungsi eksternal berisi pemeriksaan tipe argumen, sebuah tabel data.table dengan nomor baris yang dicampur secara acak dari samples_index dan nomor batch, penghitung dan jumlah batch maksimum, serta ekspresi SQL untuk membongkar data dari database. Selain itu, kami mendefinisikan analog cepat dari fungsi di dalamnya keras::to_categorical(). Kami menggunakan hampir semua data untuk pelatihan, menyisakan setengah persen untuk validasi, sehingga ukuran epoch dibatasi oleh parameter steps_per_epoch saat dipanggil keras::fit_generator(), dan kondisinya if (i > max_i) hanya berfungsi untuk iterator validasi.
Dalam fungsi internal, indeks baris diambil untuk batch berikutnya, catatan diturunkan dari database dengan peningkatan penghitung batch, penguraian JSON (fungsi cpp_process_json_vector(), ditulis dalam C++) dan membuat array yang sesuai dengan gambar. Kemudian vektor one-hot dengan label kelas dibuat, array dengan nilai piksel dan label digabungkan menjadi sebuah daftar, yang merupakan nilai kembalian. Untuk mempercepat pekerjaan, kami menggunakan pembuatan indeks dalam tabel data.table dan modifikasi melalui tautan - tanpa paket βchipβ ini tabel data Sulit membayangkan bekerja secara efektif dengan sejumlah besar data di R.
Hasil pengukuran kecepatan pada laptop Core i5 adalah sebagai berikut:
Jika Anda memiliki jumlah RAM yang cukup, Anda dapat mempercepat pengoperasian database dengan mentransfernya ke RAM yang sama (32 GB sudah cukup untuk tugas kami). Di Linux, partisi dipasang secara default /dev/shm, menghabiskan hingga setengah kapasitas RAM. Anda dapat menyorot lebih banyak dengan mengedit /etc/fstabuntuk mendapatkan rekaman seperti tmpfs /dev/shm tmpfs defaults,size=25g 0 0. Pastikan untuk reboot dan periksa hasilnya dengan menjalankan perintah df -h.
Iterator untuk data pengujian terlihat jauh lebih sederhana, karena kumpulan data pengujian sepenuhnya cocok dengan RAM:
Arsitektur pertama yang digunakan adalah mobilenet v1, fitur-fiturnya dibahas dalam ini pesan. Ini disertakan sebagai standar keras dan, oleh karena itu, tersedia dalam paket dengan nama yang sama untuk R. Namun ketika mencoba menggunakannya dengan gambar saluran tunggal, terjadi hal yang aneh: tensor masukan harus selalu berdimensi (batch, height, width, 3), artinya jumlah saluran tidak dapat diubah. Tidak ada batasan seperti itu dalam Python, jadi kami bergegas dan menulis implementasi arsitektur ini sendiri, mengikuti artikel asli (tanpa dropout yang ada di versi keras):
Kerugian dari pendekatan ini sangat jelas. Saya ingin menguji banyak model, namun sebaliknya, saya tidak ingin menulis ulang setiap arsitektur secara manual. Kami juga kehilangan kesempatan untuk menggunakan bobot model yang telah dilatih sebelumnya di imagenet. Seperti biasa, mempelajari dokumentasi membantu. Fungsi get_config() memungkinkan Anda mendapatkan deskripsi model dalam bentuk yang sesuai untuk diedit (base_model_conf$layers - daftar R biasa), dan fungsinya from_config() melakukan konversi terbalik ke objek model:
Sekarang tidak sulit untuk menulis fungsi universal untuk mendapatkan fungsi apa pun yang disediakan keras model dengan atau tanpa beban dilatih di imagenet:
Saat menggunakan gambar saluran tunggal, tidak ada bobot yang telah dilatih sebelumnya yang digunakan. Ini bisa diperbaiki: menggunakan fungsi tersebut get_weights() dapatkan bobot model dalam bentuk daftar array R, ubah dimensi elemen pertama daftar ini (dengan mengambil satu saluran warna atau rata-rata ketiganya), lalu muat kembali bobot ke dalam model dengan fungsi set_weights(). Kami tidak pernah menambahkan fungsi ini, karena pada tahap ini sudah jelas bahwa bekerja dengan gambar berwarna lebih produktif.
Kami melakukan sebagian besar eksperimen menggunakan mobilenet versi 1 dan 2, serta resnet34. Arsitektur yang lebih modern seperti SE-ResNeXt tampil baik dalam kompetisi ini. Sayangnya, kami tidak memiliki implementasi yang sudah jadi, dan kami tidak menulis implementasi kami sendiri (tetapi kami pasti akan menulisnya).
5. Parameterisasi skrip
Untuk kenyamanan, semua kode untuk memulai pelatihan dirancang sebagai skrip tunggal, menggunakan parameterisasi dokumen sebagai berikut:
doc <- '
Usage:
train_nn.R --help
train_nn.R --list-models
train_nn.R [options]
Options:
-h --help Show this message.
-l --list-models List available models.
-m --model=<model> Neural network model name [default: mobilenet_v2].
-b --batch-size=<size> Batch size [default: 32].
-s --scale-factor=<ratio> Scale factor [default: 0.5].
-c --color Use color lines [default: FALSE].
-d --db-dir=<path> Path to database directory [default: Sys.getenv("db_dir")].
-r --validate-ratio=<ratio> Validate sample ratio [default: 0.995].
-n --n-gpu=<number> Number of GPUs [default: 1].
'
args <- docopt::docopt(doc)
Paket dokumen mewakili implementasinya http://docopt.org/ untuk R. Dengan bantuannya, skrip diluncurkan dengan perintah sederhana seperti 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, jika mengajukan train_nn.R dapat dieksekusi (perintah ini akan mulai melatih model resnet50 pada gambar tiga warna berukuran 128x128 piksel, database harus berada di dalam folder /home/andrey/doodle_db). Anda dapat menambahkan kecepatan pembelajaran, jenis pengoptimal, dan parameter lain yang dapat disesuaikan ke dalam daftar. Dalam proses penyusunan publikasi, ternyata arsitekturnya mobilenet_v2 dari versi saat ini keras dalam penggunaan R tidak harus karena perubahan yang tidak diperhitungkan dalam paket R, kami menunggu mereka memperbaikinya.
Pendekatan ini memungkinkan untuk mempercepat eksperimen dengan model yang berbeda secara signifikan dibandingkan dengan peluncuran skrip yang lebih tradisional di RStudio (kami mencatat paket ini sebagai alternatif yang memungkinkan. tfruns). Namun keunggulan utamanya adalah kemampuan untuk dengan mudah mengelola peluncuran skrip di Docker atau hanya di server, tanpa menginstal RStudio untuk ini.
6. Dockerisasi skrip
Kami menggunakan Docker untuk memastikan portabilitas lingkungan untuk model pelatihan antar anggota tim dan untuk penerapan cepat di cloud. Anda dapat mulai mengenal alat ini, yang relatif tidak biasa bagi seorang programmer R, dengan ini serangkaian publikasi atau kursus video.
Docker memungkinkan Anda membuat image Anda sendiri dari awal dan menggunakan image lain sebagai dasar untuk membuat image Anda sendiri. Saat menganalisis opsi yang tersedia, kami sampai pada kesimpulan bahwa menginstal NVIDIA, driver CUDA+cuDNN, dan pustaka Python adalah bagian gambar yang cukup banyak, dan kami memutuskan untuk menggunakan gambar resmi sebagai dasar. tensorflow/tensorflow:1.12.0-gpu, menambahkan paket R yang diperlukan di sana.
File buruh pelabuhan terakhir terlihat seperti ini:
Untuk kenyamanan, paket yang digunakan dimasukkan ke dalam variabel; sebagian besar skrip tertulis disalin ke dalam wadah selama perakitan. Kami juga mengubah shell perintah menjadi /bin/bash untuk kemudahan penggunaan konten /etc/os-release. Hal ini menghindari kebutuhan untuk menentukan versi OS dalam kode.
Selain itu, skrip bash kecil telah ditulis yang memungkinkan Anda meluncurkan container dengan berbagai perintah. Misalnya, ini bisa berupa skrip untuk melatih jaringan saraf yang sebelumnya ditempatkan di dalam penampung, atau shell perintah untuk melakukan debug dan memantau pengoperasian penampung:
Skrip untuk meluncurkan penampung
#!/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}
Jika skrip bash ini dijalankan tanpa parameter, skrip tersebut akan dipanggil di dalam container train_nn.R dengan nilai default; jika argumen posisi pertama adalah "bash", maka container akan dimulai secara interaktif dengan shell perintah. Dalam semua kasus lain, nilai argumen posisi diganti: CMD="Rscript /app/train_nn.R $@".
Perlu dicatat bahwa direktori dengan data sumber dan database, serta direktori untuk menyimpan model terlatih, dipasang di dalam wadah dari sistem host, yang memungkinkan Anda mengakses hasil skrip tanpa manipulasi yang tidak perlu.
7. Menggunakan banyak GPU di Google Cloud
Salah satu keistimewaan kompetisi ini adalah datanya yang sangat berisik (lihat gambar judul, dipinjam dari @Leigh.plt dari ODS slack). Batch besar membantu mengatasi hal ini, dan setelah melakukan percobaan pada PC dengan 1 GPU, kami memutuskan untuk menguasai model pelatihan pada beberapa GPU di cloud. GoogleCloud yang digunakan (panduan bagus untuk dasar-dasarnya) karena banyaknya pilihan konfigurasi yang tersedia, harga wajar, dan bonus $300. Karena keserakahan, saya memesan instance 4xV100 dengan SSD dan banyak RAM, dan itu adalah kesalahan besar. Mesin seperti itu menghabiskan uang dengan cepat; Anda bisa bangkrut saat bereksperimen tanpa saluran pipa yang terbukti. Untuk tujuan pendidikan, lebih baik menggunakan K80. Namun sejumlah besar RAM berguna - SSD cloud tidak terkesan dengan kinerjanya, sehingga database dipindahkan ke dev/shm.
Yang paling menarik adalah potongan kode yang bertanggung jawab untuk menggunakan banyak GPU. Pertama, model dibuat di CPU menggunakan manajer konteks, seperti di Python:
Teknik klasik membekukan semua lapisan kecuali yang terakhir, melatih lapisan terakhir, mencairkan dan melatih ulang seluruh model untuk beberapa GPU tidak dapat diterapkan.
Pelatihan dipantau tanpa digunakan. papan tensor, membatasi diri untuk mencatat log dan menyimpan model dengan nama yang informatif setelah setiap zaman:
Beberapa permasalahan yang kami temui belum teratasi:
Π² keras tidak ada fungsi siap pakai untuk secara otomatis mencari kecepatan pemelajaran optimal (analog lr_finder di perpustakaan cepat); Dengan beberapa upaya, dimungkinkan untuk mem-porting implementasi pihak ketiga ke R, misalnya, ini;
sebagai konsekuensi dari poin sebelumnya, tidak mungkin memilih kecepatan pelatihan yang tepat saat menggunakan beberapa GPU;
kurangnya arsitektur jaringan saraf modern, terutama yang telah dilatih sebelumnya tentang imagenet;
tidak ada kebijakan satu siklus dan kecepatan pembelajaran yang diskriminatif (cosine annealing sesuai permintaan kami diimplementasikan, Terima kasih skidan).
Hal bermanfaat apa yang didapat dari kompetisi ini:
Pada perangkat keras berdaya relatif rendah, Anda dapat bekerja dengan volume data yang layak (berkali-kali lipat ukuran RAM) tanpa kesulitan. Kantong plastik tabel data menghemat memori karena modifikasi tabel di tempat, sehingga menghindari penyalinan, dan bila digunakan dengan benar, kemampuannya hampir selalu menunjukkan kecepatan tertinggi di antara semua alat bahasa skrip yang kami kenal. Menyimpan data dalam database memungkinkan Anda, dalam banyak kasus, untuk tidak memikirkan sama sekali tentang perlunya memasukkan seluruh kumpulan data ke dalam RAM.
Fungsi lambat di R dapat diganti dengan fungsi cepat di C++ menggunakan paket RCPP. Jika selain digunakan Benang Rcpp ΠΈΠ»ΠΈ RcppParalel, kami mendapatkan implementasi multi-utas lintas platform, jadi tidak perlu memparalelkan kode di level R.
Kemasan RCPP dapat digunakan tanpa pengetahuan serius tentang C++, persyaratan minimum telah diuraikan di sini. File header untuk sejumlah C-library keren seperti xtensor tersedia di CRAN, yaitu infrastruktur sedang dibentuk untuk implementasi proyek yang mengintegrasikan kode C++ berkinerja tinggi yang sudah jadi ke dalam R. Kenyamanan tambahan adalah penyorotan sintaksis dan penganalisis kode C++ statis di RStudio.
dokumen memungkinkan Anda menjalankan skrip mandiri dengan parameter. Ini nyaman untuk digunakan pada server jarak jauh, termasuk. di bawah buruh pelabuhan. Di RStudio, melakukan eksperimen berjam-jam dengan melatih jaringan saraf tidak nyaman, dan memasang IDE di server itu sendiri tidak selalu dapat dibenarkan.
Docker memastikan portabilitas kode dan reproduktifitas hasil antara pengembang dengan versi OS dan perpustakaan yang berbeda, serta kemudahan eksekusi di server. Anda dapat meluncurkan seluruh alur pelatihan hanya dengan satu perintah.
Google Cloud adalah cara yang hemat anggaran untuk bereksperimen pada perangkat keras yang mahal, namun Anda harus memilih konfigurasi dengan hati-hati.
Mengukur kecepatan setiap fragmen kode sangat berguna, terutama saat menggabungkan R dan C++, dan dengan paketnya bangku - juga sangat mudah.
Secara keseluruhan pengalaman ini sangat bermanfaat dan kami terus berupaya menyelesaikan beberapa masalah yang muncul.