1 - Description


MovieLens is a data set that contains ratings from the MovieLense website (http://movielens.org). This data set is broken into a number of different sizes for research purposes. We will b using the small data set containing 100,000 ratings applied to 9,000 movies by 700 users.

Using the MovieLense data set we will construct a simple recommender system to recommend movies to users. This system will be built using user based collaborative filtering using both hand-built algorithms and the recomenderlab package.

2 - DataSet


The MovieLens data is broken into two data sets that we are interested in using. The first is the movies data that contains the movie ID, the title, and genres. The second is the user ratings data that contains the user ID, the movie ID, and the rating, and a time-stamp in UNIX time. We will drop the time-stamp data for this project.

Reading in the required data sets.

Lets take a quick look at the data that we have loaded.

2.1 - Movies

2.2 - Ratings

3 - Building the Recommender by Hand


The first thing that we need to do is construct the functions that we will use to generate recommendations. We will be using the cosine similarity between users to generate our recommendations so we first build a cosine similarity function. We then build our function to generate the recommendations. This function takes the similarity matrix generated by the cosine similarity function, the movies and ratings data, and info on the user, number of recommendations requested, and the number of nearest neighbors to use.

4 - Recommender with Normalization


We first try to build a recommendation system using normalization. There are some issues that we will see below. The first is the meaning of zero. In order for R to calculate the matrix multiplication used in our similarity function we need to fill the NA’s as 0. If we don’t we get a matrix with diagonals of 0 and the rest of the element is NA. However once normalized 0 means a centered review and this leads to issues with the list of movies that get recommended.

4.1 - Converting the Pairwise Ratings into a Sparse Matrix with Normalization

To calculate the user based collaborative filter we need to transform the data from our pairwise data to a sparse matrix. We will use the TidyR package to reshape the data.

dim(user_movie_mat)
[1]  671 9066

4.2 - Calculating Similaity using the Cosine Similarity

Now that we have the data loaded into a sparse matrix we want to calculate the similarity using the cosine distance of each of the users. We also plot the similarity values. We see that there may be issues already given that there is not a line indicating each user’s similarity with themselves.

4.3 Recomending a Movie Based on the Cosine Similarity with Normalization

We will now use the recommender and the normalized similarity matrix to recommend a set of 10 movies to user 100 using the 30 nearest neighbors.

norm_recs
      [,1]                                                  
 [1,] "Young Poisoner's Handbook, The (1995)"               
 [2,] "Addams Family Values (1993)"                         
 [3,] "Contact (1997)"                                      
 [4,] "No Holds Barred (1989)"                              
 [5,] "National Velvet (1944)"                              
 [6,] "Dracula: Dead and Loving It (1995)"                  
 [7,] "Shanghai Triad (Yao a yao yao dao waipo qiao) (1995)"
 [8,] "Friday (1995)"                                       
 [9,] "Misérables, Les (1995)"                              
[10,] "Screamers (1995)"                                    

5 - Building a Recommendation System Using Non-Normalized Data


Given that we ran into a potential issue with the normalized data we will build out the recommender using the same data and functions with normalizing the data first.

5.1 - Generating the Similarity Matrix

We first need to recreate the user matrix without normalization.

5.2 - Generating the Cosine Similarities

Now that we have our non-normalized matrix we will generate the cosine similarities. We see that plot of the similarity matrix also lacks any real patterns and not surprisingly we get a different set of movies recommended to the user.

5.3 - Non-Normalized Movie Recommendations

We note that this is a different set then

recs
      [,1]                                    
 [1,] "Crossing Guard, The (1995)"            
 [2,] "Murder in the First (1995)"            
 [3,] "On Golden Pond (1981)"                 
 [4,] "Howling, The (1980)"                   
 [5,] "Fifth Element, The (1997)"             
 [6,] "Hot Lead and Cold Feet (1978)"         
 [7,] "Metroland (1997)"                      
 [8,] "Morning After, The (1986)"             
 [9,] "Sister Act 2: Back in the Habit (1993)"
[10,] "Where the Money Is (2000)"             

6 - Building A Recommendation System Using recomenderlab


For the final portion of the project we will recommenderlab package to build a recommendation for users. The process is relatively straight forward as the package takes the sparse user-review matrix, converts it to a real rating matrix, and then builds the recommender model based on this information.

movie_lab <- function(user_mat, movies, user, num_recs = 10, neigh = 10){
  #Convert rating matrix into a recommenderlab sparse matrix
  user_mat <- as(user_mat, "realRatingMatrix")
  
  #Create Recommender Model. "UBCF" stands for User-Based Collaborative Filtering
  recommender_model <- Recommender(user_mat, 
                                   method = "UBCF", 
                                   param=list(method="Cosine",nn = neigh))
  recom <- predict(recommender_model, 
                   user_mat[user], 
                   n=num_recs) #Obtain top 10 recommendations for 1st user in dataset
  
  recom_list <- as(recom, "list") #convert recommenderlab object to readable list
  
  recom_result <- matrix(0,num_recs)
  for (i in c(1:num_recs)){
    recom_result[i] <- movies$title[as.integer(recom_list[[1]][i])]
  }h
Error: unexpected symbol in:
"    recom_result[i] <- movies$title[as.integer(recom_list[[1]][i])]
  }h"

6.1 - Recommenderlab Movie Recommendations

Using the function above we can now generate the top to recommendation for user 100 using the 30 nearest neighbors. We see that these recommendations are once again different from the ones that we generated by hand above.

lab_recs
      [,1]                         
 [1,] "Scarlet Letter, The (1926)" 
 [2,] "Only You (1994)"            
 [3,] "Crooklyn (1994)"            
 [4,] "Substitute, The (1996)"     
 [5,] "Mary Reilly (1996)"         
 [6,] "Month by the Lake, A (1995)"
 [7,] "Top Hat (1935)"             
 [8,] "Eye for an Eye (1996)"      
 [9,] "Mighty Aphrodite (1995)"    
[10,] "Tales from the Hood (1995)" 

7 - Comparison of the Three Recomendations and Performance

The first thing that we note is that there is a definite difference in the performance of the hand coded algorithm and the recomenderlab package. The hand coded portion of the algorithm that generates the cosine similarity was a slow process. It took 20 to 30 seconds if not more to calculate the multiplications to generate the similarity matrix. The recommenderlab package does this process much more quickly and the model can be generated each time the function is called without performance hits.

The second thing that we notice is that all three of the methods of doing recommendations generate different results as we can see below. My best guess is that the method that I used in the function that generates the recommendations from the similarity matrix is not using the correct method for getting the best results. It is interesting to see that the normalized and non-normalized lead to different results but this is likely the fact that we need to include zero’s to do the matrix multiplication which have a meaning once we normalize the data.

LS0tDQp0aXRsZTogJ0RBVEE2NDMgLSBQcm9qZWN0IDE6IEJhc2ljIFJlY29tbWVuZGVyIFN5c3RlbScNCmF1dGhvcjogIkVyaWsgTnlsYW5kZXIiDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6IGRlZmF1bHQNCiAgcGRmX2RvY3VtZW50OiBkZWZhdWx0DQotLS0NCmBgYHtSLCBpbmNsdWRlID0gRkFMU0V9DQpyZXF1aXJlKHRpZHlyKQ0KcmVxdWlyZShkcGx5cikNCmBgYA0KIyMgMSAtIERlc2NyaXB0aW9uDQoqKioNCltNb3ZpZUxlbnNdKGh0dHBzOi8vZ3JvdXBsZW5zLm9yZy9kYXRhc2V0cy9tb3ZpZWxlbnMvKSBpcyBhIGRhdGEgc2V0IHRoYXQgY29udGFpbnMgcmF0aW5ncyBmcm9tIHRoZSBNb3ZpZUxlbnNlIHdlYnNpdGUgKGh0dHA6Ly9tb3ZpZWxlbnMub3JnKS4gVGhpcyBkYXRhIHNldCBpcyBicm9rZW4gaW50byBhIG51bWJlciBvZiBkaWZmZXJlbnQgc2l6ZXMgZm9yIHJlc2VhcmNoIHB1cnBvc2VzLiBXZSB3aWxsIGIgdXNpbmcgdGhlIHNtYWxsIGRhdGEgc2V0IGNvbnRhaW5pbmcgMTAwLDAwMCByYXRpbmdzIGFwcGxpZWQgdG8gOSwwMDAgbW92aWVzIGJ5IDcwMCB1c2Vycy4gIA0KDQpVc2luZyB0aGUgTW92aWVMZW5zZSBkYXRhIHNldCB3ZSB3aWxsIGNvbnN0cnVjdCBhIHNpbXBsZSByZWNvbW1lbmRlciBzeXN0ZW0gdG8gcmVjb21tZW5kIG1vdmllcyB0byB1c2Vycy4gVGhpcyBzeXN0ZW0gd2lsbCBiZSBidWlsdCB1c2luZyB1c2VyIGJhc2VkIGNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nIHVzaW5nIGJvdGggaGFuZC1idWlsdCBhbGdvcml0aG1zIGFuZCB0aGUgKnJlY29tZW5kZXJsYWIqIHBhY2thZ2UuDQoNCiMjIDIgLSBEYXRhU2V0DQoqKioNClRoZSBNb3ZpZUxlbnMgZGF0YSBpcyBicm9rZW4gaW50byB0d28gZGF0YSBzZXRzIHRoYXQgd2UgYXJlIGludGVyZXN0ZWQgaW4gdXNpbmcuIFRoZSBmaXJzdCBpcyB0aGUgbW92aWVzIGRhdGEgdGhhdCBjb250YWlucyB0aGUgbW92aWUgSUQsIHRoZSB0aXRsZSwgYW5kIGdlbnJlcy4gVGhlIHNlY29uZCBpcyB0aGUgdXNlciByYXRpbmdzIGRhdGEgdGhhdCBjb250YWlucyB0aGUgdXNlciBJRCwgdGhlIG1vdmllIElELCBhbmQgdGhlIHJhdGluZywgYW5kIGEgdGltZS1zdGFtcCBpbiBVTklYIHRpbWUuIFdlIHdpbGwgZHJvcCB0aGUgdGltZS1zdGFtcCBkYXRhIGZvciB0aGlzIHByb2plY3QuDQoNClJlYWRpbmcgaW4gdGhlIHJlcXVpcmVkIGRhdGEgc2V0cy4NCg0KYGBge1IsIGluY2x1ZGUgPSBUUlVFfQ0KbW92aWVzIDwtIHJlYWQuY3N2KCJ+L0dpdEh1Yi9EQVRBNjQzL2RhdGEvbWwtbGF0ZXN0LXNtYWxsL21vdmllcy5jc3YiLCBoZWFkZXIgPSBUUlVFLCBzZXAgPSAiLCIsDQogICAgICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFLCBlbmNvZGluZyA9ICJVVEYtOCIpDQpyYXRpbmdzIDwtIHJlYWQuY3N2KCJ+L0dpdEh1Yi9EQVRBNjQzL2RhdGEvbWwtbGF0ZXN0LXNtYWxsL3JhdGluZ3MuY3N2IiwgaGVhZGVyID0gVFJVRSwgc2VwID0iLCIsDQogICAgICAgICAgICAgICAgICAgIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkNCnJhdGluZ3MgPC0gcmF0aW5nc1ssYygxLDIsMyldDQpgYGANCg0KTGV0cyB0YWtlIGEgcXVpY2sgbG9vayBhdCB0aGUgZGF0YSB0aGF0IHdlIGhhdmUgbG9hZGVkLg0KDQojIyMjIDIuMSAtIE1vdmllcw0KYGBge1J9DQpkaW0obW92aWVzKQ0KaGVhZChtb3ZpZXMpDQpgYGANCg0KIyMjIyAyLjIgLSBSYXRpbmdzDQpgYGB7Un0NCmRpbShyYXRpbmdzKQ0KaGVhZChyYXRpbmdzKQ0KYGBgDQoNCg0KIyMgMyAtIEJ1aWxkaW5nIHRoZSBSZWNvbW1lbmRlciBieSBIYW5kDQoqKioNClRoZSBmaXJzdCB0aGluZyB0aGF0IHdlIG5lZWQgdG8gZG8gaXMgY29uc3RydWN0IHRoZSBmdW5jdGlvbnMgdGhhdCB3ZSB3aWxsIHVzZSB0byBnZW5lcmF0ZSByZWNvbW1lbmRhdGlvbnMuIFdlIHdpbGwgYmUgdXNpbmcgdGhlIGNvc2luZSBzaW1pbGFyaXR5IGJldHdlZW4gdXNlcnMgdG8gZ2VuZXJhdGUgb3VyIHJlY29tbWVuZGF0aW9ucyBzbyB3ZSBmaXJzdCBidWlsZCBhIGNvc2luZSBzaW1pbGFyaXR5IGZ1bmN0aW9uLiBXZSB0aGVuIGJ1aWxkIG91ciBmdW5jdGlvbiB0byBnZW5lcmF0ZSB0aGUgcmVjb21tZW5kYXRpb25zLiBUaGlzIGZ1bmN0aW9uIHRha2VzIHRoZSBzaW1pbGFyaXR5IG1hdHJpeCBnZW5lcmF0ZWQgYnkgdGhlIGNvc2luZSBzaW1pbGFyaXR5IGZ1bmN0aW9uLCB0aGUgbW92aWVzIGFuZCByYXRpbmdzIGRhdGEsIGFuZCBpbmZvIG9uIHRoZSB1c2VyLCBudW1iZXIgb2YgcmVjb21tZW5kYXRpb25zIHJlcXVlc3RlZCwgYW5kIHRoZSBudW1iZXIgb2YgbmVhcmVzdCBuZWlnaGJvcnMgdG8gdXNlLg0KDQpgYGB7Un0NCiMgQ2FsY3VsYXRlIHRoZSBjb3NpbmUgc2ltaWxhcml0eSB1c2luZyBidWlsdCBpbiBsaW5lYXIgYWxnZWJyYS4NCmNvc2luZVNpbSA8LSBmdW5jdGlvbih4KXsNCiAgeCA8LSBhcy5kaXN0KHglKiV0KHgpLyhzcXJ0KHJvd1N1bXMoeF4yKSAlKiUgdChyb3dTdW1zKHheMikpKSkpIA0KICByZXR1cm4oeCkNCn0NCg0KIyBNb3ZlIHJlY29tbWVuZGF0aW9uIHVuY3Rpb24NCm1vdmllX3JlYyA8LSBmdW5jdGlvbihzaW1fbWF0LCBtb3ZpZXMsIHJhdGluZ3MsIHVzZXIsIG51bV9yZWNzID0gMTAsIG5uID0gMTApew0KICBzaW1fdmVjIDwtIHNpbV9tYXRbdXNlcixdDQogIHNpbV92ZWMgPC0gbmFtZXMoc29ydChzaW1fdmVjKSkNCiAgc2ltX3ZlYyA8LSBzaW1fdmVjWzI6bm4rMV0NCiAgcmV2aWV3ZWQgPC0gcmF0aW5nc1tyYXRpbmdzJHVzZXJJZCA9PSB1c2VyLF0NCiAgDQogIHN1Z2dlc3RlZCA8LSByYXRpbmdzICU+JQ0KICAgIGZpbHRlcihyYXRpbmdzJHVzZXJJZCAlaW4lIHNpbV92ZWMpICU+JQ0KICAgIGdyb3VwX2J5KG1vdmllSWQpICU+JQ0KICAgIHN1bW1hcmlzZSgNCiAgICAgIGNvdW50ID0gbigpLCAjSG93IG1hbnkgdGltZXMgd2FzIHRoZSBtb3ZpZSByYXRlZA0KICAgICAgYXZnX3JhdGluZyA9IG1lYW4ocmF0aW5nKSAjV2hhdCBpcyB0aGUgYXZlcmFnZSByYXRpbmcgZnJvbSB0aGUgc2ltaWxhciByZXZpZXdlcnMNCiAgICApICU+JQ0KICAgIGZpbHRlcighKG1vdmllSWQgJWluJSByZXZpZXdlZCRtb3ZpZUlkKSkgJT4lDQogICAgYXJyYW5nZShkZXNjKGF2Z19yYXRpbmcpLCBkZXNjKGNvdW50KSkNCg0KICByZWNfbGlzdCA8LSBtYXRyaXgoMCwgbnVtX3JlY3MpDQogIGZvcihpIGluIDE6bnVtX3JlY3Mpew0KICAgIHJlY19saXN0W2ldIDwtIG1vdmllcyR0aXRsZVtzdWdnZXN0ZWQkbW92aWVJZFtpXV0NCiAgfQ0KICByZXR1cm4ocmVjX2xpc3QpDQp9DQpgYGANCg0KIyMgNCAtIFJlY29tbWVuZGVyIHdpdGggTm9ybWFsaXphdGlvbg0KKioqDQpXZSBmaXJzdCB0cnkgdG8gYnVpbGQgYSByZWNvbW1lbmRhdGlvbiBzeXN0ZW0gdXNpbmcgbm9ybWFsaXphdGlvbi4gVGhlcmUgYXJlIHNvbWUgaXNzdWVzIHRoYXQgd2Ugd2lsbCBzZWUgYmVsb3cuIFRoZSBmaXJzdCBpcyB0aGUgbWVhbmluZyBvZiB6ZXJvLiBJbiBvcmRlciBmb3IgUiB0byBjYWxjdWxhdGUgdGhlIG1hdHJpeCBtdWx0aXBsaWNhdGlvbiB1c2VkIGluIG91ciBzaW1pbGFyaXR5IGZ1bmN0aW9uIHdlIG5lZWQgdG8gZmlsbCB0aGUgTkEncyBhcyAwLiBJZiB3ZSBkb24ndCB3ZSBnZXQgYSBtYXRyaXggd2l0aCBkaWFnb25hbHMgb2YgMCBhbmQgdGhlIHJlc3Qgb2YgdGhlIGVsZW1lbnQgaXMgTkEuIEhvd2V2ZXIgb25jZSBub3JtYWxpemVkIDAgbWVhbnMgYSBjZW50ZXJlZCByZXZpZXcgYW5kIHRoaXMgbGVhZHMgdG8gaXNzdWVzIHdpdGggdGhlIGxpc3Qgb2YgbW92aWVzIHRoYXQgZ2V0IHJlY29tbWVuZGVkLg0KDQojIyMjIDQuMSAtIENvbnZlcnRpbmcgdGhlIFBhaXJ3aXNlIFJhdGluZ3MgaW50byBhIFNwYXJzZSBNYXRyaXggd2l0aCBOb3JtYWxpemF0aW9uDQpUbyBjYWxjdWxhdGUgdGhlIHVzZXIgYmFzZWQgY29sbGFib3JhdGl2ZSBmaWx0ZXIgd2UgbmVlZCB0byB0cmFuc2Zvcm0gdGhlIGRhdGEgZnJvbSBvdXIgcGFpcndpc2UgZGF0YSB0byBhIHNwYXJzZSBtYXRyaXguIFdlIHdpbGwgdXNlIHRoZSAqVGlkeVIqIHBhY2thZ2UgdG8gcmVzaGFwZSB0aGUgZGF0YS4gIA0KYGBge1J9DQojIENyZWF0aW5nIGEgc3BhcnNlIG1hdHJpeCBvZiB1c2VycyBhcyByb3dzIGFuZCBtb3ZpZUlkIGFzIGNvbHVtbnMuDQp1c2VyX21vdmllX21hdCA8LSByYXRpbmdzICU+JQ0KICBzcHJlYWQoa2V5ID0gbW92aWVJZCwgdmFsdWUgPSByYXRpbmcpICU+JQ0KICBhcy5tYXRyaXgoKQ0KDQp1c2VyX21vdmllX21hdCA9IHVzZXJfbW92aWVfbWF0WywtMV0gI3JlbW92ZSB1c2VySUQgY29sLiBSb3dzIGFyZSB1c2VySWRzLCBjb2xzIGFyZSBtb3ZpZUlkcw0KDQp1c2VyX21vdmllX21hdCA8LSB0KHNjYWxlKHQodXNlcl9tb3ZpZV9tYXQpLCBjZW50ZXIgPSBUUlVFLCBzY2FsZSA9IFRSVUUpKSAjcHJlZm9ybWlubmcgcm93IHdpc2Ugbm9ybWFsaXphdGlvbi4NCg0KdXNlcl9tb3ZpZV9tYXRbaXMubmEodXNlcl9tb3ZpZV9tYXQpXSA8LSAwICNjb252ZXJ0aW5nIHRoZSBOQSdzIHRvIDAgc28gdGhhdCB3ZSBjYW4gbXVsdGlwbHkgdGhlIG1hdHJpY2VzIGJlbG93LiANCmBgYA0KDQojIyMjIDQuMiAtIENhbGN1bGF0aW5nIFNpbWlsYWl0eSB1c2luZyB0aGUgQ29zaW5lIFNpbWlsYXJpdHkNCk5vdyB0aGF0IHdlIGhhdmUgdGhlIGRhdGEgbG9hZGVkIGludG8gYSBzcGFyc2UgbWF0cml4IHdlIHdhbnQgdG8gY2FsY3VsYXRlIHRoZSBzaW1pbGFyaXR5IHVzaW5nIHRoZSBjb3NpbmUgZGlzdGFuY2Ugb2YgZWFjaCBvZiB0aGUgdXNlcnMuIFdlIGFsc28gcGxvdCB0aGUgc2ltaWxhcml0eSB2YWx1ZXMuIFdlIHNlZSB0aGF0IHRoZXJlIG1heSBiZSBpc3N1ZXMgYWxyZWFkeSBnaXZlbiB0aGF0IHRoZXJlIGlzIG5vdCBhIGxpbmUgaW5kaWNhdGluZyBlYWNoIHVzZXIncyBzaW1pbGFyaXR5IHdpdGggdGhlbXNlbHZlcy4NCg0KYGBge1J9DQojIENyZWF0aW5nIHRoZSBzaW1pbGFyaXR5IG1hdHJpeA0Kc2ltaWxhcl9yZXZpZXdzX25vcm0gPC0gYXMubWF0cml4KGNvc2luZVNpbSh1c2VyX21vdmllX21hdCkpDQoNCmltYWdlKHNpbWlsYXJfcmV2aWV3c19ub3JtKQ0KYGBgDQoNCg0KIyMjIyA0LjMgUmVjb21lbmRpbmcgYSBNb3ZpZSBCYXNlZCBvbiB0aGUgQ29zaW5lIFNpbWlsYXJpdHkgd2l0aCBOb3JtYWxpemF0aW9uDQpXZSB3aWxsIG5vdyB1c2UgdGhlIHJlY29tbWVuZGVyIGFuZCB0aGUgbm9ybWFsaXplZCBzaW1pbGFyaXR5IG1hdHJpeCB0byByZWNvbW1lbmQgYSBzZXQgb2YgMTAgbW92aWVzIHRvIHVzZXIgMTAwIHVzaW5nIHRoZSAzMCBuZWFyZXN0IG5laWdoYm9ycy4gDQpgYGB7Un0NCm5vcm1fcmVjcyA8LSBtb3ZpZV9yZWMoc2ltaWxhcl9yZXZpZXdzX25vcm0sIG1vdmllcywgcmF0aW5ncywgdXNlciA9IDEwMCwgbnVtX3JlY3MgPSAxMCwgbm4gPSAzMCkNCm5vcm1fcmVjcw0KYGBgDQoNCiMjIDUgLSBCdWlsZGluZyBhIFJlY29tbWVuZGF0aW9uIFN5c3RlbSBVc2luZyBOb24tTm9ybWFsaXplZCBEYXRhDQoqKioNCkdpdmVuIHRoYXQgd2UgcmFuIGludG8gYSBwb3RlbnRpYWwgaXNzdWUgd2l0aCB0aGUgbm9ybWFsaXplZCBkYXRhIHdlIHdpbGwgYnVpbGQgb3V0IHRoZSByZWNvbW1lbmRlciB1c2luZyB0aGUgc2FtZSBkYXRhIGFuZCBmdW5jdGlvbnMgd2l0aCBub3JtYWxpemluZyB0aGUgZGF0YSBmaXJzdC4NCg0KIyMjIyA1LjEgLSBHZW5lcmF0aW5nIHRoZSBTaW1pbGFyaXR5IE1hdHJpeA0KV2UgZmlyc3QgbmVlZCB0byByZWNyZWF0ZSB0aGUgdXNlciBtYXRyaXggd2l0aG91dCBub3JtYWxpemF0aW9uLg0KYGBge1J9DQojIENyZWF0aW5nIGEgc3BhcnNlIG1hdHJpeCBvZiB1c2VycyBhcyByb3dzIGFuZCBtb3ZpZUlkIGFzIGNvbHVtbnMuDQp1c2VyX21vdmllX21hdCA8LSByYXRpbmdzICU+JQ0KICBzcHJlYWQoa2V5ID0gbW92aWVJZCwgdmFsdWUgPSByYXRpbmcpICU+JQ0KICBhcy5tYXRyaXgoKQ0KDQp1c2VyX21vdmllX21hdCA9IHVzZXJfbW92aWVfbWF0WywtMV0gI3JlbW92ZSB1c2VySUQgY29sLiBSb3dzIGFyZSB1c2VySWRzLCBjb2xzIGFyZSBtb3ZpZUlkcw0KDQp1c2VyX21vdmllX21hdFtpcy5uYSh1c2VyX21vdmllX21hdCldIDwtIDAgI2NvbnZlcnRpbmcgdGhlIE5BJ3MgdG8gMCBzbyB0aGF0IHdlIGNhbiBtdWx0aXBseSB0aGUgbWF0cmljZXMgYmVsb3cuIA0KYGBgDQoNCiMjIyMgNS4yIC0gR2VuZXJhdGluZyB0aGUgQ29zaW5lIFNpbWlsYXJpdGllcw0KTm93IHRoYXQgd2UgaGF2ZSBvdXIgbm9uLW5vcm1hbGl6ZWQgbWF0cml4IHdlIHdpbGwgZ2VuZXJhdGUgdGhlIGNvc2luZSBzaW1pbGFyaXRpZXMuIFdlIHNlZSB0aGF0IHBsb3Qgb2YgdGhlIHNpbWlsYXJpdHkgbWF0cml4IGFsc28gbGFja3MgYW55IHJlYWwgcGF0dGVybnMgYW5kIG5vdCBzdXJwcmlzaW5nbHkgd2UgZ2V0IGEgZGlmZmVyZW50IHNldCBvZiBtb3ZpZXMgcmVjb21tZW5kZWQgdG8gdGhlIHVzZXIuIA0KYGBge1J9DQojIENyZWF0aW5nIHRoZSBzaW1pbGFyaXR5IG1hdHJpeA0Kc2ltaWxhcl9yZXZpZXdzIDwtIGFzLm1hdHJpeChjb3NpbmVTaW0odXNlcl9tb3ZpZV9tYXQpKQ0KDQppbWFnZShzaW1pbGFyX3Jldmlld3MpDQpgYGANCiMjIyMgNS4zIC0gTm9uLU5vcm1hbGl6ZWQgTW92aWUgUmVjb21tZW5kYXRpb25zDQpXZSBub3RlIHRoYXQgdGhpcyBpcyBhIGRpZmZlcmVudCBzZXQgdGhlbiANCmBgYHtSfQ0KcmVjcyA8LSBtb3ZpZV9yZWMoc2ltaWxhcl9yZXZpZXdzLCBtb3ZpZXMsIHJhdGluZ3MsIHVzZXIgPSAxMDAsIG51bV9yZWNzID0gMTAsIG5uID0gMzApDQpyZWNzDQpgYGANCg0KIyMgNiAtIEJ1aWxkaW5nIEEgUmVjb21tZW5kYXRpb24gU3lzdGVtIFVzaW5nICpyZWNvbWVuZGVybGFiKg0KKioqDQpGb3IgdGhlIGZpbmFsIHBvcnRpb24gb2YgdGhlIHByb2plY3Qgd2Ugd2lsbCAqcmVjb21tZW5kZXJsYWIqIHBhY2thZ2UgdG8gYnVpbGQgYSByZWNvbW1lbmRhdGlvbiBmb3IgdXNlcnMuIFRoZSBwcm9jZXNzIGlzIHJlbGF0aXZlbHkgc3RyYWlnaHQgZm9yd2FyZCBhcyB0aGUgcGFja2FnZSB0YWtlcyB0aGUgc3BhcnNlIHVzZXItcmV2aWV3IG1hdHJpeCwgY29udmVydHMgaXQgdG8gYSByZWFsIHJhdGluZyBtYXRyaXgsIGFuZCB0aGVuIGJ1aWxkcyB0aGUgcmVjb21tZW5kZXIgbW9kZWwgYmFzZWQgb24gdGhpcyBpbmZvcm1hdGlvbi4gDQpgYGB7Un0NCmxpYnJhcnkocmVjb21tZW5kZXJsYWIpDQoNCiMgQ3JlYXRpbmcgYSBzcGFyc2UgbWF0cml4IG9mIHVzZXJzIGFzIHJvd3MgYW5kIG1vdmllSWQgYXMgY29sdW1ucy4NCnVzZXJfbW92aWVfbWF0IDwtIHJhdGluZ3MgJT4lDQogIHNwcmVhZChrZXkgPSBtb3ZpZUlkLCB2YWx1ZSA9IHJhdGluZykgJT4lDQogIGFzLm1hdHJpeCgpDQoNCnVzZXJfbW92aWVfbWF0ID0gdXNlcl9tb3ZpZV9tYXRbLC0xXQ0KDQptb3ZpZV9sYWIgPC0gZnVuY3Rpb24odXNlcl9tYXQsIG1vdmllcywgdXNlciwgbnVtX3JlY3MgPSAxMCwgbmVpZ2ggPSAxMCl7DQogICNDb252ZXJ0IHJhdGluZyBtYXRyaXggaW50byBhIHJlY29tbWVuZGVybGFiIHNwYXJzZSBtYXRyaXgNCiAgdXNlcl9tYXQgPC0gYXModXNlcl9tYXQsICJyZWFsUmF0aW5nTWF0cml4IikNCiAgDQogICNDcmVhdGUgUmVjb21tZW5kZXIgTW9kZWwuICJVQkNGIiBzdGFuZHMgZm9yIFVzZXItQmFzZWQgQ29sbGFib3JhdGl2ZSBGaWx0ZXJpbmcNCiAgcmVjb21tZW5kZXJfbW9kZWwgPC0gUmVjb21tZW5kZXIodXNlcl9tYXQsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiVUJDRiIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwYXJhbT1saXN0KG1ldGhvZD0iQ29zaW5lIixubiA9IG5laWdoKSkNCiAgcmVjb20gPC0gcHJlZGljdChyZWNvbW1lbmRlcl9tb2RlbCwgDQogICAgICAgICAgICAgICAgICAgdXNlcl9tYXRbdXNlcl0sIA0KICAgICAgICAgICAgICAgICAgIG49bnVtX3JlY3MpICNPYnRhaW4gdG9wIDEwIHJlY29tbWVuZGF0aW9ucyBmb3IgMXN0IHVzZXIgaW4gZGF0YXNldA0KICANCiAgcmVjb21fbGlzdCA8LSBhcyhyZWNvbSwgImxpc3QiKSAjY29udmVydCByZWNvbW1lbmRlcmxhYiBvYmplY3QgdG8gcmVhZGFibGUgbGlzdA0KICANCiAgcmVjb21fcmVzdWx0IDwtIG1hdHJpeCgwLG51bV9yZWNzKQ0KICBmb3IgKGkgaW4gYygxOm51bV9yZWNzKSl7DQogICAgcmVjb21fcmVzdWx0W2ldIDwtIG1vdmllcyR0aXRsZVthcy5pbnRlZ2VyKHJlY29tX2xpc3RbWzFdXVtpXSldDQogIH0NCiAgcmV0dXJuKHJlY29tX3Jlc3VsdCkNCn0NCmBgYA0KIyMjIyA2LjEgLSBSZWNvbW1lbmRlcmxhYiBNb3ZpZSBSZWNvbW1lbmRhdGlvbnMNClVzaW5nIHRoZSBmdW5jdGlvbiBhYm92ZSB3ZSBjYW4gbm93IGdlbmVyYXRlIHRoZSB0b3AgdG8gcmVjb21tZW5kYXRpb24gZm9yIHVzZXIgMTAwIHVzaW5nIHRoZSAzMCBuZWFyZXN0IG5laWdoYm9ycy4gV2Ugc2VlIHRoYXQgdGhlc2UgcmVjb21tZW5kYXRpb25zIGFyZSBvbmNlIGFnYWluIGRpZmZlcmVudCBmcm9tIHRoZSBvbmVzIHRoYXQgd2UgZ2VuZXJhdGVkIGJ5IGhhbmQgYWJvdmUuDQpgYGB7Un0NCmxhYl9yZWNzIDwtIG1vdmllX2xhYih1c2VyX21vdmllX21hdCwgbW92aWVzLCB1c2VyID0gMTAwLCBudW1fcmVjcyA9IDEwLCBuZWlnaCA9IDMwKQ0KbGFiX3JlY3MNCmBgYA0KDQojIyA3IC0gQ29tcGFyaXNvbiBvZiB0aGUgVGhyZWUgUmVjb21lbmRhdGlvbnMgYW5kIFBlcmZvcm1hbmNlDQpUaGUgZmlyc3QgdGhpbmcgdGhhdCB3ZSBub3RlIGlzIHRoYXQgdGhlcmUgaXMgYSBkZWZpbml0ZSBkaWZmZXJlbmNlIGluIHRoZSBwZXJmb3JtYW5jZSBvZiB0aGUgaGFuZCBjb2RlZCBhbGdvcml0aG0gYW5kIHRoZSAqcmVjb21lbmRlcmxhYiogcGFja2FnZS4gVGhlIGhhbmQgY29kZWQgcG9ydGlvbiBvZiB0aGUgYWxnb3JpdGhtIHRoYXQgZ2VuZXJhdGVzIHRoZSBjb3NpbmUgc2ltaWxhcml0eSB3YXMgYSBzbG93IHByb2Nlc3MuIEl0IHRvb2sgMjAgdG8gMzAgc2Vjb25kcyBpZiBub3QgbW9yZSB0byBjYWxjdWxhdGUgdGhlIG11bHRpcGxpY2F0aW9ucyB0byBnZW5lcmF0ZSB0aGUgc2ltaWxhcml0eSBtYXRyaXguIFRoZSAqcmVjb21tZW5kZXJsYWIqIHBhY2thZ2UgZG9lcyB0aGlzIHByb2Nlc3MgbXVjaCBtb3JlIHF1aWNrbHkgYW5kIHRoZSBtb2RlbCBjYW4gYmUgZ2VuZXJhdGVkIGVhY2ggdGltZSB0aGUgZnVuY3Rpb24gaXMgY2FsbGVkIHdpdGhvdXQgcGVyZm9ybWFuY2UgaGl0cy4gIA0KDQpUaGUgc2Vjb25kIHRoaW5nIHRoYXQgd2Ugbm90aWNlIGlzIHRoYXQgYWxsIHRocmVlIG9mIHRoZSBtZXRob2RzIG9mIGRvaW5nIHJlY29tbWVuZGF0aW9ucyBnZW5lcmF0ZSBkaWZmZXJlbnQgcmVzdWx0cyBhcyB3ZSBjYW4gc2VlIGJlbG93LiBNeSBiZXN0IGd1ZXNzIGlzIHRoYXQgdGhlIG1ldGhvZCB0aGF0IEkgdXNlZCBpbiB0aGUgZnVuY3Rpb24gdGhhdCBnZW5lcmF0ZXMgdGhlIHJlY29tbWVuZGF0aW9ucyBmcm9tIHRoZSBzaW1pbGFyaXR5IG1hdHJpeCBpcyBub3QgdXNpbmcgdGhlIGNvcnJlY3QgbWV0aG9kIGZvciBnZXR0aW5nIHRoZSBiZXN0IHJlc3VsdHMuIEl0IGlzIGludGVyZXN0aW5nIHRvIHNlZSB0aGF0IHRoZSBub3JtYWxpemVkIGFuZCBub24tbm9ybWFsaXplZCBsZWFkIHRvIGRpZmZlcmVudCByZXN1bHRzIGJ1dCB0aGlzIGlzIGxpa2VseSB0aGUgZmFjdCB0aGF0IHdlIG5lZWQgdG8gaW5jbHVkZSB6ZXJvJ3MgdG8gZG8gdGhlIG1hdHJpeCBtdWx0aXBsaWNhdGlvbiB3aGljaCBoYXZlIGEgbWVhbmluZyBvbmNlIHdlIG5vcm1hbGl6ZSB0aGUgZGF0YS4NCmBgYHtSfQ0KcmVzdWx0cyA8LSBkYXRhLmZyYW1lKE5vcm1hbGl6ZWQgPSBub3JtX3JlY3MsIE5vbl9Ob3JtYWxpemVkID0gcmVjcywgUmVjb21tZW5kZXJsYWIgPSBsYWJfcmVjcykNCnJlc3VsdHMNCmBgYA==