1 Objective

Menggunakan model yang sudah dibuat di R untuk kebutuhan prediksi.

2 Load Package

Untuk pembahasan kali ini kita tidak akan menggunakan package tambahan selain base dan stats.

3 Background

Saya pernah mengikuti sebuah training tentang Machine Learning. Kemudian pada suatu ketika setelah trainer menjelaskan dan peserta ikut mempraktikan, trainer mengatakan hal yang menurut saya agak menggelitik. Kurang lebih begini yang disampaikan.

“Inilah kelebihan software **** ini (bukan menggunakan R waktu itu trainingnya), hasil modelnya dapat disimpan dalam sebuah external file. Kalau nanti mau digunakan untuk melakukan prediksi data baru cukup load saja model di external file ini. Software lain (saya lupa dia menyebutkan nama software lain atau tidak) tidak bisa begini.”

Selesai trainer mengatakan itu, otak saya merasa greget. Ingin sekali mengatakan “Hey! R juga bisa keles!”. Bagaimana caranya? Mari kita bahas.

4 Save Object

Seperti yang kita tahu, jika kita mengerjakan sebuah pekerjaan di R, kemudian kita tutup session R tersebut maka semua objek yang sudah dibuat akan hilang. Untuk tetap dapat menggunakan kembali objek yang sudah dibuat maka kita perlu menyimpannya. Saya sangat menyarankan untuk tidak menggunakan pilihan save ketika menutup R atau RStudio.

Di R ada 2 (dua) fungsi yang paling dikenal untuk menyimpan objek ke dalam file eksternal. File eksternal ini bukan berupa *.txt, *.csv atau format menyimpan data yang lain. File ini yang nantinya akan dipanggil oleh R untuk digunakan kembali. Objek yang disimpan ini tidak terbatas pada objek berupa model saja, tapi juga data.frame dan semua objek pada session R tersebut.

5 Function save() and saveRDS()

Hadley Wickham pernah membuat sebuah cuitan di Twitter tentang menyimpan objek di R dan dia lebih menyarankan menggunakan fungsi saveRDS() dan readRDS().

5.1 What is the difference of save() and saveRDS()?

Perbedaan yang paling mendasar antara fungsi save() dan saveRDS() adalah

  • save() dapat menyimpan banyak objek sekaligus dalam sebuah file eksternal.
  • saveRDS() hanya menyimpan satu objek saja dalam sebuah file eksternal.

Perbedaan lainnya adalah save() akan secara otomatis mengganti objek yang ada pada sessio R berjalan dengan objek yang ada pada file eksternal jika ada nama objek yang sama tanpa pemberitahuan. Resiko ditanggung pengguna!

Tidak seperti fungsi save() yang menyimpan objek dengan namanya, saveRDS() hanya menyimpan struktur objek tanpa namanya. Ketika kita ingin memanggil objek yang disimpan menggunakan fungsi saveRDS() harus dimasukkan ke sebuah objek baru.

Perbedaan selanjutnya kita bahas dengan contoh.

5.2 save()

iris_df <- iris
dim(iris_df)
[1] 150   5
save(list = "iris_df", file = "iris_df.rda")

Data iris_df awalnya berisi 150 baris data. Sekarang kita akan kurangi menjadi 100 baris data misalnya, dengan nama yang sama.

iris_df <- iris_df[1:100,]
dim(iris_df)
[1] 100   5

Kita ingat bahwa data iris_df yang kita simpan berisi 150 baris dan data iris_df saat ini berisi 100 baris data. Sekarang kita panggil lagi data iris_df yang tadi disimpan dengan fungsi load() dan perhatikan banyaknya baris di data iris_df setelah dipanggil ini.

load("iris_df.rda")
dim(iris_df)
[1] 150   5

Lihat? Data iris_df yang ada di session sebelum dipanggil sudah digantikan dengan iris_df yang dipanggil tadi menjadi 150 baris data kembali.

5.3 saveRDS()

Bagaimana dengan fungsi saveRDS()?

saveRDS(object = iris_df, file = "iris_df.rds")
dim(iris_df)
[1] 150   5

Data iris_df masih 150 baris data. Kita kurangi lagi menjadi 100 baris data.

iris_df <- iris_df[1:100,]
dim(iris_df)
[1] 100   5

Selanjutnya kita panggil hasil penyimpanan tadi menggunakan fungsi readRDS().

