추천시스템

본 내용은 Building a Recommendation System with R 내용을 기반으로 작성 되었다.

추천 시스템의 종류

  • 콘텐츠 기반 추천 시스템
  • 지식기반 추천 시스템
  • 하이브리드 시스템

콘텐츠 기반 추천 시스템

콘텐츠 기반 추천 시스템 : 사용자들 간에 유사도를 고려하여 사용자들에게 아이템을 추천하는 시스템

따라서, 사용자가 과거에 좋아했던 아이템들과 비슷한 제품을 추천

ex) 영화추천

추천과정에서 주변 사용자들의 선호도를 고려하지 않고 사용자의 과거 선호도와 아이템의 속성 및 특징을 중점으로 추천한다.

지식 기반 추천 시스템

추천하기 전에 아이템의 특징과 명시적 질문을 통해 획득한 사용자 선호도와 추천 범위 등에서 정보를 고려해 추천

모델의 정확도는 추천된 아이템이 얼마나 사용자에게 유용한가를 기반으로 평가

사용자들의 구매 이력이 적은 경우 사용.


데이터의 전처리

대부분의 데이터들은 고차원이며 희소한 데이터이므로 이를 바로 분석모델에 적용할 경우 예측능력이 현저하게 낮아지는 현상이 발생하는데 이를 차원의 저주(THe cusse of dimensionality)라 한다.

이를 해소하는 방법중 하나로 차원축소(Dimensionality Reduction)를 사용한다. 차원 축소 기법들의 종류로는 Principal component analysis(PCA), Non-negative Matrix factorization(NMF), Topological Data Analysis(TDA), auto encoder ,t-Stochastic Neighbor Embedding(T-SNE) 등이 있다.


유사도의 측정

추천 시스템은 자료간 유사도를 고려하여 작동

유사도의 3가지 조건과 거리의 3가지 조건

  • 유사도의 3가지 조건

유사도는 양수이어야하며 \(d(x,y)=d(y,x)\)를 만족하고 \(s(x,y)\le s(x,x)\)를 만족하여야 한다.

  • 거리의 3가지 조건

양수이어야하며, \(d(x,y)=d(y,x)\) 즉, 대칭성을 만족하며, 동일성 즉, 오직\(d(x,y)=0\)때만 \(x=y\)를 만족하여야 한다.


거리계산의 종류

  • 민코우스키(MinKowski) 거리

\(Dist(x,y)=[\sum(x_{ri}-x_{si})^p]^{1/p}\)

  • 맨하튼 거리

두 점 간의 차이의 절대값을 합한 값, r=1일 때의 민코우스키의 거리와 같음. \(Dist(x,y)=\sum|x_i-y_i|\)

  • 유클리드 거리

민코우스키거리의 특별 경우(r=2) 자료의 분포적 특성을 고려할 수 없으며, 단위 또한 같아야한다는 단점이 존재.

\(Dist(x,y)=\sqrt{\sum(x_{ri}-x_{si})^2}=\sqrt{[X_r-X_s]^T[X_r-X_s]}\)

  • 체비셰프 거리(=maximum distance)

두 집단에서 가장 긴 지점에서의 거리

