1 Introduction

In this project, we are going to review the Matrix Factorization methods with the same data set we have used in the earlier project (MovieLense). In the previous project, while we were creating the Movie Ratings Matrix, we used “realRatingMatrix” class“. More details on”realRatingMatrix" can be found here: https://www.rdocumentation.org/packages/recommenderlab/versions/0.2-5/topics/realRatingMatrix .With this project, we are going to implement SVD with using realRatingMatrix class and recommenderlab package and keeping the matrix as sparse matrix of class dgCMatrix and replacing the NA(or 0) values with calculating the baseline predictor.

# Load Libraries

library(tidyverse)
## -- Attaching packages -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.2.1     v purrr   0.3.3
## v tibble  2.1.3     v dplyr   0.8.3
## v tidyr   1.0.0     v stringr 1.4.0
## v readr   1.3.1     v forcats 0.4.0
## -- Conflicts ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
library(kableExtra)
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
library(recommenderlab)
## Warning: package 'recommenderlab' was built under R version 3.6.3
## Loading required package: Matrix
## 
## Attaching package: 'Matrix'
## The following objects are masked from 'package:tidyr':
## 
##     expand, pack, unpack
## Loading required package: arules
## 
## Attaching package: 'arules'
## The following object is masked from 'package:dplyr':
## 
##     recode
## The following objects are masked from 'package:base':
## 
##     abbreviate, write
## Loading required package: proxy
## Warning: package 'proxy' was built under R version 3.6.3
## 
## Attaching package: 'proxy'
## The following object is masked from 'package:Matrix':
## 
##     as.matrix
## The following objects are masked from 'package:stats':
## 
##     as.dist, dist
## The following object is masked from 'package:base':
## 
##     as.matrix
## Loading required package: registry
## Registered S3 methods overwritten by 'registry':
##   method               from 
##   print.registry_field proxy
##   print.registry_entry proxy
library(ggplot2)
library(caTools)

2 Data Collection

Similar to project 2, we will load the MoviLense and create our Matrix

set.seed(1)
data("MovieLense")
MovieLense
## 943 x 1664 rating matrix of class 'realRatingMatrix' with 99392 ratings.
# create the matrix
movie_lense_matrix <- MovieLense@data

3 Model Development Approach 1

3.1 Matrix Factorization

Users and items gets modeled to a joint latent factor space of dimensionality f so that the user-item iteractions are modeled as the inner products within the space f. 

\(i --> item\)

\(q_{i} --> item vector\)

\(p_{u} --> user vector\)

For a given item \(i\), the elements of \(q_{i}\) measure the extent to which the item possesses those factors positive or negative. For a given user u, the elements of \(p_{u}\) measure the extent of interest the user has in items that are high on the corresponding factors, again, positive or negative. The resulting dot product, \(q_{i}^{T}\)\(p_{u}\), captures the interaction between user \(u\) and item \(i\) —the user’s overall interest in the item’s characteristics. This approximates user \(u\)’s rating of item i, which is denoted by \(r_{ui}\), leading to the estimate \(r_{ui}=q_{i}^{T}p_{u}\) . This model would be singular value decomposition(SVD) an approach to idenfitfy latent sementic factors.

In a nutshell, SVD is

\(R=PAQ^{T}\) –> We have three matrices, P, A, Q where we multiply them we get the matrix of R. R is m * n ratings matrix, P is m * k user feature affinity matrix, Q is n * k item feature relevance matrix and A is k * k diagonal feature weight matrix. The R , original matrix can be estimated by the product of all these matrices.

** SVD describes preference in terms of latent features

** These features are learned from the rating data

** As explained in the begining, defines a shared vector space for users and items.

We created the movie_lense_matrix, we know from previous project that we need to subset.

# subset the dataset
movies_1 <- movie_lense_matrix[rowCounts(MovieLense) > 100, colCounts(MovieLense) > 100]
#replace 0 with NA
movies_1[,][movies_1[,] == 0] <- NA
summary(as.vector(as.matrix(movies_1)))
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##    1.00    3.00    4.00    3.69    4.00    5.00   73782

3.2 Data Preperation

SVD requires that there are no missing values. In our subset matrix, we have 73,782 missing values that we can replace them with the mean value. We can also use the baseline predictor approach. We calculate the user and item biases in the matrix, then replace the missing values with the sum of the raw mean, user and item biases.

# get mean value of the matrix
raw_mean <- mean(as.vector(as.matrix(movies_1)), na.rm = TRUE )
raw_mean
## [1] 3.69468
# count number of non-NA's in each row of training set
row_valid <- rowSums(!is.na(movies_1[,]))

# count number of non-NA's in each column of training set
col_valid <- colSums(!is.na(movies_1[,]))

# calculate user biases
user_biases <- rowMeans(movies_1[,] - raw_mean, na.rm = TRUE) / row_valid

# calculate item biases
item_biases <- colMeans(movies_1[,] - raw_mean, na.rm = TRUE) / col_valid