iris_df_loaded <- readRDS("iris_df.rds")
dim(iris_df_loaded)
[1] 150   5
dim(iris_df)
[1] 100   5

Dengan saveRDS() tidak akan mengganti objek yang saat ini ada di session R jika nama objeknya dibedakan.

6 Build Pretrained Model

Itu tadi pengantar dari tulisan ini. Selanjutnya kita akan membuat sebuah model sederhana menggunakan fungsi lm() untuk membuat model regresi linier. Gunakan data mtcars dengan peubah mpg sebagai peubah respon dan peubah hp dan wt sebagai peubah penjelas atau prediktor. Sebelum membuat model, kita bagi data secara acak untuk data training dan testing.

set.seed(1001)
idx <- sample(1:nrow(mtcars), 0.8*nrow(mtcars))
training <- mtcars[idx, ]
dim(training)
[1] 25 11
testing <- mtcars[-idx, ]
dim(testing)
[1]  7 11
model.lm <- lm(mpg ~ hp + wt, data = training)
summary(model.lm)

Call:
lm(formula = mpg ~ hp + wt, data = training)

Residuals:
    Min      1Q  Median      3Q     Max 
-3.6761 -1.8869 -0.4377  1.1040  5.7707 

Coefficients:
            Estimate Std. Error t value            Pr(>|t|)    
(Intercept) 37.86424    2.00683  18.868 0.00000000000000449 ***
hp          -0.03080    0.01033  -2.982             0.00687 ** 
wt          -4.18265    0.78565  -5.324 0.00002414035741766 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 2.597 on 22 degrees of freedom
Multiple R-squared:  0.8227,    Adjusted R-squared:  0.8066 
F-statistic: 51.06 on 2 and 22 DF,  p-value: 0.000000005427
AIC(model.lm)
[1] 123.4608

Anggap saja ini adalah model terbaik yang kita peroleh dan akan digunakan dalam memprediksi. Selanjutnya kita simpan objek model ini dalam file eksternal.

saveRDS(object = model.lm, file = "best_model.rds")
file.exists("best_model.rds")
[1] TRUE

7 Use Pretrained Model

Kita panggil model yang sudah dibuat dari file eksternal tadi untuk melakukan prediksi.

mymodel <- readRDS("best_model.rds")
testing$predicted <- predict(mymodel, newdata = testing)
head(testing)

Sebagai tambahan, mari kita hitung RMSE dari hasil prediksi ini.

sqrt(mean((testing$predicted - testing$mpg)^2))
[1] 2.675454
plot(testing$mpg, testing$predicted, pch = 19)

8 Conclusion

Menyimpan model atau objek lain dari R ke dalam file eksternal dapat menggunakan fungsi saveRDS() dan save(). Sebaiknya gunakan saveRDS() dan readRDS() jika Anda hanya ingin menyimpan satu objek saja di dalam sebuah file eksternal dan tidak ingin ada objek yang diganti tanpa sepengetahuan Anda ketika memanggil kembali objek tersebut. GUnakan save() dan load() jika banyak objek yg ingin disimpan dengan nama yang sama dan tidak ada objek lain yang akan diganti, atau ketika Anda yakin tidak ada masalah jika ada objek di session R saat ini yang akan diganti. Gunakan salah satu dari fungsi ini untuk menggunakan model yang sudah dibuat.