\(max(|x_1-y_1|,\cdots,|x_p-y_p|\)

  • 표준화 거리

유클리드 거리를 공분산으로 나눈 거리

\([(X_r-X_s)^TD^{-1}(X_r-X_s)]^{1/2} , D=diag(S_{11},\cdots,S_{pp})\)

  • 마할라노비스(Mahalanobis) 거리

공분산 구조를 함께 고려한 통계적 거리, 자료의 분포적 특성을 고려하기 위한 방법.

참고:공분산이 단위행렬이 되면 유클리드거리와 같아지는데 이러한 변환을 화이트닝변환이라 한다.

\([(X_r-X_s)^TS^{-1}(X_r-X_s)]^{1/2} , S=표본공분산행렬\)

  • 캔버라 거리

맨하튼 거리에 가중치를 적용한 거리의 개념

\(dist(x,y)=\sum\frac{|x_i-y_i|}{x_i-y_i}\)

method에 euclidean, maximum, manhattan, canbera, minkowski 등이 들어갈 수 있다.

x1=rnorm(30)
x2=rnorm(30)
dist(rbind(x1,x2),method='euclidean')
         x1
x2 8.225788
  • 코사인 거리

\(cos(X_r,X_s)=\frac{X_r\times X_s}{|X_r||X_s|}\)

코사인 유사도는 무게나 크기는 전혀 고려하지 않고 벡터 사이의 각도만으로 측정하는 것과 유사

library('lsa')
x1=sample(c(0,1),30,replace = T)
x2=sample(c(0,1),30,replace = T)
cosine(x1,x2)
         [,1]
[1,] 0.571662
  • 피어슨 거리

피어슨 상관계수의 범위가 -1~1이므로 1에서 피어슨 상관계수를 뺀 값을 거리로 사용


차 원 축 소 기 법

PCA

축을 변환하여 행렬을 새로운 형태로 변환하는 기법. 자체로는 차원이 줄어들지 않는다. 선형 분석방식이므로 비선형성을 고려하지 못한다는 단점이 존재

Propertion of Variance: 분산비율, 각 주성분의 차지하는 비율을 말하며 클 수록 영향을 많이 미친다는 것을 의미한다. Cumulative Proportion: 분산의 누적 합계

prcomp 의 경우 SVD(특이값 분해)를 사용하고, princomp의 경우 스팩트럼 분해를 사용한다는 측면에서 prcomp의 수치 정확도가 더 높다.

data(USArrests)
Warning message:
In strsplit(code, "\n", fixed = TRUE) :
  input string 1 is invalid in this locale
rownames(USArrests)
 [1] "Alabama"        "Alaska"         "Arizona"       
 [4] "Arkansas"       "California"     "Colorado"      
 [7] "Connecticut"    "Delaware"       "Florida"       
[10] "Georgia"        "Hawaii"         "Idaho"         
[13] "Illinois"       "Indiana"        "Iowa"          
[16] "Kansas"         "Kentucky"       "Louisiana"     
[19] "Maine"          "Maryland"       "Massachusetts" 
[22] "Michigan"       "Minnesota"      "Mississippi"   
[25] "Missouri"       "Montana"        "Nebraska"      
[28] "Nevada"         "New Hampshire"  "New Jersey"    
[31] "New Mexico"     "New York"       "North Carolina"
[34] "North Dakota"   "Ohio"           "Oklahoma"      
[37] "Oregon"         "Pennsylvania"   "Rhode Island"  
[40] "South Carolina" "South Dakota"   "Tennessee"     
[43] "Texas"          "Utah"           "Vermont"       
[46] "Virginia"       "Washington"     "West Virginia" 
[49] "Wisconsin"      "Wyoming"       
names(USArrests)
[1] "Murder"   "Assault"  "UrbanPop" "Rape"    
head(USArrests)
#변수별 분산
apply(USArrests,2,var)
    Murder    Assault   UrbanPop       Rape 
  18.97047 6945.16571  209.51878   87.72916 
pca=prcomp(USArrests,scale=T)
summary(pca)
Importance of components:
                          PC1    PC2     PC3     PC4
Standard deviation     1.5749 0.9949 0.59713 0.41645
Proportion of Variance 0.6201 0.2474 0.08914 0.04336
Cumulative Proportion  0.6201 0.8675 0.95664 1.00000
biplot(pca)

plot(pca)

t-SNE

t-sne 설명

library(Rtsne)
tsne=Rtsne(USArrests,dim=2, perplexity=10, verbose=TRUE, max_iter = 500)
Read the 50 x 4 data matrix successfully!
Using no_dims = 2, perplexity = 10.000000, and theta = 0.500000
Computing input similarities...
Normalizing input...
Building tree...
 - point 0 of 50
Done in 0.00 seconds (sparsity = 0.698400)!
Learning embedding...
Iteration 50: error is 51.184210 (50 iterations in 0.01 seconds)
Iteration 100: error is 47.904857 (50 iterations in 0.01 seconds)
Iteration 150: error is 53.147732 (50 iterations in 0.01 seconds)
Iteration 200: error is 50.765835 (50 iterations in 0.01 seconds)
Iteration 250: error is 48.690994 (50 iterations in 0.01 seconds)
Iteration 300: error is 1.383360 (50 iterations in 0.00 seconds)
Iteration 350: error is 0.819592 (50 iterations in 0.00 seconds)
Iteration 400: error is 0.393121 (50 iterations in 0.01 seconds)
Iteration 450: error is 0.251557 (50 iterations in 0.00 seconds)
Iteration 500: error is 0.131463 (50 iterations in 0.01 seconds)
Fitting performed in 0.06 seconds.
library(tsne)
data(iris)
iris_tsne = tsne::tsne(as.matrix(iris[1:4]))
sigma summary: Min. : 0.486505661043274 |1st Qu. : 0.587913800179832 |Median : 0.614872437640536 |Mean : 0.623051089344394 |3rd Qu. : 0.654914112723525 |Max. : 0.796707932771489 |
Epoch: Iteration #100 error is: 13.2837229048556
Epoch: Iteration #200 error is: 0.266909913372238
Epoch: Iteration #300 error is: 0.265050123836646
Epoch: Iteration #400 error is: 0.26471624264499
Epoch: Iteration #500 error is: 0.264477221499082
Epoch: Iteration #600 error is: 0.264232266607102
Epoch: Iteration #700 error is: 0.263976211815421
Epoch: Iteration #800 error is: 0.263792910643845
Epoch: Iteration #900 error is: 0.263675732682757
Epoch: Iteration #1000 error is: 0.263605324162608
df_iris_tsne = iris_tsne %>% 
  as.data.frame() %>% 
  tbl_df() %>% 
  mutate(species = iris$Species)
df_iris_tsne %>% 
  ggplot(aes(x = V1, y = V2, color = species)) +
  geom_point() +
  scale_color_brewer(palette = 'Paired') +
  ggtitle('t-SNE result : iris dataset (Real Species)') +
  theme(axis.title.x = element_blank(),
        axis.title.y = element_blank())

NMF

1개의 데이터 행렬을 2개로 분리

NMF설명

library(NMF)
res=nmf(USArrests,3)
V.hat <- fitted(res) 
w <- basis(res)

TDA

TDA 설명

# install_version('phom')
library(phom) # phom 설치 확인 
Warning message:
In strsplit(code, "\n", fixed = TRUE) :
  input string 1 is invalid in this locale
data = as.matrix(iris[,-5]) 
head(data) 
     Sepal.Length Sepal.Width Petal.Length Petal.Width
[1,]          5.1         3.5          1.4         0.2
[2,]          4.9         3.0          1.4         0.2
[3,]          4.7         3.2          1.3         0.2
[4,]          4.6         3.1          1.5         0.2
[5,]          5.0         3.6          1.4         0.2
[6,]          5.4         3.9          1.7         0.4
max_dim = 0 
max_f = 1 
irisInt0 = pHom(data, dimension=max_dim,# maximum dimension of persistent homology computed 
                max_filtration_value=max_f, # maximum dimension of filtration complex 
                mode="vr",                 # type of filtration complex 
                metric="euclidean")
plotBarcodeDiagram(irisInt0, max_dim, max_f, title="H0 Barcode plot of Iris Data")

지도학습

계층적 군집, k-means cluster, partitioning around medoids(PAM), 혼합분포 군집, 밀도기반 군집,서포트 벡터 머신 등이 있으며 추후 업데이트 예정


R에서 사용해보기

library(recommenderlab)
data_package=data(package='recommenderlab')
data_package$results[,'Item']
[1] "Jester5k"                   
[2] "JesterJokes (Jester5k)"     
[3] "MSWeb"                      
[4] "MovieLense"                 
[5] "MovieLenseMeta (MovieLense)"
data("MovieLense")
a=as(MovieLense,'matrix')
aa <- as(as(a, "matrix"), "realRatingMatrix")
head(a[1,1:5])
 Toy Story (1995)  GoldenEye (1995) Four Rooms (1995) 
                5                 3                 4 
Get Shorty (1995)    Copycat (1995) 
                3                 3 

유사도 행렬 계산

witch는 users 와 items를 지원하며, method는 cosine, pearson,jaccard를 지원한다.

similarity_users=similarity(MovieLense[1:4,],method='cosine',which='users')
as.matrix(similarity_users)
          1         2         3         4
1 0.0000000 0.9605820 0.8339504 0.9192637
2 0.9605820 0.0000000 0.9268716 0.9370341
3 0.8339504 0.9268716 0.0000000 0.9130323
4 0.9192637 0.9370341 0.9130323 0.0000000
image(as.matrix(similarity_users))

추천 item에 대한 유사도 계산

similarity_items=similarity(MovieLense[,1:4],method='cosine',which='items')
as.matrix(similarity_items)
                  Toy Story (1995) GoldenEye (1995)
Toy Story (1995)         0.0000000        0.9487374
GoldenEye (1995)         0.9487374        0.0000000
Four Rooms (1995)        0.9132997        0.9088797
Get Shorty (1995)        0.9429069        0.9394926
                  Four Rooms (1995) Get Shorty (1995)
Toy Story (1995)          0.9132997         0.9429069
GoldenEye (1995)          0.9088797         0.9394926
Four Rooms (1995)         0.0000000         0.8991940
Get Shorty (1995)         0.8991940         0.0000000
image(as.matrix(similarity_items))

적용 가능한 모델 확인과 설명

recommender_models=recommenderRegistry$get_entries(dataType='realRatingMatrix')
names(recommender_models)
[1] "ALS_realRatingMatrix"         
[2] "ALS_implicit_realRatingMatrix"
[3] "IBCF_realRatingMatrix"        
[4] "POPULAR_realRatingMatrix"     
[5] "RANDOM_realRatingMatrix"      
[6] "RERECOMMEND_realRatingMatrix" 
[7] "SVD_realRatingMatrix"         
[8] "SVDF_realRatingMatrix"        
[9] "UBCF_realRatingMatrix"        
lapply(recommender_models,'[[','description')
$`ALS_realRatingMatrix`
[1] "Recommender for explicit ratings based on latent factors, calculated by alternating least squares algorithm."

$ALS_implicit_realRatingMatrix
[1] "Recommender for implicit data based on latent factors, calculated by alternating least squares algorithm."

$IBCF_realRatingMatrix
[1] "Recommender based on item-based collaborative filtering."

$POPULAR_realRatingMatrix
[1] "Recommender based on item popularity."

$RANDOM_realRatingMatrix
[1] "Produce random recommendations (real ratings)."

$RERECOMMEND_realRatingMatrix
[1] "Re-recommends highly rated items (real ratings)."

$SVD_realRatingMatrix
[1] "Recommender based on SVD approximation with column-mean imputation."

$SVDF_realRatingMatrix
[1] "Recommender based on Funk SVD with gradient descend."

$UBCF_realRatingMatrix
[1] "Recommender based on user-based collaborative filtering."

매개변수에 대한 설명

data.frame(recommender_models$IBCF_realRatingMatrix$parameters)

모델을 좀더 대중적으로 일반화 해보자.

평점 분포

vector_ratings=as.vector(MovieLense@data)
table_ratings=table(vector_ratings)
table_ratings
vector_ratings
      0       1       2       3       4       5 
1469760    6059   11307   27002   33947   21077 
vector_ratings2=vector_ratings[vector_ratings!=0]
vector_ratings2=factor(vector_ratings2)
qplot(vector_ratings2)

조회수 상위랭킹 영화

views_per_movie=colCounts(MovieLense)
table_views=data.frame(movie=names(views_per_movie),views=views_per_movie)
table_views2=table_views[order(table_views$views,decreasing = T),]
ggplot(table_views[1:6,],aes(x=movie,y=views))+geom_bar(stat='identity')+
  theme(axis.text.x=element_text(angle=45,hjust=1))+
  ggtitle('Number of views of the top movies')

평균 영화 평점 분포와 조회수 100이상의 평균 평점 분포

average_ratings=colMeans(MovieLense)
qplot(average_ratings)+stat_bin(binwidth = .1)

average_ratings_relevant=average_ratings[views_per_movie>100]
qplot(average_ratings_relevant)+stat_bin(binwidth = .1)

결측 분포를 보여주는 히트맵

image(MovieLense)

영화를 많이 본 사용자와 많은 사용자가 본 영화를 시각화

min_n_movies=quantile(rowCounts(MovieLense),.99)
min_n_users=quantile(colCounts(MovieLense),.99)
image(MovieLense[rowCounts(MovieLense)>min_n_movies,
      colCounts(MovieLense)>min_n_users])

시청 횟수가 적은 영화는 정보부족으로 인해 평점에 편향을 미칠 수 있고, 거의 평점을 매기지 않은 사용자는 등급에 편향을 미칠 수 있다.

따라서, 영화를 50편 이상 이용한 사용자이고 적어도 100번 이상 시청된 영화를 추출하여 시각화

ratings_movies=MovieLense[rowCounts(MovieLense)>50,colCounts(MovieLense)>100]
ratings_movies=MovieLense[rowCounts(MovieLense)>50,colCounts(MovieLense)>100]
min_n_movies=quantile(rowCounts(ratings_movies),.98)
min_n_users=quantile(colCounts(ratings_movies),.98)
image(ratings_movies[rowCounts(ratings_movies)>min_n_movies,
                 colCounts(ratings_movies)>min_n_users])

평균 평점은 여러 사용자마다 크게 다름을 볼 수 있다.

average_ratings_per_user=rowMeans(ratings_movies)
qplot(average_ratings_per_user)+stat_bin(binwidth = .1)+ggtitle('Distribution of the average rating per user')

모든 영화에 높은 평점을 부여하는 고객은 결과를 왜곡시킬 수 있으므로 평균 평점이 0이되도록 변환(정규화)

ratings_movies_norm=normalize(ratings_movies)
image(ratings_movies_norm[rowCounts(ratings_movies_norm)>min_n_movies,
colCounts(ratings_movies_norm)>min_n_users])


데이터의 이진화

이진화의 방법 영화 평점을 매기면1, 아니면 0 상세 평점에 대한 정보가 손실되는게 단점.

평점이 일정 기준 이상인 경우 1, 아니면 0 이경우 나쁜 평점이 부여된 영화를 평가 대상에서 제외하는 효과

case1)

ratings_movies_watched=binarize(ratings_movies,minRating=1)
min_movies_binary=quantile(rowCounts(ratings_movies),.95)
min_users_binary=quantile(colCounts(ratings_movies),.95)
image(ratings_movies_watched[rowCounts(ratings_movies)>min_movies_binary,
                             colCounts(ratings_movies)>min_users_binary])

case2)

ratings_movies_good=binarize(ratings_movies,minRating=3)
image(ratings_movies_good[rowCounts(ratings_movies)>min_movies_binary,
                             colCounts(ratings_movies)>min_users_binary])


아이템 기반 협업 필터링

협업필터링 이란 : 여러 사용자에 대한 정보를 고려한 추천 형태

아이템을 추천하기 위해 서로 협력한다는 뜻에서 협업 필터링이라 표현

학습 알고리즘

  • 각 아이템에 대해 유사한 이용자의 자료를 바탕으로 서로 얼마나 유사한지 측정
  • 각 아이템에 대해 가장 유사한 아이템k 개 선별
  • 각 사용자에 대해 구매 내역과 가장 유사한 아이템 선별
which_train=sample(x=c(T,F),size=nrow(ratings_movies),replace=T,prob=c(.7,.3))
train=ratings_movies[which_train,]
test=ratings_movies[!which_train,]
which_set=sample(x=1:5,size=nrow(ratings_movies),replace=T)
for(i_model in 1:5){
  which_train=which_set==i_model
  train<-ratings_movies[which_train,]
  test<-ratings_movies[!which_train,]
}

method는 유사도 함수를 나타내며 k는 유사한 아이템의 개수를 의미

head(apply(model_details$sim,1,function(x)sum(x>0)))
       Toy Story (1995)        GoldenEye (1995) 
                     30                      30 
      Get Shorty (1995)   Twelve Monkeys (1995) 
                     30                      30 
            Babe (1995) Dead Man Walking (1995) 
                     30                      30 
Warning message:
In strsplit(code, "\n", fixed = TRUE) :
  input string 1 is invalid in this locale

예측 알고리즘

  • 해당 아이템과 관련된 구매 평점을 추출해 가중치로 사용
  • 해당 아이템과 관련된 구매 아이템의 유사도를 추출
  • 각 가중치에 관련 유사도를 곱하고 모든 곱한 결과를 더한다.
  • 상위 n개를 추천한다.
recc_predicted=predict(model,newdata=test,n=n_recommended)
t(data.frame(recc_predicted@items))
     [,1] [,2] [,3] [,4] [,5] [,6]
