Xgboost: RFC: JSON sebagai Format Serialisasi Model Generasi Berikutnya

Dibuat pada 8 Des 2018  ·  48Komentar  ·  Sumber: dmlc/xgboost

RFC: JSON sebagai Format Serialisasi Model Generasi Berikutnya

  1. Motivasi: Mengapa format serialisasi baru?
  2. Proposal: Gunakan serialisasi JSON untuk memastikan kompatibilitas mundur dan maju
  3. Versi semantik
  4. Manfaat lain dari JSON
  5. Implementasi proposal dengan parser DMLC JSON
  6. Mengatasi kemungkinan keberatan
  7. Lampiran A: Skema 1.0 Penuh
  8. Lampiran B: Representasi nilai yang hilang untuk berbagai tipe data

Dalam dokumen RFC ini, saya mengusulkan bahwa proyek XGBoost pada akhirnya akan bermigrasi ke JSON untuk membuat serial model pohon keputusan.

Terima kasih khusus kepada @trivialfis karena awalnya datang dengan ide menggunakan JSON.

Motivasi: Mengapa format serialisasi baru?

Metode serialisasi saat ini yang digunakan di XGBoost adalah dump biner dari parameter model. Misalnya, kelas pohon keputusan ( RegTree ) diserialisasikan sebagai berikut:

/* Adopted from include/xgboost/tree_model.h; simplified for presentation */
void Save(dmlc::Stream* fo) {
  // Save tree-wide parameters
  fo->Write(&param, sizeof(TreeParam));
  // Save tree nodes
  fo->Write(nodes_.data(), sizeof(Node) * nodes_.size());
  // Save per-node statistics
  fo->Write(stats_.data(), sizeof(NodeStat) * stats_.size());
  // Save optional leaf vector
  if (param.size_leaf_vector != 0) {
    fo->Write(leaf_vector_);
  }
}
void Load(dmlc::Stream* fi) {
  // Load tree-wide parameters
  fi->Read(&param, sizeof(TreeParam));
  nodes_.resize(param.num_nodes);
  stats_.resize(param.num_nodes);
  // Load tree nodes
  fi->Read(nodes_.data(), sizeof(Node) * nodes_.size());
  // Load per-node statistics
  fi->Read(stats_.data(), sizeof(NodeStat) * stats_.size());
  // Load optional leaf vector
  if (param.size_leaf_vector != 0) {
    fi->Read(&leaf_vector_);
  }
}

Metode serialisasi biner memiliki beberapa keuntungan: mudah diimplementasikan, memiliki overhead memori yang rendah (karena kita dapat membaca dari dan menulis ke file stream secara langsung), cepat untuk dijalankan (tidak diperlukan pra-pemrosesan), dan menghasilkan produk yang ringkas. binari model.

Sayangnya, metode ini memiliki satu kelemahan signifikan: tidak mungkin menambahkan bidang baru tanpa merusak kompatibilitas mundur . Kompatibilitas mundur mengacu pada kemampuan versi baru XGBoost untuk membaca file model yang dihasilkan oleh versi XGBoost yang lebih lama. Untuk melihat mengapa kita tidak bisa menambahkan bidang baru, mari kita lihat contoh kelas pohon keputusan yang ditunjukkan di atas. Kelas NodeStat saat ini memiliki empat bidang:

/* NodeStat: Class to store statistics for a node in a decision tree
 * Version 1 */
struct NodeStat {
  // loss change caused by current split
  float loss_chg;
  // sum of hessian values, used to measure coverage of data
  float sum_hess;
  // weight of current node
  float base_weight;
  // number of leaf nodes among the children
  int leaf_child_cnt;
};

Total ukuran byte NodeStat adalah 16 byte. Sekarang bayangkan skenario fiktif di mana kita ingin menambahkan bidang baru bernama instance_cnt . Bidang ini akan menyimpan jumlah instance (titik data) yang terkait dengan node:

/* NodeStat: Class to store statistics for a node in a decision tree
 * Version 2 */
struct NodeStat {
  // loss change caused by current split
  float loss_chg;
  // sum of hessian values, used to measure coverage of data
  float sum_hess;
  // weight of current node
  float base_weight;
  // number of leaf nodes among the children
  int leaf_child_cnt;
  // NEW: number of instances associated with the node
  int64_t instance_cnt;
};

Perhatikan bahwa versi baru NodeStat sekarang 24 byte. Sekarang kami telah merusak kompatibilitas mundur: ketika versi terbaru XGBoost menjalankan cuplikan

// Load per-node statistics
fi->Read(stats_.data(), sizeof(NodeStat) * stats_.size());

ia akan mencoba membaca 24 * M byte, di mana M adalah jumlah node di pohon keputusan. Namun, jika model yang disimpan diserialisasikan di XGBoost versi lama, hanya akan ada 16 * M byte untuk dibaca di file serial! Program akan macet atau menunjukkan beberapa jenis perilaku yang tidak ditentukan.

Apa yang akan menjadi solusi? Kami dapat menambahkan logika tambahan dalam cuplikan di atas untuk memeriksa versi XGBoost mana yang menghasilkan file serial:

if ( /* if the model was generated with old version */ ) {
  for (size_t i = 0; i < stats.size(); ++i) {
    NodeStatOld tmp;  // Version 1 of NodeStat
    fi->Read(&tmp, sizeof(NodeStatOld));
    stats_[i].loss_chg = tmp.loss_chg;
    stats_[i].sum_hess = tmp.sum_hess;
    stats_[i].base_weight = tmp.base_weight;
    stats_[i].leaf_child_cnt = tmp.leaf_child_cnt;
    // instance count is missing
    stats_[i].instance_cnt = -1;
  }
} else {  /* the model was generated with new version */
  fi->Read(stats_.data(), sizeof(NodeStat) * stats_.size());
}