# memory cleanup
rm(row_valid, col_valid)
for (i in 1:nrow(movies_1)) {
  for (j in 1:ncol(movies_1)) {
    
    # if the matrix element has an NA, fill in with baseline predictor
    if(is.na(movies_1[i,j])) {
          movies_1[i,j] <- raw_mean + user_biases[i] + item_biases[j]
          
          # ensure new values are within valid ratings bounds
          if (movies_1[i,j] > 5) movies_1[i,j] <- 5
          if (movies_1[i,j] < 0) movies_1[i,j] <- 0
    } # end if
    
  } # end for j
} # end for i
summary(as.vector(as.matrix(movies_1)))
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   1.000   3.688   3.695   3.694   3.704   5.000
summary(user_biases)
##       Min.    1st Qu.     Median       Mean    3rd Qu.       Max. 
## -2.457e-02 -1.888e-03  3.498e-04 -5.813e-05  2.328e-03  1.136e-02
summary(item_biases)
##       Min.    1st Qu.     Median       Mean    3rd Qu.       Max. 
## -0.0213567 -0.0028758 -0.0002285 -0.0010286  0.0015623  0.0106482

We handled the missing values and now we can start calculating the SVD.

rank <- qr(as.matrix(movies_1))$rank
rank
## [1] 332

The matrix has 332 columns, our subset matrix is 358x332. Referencing the earlier approach of \(R=PAQ^{T}\) .

  • Matrix P will be 358 * 332

  • Matrix A will be 332 * 332

  • Matrix Q will be 332 * 332

Calculating the SVD.

# calculate svd
movies_1_svd <- svd(as.matrix(movies_1))
plot(movies_1_svd$d)

The singular values are low throughout the 0 to 300.

3.3 Dimensionality Reduction

Singular value decompoisition allows an exact representation of any matrix, and also allows us to eliminate the less importatn features that representation to produce an approximate representation with any desired number of dimensions. The fewer the dimensions we choose, the less accurate will be the approximation. Let’s say we have a huge matrix R with its components P, A and Q. They are all large. The best way to reduce the dimensionality of the three matrices would be to set the smallest of the singular values to zero. If we sent the smallest singular values to 0, then we can also eliminate the corresponding columns of P and Q. We can sum the squares of each singular value and then identify the first \(k\) singular values within matrix A. Based on the singular values plotted, we can see that it will around the start of the singular values.

# sum of squares of all singular values
sum_squares <- sum(movies_1_svd$d^2)
sum_squares
## [1] 1671774
#checksum of squares for singular values
perc_vec <- NULL
for (i in 1:length(movies_1_svd$d)) {
  perc_vec[i] <- sum(movies_1_svd$d[1:i]^2) / sum_squares
}
plot(perc_vec)

k <- length(perc_vec[perc_vec <= .99])
k
## [1] 64

We can see that first 64 singular values whose squares sum to at least 99% of the total of the sum of squares of all the singular values. Let’s calculate our \(PRQ_{T}\) matrices.

# calculate size of A matrix
A_k <- Diagonal(x = movies_1_svd$d[1:k])
#calculate P matrix
P_k <- movies_1_svd$u[, 1:k]
#calculate V matrix (transpose of V matrix)
Q_k <- t(movies_1_svd$v)[1:k,]
#product of all these matrices will give us the estimated matrix
predicted <- P_k %*% A_k %*% Q_k
predicted[0:5]
## [1] 5.571451 3.882264 3.446047 3.431647 3.845288

We see the values are higher than 5. Let’s set all the ratings within 0 and 5.

# set all vals > 0 to 5
predicted[,][predicted[,] > 5] <- 5

# set all vals < -10 to -10
predicted[,][predicted[,] < 0] <- 0
predicted[0:5]
## [1] 5.000000 3.882264 3.446047 3.431647 3.845288
predicted_matrix <- as.matrix(predicted)

4 Model Development Approach 2

4.1 Single Value Decomposition - Recommender Package -

As a second approach, we are going implement single value decompposition to the MovieLens Dataset in a different way where we will use recommenderlab package. In our first approach, we created our matrix, handled missing values and calculated each matrix and further calculated the predicted matrix with the product of all three matrices. \(R=PAQ^{T}\) . In this approach, we are going to convert our Movie Lense matrix into “realRatingMatrix” and further implement SVD.

# create the matrix
movie_lense_matrix_2 <- as(as.matrix(movie_lense_matrix), "realRatingMatrix")
# subset the data
# subset the dataset
movies_2 <- movie_lense_matrix_2[rowCounts(MovieLense) > 100, colCounts(MovieLense) > 100]
# create the svd model
model_svd <- Recommender(movies_2, method="SVD", parameter=list(k=64))
predicted_svd <- predict(model_svd, newdata=movies_2, type="ratings")
predicted_svd[0:5,]
## 5 x 332 rating matrix of class 'realRatingMatrix' with 0 ratings.

5 Conclusion

Based on our two Single Value Decomposition methods we created, we can see that basic matrix factorizational model using SVD can be proper way of creating recommenders system. It is scalable however it is important to handle the missing values properly.