X2     56   73   96  109  127  152
X3     38  249  229    7  163   13
X5     34   62   99  168  186  251
X6     52  178  212  207  216  292
X7    171  158  209   81  252  198
X8     66  106  149  217  319  327
X10   130  181  109  325  305  314
X11   120  108  228  118  329  265
X14     5  139  280   33    7  208
X15   177  306  174  239  161  178
X16    17   20   22   40   44   55
X18    34  143  329   33  189  282
X21   213  319  315  101    7  253
X23    68  263  199  243  271  172
X24     1   23   29   34   48   87
X25    10   12   21   39   48   60
X26   208  228  246  296   47   52
X28     6  125  128  196  201  220
X37    31   40   56   83   91   99
X38     5   14   24   28   29   31
X43   228   44  222  194  121   68
X44    52   89  184  188  209  106
X48    11   58   79  168  206  242
X49   314  190  316  204  110  167
X56    14  304  232  309  255  276
X57   177  191  219  228  247  316
X58    71  145  188   52   39  326
X60   320  246   17  167  146  131
X63   298  249  148   28   72  194
X64   219  304  282   15  162  267
X65    14   17   34   61   71   72
X69    12   42   63   78  156  168
X70    51    9   33   31  295  146
X72   174  265  240  282  108   66
X73    31   56  179  330  210   55
X75    11  165  247  308  223  161
X77     9   21   28   40   44   70
X79     6   10   13   17   19   25
X81    10   11   47  295   32    9
X83   282  275   80  265  210  150
X84    38  162  192  269   66  159
X85   318  290  215  174  322   63
X87    28  180  121  289   80  244
X89     8   30   34   36   40   67
X90   209  327   47  216  206   95
X91    44   45   36  117  100  221
X94   190  219  327   81   65  186
X95   304  249  295  219  265    3
X96     3    6   15   16   18   32
X97     3    7   10   22   25   27
X99    31  183  289  108  161  282
X101   27   32   40   52  224  226
X102   92  101  292   59  158   66
X104   74  228   83  153   14  329
X106   25  235  251  304  263  228
X109  160  206  211  221  149  332
X110    4   14   80  162  128  229
X115    2   16   19   59   66   74
X116  229  104  209   46  108   13
X117   18   22   31   34   38   47
X118    3    9   20   21   23   30
X121   51  122  140  145  151  156
X122   25   40   42   58   63   68
X125  102  235   34  330  186  275
X128   93  328   79  320  190  183
X130  292  167   46   42  175   49
X135    2    3    4   14   16   29
X138   12   13   19   21   22   25
X144   80  301  282  283   91  157
X148    3    6    9   10   11   12
X151  202  187  251  162   34  219
X152   13   23   27   40   51   52
X154   13   88  101  181  303   79
X161   14   45   55   93  113  114
X167   13   28   29   33   35   49
X168    9   60   80  144  239   49
X174   42   44   45   55   59  110
X176   40   63   64  280  133  213
X178   81  318  249  215   33  276
X180    2    5    7   13   15   20
X181  127  108   99  198  199  134
X183   51   74  131  138  153  167
X184   52  184  327  281  321  290
X186   66   73  162  196  208  205
X188   79  145  188  174  160  206
X189   56   40  292  179  246  304
X190   12   50  168  266  194  237
X193   80   32  187  206  222   68
X195  134   52   46  206  229  160
X197  171   80   53  289  153   14
X198   38  206  212  325   17    5
X200  316  174  173  292  213  191
X201   79  314  282  326  269   52
X206   79  232  249   24  175   65
X207   51   80  251  132  162  229
X210   92  180  219  241  329  229
X214  152  217   15  314  143  136
X215   14   31   49  119  216  324
X216   22  240  246   36  149  192
X217    5   14   21   47   49   74
X218   30   54   74   78  107  116
X221   57  282  315  222  221  187
X227   55  100  275  323  314  226
X232   11  104  123  137  140  195
X233   17   40   44   45   66   75
X234  314  158  188  172   65  167
X235  172  283  315   51  124  230
X236  324  246  204   42   33   76
X239   11   85   90   98  112  113
X243   43  151  223  259  265   92
X244  162  233  157  190  263  208
X246  219  238   75  186   84  303
X248   65   74  133  201  265  286
X249   32  304  269  178  216   63
X251    3   43   65  158  188  232
X254  259   66   62   47  219  226
X255   12  314  141  319  196    1
X256   14   49   51  167  219  263
X262  158  208  173  141  157  145
X264    2   30   41   55   67   75
X267   51  106  329   80  251  167
X268  304   80  231  216  233  178
X269  186  222   25  304  192  289
X270   70   74   84  104  143  158
X271  162  137   38  301  328  228
X274   29   38   39   55  132  136
X275   32   43   94  131  135  158
X276  255  320  323  183  269  208
X277   57   75   10  118  105   34
X279  130  194    5  210  293  146
X280   14  235  167  308  269  178
X283    4    7   13   15   18   41
X286  188  329   67   80  158  186
X287   29   39   40   72   81  152
X288   45   54   68  101  112  116
X290  207  283  232   65  219  316
X292   88  241  171  325    8  134
X293  167  332  318  212   51   53
X294  121  233  322    7  321   37
X295   25   60   65  168  215  152
X296   32   72  217  289  222  317
X297  134   69  231  106  260  270
X298  185  251  272  298  311  328
X299  219  251  208  288  295  289
X301  250  200  148  119  198  199
X303  187  319  212  216   80  134
X305  115  282  283  231  147   40
X307   93   99  120  135  182  185
X308  178  317  233  222  205  289
X311  237  219  153  323   81  171
X312   46   50  133  206  220  332
X313  131  304  179  167  181  171
X314   59  157  162  208  219  222
X315   48   55   65   93  207  218
X316  164  172  186  214  273  275
X318   70   63  149  331  139  206
X320  144  102  215  305  190  163
X323  208   32  109  229  232  186
X324   58   64  178  206   16  220
X325  122  223  297  145  246   33
X327  290  185  219  222  205  318
X328   15  188  320  168   24  232
X330   20   24   46   51   92  102
X331   13   16   24   36   44   74
X332  236  178  107    9  131    3
X334  171  211   65   63  215  226
 [ reached getOption("max.print") -- omitted 295 rows ]
recc_user_1=recc_predicted@items[[1]]
movies_user_1=recc_predicted@itemLabels[recc_user_1]
recc_matrix=sapply(recc_predicted@items,function(x){colnames(ratings_movies)[x]})
number_of_items=factor(table(recc_matrix))
qplot(number_of_items)

number_of_items_sorted=sort(number_of_items,decreasing = T)
number_of_items_top=head(number_of_items_sorted,4)

사용자 기반

같은 사람들이 구매한 것과 유사한 아이템 선별 새 사용자에게 구매했던 것과 비슷한 아이템 추천

알고리즘

각 사용자와 새로운 사용자와의 유사도 측정 가장 유사한 사용자들을 선별(k nn 사용) 가장 유사한 사용자가 구매한 아이템들에 평점을 매긴다. 평점은 유사도를 가중치로 하는 가중평점이나 평균평점을 사용

nn은 유사한 사용자의 수를 의미(디폴트 =25)


model2=Recommender(data=train,method='UBCF')
model2
model_details=getModel(model2)
n_recommended=6
recc_predicted=predict(model2,test,n_recommended)

유저기반 협업필터링은 초기 데이터에 접근해야 하므로 사후학습모델에 해당된다. 또한 전체 데이터베이스에 리소스 소모가 심하다.

일반적으로 UBCF가 IBCF보다 좀 더 정확하다고 입증되었으므로 UBCF를 추천.


이진데이터에 대한 협업필터링

\(distance(item_1,item_2)=\frac{item_1 \cap item_2}{item_2 \cup item_2}\)

이럴 때 사용 가능 어떤 아이템을 구매했는지 알지만 평점을 모른다. 각 사용자에 대한 구매한 아이템을 알지 못하지만 좋아하는 아이템은 알고 있다.

아이템 기반


ratings_movies_watched=binarize(ratings_movies,minRating=1)
qplot(rowSums(ratings_movies_watched))+stat_bin(binwidth=10)

model=Recommender(train,method='IBCF',parameter=list(method='Jaccard'))
model_details=getModel(model)
n_recommended=6
recc_predicted=predict(model,test,n=n_recommended)
recc_matrix=sapply(recc_predicted@items,function(x){colnames(ratings_movies)[x]})

사용자기반


model=Recommender(train,method='UBCF',parameter=list(method='Jaccard'))
model_details=getModel(model)
n_recommended=6
recc_predicted=predict(model,test,n=n_recommended)
recc_matrix=sapply(recc_predicted@items,function(x){colnames(ratings_movies)[x]})

협업 필터링의 문제점

새로운 사용자가 아직 영화를 보지 못했으면 사용 불가능. IBCF의 경우 구매 아이템을 알아야 하며, UBCF는 기존 사용자가 새로운 사용자와 유사한 선호도를 가졌는지 알아야 한다.

새 아이템을 다른사람이 구매하지 않은 경우에는 절대로 추천되지 않음.

또한 평점 매트릭스만 고려하며, 사용자의 정보를 고려하지 못함.

콘텐츠 기반 필터링 아이템에 대한 세부정보로 시작되며 다른 사용자들을 고려할 필요가 없다. 각 사용자에 대해 알고리즘은 과거 구매와 유사한 아이템을 추천

하이브리드 추천 시스템

병렬 하이브리드 시스템 : 추천 모듈을 병렬로 각각 실행하고 그 결과를 결합한다. 각 사용자에 대한 추천 결과를 결합

파이프라인 하이브리드 시스템 : 서로 다른 추천 시스템을 차래대로 수행

모놀리식 하이브리드 시스템 : 동일한 추천 알고리즘 내에서 다양한 방식으로 수행한 결과를 통합.