Itu banyak baris untuk membaca satu vektor C++. Secara umum, logika tambahan untuk kompatibilitas mundur dapat terakumulasi dari waktu ke waktu dan menimbulkan beban pemeliharaan di masa mendatang bagi kontributor. Lebih buruk lagi, basis kode XGBoost saat ini tidak menyimpan nomor versi. Jadi tidak ada cara untuk menanyakan versi file serial. Solusi yang diusulkan tidak hanya berantakan tetapi juga sebenarnya tidak layak.

Proposal: Gunakan serialisasi JSON untuk memastikan kompatibilitas mundur dan maju

JSON (JavaScript Object Notation) adalah format serialisasi ringan yang dibangun di atas dua struktur dasar: objek JSON (satu set pasangan kunci-nilai) dan array JSON (daftar nilai yang diurutkan). Di bagian ini, saya akan berargumen bahwa adalah mungkin untuk JSON sebagai metode bukti masa depan untuk memastikan kompatibilitas ke belakang dan ke depan, yang didefinisikan sebagai berikut:

  • Kompatibilitas mundur : Versi XGBoost yang lebih baru dapat membaca model yang diproduksi oleh versi XGBoost sebelumnya.
  • Kompatibilitas ke depan : Versi XGBoost sebelumnya dapat membaca model yang dihasilkan oleh versi XGBoost yang lebih baru.

Kami akan menggunakan dua karakteristik objek JSON yang berguna:

  • Kehadiran kunci tambahan dalam objek JSON tidak menyebabkan kesalahan fatal.
  • Jika objek JSON kehilangan beberapa kunci yang kami inginkan, kami dapat mendeteksi fakta ini dengan andal.

Mari kembali ke contoh NodeStat dari bagian sebelumnya. Versi 1 dari NodeStat dapat diekspresikan sebagai objek JSON sebagai berikut:

{
  "loss_chg": -1.5,
  "sum_hess": 12.4,
  "base_weight": 1.0,
  "leaf_child_cnt": 2
}

(Nilai dibuat sebagai contoh.) Versi 2 akan menjadi seperti

{
  "loss_chg": -1.5,
  "sum_hess": 12.4,
  "base_weight": 1.0,
  "leaf_child_cnt": 2,
  "instance_cnt": 3230
}

Mari kita periksa dulu apakah kompatibilitas mundur berlaku. Versi terbaru XGBoost akan mencoba membaca kunci loss_chg , sum_hess , base_weight , leaf_child_cnt , dan instance_cnt dari JSON yang diberikan mengajukan. Jika file JSON diproduksi oleh versi lama, itu hanya akan memiliki loss_chg , sum_hess , base_weight , dan leaf_child_cnt . Pengurai JSON dapat dengan andal mendeteksi bahwa instance_cnt hilang. Ini jauh lebih baik daripada mengalami crash program saat menemukan byte yang hilang, seperti yang akan dilakukan parser biner. Setelah mendeteksi kunci yang hilang, kami dapat menandai nilai yang hilang terkait. Untuk contoh NodeStat , kami akan memasukkan -1 untuk menunjukkan nilai yang hilang, karena -1 tidak dapat menjadi nilai yang valid untuk jumlah instans. Untuk bidang opsional seperti instance_cnt , respons ini sudah cukup.

Di sisi lain, beberapa bidang tidak begitu opsional. Misalnya, fitur baru dapat ditambahkan yang memanfaatkan pasangan nilai kunci baru. Dalam hal ini, kita perlu membuat kesalahan setiap kali pengguna mencoba menggunakan fitur tertentu. Namun, kami masih melakukan yang lebih baik dari sebelumnya, karena parser JSON tidak harus melakukan penanganan kesalahan semacam ini. Pengurai JSON hanya akan menandai pasangan nilai kunci yang hilang sebagai hilang, dan bagian selanjutnya dari basis kode yang menggunakan pasangan yang hilang dapat memilih untuk melaporkan kesalahan. Kami menyediakan apa yang dikenal sebagai degradasi anggun , di mana pengguna dapat membawa file model lama dan masih menggunakan fungsionalitas dasar.

Bagaimana dengan kompatibilitas ke depan? File JSON yang diberikan oleh versi XGBoost yang lebih baru akan berisi kunci tambahan instance_cnt , yang dapat diabaikan begitu saja oleh versi yang lebih lama. Jadi kompatibilitas ke depan juga berlaku.

Dibebaskan dari masalah kompatibilitas di masa depan, pengembang akan dapat fokus pada pekerjaan pengembangan yang lebih menarik.

Versi semantik

Setiap file JSON yang diproduksi oleh XGBoost harus memiliki bidang versi mayor dan minor di tingkat root:

{
  "major_version": 1,
  "minor_version": 0,

  ... other key-value pairs ...
}

Semua versi yang memiliki versi utama yang sama kompatibel satu sama lain. Misalnya, program yang menggunakan format versi 1.1 dapat membaca model dengan versi apa pun dari formulir 1.x. Versi minor harus ditingkatkan setiap kali bidang baru ditambahkan ke objek JSON.

Versi utama harus disimpan ke 1, kecuali jika terjadi revisi besar yang merusak kompatibilitas mundur dan/atau maju. Jadi versi 2.x tidak akan kompatibel dengan 1.x.

Lihat Lampiran A untuk skema lengkap Versi 1.0.

Manfaat lain dari JSON

