Jeremy Howard recently taught the Fastai Deep Learning for Coders (Part 1) course. This course has a lesson on Collaborative Filtering where he uses MovieLens dataset to demonstrate models for predicting ratings of movies. This content is covered in videos of lecture 5 and lecture 6.

This notebook is an attempt to create a R version (using Reticulate package) of the MovieLens python notebook covered in the course. While we can have separate python and R chunks with interoperability using Reticulate, I have tried to do everything in R since it will be easier to use this as standalone R script also. This course uses a library fastai (written by Jeremy) which is a wrapper around PyTorch.

It will be helpful to listen to the lectures before going through this notebook since the concepts of the model and approach are discussed in the lecture and this notebook is just a replication attempt of the material from the course using R.

Initial Setup

# import R libraries
library(reticulate)
library(ggplot2)
library(dplyr)

Attaching package: ‘dplyr’

The following objects are masked from ‘package:stats’:

    filter, lag

The following objects are masked from ‘package:base’:

    intersect, setdiff, setequal, union
library(irlba)
Loading required package: Matrix

In this course, they talk about several cloud GPU options and provided a conda environment fastai in those environments. I am using the Paperspace setup that is covered in Lesson 1. Reshama Shaik has also listed detailed upto date instructions on getting set up in Paperspace. For setting up RStudio server in the same machine, I followed Cloud GPU setup section in TensorFlow for R site (except for the EC2 part).

Since the machine already comes with fastai conda environment, we first need to use the python that is part of the fastai environment and also use the fastai conda environment.

use_python("/home/paperspace/anaconda3/envs/fastai/bin/python", required = TRUE)
use_condaenv("fastai")
py_config()
python:         /home/paperspace/anaconda3/envs/fastai/bin/python
libpython:      /home/paperspace/anaconda3/envs/fastai/lib/libpython3.6m.so
pythonhome:     /home/paperspace/anaconda3/envs/fastai:/home/paperspace/anaconda3/envs/fastai
version:        3.6.4 |Anaconda, Inc.| (default, Dec 21 2017, 21:42:08)  [GCC 7.2.0]
numpy:          /home/paperspace/anaconda3/envs/fastai/lib/python3.6/site-packages/numpy
numpy_version:  1.13.3

NOTE: Python version was forced by use_python function
main = import_main()
bi = import_builtins()

The fastai library is located in the folder ~/fastai. So first, we import fastai.learner and fastai.columndata (libraries used in the notebook) from the folder.

# get relevant python imports
fstai_learner = import_from_path("fastai.learner", "../../fastai")
fstai_coldata = import_from_path("fastai.column_data", "../../fastai")

These modules import several other modules. So the import command used in the python notebook is directly called below so that all the other modules are also available for use later.

