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()
##
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
##
## group_rows
## 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)
Model Development Approach 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
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
## 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
## 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))

The singular values are low throughout the 0 to 300.
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
## [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
## [1] 5.000000 3.882264 3.446047 3.431647 3.845288
predicted_matrix <- as.matrix(predicted)
LS0tDQp0aXRsZTogIk1hdHJpeCBGYWN0b3JpemF0aW9uIC0gUHJvamVjdCAzIg0KYXV0aG9yOiBBbmlsIEFreWlsZGlyaW0NCmRhdGU6ICIwNi8yMS8yMDIwIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50Og0KICAgIGNvZGVfZG93bmxvYWQ6IHllcw0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIGhpZ2hsaWdodDogcHlnbWVudHMNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiBmbGF0bHkNCiAgICB0b2M6IHllcw0KICAgIHRvY19mbG9hdDogeWVzDQogIHBkZl9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KLS0tDQoNCiMgSW50cm9kdWN0aW9uDQoNCkluIHRoaXMgcHJvamVjdCwgd2UgYXJlIGdvaW5nIHRvIHJldmlldyB0aGUgTWF0cml4IEZhY3Rvcml6YXRpb24gbWV0aG9kcyB3aXRoIHRoZSBzYW1lIGRhdGEgc2V0IHdlIGhhdmUgdXNlZCBpbiB0aGUgZWFybGllciBwcm9qZWN0IChNb3ZpZUxlbnNlKS4gSW4gdGhlIHByZXZpb3VzIHByb2plY3QsIHdoaWxlIHdlIHdlcmUgY3JlYXRpbmcgdGhlIE1vdmllIFJhdGluZ3MgTWF0cml4LCB3ZSB1c2VkICAicmVhbFJhdGluZ01hdHJpeCIgY2xhc3MiLiBNb3JlIGRldGFpbHMgb24gInJlYWxSYXRpbmdNYXRyaXgiIGNhbiBiZSBmb3VuZCBoZXJlOiBodHRwczovL3d3dy5yZG9jdW1lbnRhdGlvbi5vcmcvcGFja2FnZXMvcmVjb21tZW5kZXJsYWIvdmVyc2lvbnMvMC4yLTUvdG9waWNzL3JlYWxSYXRpbmdNYXRyaXggLldpdGggdGhpcyBwcm9qZWN0LCB3ZSBhcmUgZ29pbmcgdG8gaW1wbGVtZW50IFNWRCB3aXRoIHVzaW5nIHJlYWxSYXRpbmdNYXRyaXggY2xhc3MgYW5kIHJlY29tbWVuZGVybGFiIHBhY2thZ2UgYW5kIGtlZXBpbmcgdGhlIG1hdHJpeCBhcyBzcGFyc2UgbWF0cml4IG9mIGNsYXNzIGRnQ01hdHJpeCBhbmQgcmVwbGFjaW5nIHRoZSBOQShvciAwKSB2YWx1ZXMgd2l0aCBjYWxjdWxhdGluZyB0aGUgYmFzZWxpbmUgcHJlZGljdG9yLg0KDQpgYGB7cn0NCiMgTG9hZCBMaWJyYXJpZXMNCg0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGthYmxlRXh0cmEpDQpsaWJyYXJ5KHJlY29tbWVuZGVybGFiKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShjYVRvb2xzKQ0KDQpgYGANCg0KIyBEYXRhIENvbGxlY3Rpb24NCg0KU2ltaWxhciB0byBwcm9qZWN0IDIsIHdlIHdpbGwgbG9hZCB0aGUgTW92aUxlbnNlIGFuZCBjcmVhdGUgb3VyIE1hdHJpeA0KDQoNCmBgYHtyfQ0KDQpzZXQuc2VlZCgxKQ0KZGF0YSgiTW92aWVMZW5zZSIpDQpNb3ZpZUxlbnNlDQoNCmBgYA0KDQoNCmBgYHtyfQ0KDQojIGNyZWF0ZSB0aGUgbWF0cml4DQptb3ZpZV9sZW5zZV9tYXRyaXggPC0gTW92aWVMZW5zZUBkYXRhDQpgYGANCg0KIyBNb2RlbCBEZXZlbG9wbWVudCBBcHByb2FjaCAxIA0KDQojIyBNYXRyaXggRmFjdG9yaXphdGlvbg0KDQpVc2VycyBhbmQgaXRlbXMgZ2V0cyBtb2RlbGVkIHRvIGEgam9pbnQgbGF0ZW50IGZhY3RvciBzcGFjZSBvZiBkaW1lbnNpb25hbGl0eSBmIHNvIHRoYXQgdGhlIHVzZXItaXRlbSBpdGVyYWN0aW9ucyBhcmUgbW9kZWxlZCBhcyB0aGUgaW5uZXIgcHJvZHVjdHMgd2l0aGluIHRoZSBzcGFjZSBmLiANCg0KJGkgLS0+IGl0ZW0kDQoNCiRxX3tpfSAtLT4gaXRlbSB2ZWN0b3IkDQoNCiRwX3t1fSAtLT4gdXNlciB2ZWN0b3IkDQoNCkZvciBhIGdpdmVuIGl0ZW0gJGkkLCB0aGUgZWxlbWVudHMgb2YgJHFfe2l9JCBtZWFzdXJlIHRoZSBleHRlbnQgdG8gd2hpY2ggdGhlIGl0ZW0gcG9zc2Vzc2VzIHRob3NlIGZhY3RvcnMgcG9zaXRpdmUgb3IgbmVnYXRpdmUuIEZvciBhIGdpdmVuIHVzZXIgdSwgdGhlIGVsZW1lbnRzIG9mICRwX3t1fSQgbWVhc3VyZSB0aGUgZXh0ZW50IG9mIGludGVyZXN0IHRoZSB1c2VyIGhhcyBpbiBpdGVtcyB0aGF0IGFyZSBoaWdoIG9uIHRoZSBjb3JyZXNwb25kaW5nIGZhY3RvcnMsIGFnYWluLCBwb3NpdGl2ZSBvciBuZWdhdGl2ZS4gVGhlIHJlc3VsdGluZyBkb3QgcHJvZHVjdCwgJHFfe2l9XntUfSQkcF97dX0kLCBjYXB0dXJlcyB0aGUgaW50ZXJhY3Rpb24gYmV0d2VlbiB1c2VyICR1JCBhbmQgaXRlbSAkaSQg4oCUdGhlIHVzZXLigJlzIG92ZXJhbGwgaW50ZXJlc3QgaW4gdGhlIGl0ZW3igJlzIGNoYXJhY3RlcmlzdGljcy4gVGhpcyBhcHByb3hpbWF0ZXMgdXNlciAkdSTigJlzIHJhdGluZyBvZiBpdGVtIGksIHdoaWNoIGlzIGRlbm90ZWQgYnkgJHJfe3VpfSQsIGxlYWRpbmcgdG8gdGhlIGVzdGltYXRlICRyX3t1aX09cV97aX1ee1R9cF97dX0kIC4gVGhpcyBtb2RlbCB3b3VsZCBiZSBzaW5ndWxhciB2YWx1ZSBkZWNvbXBvc2l0aW9uKFNWRCkgYW4gYXBwcm9hY2ggdG8gaWRlbmZpdGZ5IGxhdGVudCBzZW1lbnRpYyBmYWN0b3JzLiANCg0KSW4gYSBudXRzaGVsbCwgU1ZEIGlzDQoNCiRSPVBBUV57VH0kIC0tPiBXZSBoYXZlIHRocmVlIG1hdHJpY2VzLCBQLCBBLCBRIHdoZXJlIHdlIG11bHRpcGx5IHRoZW0gd2UgZ2V0IHRoZSBtYXRyaXggb2YgUi4gUiBpcyBtICogbiByYXRpbmdzIG1hdHJpeCwgUCBpcyBtICogayB1c2VyIGZlYXR1cmUgYWZmaW5pdHkgbWF0cml4LCBRIGlzIG4gKiBrIGl0ZW0gZmVhdHVyZSByZWxldmFuY2UgbWF0cml4IGFuZCBBIGlzIGsgKiBrIGRpYWdvbmFsIGZlYXR1cmUgd2VpZ2h0IG1hdHJpeC4gVGhlIFIgLCBvcmlnaW5hbCBtYXRyaXggY2FuIGJlIGVzdGltYXRlZCBieSB0aGUgcHJvZHVjdCBvZiBhbGwgdGhlc2UgbWF0cmljZXMuIA0KDQoqKiBTVkQgZGVzY3JpYmVzIHByZWZlcmVuY2UgaW4gdGVybXMgb2YgbGF0ZW50IGZlYXR1cmVzDQoNCioqIFRoZXNlIGZlYXR1cmVzIGFyZSBsZWFybmVkIGZyb20gdGhlIHJhdGluZyBkYXRhIA0KDQoqKiBBcyBleHBsYWluZWQgaW4gdGhlIGJlZ2luaW5nLCBkZWZpbmVzIGEgc2hhcmVkIHZlY3RvciBzcGFjZSBmb3IgdXNlcnMgYW5kIGl0ZW1zLg0KDQoNCldlIGNyZWF0ZWQgdGhlIG1vdmllX2xlbnNlX21hdHJpeCwgd2Uga25vdyBmcm9tIHByZXZpb3VzIHByb2plY3QgdGhhdCB3ZSBuZWVkIHRvIHN1YnNldC4gDQoNCg0KDQpgYGB7cn0NCiMgc3Vic2V0IHRoZSBkYXRhc2V0DQptb3ZpZXNfMSA8LSBtb3ZpZV9sZW5zZV9tYXRyaXhbcm93Q291bnRzKE1vdmllTGVuc2UpID4gMTAwLCBjb2xDb3VudHMoTW92aWVMZW5zZSkgPiAxMDBdDQoNCmBgYA0KDQpgYGB7cn0NCiNyZXBsYWNlIDAgd2l0aCBOQQ0KbW92aWVzXzFbLF1bbW92aWVzXzFbLF0gPT0gMF0gPC0gTkENCg0KYGBgDQoNCg0KYGBge3J9DQoNCnN1bW1hcnkoYXMudmVjdG9yKGFzLm1hdHJpeChtb3ZpZXNfMSkpKQ0KDQoNCmBgYA0KDQoNCiMjIERhdGEgUHJlcGVyYXRpb24NCg0KU1ZEIHJlcXVpcmVzIHRoYXQgdGhlcmUgYXJlIG5vIG1pc3NpbmcgdmFsdWVzLiBJbiBvdXIgc3Vic2V0IG1hdHJpeCwgd2UgaGF2ZSA3Myw3ODIgbWlzc2luZyB2YWx1ZXMgdGhhdCB3ZSBjYW4gcmVwbGFjZSB0aGVtIHdpdGggdGhlIG1lYW4gdmFsdWUuIFdlIGNhbiBhbHNvIHVzZSB0aGUgYmFzZWxpbmUgcHJlZGljdG9yIGFwcHJvYWNoLiBXZSBjYWxjdWxhdGUgdGhlIHVzZXIgYW5kIGl0ZW0gYmlhc2VzIGluIHRoZSBtYXRyaXgsIHRoZW4gcmVwbGFjZSB0aGUgbWlzc2luZyB2YWx1ZXMgd2l0aCB0aGUgc3VtIG9mIHRoZSByYXcgbWVhbiwgdXNlciBhbmQgaXRlbSBiaWFzZXMuIA0KDQoNCmBgYHtyfQ0KDQojIGdldCBtZWFuIHZhbHVlIG9mIHRoZSBtYXRyaXgNCnJhd19tZWFuIDwtIG1lYW4oYXMudmVjdG9yKGFzLm1hdHJpeChtb3ZpZXNfMSkpLCBuYS5ybSA9IFRSVUUgKQ0KcmF3X21lYW4NCg0KYGBgDQoNCg0KYGBge3J9DQojIGNvdW50IG51bWJlciBvZiBub24tTkEncyBpbiBlYWNoIHJvdyBvZiB0cmFpbmluZyBzZXQNCnJvd192YWxpZCA8LSByb3dTdW1zKCFpcy5uYShtb3ZpZXNfMVssXSkpDQoNCiMgY291bnQgbnVtYmVyIG9mIG5vbi1OQSdzIGluIGVhY2ggY29sdW1uIG9mIHRyYWluaW5nIHNldA0KY29sX3ZhbGlkIDwtIGNvbFN1bXMoIWlzLm5hKG1vdmllc18xWyxdKSkNCg0KIyBjYWxjdWxhdGUgdXNlciBiaWFzZXMNCnVzZXJfYmlhc2VzIDwtIHJvd01lYW5zKG1vdmllc18xWyxdIC0gcmF3X21lYW4sIG5hLnJtID0gVFJVRSkgLyByb3dfdmFsaWQNCg0KIyBjYWxjdWxhdGUgaXRlbSBiaWFzZXMNCml0ZW1fYmlhc2VzIDwtIGNvbE1lYW5zKG1vdmllc18xWyxdIC0gcmF3X21lYW4sIG5hLnJtID0gVFJVRSkgLyBjb2xfdmFsaWQNCg0KIyBtZW1vcnkgY2xlYW51cA0Kcm0ocm93X3ZhbGlkLCBjb2xfdmFsaWQpDQoNCmBgYA0KDQoNCg0KYGBge3J9DQpmb3IgKGkgaW4gMTpucm93KG1vdmllc18xKSkgew0KICBmb3IgKGogaW4gMTpuY29sKG1vdmllc18xKSkgew0KICAgIA0KICAgICMgaWYgdGhlIG1hdHJpeCBlbGVtZW50IGhhcyBhbiBOQSwgZmlsbCBpbiB3aXRoIGJhc2VsaW5lIHByZWRpY3Rvcg0KICAgIGlmKGlzLm5hKG1vdmllc18xW2ksal0pKSB7DQogICAgICAgICAgbW92aWVzXzFbaSxqXSA8LSByYXdfbWVhbiArIHVzZXJfYmlhc2VzW2ldICsgaXRlbV9iaWFzZXNbal0NCiAgICAgICAgICANCiAgICAgICAgICAjIGVuc3VyZSBuZXcgdmFsdWVzIGFyZSB3aXRoaW4gdmFsaWQgcmF0aW5ncyBib3VuZHMNCiAgICAgICAgICBpZiAobW92aWVzXzFbaSxqXSA+IDUpIG1vdmllc18xW2ksal0gPC0gNQ0KICAgICAgICAgIGlmIChtb3ZpZXNfMVtpLGpdIDwgMCkgbW92aWVzXzFbaSxqXSA8LSAwDQogICAgfSAjIGVuZCBpZg0KICAgIA0KICB9ICMgZW5kIGZvciBqDQp9ICMgZW5kIGZvciBpDQoNCmBgYA0KDQoNCmBgYHtyfQ0Kc3VtbWFyeShhcy52ZWN0b3IoYXMubWF0cml4KG1vdmllc18xKSkpDQoNCg0KYGBgDQoNCg0KYGBge3J9DQoNCnN1bW1hcnkodXNlcl9iaWFzZXMpDQoNCg0KYGBgDQoNCmBgYHtyfQ0KDQpzdW1tYXJ5KGl0ZW1fYmlhc2VzKQ0KDQoNCmBgYA0KDQpXZSBoYW5kbGVkIHRoZSBtaXNzaW5nIHZhbHVlcyBhbmQgbm93IHdlIGNhbiBzdGFydCBjYWxjdWxhdGluZyB0aGUgU1ZELg0KDQoNCmBgYHtyfQ0KDQpyYW5rIDwtIHFyKGFzLm1hdHJpeChtb3ZpZXNfMSkpJHJhbmsNCnJhbmsNCg0KDQpgYGANCg0KVGhlIG1hdHJpeCBoYXMgMzMyIGNvbHVtbnMsIG91ciBzdWJzZXQgbWF0cml4IGlzIDM1OHgzMzIuIFJlZmVyZW5jaW5nIHRoZSBlYXJsaWVyIGFwcHJvYWNoIG9mICRSPVBBUV57VH0kIC4NCg0KKiBNYXRyaXggUCB3aWxsIGJlIDM1OCAqIDMzMiANCg0KKiBNYXRyaXggQSB3aWxsIGJlIDMzMiAqIDMzMiANCg0KKiBNYXRyaXggUSB3aWxsIGJlIDMzMiAqIDMzMg0KDQoNCkNhbGN1bGF0aW5nIHRoZSBTVkQuDQoNCg0KYGBge3J9DQojIGNhbGN1bGF0ZSBzdmQNCm1vdmllc18xX3N2ZCA8LSBzdmQoYXMubWF0cml4KG1vdmllc18xKSkNCg0KYGBgDQoNCmBgYHtyfQ0KDQpwbG90KG1vdmllc18xX3N2ZCRkKQ0KDQpgYGANCg0KVGhlIHNpbmd1bGFyIHZhbHVlcyBhcmUgbG93IHRocm91Z2hvdXQgdGhlIDAgdG8gMzAwLiANCg0KIyMgRGltZW5zaW9uYWxpdHkgUmVkdWN0aW9uDQoNClNpbmd1bGFyIHZhbHVlIGRlY29tcG9pc2l0aW9uIGFsbG93cyBhbiBleGFjdCByZXByZXNlbnRhdGlvbiBvZiBhbnkgbWF0cml4LCBhbmQgYWxzbyBhbGxvd3MgdXMgdG8gZWxpbWluYXRlIHRoZSBsZXNzIGltcG9ydGF0biBmZWF0dXJlcyB0aGF0IHJlcHJlc2VudGF0aW9uIHRvIHByb2R1Y2UgYW4gYXBwcm94aW1hdGUgcmVwcmVzZW50YXRpb24gd2l0aCBhbnkgZGVzaXJlZCBudW1iZXIgb2YgZGltZW5zaW9ucy4gVGhlIGZld2VyIHRoZSBkaW1lbnNpb25zIHdlIGNob29zZSwgdGhlIGxlc3MgYWNjdXJhdGUgd2lsbCBiZSB0aGUgYXBwcm94aW1hdGlvbi4gTGV0J3Mgc2F5IHdlIGhhdmUgYSBodWdlIG1hdHJpeCBSIHdpdGggaXRzIGNvbXBvbmVudHMgUCwgQSBhbmQgUS4gVGhleSBhcmUgYWxsIGxhcmdlLiBUaGUgYmVzdCB3YXkgdG8gcmVkdWNlIHRoZSBkaW1lbnNpb25hbGl0eSBvZiB0aGUgdGhyZWUgbWF0cmljZXMgd291bGQgYmUgdG8gc2V0IHRoZSBzbWFsbGVzdCBvZiB0aGUgc2luZ3VsYXIgdmFsdWVzIHRvIHplcm8uIElmIHdlIHNlbnQgdGhlIHNtYWxsZXN0IHNpbmd1bGFyIHZhbHVlcyB0byAwLCB0aGVuIHdlIGNhbiBhbHNvIGVsaW1pbmF0ZSB0aGUgY29ycmVzcG9uZGluZyBjb2x1bW5zIG9mIFAgYW5kIFEuIFdlIGNhbiBzdW0gdGhlIHNxdWFyZXMgb2YgZWFjaCBzaW5ndWxhciB2YWx1ZSBhbmQgdGhlbiBpZGVudGlmeSB0aGUgZmlyc3QgJGskIHNpbmd1bGFyIHZhbHVlcyB3aXRoaW4gbWF0cml4IEEuIEJhc2VkIG9uIHRoZSBzaW5ndWxhciB2YWx1ZXMgcGxvdHRlZCwgd2UgY2FuIHNlZSB0aGF0IGl0IHdpbGwgYXJvdW5kIHRoZSBzdGFydCBvZiB0aGUgc2luZ3VsYXIgdmFsdWVzLiANCg0KYGBge3J9DQojIHN1bSBvZiBzcXVhcmVzIG9mIGFsbCBzaW5ndWxhciB2YWx1ZXMNCnN1bV9zcXVhcmVzIDwtIHN1bShtb3ZpZXNfMV9zdmQkZF4yKQ0Kc3VtX3NxdWFyZXMNCg0KYGBgDQoNCg0KYGBge3J9DQojY2hlY2tzdW0gb2Ygc3F1YXJlcyBmb3Igc2luZ3VsYXIgdmFsdWVzDQpwZXJjX3ZlYyA8LSBOVUxMDQpmb3IgKGkgaW4gMTpsZW5ndGgobW92aWVzXzFfc3ZkJGQpKSB7DQogIHBlcmNfdmVjW2ldIDwtIHN1bShtb3ZpZXNfMV9zdmQkZFsxOmldXjIpIC8gc3VtX3NxdWFyZXMNCn0NCnBsb3QocGVyY192ZWMpDQoNCmBgYA0KDQpgYGB7cn0NCg0KayA8LSBsZW5ndGgocGVyY192ZWNbcGVyY192ZWMgPD0gLjk5XSkNCmsNCg0KYGBgDQoNCldlIGNhbiBzZWUgdGhhdCBmaXJzdCA2NCBzaW5ndWxhciB2YWx1ZXMgd2hvc2Ugc3F1YXJlcyBzdW0gdG8gYXQgbGVhc3QgOTklIG9mIHRoZSB0b3RhbCBvZiB0aGUgc3VtIG9mIHNxdWFyZXMgb2YgYWxsIHRoZSBzaW5ndWxhciB2YWx1ZXMuIExldCdzIGNhbGN1bGF0ZSBvdXIgJFBSUV97VH0kIG1hdHJpY2VzLg0KDQoNCg0KYGBge3J9DQojIGNhbGN1bGF0ZSBzaXplIG9mIEEgbWF0cml4DQpBX2sgPC0gRGlhZ29uYWwoeCA9IG1vdmllc18xX3N2ZCRkWzE6a10pDQoNCg0KYGBgDQoNCmBgYHtyfQ0KI2NhbGN1bGF0ZSBQIG1hdHJpeA0KUF9rIDwtIG1vdmllc18xX3N2ZCR1WywgMTprXQ0KDQpgYGANCg0KDQoNCmBgYHtyfQ0KI2NhbGN1bGF0ZSBWIG1hdHJpeCAodHJhbnNwb3NlIG9mIFYgbWF0cml4KQ0KUV9rIDwtIHQobW92aWVzXzFfc3ZkJHYpWzE6ayxdDQoNCg0KYGBgDQoNCg0KYGBge3J9DQojcHJvZHVjdCBvZiBhbGwgdGhlc2UgbWF0cmljZXMgd2lsbCBnaXZlIHVzIHRoZSBlc3RpbWF0ZWQgbWF0cml4DQpwcmVkaWN0ZWQgPC0gUF9rICUqJSBBX2sgJSolIFFfaw0KDQpgYGANCg0KYGBge3J9DQpwcmVkaWN0ZWRbMDo1XQ0KDQoNCmBgYA0KDQpXZSBzZWUgdGhlIHZhbHVlcyBhcmUgaGlnaGVyIHRoYW4gNS4gTGV0J3Mgc2V0IGFsbCB0aGUgcmF0aW5ncyB3aXRoaW4gMCBhbmQgNS4NCg0KYGBge3J9DQoNCiMgc2V0IGFsbCB2YWxzID4gMCB0byA1DQpwcmVkaWN0ZWRbLF1bcHJlZGljdGVkWyxdID4gNV0gPC0gNQ0KDQojIHNldCBhbGwgdmFscyA8IC0xMCB0byAtMTANCnByZWRpY3RlZFssXVtwcmVkaWN0ZWRbLF0gPCAwXSA8LSAwDQoNCmBgYA0KDQoNCmBgYHtyfQ0KcHJlZGljdGVkWzA6NV0NCg0KDQpgYGANCg0KYGBge3J9DQoNCnByZWRpY3RlZF9tYXRyaXggPC0gYXMubWF0cml4KHByZWRpY3RlZCkNCg0KYGBgDQoNCg0KDQojIE1vZGVsIERldmVsb3BtZW50IEFwcHJvYWNoIDINCg0KIyMgU2luZ2xlIFZhbHVlIERlY29tcG9zaXRpb24gLSBSZWNvbW1lbmRlciBQYWNrYWdlIC0gDQoNCkFzIGEgc2Vjb25kIGFwcHJvYWNoLCB3ZSBhcmUgZ29pbmcgaW1wbGVtZW50IHNpbmdsZSB2YWx1ZSBkZWNvbXBwb3NpdGlvbiB0byB0aGUgTW92aWVMZW5zIERhdGFzZXQgaW4gYSBkaWZmZXJlbnQgd2F5IHdoZXJlIHdlIHdpbGwgdXNlIHJlY29tbWVuZGVybGFiIHBhY2thZ2UuIEluIG91ciBmaXJzdCBhcHByb2FjaCwgd2UgY3JlYXRlZCBvdXIgbWF0cml4LCBoYW5kbGVkIG1pc3NpbmcgdmFsdWVzIGFuZCBjYWxjdWxhdGVkIGVhY2ggbWF0cml4IGFuZCBmdXJ0aGVyIGNhbGN1bGF0ZWQgdGhlIHByZWRpY3RlZCBtYXRyaXggd2l0aCB0aGUgcHJvZHVjdCBvZiBhbGwgdGhyZWUgbWF0cmljZXMuICRSPVBBUV57VH0kIC4gSW4gdGhpcyBhcHByb2FjaCwgd2UgYXJlIGdvaW5nIHRvIGNvbnZlcnQgb3VyIE1vdmllIExlbnNlIG1hdHJpeCBpbnRvICJyZWFsUmF0aW5nTWF0cml4IiBhbmQgZnVydGhlciBpbXBsZW1lbnQgU1ZELg0KDQpgYGB7cn0NCiMgY3JlYXRlIHRoZSBtYXRyaXgNCm1vdmllX2xlbnNlX21hdHJpeF8yIDwtIGFzKGFzLm1hdHJpeChtb3ZpZV9sZW5zZV9tYXRyaXgpLCAicmVhbFJhdGluZ01hdHJpeCIpDQojIHN1YnNldCB0aGUgZGF0YQ0KIyBzdWJzZXQgdGhlIGRhdGFzZXQNCm1vdmllc18yIDwtIG1vdmllX2xlbnNlX21hdHJpeF8yW3Jvd0NvdW50cyhNb3ZpZUxlbnNlKSA+IDEwMCwgY29sQ291bnRzKE1vdmllTGVuc2UpID4gMTAwXQ0KDQpgYGANCg0KYGBge3J9DQojIGNyZWF0ZSB0aGUgc3ZkIG1vZGVsDQptb2RlbF9zdmQgPC0gUmVjb21tZW5kZXIobW92aWVzXzIsIG1ldGhvZD0iU1ZEIiwgcGFyYW1ldGVyPWxpc3Qoaz02NCkpDQoNCmBgYA0KDQpgYGB7cn0NCg0KcHJlZGljdGVkX3N2ZCA8LSBwcmVkaWN0KG1vZGVsX3N2ZCwgbmV3ZGF0YT1tb3ZpZXNfMiwgdHlwZT0icmF0aW5ncyIpDQoNCmBgYA0KDQoNCmBgYHtyfQ0KcHJlZGljdGVkX3N2ZFswOjUsXQ0KDQpgYGANCg0KIyBDb25jbHVzaW9uDQoNCkJhc2VkIG9uIG91ciB0d28gU2luZ2xlIFZhbHVlIERlY29tcG9zaXRpb24gbWV0aG9kcyB3ZSBjcmVhdGVkLCB3ZSBjYW4gc2VlIHRoYXQgYmFzaWMgbWF0cml4IGZhY3Rvcml6YXRpb25hbCBtb2RlbCB1c2luZyBTVkQgY2FuIGJlIHByb3BlciB3YXkgb2YgY3JlYXRpbmcgcmVjb21tZW5kZXJzIHN5c3RlbS4gSXQgaXMgc2NhbGFibGUgaG93ZXZlciBpdCBpcyBpbXBvcnRhbnQgdG8gaGFuZGxlIHRoZSBtaXNzaW5nIHZhbHVlcyBwcm9wZXJseS4gDQoNCiMjIFJlZmVyZW5jZXM6DQoNCg0KKFJlZmVyZW5jZTogaHR0cHM6Ly93d3cuY291cnNlcmEub3JnL2xlY3R1cmUvbWF0cml4LWZhY3Rvcml6YXRpb24vc2luZ3VsYXItdmFsdWUtZGVjb21wb3NpdGlvbi1LNU5CeSApDQoNCihSZWZlcmVuY2U6IGh0dHBzOi8vZGF0YWpvYnMuY29tL2RhdGEtc2NpZW5jZS1yZXBvL1JlY29tbWVuZGVyLVN5c3RlbXMtJTVCTmV0ZmxpeCU1RC5wZGYgKQ0KDQooUmVmZXJlbmNlOiBNaW5pbmcgb2YgTWFzc2l2ZSBEYXRhIFNldHMgLSAxMS4zIFNpZ251bGFyLVZhbHVlIERlY29tcG9zaXRpb24pDQoNCg0KDQo=