5.1 References:

(Reference: https://www.coursera.org/lecture/matrix-factorization/singular-value-decomposition-K5NBy )

(Reference: https://datajobs.com/data-science-repo/Recommender-Systems-%5BNetflix%5D.pdf )

(Reference: Mining of Massive Data Sets - 11.3 Signular-Value Decomposition)

LS0tDQp0aXRsZTogIk1hdHJpeCBGYWN0b3JpemF0aW9uIC0gUHJvamVjdCAzIg0KYXV0aG9yOiBBbmlsIEFreWlsZGlyaW0NCmRhdGU6ICIwNi8yMS8yMDIwIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50Og0KICAgIGNvZGVfZG93bmxvYWQ6IHllcw0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogcHlnbWVudHMNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiBmbGF0bHkNCiAgICB0b2M6IHllcw0KICAgIHRvY19mbG9hdDogeWVzDQogIHBkZl9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KLS0tDQoNCiMgSW50cm9kdWN0aW9uDQoNCkluIHRoaXMgcHJvamVjdCwgd2UgYXJlIGdvaW5nIHRvIHJldmlldyB0aGUgTWF0cml4IEZhY3Rvcml6YXRpb24gbWV0aG9kcyB3aXRoIHRoZSBzYW1lIGRhdGEgc2V0IHdlIGhhdmUgdXNlZCBpbiB0aGUgZWFybGllciBwcm9qZWN0IChNb3ZpZUxlbnNlKS4gSW4gdGhlIHByZXZpb3VzIHByb2plY3QsIHdoaWxlIHdlIHdlcmUgY3JlYXRpbmcgdGhlIE1vdmllIFJhdGluZ3MgTWF0cml4LCB3ZSB1c2VkICAicmVhbFJhdGluZ01hdHJpeCIgY2xhc3MiLiBNb3JlIGRldGFpbHMgb24gInJlYWxSYXRpbmdNYXRyaXgiIGNhbiBiZSBmb3VuZCBoZXJlOiBodHRwczovL3d3dy5yZG9jdW1lbnRhdGlvbi5vcmcvcGFja2FnZXMvcmVjb21tZW5kZXJsYWIvdmVyc2lvbnMvMC4yLTUvdG9waWNzL3JlYWxSYXRpbmdNYXRyaXggLldpdGggdGhpcyBwcm9qZWN0LCB3ZSBhcmUgZ29pbmcgdG8gaW1wbGVtZW50IFNWRCB3aXRoIHVzaW5nIHJlYWxSYXRpbmdNYXRyaXggY2xhc3MgYW5kIHJlY29tbWVuZGVybGFiIHBhY2thZ2UgYW5kIGtlZXBpbmcgdGhlIG1hdHJpeCBhcyBzcGFyc2UgbWF0cml4IG9mIGNsYXNzIGRnQ01hdHJpeCBhbmQgcmVwbGFjaW5nIHRoZSBOQShvciAwKSB2YWx1ZXMgd2l0aCBjYWxjdWxhdGluZyB0aGUgYmFzZWxpbmUgcHJlZGljdG9yLg0KDQpgYGB7cn0NCiMgTG9hZCBMaWJyYXJpZXMNCg0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGthYmxlRXh0cmEpDQpsaWJyYXJ5KHJlY29tbWVuZGVybGFiKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShjYVRvb2xzKQ0KDQpgYGANCg0KIyBEYXRhIENvbGxlY3Rpb24NCg0KU2ltaWxhciB0byBwcm9qZWN0IDIsIHdlIHdpbGwgbG9hZCB0aGUgTW92aUxlbnNlIGFuZCBjcmVhdGUgb3VyIE1hdHJpeA0KDQoNCmBgYHtyfQ0KDQpzZXQuc2VlZCgxKQ0KZGF0YSgiTW92aWVMZW5zZSIpDQpNb3ZpZUxlbnNlDQoNCmBgYA0KDQoNCmBgYHtyfQ0KDQojIGNyZWF0ZSB0aGUgbWF0cml4DQptb3ZpZV9sZW5zZV9tYXRyaXggPC0gTW92aWVMZW5zZUBkYXRhDQpgYGANCg0KIyBNb2RlbCBEZXZlbG9wbWVudCBBcHByb2FjaCAxIA0KDQojIyBNYXRyaXggRmFjdG9yaXphdGlvbg0KDQpVc2VycyBhbmQgaXRlbXMgZ2V0cyBtb2RlbGVkIHRvIGEgam9pbnQgbGF0ZW50IGZhY3RvciBzcGFjZSBvZiBkaW1lbnNpb25hbGl0eSBmIHNvIHRoYXQgdGhlIHVzZXItaXRlbSBpdGVyYWN0aW9ucyBhcmUgbW9kZWxlZCBhcyB0aGUgaW5uZXIgcHJvZHVjdHMgd2l0aGluIHRoZSBzcGFjZSBmLiANCg0KJGkgLS0+IGl0ZW0kDQoNCiRxX3tpfSAtLT4gaXRlbSB2ZWN0b3IkDQoNCiRwX3t1fSAtLT4gdXNlciB2ZWN0b3IkDQoNCkZvciBhIGdpdmVuIGl0ZW0gJGkkLCB0aGUgZWxlbWVudHMgb2YgJHFfe2l9JCBtZWFzdXJlIHRoZSBleHRlbnQgdG8gd2hpY2ggdGhlIGl0ZW0gcG9zc2Vzc2VzIHRob3NlIGZhY3RvcnMgcG9zaXRpdmUgb3IgbmVnYXRpdmUuIEZvciBhIGdpdmVuIHVzZXIgdSwgdGhlIGVsZW1lbnRzIG9mICRwX3t1fSQgbWVhc3VyZSB0aGUgZXh0ZW50IG9mIGludGVyZXN0IHRoZSB1c2VyIGhhcyBpbiBpdGVtcyB0aGF0IGFyZSBoaWdoIG9uIHRoZSBjb3JyZXNwb25kaW5nIGZhY3RvcnMsIGFnYWluLCBwb3NpdGl2ZSBvciBuZWdhdGl2ZS4gVGhlIHJlc3VsdGluZyBkb3QgcHJvZHVjdCwgJHFfe2l9XntUfSQkcF97dX0kLCBjYXB0dXJlcyB0aGUgaW50ZXJhY3Rpb24gYmV0d2VlbiB1c2VyICR1JCBhbmQgaXRlbSAkaSQg4oCUdGhlIHVzZXLigJlzIG92ZXJhbGwgaW50ZXJlc3QgaW4gdGhlIGl0ZW3igJlzIGNoYXJhY3RlcmlzdGljcy4gVGhpcyBhcHByb3hpbWF0ZXMgdXNlciAkdSTigJlzIHJhdGluZyBvZiBpdGVtIGksIHdoaWNoIGlzIGRlbm90ZWQgYnkgJHJfe3VpfSQsIGxlYWRpbmcgdG8gdGhlIGVzdGltYXRlICRyX3t1aX09cV97aX1ee1R9cF97dX0kIC4gVGhpcyBtb2RlbCB3b3VsZCBiZSBzaW5ndWxhciB2YWx1ZSBkZWNvbXBvc2l0aW9uKFNWRCkgYW4gYXBwcm9hY2ggdG8gaWRlbmZpdGZ5IGxhdGVudCBzZW1lbnRpYyBmYWN0b3JzLiANCg0KSW4gYSBudXRzaGVsbCwgU1ZEIGlzDQoNCiRSPVBBUV57VH0kIC0tPiBXZSBoYXZlIHRocmVlIG1hdHJpY2VzLCBQLCBBLCBRIHdoZXJlIHdlIG11bHRpcGx5IHRoZW0gd2UgZ2V0IHRoZSBtYXRyaXggb2YgUi4gUiBpcyBtICogbiByYXRpbmdzIG1hdHJpeCwgUCBpcyBtICogayB1c2VyIGZlYXR1cmUgYWZmaW5pdHkgbWF0cml4LCBRIGlzIG4gKiBrIGl0ZW0gZmVhdHVyZSByZWxldmFuY2UgbWF0cml4IGFuZCBBIGlzIGsgKiBrIGRpYWdvbmFsIGZlYXR1cmUgd2VpZ2h0IG1hdHJpeC4gVGhlIFIgLCBvcmlnaW5hbCBtYXRyaXggY2FuIGJlIGVzdGltYXRlZCBieSB0aGUgcHJvZHVjdCBvZiBhbGwgdGhlc2UgbWF0cmljZXMuIA0KDQoqKiBTVkQgZGVzY3JpYmVzIHByZWZlcmVuY2UgaW4gdGVybXMgb2YgbGF0ZW50IGZlYXR1cmVzDQoNCioqIFRoZXNlIGZlYXR1cmVzIGFyZSBsZWFybmVkIGZyb20gdGhlIHJhdGluZyBkYXRhIA0KDQoqKiBBcyBleHBsYWluZWQgaW4gdGhlIGJlZ2luaW5nLCBkZWZpbmVzIGEgc2hhcmVkIHZlY3RvciBzcGFjZSBmb3IgdXNlcnMgYW5kIGl0ZW1zLg0KDQoNCldlIGNyZWF0ZWQgdGhlIG1vdmllX2xlbnNlX21hdHJpeCwgd2Uga25vdyBmcm9tIHByZXZpb3VzIHByb2plY3QgdGhhdCB3ZSBuZWVkIHRvIHN1YnNldC4gDQoNCg0KDQpgYGB7cn0NCiMgc3Vic2V0IHRoZSBkYXRhc2V0DQptb3ZpZXNfMSA8LSBtb3ZpZV9sZW5zZV9tYXRyaXhbcm93Q291bnRzKE1vdmllTGVuc2UpID4gMTAwLCBjb2xDb3VudHMoTW92aWVMZW5zZSkgPiAxMDBdDQoNCmBgYA0KDQpgYGB7cn0NCiNyZXBsYWNlIDAgd2l0aCBOQQ0KbW92aWVzXzFbLF1bbW92aWVzXzFbLF0gPT0gMF0gPC0gTkENCg0KYGBgDQoNCg0KYGBge3J9DQoNCnN1bW1hcnkoYXMudmVjdG9yKGFzLm1hdHJpeChtb3ZpZXNfMSkpKQ0KDQoNCmBgYA0KDQoNCiMjIERhdGEgUHJlcGVyYXRpb24NCg0KU1ZEIHJlcXVpcmVzIHRoYXQgdGhlcmUgYXJlIG5vIG1pc3NpbmcgdmFsdWVzLiBJbiBvdXIgc3Vic2V0IG1hdHJpeCwgd2UgaGF2ZSA3Myw3ODIgbWlzc2luZyB2YWx1ZXMgdGhhdCB3ZSBjYW4gcmVwbGFjZSB0aGVtIHdpdGggdGhlIG1lYW4gdmFsdWUuIFdlIGNhbiBhbHNvIHVzZSB0aGUgYmFzZWxpbmUgcHJlZGljdG9yIGFwcHJvYWNoLiBXZSBjYWxjdWxhdGUgdGhlIHVzZXIgYW5kIGl0ZW0gYmlhc2VzIGluIHRoZSBtYXRyaXgsIHRoZW4gcmVwbGFjZSB0aGUgbWlzc2luZyB2YWx1ZXMgd2l0aCB0aGUgc3VtIG9mIHRoZSByYXcgbWVhbiwgdXNlciBhbmQgaXRlbSBiaWFzZXMuIA0KDQoNCmBgYHtyfQ0KDQojIGdldCBtZWFuIHZhbHVlIG9mIHRoZSBtYXRyaXgNCnJhd19tZWFuIDwtIG1lYW4oYXMudmVjdG9yKGFzLm1hdHJpeChtb3ZpZXNfMSkpLCBuYS5ybSA9IFRSVUUgKQ0KcmF3X21lYW4NCg0KYGBgDQoNCg0KYGBge3J9DQojIGNvdW50IG51bWJlciBvZiBub24tTkEncyBpbiBlYWNoIHJvdyBvZiB0cmFpbmluZyBzZXQNCnJvd192YWxpZCA8LSByb3dTdW1zKCFpcy5uYShtb3ZpZXNfMVssXSkpDQoNCiMgY291bnQgbnVtYmVyIG9mIG5vbi1OQSdzIGluIGVhY2ggY29sdW1uIG9mIHRyYWluaW5nIHNldA0KY29sX3ZhbGlkIDwtIGNvbFN1bXMoIWlzLm5hKG1vdmllc18xWyxdKSkNCg0KIyBjYWxjdWxhdGUgdXNlciBiaWFzZXMNCnVzZXJfYmlhc2VzIDwtIHJvd01lYW5zKG1vdmllc18xWyxdIC0gcmF3X21lYW4sIG5hLnJtID0gVFJVRSkgLyByb3dfdmFsaWQNCg0KIyBjYWxjdWxhdGUgaXRlbSBiaWFzZXMNCml0ZW1fYmlhc2VzIDwtIGNvbE1lYW5zKG1vdmllc18xWyxdIC0gcmF3X21lYW4sIG5hLnJtID0gVFJVRSkgLyBjb2xfdmFsaWQNCg0KIyBtZW1vcnkgY2xlYW51cA0Kcm0ocm93X3ZhbGlkLCBjb2xfdmFsaWQpDQoNCmBgYA0KDQoNCg0KYGBge3J9DQpmb3IgKGkgaW4gMTpucm93KG1vdmllc18xKSkgew0KICBmb3IgKGogaW4gMTpuY29sKG1vdmllc18xKSkgew0KICAgIA0KICAgICMgaWYgdGhlIG1hdHJpeCBlbGVtZW50IGhhcyBhbiBOQSwgZmlsbCBpbiB3aXRoIGJhc2VsaW5lIHByZWRpY3Rvcg0KICAgIGlmKGlzLm5hKG1vdmllc18xW2ksal0pKSB7DQogICAgICAgICAgbW92aWVzXzFbaSxqXSA8LSByYXdfbWVhbiArIHVzZXJfYmlhc2VzW2ldICsgaXRlbV9iaWFzZXNbal0NCiAgICAgICAgICANCiAgICAgICAgICAjIGVuc3VyZSBuZXcgdmFsdWVzIGFyZSB3aXRoaW4gdmFsaWQgcmF0aW5ncyBib3VuZHMNCiAgICAgICAgICBpZiAobW92aWVzXzFbaSxqXSA+IDUpIG1vdmllc18xW2ksal0gPC0gNQ0KICAgICAgICAgIGlmIChtb3ZpZXNfMVtpLGpdIDwgMCkgbW92aWVzXzFbaSxqXSA8LSAwDQogICAgfSAjIGVuZCBpZg0KICAgIA0KICB9ICMgZW5kIGZvciBqDQp9ICMgZW5kIGZvciBpDQoNCmBgYA0KDQoNCmBgYHtyfQ0Kc3VtbWFyeShhcy52ZWN0b3IoYXMubWF0cml4KG1vdmllc18xKSkpDQoNCg0KYGBgDQoNCg0KYGBge3J9DQoNCnN1bW1hcnkodXNlcl9iaWFzZXMpDQoNCg0KYGBgDQoNCmBgYHtyfQ0KDQpzdW1tYXJ5KGl0ZW1fYmlhc2VzKQ0KDQoNCmBgYA0KDQpXZSBoYW5kbGVkIHRoZSBtaXNzaW5nIHZhbHVlcyBhbmQgbm93IHdlIGNhbiBzdGFydCBjYWxjdWxhdGluZyB0aGUgU1ZELg0KDQoNCmBgYHtyfQ0KDQpyYW5rIDwtIHFyKGFzLm1hdHJpeChtb3ZpZXNfMSkpJHJhbmsNCnJhbmsNCg0KDQpgYGANCg0KVGhlIG1hdHJpeCBoYXMgMzMyIGNvbHVtbnMsIG91ciBzdWJzZXQgbWF0cml4IGlzIDM1OHgzMzIuIFJlZmVyZW5jaW5nIHRoZSBlYXJsaWVyIGFwcHJvYWNoIG9mICRSPVBBUV57VH0kIC4NCg0KKiBNYXRyaXggUCB3aWxsIGJlIDM1OCAqIDMzMiANCg0KKiBNYXRyaXggQSB3aWxsIGJlIDMzMiAqIDMzMiANCg0KKiBNYXRyaXggUSB3aWxsIGJlIDMzMiAqIDMzMg0KDQoNCkNhbGN1bGF0aW5nIHRoZSBTVkQuDQoNCg0KYGBge3J9DQojIGNhbGN1bGF0ZSBzdmQNCm1vdmllc18xX3N2ZCA8LSBzdmQoYXMubWF0cml4KG1vdmllc18xKSkNCg0KYGBgDQoNCmBgYHtyfQ0KDQpwbG90KG1vdmllc18xX3N2ZCRkKQ0KDQpgYGANCg0KVGhlIHNpbmd1bGFyIHZhbHVlcyBhcmUgbG93IHRocm91Z2hvdXQgdGhlIDAgdG8gMzAwLiANCg0KIyMgRGltZW5zaW9uYWxpdHkgUmVkdWN0aW9uDQoNClNpbmd1bGFyIHZhbHVlIGRlY29tcG9pc2l0aW9uIGFsbG93cyBhbiBleGFjdCByZXByZXNlbnRhdGlvbiBvZiBhbnkgbWF0cml4LCBhbmQgYWxzbyBhbGxvd3MgdXMgdG8gZWxpbWluYXRlIHRoZSBsZXNzIGltcG9ydGF0biBmZWF0dXJlcyB0aGF0IHJlcHJlc2VudGF0aW9uIHRvIHByb2R1Y2UgYW4gYXBwcm94aW1hdGUgcmVwcmVzZW50YXRpb24gd2l0aCBhbnkgZGVzaXJlZCBudW1iZXIgb2YgZGltZW5zaW9ucy4gVGhlIGZld2VyIHRoZSBkaW1lbnNpb25zIHdlIGNob29zZSwgdGhlIGxlc3MgYWNjdXJhdGUgd2lsbCBiZSB0aGUgYXBwcm94aW1hdGlvbi4gTGV0J3Mgc2F5IHdlIGhhdmUgYSBodWdlIG1hdHJpeCBSIHdpdGggaXRzIGNvbXBvbmVudHMgUCwgQSBhbmQgUS4gVGhleSBhcmUgYWxsIGxhcmdlLiBUaGUgYmVzdCB3YXkgdG8gcmVkdWNlIHRoZSBkaW1lbnNpb25hbGl0eSBvZiB0aGUgdGhyZWUgbWF0cmljZXMgd291bGQgYmUgdG8gc2V0IHRoZSBzbWFsbGVzdCBvZiB0aGUgc2luZ3VsYXIgdmFsdWVzIHRvIHplcm8uIElmIHdlIHNlbnQgdGhlIHNtYWxsZXN0IHNpbmd1bGFyIHZhbHVlcyB0byAwLCB0aGVuIHdlIGNhbiBhbHNvIGVsaW1pbmF0ZSB0aGUgY29ycmVzcG9uZGluZyBjb2x1bW5zIG9mIFAgYW5kIFEuIFdlIGNhbiBzdW0gdGhlIHNxdWFyZXMgb2YgZWFjaCBzaW5ndWxhciB2YWx1ZSBhbmQgdGhlbiBpZGVudGlmeSB0aGUgZmlyc3QgJGskIHNpbmd1bGFyIHZhbHVlcyB3aXRoaW4gbWF0cml4IEEuIEJhc2VkIG9uIHRoZSBzaW5ndWxhciB2YWx1ZXMgcGxvdHRlZCwgd2UgY2FuIHNlZSB0aGF0IGl0IHdpbGwgYXJvdW5kIHRoZSBzdGFydCBvZiB0aGUgc2luZ3VsYXIgdmFsdWVzLiANCg0KYGBge3J9DQojIHN1bSBvZiBzcXVhcmVzIG9mIGFsbCBzaW5ndWxhciB2YWx1ZXMNCnN1bV9zcXVhcmVzIDwtIHN1bShtb3ZpZXNfMV9zdmQkZF4yKQ0Kc3VtX3NxdWFyZXMNCg0KYGBgDQoNCg0KYGBge3J9DQojY2hlY2tzdW0gb2Ygc3F1YXJlcyBmb3Igc2luZ3VsYXIgdmFsdWVzDQpwZXJjX3ZlYyA8LSBOVUxMDQpmb3IgKGkgaW4gMTpsZW5ndGgobW92aWVzXzFfc3ZkJGQpKSB7DQogIHBlcmNfdmVjW2ldIDwtIHN1bShtb3ZpZXNfMV9zdmQkZFsxOmldXjIpIC8gc3VtX3NxdWFyZXMNCn0NCnBsb3QocGVyY192ZWMpDQoNCmBgYA0KDQpgYGB7cn0NCg0KayA8LSBsZW5ndGgocGVyY192ZWNbcGVyY192ZWMgPD0gLjk5XSkNCmsNCg0KYGBgDQoNCldlIGNhbiBzZWUgdGhhdCBmaXJzdCA2NCBzaW5ndWxhciB2YWx1ZXMgd2hvc2Ugc3F1YXJlcyBzdW0gdG8gYXQgbGVhc3QgOTklIG9mIHRoZSB0b3RhbCBvZiB0aGUgc3VtIG9mIHNxdWFyZXMgb2YgYWxsIHRoZSBzaW5ndWxhciB2YWx1ZXMuIExldCdzIGNhbGN1bGF0ZSBvdXIgJFBSUV97VH0kIG1hdHJpY2VzLg0KDQoNCg0KYGBge3J9DQojIGNhbGN1bGF0ZSBzaXplIG9mIEEgbWF0cml4DQpBX2sgPC0gRGlhZ29uYWwoeCA9IG1vdmllc18xX3N2ZCRkWzE6a10pDQoNCg0KYGBgDQoNCmBgYHtyfQ0KI2NhbGN1bGF0ZSBQIG1hdHJpeA0KUF9rIDwtIG1vdmllc18xX3N2ZCR1WywgMTprXQ0KDQpgYGANCg0KDQoNCmBgYHtyfQ0KI2NhbGN1bGF0ZSBWIG1hdHJpeCAodHJhbnNwb3NlIG9mIFYgbWF0cml4KQ0KUV9rIDwtIHQobW92aWVzXzFfc3ZkJHYpWzE6ayxdDQoNCg0KYGBgDQoNCg0KYGBge3J9DQojcHJvZHVjdCBvZiBhbGwgdGhlc2UgbWF0cmljZXMgd2lsbCBnaXZlIHVzIHRoZSBlc3RpbWF0ZWQgbWF0cml4DQpwcmVkaWN0ZWQgPC0gUF9rICUqJSBBX2sgJSolIFFfaw0KDQpgYGANCg0KYGBge3J9DQpwcmVkaWN0ZWRbMDo1XQ0KDQoNCmBgYA0KDQpXZSBzZWUgdGhlIHZhbHVlcyBhcmUgaGlnaGVyIHRoYW4gNS4gTGV0J3Mgc2V0IGFsbCB0aGUgcmF0aW5ncyB3aXRoaW4gMCBhbmQgNS4NCg0KYGBge3J9DQoNCiMgc2V0IGFsbCB2YWxzID4gMCB0byA1DQpwcmVkaWN0ZWRbLF1bcHJlZGljdGVkWyxdID4gNV0gPC0gNQ0KDQojIHNldCBhbGwgdmFscyA8IC0xMCB0byAtMTANCnByZWRpY3RlZFssXVtwcmVkaWN0ZWRbLF0gPCAwXSA8LSAwDQoNCmBgYA0KDQoNCmBgYHtyfQ0KcHJlZGljdGVkWzA6NV0NCg0KDQpgYGANCg0KYGBge3J9DQoNCnByZWRpY3RlZF9tYXRyaXggPC0gYXMubWF0cml4KHByZWRpY3RlZCkNCg0KYGBgDQoNCg0KDQojIE1vZGVsIERldmVsb3BtZW50IEFwcHJvYWNoIDINCg0KIyMgU2luZ2xlIFZhbHVlIERlY29tcG9zaXRpb24gLSBSZWNvbW1lbmRlciBQYWNrYWdlIC0gDQoNCkFzIGEgc2Vjb25kIGFwcHJvYWNoLCB3ZSBhcmUgZ29pbmcgaW1wbGVtZW50IHNpbmdsZSB2YWx1ZSBkZWNvbXBwb3NpdGlvbiB0byB0aGUgTW92aWVMZW5zIERhdGFzZXQgaW4gYSBkaWZmZXJlbnQgd2F5IHdoZXJlIHdlIHdpbGwgdXNlIHJlY29tbWVuZGVybGFiIHBhY2thZ2UuIEluIG91ciBmaXJzdCBhcHByb2FjaCwgd2UgY3JlYXRlZCBvdXIgbWF0cml4LCBoYW5kbGVkIG1pc3NpbmcgdmFsdWVzIGFuZCBjYWxjdWxhdGVkIGVhY2ggbWF0cml4IGFuZCBmdXJ0aGVyIGNhbGN1bGF0ZWQgdGhlIHByZWRpY3RlZCBtYXRyaXggd2l0aCB0aGUgcHJvZHVjdCBvZiBhbGwgdGhyZWUgbWF0cmljZXMuICRSPVBBUV57VH0kIC4gSW4gdGhpcyBhcHByb2FjaCwgd2UgYXJlIGdvaW5nIHRvIGNvbnZlcnQgb3VyIE1vdmllIExlbnNlIG1hdHJpeCBpbnRvICJyZWFsUmF0aW5nTWF0cml4IiBhbmQgZnVydGhlciBpbXBsZW1lbnQgU1ZELg0KDQpgYGB7cn0NCiMgY3JlYXRlIHRoZSBtYXRyaXgNCm1vdmllX2xlbnNlX21hdHJpeF8yIDwtIGFzKGFzLm1hdHJpeChtb3ZpZV9sZW5zZV9tYXRyaXgpLCAicmVhbFJhdGluZ01hdHJpeCIpDQojIHN1YnNldCB0aGUgZGF0YQ0KIyBzdWJzZXQgdGhlIGRhdGFzZXQNCm1vdmllc18yIDwtIG1vdmllX2xlbnNlX21hdHJpeF8yW3Jvd0NvdW50cyhNb3ZpZUxlbnNlKSA+IDEwMCwgY29sQ291bnRzKE1vdmllTGVuc2UpID4gMTAwXQ0KDQpgYGANCg0KYGBge3J9DQojIGNyZWF0ZSB0aGUgc3ZkIG1vZGVsDQptb2RlbF9zdmQgPC0gUmVjb21tZW5kZXIobW92aWVzXzIsIG1ldGhvZD0iU1ZEIiwgcGFyYW1ldGVyPWxpc3Qoaz02NCkpDQoNCmBgYA0KDQpgYGB7cn0NCg0KcHJlZGljdGVkX3N2ZCA8LSBwcmVkaWN0KG1vZGVsX3N2ZCwgbmV3ZGF0YT1tb3ZpZXNfMiwgdHlwZT0icmF0aW5ncyIpDQoNCmBgYA0KDQoNCmBgYHtyfQ0KcHJlZGljdGVkX3N2ZFswOjUsXQ0KDQpgYGANCg0KIyBDb25jbHVzaW9uDQoNCkJhc2VkIG9uIG91ciB0d28gU2luZ2xlIFZhbHVlIERlY29tcG9zaXRpb24gbWV0aG9kcyB3ZSBjcmVhdGVkLCB3ZSBjYW4gc2VlIHRoYXQgYmFzaWMgbWF0cml4IGZhY3Rvcml6YXRpb25hbCBtb2RlbCB1c2luZyBTVkQgY2FuIGJlIHByb3BlciB3YXkgb2YgY3JlYXRpbmcgcmVjb21tZW5kZXJzIHN5c3RlbS4gSXQgaXMgc2NhbGFibGUgaG93ZXZlciBpdCBpcyBpbXBvcnRhbnQgdG8gaGFuZGxlIHRoZSBtaXNzaW5nIHZhbHVlcyBwcm9wZXJseS4gDQoNCiMjIFJlZmVyZW5jZXM6DQoNCg0KKFJlZmVyZW5jZTogaHR0cHM6Ly93d3cuY291cnNlcmEub3JnL2xlY3R1cmUvbWF0cml4LWZhY3Rvcml6YXRpb24vc2luZ3VsYXItdmFsdWUtZGVjb21wb3NpdGlvbi1LNU5CeSApDQoNCihSZWZlcmVuY2U6IGh0dHBzOi8vZGF0YWpvYnMuY29tL2RhdGEtc2NpZW5jZS1yZXBvL1JlY29tbWVuZGVyLVN5c3RlbXMtJTVCTmV0ZmxpeCU1RC5wZGYgKQ0KDQooUmVmZXJlbmNlOiBNaW5pbmcgb2YgTWFzc2l2ZSBEYXRhIFNldHMgLSAxMS4zIFNpZ251bGFyLVZhbHVlIERlY29tcG9zaXRpb24pDQoNCg0KDQo=