Our Challenges
Trong post trước chúng ta đã làm quen với việc xây dựng một Recommender Engine từ khâu xử lí dữ liệu - chuẩn bị dữ liệu đến huấn luyện Recommender Engine. Là một case thuộc kiểu Toy Example nên bộ dữ liệu được lựa chọn một cách cố ý theo hướng đẹp và rất đầy đủ thông tin, nhất là ratings của những bộ phim. Đối với một hệ thống khuyến nghị thì ratings là một “nguyên liệu” không thể thiếu. Tuy nhiên thực tế thì không phải lúc nào chúng ta cũng có thông tin tối quan trọng này. Post này sẽ hướng dẫn xây dựng và huấn luyện một Recommender khi không có thông tin về ratings với ngôn ngữ R.
About Data Used
Dữ liệu sử dụng trong post này là E-Commerce Data về các giao dịch thương mại điện tử của một công ti ở Anh. Download cùng với mô tả về bộ dữ liệu này có thể lấy ở đây. Đọc bộ dữ liệu này rồi xem qua:
Table 1: Some Observations from raw data
536365 |
85123A |
WHITE HANGING HEART T-LIGHT HOLDER |
6 |
12/1/2010 8:26 |
2.55 |
17850 |
United Kingdom |
536365 |
71053 |
WHITE METAL LANTERN |
6 |
12/1/2010 8:26 |
3.39 |
17850 |
United Kingdom |
536365 |
84406B |
CREAM CUPID HEARTS COAT HANGER |
8 |
12/1/2010 8:26 |
2.75 |
17850 |
United Kingdom |
536365 |
84029G |
KNITTED UNION FLAG HOT WATER BOTTLE |
6 |
12/1/2010 8:26 |
3.39 |
17850 |
United Kingdom |
536365 |
84029E |
RED WOOLLY HOTTIE WHITE HEART. |
6 |
12/1/2010 8:26 |
3.39 |
17850 |
United Kingdom |
536365 |
22752 |
SET 7 BABUSHKA NESTING BOXES |
2 |
12/1/2010 8:26 |
7.65 |
17850 |
United Kingdom |
Các tên biến số là rất dễ hiểu. Ví dụ InvoiceNo là mã hóa đơn, StockCode là mã hàng hóa và là hàng hóa gì thì được miêu tả ở Description còn CustomerID là mã khách hàng.
Recommender Engine without Ratings
Trong tình huống không có thông tin về ratings của các items chúng ta có thể xây dựng Recommender Engine từ binary matrix - là kiểu ma trận dạng như sau:
## item1 item2 item3 item4 item5
## user1 0 1 1 0 0
## user2 1 0 0 1 1
## user3 0 0 0 0 1
## user4 1 1 1 1 1
## user5 1 1 1 0 0
Trước hết chúng ta convert dữ liệu nguyên thủy ban đầu về binary data frame như sau:
Table 2: Some Observations from binary data (Option 2)
12346 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
12347 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
12348 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
12349 |
0 |
0 |
0 |
0 |
1 |
1 |
0 |
0 |
0 |
0 |
12350 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
12352 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
12353 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
12354 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
12355 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
12356 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
Dòng 4 của Table 2 chỉ ra rằng khách hàng có mã CustomerID là 12349 mua các item có mã 22555 và 23108 - ứng với giá trị là 1 của binary data frame. Những item mà khách hàng này không mua có giá trị là 0.
Data Preparation for Recommender Engine
Đến đây cần convert binary data frame đã chuẩn bị ở trên về binary matrix - là cấu trúc dữ liệu đòi hỏi cho việc training các Recommender Engines của thư viện recommenderlab của Michael Hahsler như sau:
Binary matrix cho một số quan sát (Figure 1):