py_run_string("
from fastai.learner import *
from fastai.column_data import *
              ")

Get Data

The ratings dataset has the ratings for different users and movies. The movies dataset has the movie title information.

datapath = "../../data/ml-latest-small/"
ratings = read.csv(paste0(datapath, "ratings.csv"), stringsAsFactors = FALSE)
head(ratings)
movies = read.csv(paste0(datapath, "movies.csv"), stringsAsFactors = FALSE)
head(movies)

Model

Each user is \(i\) represent by an embedding vector \(u_i\) consisting of n_factor values and a user bias value \(ub_i\). Similarly a movie \(j\) is represented by an embedding vector \(m_j\) consisting of n_factor values and movie bias value \(mb_j\). The model of rating \(r_{ij}\) given by user \(i\) to movie \(j\) is: \[ r_{ij} = u_i^Tv_j + b_i + m_j \]

Model Fitting

First, the set of user/movie combinations that would be used as validation set is determined. Then, the following parameters are set:

For few of the code snippets below, I have included the python code in comments to show the correspondence between python code and R code with reticulate. In most cases, I just had to do one of the following things to get things to work:

# python code
# val_idxs = get_cv_idxs(len(ratings))
#
val_idxs = py$get_cv_idxs(nrow(ratings))
val_idxs = as.integer(val_idxs)
wd = 2e-4
n_factors = 50L

Next a data loader object cf and a learner object learn is created

# python code
# cf = CollabFilterDataset.from_csv(path, 'ratings.csv', 'userId', 'movieId', 'rating')
#
cf = py$CollabFilterDataset$from_csv(datapath, 'ratings.csv', 'userId', 'movieId', 'rating')
# python code
# learn = cf.get_learner(n_factors, val_idxs, 64, opt_fn=optim.Adam)
#
learn = cf$get_learner(n_factors, val_idxs, 64L, opt_fn=py$optim$Adam)

We review the model

learn$model
EmbeddingDotBias(
  (u): Embedding(671, 50)
  (i): Embedding(9066, 50)
  (ub): Embedding(671, 1)
  (ib): Embedding(9066, 1)
)

The model has the following components:

Next we fit the model

In Jupyter notebook, a widget shows a nice output of progress. That output doesn’t render properly within RStudio and the html generated document has too much output. For now, I have turned off output and am just storing the final validation loss metric (MSE loss in this case)

paste0("MSE of validation set = ", round(mdlfit[[1]], 3))
[1] "MSE of validation set = 0.766"

Next we compare the predicted and actual ratings for the validation set

yval_preds = learn$predict()
yval=learn$data$val_y
dfplt = data.frame(yval = yval, yval_preds = yval_preds)
ggplot(dfplt) + geom_histogram(aes(x = yval_preds)) + facet_grid(yval ~ .) + 
   xlab("predicted ratings") + theme_bw()

Interpretation of Movie Bias

The model uses a sequential numeric id for users and movies, we first get the movie to id mapping from the data object cf.

movie2idx = cf$item2idx
movie2idx = unlist(movie2idx)
head(movie2idx)
  31 1029 1061 1129 1172 1263 
   0    1    2    3    4    5 
idx2movie = as.numeric(names(movie2idx))
names(idx2movie) = movie2idx
head(idx2movie)
   0    1    2    3    4    5 
  31 1029 1061 1129 1172 1263 

Get the top 3000 movies that got the most ratings

topmovies = ratings %>% group_by(movieId) %>% summarize(cnt = n()) %>% arrange(desc(cnt)) %>% slice(1:3000)
topmoviesidx = movie2idx[as.character(topmovies$movieId)]
topmoviesidx = np_array(topmoviesidx)

Get the movie bias variable for the top 3000 movies

# python code
# m=learn.model
# movie_bias = to_np(m.ib(V(topMovieIdx)))
#
m = learn$model
movie_bias = py$to_np(m$ib(py$V(topmoviesidx)))
movie_bias[1:20]
 [1] 0.8358478 0.9074793 1.3072114 0.8890176 0.8090852 0.4687574 0.8606437 0.5267966
 [9] 1.0062566 0.6911108 0.7748715 0.5598993 0.5669060 0.8955097 0.7633992 0.7947082
[17] 0.2389306 0.6011345 0.2934838 0.7729112
topmovies$movie_bias = movie_bias[,1]
topmovies = left_join(topmovies, movies %>% select(movieId, title), by = "movieId")

Find the lowest rated movies (lowest values of movie bias).

topmovies %>% arrange(movie_bias) %>% slice(1:15)

Find the highest rated movies (highest values of movie bias)

topmovies %>% arrange(desc(movie_bias)) %>% select(title) %>% slice(1:15)

Interpretation of Embeddings

Get the embeddings of movies

# python code
# movie_emb = to_np(m.i(V(topMovieIdx)))
#
movie_emb = py$to_np(m$i(py$V(topmoviesidx)))
dim(movie_emb)
[1] 3000   50

Since there are 50 dimensions, a PCA is done to examine the first two principal components.

pcamdl = prcomp_irlba(movie_emb, n = 2)
loadings_df = data.frame(pcamdl$rotation)
scores_df = data.frame(pcamdl$x)
scores_df$movieidx = py_to_r(topmoviesidx)
scores_df$movieId = idx2movie[as.character(scores_df$movieidx)]
scores_df = inner_join(scores_df, movies %>% select(movieId, title), by = "movieId")

Check the movies that have highest and lowest scores for PC1.

scores_df %>% arrange(PC1) %>% slice(1:10) %>% select(title)
scores_df %>% arrange(desc(PC1)) %>% slice(1:10) %>% select(title)

Check the movies that have highest and lowest scores for PC2.

scores_df %>% arrange(PC2) %>% slice(1:10) %>% select(title)
scores_df %>% arrange(desc(PC2)) %>% slice(1:10) %>% select(title)

The above results could be used to assign some sort of meaning to the first 2 principal components. Next a scatter plot of PC1 vs PC2 with labels of movies is shown. This could be used to check which movies cluster together.

set.seed(12345)
scores_df_samp = scores_df %>% sample_n(50)
ggplot(scores_df_samp) + geom_point(aes(x = PC1, y = PC2)) + 
    geom_text(aes(x = PC1, y = PC2, label = title), size = 2.5, hjust = 0, nudge_x = 0.02) +
     theme_bw()

Summary

Reticulate package is a great addition to R. Working through this example showed that it is not too hard to develop a R version of the analysis in python thanks to reticulate.890opkl,m

sessionInfo()
R version 3.4.3 (2017-11-30)
Platform: x86_64-pc-linux-gnu (64-bit)
Running under: Ubuntu 16.04.3 LTS

Matrix products: default
BLAS: /usr/lib/libblas/libblas.so.3.6.0
LAPACK: /home/paperspace/anaconda3/envs/fastai/lib/libmkl_intel_lp64.so

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C               LC_TIME=en_US.UTF-8       
 [4] LC_COLLATE=en_US.UTF-8     LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                  LC_ADDRESS=C              
[10] LC_TELEPHONE=C             LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] bindrcpp_0.2   irlba_2.3.2    Matrix_1.2-11  dplyr_0.7.4    ggplot2_2.2.1  reticulate_1.6

loaded via a namespace (and not attached):
 [1] Rcpp_0.12.14     knitr_1.20       bindr_0.1.1      magrittr_1.5     munsell_0.4.3   
 [6] colorspace_1.3-2 lattice_0.20-35  R6_2.2.2         rlang_0.1.6      stringr_1.3.0   
