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:
- weight decay (wd)
- Number of factors in embedding
n_factors
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:
- Replace period in python code with dollar
- If it is a python variable or function, prefix a
py$
- Explicitly convert an R object into python object when passing to a function using
r_to_py
if needed.
- If a python function requires an integer as argument, then I ensured that an integer is passed as an argument (for example, passing
2L
instead of 2
)
# 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:
- The vector of
n_factors = 50
for each of n_users = 671
is represented by a n_users x n_factors
embedding matrix u
.
- The vector of
n_factors = 50
for each of n_movies = 9066
is represented by a n_movies x n_factors
embedding matrix i
.
- Users have a bias vector
ub
of length n_users = 671
and movies have a bias vector ib
of length n_movies = 9066
.
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=