Kompatibilitas adalah motivator terbesar bagi saya untuk mengusulkan JSON sebagai format serialisasi generasi berikutnya. Namun, ada manfaat lain juga:

  • Parser JSON tersedia di hampir semua bahasa dan platform, jadi kami mendapatkan parser dan serializer gratis (dalam hal upaya pemrogram) untuk banyak bahasa. Akan mudah untuk mengurai model XGBoost dengan Python, Java, JavaScript, dan banyak lainnya. Fakta ini ditunjukkan oleh @KOLANICH.
  • Ini adalah teks yang dapat dibaca manusia, bagus untuk keterbacaan.
  • Objek parameter DMLC ( dmlc::Parameter ) direpresentasikan dengan rapi sebagai objek JSON, karena objek parameter hanyalah kumpulan pasangan nilai kunci. Kita sekarang bebas untuk menambahkan lebih banyak bidang ke objek parameter DMLC.
  • Menjadi teks, itu adalah endian-independen, asalkan kami menggunakan UTF-8 untuk pengkodean teks. Saya telah menerima laporan bahwa mesin big-endian gagal memuat file serial biner yang disalin dari mesin little-endian.
  • JSON relatif kompak dibandingkan dengan format berbasis teks lainnya (misalnya XML).

Implementasi proposal dengan parser DMLC JSON

Ada banyak perpustakaan JSON pihak ketiga yang sangat baik untuk C++, misalnya Tencent/RapidJSON . Namun, kami akan menghindari perpustakaan pihak ketiga. Seperti yang ditunjukkan @tqchen , dependensi ekstra mempersulit porting XGBoost ke berbagai platform dan target. Selain itu, sebagian besar perpustakaan JSON mengasumsikan objek tanpa skema bentuk bebas, di mana objek dan array JSON dapat saling bersarang secara sewenang-wenang (terima kasih @KOLANICH dan @tqchen untuk menunjukkan hal ini). Asumsi ini menambahkan overhead yang tidak perlu, ketika format JSON XGBoost yang kami usulkan memiliki skema reguler.

Untungnya, repositori DMLC-Core dilengkapi dengan parser dan serializer JSON bawaan. Terlebih lagi, parser DMLC JSON ( dmlc::JSONReader ) memungkinkan kita membuat asumsi tentang bagaimana objek dan array JSON akan disarangkan . Misalnya, jika kita mengharapkan objek JSON yang berisi pasangan nilai kunci tunggal dengan kunci foo dan nilai array bilangan bulat, kita akan menulis:

dmlc::JSONReader reader(&input_stream);
std::string key, value;
reader.BeginObject();  // Assume: the top-level object is JSON object, not array
assert(reader.NextObjectItem(&key));  // Assume: there is one key
assert(key == "foo");  // Assume: the key is "foo"
reader.BeginArray();  // Assume: the value for "foo" is an array

std::vector<int> foo_values;
while (reader.NextArrayItem()) {
  int val;
  reader.Read(&val);  // Assume: each element in the array is an integer
  foo_values.push_back(val);
}
assert(!reader.NextObjectItem(&key));  // Assume: there is only one key

Satu bagian yang sulit adalah bahwa kelas pembantu bawaan untuk struktur membaca akan menimbulkan kesalahan saat menemukan pasangan nilai kunci yang hilang atau ekstra. Saya akan menulis kelas pembantu khusus sebagai bagian dari konsep implementasi RFC. Kelas pembantu khusus akan menangani pasangan nilai kunci yang hilang dengan cara yang konsisten. Lampiran B menjelaskan bagaimana nilai yang hilang direpresentasikan untuk setiap tipe data.

Mengatasi kemungkinan keberatan

Q. Bagaimana dengan efisiensi ruang? JSON akan menghasilkan file model yang lebih besar daripada metode serialisasi biner saat ini.

A. Ada beberapa perpustakaan pihak ketiga (mis. MessagePack ) yang memungkinkan kita mengkodekan JSON ke dalam bentuk biner yang lebih ringkas. Kami akan mempertimbangkan untuk menambahkan plugin untuk menghasilkan pengkodean biner, sehingga dependensi pihak ketiga tetap opsional. Masih akan ada beberapa overhead untuk menyimpan integer dan penyimpanan float, tetapi biayanya menurut saya berharga; lihat bagian sebelumnya untuk manfaat kompatibilitas JSON.

T. Bagaimana dengan kinerja baca/tulis? Semua operasi teks akan memperlambat segalanya.

A. Saya akui sulit untuk mengalahkan kinerja serialisasi biner; bagaimana Anda bisa melakukan lebih baik daripada hanya membuang pola bit dari memori? Kami benar-benar melakukan trade-off di sini, memperdagangkan kecepatan baca/tulis untuk ekstensibilitas di masa mendatang. Namun, kami dapat mengurangi masalah dengan membuat serialisasi pohon keputusan secara paralel, lihat Microsoft/LightGBM#1083 sebagai contoh.

T. Format biner tidak perlu ditutup untuk ekstensi di masa mendatang. Dengan hati-hati, dimungkinkan untuk merancang format biner baru dengan mempertimbangkan ekstensibilitas masa depan.

J. Sangat mungkin untuk merancang format biner untuk mengaktifkan ekstensi di masa mendatang. Faktanya, jika versi XGBoost saat ini menyimpan nomor versi dalam artefak model, kami dapat menambahkan logika ekstra untuk menangani berbagai format. Namun, saya masih berpikir JSON lebih unggul, karena dua alasan: 1) merancang format biner yang dapat diperluas membutuhkan ketelitian dan kehati-hatian yang tinggi untuk melakukannya dengan benar; dan 2) logika ekstra untuk mengelola beberapa versi dalam pengurai khusus dapat menjadi berantakan, seperti yang kita lihat di bagian pertama.

Lampiran A. Skema 1.0 Lengkap

Skema lengkap dapat diakses di https://xgboost-json-schema.readthedocs.io/en/latest/. Sumber di-host di https://github.com/hcho3/xgboost-json-schema.

Lampiran B: Representasi nilai yang hilang untuk berbagai tipe data

  • String: ""
  • Titik-mengambang: nan
  • Bilangan bulat: std::numeric_limits<int_type>::max()
  • Objek/Array JSON: null (objek nol)