[11] plyr_1.8.4       tools_3.4.3      grid_3.4.3       gtable_0.2.0     yaml_2.1.18     
[16] lazyeval_0.2.1   assertthat_0.2.0 tibble_1.4.2     reshape2_1.4.3   glue_1.2.0      
[21] labeling_0.3     stringi_1.1.6    compiler_3.4.3   pillar_1.1.0     scales_0.5.0    
[26] jsonlite_1.5     pkgconfig_2.0.1 
LS0tCnRpdGxlOiAiRmFzdGFpIENvbGxhYm9yYXRpdmUgRmlsdGVyaW5nIHdpdGggUiBhbmQgUmV0aWN1bGF0ZSIKb3V0cHV0OiBodG1sX25vdGVib29rCmVkaXRvcl9vcHRpb25zOiAKICBjaHVua19vdXRwdXRfdHlwZTogaW5saW5lCi0tLQoKSmVyZW15IEhvd2FyZCByZWNlbnRseSB0YXVnaHQgdGhlIFtGYXN0YWkgRGVlcCBMZWFybmluZyBmb3IgQ29kZXJzIChQYXJ0IDEpXShodHRwOi8vY291cnNlLmZhc3QuYWkvKSBjb3Vyc2UuIFRoaXMgY291cnNlIGhhcyBhIGxlc3NvbiBvbiBDb2xsYWJvcmF0aXZlIEZpbHRlcmluZyB3aGVyZSBoZSB1c2VzIE1vdmllTGVucyBkYXRhc2V0IHRvIGRlbW9uc3RyYXRlIG1vZGVscyBmb3IgcHJlZGljdGluZyByYXRpbmdzIG9mIG1vdmllcy4gVGhpcyBjb250ZW50IGlzIGNvdmVyZWQgaW4gdmlkZW9zIG9mIFtsZWN0dXJlIDVdKGh0dHA6Ly9jb3Vyc2UuZmFzdC5haS9sZXNzb25zL2xlc3NvbjUuaHRtbCkgYW5kIFtsZWN0dXJlIDZdKGh0dHA6Ly9jb3Vyc2UuZmFzdC5haS9sZXNzb25zL2xlc3NvbjYuaHRtbCkuIAoKVGhpcyBub3RlYm9vayBpcyBhbiBhdHRlbXB0IHRvIGNyZWF0ZSBhIFIgdmVyc2lvbiAodXNpbmcgUmV0aWN1bGF0ZSBwYWNrYWdlKSBvZiB0aGUgTW92aWVMZW5zIHB5dGhvbiBbbm90ZWJvb2tdKGh0dHBzOi8vZ2l0aHViLmNvbS9mYXN0YWkvZmFzdGFpL2Jsb2IvbWFzdGVyL2NvdXJzZXMvZGwxL2xlc3NvbjUtbW92aWVsZW5zLmlweW5iKSBjb3ZlcmVkIGluIHRoZSAgY291cnNlLiAgV2hpbGUgd2UgY2FuIGhhdmUgc2VwYXJhdGUgcHl0aG9uIGFuZCBSIGNodW5rcyB3aXRoIGludGVyb3BlcmFiaWxpdHkgdXNpbmcgUmV0aWN1bGF0ZSwgSSBoYXZlIHRyaWVkIHRvIGRvIGV2ZXJ5dGhpbmcgaW4gUiBzaW5jZSBpdCB3aWxsIGJlIGVhc2llciB0byB1c2UgdGhpcyBhcyBzdGFuZGFsb25lIFIgc2NyaXB0IGFsc28uIFRoaXMgY291cnNlIHVzZXMgYSBsaWJyYXJ5IGZhc3RhaSAod3JpdHRlbiBieSBKZXJlbXkpIHdoaWNoIGlzIGEgd3JhcHBlciBhcm91bmQgUHlUb3JjaC4gCgpJdCB3aWxsIGJlIGhlbHBmdWwgdG8gbGlzdGVuIHRvIHRoZSBsZWN0dXJlcyBiZWZvcmUgZ29pbmcgdGhyb3VnaCB0aGlzIG5vdGVib29rIHNpbmNlIHRoZSBjb25jZXB0cyBvZiB0aGUgbW9kZWwgYW5kIGFwcHJvYWNoIGFyZSBkaXNjdXNzZWQgaW4gdGhlIGxlY3R1cmUgYW5kIHRoaXMgbm90ZWJvb2sgaXMganVzdCBhIHJlcGxpY2F0aW9uIGF0dGVtcHQgb2YgdGhlIG1hdGVyaWFsIGZyb20gdGhlIGNvdXJzZSB1c2luZyBSLgoKIyMgSW5pdGlhbCBTZXR1cAoKYGBge3J9CiMgaW1wb3J0IFIgbGlicmFyaWVzCmxpYnJhcnkocmV0aWN1bGF0ZSkKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KGlybGJhKQpgYGAKCkluIHRoaXMgY291cnNlLCB0aGV5IHRhbGsgYWJvdXQgc2V2ZXJhbCBjbG91ZCBHUFUgb3B0aW9ucyBhbmQgcHJvdmlkZWQgYSBjb25kYSBlbnZpcm9ubWVudCBmYXN0YWkgaW4gdGhvc2UgZW52aXJvbm1lbnRzLiBJIGFtIHVzaW5nIHRoZSBQYXBlcnNwYWNlIHNldHVwIHRoYXQgaXMgY292ZXJlZCBpbiBbTGVzc29uIDFdKGh0dHA6Ly9jb3Vyc2UuZmFzdC5haS9sZXNzb25zL2xlc3NvbjEuaHRtbCkuIFJlc2hhbWEgU2hhaWsgaGFzIGFsc28gbGlzdGVkIGRldGFpbGVkIHVwdG8gZGF0ZSBbaW5zdHJ1Y3Rpb25zXShodHRwczovL2dpdGh1Yi5jb20vcmVzaGFtYXMvZmFzdGFpX2RlZXBsZWFybl9wYXJ0MS9ibG9iL21hc3Rlci90b29scy9wYXBlcnNwYWNlLm1kKSBvbiBnZXR0aW5nIHNldCB1cCBpbiBQYXBlcnNwYWNlLiBGb3Igc2V0dGluZyB1cCBSU3R1ZGlvIHNlcnZlciBpbiB0aGUgc2FtZSBtYWNoaW5lLCBJIGZvbGxvd2VkIENsb3VkIEdQVSBbc2V0dXAgc2VjdGlvbl0oaHR0cHM6Ly90ZW5zb3JmbG93LnJzdHVkaW8uY29tL3Rvb2xzL2Nsb3VkX2dwdS5odG1sKSBpbiBUZW5zb3JGbG93IGZvciBSIHNpdGUgKGV4Y2VwdCBmb3IgdGhlIEVDMiBwYXJ0KS4KClNpbmNlIHRoZSBtYWNoaW5lIGFscmVhZHkgY29tZXMgd2l0aCBmYXN0YWkgY29uZGEgZW52aXJvbm1lbnQsIHdlIGZpcnN0IG5lZWQgdG8gdXNlIHRoZSBweXRob24gdGhhdCBpcyBwYXJ0IG9mIHRoZSBmYXN0YWkgZW52aXJvbm1lbnQgYW5kIGFsc28gdXNlIHRoZSBmYXN0YWkgY29uZGEgZW52aXJvbm1lbnQuCgpgYGB7cn0KCnVzZV9weXRob24oIi9ob21lL3BhcGVyc3BhY2UvYW5hY29uZGEzL2VudnMvZmFzdGFpL2Jpbi9weXRob24iLCByZXF1aXJlZCA9IFRSVUUpCnVzZV9jb25kYWVudigiZmFzdGFpIikKcHlfY29uZmlnKCkKCm1haW4gPSBpbXBvcnRfbWFpbigpCmJpID0gaW1wb3J0X2J1aWx0aW5zKCkKYGBgCgpUaGUgZmFzdGFpIGxpYnJhcnkgaXMgbG9jYXRlZCBpbiB0aGUgZm9sZGVyIH4vZmFzdGFpLiBTbyBmaXJzdCwgd2UgaW1wb3J0IGZhc3RhaS5sZWFybmVyIGFuZCBmYXN0YWkuY29sdW1uZGF0YSAobGlicmFyaWVzIHVzZWQgaW4gdGhlIG5vdGVib29rKSBmcm9tIHRoZSBmb2xkZXIuCgpgYGB7cn0KIyBnZXQgcmVsZXZhbnQgcHl0aG9uIGltcG9ydHMKZnN0YWlfbGVhcm5lciA9IGltcG9ydF9mcm9tX3BhdGgoImZhc3RhaS5sZWFybmVyIiwgIi4uLy4uL2Zhc3RhaSIpCmZzdGFpX2NvbGRhdGEgPSBpbXBvcnRfZnJvbV9wYXRoKCJmYXN0YWkuY29sdW1uX2RhdGEiLCAiLi4vLi4vZmFzdGFpIikKYGBgCgpUaGVzZSBtb2R1bGVzIGltcG9ydCBzZXZlcmFsIG90aGVyIG1vZHVsZXMuIFNvIHRoZSBpbXBvcnQgY29tbWFuZCB1c2VkIGluIHRoZSBweXRob24gbm90ZWJvb2sgaXMgZGlyZWN0bHkgY2FsbGVkIGJlbG93IHNvIHRoYXQgYWxsIHRoZSBvdGhlciBtb2R1bGVzIGFyZSBhbHNvIGF2YWlsYWJsZSBmb3IgdXNlIGxhdGVyLgoKYGBge3J9CnB5X3J1bl9zdHJpbmcoIgpmcm9tIGZhc3RhaS5sZWFybmVyIGltcG9ydCAqCmZyb20gZmFzdGFpLmNvbHVtbl9kYXRhIGltcG9ydCAqCiAgICAgICAgICAgICAgIikKYGBgCgojIyBHZXQgRGF0YQoKVGhlIHJhdGluZ3MgZGF0YXNldCBoYXMgdGhlIHJhdGluZ3MgZm9yIGRpZmZlcmVudCB1c2VycyBhbmQgbW92aWVzLiBUaGUgbW92aWVzIGRhdGFzZXQgaGFzIHRoZSBtb3ZpZSB0aXRsZSBpbmZvcm1hdGlvbi4KCmBgYHtyfQpkYXRhcGF0aCA9ICIuLi8uLi9kYXRhL21sLWxhdGVzdC1zbWFsbC8iCgpyYXRpbmdzID0gcmVhZC5jc3YocGFzdGUwKGRhdGFwYXRoLCAicmF0aW5ncy5jc3YiKSwgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFKQpoZWFkKHJhdGluZ3MpCgptb3ZpZXMgPSByZWFkLmNzdihwYXN0ZTAoZGF0YXBhdGgsICJtb3ZpZXMuY3N2IiksIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkKaGVhZChtb3ZpZXMpCmBgYAoKIyMgTW9kZWwKCkVhY2ggdXNlciBpcyAkaSQgcmVwcmVzZW50IGJ5IGFuIGVtYmVkZGluZyB2ZWN0b3IgJHVfaSQgY29uc2lzdGluZyBvZiBgbl9mYWN0b3JgIHZhbHVlcyBhbmQgYSB1c2VyIGJpYXMgdmFsdWUgJHViX2kkLiBTaW1pbGFybHkgYSBtb3ZpZSAkaiQgaXMgcmVwcmVzZW50ZWQgYnkgYW4gZW1iZWRkaW5nIHZlY3RvciAkbV9qJCBjb25zaXN0aW5nIG9mIGBuX2ZhY3RvcmAgdmFsdWVzIGFuZCBtb3ZpZSBiaWFzIHZhbHVlICRtYl9qJC4gVGhlIG1vZGVsIG9mIHJhdGluZyAkcl97aWp9JCBnaXZlbiBieSB1c2VyICRpJCB0byBtb3ZpZSAkaiQgaXM6CiQkIHJfe2lqfSA9IHVfaV5Udl9qICsgYl9pICsgbV9qICQkCgojIyBNb2RlbCBGaXR0aW5nCgpGaXJzdCwgdGhlIHNldCBvZiB1c2VyL21vdmllIGNvbWJpbmF0aW9ucyB0aGF0IHdvdWxkIGJlIHVzZWQgYXMgdmFsaWRhdGlvbiBzZXQgaXMgZGV0ZXJtaW5lZC4gVGhlbiwgdGhlIGZvbGxvd2luZyBwYXJhbWV0ZXJzIGFyZSBzZXQ6CgoqIHdlaWdodCBkZWNheSAod2QpCiogTnVtYmVyIG9mIGZhY3RvcnMgaW4gZW1iZWRkaW5nIGBuX2ZhY3RvcnNgCgpGb3IgZmV3IG9mIHRoZSBjb2RlIHNuaXBwZXRzIGJlbG93LCBJIGhhdmUgaW5jbHVkZWQgdGhlIHB5dGhvbiBjb2RlIGluIGNvbW1lbnRzIHRvIHNob3cgdGhlIGNvcnJlc3BvbmRlbmNlIGJldHdlZW4gcHl0aG9uIGNvZGUgYW5kIFIgY29kZSB3aXRoIHJldGljdWxhdGUuIEluIG1vc3QgY2FzZXMsIEkganVzdCBoYWQgdG8gZG8gb25lIG9mIHRoZSBmb2xsb3dpbmcgdGhpbmdzIHRvIGdldCB0aGluZ3MgdG8gd29yazoKCiogUmVwbGFjZSBwZXJpb2QgaW4gcHl0aG9uIGNvZGUgd2l0aCBkb2xsYXIKKiBJZiBpdCBpcyBhIHB5dGhvbiB2YXJpYWJsZSBvciBmdW5jdGlvbiwgcHJlZml4IGEgYHB5JGAKKiBFeHBsaWNpdGx5IGNvbnZlcnQgYW4gUiBvYmplY3QgaW50byBweXRob24gb2JqZWN0IHdoZW4gcGFzc2luZyB0byBhIGZ1bmN0aW9uIHVzaW5nIGByX3RvX3B5YCBpZiBuZWVkZWQuCiogSWYgYSBweXRob24gZnVuY3Rpb24gcmVxdWlyZXMgYW4gaW50ZWdlciBhcyBhcmd1bWVudCwgdGhlbiBJIGVuc3VyZWQgdGhhdCBhbiBpbnRlZ2VyIGlzIHBhc3NlZCBhcyBhbiBhcmd1bWVudCAoZm9yIGV4YW1wbGUsIHBhc3NpbmcgYDJMYCBpbnN0ZWFkIG9mIGAyYCkKCmBgYHtyfQojIHB5dGhvbiBjb2RlCiMgdmFsX2lkeHMgPSBnZXRfY3ZfaWR4cyhsZW4ocmF0aW5ncykpCiMKdmFsX2lkeHMgPSBweSRnZXRfY3ZfaWR4cyhucm93KHJhdGluZ3MpKQp2YWxfaWR4cyA9IGFzLmludGVnZXIodmFsX2lkeHMpCndkID0gMmUtNApuX2ZhY3RvcnMgPSA1MEwKYGBgCgpOZXh0IGEgZGF0YSBsb2FkZXIgb2JqZWN0IGBjZmAgYW5kIGEgbGVhcm5lciBvYmplY3QgYGxlYXJuYCBpcyBjcmVhdGVkCmBgYHtyfQojIHB5dGhvbiBjb2RlCiMgY2YgPSBDb2xsYWJGaWx0ZXJEYXRhc2V0LmZyb21fY3N2KHBhdGgsICdyYXRpbmdzLmNzdicsICd1c2VySWQnLCAnbW92aWVJZCcsICdyYXRpbmcnKQojCmNmID0gcHkkQ29sbGFiRmlsdGVyRGF0YXNldCRmcm9tX2NzdihkYXRhcGF0aCwgJ3JhdGluZ3MuY3N2JywgJ3VzZXJJZCcsICdtb3ZpZUlkJywgJ3JhdGluZycpCgojIHB5dGhvbiBjb2RlCiMgbGVhcm4gPSBjZi5nZXRfbGVhcm5lcihuX2ZhY3RvcnMsIHZhbF9pZHhzLCA2NCwgb3B0X2ZuPW9wdGltLkFkYW0pCiMKbGVhcm4gPSBjZiRnZXRfbGVhcm5lcihuX2ZhY3RvcnMsIHZhbF9pZHhzLCA2NEwsIG9wdF9mbj1weSRvcHRpbSRBZGFtKQpgYGAKCldlIHJldmlldyB0aGUgbW9kZWwKYGBge3J9CmxlYXJuJG1vZGVsCmBgYAoKVGhlIG1vZGVsIGhhcyB0aGUgZm9sbG93aW5nIGNvbXBvbmVudHM6CgoqIFRoZSB2ZWN0b3Igb2YgYG5fZmFjdG9ycyA9IDUwYCBmb3IgZWFjaCBvZiBgbl91c2VycyA9IDY3MWAgaXMgcmVwcmVzZW50ZWQgYnkgYSBgbl91c2VycyB4IG5fZmFjdG9yc2AgZW1iZWRkaW5nIG1hdHJpeCBgdWAuCiogVGhlIHZlY3RvciBvZiBgbl9mYWN0b3JzID0gNTBgIGZvciBlYWNoIG9mIGBuX21vdmllcyA9IDkwNjZgIGlzIHJlcHJlc2VudGVkIGJ5IGEgYG5fbW92aWVzIHggbl9mYWN0b3JzYCBlbWJlZGRpbmcgbWF0cml4IGBpYC4KKiBVc2VycyBoYXZlIGEgYmlhcyB2ZWN0b3IgYHViYCBvZiBsZW5ndGggYG5fdXNlcnMgPSA2NzFgIGFuZCBtb3ZpZXMgaGF2ZSBhIGJpYXMgdmVjdG9yIGBpYmAgb2YgbGVuZ3RoIGBuX21vdmllcyA9IDkwNjZgLiAKCk5leHQgd2UgZml0IHRoZSBtb2RlbApgYGB7ciByZXN1bHRzPSJoaWRlIn0KIyBweXRob24gY29kZQojIGxlYXJuLmZpdCgxZS0yLCAyLCB3ZHM9d2QsIGN5Y2xlX2xlbj0xLCBjeWNsZV9tdWx0PTIpCiMKbWRsZml0ID0gbGVhcm4kZml0KDFlLTIsIDJMLCB3ZHM9d2QsIGN5Y2xlX2xlbj0xTCwgY3ljbGVfbXVsdD0yTCkKYGBgCgpJbiBKdXB5dGVyIG5vdGVib29rLCBhIHdpZGdldCBzaG93cyBhIG5pY2Ugb3V0cHV0IG9mIHByb2dyZXNzLiBUaGF0IG91dHB1dCBkb2Vzbid0IHJlbmRlciBwcm9wZXJseSB3aXRoaW4gUlN0dWRpbyBhbmQgdGhlIGh0bWwgZ2VuZXJhdGVkIGRvY3VtZW50IGhhcyB0b28gbXVjaCBvdXRwdXQuIEZvciBub3csIEkgaGF2ZSB0dXJuZWQgb2ZmIG91dHB1dCBhbmQgYW0ganVzdCBzdG9yaW5nIHRoZSBmaW5hbCB2YWxpZGF0aW9uIGxvc3MgbWV0cmljIChNU0UgbG9zcyBpbiB0aGlzIGNhc2UpCgpgYGB7cn0KcGFzdGUwKCJNU0Ugb2YgdmFsaWRhdGlvbiBzZXQgPSAiLCByb3VuZChtZGxmaXRbWzFdXSwgMykpCmBgYAoKTmV4dCB3ZSBjb21wYXJlIHRoZSBwcmVkaWN0ZWQgYW5kIGFjdHVhbCByYXRpbmdzIGZvciB0aGUgdmFsaWRhdGlvbiBzZXQKCmBgYHtyfQp5dmFsX3ByZWRzID0gbGVhcm4kcHJlZGljdCgpCnl2YWw9bGVhcm4kZGF0YSR2YWxfeQpgYGAKCmBgYHtyfQpkZnBsdCA9IGRhdGEuZnJhbWUoeXZhbCA9IHl2YWwsIHl2YWxfcHJlZHMgPSB5dmFsX3ByZWRzKQpnZ3Bsb3QoZGZwbHQpICsgZ2VvbV9oaXN0b2dyYW0oYWVzKHggPSB5dmFsX3ByZWRzKSkgKyBmYWNldF9ncmlkKHl2YWwgfiAuKSArIAogICB4bGFiKCJwcmVkaWN0ZWQgcmF0aW5ncyIpICsgdGhlbWVfYncoKQpgYGAKCiMjIEludGVycHJldGF0aW9uIG9mIE1vdmllIEJpYXMKVGhlIG1vZGVsIHVzZXMgYSBzZXF1ZW50aWFsIG51bWVyaWMgaWQgZm9yIHVzZXJzIGFuZCBtb3ZpZXMsIHdlIGZpcnN0IGdldCB0aGUgbW92aWUgdG8gaWQgbWFwcGluZyBmcm9tIHRoZSBkYXRhIG9iamVjdCBgY2ZgLiAKYGBge3J9Cm1vdmllMmlkeCA9IGNmJGl0ZW0yaWR4Cm1vdmllMmlkeCA9IHVubGlzdChtb3ZpZTJpZHgpCmhlYWQobW92aWUyaWR4KQpgYGAKCmBgYHtyfQppZHgybW92aWUgPSBhcy5udW1lcmljKG5hbWVzKG1vdmllMmlkeCkpCm5hbWVzKGlkeDJtb3ZpZSkgPSBtb3ZpZTJpZHgKaGVhZChpZHgybW92aWUpCmBgYAoKR2V0IHRoZSB0b3AgMzAwMCBtb3ZpZXMgdGhhdCBnb3QgdGhlIG1vc3QgcmF0aW5ncwpgYGB7cn0KdG9wbW92aWVzID0gcmF0aW5ncyAlPiUgZ3JvdXBfYnkobW92aWVJZCkgJT4lIHN1bW1hcml6ZShjbnQgPSBuKCkpICU+JSBhcnJhbmdlKGRlc2MoY250KSkgJT4lIHNsaWNlKDE6MzAwMCkKdG9wbW92aWVzaWR4ID0gbW92aWUyaWR4W2FzLmNoYXJhY3Rlcih0b3Btb3ZpZXMkbW92aWVJZCldCnRvcG1vdmllc2lkeCA9IG5wX2FycmF5KHRvcG1vdmllc2lkeCkKYGBgCgpHZXQgdGhlIG1vdmllIGJpYXMgdmFyaWFibGUgZm9yIHRoZSB0b3AgMzAwMCBtb3ZpZXMKYGBge3J9CiMgcHl0aG9uIGNvZGUKIyBtPWxlYXJuLm1vZGVsCiMgbW92aWVfYmlhcyA9IHRvX25wKG0uaWIoVih0b3BNb3ZpZUlkeCkpKQojCm0gPSBsZWFybiRtb2RlbAptb3ZpZV9iaWFzID0gcHkkdG9fbnAobSRpYihweSRWKHRvcG1vdmllc2lkeCkpKQptb3ZpZV9iaWFzWzE6MjBdCmBgYAoKYGBge3J9CnRvcG1vdmllcyRtb3ZpZV9iaWFzID0gbW92aWVfYmlhc1ssMV0KdG9wbW92aWVzID0gbGVmdF9qb2luKHRvcG1vdmllcywgbW92aWVzICU+JSBzZWxlY3QobW92aWVJZCwgdGl0bGUpLCBieSA9ICJtb3ZpZUlkIikKYGBgCgpGaW5kIHRoZSBsb3dlc3QgcmF0ZWQgbW92aWVzIChsb3dlc3QgdmFsdWVzIG9mIG1vdmllIGJpYXMpLiAKYGBge3J9CnRvcG1vdmllcyAlPiUgYXJyYW5nZShtb3ZpZV9iaWFzKSAlPiUgc2xpY2UoMToxNSkKYGBgCgpGaW5kIHRoZSBoaWdoZXN0IHJhdGVkIG1vdmllcyAoaGlnaGVzdCB2YWx1ZXMgb2YgbW92aWUgYmlhcykKYGBge3J9CnRvcG1vdmllcyAlPiUgYXJyYW5nZShkZXNjKG1vdmllX2JpYXMpKSAlPiUgc2VsZWN0KHRpdGxlKSAlPiUgc2xpY2UoMToxNSkKYGBgCgojIyBJbnRlcnByZXRhdGlvbiBvZiBFbWJlZGRpbmdzCgpHZXQgdGhlIGVtYmVkZGluZ3Mgb2YgbW92aWVzCmBgYHtyfQojIHB5dGhvbiBjb2RlCiMgbW92aWVfZW1iID0gdG9fbnAobS5pKFYodG9wTW92aWVJZHgpKSkKIwptb3ZpZV9lbWIgPSBweSR0b19ucChtJGkocHkkVih0b3Btb3ZpZXNpZHgpKSkKZGltKG1vdmllX2VtYikKYGBgCgpTaW5jZSB0aGVyZSBhcmUgNTAgZGltZW5zaW9ucywgYSBQQ0EgaXMgZG9uZSB0byBleGFtaW5lIHRoZSBmaXJzdCB0d28gcHJpbmNpcGFsIGNvbXBvbmVudHMuIAoKYGBge3J9CnBjYW1kbCA9IHByY29tcF9pcmxiYShtb3ZpZV9lbWIsIG4gPSAyKQpsb2FkaW5nc19kZiA9IGRhdGEuZnJhbWUocGNhbWRsJHJvdGF0aW9uKQpzY29yZXNfZGYgPSBkYXRhLmZyYW1lKHBjYW1kbCR4KQpgYGAKCgpgYGB7cn0Kc2NvcmVzX2RmJG1vdmllaWR4ID0gcHlfdG9fcih0b3Btb3ZpZXNpZHgpCnNjb3Jlc19kZiRtb3ZpZUlkID0gaWR4Mm1vdmllW2FzLmNoYXJhY3RlcihzY29yZXNfZGYkbW92aWVpZHgpXQpzY29yZXNfZGYgPSBpbm5lcl9qb2luKHNjb3Jlc19kZiwgbW92aWVzICU+JSBzZWxlY3QobW92aWVJZCwgdGl0bGUpLCBieSA9ICJtb3ZpZUlkIikKYGBgCgpDaGVjayB0aGUgbW92aWVzIHRoYXQgaGF2ZSBoaWdoZXN0IGFuZCBsb3dlc3Qgc2NvcmVzIGZvciBQQzEuCmBgYHtyfQpzY29yZXNfZGYgJT4lIGFycmFuZ2UoUEMxKSAlPiUgc2xpY2UoMToxMCkgJT4lIHNlbGVjdCh0aXRsZSkKc2NvcmVzX2RmICU+JSBhcnJhbmdlKGRlc2MoUEMxKSkgJT4lIHNsaWNlKDE6MTApICU+JSBzZWxlY3QodGl0bGUpCmBgYAoKQ2hlY2sgdGhlIG1vdmllcyB0aGF0IGhhdmUgaGlnaGVzdCBhbmQgbG93ZXN0IHNjb3JlcyBmb3IgUEMyLgpgYGB7cn0Kc2NvcmVzX2RmICU+JSBhcnJhbmdlKFBDMikgJT4lIHNsaWNlKDE6MTApICU+JSBzZWxlY3QodGl0bGUpCnNjb3Jlc19kZiAlPiUgYXJyYW5nZShkZXNjKFBDMikpICU+JSBzbGljZSgxOjEwKSAlPiUgc2VsZWN0KHRpdGxlKQpgYGAKClRoZSBhYm92ZSByZXN1bHRzIGNvdWxkIGJlIHVzZWQgdG8gYXNzaWduIHNvbWUgc29ydCBvZiBtZWFuaW5nIHRvIHRoZSBmaXJzdCAyIHByaW5jaXBhbCBjb21wb25lbnRzLiBOZXh0IGEgc2NhdHRlciBwbG90IG9mIFBDMSB2cyBQQzIgd2l0aCBsYWJlbHMgb2YgbW92aWVzIGlzIHNob3duLiBUaGlzIGNvdWxkIGJlIHVzZWQgdG8gY2hlY2sgd2hpY2ggbW92aWVzIGNsdXN0ZXIgdG9nZXRoZXIuCgpgYGB7cn0Kc2V0LnNlZWQoMTIzNDUpCnNjb3Jlc19kZl9zYW1wID0gc2NvcmVzX2RmICU+JSBzYW1wbGVfbig1MCkKZ2dwbG90KHNjb3Jlc19kZl9zYW1wKSArIGdlb21fcG9pbnQoYWVzKHggPSBQQzEsIHkgPSBQQzIpKSArIAogICAgZ2VvbV90ZXh0KGFlcyh4ID0gUEMxLCB5ID0gUEMyLCBsYWJlbCA9IHRpdGxlKSwgc2l6ZSA9IDIuNSwgaGp1c3QgPSAwLCBudWRnZV94ID0gMC4wMikgKwogICAgIHRoZW1lX2J3KCkKYGBgCgojIyBTdW1tYXJ5CgpSZXRpY3VsYXRlIHBhY2thZ2UgaXMgYSBncmVhdCBhZGRpdGlvbiB0byBSLiBXb3JraW5nIHRocm91Z2ggdGhpcyBleGFtcGxlIHNob3dlZCB0aGF0IGl0IGlzIG5vdCB0b28gaGFyZCB0byBkZXZlbG9wIGEgUiB2ZXJzaW9uIG9mIHRoZSBhbmFseXNpcyBpbiBweXRob24gdGhhbmtzIHRvIHJldGljdWxhdGUuODkwb3BrbCxtCgpgYGB7cn0Kc2Vzc2lvbkluZm8oKQpgYGA=