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
InvoiceNo StockCode Description Quantity InvoiceDate UnitPrice CustomerID Country
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)
CustomerID 23566 85067 44089C 21814 22555 23108 22067 85110 90059B 84031A
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
CustomerID StockCode Description
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

Approach to Evaluating Recommender Performance

Mặc dù thư viện recommenderlab có các hàm để đánh giá hiệu quả của Recommender Engine và đã được Suresh K. Gorakala sử dụng cho một case study trong textbook Building a Recommendation System with R nhưng áp dụng nguyên mà không có hiệu chỉnh gì thì sẽ không có nhiều ý nghĩa vì một vài lí do được trình bày ngay sau đây. Trước hết nhắc lại rằng khoảng thời gian mà chúng ta quan sát hành vi của các customers là từ 2010-12-01 08:26:00 đến 2011-12-09 12:50:00:

## [1] "2010-12-01 08:26:00 UTC" "2011-12-09 12:50:00 UTC"

Trong khi đó, những items được khuyến nghị cho các customers hoàn toàn dựa trên data về hành vi của họ trong khoảng thời gian này. Nghĩa là các items được Engine khuyến nghị hoàn toàn dựa vào những sự giao dịch/tương tác ĐÃ XẨY RA trong khi thực tế họ mua sắm những items nào thì chúng ta đã biết.

Do vậy, cách tiếp cận để đánh giá chất lượng của Recommender như sau có thể sẽ phù hợp hơn: so sánh các items được khuyến nghị cho customers với các items mà họ SẼ MUA sau thời điểm 2011-12-09 12:50:00. Đây là cách tiếp cận hợp lí hơn. Tình huống này cũng giống như chúng ta xây dựng một mô hình AR dự báo giá cổ phiếu tại thời điểm t + 1 (tức là giá của 1 ngày nữa) chỉ căn cứ vào giá của hiện tại (thời điểm t) và các biến trễ của t (thông tin trong quá khứ). Để biết mức độ chính xác của dự báo thì cách tiếp cận là, ví dụ, so sánh giá thực tế của 1 ngày sắp tới với giá được dự báo từ mô hình AR. Hướng tiếp cận này sẽ được trình bày chi tiết trong phần kế tiếp của series về Recommendation System.

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=