RFC

Semua 48 komentar

Saya memiliki beberapa masalah lagi yang perlu ditangani, sebagian besar terkait dengan parameter. Memperkenalkan JSON adalah bagian dari rencana. Itu tidak akan bertentangan dengan proposal saat ini (pekerjaan bagus). Akan menambahkan komentar yang tepat sesegera mungkin. :) Dan itu sangat bagus untuk memiliki RFC terorganisir, itu jauh lebih baik daripada usaha awal saya.

(misalnya MessagePack)

... dan CBOR

@hcho3 dapatkah Anda menjelaskan lebih lanjut tentang bagaimana kami menyimpan struktur pohon dan memberikan contoh minimum?

@tqchen Saya akan segera memasang skema lengkap, dan sebagai bagian dari itu akan menjelaskan bagaimana struktur pohon akan disimpan.

Model lainnya

bermigrasi ke JSON untuk membuat serial model pohon keputusan.

Model linier juga didukung, sebenarnya jauh lebih mudah daripada model pohon. Dan saya memiliki hasrat untuk menambahkan algoritme baru yang menarik.

Menghentikan (mengubah) parameter

Alamat RFC menambahkan parameter baru secara eksplisit. Namun pada kenyataannya, kami mungkin akan menghentikan parameter di masa mendatang karena fitur algoritme yang tidak digunakan lagi, parameter yang diduplikasi untuk komponen yang berbeda ( n_gpus dan gpu_id muncul di benak) atau fitur yang rusak dinonaktifkan untuk sementara. Untungnya, menangani parameter yang dihapus adalah kebalikan dari menambahkan parameter baru, yang dibahas dalam RFC ini.

Kompatibilitas terbalik

Membaca model lama dari versi XGBoost yang lebih baru. Parameter tambahan dihapus dalam versi yang lebih baru tetapi disajikan dalam file model lama dapat diabaikan begitu saja. Ini sesuai dengan situasi kompatibilitas ke depan untuk menambahkan parameter baru.

Kompatibilitas ke depan

Membaca model baru dari versi XGBoost yang lebih lama. Ini sesuai dengan situasi kompatibilitas mundur dalam menambahkan parameter baru. Di sini kita akan menghapus parameter, yang tidak disajikan dalam file model, sebagai nilai yang hilang. Karena sebagian besar parameter memiliki nilai default, parameter yang tidak ditampilkan dalam file model (hilang) juga dapat diabaikan begitu saja.

Meringkaskan

| | Maju | Mundur |
|:------:|:-------------------------------------------------- --------------:|:--------------------------------- ------------:|
| Tambahkan | Kunci ekstra dapat diabaikan begitu saja oleh versi yang lebih lama . | Gunakan nilai default untuk menyerahkan nilai yang hilang atau lakukan degradasi dengan anggun |
| Hapus | Gunakan nilai default untuk menyerahkan nilai yang hilang atau lakukan degradasi dengan anggun | Kunci ekstra dapat diabaikan oleh versi yang lebih baru |

Menyimpan snapshot lengkap XGBoost

Sebagian besar kelas (dalam C++) di XGBoost memiliki parameternya sendiri. Misalnya, tujuan, split-evaluator. Saat ini parameter ini tidak disimpan secara terpadu dan pada kenyataannya, jarang disimpan. Harapan saya adalah kami menambahkan antarmuka IO ke kelas-kelas ini, dan mengulang semuanya saat Menyimpan/Memuat XGBoost. Sebagai contoh, kelas learner dapat memanggil metode Save/Load dari updater , lalu updater memanggil Save/Load dari split-evaluator s dan segera. Setelah selesai dan tumpukan panggilan kembali, kita akan memiliki representasi lengkap dari snapshot status XGBoost.

Masalah

Dengan cara ini kita mungkin sebenarnya membutuhkan representasi skema yang lebih sedikit, misalnya, MonotonicConstraint adalah salah satu dari split-evaluators , yang berisi parameter array ( std::vector ). Dan kami memiliki daftar split-evaluators . Belum jelas bagi saya bagaimana mencapai ini dengan dmlc::JSON* tanpa banyak kode boilerplate. Perlu diketahui bahwa setiap kelas pembantu yang ditambahkan adalah bagian dari kode yang sulit untuk diintegrasikan.

Validasi parameter

Ini adalah rencana masa depan. Jika ada kesalahan ketik atau parameter yang tidak digunakan dalam model yang ditentukan pengguna, XGBoost mengabaikannya, yang dapat menyebabkan penurunan kinerja atau perilaku yang tidak diharapkan. Kami @RAMitchell ingin menambahkan pemeriksaan ekstra untuk parameter yang salah/tidak digunakan. Belum jelas bagi saya bagaimana mencapai ini, tetapi ide dasarnya adalah membiarkan setiap komponen bertanggung jawab untuk memeriksa/menandai parameter unik mereka sendiri dan membiarkan kelas manajer (mungkin learner ) untuk memeriksa yang tidak digunakan (tidak ditandai oleh komponen lain) parameter setelah semua komponen selesai dengan konfigurasinya. Mungkin logika ini dapat bekerja sama untuk menangani parameter model IO yang hilang/tambahan?

@hcho3 Jangan ragu, beri tahu saya jika saya dapat membantu dengan cara apa pun.