LS0tDQp0aXRsZTogICJVc2UgUHJldHJhaW5lZCBNb2RlbCBpbiBSIg0KYXV0aG9yOiAiQnkgQWVwIEhpZGF5YXR1bG9oIg0KZGF0ZTogICAiMjAxOSBBdWd1c3QgMjMiDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOg0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6IHNwYWNlbGFiDQogICAgZGZfcHJpbnQ6IHBhZ2VkDQogICAgdG9jOiB5ZXMNCiAgICB0b2NfZGVwdGg6IDQNCiAgICB0b2NfZmxvYXQ6IHRydWUNCi0tLQ0KDQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KDQpib2R5eyAvKiBOb3JtYWwgICovDQogICAgICBmb250LXNpemU6IDE0cHg7DQogIH0NCnRkIHsgIC8qIFRhYmxlICAqLw0KICBmb250LXNpemU6IDEycHg7DQp9DQpoMS50aXRsZSB7DQogIGZvbnQtc2l6ZTogMzhweDsNCiAgY29sb3I6IGxpZ2h0Ymx1ZTsNCiAgZm9udC13ZWlnaHQ6IGJvbGQ7DQp9DQpoMSB7IC8qIEhlYWRlciAxICovDQogIGZvbnQtc2l6ZTogMjRweDsNCiAgY29sb3I6IERhcmtCbHVlOw0KfQ0KaDIgeyAvKiBIZWFkZXIgMiAqLw0KICBmb250LXNpemU6IDIwcHg7DQogIGNvbG9yOiBEYXJrQmx1ZTsNCn0NCmgzIHsgLyogSGVhZGVyIDMgKi8NCiAgZm9udC1zaXplOiAxNnB4Ow0KIyAgZm9udC1mYW1pbHk6ICJUaW1lcyBOZXcgUm9tYW4iLCBUaW1lcywgc2VyaWY7DQogIGNvbG9yOiBEYXJrQmx1ZTsNCn0NCmg0IHsgLyogSGVhZGVyIDQgKi8NCiAgZm9udC1zaXplOiAxNHB4Ow0KICBjb2xvcjogRGFya0JsdWU7DQp9DQpjb2RlLnJ7IC8qIENvZGUgYmxvY2sgKi8NCiAgICBmb250LXNpemU6IDEycHg7DQp9DQpwcmUgeyAvKiBDb2RlIGJsb2NrIC0gZGV0ZXJtaW5lcyBjb2RlIHNwYWNpbmcgYmV0d2VlbiBsaW5lcyAqLw0KICAgIGZvbnQtc2l6ZTogMTJweDsNCn0NCjwvc3R5bGU+DQoNCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQoja25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG89VFJVRSwgcmVzdWx0cz0naG9sZCcsIHdhcm5pbmc9RkFMU0UsIGZpZy5zaG93PSdob2xkJywgbWVzc2FnZT1GQUxTRSkgDQpvcHRpb25zKHNjaXBlbiA9IDk5KQ0KYGBgDQoNCg0KIyBPYmplY3RpdmUNCg0KTWVuZ2d1bmFrYW4gbW9kZWwgeWFuZyBzdWRhaCBkaWJ1YXQgZGkgUiB1bnR1ayBrZWJ1dHVoYW4gcHJlZGlrc2kuDQoNCiMgTG9hZCBQYWNrYWdlDQoNClVudHVrIHBlbWJhaGFzYW4ga2FsaSBpbmkga2l0YSB0aWRhayBha2FuIG1lbmdndW5ha2FuIHBhY2thZ2UgdGFtYmFoYW4gc2VsYWluIGBiYXNlYCBkYW4gYHN0YXRzYC4NCg0KIyBCYWNrZ3JvdW5kDQoNClNheWEgcGVybmFoIG1lbmdpa3V0aSBzZWJ1YWggdHJhaW5pbmcgdGVudGFuZyBNYWNoaW5lIExlYXJuaW5nLiBLZW11ZGlhbiBwYWRhIHN1YXR1IGtldGlrYSBzZXRlbGFoIHRyYWluZXIgbWVuamVsYXNrYW4gZGFuIHBlc2VydGEgaWt1dCBtZW1wcmFrdGlrYW4sIHRyYWluZXIgbWVuZ2F0YWthbiBoYWwgeWFuZyBtZW51cnV0IHNheWEgYWdhayBtZW5nZ2VsaXRpay4gS3VyYW5nIGxlYmloIGJlZ2luaSB5YW5nIGRpc2FtcGFpa2FuLg0KDQo+ICJJbmlsYWgga2VsZWJpaGFuIHNvZnR3YXJlICoqKiogaW5pIChidWthbiBtZW5nZ3VuYWthbiBSIHdha3R1IGl0dSB0cmFpbmluZ255YSksIGhhc2lsIG1vZGVsbnlhIGRhcGF0IGRpc2ltcGFuIGRhbGFtIHNlYnVhaCBleHRlcm5hbCBmaWxlLiBLYWxhdSBuYW50aSBtYXUgZGlndW5ha2FuIHVudHVrIG1lbGFrdWthbiBwcmVkaWtzaSBkYXRhIGJhcnUgY3VrdXAgbG9hZCBzYWphIG1vZGVsIGRpIGV4dGVybmFsIGZpbGUgaW5pLiBTb2Z0d2FyZSBsYWluIChzYXlhIGx1cGEgZGlhIG1lbnllYnV0a2FuIG5hbWEgc29mdHdhcmUgbGFpbiBhdGF1IHRpZGFrKSB0aWRhayBiaXNhIGJlZ2luaS4iDQoNClNlbGVzYWkgdHJhaW5lciBtZW5nYXRha2FuIGl0dSwgb3RhayBzYXlhIG1lcmFzYSAqZ3JlZ2V0Ki4gSW5naW4gc2VrYWxpIG1lbmdhdGFrYW4gIkhleSEgUiBqdWdhIGJpc2Ega2VsZXMhIi4gQmFnYWltYW5hIGNhcmFueWE/IE1hcmkga2l0YSBiYWhhcy4NCg0KIyBTYXZlIE9iamVjdA0KDQpTZXBlcnRpIHlhbmcga2l0YSB0YWh1LCBqaWthIGtpdGEgbWVuZ2VyamFrYW4gc2VidWFoIHBla2VyamFhbiBkaSBSLCBrZW11ZGlhbiBraXRhIHR1dHVwICpzZXNzaW9uKiBSIHRlcnNlYnV0IG1ha2Egc2VtdWEgb2JqZWsgeWFuZyBzdWRhaCBkaWJ1YXQgYWthbiBoaWxhbmcuIFVudHVrIHRldGFwIGRhcGF0IG1lbmdndW5ha2FuIGtlbWJhbGkgb2JqZWsgeWFuZyBzdWRhaCBkaWJ1YXQgbWFrYSBraXRhIHBlcmx1IG1lbnlpbXBhbm55YS4gU2F5YSBzYW5nYXQgbWVueWFyYW5rYW4gdW50dWsgdGlkYWsgbWVuZ2d1bmFrYW4gcGlsaWhhbiBgc2F2ZWAga2V0aWthIG1lbnV0dXAgUiBhdGF1IFJTdHVkaW8uDQoNCkRpIFIgYWRhIDIgKGR1YSkgZnVuZ3NpIHlhbmcgcGFsaW5nIGRpa2VuYWwgdW50dWsgbWVueWltcGFuIG9iamVrIGtlIGRhbGFtIGZpbGUgZWtzdGVybmFsLiBGaWxlIGVrc3Rlcm5hbCBpbmkgYnVrYW4gYmVydXBhIFwqLnR4dCwgXCouY3N2IGF0YXUgZm9ybWF0IG1lbnlpbXBhbiBkYXRhIHlhbmcgbGFpbi4gRmlsZSBpbmkgeWFuZyBuYW50aW55YSBha2FuIGRpcGFuZ2dpbCBvbGVoIFIgdW50dWsgZGlndW5ha2FuIGtlbWJhbGkuIE9iamVrIHlhbmcgZGlzaW1wYW4gaW5pIHRpZGFrIHRlcmJhdGFzIHBhZGEgb2JqZWsgYmVydXBhIG1vZGVsIHNhamEsIHRhcGkganVnYSBkYXRhLmZyYW1lIGRhbiBzZW11YSBvYmplayBwYWRhIHNlc3Npb24gUiB0ZXJzZWJ1dC4NCg0KIyBGdW5jdGlvbiBzYXZlKCkgYW5kIHNhdmVSRFMoKQ0KDQo8cCBhbGlnbj0iY2VudGVyIj4NCjxpbWcgc3JjPSJ0d2VldC5wbmciPg0KPC9wPg0KDQpbSGFkbGV5IFdpY2toYW1dKGh0dHBzOi8vdHdpdHRlci5jb20vaGFkbGV5d2lja2hhbS8pIHBlcm5haCBtZW1idWF0IHNlYnVhaCBjdWl0YW4gZGkgVHdpdHRlciB0ZW50YW5nIG1lbnlpbXBhbiBvYmplayBkaSBSIGRhbiBkaWEgbGViaWggbWVueWFyYW5rYW4gbWVuZ2d1bmFrYW4gZnVuZ3NpIGBzYXZlUkRTKClgIGRhbiBgcmVhZFJEUygpYC4NCg0KIyMgV2hhdCBpcyB0aGUgZGlmZmVyZW5jZSBvZiBzYXZlKCkgYW5kIHNhdmVSRFMoKT8NCg0KUGVyYmVkYWFuIHlhbmcgcGFsaW5nIG1lbmRhc2FyIGFudGFyYSBmdW5nc2kgYHNhdmUoKWAgZGFuIGBzYXZlUkRTKClgIGFkYWxhaA0KDQoqIGBzYXZlKClgIGRhcGF0IG1lbnlpbXBhbiBiYW55YWsgb2JqZWsgc2VrYWxpZ3VzIGRhbGFtIHNlYnVhaCBmaWxlIGVrc3Rlcm5hbC4NCiogYHNhdmVSRFMoKWAgaGFueWEgbWVueWltcGFuICoqc2F0dSBvYmplayBzYWphKiogZGFsYW0gc2VidWFoIGZpbGUgZWtzdGVybmFsLg0KDQpQZXJiZWRhYW4gbGFpbm55YSBhZGFsYWggYHNhdmUoKWAgYWthbiBzZWNhcmEgb3RvbWF0aXMgbWVuZ2dhbnRpIG9iamVrIHlhbmcgYWRhIHBhZGEgc2Vzc2lvIFIgYmVyamFsYW4gZGVuZ2FuIG9iamVrIHlhbmcgYWRhIHBhZGEgZmlsZSBla3N0ZXJuYWwgamlrYSBhZGEgbmFtYSBvYmplayB5YW5nIHNhbWEgdGFucGEgcGVtYmVyaXRhaHVhbi4gUmVzaWtvIGRpdGFuZ2d1bmcgcGVuZ2d1bmEhDQoNClRpZGFrIHNlcGVydGkgZnVuZ3NpIGBzYXZlKClgIHlhbmcgbWVueWltcGFuIG9iamVrIGRlbmdhbiBuYW1hbnlhLCBgc2F2ZVJEUygpYCBoYW55YSBtZW55aW1wYW4gc3RydWt0dXIgb2JqZWsgdGFucGEgbmFtYW55YS4gS2V0aWthIGtpdGEgaW5naW4gbWVtYW5nZ2lsIG9iamVrIHlhbmcgZGlzaW1wYW4gbWVuZ2d1bmFrYW4gZnVuZ3NpIGBzYXZlUkRTKClgIGhhcnVzIGRpbWFzdWtrYW4ga2Ugc2VidWFoIG9iamVrIGJhcnUuDQoNClBlcmJlZGFhbiBzZWxhbmp1dG55YSBraXRhIGJhaGFzIGRlbmdhbiBjb250b2guDQoNCiMjIHNhdmUoKQ0KDQpgYGB7cn0NCmlyaXNfZGYgPC0gaXJpcw0KZGltKGlyaXNfZGYpDQpzYXZlKGxpc3QgPSAiaXJpc19kZiIsIGZpbGUgPSAiaXJpc19kZi5yZGEiKQ0KYGBgDQoNCkRhdGEgYGlyaXNfZGZgIGF3YWxueWEgYmVyaXNpIDE1MCBiYXJpcyBkYXRhLiBTZWthcmFuZyBraXRhIGFrYW4ga3VyYW5naSBtZW5qYWRpIDEwMCBiYXJpcyBkYXRhIG1pc2FsbnlhLCBkZW5nYW4gbmFtYSB5YW5nIHNhbWEuDQoNCmBgYHtyfQ0KaXJpc19kZiA8LSBpcmlzX2RmWzE6MTAwLF0NCmRpbShpcmlzX2RmKQ0KYGBgDQoNCktpdGEgaW5nYXQgYmFod2EgZGF0YSBgaXJpc19kZmAgeWFuZyBraXRhIHNpbXBhbiBiZXJpc2kgMTUwIGJhcmlzIGRhbiBkYXRhIGBpcmlzX2RmYCBzYWF0IGluaSBiZXJpc2kgMTAwIGJhcmlzIGRhdGEuIFNla2FyYW5nIGtpdGEgcGFuZ2dpbCBsYWdpIGRhdGEgYGlyaXNfZGZgIHlhbmcgdGFkaSBkaXNpbXBhbiBkZW5nYW4gZnVuZ3NpIGBsb2FkKClgIGRhbiBwZXJoYXRpa2FuIGJhbnlha255YSBiYXJpcyBkaSBkYXRhIGBpcmlzX2RmYCBzZXRlbGFoIGRpcGFuZ2dpbCBpbmkuDQoNCmBgYHtyfQ0KbG9hZCgiaXJpc19kZi5yZGEiKQ0KZGltKGlyaXNfZGYpDQpgYGANCg0KTGloYXQ/IERhdGEgYGlyaXNfZGZgIHlhbmcgYWRhIGRpIHNlc3Npb24gc2ViZWx1bSBkaXBhbmdnaWwgc3VkYWggZGlnYW50aWthbiBkZW5nYW4gYGlyaXNfZGZgIHlhbmcgZGlwYW5nZ2lsIHRhZGkgbWVuamFkaSAxNTAgYmFyaXMgZGF0YSBrZW1iYWxpLg0KDQojIyBzYXZlUkRTKCkNCg0KQmFnYWltYW5hIGRlbmdhbiBmdW5nc2kgYHNhdmVSRFMoKWA/DQoNCmBgYHtyfQ0Kc2F2ZVJEUyhvYmplY3QgPSBpcmlzX2RmLCBmaWxlID0gImlyaXNfZGYucmRzIikNCmRpbShpcmlzX2RmKQ0KYGBgDQoNCkRhdGEgYGlyaXNfZGZgIG1hc2loIDE1MCBiYXJpcyBkYXRhLiBLaXRhIGt1cmFuZ2kgbGFnaSBtZW5qYWRpIDEwMCBiYXJpcyBkYXRhLg0KDQpgYGB7cn0NCmlyaXNfZGYgPC0gaXJpc19kZlsxOjEwMCxdDQpkaW0oaXJpc19kZikNCmBgYA0KDQpTZWxhbmp1dG55YSBraXRhIHBhbmdnaWwgaGFzaWwgcGVueWltcGFuYW4gdGFkaSBtZW5nZ3VuYWthbiBmdW5nc2kgYHJlYWRSRFMoKWAuIA0KDQpgYGB7cn0NCmlyaXNfZGZfbG9hZGVkIDwtIHJlYWRSRFMoImlyaXNfZGYucmRzIikNCmRpbShpcmlzX2RmX2xvYWRlZCkNCmRpbShpcmlzX2RmKQ0KYGBgDQoNCkRlbmdhbiBgc2F2ZVJEUygpYCB0aWRhayBha2FuIG1lbmdnYW50aSBvYmplayB5YW5nIHNhYXQgaW5pIGFkYSBkaSBzZXNzaW9uIFIgamlrYSBuYW1hIG9iamVrbnlhIGRpYmVkYWthbi4NCg0KIyBCdWlsZCBQcmV0cmFpbmVkIE1vZGVsDQoNCkl0dSB0YWRpIHBlbmdhbnRhciBkYXJpIHR1bGlzYW4gaW5pLiBTZWxhbmp1dG55YSBraXRhIGFrYW4gbWVtYnVhdCBzZWJ1YWggbW9kZWwgc2VkZXJoYW5hIG1lbmdndW5ha2FuIGZ1bmdzaSBgbG0oKWAgdW50dWsgbWVtYnVhdCBtb2RlbCByZWdyZXNpIGxpbmllci4gR3VuYWthbiBkYXRhIGBtdGNhcnNgIGRlbmdhbiBwZXViYWggYG1wZ2Agc2ViYWdhaSBwZXViYWggcmVzcG9uIGRhbiBwZXViYWggYGhwYCBkYW4gYHd0YCBzZWJhZ2FpIHBldWJhaCBwZW5qZWxhcyBhdGF1IHByZWRpa3Rvci4gU2ViZWx1bSBtZW1idWF0IG1vZGVsLCBraXRhIGJhZ2kgZGF0YSBzZWNhcmEgYWNhayB1bnR1ayBkYXRhIHRyYWluaW5nIGRhbiB0ZXN0aW5nLg0KDQpgYGB7cn0NCnNldC5zZWVkKDEwMDEpDQppZHggPC0gc2FtcGxlKDE6bnJvdyhtdGNhcnMpLCAwLjgqbnJvdyhtdGNhcnMpKQ0KdHJhaW5pbmcgPC0gbXRjYXJzW2lkeCwgXQ0KZGltKHRyYWluaW5nKQ0KdGVzdGluZyA8LSBtdGNhcnNbLWlkeCwgXQ0KZGltKHRlc3RpbmcpDQpgYGANCg0KDQpgYGB7cn0NCm1vZGVsLmxtIDwtIGxtKG1wZyB+IGhwICsgd3QsIGRhdGEgPSB0cmFpbmluZykNCnN1bW1hcnkobW9kZWwubG0pDQpBSUMobW9kZWwubG0pDQpgYGANCg0KQW5nZ2FwIHNhamEgaW5pIGFkYWxhaCBtb2RlbCB0ZXJiYWlrIHlhbmcga2l0YSBwZXJvbGVoIGRhbiBha2FuIGRpZ3VuYWthbiBkYWxhbSBtZW1wcmVkaWtzaS4gU2VsYW5qdXRueWEga2l0YSBzaW1wYW4gb2JqZWsgbW9kZWwgaW5pIGRhbGFtIGZpbGUgZWtzdGVybmFsLg0KDQpgYGB7cn0NCnNhdmVSRFMob2JqZWN0ID0gbW9kZWwubG0sIGZpbGUgPSAiYmVzdF9tb2RlbC5yZHMiKQ0KZmlsZS5leGlzdHMoImJlc3RfbW9kZWwucmRzIikNCmBgYA0KDQojIFVzZSBQcmV0cmFpbmVkIE1vZGVsDQoNCktpdGEgcGFuZ2dpbCBtb2RlbCB5YW5nIHN1ZGFoIGRpYnVhdCBkYXJpIGZpbGUgZWtzdGVybmFsIHRhZGkgdW50dWsgbWVsYWt1a2FuIHByZWRpa3NpLg0KDQpgYGB7cn0NCm15bW9kZWwgPC0gcmVhZFJEUygiYmVzdF9tb2RlbC5yZHMiKQ0KdGVzdGluZyRwcmVkaWN0ZWQgPC0gcHJlZGljdChteW1vZGVsLCBuZXdkYXRhID0gdGVzdGluZykNCmhlYWQodGVzdGluZykNCmBgYA0KDQpTZWJhZ2FpIHRhbWJhaGFuLCBtYXJpIGtpdGEgaGl0dW5nIFJNU0UgZGFyaSBoYXNpbCBwcmVkaWtzaSBpbmkuDQoNCmBgYHtyfQ0Kc3FydChtZWFuKCh0ZXN0aW5nJHByZWRpY3RlZCAtIHRlc3RpbmckbXBnKV4yKSkNCnBsb3QodGVzdGluZyRtcGcsIHRlc3RpbmckcHJlZGljdGVkLCBwY2ggPSAxOSkNCmBgYA0KDQojIENvbmNsdXNpb24NCg0KTWVueWltcGFuIG1vZGVsIGF0YXUgb2JqZWsgbGFpbiBkYXJpIFIga2UgZGFsYW0gZmlsZSBla3N0ZXJuYWwgZGFwYXQgbWVuZ2d1bmFrYW4gZnVuZ3NpIGBzYXZlUkRTKClgIGRhbiBgc2F2ZSgpYC4gU2ViYWlrbnlhIGd1bmFrYW4gYHNhdmVSRFMoKWAgZGFuIGByZWFkUkRTKClgIGppa2EgQW5kYSBoYW55YSBpbmdpbiBtZW55aW1wYW4gc2F0dSBvYmplayBzYWphIGRpIGRhbGFtIHNlYnVhaCBmaWxlIGVrc3Rlcm5hbCBkYW4gdGlkYWsgaW5naW4gYWRhIG9iamVrIHlhbmcgZGlnYW50aSB0YW5wYSBzZXBlbmdldGFodWFuIEFuZGEga2V0aWthIG1lbWFuZ2dpbCBrZW1iYWxpIG9iamVrIHRlcnNlYnV0LiBHVW5ha2FuIGBzYXZlKClgIGRhbiBgbG9hZCgpYCBqaWthIGJhbnlhayBvYmplayB5ZyBpbmdpbiBkaXNpbXBhbiBkZW5nYW4gbmFtYSB5YW5nIHNhbWEgZGFuIHRpZGFrIGFkYSBvYmplayBsYWluIHlhbmcgYWthbiBkaWdhbnRpLCBhdGF1IGtldGlrYSBBbmRhIHlha2luIHRpZGFrIGFkYSBtYXNhbGFoIGppa2EgYWRhIG9iamVrIGRpIHNlc3Npb24gUiBzYWF0IGluaSB5YW5nIGFrYW4gZGlnYW50aS4gR3VuYWthbiBzYWxhaCBzYXR1IGRhcmkgZnVuZ3NpIGluaSB1bnR1ayBtZW5nZ3VuYWthbiBtb2RlbCB5YW5nIHN1ZGFoIGRpYnVhdC4NCg==