LS0tDQp0aXRsZTogIlJlY29tbWVuZGF0aW9uIFN5c3RlbSINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6DQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQotLS0NCg0KDQpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSkNCmBgYA0KDQojIOy2lOyynOyLnOyKpO2FnHsudGFic2V0IC50YWJzZXQtZmFkZSAudGFic2V0LXBpbGxzfQ0KDQoq67O4IOuCtOyaqeydgCBCdWlsZGluZyBhIFJlY29tbWVuZGF0aW9uIFN5c3RlbSB3aXRoIFIg64K07Jqp7J2EIOq4sOuwmOycvOuhnCDsnpHshLEg65CY7JeI64ukLiAqDQoNCiMjIOy2lOyynCDsi5zsiqTthZzsnZgg7KKF66WYDQoNCisg7L2Y7YWQ7LigIOq4sOuwmCDstpTsspwg7Iuc7Iqk7YWcDQorIOyngOyLneq4sOuwmCDstpTsspwg7Iuc7Iqk7YWcDQorIO2VmOydtOu4jOumrOuTnCDsi5zsiqTthZwNCg0KIyMjIOy9mO2FkOy4oCDquLDrsJgg7LaU7LKcIOyLnOyKpO2FnA0KDQrsvZjthZDsuKAg6riw67CYIOy2lOyynCDsi5zsiqTthZwgOiDsgqzsmqnsnpDrk6Qg6rCE7JeQIOycoOyCrOuPhOulvCDqs6DroKTtlZjsl6wg7IKs7Jqp7J6Q65Ok7JeQ6rKMIOyVhOydtO2FnOydhCDstpTsspztlZjripQg7Iuc7Iqk7YWcDQoNCuuUsOudvOyEnCwg7IKs7Jqp7J6Q6rCAIOqzvOqxsOyXkCDsoovslYTtlojrjZgg7JWE7J207YWc65Ok6rO8IOu5hOyKt+2VnCDsoJztkojsnYQg7LaU7LKcDQoNCmV4KSDsmIHtmZTstpTsspwNCg0KDQrstpTsspzqs7zsoJXsl5DshJwg7KO867OAIOyCrOyaqeyekOuTpOydmCDshKDtmLjrj4Trpbwg6rOg66Ck7ZWY7KeAIOyViuqzoCDsgqzsmqnsnpDsnZgg6rO86rGwIOyEoO2YuOuPhOyZgCDslYTsnbTthZzsnZgg7IaN7ISxIOuwjyDtirnsp5XsnYQg7KSR7KCQ7Jy866GcIOy2lOyynO2VnOuLpC4NCg0KDQojIyMg7KeA7IudIOq4sOuwmCDstpTsspwg7Iuc7Iqk7YWcDQoNCuy2lOyynO2VmOq4sCDsoITsl5Ag7JWE7J207YWc7J2YIO2KueynleqzvCDrqoXsi5zsoIEg7KeI66y47J2EIO2Gte2VtCDtmo3rk53tlZwg7IKs7Jqp7J6QIOyEoO2YuOuPhOyZgCDstpTsspwg67KU7JyEIOuTseyXkOyEnCDsoJXrs7Trpbwg6rOg66Ck7ZW0IOy2lOyynA0KDQrrqqjrjbjsnZgg7KCV7ZmV64+E64qUIOy2lOyynOuQnCDslYTsnbTthZzsnbQg7Ja866eI64KYIOyCrOyaqeyekOyXkOqyjCDsnKDsmqntlZzqsIDrpbwg6riw67CY7Jy866GcIO2PieqwgA0KDQrsgqzsmqnsnpDrk6TsnZgg6rWs66ekIOydtOugpeydtCDsoIHsnYAg6rK97JqwIOyCrOyaqS4NCg0KKioqKg0KDQojIyDrjbDsnbTthLDsnZgg7KCE7LKY66asDQoNCuuMgOu2gOu2hOydmCDrjbDsnbTthLDrk6TsnYAg6rOg7LCo7JuQ7J2066mwIO2drOyGjO2VnCDrjbDsnbTthLDsnbTrr4DroZwg7J2066W8IOuwlOuhnCDrtoTshJ3rqqjrjbjsl5Ag7KCB7Jqp7ZWgIOqyveyasCDsmIjsuKHriqXroKXsnbQg7ZiE7KCA7ZWY6rKMIOuCruyVhOyngOuKlCDtmITsg4HsnbQg67Cc7IOd7ZWY64qU642wIOydtOulvCDssKjsm5DsnZgg7KCA7KO8KFRIZSBjdXNzZSBvZiBkaW1lbnNpb25hbGl0eSnrnbwg7ZWc64ukLiANCg0K7J2066W8IO2VtOyGjO2VmOuKlCDrsKnrspXspJEg7ZWY64KY66GcIOywqOybkOy2leyGjChEaW1lbnNpb25hbGl0eSBSZWR1Y3Rpb24p66W8IOyCrOyaqe2VnOuLpC4gDQrssKjsm5Ag7LaV7IaMIOq4sOuyleuTpOydmCDsooXrpZjroZzripQgUHJpbmNpcGFsIGNvbXBvbmVudCBhbmFseXNpcyhQQ0EpLCBOb24tbmVnYXRpdmUgTWF0cml4IGZhY3Rvcml6YXRpb24oTk1GKSwgVG9wb2xvZ2ljYWwgRGF0YSBBbmFseXNpcyhUREEpLCBhdXRvIGVuY29kZXIgLHQtU3RvY2hhc3RpYyBOZWlnaGJvciBFbWJlZGRpbmcoVC1TTkUpIOuTseydtCDsnojri6QuDQoNCioqKioNCg0KDQoNCiMjIyDsnKDsgqzrj4TsnZgg7Lih7KCVDQoNCuy2lOyynCDsi5zsiqTthZzsnYAg7J6Q66OM6rCEIOycoOyCrOuPhOulvCDqs6DroKTtlZjsl6wg7J6R64+ZDQoNCioq7Jyg7IKs64+E7J2YIDPqsIDsp4Ag7KGw6rG06rO8IOqxsOumrOydmCAz6rCA7KeAIOyhsOqxtCoqDQoNCisgKirsnKDsgqzrj4TsnZggM+qwgOyngCDsobDqsbQqKg0KDQrsnKDsgqzrj4TripQg7JaR7IiY7J207Ja07JW87ZWY66mwICRkKHgseSk9ZCh5LHgpJOulvCDrp4zsobHtlZjqs6AgJHMoeCx5KVxsZSBzKHgseCkk66W8IOunjOyhse2VmOyXrOyVvCDtlZzri6QuDQoNCisgKirqsbDrpqzsnZggM+qwgOyngCDsobDqsbQqKg0KDQrslpHsiJjsnbTslrTslbztlZjrqbAsICRkKHgseSk9ZCh5LHgpJCDspoksIOuMgOy5reyEseydhCDrp4zsobHtlZjrqbAsIOuPmeydvOyEsSDspoksIOyYpOyngSRkKHgseSk9MCTrlYzrp4wgJHg9eSTrpbwg66eM7KGx7ZWY7Jes7JW8IO2VnOuLpC4NCg0KKioqKiANCg0KIyMjIOqxsOumrOqzhOyCsOydmCDsooXrpZgNCg0KKyAqKuuvvOy9lOyasOyKpO2CpChNaW5Lb3dza2kpIOqxsOumrCoqDQoNCiREaXN0KHgseSk9W1xzdW0oeF97cml9LXhfe3NpfSlecF1eezEvcH0kDQoNCisgKirrp6jtlZjtirwg6rGw66asKioNCg0K65GQIOygkCDqsITsnZgg7LCo7J207J2YIOygiOuMgOqwkuydhCDtlantlZwg6rCSLCByPTHsnbwg65WM7J2YIOuvvOy9lOyasOyKpO2CpOydmCDqsbDrpqzsmYAg6rCZ7J2MLg0KJERpc3QoeCx5KT1cc3VtfHhfaS15X2l8JA0KDQorICoq7Jyg7YG066as65OcIOqxsOumrCoqIA0KDQrrr7zsvZTsmrDsiqTtgqTqsbDrpqzsnZgg7Yq567OEIOqyveyasChyPTIpDQrsnpDro4zsnZgg67aE7Y+s7KCBIO2KueyEseydhCDqs6DroKTtlaAg7IiYIOyXhuycvOupsCwg64uo7JyEIOuYkO2VnCDqsJnslYTslbztlZzri6TripQg64uo7KCQ7J20IOyhtOyerC4NCg0KJERpc3QoeCx5KT1cc3FydHtcc3VtKHhfe3JpfS14X3tzaX0pXjJ9PVxzcXJ0e1tYX3ItWF9zXV5UW1hfci1YX3NdfSQNCg0KDQorICoq7LK067mE7IWw7ZSEIOqxsOumrCg9bWF4aW11bSBkaXN0YW5jZSkqKg0KDQrrkZAg7KeR64uo7JeQ7IScIOqwgOyepSDquLQg7KeA7KCQ7JeQ7ISc7J2YIOqxsOumrA0KDQokbWF4KHx4XzEteV8xfCxcY2RvdHMsfHhfcC15X3B8JA0KDQoNCisgKirtkZzspIDtmZQg6rGw66asKioNCg0K7Jyg7YG066as65OcIOqxsOumrOulvCDqs7XrtoTsgrDsnLzroZwg64KY64iIIOqxsOumrA0KDQokWyhYX3ItWF9zKV5URF57LTF9KFhfci1YX3MpXV57MS8yfSAsIEQ9ZGlhZyhTX3sxMX0sXGNkb3RzLFNfe3BwfSkkDQoNCisgKirrp4jtlaDrnbzrhbjruYTsiqQoTWFoYWxhbm9iaXMpIOqxsOumrCoqDQoNCuqzteu2hOyCsCDqtazsobDrpbwg7ZWo6ruYIOqzoOugpO2VnCDthrXqs4TsoIEg6rGw66asLCDsnpDro4zsnZgg67aE7Y+s7KCBIO2KueyEseydhCDqs6DroKTtlZjquLAg7JyE7ZWcIOuwqeuylS4gDQoNCirssLjqs6A66rO167aE7IKw7J20IOuLqOychO2WieugrOydtCDrkJjrqbQg7Jyg7YG066as65Oc6rGw66as7JmAIOqwmeyVhOyngOuKlOuNsCDsnbTrn6ztlZwg67OA7ZmY7J2EIO2ZlOydtO2KuOuLneuzgO2ZmOydtOudvCDtlZzri6QuKg0KDQokWyhYX3ItWF9zKV5UU157LTF9KFhfci1YX3MpXV57MS8yfSAsIFM97ZGc67O46rO167aE7IKw7ZaJ66CsJA0KDQoNCisgKirsupTrsoTrnbwg6rGw66asICoqDQoNCuunqO2VmO2KvCDqsbDrpqzsl5Ag6rCA7KSR7LmY66W8IOyggeyaqe2VnCDqsbDrpqzsnZgg6rCc64WQDQoNCg0KJGRpc3QoeCx5KT1cc3VtXGZyYWN7fHhfaS15X2l8fXt4X2kteV9pfSQNCg0KbWV0aG9k7JeQIGV1Y2xpZGVhbiwgbWF4aW11bSwgbWFuaGF0dGFuLCBjYW5iZXJhLCBtaW5rb3dza2kg65Ox7J20IOuTpOyWtOqwiCDsiJgg7J6I64ukLiANCg0KYGBge3J9DQp4MT1ybm9ybSgzMCkNCngyPXJub3JtKDMwKQ0KZGlzdChyYmluZCh4MSx4MiksbWV0aG9kPSdldWNsaWRlYW4nKQ0KYGBgDQoNCg0KKyAqKuy9lOyCrOyduCDqsbDrpqwqKg0KDQokY29zKFhfcixYX3MpPVxmcmFje1hfclx0aW1lcyBYX3N9e3xYX3J8fFhfc3x9JA0KDQrsvZTsgqzsnbgg7Jyg7IKs64+E64qUIOustOqyjOuCmCDtgazquLDripQg7KCE7ZiAIOqzoOugpO2VmOyngCDslYrqs6Ag67Kh7YSwIOyCrOydtOydmCDqsIHrj4Trp4zsnLzroZwg7Lih7KCV7ZWY64qUIOqyg+qzvCDsnKDsgqwNCg0KYGBge3J9DQpsaWJyYXJ5KCdsc2EnKQ0KeDE9c2FtcGxlKGMoMCwxKSwzMCxyZXBsYWNlID0gVCkNCngyPXNhbXBsZShjKDAsMSksMzAscmVwbGFjZSA9IFQpDQpjb3NpbmUoeDEseDIpDQpgYGANCg0KKyAqKu2UvOyWtOyKqCDqsbDrpqwgKioNCg0K7ZS87Ja07IqoIOyDgeq0gOqzhOyImOydmCDrspTsnITqsIAgLTF+MeydtOuvgOuhnCAx7JeQ7IScIO2UvOyWtOyKqCDsg4HqtIDqs4TsiJjrpbwg67qAIOqwkuydhCDqsbDrpqzroZwg7IKs7JqpDQoNCioqKioqDQoNCiMjIOywqCDsm5Ag7LaVIOyGjCDquLAg67KVDQoNCioqUENBKioNCg0K7LaV7J2EIOuzgO2ZmO2VmOyXrCDtlonroKzsnYQg7IOI66Gc7Jq0IO2Yle2DnOuhnCDrs4DtmZjtlZjripQg6riw67KVLiDsnpDssrTroZzripQg7LCo7JuQ7J20IOykhOyWtOuTpOyngCDslYrripTri6QuIA0K7ISg7ZiVIOu2hOyEneuwqeyLneydtOuvgOuhnCDruYTshKDtmJXshLHsnYQg6rOg66Ck7ZWY7KeAIOuqu+2VnOuLpOuKlCDri6jsoJDsnbQg7KG07J6sDQoNClByb3BlcnRpb24gb2YgVmFyaWFuY2U6IOu2hOyCsOu5hOycqCwg6rCBIOyjvOyEseu2hOydmCDssKjsp4DtlZjripQg67mE7Jyo7J2EIOunkO2VmOupsCDtgbQg7IiY66GdIOyYge2WpeydhCDrp47snbQg66+47Lmc64uk64qUIOqyg+ydhCDsnZjrr7jtlZzri6QuIEN1bXVsYXRpdmUgUHJvcG9ydGlvbjog67aE7IKw7J2YIOuIhOyggSDtlanqs4QNCg0KcHJjb21wIOydmCDqsr3smrAgU1ZEKO2KueydtOqwkiDrtoTtlbQp66W8IOyCrOyaqe2VmOqzoCwgcHJpbmNvbXDsnZgg6rK97JqwIOyKpO2Mqe2KuOufvCDrtoTtlbTrpbwg7IKs7Jqp7ZWc64uk64qUIOy4oeuptOyXkOyEnCBwcmNvbXDsnZgg7IiY7LmYIOygle2ZleuPhOqwgCDrjZQg64aS64ukLg0KDQpgYGB7cn0NCmRhdGEoVVNBcnJlc3RzKQ0Kcm93bmFtZXMoVVNBcnJlc3RzKQ0KbmFtZXMoVVNBcnJlc3RzKQ0KaGVhZChVU0FycmVzdHMpDQoj67OA7IiY67OEIOu2hOyCsA0KYXBwbHkoVVNBcnJlc3RzLDIsdmFyKQ0KcGNhPXByY29tcChVU0FycmVzdHMsc2NhbGU9VCkNCnN1bW1hcnkocGNhKQ0KYmlwbG90KHBjYSkNCnBsb3QocGNhKQ0KYGBgDQoNCioqdC1TTkUqKg0KDQpbdC1zbmUg7ISk66qFXShodHRwczovL3d3dy5zbGlkZXNoYXJlLm5ldC9zc3VzZXIwNmUwYzUvdmlzdWFsaXppbmctZGF0YS11c2luZy10c25lLTczNjIxMDMzKQ0KDQpgYGB7cn0NCg0KbGlicmFyeShSdHNuZSkNCg0KdHNuZT1SdHNuZShVU0FycmVzdHMsZGltPTIsIHBlcnBsZXhpdHk9MTAsIHZlcmJvc2U9VFJVRSwgbWF4X2l0ZXIgPSA1MDApDQoNCmxpYnJhcnkodHNuZSkNCmRhdGEoaXJpcykNCmlyaXNfdHNuZSA9IHRzbmU6OnRzbmUoYXMubWF0cml4KGlyaXNbMTo0XSkpDQpkZl9pcmlzX3RzbmUgPSBpcmlzX3RzbmUgJT4lIA0KICBhcy5kYXRhLmZyYW1lKCkgJT4lIA0KICB0YmxfZGYoKSAlPiUgDQogIG11dGF0ZShzcGVjaWVzID0gaXJpcyRTcGVjaWVzKQ0KZGZfaXJpc190c25lICU+JSANCiAgZ2dwbG90KGFlcyh4ID0gVjEsIHkgPSBWMiwgY29sb3IgPSBzcGVjaWVzKSkgKw0KICBnZW9tX3BvaW50KCkgKw0KICBzY2FsZV9jb2xvcl9icmV3ZXIocGFsZXR0ZSA9ICdQYWlyZWQnKSArDQogIGdndGl0bGUoJ3QtU05FIHJlc3VsdCA6IGlyaXMgZGF0YXNldCAoUmVhbCBTcGVjaWVzKScpICsNCiAgdGhlbWUoYXhpcy50aXRsZS54ID0gZWxlbWVudF9ibGFuaygpLA0KICAgICAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X2JsYW5rKCkpDQoNCmBgYA0KDQoqKk5NRioqDQoNCjHqsJzsnZgg642w7J207YSwIO2WieugrOydhCAy6rCc66GcIOu2hOumrA0KDQpbTk1G7ISk66qFXShodHRwczovL2JjaG8udGlzdG9yeS5jb20vMTIxNikNCg0KYGBge3J9DQpsaWJyYXJ5KE5NRikNCnJlcz1ubWYoVVNBcnJlc3RzLDMpDQoNClYuaGF0IDwtIGZpdHRlZChyZXMpIA0KdyA8LSBiYXNpcyhyZXMpDQoNCmBgYA0KDQoqKlREQSoqDQoNCltUREEg7ISk66qFXShodHRwczovL3NreWVvbmcubmV0LzE1NCkNCg0KYGBge3J9DQojIGluc3RhbGxfdmVyc2lvbigncGhvbScpDQpsaWJyYXJ5KHBob20pICMgcGhvbSDshKTsuZgg7ZmV7J24IA0KZGF0YSA9IGFzLm1hdHJpeChpcmlzWywtNV0pIA0KaGVhZChkYXRhKSANCm1heF9kaW0gPSAwIA0KbWF4X2YgPSAxIA0KaXJpc0ludDAgPSBwSG9tKGRhdGEsIGRpbWVuc2lvbj1tYXhfZGltLCMgbWF4aW11bSBkaW1lbnNpb24gb2YgcGVyc2lzdGVudCBob21vbG9neSBjb21wdXRlZCANCiAgICAgICAgICAgICAgICBtYXhfZmlsdHJhdGlvbl92YWx1ZT1tYXhfZiwgIyBtYXhpbXVtIGRpbWVuc2lvbiBvZiBmaWx0cmF0aW9uIGNvbXBsZXggDQogICAgICAgICAgICAgICAgbW9kZT0idnIiLCAgICAgICAgICAgICAgICAgIyB0eXBlIG9mIGZpbHRyYXRpb24gY29tcGxleCANCiAgICAgICAgICAgICAgICBtZXRyaWM9ImV1Y2xpZGVhbiIpDQpwbG90QmFyY29kZURpYWdyYW0oaXJpc0ludDAsIG1heF9kaW0sIG1heF9mLCB0aXRsZT0iSDAgQmFyY29kZSBwbG90IG9mIElyaXMgRGF0YSIpDQoNCmBgYA0KDQoNCioq7KeA64+E7ZWZ7Iq1KioNCg0K6rOE7Li17KCBIOq1sOynkSwgay1tZWFucyBjbHVzdGVyLCBwYXJ0aXRpb25pbmcgYXJvdW5kIG1lZG9pZHMoUEFNKSwg7Zi87ZWp67aE7Y+sIOq1sOynkSwg67CA64+E6riw67CYIOq1sOynkSzshJztj6ztirgg67Kh7YSwIOuouOyLoCDrk7HsnbQg7J6I7Jy866mwIOy2lO2bhCDsl4XrjbDsnbTtirgg7JiI7KCVDQoNCioqKioqDQoNCiMjIFLsl5DshJwg7IKs7Jqp7ZW067O06riwIA0KDQpgYGB7cn0NCg0KbGlicmFyeShyZWNvbW1lbmRlcmxhYikNCmRhdGFfcGFja2FnZT1kYXRhKHBhY2thZ2U9J3JlY29tbWVuZGVybGFiJykNCmRhdGFfcGFja2FnZSRyZXN1bHRzWywnSXRlbSddDQpkYXRhKCJNb3ZpZUxlbnNlIikNCg0KDQphPWFzKE1vdmllTGVuc2UsJ21hdHJpeCcpDQphYSA8LSBhcyhhcyhhLCAibWF0cml4IiksICJyZWFsUmF0aW5nTWF0cml4IikNCg0KaGVhZChhWzEsMTo1XSkNCmBgYA0KDQoNCuycoOyCrOuPhCDtlonroKwg6rOE7IKwDQoNCndpdGNo64qUIHVzZXJzIOyZgCBpdGVtc+ulvCDsp4Dsm5DtlZjrqbAsDQptZXRob2TripQgY29zaW5lLCBwZWFyc29uLGphY2NhcmTrpbwg7KeA7JuQ7ZWc64ukLg0KDQpgYGB7cn0NCnNpbWlsYXJpdHlfdXNlcnM9c2ltaWxhcml0eShNb3ZpZUxlbnNlWzE6NCxdLG1ldGhvZD0nY29zaW5lJyx3aGljaD0ndXNlcnMnKQ0KYXMubWF0cml4KHNpbWlsYXJpdHlfdXNlcnMpDQppbWFnZShhcy5tYXRyaXgoc2ltaWxhcml0eV91c2VycykpDQpgYGANCg0K7LaU7LKcIGl0ZW3sl5Ag64yA7ZWcIOycoOyCrOuPhCDqs4TsgrANCg0KYGBge3J9DQpzaW1pbGFyaXR5X2l0ZW1zPXNpbWlsYXJpdHkoTW92aWVMZW5zZVssMTo0XSxtZXRob2Q9J2Nvc2luZScsd2hpY2g9J2l0ZW1zJykNCmFzLm1hdHJpeChzaW1pbGFyaXR5X2l0ZW1zKQ0KDQppbWFnZShhcy5tYXRyaXgoc2ltaWxhcml0eV9pdGVtcykpDQoNCmBgYA0KDQrsoIHsmqkg6rCA64ql7ZWcIOuqqOuNuCDtmZXsnbjqs7wg7ISk66qFDQoNCmBgYHtyfQ0KcmVjb21tZW5kZXJfbW9kZWxzPXJlY29tbWVuZGVyUmVnaXN0cnkkZ2V0X2VudHJpZXMoZGF0YVR5cGU9J3JlYWxSYXRpbmdNYXRyaXgnKQ0KbmFtZXMocmVjb21tZW5kZXJfbW9kZWxzKQ0KbGFwcGx5KHJlY29tbWVuZGVyX21vZGVscywnW1snLCdkZXNjcmlwdGlvbicpDQoNCmBgYA0KDQrrp6TqsJzrs4DsiJjsl5Ag64yA7ZWcIOyEpOuqhQ0KDQpgYGB7cn0NCmRhdGEuZnJhbWUocmVjb21tZW5kZXJfbW9kZWxzJElCQ0ZfcmVhbFJhdGluZ01hdHJpeCRwYXJhbWV0ZXJzKQ0KYGBgDQoNCuuqqOuNuOydhCDsooDrjZQg64yA7KSR7KCB7Jy866GcIOydvOuwmO2ZlCDtlbTrs7TsnpAuDQoNCg0K7Y+J7KCQIOu2hO2PrA0KDQpgYGB7cn0NCnZlY3Rvcl9yYXRpbmdzPWFzLnZlY3RvcihNb3ZpZUxlbnNlQGRhdGEpDQp0YWJsZV9yYXRpbmdzPXRhYmxlKHZlY3Rvcl9yYXRpbmdzKQ0KdGFibGVfcmF0aW5ncw0KDQp2ZWN0b3JfcmF0aW5nczI9dmVjdG9yX3JhdGluZ3NbdmVjdG9yX3JhdGluZ3MhPTBdDQp2ZWN0b3JfcmF0aW5nczI9ZmFjdG9yKHZlY3Rvcl9yYXRpbmdzMikNCg0KDQpxcGxvdCh2ZWN0b3JfcmF0aW5nczIpDQpgYGANCg0K7KGw7ZqM7IiYIOyDgeychOuere2CuSDsmIHtmZQNCg0KYGBge3J9DQp2aWV3c19wZXJfbW92aWU9Y29sQ291bnRzKE1vdmllTGVuc2UpDQp0YWJsZV92aWV3cz1kYXRhLmZyYW1lKG1vdmllPW5hbWVzKHZpZXdzX3Blcl9tb3ZpZSksdmlld3M9dmlld3NfcGVyX21vdmllKQ0KdGFibGVfdmlld3MyPXRhYmxlX3ZpZXdzW29yZGVyKHRhYmxlX3ZpZXdzJHZpZXdzLGRlY3JlYXNpbmcgPSBUKSxdDQoNCmdncGxvdCh0YWJsZV92aWV3c1sxOjYsXSxhZXMoeD1tb3ZpZSx5PXZpZXdzKSkrZ2VvbV9iYXIoc3RhdD0naWRlbnRpdHknKSsNCiAgdGhlbWUoYXhpcy50ZXh0Lng9ZWxlbWVudF90ZXh0KGFuZ2xlPTQ1LGhqdXN0PTEpKSsNCiAgZ2d0aXRsZSgnTnVtYmVyIG9mIHZpZXdzIG9mIHRoZSB0b3AgbW92aWVzJykNCmBgYA0KDQrtj4nqt6Ag7JiB7ZmUIO2PieygkCDrtoTtj6zsmYAg7KGw7ZqM7IiYIDEwMOydtOyDgeydmCDtj4nqt6Ag7Y+J7KCQIOu2hO2PrA0KDQpgYGB7cn0NCmF2ZXJhZ2VfcmF0aW5ncz1jb2xNZWFucyhNb3ZpZUxlbnNlKQ0KcXBsb3QoYXZlcmFnZV9yYXRpbmdzKStzdGF0X2JpbihiaW53aWR0aCA9IC4xKQ0KDQphdmVyYWdlX3JhdGluZ3NfcmVsZXZhbnQ9YXZlcmFnZV9yYXRpbmdzW3ZpZXdzX3Blcl9tb3ZpZT4xMDBdDQpxcGxvdChhdmVyYWdlX3JhdGluZ3NfcmVsZXZhbnQpK3N0YXRfYmluKGJpbndpZHRoID0gLjEpDQoNCmBgYA0KDQrqsrDsuKEg67aE7Y+s66W8IOuztOyXrOyjvOuKlCDtnojtirjrp7UNCg0KYGBge3J9DQppbWFnZShNb3ZpZUxlbnNlKQ0KDQpgYGANCg0K7JiB7ZmU66W8IOunjuydtCDrs7gg7IKs7Jqp7J6Q7JmAIOunjuydgCDsgqzsmqnsnpDqsIAg67O4IOyYge2ZlOulvCDsi5zqsIHtmZQgDQoNCmBgYHtyfQ0KbWluX25fbW92aWVzPXF1YW50aWxlKHJvd0NvdW50cyhNb3ZpZUxlbnNlKSwuOTkpDQptaW5fbl91c2Vycz1xdWFudGlsZShjb2xDb3VudHMoTW92aWVMZW5zZSksLjk5KQ0KaW1hZ2UoTW92aWVMZW5zZVtyb3dDb3VudHMoTW92aWVMZW5zZSk+bWluX25fbW92aWVzLA0KICAgICAgY29sQ291bnRzKE1vdmllTGVuc2UpPm1pbl9uX3VzZXJzXSkNCg0KYGBgDQoNCuyLnOyyrSDtmp/siJjqsIAg7KCB7J2AIOyYge2ZlOuKlCDsoJXrs7TrtoDsobHsnLzroZwg7J247ZW0IO2PieygkOyXkCDtjrjtlqXsnYQg66+47LmgIOyImCDsnojqs6AsIOqxsOydmCDtj4nsoJDsnYQg66ek6riw7KeAIOyViuydgCDsgqzsmqnsnpDripQg65Ox6riJ7JeQIO2OuO2WpeydhCDrr7jsuaAg7IiYIOyeiOuLpC4gDQoNCuuUsOudvOyEnCwg7JiB7ZmU66W8IDUw7Y64IOydtOyDgSDsnbTsmqntlZwg7IKs7Jqp7J6Q7J206rOgIOyggeyWtOuPhCAxMDDrsogg7J207IOBIOyLnOyyreuQnCDsmIHtmZTrpbwg7LaU7Lac7ZWY7JesIOyLnOqwge2ZlA0KDQpgYGB7cn0NCnJhdGluZ3NfbW92aWVzPU1vdmllTGVuc2Vbcm93Q291bnRzKE1vdmllTGVuc2UpPjUwLGNvbENvdW50cyhNb3ZpZUxlbnNlKT4xMDBdDQoNCnJhdGluZ3NfbW92aWVzPU1vdmllTGVuc2Vbcm93Q291bnRzKE1vdmllTGVuc2UpPjUwLGNvbENvdW50cyhNb3ZpZUxlbnNlKT4xMDBdDQoNCm1pbl9uX21vdmllcz1xdWFudGlsZShyb3dDb3VudHMocmF0aW5nc19tb3ZpZXMpLC45OCkNCm1pbl9uX3VzZXJzPXF1YW50aWxlKGNvbENvdW50cyhyYXRpbmdzX21vdmllcyksLjk4KQ0KaW1hZ2UocmF0aW5nc19tb3ZpZXNbcm93Q291bnRzKHJhdGluZ3NfbW92aWVzKT5taW5fbl9tb3ZpZXMsDQogICAgICAgICAgICAgICAgIGNvbENvdW50cyhyYXRpbmdzX21vdmllcyk+bWluX25fdXNlcnNdKQ0KDQpgYGANCg0KDQoNCu2Pieq3oCDtj4nsoJDsnYAg7Jes65+sIOyCrOyaqeyekOuniOuLpCDtgazqsowg64uk66aE7J2EIOuzvCDsiJgg7J6I64ukLg0KDQpgYGB7cn0NCmF2ZXJhZ2VfcmF0aW5nc19wZXJfdXNlcj1yb3dNZWFucyhyYXRpbmdzX21vdmllcykNCnFwbG90KGF2ZXJhZ2VfcmF0aW5nc19wZXJfdXNlcikrc3RhdF9iaW4oYmlud2lkdGggPSAuMSkrZ2d0aXRsZSgnRGlzdHJpYnV0aW9uIG9mIHRoZSBhdmVyYWdlIHJhdGluZyBwZXIgdXNlcicpDQpgYGANCg0K66qo65OgIOyYge2ZlOyXkCDrhpLsnYAg7Y+J7KCQ7J2EIOu2gOyXrO2VmOuKlCDqs6DqsJ3snYAg6rKw6rO866W8IOyZnOqzoeyLnO2CrCDsiJgg7J6I7Jy866+A66GcIO2Pieq3oCDtj4nsoJDsnbQgMOydtOuQmOuPhOuhnSDrs4DtmZgo7KCV6rec7ZmUKQ0KYGBge3J9DQpyYXRpbmdzX21vdmllc19ub3JtPW5vcm1hbGl6ZShyYXRpbmdzX21vdmllcykNCmltYWdlKHJhdGluZ3NfbW92aWVzX25vcm1bcm93Q291bnRzKHJhdGluZ3NfbW92aWVzX25vcm0pPm1pbl9uX21vdmllcywNCmNvbENvdW50cyhyYXRpbmdzX21vdmllc19ub3JtKT5taW5fbl91c2Vyc10pDQoNCmBgYA0KDQoqKioqDQoNCioq642w7J207YSw7J2YIOydtOynhO2ZlCoqDQoNCuydtOynhO2ZlOydmCDrsKnrspUNCuyYge2ZlCDtj4nsoJDsnYQg66ek6riw66m0MSwg7JWE64uI66m0IDANCuyDgeyEuCDtj4nsoJDsl5Ag64yA7ZWcIOygleuztOqwgCDshpDsi6TrkJjripTqsowg64uo7KCQLg0KDQrtj4nsoJDsnbQg7J287KCVIOq4sOykgCDsnbTsg4Hsnbgg6rK97JqwIDEsIOyVhOuLiOuptCAwDQrsnbTqsr3smrAg64KY7IGcIO2PieygkOydtCDrtoDsl6zrkJwg7JiB7ZmU66W8IO2PieqwgCDrjIDsg4Hsl5DshJwg7KCc7Jm47ZWY64qUIO2aqOqzvA0KDQpjYXNlMSkNCg0KYGBge3J9DQpyYXRpbmdzX21vdmllc193YXRjaGVkPWJpbmFyaXplKHJhdGluZ3NfbW92aWVzLG1pblJhdGluZz0xKQ0KbWluX21vdmllc19iaW5hcnk9cXVhbnRpbGUocm93Q291bnRzKHJhdGluZ3NfbW92aWVzKSwuOTUpDQptaW5fdXNlcnNfYmluYXJ5PXF1YW50aWxlKGNvbENvdW50cyhyYXRpbmdzX21vdmllcyksLjk1KQ0KDQppbWFnZShyYXRpbmdzX21vdmllc193YXRjaGVkW3Jvd0NvdW50cyhyYXRpbmdzX21vdmllcyk+bWluX21vdmllc19iaW5hcnksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNvbENvdW50cyhyYXRpbmdzX21vdmllcyk+bWluX3VzZXJzX2JpbmFyeV0pDQpgYGANCg0KY2FzZTIpDQoNCmBgYHtyfQ0KcmF0aW5nc19tb3ZpZXNfZ29vZD1iaW5hcml6ZShyYXRpbmdzX21vdmllcyxtaW5SYXRpbmc9MykNCmltYWdlKHJhdGluZ3NfbW92aWVzX2dvb2Rbcm93Q291bnRzKHJhdGluZ3NfbW92aWVzKT5taW5fbW92aWVzX2JpbmFyeSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY29sQ291bnRzKHJhdGluZ3NfbW92aWVzKT5taW5fdXNlcnNfYmluYXJ5XSkNCmBgYA0KDQoNCioqKioqDQoNCioq7JWE7J207YWcIOq4sOuwmCDtmJHsl4Ug7ZWE7YSw66eBKioNCg0K7ZiR7JeF7ZWE7YSw66eBIOydtOuegCA6IOyXrOufrCDsgqzsmqnsnpDsl5Ag64yA7ZWcIOygleuztOulvCDqs6DroKTtlZwg7LaU7LKcIO2Yle2DnA0KDQrslYTsnbTthZzsnYQg7LaU7LKc7ZWY6riwIOychO2VtCDshJzroZwg7ZiR66Cl7ZWc64uk64qUIOucu+yXkOyEnCDtmJHsl4Ug7ZWE7YSw66eB7J206528IO2RnO2YhA0KDQoqKu2VmeyKtSDslYzqs6DrpqzsppgqKg0KDQorIOqwgSDslYTsnbTthZzsl5Ag64yA7ZW0IOycoOyCrO2VnCDsnbTsmqnsnpDsnZgg7J6Q66OM66W8IOuwlO2DleycvOuhnCDshJzroZwg7Ja866eI64KYIOycoOyCrO2VnOyngCDsuKHsoJUNCisg6rCBIOyVhOydtO2FnOyXkCDrjIDtlbQg6rCA7J6lIOycoOyCrO2VnCDslYTsnbTthZxrIOqwnCDshKDrs4QNCisg6rCBIOyCrOyaqeyekOyXkCDrjIDtlbQg6rWs66ekIOuCtOyXreqzvCDqsIDsnqUg7Jyg7IKs7ZWcIOyVhOydtO2FnCDshKDrs4QNCg0KYGBge3J9DQp3aGljaF90cmFpbj1zYW1wbGUoeD1jKFQsRiksc2l6ZT1ucm93KHJhdGluZ3NfbW92aWVzKSxyZXBsYWNlPVQscHJvYj1jKC43LC4zKSkNCnRyYWluPXJhdGluZ3NfbW92aWVzW3doaWNoX3RyYWluLF0NCnRlc3Q9cmF0aW5nc19tb3ZpZXNbIXdoaWNoX3RyYWluLF0NCndoaWNoX3NldD1zYW1wbGUoeD0xOjUsc2l6ZT1ucm93KHJhdGluZ3NfbW92aWVzKSxyZXBsYWNlPVQpDQpmb3IoaV9tb2RlbCBpbiAxOjUpew0KICB3aGljaF90cmFpbj13aGljaF9zZXQ9PWlfbW9kZWwNCiAgdHJhaW48LXJhdGluZ3NfbW92aWVzW3doaWNoX3RyYWluLF0NCiAgdGVzdDwtcmF0aW5nc19tb3ZpZXNbIXdoaWNoX3RyYWluLF0NCn0NCmBgYA0KDQptZXRob2TripQg7Jyg7IKs64+EIO2VqOyImOulvCDrgpjtg4DrgrTrqbAga+uKlCDsnKDsgqztlZwg7JWE7J207YWc7J2YIOqwnOyImOulvCDsnZjrr7gNCg0KYGBge3J9DQpyZWNvbW1lbmRlcl9tb2RlbHM9cmVjb21tZW5kZXJSZWdpc3RyeSRnZXRfZW50cmllcyhkYXRhVHlwZT0ncmVhbFJhdGluZ01hdHJpeCcpDQptb2RlbD1SZWNvbW1lbmRlcihkYXRhPXRyYWluLG1ldGhvZD0nSUJDRicscGFyYW1ldGVyPWxpc3Qoaz0zMCkpDQptb2RlbF9kZXRhaWxzPWdldE1vZGVsKG1vZGVsKQ0KI+ycoOyCrOuPhCDtlonroKwNCm1vZGVsX2RldGFpbHMkc2ltDQpuX2l0ZW1zX3RvcD0yMA0KDQpoZWFkKGFwcGx5KG1vZGVsX2RldGFpbHMkc2ltLDEsZnVuY3Rpb24oeClzdW0oeD4wKSkpDQppbWFnZShtb2RlbF9kZXRhaWxzJHNpbVsxOjIwLDE6NTBdKQ0KDQpyb3dfc3Vtcz1yb3dTdW1zKG1vZGVsX2RldGFpbHMkc2ltPjApDQpjb2xfc3Vtcz1jb2xTdW1zKG1vZGVsX2RldGFpbHMkc2ltPjApDQpxcGxvdChjb2xfc3Vtcykrc3RhdF9iaW4oYmlud2lkdGggPSAxKQ0Kd2hpY2hfbWF4PW9yZGVyKGNvbF9zdW1zLGRlY3JlYXNpbmcgPSBUKVsxOjEwXQ0Kcm93bmFtZXMobW9kZWxfZGV0YWlscyRzaW0pW3doaWNoX21heF0NCg0KI+y2lOyynOuqqOuNuOydmCDsoIHsmqkNCm5fcmVjb21tZW5kZWQgPTYNCg0KYGBgDQoNCioq7JiI7LihIOyVjOqzoOumrOymmCoqDQoNCisg7ZW064u5IOyVhOydtO2FnOqzvCDqtIDroKjrkJwg6rWs66ekIO2PieygkOydhCDstpTstpztlbQg6rCA7KSR7LmY66GcIOyCrOyaqQ0KKyDtlbTri7kg7JWE7J207YWc6rO8IOq0gOugqOuQnCDqtazrp6Qg7JWE7J207YWc7J2YIOycoOyCrOuPhOulvCDstpTstpwNCisg6rCBIOqwgOykkey5mOyXkCDqtIDroKgg7Jyg7IKs64+E66W8IOqzse2VmOqzoCDrqqjrk6Ag6rOx7ZWcIOqysOqzvOulvCDrjZTtlZzri6QuDQorIOyDgeychCBu6rCc66W8IOy2lOyynO2VnOuLpC4NCg0KYGBge3J9DQpyZWNjX3ByZWRpY3RlZD1wcmVkaWN0KG1vZGVsLG5ld2RhdGE9dGVzdCxuPW5fcmVjb21tZW5kZWQpDQp0KGRhdGEuZnJhbWUocmVjY19wcmVkaWN0ZWRAaXRlbXMpKQ0KcmVjY191c2VyXzE9cmVjY19wcmVkaWN0ZWRAaXRlbXNbWzFdXQ0KbW92aWVzX3VzZXJfMT1yZWNjX3ByZWRpY3RlZEBpdGVtTGFiZWxzW3JlY2NfdXNlcl8xXQ0KcmVjY19tYXRyaXg9c2FwcGx5KHJlY2NfcHJlZGljdGVkQGl0ZW1zLGZ1bmN0aW9uKHgpe2NvbG5hbWVzKHJhdGluZ3NfbW92aWVzKVt4XX0pDQoNCm51bWJlcl9vZl9pdGVtcz1mYWN0b3IodGFibGUocmVjY19tYXRyaXgpKQ0KcXBsb3QobnVtYmVyX29mX2l0ZW1zKQ0KDQpudW1iZXJfb2ZfaXRlbXNfc29ydGVkPXNvcnQobnVtYmVyX29mX2l0ZW1zLGRlY3JlYXNpbmcgPSBUKQ0KbnVtYmVyX29mX2l0ZW1zX3RvcD1oZWFkKG51bWJlcl9vZl9pdGVtc19zb3J0ZWQsNCkNCg0KYGBgDQoNCioqKioqDQoNCioq7IKs7Jqp7J6QIOq4sOuwmCoqDQoNCg0K6rCZ7J2AIOyCrOuejOuTpOydtCDqtazrp6TtlZwg6rKD6rO8IOycoOyCrO2VnCDslYTsnbTthZwg7ISg67OEDQrsg4gg7IKs7Jqp7J6Q7JeQ6rKMIOq1rOunpO2WiOuNmCDqsoPqs7wg67mE7Iq37ZWcIOyVhOydtO2FnCDstpTsspwNCg0KDQoNCioq7JWM6rOg66as7KaYKioNCg0K6rCBIOyCrOyaqeyekOyZgCDsg4jroZzsmrQg7IKs7Jqp7J6Q7JmA7J2YIOycoOyCrOuPhCDsuKHsoJUNCuqwgOyepSDsnKDsgqztlZwg7IKs7Jqp7J6Q65Ok7J2EIOyEoOuzhChrIG5uIOyCrOyaqSkNCuqwgOyepSDsnKDsgqztlZwg7IKs7Jqp7J6Q6rCAIOq1rOunpO2VnCDslYTsnbTthZzrk6Tsl5Ag7Y+J7KCQ7J2EIOunpOq4tOuLpC4NCu2PieygkOydgCDsnKDsgqzrj4Trpbwg6rCA7KSR7LmY66GcIO2VmOuKlCDqsIDspJHtj4nsoJDsnbTrgpgg7Y+J6reg7Y+J7KCQ7J2EIOyCrOyaqQ0KDQpubuydgCDsnKDsgqztlZwg7IKs7Jqp7J6Q7J2YIOyImOulvCDsnZjrr7go65SU7Y+07Yq4ID0yNSkNCg0KYGBge3J9DQoNCm1vZGVsMj1SZWNvbW1lbmRlcihkYXRhPXRyYWluLG1ldGhvZD0nVUJDRicpDQptb2RlbDINCm1vZGVsX2RldGFpbHM9Z2V0TW9kZWwobW9kZWwyKQ0Kbl9yZWNvbW1lbmRlZD02DQpyZWNjX3ByZWRpY3RlZD1wcmVkaWN0KG1vZGVsMix0ZXN0LG5fcmVjb21tZW5kZWQpDQoNCmBgYA0KDQoNCuycoOyggOq4sOuwmCDtmJHsl4XtlYTthLDrp4HsnYAg7LSI6riwIOuNsOydtO2EsOyXkCDsoJHqt7ztlbTslbwg7ZWY66+A66GcIOyCrO2bhO2VmeyKteuqqOuNuOyXkCDtlbTri7nrkJzri6QuIOuYkO2VnCDsoITssrQg642w7J207YSw67Kg7J207Iqk7JeQIOumrOyGjOyKpCDshozrqqjqsIAg7Ius7ZWY64ukLiANCg0K7J2867CY7KCB7Jy866GcIFVCQ0bqsIAgSUJDRuuztOuLpCDsooAg642UIOygle2Zle2VmOuLpOqzoCDsnoXspp3rkJjsl4jsnLzrr4DroZwgVUJDRuulvCDstpTsspwuDQoNCioqKioqDQoNCioq7J207KeE642w7J207YSw7JeQIOuMgO2VnCDtmJHsl4XtlYTthLDrp4EqKg0KDQokZGlzdGFuY2UoaXRlbV8xLGl0ZW1fMik9XGZyYWN7aXRlbV8xIFxjYXAgaXRlbV8yfXtpdGVtXzIgXGN1cCBpdGVtXzJ9JA0KDQrsnbTrn7Qg65WMIOyCrOyaqSDqsIDriqUNCuyWtOuWpCDslYTsnbTthZzsnYQg6rWs66ek7ZaI64qU7KeAIOyVjOyngOunjCDtj4nsoJDsnYQg66qo66W464ukLg0K6rCBIOyCrOyaqeyekOyXkCDrjIDtlZwg6rWs66ek7ZWcIOyVhOydtO2FnOydhCDslYzsp4Ag66q77ZWY7KeA66eMIOyii+yVhO2VmOuKlCDslYTsnbTthZzsnYAg7JWM6rOgIOyeiOuLpC4NCg0K7JWE7J207YWcIOq4sOuwmCANCg0KYGBge3J9DQoNCnJhdGluZ3NfbW92aWVzX3dhdGNoZWQ9YmluYXJpemUocmF0aW5nc19tb3ZpZXMsbWluUmF0aW5nPTEpDQpxcGxvdChyb3dTdW1zKHJhdGluZ3NfbW92aWVzX3dhdGNoZWQpKStzdGF0X2JpbihiaW53aWR0aD0xMCkNCg0KbW9kZWw9UmVjb21tZW5kZXIodHJhaW4sbWV0aG9kPSdJQkNGJyxwYXJhbWV0ZXI9bGlzdChtZXRob2Q9J0phY2NhcmQnKSkNCm1vZGVsX2RldGFpbHM9Z2V0TW9kZWwobW9kZWwpDQpuX3JlY29tbWVuZGVkPTYNCnJlY2NfcHJlZGljdGVkPXByZWRpY3QobW9kZWwsdGVzdCxuPW5fcmVjb21tZW5kZWQpDQpyZWNjX21hdHJpeD1zYXBwbHkocmVjY19wcmVkaWN0ZWRAaXRlbXMsZnVuY3Rpb24oeCl7Y29sbmFtZXMocmF0aW5nc19tb3ZpZXMpW3hdfSkNCg0KYGBgDQoNCg0K7IKs7Jqp7J6Q6riw67CYDQoNCmBgYHtyfQ0KDQptb2RlbD1SZWNvbW1lbmRlcih0cmFpbixtZXRob2Q9J1VCQ0YnLHBhcmFtZXRlcj1saXN0KG1ldGhvZD0nSmFjY2FyZCcpKQ0KbW9kZWxfZGV0YWlscz1nZXRNb2RlbChtb2RlbCkNCm5fcmVjb21tZW5kZWQ9Ng0KcmVjY19wcmVkaWN0ZWQ9cHJlZGljdChtb2RlbCx0ZXN0LG49bl9yZWNvbW1lbmRlZCkNCnJlY2NfbWF0cml4PXNhcHBseShyZWNjX3ByZWRpY3RlZEBpdGVtcyxmdW5jdGlvbih4KXtjb2xuYW1lcyhyYXRpbmdzX21vdmllcylbeF19KQ0KDQpgYGANCg0KDQoqKu2YkeyXhSDtlYTthLDrp4HsnZgg66y47KCc7KCQKioNCg0K7IOI66Gc7Jq0IOyCrOyaqeyekOqwgCDslYTsp4Eg7JiB7ZmU66W8IOuztOyngCDrqrvtlojsnLzrqbQg7IKs7JqpIOu2iOqwgOuKpS4NCklCQ0bsnZgg6rK97JqwIOq1rOunpCDslYTsnbTthZzsnYQg7JWM7JWE7JW8IO2VmOupsCwgVUJDRuuKlCDquLDsobQg7IKs7Jqp7J6Q6rCAIOyDiOuhnOyatCDsgqzsmqnsnpDsmYAg7Jyg7IKs7ZWcIOyEoO2YuOuPhOulvCDqsIDsoYzripTsp4Ag7JWM7JWE7JW8IO2VnOuLpC4NCg0K7IOIIOyVhOydtO2FnOydhCDri6TrpbjsgqzrnozsnbQg6rWs66ek7ZWY7KeAIOyViuydgCDqsr3smrDsl5DripQg7KCI64yA66GcIOy2lOyynOuQmOyngCDslYrsnYwuDQoNCuuYkO2VnCDtj4nsoJAg66ek7Yq466at7Iqk66eMIOqzoOugpO2VmOupsCwg7IKs7Jqp7J6Q7J2YIOygleuztOulvCDqs6DroKTtlZjsp4Ag66q77ZWoLg0KDQrsvZjthZDsuKAg6riw67CYIO2VhO2EsOungQ0K7JWE7J207YWc7JeQIOuMgO2VnCDshLjrtoDsoJXrs7TroZwg7Iuc7J6R65CY66mwIOuLpOuluCDsgqzsmqnsnpDrk6TsnYQg6rOg66Ck7ZWgIO2VhOyalOqwgCDsl4bri6QuDQrqsIEg7IKs7Jqp7J6Q7JeQIOuMgO2VtCDslYzqs6DrpqzsppjsnYAg6rO86rGwIOq1rOunpOyZgCDsnKDsgqztlZwg7JWE7J207YWc7J2EIOy2lOyynA0KDQrtlZjsnbTruIzrpqzrk5wg7LaU7LKcIOyLnOyKpO2FnA0KDQrrs5HroKwg7ZWY7J2067iM66as65OcIOyLnOyKpO2FnCA6IOy2lOyynCDrqqjrk4jsnYQg67OR66Cs66GcIOqwgeqwgSDsi6TtlontlZjqs6Ag6re4IOqysOqzvOulvCDqsrDtlantlZzri6QuDQrqsIEg7IKs7Jqp7J6Q7JeQIOuMgO2VnCDstpTsspwg6rKw6rO866W8IOqysO2VqQ0KDQrtjIzsnbTtlITrnbzsnbgg7ZWY7J2067iM66as65OcIOyLnOyKpO2FnCA6IOyEnOuhnCDri6Trpbgg7LaU7LKcIOyLnOyKpO2FnOydhCDssKjrnpjrjIDroZwg7IiY7ZaJDQoNCuuqqOuGgOumrOyLnSDtlZjsnbTruIzrpqzrk5wg7Iuc7Iqk7YWcIDog64+Z7J287ZWcIOy2lOyynCDslYzqs6Drpqzsppgg64K07JeQ7IScIOuLpOyWke2VnCDrsKnsi53snLzroZwg7IiY7ZaJ7ZWcIOqysOqzvOulvCDthrXtlakuDQoNCg==