@trivialfis Saya suka ide snapshot lengkap, setelah melakukan peretasan yang berantakan untuk mempertahankan parameter prediktor GPU (#3856). Karena itu, kami ingin melakukannya dengan cara meminimalkan kode boilerplate. Saya akan membaca ulang komentar Anda ketika saya menyelesaikan draft untuk skema 1.0. Terutama, saya mendokumentasikan apa yang sedang disimpan.

@trivialfi Juga, saya dulu takut menyimpan lebih banyak hal ke dalam model, karena kewajiban pemeliharaan di masa depan yang disebabkan oleh serialisasi biner. Saya mungkin lebih nyaman sekarang dengan menyimpan semuanya, sekarang serialisasi JSON dapat mencegah sakit kepala kompatibilitas.

IMHO kita harus membuat XGBoost menyimpan sendiri hyperparams yang digunakan untuk membuat dan mengevaluasi model (dengan kemungkinan untuk menonaktifkannya) dan nama dan tipe kolom menjaga urutannya (dengan kemungkinan untuk menonaktifkannya dan/atau membuang nama yang disimpan dan menggunakan bilangan bulat (mulai dari 0) tanpa memuat ulang model). Nama kolom diperlukan untuk membentuk kembali DataFrame panda untuk membuat kolom memiliki urutan yang sama dengan model yang dilatih (saat ini saya menyimpan info ini dalam file terpisah). Tapi saya masih bersikeras untuk menjaga data dengan skema yang ketat, tidak ada barang sewenang-wenang yang harus disimpan di sana, karena itu akan menghasilkan model yang membengkak.

@KOLANICH Saya tidak begitu yakin tentang itu, karena nama dan tipe kolom berada di luar basis kode C++ inti. Sejauh ini, kami memiliki pembungkus Python / R / Java untuk menyimpan informasi ini.

@trivialfis @RAMitchell Bagaimana menurut Anda?

@KOLANICH @hcho3

dan kolom nama dan jenis menjaga pesanan mereka

Ini adalah saran yang layak agar kami mengelola semua ini di C++, ini dapat mengarah pada manajemen data yang lebih bersih. Tapi itu topik untuk hari lain karena kita harus terlebih dahulu mengimplementasikan kode manajemen ini kemudian mengimplementasikan IO. Menambahkan fitur ini nantinya tidak akan terlalu merepotkan karena kami akan memiliki kompatibilitas mundur yang sangat baik seperti yang dijelaskan oleh @hcho3 . Masalah baru mungkin diinginkan setelah ini diselesaikan.

tidak ada barang sewenang-wenang yang harus disimpan di sana, karena akan menyebabkan kembung.

Anda dapat menyarankan item spesifik apa yang tidak boleh disimpan ke dalam file model setelah memiliki draf awal (saya membayangkan akan ada proses peninjauan yang lama, Anda dipersilakan untuk bergabung), jika tidak, sulit untuk berdebat apa yang masuk ke dalam kategori "sewenang-wenang hal-hal".

@trivialfis @tqchen Saya telah mengunggah skema lengkap sebagai lampiran PDF.

Saya mungkin ingin memasang dokumen skema di repo GitHub, sehingga kami dapat memperbarui sumber LaTeX pada setiap pembaruan skema dan mengkompilasinya secara otomatis ke dalam PDF.

@trivialfis Dokumen PDF tidak menjawab saran Anda. Bisakah Anda melihat dan melihat bagaimana ide snapshot lengkap dapat diimplementasikan?

@hcho3 Jika Anda meletakkan dokumen di repo GitHub, saya dapat membuat PR. Kemudian Anda dapat mengomentari perubahan saya. :)

Mari kita gunakan penurunan harga untuk mendokumentasikan skema, pada akhirnya harus ada di dokumen

jika tidak, sulit untuk memperdebatkan apa yang masuk ke dalam kategori "barang sewenang-wenang".

Saya benar-benar bermaksud hal-hal yang sewenang-wenang di sana. Misalnya cuaca di Venus atau set data multi-GiB penuh. Jika kami mengizinkan pengembang melakukan hal-hal seperti itu dengan mudah, mereka akan melakukannya dan ini akan menghasilkan file model yang sangat membengkak.

Mari kita gunakan penurunan harga untuk mendokumentasikan skema, pada akhirnya harus ada di dokumen

Mengapa tidak menggunakan JSONSchema? Ini adalah spesifikasi yang dapat dibaca mesin, dapat divalidasi secara otomatis, dan dapat dirender ke dalam dokumentasi, termasuk interaktif yang bagus.

@KOLANICH

Saya benar-benar bermaksud hal-hal yang sewenang-wenang di sana. Misalnya cuaca di Venus atau set data multi-GiB penuh.

Anda tidak dapat benar-benar mencegah pengguna untuk menyimpan hal-hal arbitrer dalam file JSON mereka. Apa yang dapat kita lakukan adalah menghitung bidang mana yang dikenali oleh XGBoost. Dokumen skema saya melakukan ini. Hal-hal yang tidak dikenali oleh skema akan diabaikan.

Mengapa tidak menggunakan JSONSchema?

Saya tidak yakin bagaimana kami dapat mendukung kelas StringKeyValuePairCollection yang memungkinkan pasangan nilai kunci arbitrer.

Untuk saat ini, saya akan mengetik dokumen skema di RST.

Anda tidak dapat benar-benar mencegah pengguna untuk menyimpan hal-hal arbitrer dalam file JSON mereka.

Ya, kami tidak bisa. Tetapi kami dapat mencegah pengguna untuk menyimpan data arbitrer mereka dengan menghindari menyediakan API untuk menambahkan dan mengeditnya. Jadi jika dia ingin melakukannya, dia harus mengurai gumpalan yang dihasilkan dan menambahkannya secara manual dan membuat serial kembali, jadi mungkin lebih mudah baginya untuk menyimpannya dalam file terpisah.

@tqchen Saya telah mengatur skema di RST: https://xgboost-json-schema.readthedocs.io/en/latest/

@trivialfis Anda dapat mengirimkan permintaan tarik di https://github.com/hcho3/xgboost-json-schema

Pembaruan: Lihat hcho3/xgboost-json-schema#3 untuk diskusi tentang membuat serial snapshot XGBoost yang lebih lengkap.