Dữ liệu là một ma trận thưa kiểu nhị phân (Binary Sparse Matrix) như chúng ta đã biết. Đây là thực tế phổ biến khi xây dựng các hệ thống khuyến nghị: hầu hét các cell của ma trận có giá trị là zero (thực chất là NA - Not Available, một vấn đề được gọi là Problem of Data Sparsity cho lớp bài toán này) dẫn đến mức độ che phủ dữ liệu của ma trận rất thấp (hầu hết là dưới 10%, mức phổ biến là 5%-6%). Với dữ liệu đã được chuyển về ma trận thưa, sử dụng 80% dữ liệu để huấn luyện Recommender Engine và 20% để test:
Vì dữ liệu đầu vào là binary sparse matrix nên thước đo tương đồng (similarity measure) theo Jaccard sẽ phù hợp hơn. R codes dưới đây thực hiện huấn luyện Recommender Engine trên train data theo Jaccard Similarity sử dụng Item-base Approach (có thể mất nhiều thời gian để train Engine):
Với Engine đã có chúng ta có thể sử dụng để khuyến nghị, ví dụ, 5 items cho mỗi một user/customer:
Viết hàm có tên item_recommended_user()
trả về các items dưới dạng StockCode được khuyến nghị cho user/customer:
Sử dụng hàm trên để extract ra các items được khuyến nghị cho tất cả các users/customers:
Join với dữ liệu về miêu tả cho items theo StockCode:
Show các items khuyến nghị cho, ví dụ, customer thứ nhất:
Table 3: Some Items Recommended for CustomerID = 12347
12347 |
20724 |
RED RETROSPOT CHARLOTTE BAG |
12347 |
20723 |
STRAWBERRY CHARLOTTE BAG |
12347 |
23204 |
CHARLOTTE BAG APPLES DESIGN |
12347 |
22355 |
CHARLOTTE BAG SUKI DESIGN |
12347 |
22730 |
ALARM CLOCK BAKELIKE IVORY |
LS0tDQp0aXRsZTogJ1JlY29tbWVuZGF0aW9uIFN5c3RlbSAoUGFydCAyKScNCmF1dGhvcjogJ0F1dGhvcjogTmd1eWVuIENoaSBEdW5nJw0Kc3VidGl0bGU6ICJSIE1hY2hpbmUgTGVhcm5pbmcgU2VyaWVzIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgIyBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBoaWdobGlnaHQ6IHplbmJ1cm4NCiAgICAjIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJmbGF0bHkiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsIGZpZy53aWR0aCA9IDEwLCBmaWcuaGVpZ2h0ID0gNikNCmBgYA0KDQoNCiMgT3VyIENoYWxsZW5nZXMNCg0KVHJvbmcgW3Bvc3QgdHLGsOG7m2NdKGh0dHBzOi8vcnB1YnMuY29tL2NoaWR1bmdrdC82MzQzMDApIGNow7puZyB0YSDEkcOjIGzDoG0gcXVlbiB24bubaSB2aeG7h2MgeMOieSBk4buxbmcgbeG7mXQgUmVjb21tZW5kZXIgRW5naW5lIHThu6sga2jDonUgeOG7rSBsw60gZOG7ryBsaeG7h3UgLSBjaHXhuqluIGLhu4sgZOG7ryBsaeG7h3UgxJHhur9uIGh14bqlbiBsdXnhu4duIFJlY29tbWVuZGVyIEVuZ2luZS4gTMOgIG3hu5l0IGNhc2UgdGh14buZYyBraeG7g3UgVG95IEV4YW1wbGUgbsOqbiBi4buZIGThu68gbGnhu4d1IMSRxrDhu6NjIGzhu7FhIGNo4buNbiBt4buZdCBjw6FjaCBj4buRIMO9IHRoZW8gaMaw4bubbmcgxJHhurlwIHbDoCBy4bqldCDEkeG6p3kgxJHhu6cgdGjDtG5nIHRpbiwgbmjhuqV0IGzDoCByYXRpbmdzIGPhu6dhIG5o4buvbmcgYuG7mSBwaGltLiDEkOG7kWkgduG7m2kgbeG7mXQgaOG7hyB0aOG7kW5nIGtodXnhur9uIG5naOG7iyB0aMOsIHJhdGluZ3MgbMOgIG3hu5l0ICJuZ3V5w6puIGxp4buHdSIga2jDtG5nIHRo4buDIHRoaeG6v3UuIFR1eSBuaGnDqm4gdGjhu7FjIHThur8gdGjDrCBraMO0bmcgcGjhuqNpIGzDumMgbsOgbyBjaMO6bmcgdGEgY8WpbmcgY8OzIHRow7RuZyB0aW4gdOG7kWkgcXVhbiB0cuG7jW5nIG7DoHkuIFBvc3QgbsOgeSBz4bq9IGjGsOG7m25nIGThuqtuIHjDonkgZOG7sW5nIHbDoCBodeG6pW4gbHV54buHbiBt4buZdCBSZWNvbW1lbmRlciBraGkga2jDtG5nIGPDsyB0aMO0bmcgdGluIHbhu4EgcmF0aW5ncyB24bubaSBuZ8O0biBuZ+G7ryBSLiANCg0KIyBBYm91dCBEYXRhIFVzZWQNCg0KROG7ryBsaeG7h3Ugc+G7rSBk4bulbmcgdHJvbmcgcG9zdCBuw6B5IGzDoCAqKkUtQ29tbWVyY2UgRGF0YSoqIHbhu4EgY8OhYyBnaWFvIGThu4tjaCB0aMawxqFuZyBt4bqhaSDEkWnhu4duIHThu60gY+G7p2EgbeG7mXQgY8O0bmcgdGkg4bufIEFuaC4gRG93bmxvYWQgY8O5bmcgduG7m2kgbcO0IHThuqMgduG7gSBi4buZIGThu68gbGnhu4d1IG7DoHkgY8OzIHRo4buDIGzhuqV5IFvhu58gxJHDonldKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vY2FycmllMS9lY29tbWVyY2UtZGF0YSkuIMSQ4buNYyBi4buZIGThu68gbGnhu4d1IG7DoHkgcuG7k2kgeGVtIHF1YTogDQoNCmBgYHtyfQ0KIyBDbGVhciBSIEVudmlyb25tZW50OiANCg0Kcm0obGlzdCA9IGxzKCkpDQoNCmxpYnJhcnkodGlkeXZlcnNlKSAjIEZvciBkYXRhIHdyYW5nbGluZyBhbmQgdmlzdWFsaXphdGlvbi4gDQoNCiMgTG9hZCBkYXRhOiANCg0KZGZfdHJhbnNhY3Rpb25zIDwtIHJlYWRfY3N2KCJkYXRhLmNzdiIpDQoNCiMgU29tZSBvYnNlcnZhdGlvbnM6IA0KDQpsaWJyYXJ5KGtuaXRyKSAjIEZvciBjcmVhdGluZyB0YWJsZSBpbiBIVE1MIGZvcm0uIA0KDQpkZl90cmFuc2FjdGlvbnMgJT4lIA0KICBoZWFkKCkgJT4lIA0KICBrYWJsZShjYXB0aW9uID0gIlRhYmxlIDE6IFNvbWUgT2JzZXJ2YXRpb25zIGZyb20gcmF3IGRhdGEiKQ0KYGBgDQoNCkPDoWMgdMOqbiBiaeG6v24gc+G7kSBsw6AgcuG6pXQgZOG7hSBoaeG7g3UuIFbDrSBk4bulICoqSW52b2ljZU5vKiogbMOgIG3DoyBow7NhIMSRxqFuLCAqKlN0b2NrQ29kZSoqIGzDoCBtw6MgaMOgbmcgaMOzYSB2w6AgbMOgIGjDoG5nIGjDs2EgZ8OsIHRow6wgxJHGsOG7o2MgbWnDqnUgdOG6oyDhu58gKipEZXNjcmlwdGlvbioqIGPDsm4gKipDdXN0b21lcklEKiogbMOgIG3DoyBraMOhY2ggaMOgbmcuDQoNCiMgUmVjb21tZW5kZXIgRW5naW5lIHdpdGhvdXQgUmF0aW5ncw0KDQpUcm9uZyB0w6xuaCBodeG7kW5nIGtow7RuZyBjw7MgdGjDtG5nIHRpbiB24buBIHJhdGluZ3MgY+G7p2EgY8OhYyBpdGVtcyBjaMO6bmcgdGEgY8OzIHRo4buDIHjDonkgZOG7sW5nIFJlY29tbWVuZGVyIEVuZ2luZSB04burIGJpbmFyeSBtYXRyaXggLSBsw6Aga2nhu4N1IG1hIHRy4bqtbiBk4bqhbmcgbmjGsCBzYXU6IA0KDQoNCmBgYHtyLCBlY2hvPUZBTFNFfQ0KDQpmYWtlX2JpbiA8LSBkYXRhLmZyYW1lKGl0ZW0xID0gc2FtcGxlKHggPSAwOjEsIHNpemUgPSA1LCByZXBsYWNlID0gVFJVRSksIA0KICAgICAgICAgICAgICAgICAgICAgICBpdGVtMiA9IHNhbXBsZSh4ID0gMDoxLCBzaXplID0gNSwgcmVwbGFjZSA9IFRSVUUpLCANCiAgICAgICAgICAgICAgICAgICAgICAgaXRlbTMgPSBzYW1wbGUoeCA9IDA6MSwgc2l6ZSA9IDUsIHJlcGxhY2UgPSBUUlVFKSwgDQogICAgICAgICAgICAgICAgICAgICAgIGl0ZW00ID0gc2FtcGxlKHggPSAwOjEsIHNpemUgPSA1LCByZXBsYWNlID0gVFJVRSksIA0KICAgICAgICAgICAgICAgICAgICAgICBpdGVtNSA9IHNhbXBsZSh4ID0gMDoxLCBzaXplID0gNSwgcmVwbGFjZSA9IFRSVUUpKQ0KDQpyb3cubmFtZXMoZmFrZV9iaW4pIDwtIHBhc3RlMCgidXNlciIsIDE6NSkNCmZha2VfYmluDQoNCmBgYA0KDQoNClRyxrDhu5tjIGjhur90IGNow7puZyB0YSBjb252ZXJ0IGThu68gbGnhu4d1IG5ndXnDqm4gdGjhu6d5IGJhbiDEkeG6p3UgduG7gSBiaW5hcnkgZGF0YSBmcmFtZSBuaMawIHNhdTogDQoNCmBgYHtyfQ0KIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KIyAgT3B0aW9uIDE6IFVzZSBzcHJlYWQoKSBmdW5jdGlvbg0KIy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQ0KDQojIFJldm9tZSBkdXBsaWNhdGlvbnMgYnkgQ3VzdG9tZXJJRDogDQoNCmRmX3RyYW5zYWN0aW9ucyAlPiUgDQogIGdyb3VwX2J5KEN1c3RvbWVySUQpICU+JSANCiAgZmlsdGVyKCFkdXBsaWNhdGVkKFN0b2NrQ29kZSkpICU+JSANCiAgdW5ncm91cCgpIC0+IGRmX3RyYW5zYWN0aW9ucw0KDQojIENPbnZlcnQgdG8gdXNlci1pdGVtIGRhdGEgZnJhbWUgKGhhcyAxIHJvdyBhbmQgbiBjb2x1bW5zIGZvciBhIHNwZWNpZmljIEN1c3RvbWVySUQpOiANCg0KZGZfdHJhbnNhY3Rpb25zICU+JSANCiAgc2VsZWN0KEN1c3RvbWVySUQsIFN0b2NrQ29kZSkgJT4lIA0KICBtdXRhdGUoaXRlbV92YWx1ZSA9IDEpICU+JSANCiAgc3ByZWFkKHZhbHVlID0gaXRlbV92YWx1ZSwga2V5ID0gU3RvY2tDb2RlLCBmaWxsID0gMCkgJT4lIA0KICBtdXRhdGUoQ3VzdG9tZXJJRCA9IGFzLmNoYXJhY3RlcihDdXN0b21lcklEKSkgLT4gZGZfYmluYXJ5DQoNCiMgU2hvdyBzb21lIG9ic2VydmF0aW9uczogDQoNCnNldC5zZWVkKDI5KQ0KDQpkZl9iaW5hcnkgJT4lIA0KICBzbGljZSgxOjEwKSAlPiUgDQogIHNlbGVjdChjKDEsIHNhbXBsZSh4ID0gMTpuY29sKGRmX2JpbmFyeSkgLSAxLCBzaXplID0gMTAsIHJlcGxhY2UgPSBGQUxTRSkpKSAlPiUgDQogIGthYmxlKGNhcHRpb24gPSAiVGFibGUgMjogU29tZSBPYnNlcnZhdGlvbnMgZnJvbSBiaW5hcnkgZGF0YSAoT3B0aW9uIDIpIikNCmBgYA0KDQpEw7JuZyA0IGPhu6dhIFRhYmxlIDIgY2jhu4kgcmEgcuG6sW5nIGtow6FjaCBow6BuZyBjw7MgbcOjIEN1c3RvbWVySUQgbMOgIDEyMzQ5IG11YSBjw6FjIGl0ZW0gY8OzIG3DoyAyMjU1NSB2w6AgMjMxMDggLSDhu6luZyB24bubaSBnacOhIHRy4buLIGzDoCAxIGPhu6dhIGJpbmFyeSBkYXRhIGZyYW1lLiBOaOG7r25nIGl0ZW0gbcOgIGtow6FjaCBow6BuZyBuw6B5IGtow7RuZyBtdWEgY8OzIGdpw6EgdHLhu4sgbMOgIDAuIA0KDQojIERhdGEgUHJlcGFyYXRpb24gZm9yIFJlY29tbWVuZGVyIEVuZ2luZQ0KDQrEkOG6v24gxJHDonkgY+G6p24gY29udmVydCBiaW5hcnkgZGF0YSBmcmFtZSDEkcOjIGNodeG6qW4gYuG7iyDhu58gdHLDqm4gduG7gSBiaW5hcnkgbWF0cml4IC0gbMOgIGPhuqV1IHRyw7pjIGThu68gbGnhu4d1IMSRw7JpIGjhu49pIGNobyB2aeG7h2MgdHJhaW5pbmcgY8OhYyBSZWNvbW1lbmRlciBFbmdpbmVzIGPhu6dhIHRoxrAgdmnhu4duIFtyZWNvbW1lbmRlcmxhYl0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3JlY29tbWVuZGVybGFiL2luZGV4Lmh0bWwpIGPhu6dhIFtNaWNoYWVsIEhhaHNsZXJdKGh0dHBzOi8vczIuc211LmVkdS9JREEvcmVjb21tZW5kZXJsYWIvKSBuaMawIHNhdTogDQoNCmBgYHtyfQ0KIyBTZXQgQ3VzdG9tZXJJRDogDQoNCkN1c3RvbWVySUQgPC0gZGZfYmluYXJ5JEN1c3RvbWVySUQgDQoNCiMgQ29udmVydCB0byBiaW5hcnkgbWF0cml4OiANCg0KbWF0cml4X3dpZGUgPC0gYXMubWF0cml4KGRmX2JpbmFyeSAlPiUgc2VsZWN0KC1DdXN0b21lcklEKSkNCg0KIyBTZXQgcm93IG5hbWVzOiANCg0Kcm93bmFtZXMobWF0cml4X3dpZGUpIDwtIEN1c3RvbWVySUQgDQoNCiMgQ29udmVydCB0byBiaW5hcnlSYXRpbmdNYXRyaXggZm9yIHRyYWluaW5nIHJlY29tbWVuZGVyIGVuZ2luZXM6IA0KDQpsaWJyYXJ5KHJlY29tbWVuZGVybGFiKQ0KcmF0aW5nbWF0IDwtIGFzKG1hdHJpeF93aWRlLCAiYmluYXJ5UmF0aW5nTWF0cml4IikNCmBgYA0KDQpCaW5hcnkgbWF0cml4IGNobyBt4buZdCBz4buRIHF1YW4gc8OhdCAoRmlndXJlIDEpOiANCg0KYGBge3J9DQppbWFnZShyYXRpbmdtYXRbMToyMDAsIDE6MjAwXSwgbWFpbiA9ICJGaWd1cmUgMTogQmluYXJ5IHJhdGluZyBtYXRyaXgiKQ0KYGBgDQoNCkThu68gbGnhu4d1IGzDoCBt4buZdCBtYSB0cuG6rW4gdGjGsGEga2nhu4N1IG5o4buLIHBow6JuIChCaW5hcnkgU3BhcnNlIE1hdHJpeCkgbmjGsCBjaMO6bmcgdGEgxJHDoyBiaeG6v3QuIMSQw6J5IGzDoCB0aOG7sWMgdOG6vyBwaOG7lSBiaeG6v24ga2hpIHjDonkgZOG7sW5nIGPDoWMgaOG7hyB0aOG7kW5nIGtodXnhur9uIG5naOG7izogaOG6p3UgaMOpdCBjw6FjIGNlbGwgY+G7p2EgbWEgdHLhuq1uIGPDsyBnacOhIHRy4buLIGzDoCB6ZXJvICh0aOG7sWMgY2jhuqV0IGzDoCBOQSAtIE5vdCBBdmFpbGFibGUsIG3hu5l0IHbhuqVuIMSR4buBIMSRxrDhu6NjIGfhu41pIGzDoCBQcm9ibGVtIG9mIERhdGEgU3BhcnNpdHkgY2hvIGzhu5twIGLDoGkgdG/DoW4gbsOgeSkgZOG6q24gxJHhur9uIG3hu6ljIMSR4buZIGNoZSBwaOG7pyBk4buvIGxp4buHdSBj4bunYSBtYSB0cuG6rW4gcuG6pXQgdGjhuqVwICho4bqndSBo4bq/dCBsw6AgZMaw4bubaSAxMCUsIG3hu6ljIHBo4buVIGJp4bq/biBsw6AgNSUtNiUpLiBW4bubaSBk4buvIGxp4buHdSDEkcOjIMSRxrDhu6NjIGNodXnhu4NuIHbhu4EgbWEgdHLhuq1uIHRoxrBhLCBz4butIGThu6VuZyA4MCUgZOG7ryBsaeG7h3UgxJHhu4MgaHXhuqVuIGx1eeG7h24gUmVjb21tZW5kZXIgRW5naW5lIHbDoCAyMCUgxJHhu4MgdGVzdDogIA0KDQpgYGB7cn0NCnNldC5zZWVkKDEpDQppZCA8LSBzYW1wbGUoeCA9IDE6bnJvdyhyYXRpbmdtYXQpLCBzaXplID0gMC44Km5yb3cocmF0aW5nbWF0KSwgcmVwbGFjZSA9IEZBTFNFKQ0KZGF0YV90cmFpbiA8LSByYXRpbmdtYXRbaWQsIF0NCmRhdGFfdGVzdCA8LSByYXRpbmdtYXRbLWlkLCBdDQpgYGANCg0KVsOsIGThu68gbGnhu4d1IMSR4bqndSB2w6BvIGzDoCBiaW5hcnkgc3BhcnNlIG1hdHJpeCBuw6puIHRoxrDhu5tjIMSRbyB0xrDGoW5nIMSR4buTbmcgKHNpbWlsYXJpdHkgbWVhc3VyZSkgdGhlbyBbSmFjY2FyZF0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvSmFjY2FyZF9pbmRleCkgc+G6vSBwaMO5IGjhu6NwIGjGoW4uIFIgY29kZXMgZMaw4bubaSDEkcOieSB0aOG7sWMgaGnhu4duIGh14bqlbiBsdXnhu4duIFJlY29tbWVuZGVyIEVuZ2luZSB0csOqbiB0cmFpbiBkYXRhIHRoZW8gSmFjY2FyZCBTaW1pbGFyaXR5IHPhu60gZOG7pW5nIEl0ZW0tYmFzZSBBcHByb2FjaCAoY8OzIHRo4buDIG3huqV0IG5oaeG7gXUgdGjhu51pIGdpYW4gxJHhu4MgdHJhaW4gRW5naW5lKTogDQoNCmBgYHtyfQ0KcmVjY19tb2RlbCA8LSBSZWNvbW1lbmRlcihkYXRhID0gZGF0YV90cmFpbiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gIklCQ0YiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbWV0ZXIgPSBsaXN0KG1ldGhvZCA9ICJKYWNjYXJkIikpDQpgYGANCg0KVuG7m2kgRW5naW5lIMSRw6MgY8OzIGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgxJHhu4Mga2h1eeG6v24gbmdo4buLLCB2w60gZOG7pSwgNSBpdGVtcyBjaG8gbeG7l2kgbeG7mXQgdXNlci9jdXN0b21lcjogDQoNCmBgYHtyfQ0Kbl9yZWNvbW1lbmRlZCA8LSA1DQpyZWNjX3ByZWRpY3RlZCA8LSBwcmVkaWN0KG9iamVjdCA9IHJlY2NfbW9kZWwsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICBuZXdkYXRhID0gZGF0YV90ZXN0LA0KICAgICAgICAgICAgICAgICAgICAgICAgICBuID0gbl9yZWNvbW1lbmRlZCkNCmBgYA0KDQpWaeG6v3QgaMOgbSBjw7MgdMOqbiBgaXRlbV9yZWNvbW1lbmRlZF91c2VyKClgIHRy4bqjIHbhu4EgY8OhYyBpdGVtcyBkxrDhu5tpIGThuqFuZyBTdG9ja0NvZGUgxJHGsOG7o2Mga2h1eeG6v24gbmdo4buLIGNobyB1c2VyL2N1c3RvbWVyOiANCg0KYGBge3J9DQppdGVtX3JlY29tbWVuZGVkX3VzZXIgPC0gZnVuY3Rpb24oaSkgew0KICByZWNjX3ByZWRpY3RlZEBpdGVtTGFiZWxzW3JlY2NfcHJlZGljdGVkQGl0ZW1zW1tpXV1dIC0+IFN0b2NrQ29kZQ0KICBDdXN0b21lcklEIDwtIHJlcChyZWNjX3ByZWRpY3RlZEBpdGVtc1tpXSAlPiUgbmFtZXMoKSwgbGVuZ3RoKFN0b2NrQ29kZSkpDQogIGRmX3Jlc3VsdCA8LSB0aWJibGUoQ3VzdG9tZXJJRCA9IEN1c3RvbWVySUQsIFN0b2NrQ29kZSA9IFN0b2NrQ29kZSkNCiAgcmV0dXJuKGRmX3Jlc3VsdCkNCiAgfQ0KYGBgDQoNClPhu60gZOG7pW5nIGjDoG0gdHLDqm4gxJHhu4MgZXh0cmFjdCByYSBjw6FjIGl0ZW1zIMSRxrDhu6NjIGtodXnhur9uIG5naOG7iyBjaG8gdOG6pXQgY+G6oyBjw6FjIHVzZXJzL2N1c3RvbWVyczogDQoNCmBgYHtyfQ0KbGFwcGx5KDE6bnJvdyhkYXRhX3Rlc3QpLCBpdGVtX3JlY29tbWVuZGVkX3VzZXIpIC0+IGxpc3RfaXRlbXNSZWNvbW1lbmRlZF91c2Vycw0KZG8uY2FsbCgiYmluZF9yb3dzIiwgbGlzdF9pdGVtc1JlY29tbWVuZGVkX3VzZXJzKSAtPiBkZl9pdGVtc1JlY29tbWVuZGVkX3VzZXJzDQpgYGANCg0KSm9pbiB24bubaSBk4buvIGxp4buHdSB24buBIG1pw6p1IHThuqMgY2hvIGl0ZW1zIHRoZW8gU3RvY2tDb2RlOiANCg0KYGBge3J9DQojIERlc2NyaXB0aW9ucyBhYm91dCBpdGVtczogDQpkZl90cmFuc2FjdGlvbnMgJT4lIA0KICBmaWx0ZXIoIWR1cGxpY2F0ZWQoU3RvY2tDb2RlKSkgJT4lIA0KICBzZWxlY3QoU3RvY2tDb2RlLCBEZXNjcmlwdGlvbikgLT4gZGZfZGVzY3JpcHRpb25zDQoNCiMgSm9pbiBkYXRhIHNldHM6IA0KZnVsbF9qb2luKGRmX2l0ZW1zUmVjb21tZW5kZWRfdXNlcnMsIGRmX2Rlc2NyaXB0aW9ucywgYnkgPSAiU3RvY2tDb2RlIikgLT4gZGZfcmVjb21tZW5kZWRfZm9yX3VzZXJzDQpgYGANCg0KU2hvdyBjw6FjIGl0ZW1zIGtodXnhur9uIG5naOG7iyBjaG8sIHbDrSBk4bulLCBjdXN0b21lciB0aOG7qSBuaOG6pXQ6IA0KDQpgYGB7cn0NCmRmX3JlY29tbWVuZGVkX2Zvcl91c2VycyAlPiUgDQogIGZpbHRlcihDdXN0b21lcklEID09IHJlY2NfcHJlZGljdGVkQGl0ZW1zWzFdICU+JSBuYW1lcygpKSAlPiUgDQogIGthYmxlKGNhcHRpb24gPSAiVGFibGUgMzogU29tZSBJdGVtcyBSZWNvbW1lbmRlZCBmb3IgQ3VzdG9tZXJJRCA9IDEyMzQ3IikNCmBgYA0KDQojIEFwcHJvYWNoIHRvIEV2YWx1YXRpbmcgUmVjb21tZW5kZXIgUGVyZm9ybWFuY2UNCg0KTeG6t2MgZMO5IHRoxrAgdmnhu4duIHJlY29tbWVuZGVybGFiIGPDsyBjw6FjIGjDoG0gxJHhu4MgxJHDoW5oIGdpw6EgaGnhu4d1IHF14bqjIGPhu6dhIFJlY29tbWVuZGVyIEVuZ2luZSB2w6AgxJHDoyDEkcaw4bujYyBTdXJlc2ggSy4gR29yYWthbGEgc+G7rSBk4bulbmcgY2hvIG3hu5l0IGNhc2Ugc3R1ZHkgdHJvbmcgdGV4dGJvb2sgW0J1aWxkaW5nIGEgUmVjb21tZW5kYXRpb24gU3lzdGVtIHdpdGggUl0oaHR0cHM6Ly93d3cuYW1hem9uLmNvbS9CdWlsZGluZy1SZWNvbW1lbmRhdGlvbi1TeXN0ZW0tU3VyZXNoLUdvcmFrYWxhL2RwLzE3ODM1NTQ0OTUvcmVmPXNyXzFfMT9kY2hpbGQ9MSZrZXl3b3Jkcz1CdWlsZGluZythK1JlY29tbWVuZGF0aW9uK1N5c3RlbSt3aXRoK1ImcWlkPTE1OTQ2NTExMjkmc3I9OC0xKSBuaMawbmcgw6FwIGThu6VuZyBuZ3V5w6puIG3DoCBraMO0bmcgY8OzIGhp4buHdSBjaOG7iW5oIGfDrCB0aMOsIHPhur0ga2jDtG5nIGPDsyBuaGnhu4F1IMO9IG5naMSpYSB2w6wgbeG7mXQgdsOgaSBsw60gZG8gxJHGsOG7o2MgdHLDrG5oIGLDoHkgbmdheSBzYXUgxJHDonkuIFRyxrDhu5tjIGjhur90IG5o4bqvYyBs4bqhaSBy4bqxbmcga2hv4bqjbmcgdGjhu51pIGdpYW4gbcOgIGNow7puZyB0YSBxdWFuIHPDoXQgaMOgbmggdmkgY+G7p2EgY8OhYyBjdXN0b21lcnMgbMOgIHThu6sgMjAxMC0xMi0wMSAwODoyNjowMCDEkeG6v24gMjAxMS0xMi0wOSAxMjo1MDowMDogDQoNCmBgYHtyfQ0KbHVicmlkYXRlOjptZHlfaG0oZGZfdHJhbnNhY3Rpb25zJEludm9pY2VEYXRlKSAlPiUgcmFuZ2UoKQ0KYGBgDQoNClRyb25nIGtoaSDEkcOzLCBuaOG7r25nIGl0ZW1zIMSRxrDhu6NjIGtodXnhur9uIG5naOG7iyBjaG8gY8OhYyBjdXN0b21lcnMgaG/DoG4gdG/DoG4gZOG7sWEgdHLDqm4gZGF0YSB24buBIGjDoG5oIHZpIGPhu6dhIGjhu40gdHJvbmcga2hv4bqjbmcgdGjhu51pIGdpYW4gbsOgeS4gTmdoxKlhIGzDoCBjw6FjIGl0ZW1zIMSRxrDhu6NjIEVuZ2luZSBraHV54bq/biBuZ2jhu4sgaG/DoG4gdG/DoG4gZOG7sWEgdsOgbyBuaOG7r25nIHPhu7EgZ2lhbyBk4buLY2gvdMawxqFuZyB0w6FjICoqxJDDgyBY4bqoWSBSQSoqIHRyb25nIGtoaSB0aOG7sWMgdOG6vyBo4buNIG11YSBz4bqvbSBuaOG7r25nIGl0ZW1zIG7DoG8gdGjDrCBjaMO6bmcgdGEgxJHDoyBiaeG6v3QuIA0KDQpEbyB24bqteSwgY8OhY2ggdGnhur9wIGPhuq1uIMSR4buDIMSRw6FuaCBnacOhIGNo4bqldCBsxrDhu6NuZyBj4bunYSBSZWNvbW1lbmRlciBuaMawIHNhdSBjw7MgdGjhu4Mgc+G6vSBwaMO5IGjhu6NwIGjGoW46IHNvIHPDoW5oIGPDoWMgaXRlbXMgxJHGsOG7o2Mga2h1eeG6v24gbmdo4buLIGNobyBjdXN0b21lcnMgduG7m2kgY8OhYyBpdGVtcyBtw6AgaOG7jSAqKlPhurwgTVVBKiogc2F1IHRo4budaSDEkWnhu4NtIDIwMTEtMTItMDkgMTI6NTA6MDAuIMSQw6J5IGzDoCBjw6FjaCB0aeG6v3AgY+G6rW4gaOG7o3AgbMOtIGjGoW4uIFTDrG5oIGh14buRbmcgbsOgeSBjxaluZyBnaeG7kW5nIG5oxrAgY2jDum5nIHRhIHjDonkgZOG7sW5nIG3hu5l0IG3DtCBow6xuaCBBUiBk4buxIGLDoW8gZ2nDoSBj4buVIHBoaeG6v3UgdOG6oWkgdGjhu51pIMSRaeG7g20gdCArIDEgKHThu6ljIGzDoCBnacOhIGPhu6dhIDEgbmfDoHkgbuG7r2EpIGNo4buJIGPEg24gY+G7qSB2w6BvIGdpw6EgY+G7p2EgaGnhu4duIHThuqFpICh0aOG7nWkgxJFp4buDbSB0KSB2w6AgY8OhYyBiaeG6v24gdHLhu4UgY+G7p2EgdCAodGjDtG5nIHRpbiB0cm9uZyBxdcOhIGto4bupKS4gxJDhu4MgYmnhur90IG3hu6ljIMSR4buZIGNow61uaCB4w6FjIGPhu6dhIGThu7EgYsOhbyB0aMOsIGPDoWNoIHRp4bq/cCBj4bqtbiBsw6AsIHbDrSBk4bulLCBzbyBzw6FuaCBnacOhIHRo4buxYyB04bq/IGPhu6dhIDEgbmfDoHkgc+G6r3AgdOG7m2kgduG7m2kgZ2nDoSDEkcaw4bujYyBk4buxIGLDoW8gdOG7qyBtw7QgaMOsbmggQVIuIEjGsOG7m25nIHRp4bq/cCBj4bqtbiBuw6B5IHPhur0gxJHGsOG7o2MgdHLDrG5oIGLDoHkgY2hpIHRp4bq/dCB0cm9uZyBwaOG6p24ga+G6vyB0aeG6v3AgY+G7p2Egc2VyaWVzIHbhu4EgUmVjb21tZW5kYXRpb24gU3lzdGVtLiANCg0KIyBSZWZlcmVuY2VzDQoNCg0KMS4gW1JlY29tbWVuZGVyIFN5c3RlbXM6IEFuIEludHJvZHVjdGlvbl0oaHR0cHM6Ly93d3cuYW1hem9uLmNvbS9SZWNvbW1lbmRlci1TeXN0ZW1zLUludHJvZHVjdGlvbi1EaWV0bWFyLUphbm5hY2gvZHAvMDUyMTQ5MzM2Ni9yZWY9cGRfc2JzXzE0Xzc/X2VuY29kaW5nPVVURjgmcGRfcmRfaT0wNTIxNDkzMzY2JnBkX3JkX3I9YzgxZWFjYTctMzUzYS00MzY3LWE5YjEtZmE1ZTc3ZDg0MjJjJnBkX3JkX3c9R3kzT1YmcGRfcmRfd2c9eUJ5V1cmcGZfcmRfcD1iYzA3NDA1MS04MWQxLTQ4NzQtYTNmZC1mZDBjODY3Y2UzYjQmcGZfcmRfcj1TMlJLVDlWV1BYTllDWTlRUEVNVyZwc2M9MSZyZWZSSUQ9UzJSS1Q5VldQWE5ZQ1k5UVBFTVcpLg0KMi4gW0V2YWx1YXRpbmcgQ29sbGFib3JhdGl2ZSBGaWx0ZXJpbmcgUmVjb21tZW5kZXIgU3lzdGVtc10oaHR0cHM6Ly9ncm91cGxlbnMub3JnL3NpdGUtY29udGVudC91cGxvYWRzL2V2YWx1YXRpbmctVE9JUy0yMDA0MS5wZGYpLg0KMy4gW3JlY29tbWVuZGVybGFiOiBBIEZyYW1ld29yayBmb3IgRGV2ZWxvcGluZyBhbmQgVGVzdGluZyBSZWNvbW1lbmRhdGlvbiBBbGdvcml0aG1zXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvcmVjb21tZW5kZXJsYWIvdmlnbmV0dGVzL3JlY29tbWVuZGVybGFiLnBkZikuDQo0LiBbQ29sbGFib3JhdGl2ZSBGaWx0ZXJpbmcgTWV0aG9kcyBmb3IgQmluYXJ5IE1hcmtldCBCYXNrZXQgRGF0YSBBbmFseXNpc10oaHR0cHM6Ly9saW5rLnNwcmluZ2VyLmNvbS9jaGFwdGVyLzEwLjEwMDcvMy01NDAtNDUzMzYtOV8zNSkuIA0KNS4gW0luY3JlbWVudGFsIENvbGxhYm9yYXRpdmUgRmlsdGVyaW5nIGZvciBCaW5hcnkgUmF0aW5nc10oaHR0cHM6Ly93d3cucmVzZWFyY2hnYXRlLm5ldC9wdWJsaWNhdGlvbi8yMjExNTg0NDFfSW5jcmVtZW50YWxfQ29sbGFib3JhdGl2ZV9GaWx0ZXJpbmdfZm9yX0JpbmFyeV9SYXRpbmdzKS4gDQoNCg0KDQo=