Biarkan saya mencoba untuk menjaga benang bersama-sama. Disalin dari https://github.com/hcho3/xgboost-json-schema/issues/3 .

Sebelum saya mulai mengerjakannya, harap pertimbangkan untuk menghentikan cara kami menyimpan model saat ini. Dari skema yang ditentukan, file JSON adalah versi utf-8 dari format biner saat ini. Bisakah kita membuka kemungkinan memperkenalkan skema yang cocok dengan XGBoost lengkap itu sendiri daripada format biner lama?

Sebagai contoh:

// dalam cuplikan kode JSON adalah komentar untuk tujuan demonstrasi, tidak boleh muncul dalam file model sebenarnya.

Mari kita ambil kelas Learner dalam draft sebagai contoh:

{
  "learner_model_param" : LearnerModelParam,
  "predictor_param" : PredictorParam,
  "name_obj" : string,
  "name_gbm" : string,
  "gbm" : GBM,
  "attributes" : StringKeyValuePairCollection,
  "eval_metrics" : [ array of string ],
  "count_poisson_max_delta_step" : floating-point
}

Di sini draft menentukan kita menyimpan predictor_param dan count_posson_max_delta_step , yang bukan milik Learner itu sendiri. Sebaliknya saya mengusulkan kita menyimpan sesuatu seperti ini untuk Learner :

{
  // This belongs to learner, hence handled by learner
  "LearnerTrainParam": { LearnerTrainParam },   
  // No `LearnerModelParameter`, we don't need it since JSON can save complete model.

  "predictor" : "gpu_predictor",
  "gpu_predictor" : {
    "GPUPredictionParam": { ... }
  },

  "gbm" : "gbtree",
  "gbtree" : { GBM },

  // This can also be an array, I won't argue which one is better.
  "eval_metrics" : {
    "merror": {...},
    "mae": {...}
  }
}

Pembaruan: Sebenarnya predictor harus ditangani di gbm , tetapi biarkan tetap di sini demi demonstrasi.

Untuk IO aktual GPUPredictionParam , kita serahkan pada gpu_predictor . Hal yang sama berlaku untuk GBM .
Untuk cara melakukannya, kita bisa mengimplementasikan ini di kelas Learner :

void Learner::Load(KVStore& kv_store) {
  std::string predictor_name = kv_store["predictor"].ToString();  // say "gpu_predictor" or "cpu_predictor"
  auto p_predictor = Predictor::Create(predictor_name);
  p_predictor->Load(kv_store[predictor_name]);

  // (.. other io ...)

  KVStore& eval_metrics = kv_store["eval_metrics"];
  std::vector<Metric> metrics (eval_metrics.ToMap().size());
  for (auto& m : metrics) {
    metrics.Load(eval_metrics);
  }
}

Di dalam Metric , misalkan mae :

void Load(KVStore& kv_store) {
   KVStore self = kv_store["mae"];  // Look up itself by name.
  // load parameters from `self`
  // load other stuffs if needed.
}

Motivasi

Alasan saya ingin melakukannya dengan cara ini adalah:

  1. Tidak extra_attributes . Itu obat untuk tidak dapat menambahkan bidang baru. Sekarang kita bisa melakukannya.
  2. Modular . Setiap kelas C++ menangani apa yang dimilikinya, begitu ada kebutuhan untuk menangani perubahan kode khusus dari model yang dibuang, Learner tidak membengkak.
  3. terorganisir. Dengan cara ini akan lebih mudah bagi manusia untuk menafsirkan model karena mengikuti hierarki yang cocok dengan XGBoost itu sendiri, apa yang Anda lihat dari file JSON, adalah seperti apa tampilan internal XGBoost.
  4. Menyelesaikan. Dijelaskan dalam (3).
  5. model lainnya. Terkait dengan (2). Kita dapat menyimpan model linier dengan mudah, karena ia menangani IO-nya sendiri.
  6. Dapat di-bekerja sama ada Configuration . Di dalam XGBoost, fungsi seperti Configure , Init hanyalah cara lain untuk memuat kelas itu sendiri dari parameter.

Yang paling penting adalah (2).

Apa yang harus disimpan?

Ditunjukkan oleh @hcho3 , kita harus menyusun daftar untuk apa yang masuk ke file dump akhir. Saya akan mulai mengerjakannya setelah ini disetujui oleh peserta.

Kemungkinan keberatan

  1. Ukuran file model.
    Sebagian besar bidang yang ditambahkan adalah parameter. Mereka adalah bagian penting dari model. Representasi yang bersih dan lengkap harus sepadan dengan ruangnya.
  2. Kurang skema
    Sebelumnya saya menggunakan split_evaluator sebagai contoh di https://github.com/dmlc/xgboost/issues/3980#issuecomment -445525072 . Ada kemungkinan kita @RAMitchell ~remove~ menggantinya dengan yang lebih sederhana karena tidak kompatibel dengan GPU. Jadi kita harus tetap memiliki skema, tetapi sedikit lebih rumit daripada skema saat ini.

@hcho3 @KOLANICH @tqchen @thvasilo @RAMitchell Bisakah Anda melihatnya jika waktu memungkinkan?

@trivialfis Saya memberikan suara saya +1 untuk proposal Anda. Saya pikir adalah mungkin untuk menggunakan dmlc::JSONReader secara langsung, tanpa struktur perantara KVStore , selama kita menggunakan skema dalam bentuk yang mirip dengan proposal saya saat ini. Saya akan mengunggah implementasi uji coba, setelah saya selesai bersiap-siap untuk liburan.

@ hcho3 Biarkan saya mencoba menyusun skema. :)

Sementara banyak ide bagus di sini, saya tidak benar-benar mendapatkan motivasi utama untuk semua ini:

Sayangnya, metode ini memiliki satu kekurangan yang signifikan: tidak mungkin menambahkan bidang baru tanpa merusak kompatibilitas mundur... Basis kode XGBoost saat ini tidak menyimpan nomor versi. Jadi tidak ada cara untuk menanyakan versi file serial.

Mengapa tidak ada jalan? Misalnya, beberapa pola byte header yang berbeda dapat dimasukkan di awal (misalnya, hanya karakter "xgboost") yang akan membantu membedakan format baru (yang juga akan memiliki nomor versi yang disimpan) dari format lama yang tidak memiliki versi rekaman.

@khotilov

beberapa pola byte header yang berbeda dapat dimasukkan di awal

Ya. Ada hal-hal lain. Misalnya saya ingin melepaskan kelas Learner dari melakukan semua konfigurasi. Dan masalah endianness #4072 . Selain itu, Json seharusnya membuat segalanya "lebih mudah".

Saya tidak tahu apakah beberapa proyek hilir dapat berpadu di sini yang menggunakan model, dan masalah apa pun yang mungkin mereka miliki dengan model biner, atau manfaat potensial dari format JSON. Misalnya @hcho3 memakai topi Treelite-nya atau SHAP by @slundberg .

Proyek-proyek ini mendukung model impor dari beberapa pelajar pohon seperti LightGBM jadi mungkin mereka memiliki pandangan yang lebih baik tentang perbedaan antara pendekatan.

Satu komentar yang harus saya masukkan ke dalam diskusi ini adalah berhati-hati dalam memastikan model yang disimpan dalam format JSON sebenarnya sama dengan apa yang ada di memori setelah dimuat ulang (bit yang sama persis). Saya baru-baru ini mulai memuat versi biner model untuk SHAP alih-alih fungsionalitas dump JSON saat ini karena mereka berperilaku berbeda karena masalah presisi yang muncul dari parsing dump JSON ( @hcho3 telah melihat ini di #4060).

@thvasilo Saya akan senang mendengar tentang umpan balik.

@slundberg Ya. Saya membuat proposal di https://github.com/hcho3/xgboost-json-schema/pull/5 . Harus dapat mewakili model internal sedekat mungkin.

@slundberg Saya menulis program C kecil untuk menunjukkan bahwa nilai floating point 32-bit dapat disimpan sebagai teks tanpa kehilangan:

#include <cmath>
#include <cstdio>
#include <chrono>
#include <iostream>
#include <sstream>
#include <limits>
#include <string>
#include <iomanip>
#include <stdexcept>
#include <fmt/format.h>

void test_round_trip(float x) {
  std::string buf = fmt::format("{:.{}g}", x, std::numeric_limits<double>::max_digits10);
  float y = static_cast<float>(std::stod(buf));
  if (x != y) {
    std::ostringstream oss;
    union {
      float f;
      uint32_t u;
    } f2u;
    oss << "Round trip failed to preserve a number. ";
    f2u.f = x;
    oss << std::hex << f2u.u << " vs ";
    f2u.f = y;
    oss << std::hex << f2u.u;
    throw std::runtime_error(oss.str());
  }
}

int main(void) {
  auto tstart = std::chrono::high_resolution_clock::now();

  float x = std::numeric_limits<float>::lowest();
  const float max = std::numeric_limits<float>::max();
  uint64_t count = 0;
  while (x < max) {
    test_round_trip(x);
    ++count;
    x = std::nextafter(x, max);
  }
  auto tend = std::chrono::high_resolution_clock::now();

  std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(tend - tstart).count() << " ms elapsed" << std::endl;
  std::cout << "Count: " << count << std::endl;

  return 0;
}

Butuh waktu 55 menit untuk menyelesaikan instans c5.2xlarge.

@hcho3 bagus sekali! Kemudian kekhawatiran saya terpenuhi (saya sekarang ingin tahu apa yang menyebabkan ketidakcocokan yang saya temukan dari output JSON, tetapi tidak dapat kembali dan men-debug itu sekarang). Secara keseluruhan memiliki serialisasi model yang dapat dibaca bagus terdengar bagus.

@slundberg Saya merevisi snippet sehingga kita tidak perlu mengonversi float menjadi double saat membuat serial. Ini meningkatkan kinerja (sebesar 27%: 55 mnt -> 43 mnt).

#include <cmath>
#include <cstdio>
#include <chrono>
#include <iostream>
#include <sstream>
#include <limits>
#include <string>
#include <cstring>
#include <iomanip>
#include <stdexcept>
#include <fmt/format.h>

void test_round_trip(float x) {
  std::string buf = fmt::format("{:.{}g}", x, std::numeric_limits<float>::max_digits10);
  float y = std::strtof(buf.c_str(), NULL);
  if (errno == ERANGE) {
    const float huge_val = std::numeric_limits<float>::infinity();
    if (x > -huge_val && x < huge_val) {
      // Ignore ERANGE for denormals.
      // The C++ standard requires ERANGE to be thrown for denormals,
      // but we want to preserve them too
      errno = 0;
    } else if (errno) {
      std::cout << std::strerror(errno) << std::endl;
      throw std::invalid_argument(fmt::format("strtof failed. x = {}", buf));
    }
  }
  if (x != y) {
    std::ostringstream oss;
    union {
      float f;
      uint32_t u;
    } f2u;
    oss << "Round trip failed to preserve a number. ";
    f2u.f = x;
    oss << std::hex << f2u.u << " vs ";
    f2u.f = y;
    oss << std::hex << f2u.u;
    throw std::runtime_error(oss.str());
  }
}

int main(void) {
  auto tstart = std::chrono::high_resolution_clock::now();

  float x = std::numeric_limits<float>::lowest();
  const float max = std::numeric_limits<float>::max();
  uint64_t count = 0;
  while (x < max) {
    test_round_trip(x);
    ++count;
    x = std::nextafter(x, max);
  }
  auto tend = std::chrono::high_resolution_clock::now();

  std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(tend - tstart).count() << " ms elapsed" << std::endl;
  std::cout << "Count: " << count << std::endl;

  return 0;
}

Hai, apakah fungsi ini tersedia dalam Python sekarang? Terima kasih.

@LeZhengThu Tidak, utas ini hanya sebuah proposal. Ini akan memakan waktu cukup lama hingga fitur ini diimplementasikan.

@hcho3 Terima kasih. Semoga fitur ini bisa segera tayang. Saya sangat menyukai ide itu.

@hcho3 Terakhir kali kita berbicara tentang JSON, disarankan bahwa dmlc::Json* harus digunakan daripada implementasi 3-pihak atau implementasi Json yang dirancang sendiri. Dan saya ingat bahwa Anda memiliki beberapa pemikiran tentang cara menggunakan dmlc::Json* ?

Salah satu masalah yang mungkin muncul dengan serialisasi JSON adalah perlakuan terhadap nilai float serial. Agaknya xgboost akan menyadari bahwa semua nilai desimal di JSON harus dilemparkan sebagai pelampung; namun saya percaya sebagian besar parser JSON akan memperlakukan nilai sebagai ganda, yang mengarah ke perbedaan untuk pengguna hilir yang tidak menerapkan langkah pasca-parsing untuk melemparkan ganda ke float di objek yang dihasilkan.

Seperti yang ditunjukkan @hcho3 , dimungkinkan untuk melakukan perjalanan pulang pergi menggunakan maxdigits10 , yang menjamin bahwa float akan dapat direproduksi dari teks desimal tetapi semua nilai desimal yang diuraikan harus dilemparkan ke float untuk objek yang dihasilkan untuk mewakili keadaan model dengan benar. Itu adalah langkah yang tidak dijamin melalui parse JSON sederhana.

Seperti yang ditunjukkan oleh @thvasilo , setiap pengguna hilir seperti SHAP oleh @slundberg dan Treelite perlu mengetahui perlakuan ini dan menerapkan langkah pasca-penguraian untuk melemparkan ganda apa pun ke pelampung (dan kemudian memperlakukannya sebagai pelampung dalam perhitungan lebih lanjut, yaitu . fexp bukannya exp , dll.).

Karena ini adalah komentar pertama saya dalam masalah ini, semoga bermanfaat!

@ras44 Skema JSON dengan jelas mengatakan bahwa semua nilai floating-point adalah 32-bit: https://xgboost-json-schema.readthedocs.io/en/latest/#notations

@hcho3 Terima kasih atas catatan Anda. Poin utama saya adalah bahwa jika dump JSON akan digunakan oleh apa pun selain xgboost (yang mungkin perlu mengimplementasikan parser JSON khusus yang mengkodekan/mendekode float sesuai skema), pengguna harus mempertimbangkan banyak aspek secara berurutan menghasilkan hasil yang sama seperti xgboost. Tampaknya ini sudah terjadi dalam masalah seperti #3960, #4060, #4097, #4429. Saya memberikan PR https://github.com/dmlc/xgboost/pull/4439 menjelaskan beberapa masalah dan pertimbangan jika itu dapat berguna bagi orang-orang yang ingin bekerja dengan dump JSON.

@hcho3 Saya akan melanjutkan dan membuat implementasi konsep awal berdasarkan skema.

Saya akan mencoba menggunakan dmlc::JSON, tetapi menambahkan abstraksi kapan pun diperlukan.

Terima kasih untuk pembaruannya. Maaf saya belum punya kesempatan untuk menangani implementasinya. Beri tahu saya jika Anda memerlukan bantuan atau umpan balik.

Saya ingin meninjau kembali beberapa diskusi tentang penggunaan dependensi eksternal sebagai mesin JSON kami. Setelah beberapa diskusi dengan @trivialfis dan melihat #5046, tampak jelas bagi saya bahwa implementasi json parsial yang sederhana dimungkinkan di dmlc-core atau xgboost tetapi tidak memiliki fitur yang lengkap atau dengan kinerja yang baik. Kami mencari ribuan baris kode untuk implementasi khusus dengan kinerja yang cukup baik.

Mencapai kinerja yang lebih baik daripada implementasi naif mungkin menjadi penting karena implementasi terdistribusi kami bersambung selama pelatihan.

Saya biasanya sangat menentang dependensi tetapi dalam kasus ini menggunakan header saja, ketergantungan json dapat menyelesaikan lebih banyak masalah daripada yang ditimbulkannya.

@hcho3 @CodingCat @chenqin @tqchen

Saya cenderung kami menggunakan penulis/pembaca json yang disesuaikan selama diuji dengan baik, memperkenalkan ketergantungan di luar kendali kami dapat menjadi tanggung jawab dengan banyak fitur yang tidak digunakan. Memiliki Json dapat membantu untuk menyederhanakan protokol pemulihan rabit. pekerja yang gagal dapat memulihkan dari muatan JSON kunci/nilai berukuran fleksibel dari host yang berdekatan.

menggunakan header hanya ketergantungan json

nlohmann/JSON tidak terlalu berkinerja.

@KOLANICH Memang, antarmuka yang bagus tetapi baik penggunaan memori maupun komputasi tidak efisien.

Penutupan karena sekarang dukungan eksperimental dengan Skema digarisbawahi. Perbaikan lebih lanjut akan datang sebagai PR terpisah.

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

XiaoxiaoWang87 picture XiaoxiaoWang87  ·  3Komentar

choushishi picture choushishi  ·  3Komentar

nicoJiang picture nicoJiang  ·  4Komentar

trivialfis picture trivialfis  ·  3Komentar

ivannz picture ivannz  ·  3Komentar